mirror of
https://github.com/gethomepage/homepage.git
synced 2025-12-06 21:57:48 +01:00
Merge branch 'main' into feature/widget_strelaysrv
This commit is contained in:
@@ -2,13 +2,8 @@ import List from "components/bookmarks/list";
|
||||
|
||||
export default function BookmarksGroup({ group }) {
|
||||
return (
|
||||
<div
|
||||
key={group.name}
|
||||
className="basis-full md:basis-1/2 lg:basis-1/3 xl:basis-1/4 flex-1 p-1"
|
||||
>
|
||||
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">
|
||||
{group.name}
|
||||
</h2>
|
||||
<div key={group.name} className="basis-full md:basis-1/2 lg:basis-1/3 xl:basis-1/4 flex-1 p-1">
|
||||
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">{group.name}</h2>
|
||||
<List bookmarks={group.bookmarks} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { useContext } from "react";
|
||||
|
||||
import { SettingsContext } from "utils/settings-context";
|
||||
|
||||
export default function Item({ bookmark }) {
|
||||
const { hostname } = new URL(bookmark.href);
|
||||
const { settings } = useContext(SettingsContext);
|
||||
|
||||
return (
|
||||
<li key={bookmark.name}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.open(bookmark.href, "_blank").focus()}
|
||||
className="w-full text-left mb-3 cursor-pointer rounded-md font-medium text-theme-700 hover:text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-black/10 dark:shadow-black/20 bg-white/50 hover:bg-theme-300/10 dark:bg-white/10 dark:hover:bg-white/20"
|
||||
<a
|
||||
href={bookmark.href}
|
||||
title={bookmark.name}
|
||||
target={settings.target ?? "_blank"}
|
||||
className="block w-full text-left mb-3 cursor-pointer rounded-md font-medium text-theme-700 hover:text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-black/10 dark:shadow-black/20 bg-white/50 hover:bg-theme-300/10 dark:bg-white/10 dark:hover:bg-white/20"
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0 flex items-center justify-center w-11 bg-theme-500/10 dark:bg-theme-900/50 text-theme-700 hover:text-theme-700 dark:text-theme-200 text-sm font-medium rounded-l-md">
|
||||
@@ -17,7 +23,7 @@ export default function Item({ bookmark }) {
|
||||
<div className="px-2 py-2 truncate text-theme-500 dark:text-theme-400 opacity-50 text-xs">{hostname}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import Image from "next/future/image";
|
||||
import { useContext } from "react";
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
|
||||
import Status from "./status";
|
||||
import Widget from "./widget";
|
||||
import Docker from "./widgets/service/docker";
|
||||
|
||||
import { SettingsContext } from "utils/settings-context";
|
||||
|
||||
function resolveIcon(icon) {
|
||||
if (icon.startsWith("http")) {
|
||||
return `/api/proxy?url=${encodeURIComponent(icon)}`;
|
||||
@@ -23,6 +26,7 @@ function resolveIcon(icon) {
|
||||
|
||||
export default function Item({ service }) {
|
||||
const hasLink = service.href && service.href !== "#";
|
||||
const { settings } = useContext(SettingsContext);
|
||||
|
||||
return (
|
||||
<li key={service.name}>
|
||||
@@ -37,7 +41,7 @@ export default function Item({ service }) {
|
||||
(hasLink ? (
|
||||
<a
|
||||
href={service.href}
|
||||
target="_blank"
|
||||
target={settings.target ?? "_blank"}
|
||||
rel="noreferrer"
|
||||
className="flex-shrink-0 flex items-center justify-center w-12 "
|
||||
>
|
||||
@@ -52,7 +56,7 @@ export default function Item({ service }) {
|
||||
{hasLink ? (
|
||||
<a
|
||||
href={service.href}
|
||||
target="_blank"
|
||||
target={settings.target ?? "_blank"}
|
||||
rel="noreferrer"
|
||||
className="flex-1 flex items-center justify-between rounded-r-md "
|
||||
>
|
||||
|
||||
@@ -28,6 +28,7 @@ import Prowlarr from "./widgets/service/prowlarr";
|
||||
import Jackett from "./widgets/service/jackett";
|
||||
import AdGuard from "./widgets/service/adguard";
|
||||
import StRelaySrv from "./widgets/service/strelaysrv";
|
||||
import Mastodon from "./widgets/service/mastodon";
|
||||
|
||||
const widgetMappings = {
|
||||
docker: Docker,
|
||||
@@ -58,6 +59,7 @@ const widgetMappings = {
|
||||
jackett: Jackett,
|
||||
adguard: AdGuard,
|
||||
strelaysrv: StRelaySrv,
|
||||
mastodon: Mastodon,
|
||||
};
|
||||
|
||||
export default function Widget({ service }) {
|
||||
|
||||
37
src/components/services/widgets/service/mastodon.jsx
Normal file
37
src/components/services/widgets/service/mastodon.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Mastodon({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `instance`));
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("mastodon.user_count")} />
|
||||
<Block label={t("mastodon.status_count")} />
|
||||
<Block label={t("mastodon.domain_count")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("mastodon.user_count")} value={t("common.number", { value: statsData.stats.user_count })} />
|
||||
<Block label={t("mastodon.status_count")} value={t("common.number", { value: statsData.stats.status_count })} />
|
||||
<Block label={t("mastodon.domain_count")} value={t("common.number", { value: statsData.stats.domain_count })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import "styles/theme.css";
|
||||
import "utils/i18n";
|
||||
import { ColorProvider } from "utils/color-context";
|
||||
import { ThemeProvider } from "utils/theme-context";
|
||||
import { SettingsProvider } from "utils/settings-context";
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
@@ -18,7 +19,9 @@ function MyApp({ Component, pageProps }) {
|
||||
>
|
||||
<ColorProvider>
|
||||
<ThemeProvider>
|
||||
<Component {...pageProps} />
|
||||
<SettingsProvider>
|
||||
<Component {...pageProps} />
|
||||
</SettingsProvider>
|
||||
</ThemeProvider>
|
||||
</ColorProvider>
|
||||
</SWRConfig>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logger from "utils/logger";
|
||||
import genericProxyHandler from "utils/proxies/generic";
|
||||
import credentialedProxyHandler from "utils/proxies/credentialed";
|
||||
import rutorrentProxyHandler from "utils/proxies/rutorrent";
|
||||
@@ -82,6 +83,7 @@ const serviceProxyHandlers = {
|
||||
jackett: genericProxyHandler,
|
||||
adguard: genericProxyHandler,
|
||||
strelaysrv: genericProxyHandler,
|
||||
mastodon: genericProxyHandler,
|
||||
// uses X-API-Key (or similar) header auth
|
||||
gotify: credentialedProxyHandler,
|
||||
portainer: credentialedProxyHandler,
|
||||
@@ -99,20 +101,27 @@ const serviceProxyHandlers = {
|
||||
};
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { type } = req.query;
|
||||
try {
|
||||
const { type } = req.query;
|
||||
|
||||
const serviceProxyHandler = serviceProxyHandlers[type];
|
||||
const serviceProxyHandler = serviceProxyHandlers[type];
|
||||
|
||||
if (serviceProxyHandler) {
|
||||
if (serviceProxyHandler instanceof Function) {
|
||||
return serviceProxyHandler(req, res);
|
||||
if (serviceProxyHandler) {
|
||||
if (serviceProxyHandler instanceof Function) {
|
||||
return serviceProxyHandler(req, res);
|
||||
}
|
||||
|
||||
const { proxy, maps } = serviceProxyHandler;
|
||||
if (proxy) {
|
||||
return proxy(req, res, maps);
|
||||
}
|
||||
}
|
||||
|
||||
const { proxy, maps } = serviceProxyHandler;
|
||||
if (proxy) {
|
||||
return proxy(req, res, maps);
|
||||
}
|
||||
logger.debug("Unknown proxy service type: %s", type);
|
||||
return res.status(403).json({ error: "Unkown proxy service type" });
|
||||
}
|
||||
catch (ex) {
|
||||
logger.error(ex);
|
||||
return res.status(500).send({ error: "Unexpected error" });
|
||||
}
|
||||
|
||||
return res.status(403).json({ error: "Unkown proxy service type" });
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import Revalidate from "components/revalidate";
|
||||
import { getSettings } from "utils/config";
|
||||
import { ColorContext } from "utils/color-context";
|
||||
import { ThemeContext } from "utils/theme-context";
|
||||
import { SettingsContext } from "utils/settings-context";
|
||||
|
||||
const ThemeToggle = dynamic(() => import("components/theme-toggle"), {
|
||||
ssr: false,
|
||||
@@ -26,22 +27,23 @@ const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "search"
|
||||
|
||||
export function getStaticProps() {
|
||||
try {
|
||||
const settings = getSettings();
|
||||
const { providers, ...settings } = getSettings();
|
||||
|
||||
return {
|
||||
props: {
|
||||
settings,
|
||||
initialSettings: settings,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
props: {
|
||||
settings: {},
|
||||
initialSettings: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function Index({ settings }) {
|
||||
export default function Index({ initialSettings }) {
|
||||
const { data: errorsData } = useSWR("/api/validate");
|
||||
|
||||
if (errorsData && errorsData.length > 0) {
|
||||
@@ -68,20 +70,25 @@ export default function Index({ settings }) {
|
||||
);
|
||||
}
|
||||
|
||||
return <Home settings={settings} />;
|
||||
return <Home initialSettings={initialSettings} />;
|
||||
}
|
||||
|
||||
function Home({ settings }) {
|
||||
function Home({ initialSettings }) {
|
||||
const { i18n } = useTranslation();
|
||||
const { theme, setTheme } = useContext(ThemeContext);
|
||||
const { color, setColor } = useContext(ColorContext);
|
||||
const { settings, setSettings } = useContext(SettingsContext);
|
||||
|
||||
if (initialSettings) {
|
||||
setSettings(initialSettings);
|
||||
}
|
||||
|
||||
const { data: services } = useSWR("/api/services");
|
||||
const { data: bookmarks } = useSWR("/api/bookmarks");
|
||||
const { data: widgets } = useSWR("/api/widgets");
|
||||
|
||||
const wrappedStyle = {};
|
||||
if (settings.background) {
|
||||
if (settings && settings.background) {
|
||||
wrappedStyle.backgroundImage = `url(${settings.background})`;
|
||||
wrappedStyle.backgroundSize = "cover";
|
||||
wrappedStyle.opacity = settings.backgroundOpacity ?? 1;
|
||||
|
||||
@@ -25,6 +25,7 @@ const formats = {
|
||||
jackett: `{url}/api/v2.0/{endpoint}?apikey={key}&configured=true`,
|
||||
adguard: `{url}/control/{endpoint}`,
|
||||
strelaysrv: `{url}/{endpoint}`,
|
||||
mastodon: `{url}/api/v1/{endpoint}`,
|
||||
};
|
||||
|
||||
export function formatApiCall(api, args) {
|
||||
|
||||
76
src/utils/logger.js
Normal file
76
src/utils/logger.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import winston from "winston";
|
||||
|
||||
function messageFormatter(logInfo) {
|
||||
if (logInfo.stack) {
|
||||
return `[${logInfo.timestamp}] ${logInfo.level}: ${logInfo.stack}`;
|
||||
}
|
||||
return `[${logInfo.timestamp}] ${logInfo.level}: ${logInfo.message}`;
|
||||
};
|
||||
|
||||
const consoleFormat = winston.format.combine(
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.splat(),
|
||||
winston.format.timestamp(),
|
||||
winston.format.colorize(),
|
||||
winston.format.printf(messageFormatter)
|
||||
);
|
||||
|
||||
const fileFormat = winston.format.combine(
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.splat(),
|
||||
winston.format.timestamp(),
|
||||
winston.format.printf(messageFormatter)
|
||||
);
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: consoleFormat,
|
||||
handleExceptions: true,
|
||||
handleRejections: true
|
||||
}),
|
||||
|
||||
new winston.transports.File({
|
||||
format: fileFormat,
|
||||
filename: 'homepage.log',
|
||||
handleExceptions: true,
|
||||
handleRejections: true
|
||||
}),
|
||||
]
|
||||
});
|
||||
|
||||
function debug(message, ...args) {
|
||||
logger.debug(message, ...args);
|
||||
}
|
||||
|
||||
function verbose(message, ...args) {
|
||||
logger.verbose(message, ...args);
|
||||
}
|
||||
|
||||
function info(message, ...args) {
|
||||
logger.info(message, ...args);
|
||||
}
|
||||
|
||||
function warn(message, ...args) {
|
||||
logger.warn(message, ...args);
|
||||
}
|
||||
|
||||
function error(message, ...args) {
|
||||
logger.error(message, ...args);
|
||||
}
|
||||
|
||||
function crit(message, ...args) {
|
||||
logger.crit(message, ...args);
|
||||
}
|
||||
|
||||
const thisModule = {
|
||||
debug,
|
||||
verbose,
|
||||
info,
|
||||
warn,
|
||||
error,
|
||||
crit
|
||||
};
|
||||
|
||||
export default thisModule;
|
||||
@@ -1,6 +1,7 @@
|
||||
import getServiceWidget from "utils/service-helpers";
|
||||
import { formatApiCall } from "utils/api-helpers";
|
||||
import { httpProxy } from "utils/http";
|
||||
import logger from "utils/logger";
|
||||
|
||||
export default async function genericProxyHandler(req, res, maps) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
@@ -24,7 +25,7 @@ export default async function genericProxyHandler(req, res, maps) {
|
||||
});
|
||||
|
||||
let resultData = data;
|
||||
if (maps?.[endpoint]) {
|
||||
if ((status === 200) && (maps?.[endpoint])) {
|
||||
resultData = maps[endpoint](data);
|
||||
}
|
||||
|
||||
@@ -34,9 +35,14 @@ export default async function genericProxyHandler(req, res, maps) {
|
||||
return res.status(status).end();
|
||||
}
|
||||
|
||||
if (status >= 400) {
|
||||
logger.debug("HTTP Error %d calling %s//%s%s...", status, url.protocol, url.hostname, url.pathname);
|
||||
}
|
||||
|
||||
return res.status(status).send(resultData);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Invalid or missing proxy service type '%s' in group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
15
src/utils/settings-context.jsx
Normal file
15
src/utils/settings-context.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createContext, useState, useMemo } from "react";
|
||||
|
||||
export const SettingsContext = createContext();
|
||||
|
||||
export function SettingsProvider({ initialSettings, children }) {
|
||||
const [settings, setSettings] = useState({});
|
||||
|
||||
if (initialSettings) {
|
||||
setSettings(initialSettings);
|
||||
}
|
||||
|
||||
const value = useMemo(() => ({ settings, setSettings }), [settings]);
|
||||
|
||||
return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;
|
||||
}
|
||||
Reference in New Issue
Block a user