/* eslint-disable react/no-array-index-key */
import classNames from "classnames";
import BookmarksGroup from "components/bookmarks/group";
import ErrorBoundary from "components/errorboundry";
import QuickLaunch from "components/quicklaunch";
import ServicesGroup from "components/services/group";
import Tab, { slugifyAndEncode } from "components/tab";
import Revalidate from "components/toggles/revalidate";
import Widget from "components/widgets/widget";
import { useTranslation } from "next-i18next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import dynamic from "next/dynamic";
import Head from "next/head";
import { useRouter } from "next/router";
import Script from "next/script";
import { useContext, useEffect, useMemo, useState } from "react";
import { BiError } from "react-icons/bi";
import useSWR, { SWRConfig } from "swr";
import { ColorContext } from "utils/contexts/color";
import { SettingsContext } from "utils/contexts/settings";
import { TabContext } from "utils/contexts/tab";
import { ThemeContext } from "utils/contexts/theme";
import { bookmarksResponse, servicesResponse, widgetsResponse } from "utils/config/api-response";
import { getSettings } from "utils/config/config";
import useWindowFocus from "utils/hooks/window-focus";
import createLogger from "utils/logger";
import themes from "utils/styles/themes";
const ThemeToggle = dynamic(() => import("components/toggles/theme"), {
ssr: false,
});
const ColorToggle = dynamic(() => import("components/toggles/color"), {
ssr: false,
});
const Version = dynamic(() => import("components/version"), {
ssr: false,
});
const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "openmeteo", "search", "datetime"];
// Normalize language codes so older config values like zh-CN still point to Crowdin-provided ones
const LANGUAGE_ALIASES = {
"zh-cn": "zh-Hans",
};
const normalizeLanguage = (language) => {
if (!language) return "en";
const alias = LANGUAGE_ALIASES[language.toLowerCase()];
return alias || language;
};
export async function getStaticProps() {
let logger;
try {
logger = createLogger("index");
const { providers, ...settings } = getSettings();
const services = await servicesResponse();
const bookmarks = await bookmarksResponse();
const widgets = await widgetsResponse();
const language = normalizeLanguage(settings.language);
return {
props: {
initialSettings: settings,
fallback: {
"/api/services": services,
"/api/bookmarks": bookmarks,
"/api/widgets": widgets,
"/api/hash": false,
},
...(await serverSideTranslations(language)),
},
};
} catch (e) {
if (logger && e) {
logger.error(e);
}
return {
props: {
initialSettings: {},
fallback: {
"/api/services": [],
"/api/bookmarks": [],
"/api/widgets": [],
"/api/hash": false,
},
...(await serverSideTranslations("en")),
},
};
}
}
function Index({ initialSettings, fallback }) {
const windowFocused = useWindowFocus();
const [stale, setStale] = useState(false);
const { data: errorsData } = useSWR("/api/validate");
const { error: validateError } = errorsData || {};
const { data: hashData, mutate: mutateHash } = useSWR("/api/hash");
useEffect(() => {
if (windowFocused) {
mutateHash();
}
}, [windowFocused, mutateHash]);
useEffect(() => {
if (hashData) {
if (typeof window !== "undefined") {
const previousHash = localStorage.getItem("hash");
if (!previousHash) {
localStorage.setItem("hash", hashData.hash);
}
if (previousHash && previousHash !== hashData.hash) {
setStale(true);
localStorage.setItem("hash", hashData.hash);
fetch("/api/revalidate").then((res) => {
if (res.ok) {
window.location.reload();
}
});
}
}
}
}, [hashData]);
if (validateError) {
return (
);
}
if (stale) {
return (
);
}
if (errorsData && errorsData.length > 0) {
return (
{errorsData.map((error, i) => (
{error.config}
{error.reason}
{error.mark.snippet}
))}
);
}
return (
fetch(resource, init).then((res) => res.json()) }}>
);
}
const headerStyles = {
boxed:
"m-5 mb-0 sm:m-9 sm:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
underlined: "m-5 mb-0 sm:m-9 sm:mb-1 border-b-2 pb-4 border-theme-800 dark:border-theme-200/50",
clean: "m-5 mb-0 sm:m-9 sm:mb-0",
boxedWidgets: "m-5 mb-0 sm:m-9 sm:mb-0 sm:mt-1",
};
function getAllServices(services) {
function getServices(group) {
let nestedServices = [...group.services];
if (group.groups.length > 0) {
nestedServices = [...nestedServices, ...group.groups.map(getServices).flat()];
}
return nestedServices;
}
return [...services.map(getServices).flat()];
}
function Home({ initialSettings }) {
const { i18n } = useTranslation();
const { theme, setTheme } = useContext(ThemeContext);
const { color, setColor } = useContext(ColorContext);
const { settings, setSettings } = useContext(SettingsContext);
const { activeTab, setActiveTab } = useContext(TabContext);
const { asPath } = useRouter();
useEffect(() => {
setSettings(initialSettings);
}, [initialSettings, setSettings]);
const { data: services } = useSWR("/api/services");
const { data: bookmarks } = useSWR("/api/bookmarks");
const { data: widgets } = useSWR("/api/widgets");
const servicesAndBookmarks = [...bookmarks.map((bg) => bg.bookmarks).flat(), ...getAllServices(services)].filter(
(i) => i?.href,
);
useEffect(() => {
const language = normalizeLanguage(settings.language);
if (language) {
i18n.changeLanguage(language);
}
if (settings.theme && theme !== settings.theme) {
setTheme(settings.theme);
}
if (settings.color && color !== settings.color) {
setColor(settings.color);
}
}, [i18n, settings, color, setColor, theme, setTheme]);
const [searching, setSearching] = useState(false);
const [searchString, setSearchString] = useState("");
const headerStyle = settings?.headerStyle || "underlined";
useEffect(() => {
function handleKeyDown(e) {
if (e.target.tagName === "BODY" || e.target.id === "inner_wrapper") {
if (
(e.key.length === 1 &&
e.key.match(/(\w|\s|[à-ü]|[À-Ü]|[\w\u0430-\u044f])/gi) &&
!(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) ||
// accented characters and the bang may require modifier keys
e.key.match(/([à-ü]|[À-Ü]|!)/g) ||
(e.key === "v" && (e.ctrlKey || e.metaKey))
) {
setSearching(true);
} else if (e.key === "Escape") {
setSearchString("");
setSearching(false);
}
}
}
document.addEventListener("keydown", handleKeyDown);
return function cleanup() {
document.removeEventListener("keydown", handleKeyDown);
};
});
const tabs = useMemo(
() => [
...new Set(
Object.keys(settings.layout ?? {})
.map((groupName) => settings.layout[groupName]?.tab?.toString())
.filter((group) => group),
),
],
[settings.layout],
);
useEffect(() => {
if (!activeTab) {
const initialTab = asPath.substring(asPath.indexOf("#") + 1);
setActiveTab(initialTab === "/" ? slugifyAndEncode(tabs["0"]) : initialTab);
}
});
const servicesAndBookmarksGroups = useMemo(() => {
const tabGroupFilter = (g) => g && [activeTab, ""].includes(slugifyAndEncode(settings.layout?.[g.name]?.tab));
const undefinedGroupFilter = (g) => settings.layout?.[g.name] === undefined;
const layoutGroups = Object.keys(settings.layout ?? {})
.map((groupName) => services?.find((g) => g.name === groupName) ?? bookmarks?.find((b) => b.name === groupName))
.filter(tabGroupFilter);
if (!settings.layout && JSON.stringify(settings.layout) !== JSON.stringify(initialSettings.layout)) {
// wait for settings to populate (if different from initial settings), otherwise all the widgets will be requested initially even if we are on a single tab
return ;
}
const serviceGroups = services?.filter(tabGroupFilter).filter(undefinedGroupFilter);
const bookmarkGroups = bookmarks.filter(tabGroupFilter).filter(undefinedGroupFilter);
return (
<>
{tabs.length > 0 && (
)}
{layoutGroups.length > 0 && (
{layoutGroups.map((group) =>
group.services ? (
) : (
),
)}
)}
{serviceGroups?.length > 0 && (
{serviceGroups.map((group) => (
))}
)}
{bookmarkGroups?.length > 0 && (
{bookmarkGroups.map((group) => (
))}
)}
>
);
}, [
tabs,
activeTab,
services,
bookmarks,
settings.layout,
settings.fiveColumns,
settings.maxGroupColumns,
settings.maxBookmarkGroupColumns,
settings.disableCollapse,
settings.useEqualHeights,
settings.cardBlur,
settings.groupsInitiallyCollapsed,
settings.bookmarksStyle,
initialSettings.layout,
]);
return (
<>
{initialSettings.title || "Homepage"}
{settings.disableIndexing && }
{settings.base && }
{settings.favicon ? (
<>
>
) : (
<>
>
)}
{servicesAndBookmarksGroups}
>
);
}
export default function Wrapper({ initialSettings, fallback }) {
const { theme } = useContext(ThemeContext);
const { color } = useContext(ColorContext);
let backgroundImage = "";
let opacity = initialSettings?.backgroundOpacity ?? 0;
let backgroundBlur = false;
let backgroundSaturate = false;
let backgroundBrightness = false;
if (initialSettings?.background) {
const bg = initialSettings.background;
if (typeof bg === "object") {
backgroundImage = bg.image || "";
if (bg.opacity !== undefined) {
opacity = 1 - bg.opacity / 100;
}
backgroundBlur = bg.blur !== undefined;
backgroundSaturate = bg.saturate !== undefined;
backgroundBrightness = bg.brightness !== undefined;
} else {
backgroundImage = bg;
}
}
useEffect(() => {
const html = document.documentElement;
const body = document.body;
html.classList.remove("dark", "scheme-dark", "scheme-light");
html.classList.toggle("dark", theme === "dark");
html.classList.add(theme === "dark" ? "scheme-dark" : "scheme-light");
const desiredThemeClass = `theme-${color || initialSettings.color || "slate"}`;
const themeClassesToRemove = Array.from(html.classList).filter(
(cls) => cls.startsWith("theme-") && cls !== desiredThemeClass,
);
if (themeClassesToRemove.length) {
html.classList.remove(...themeClassesToRemove);
}
if (!html.classList.contains(desiredThemeClass)) {
html.classList.add(desiredThemeClass);
}
if (backgroundImage) {
const safeBackgroundImage = backgroundImage.replace(/'/g, "\\'");
body.style.backgroundImage = `linear-gradient(rgb(var(--bg-color) / ${opacity}), rgb(var(--bg-color) / ${opacity})), url('${safeBackgroundImage}')`;
body.style.backgroundSize = "cover";
body.style.backgroundPosition = "center";
body.style.backgroundAttachment = "fixed";
body.style.backgroundRepeat = "no-repeat";
body.style.backgroundColor = "";
} else {
body.style.backgroundImage = "none";
body.style.backgroundColor = "rgb(var(--bg-color))";
body.style.backgroundSize = "";
body.style.backgroundPosition = "";
body.style.backgroundAttachment = "";
body.style.backgroundRepeat = "";
}
return () => {
body.style.backgroundImage = "";
body.style.backgroundColor = "";
body.style.backgroundSize = "";
body.style.backgroundPosition = "";
body.style.backgroundAttachment = "";
body.style.backgroundRepeat = "";
};
}, [backgroundImage, opacity, theme, color, initialSettings.color]);
return (
);
}