/* eslint-disable react/no-array-index-key */
import useSWR, { SWRConfig } from "swr";
import Head from "next/head";
import dynamic from "next/dynamic";
import classNames from "classnames";
import { useTranslation } from "next-i18next";
import { useEffect, useContext, useState, useMemo } from "react";
import { BiError } from "react-icons/bi";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { useRouter } from "next/router";
import Tab, { slugify } from "components/tab";
import FileContent from "components/filecontent";
import ServicesGroup from "components/services/group";
import BookmarksGroup from "components/bookmarks/group";
import Widget from "components/widgets/widget";
import Revalidate from "components/toggles/revalidate";
import createLogger from "utils/logger";
import useWindowFocus from "utils/hooks/window-focus";
import { getSettings } from "utils/config/config";
import { ColorContext } from "utils/contexts/color";
import { ThemeContext } from "utils/contexts/theme";
import { SettingsContext } from "utils/contexts/settings";
import { TabContext } from "utils/contexts/tab";
import { bookmarksResponse, servicesResponse, widgetsResponse } from "utils/config/api-response";
import ErrorBoundary from "components/errorboundry";
import themes from "utils/styles/themes";
import QuickLaunch from "components/quicklaunch";
import { getStoredProvider, searchProviders } from "components/widgets/search/search";
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"];
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();
return {
props: {
initialSettings: settings,
fallback: {
"/api/services": services,
"/api/bookmarks": bookmarks,
"/api/widgets": widgets,
"/api/hash": false,
},
...(await serverSideTranslations(settings.language ?? "en")),
},
};
} catch (e) {
if (logger) {
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 { 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 (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-6 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-6 mb-0 sm:m-9 sm:mb-1 border-b-2 pb-4 border-theme-800 dark:border-theme-200/50",
clean: "m-6 mb-0 sm:m-9 sm:mb-0",
boxedWidgets: "m-6 mb-0 sm:m-9 sm:mb-0 sm:mt-1",
};
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 = [
...services.map((sg) => sg.services).flat(),
...bookmarks.map((bg) => bg.bookmarks).flat(),
].filter((i) => i?.href);
useEffect(() => {
if (settings.language) {
i18n.changeLanguage(settings.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("");
let searchProvider = null;
const searchWidget = Object.values(widgets).find((w) => w.type === "search");
if (searchWidget) {
if (Array.isArray(searchWidget.options?.provider)) {
// if search provider is a list, try to retrieve from localstorage, fall back to the first
searchProvider = getStoredProvider() ?? searchProviders[searchWidget.options.provider[0]];
} else if (searchWidget.options?.provider === "custom") {
searchProvider = {
url: searchWidget.options.url,
};
} else {
searchProvider = searchProviders[searchWidget.options?.provider];
}
}
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)/g) && !(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) ||
(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 = decodeURI(asPath.substring(asPath.indexOf("#") + 1));
setActiveTab(initialTab === "/" ? slugify(tabs["0"]) : initialTab);
}
});
const servicesAndBookmarksGroups = useMemo(() => {
const tabGroupFilter = (g) => g && [activeTab, ""].includes(slugify(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.disableCollapse,
settings.cardBlur,
initialSettings.layout,
]);
return (
<>
{settings.title || "Homepage"}
{settings.base && }
{settings.favicon ? (
<>
>
) : (
<>
>
)}
{servicesAndBookmarksGroups}
>
);
}
export default function Wrapper({ initialSettings, fallback }) {
const wrappedStyle = {};
let backgroundBlur = false;
let backgroundSaturate = false;
let backgroundBrightness = false;
if (initialSettings && initialSettings.background) {
let opacity = initialSettings.backgroundOpacity ?? 1;
let backgroundImage = initialSettings.background;
if (typeof initialSettings.background === "object") {
backgroundImage = initialSettings.background.image;
backgroundBlur = initialSettings.background.blur !== undefined;
backgroundSaturate = initialSettings.background.saturate !== undefined;
backgroundBrightness = initialSettings.background.brightness !== undefined;
if (initialSettings.background.opacity !== undefined) opacity = initialSettings.background.opacity / 100;
}
const opacityValue = 1 - opacity;
wrappedStyle.backgroundImage = `
linear-gradient(
rgb(var(--bg-color) / ${opacityValue}),
rgb(var(--bg-color) / ${opacityValue})
),
url('${backgroundImage}')`;
wrappedStyle.backgroundPosition = "center";
wrappedStyle.backgroundSize = "cover";
}
return (
);
}