From 8a25f25e3c87c1b79b41c44efe47c54e0c6d9810 Mon Sep 17 00:00:00 2001 From: Jonny McCullagh Date: Fri, 15 Nov 2024 14:15:58 +0000 Subject: [PATCH] add favicons component --- src/components/favicons/group.jsx | 77 +++++++++++++++++++++++++++++++ src/components/favicons/item.jsx | 33 +++++++++++++ src/components/favicons/list.jsx | 17 +++++++ src/pages/api/favicons.js | 5 ++ src/pages/api/hash.js | 1 + src/pages/api/validate.js | 2 +- src/pages/index.jsx | 38 +++++++++++++-- src/styles/globals.css | 6 +++ src/utils/config/api-response.js | 48 +++++++++++++++++++ 9 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 src/components/favicons/group.jsx create mode 100644 src/components/favicons/item.jsx create mode 100644 src/components/favicons/list.jsx create mode 100644 src/pages/api/favicons.js diff --git a/src/components/favicons/group.jsx b/src/components/favicons/group.jsx new file mode 100644 index 00000000..2b4bb5e4 --- /dev/null +++ b/src/components/favicons/group.jsx @@ -0,0 +1,77 @@ +import { useRef, useEffect } from "react"; +import classNames from "classnames"; +import { Disclosure, Transition } from "@headlessui/react"; +import { MdKeyboardArrowDown } from "react-icons/md"; + +import ErrorBoundary from "components/errorboundry"; +import List from "components/favicons/list"; +import ResolvedIcon from "components/resolvedicon"; + +export default function FaviconsGroup({ favicons, layout, disableCollapse, groupsInitiallyCollapsed }) { + const panel = useRef(); + console.log('+++++') + console.log(favicons) + useEffect(() => { + if (layout?.initiallyCollapsed ?? groupsInitiallyCollapsed) panel.current.style.height = `0`; + }, [layout, groupsInitiallyCollapsed]); + + return ( +
+ + {({ open }) => ( + <> + {layout?.header !== false && ( + + {layout?.icon && ( +
+ +
+ )} +

+ {favicons.name} +

+ +
+ )} + { + panel.current.style.height = `${panel.current.scrollHeight}px`; + setTimeout(() => { + panel.current.style.height = `0`; + }, 1); + }} + beforeEnter={() => { + panel.current.style.height = `0px`; + setTimeout(() => { + panel.current.style.height = `${panel.current.scrollHeight}px`; + }, 1); + }} + > + + + + + + + + )} +
+
+ ); +} diff --git a/src/components/favicons/item.jsx b/src/components/favicons/item.jsx new file mode 100644 index 00000000..77aa806f --- /dev/null +++ b/src/components/favicons/item.jsx @@ -0,0 +1,33 @@ +import { useContext } from "react"; +import classNames from "classnames"; + +import { SettingsContext } from "utils/contexts/settings"; +import ResolvedIcon from "components/resolvedicon"; + +export default function Item({ favicon }) { + const description = favicon.description ?? new URL(favicon.href).hostname; + const { settings } = useContext(SettingsContext); + + return ( +
  • + +
    + {favicon.icon && ( + + )} + {!favicon.icon && favicon.abbr} + +
    +
    +
  • + ); +} diff --git a/src/components/favicons/list.jsx b/src/components/favicons/list.jsx new file mode 100644 index 00000000..021eda51 --- /dev/null +++ b/src/components/favicons/list.jsx @@ -0,0 +1,17 @@ +import classNames from "classnames"; + +import { columnMap } from "../../utils/layout/columns"; + +import Item from "components/favicons/item"; + +export default function List({ favicons, layout }) { + return ( + + ); +} diff --git a/src/pages/api/favicons.js b/src/pages/api/favicons.js new file mode 100644 index 00000000..8ca752b2 --- /dev/null +++ b/src/pages/api/favicons.js @@ -0,0 +1,5 @@ +import { faviconsResponse } from "utils/config/api-response"; + +export default async function handler(req, res) { + res.send(await faviconsResponse()); +} diff --git a/src/pages/api/hash.js b/src/pages/api/hash.js index 992f9ea6..3a48a7a7 100644 --- a/src/pages/api/hash.js +++ b/src/pages/api/hash.js @@ -9,6 +9,7 @@ const configs = [ "settings.yaml", "services.yaml", "bookmarks.yaml", + "favicons.yaml", "widgets.yaml", "custom.css", "custom.js", diff --git a/src/pages/api/validate.js b/src/pages/api/validate.js index bab53057..ae0526cf 100644 --- a/src/pages/api/validate.js +++ b/src/pages/api/validate.js @@ -1,6 +1,6 @@ import checkAndCopyConfig from "utils/config/config"; -const configs = ["docker.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml"]; +const configs = ["docker.yaml", "favicons.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml"]; export default async function handler(req, res) { const errors = configs.map((config) => checkAndCopyConfig(config)).filter((status) => status !== true); diff --git a/src/pages/index.jsx b/src/pages/index.jsx index dd0df95f..4b019bdc 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -13,6 +13,7 @@ import { useRouter } from "next/router"; import Tab, { slugifyAndEncode } from "components/tab"; import ServicesGroup from "components/services/group"; import BookmarksGroup from "components/bookmarks/group"; +import FaviconsGroup from "components/favicons/group"; import Widget from "components/widgets/widget"; import Revalidate from "components/toggles/revalidate"; import createLogger from "utils/logger"; @@ -22,7 +23,7 @@ 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 { bookmarksResponse, faviconsResponse, servicesResponse, widgetsResponse } from "utils/config/api-response"; import ErrorBoundary from "components/errorboundry"; import themes from "utils/styles/themes"; import QuickLaunch from "components/quicklaunch"; @@ -49,12 +50,14 @@ export async function getStaticProps() { const services = await servicesResponse(); const bookmarks = await bookmarksResponse(); + const favicons = await faviconsResponse(); const widgets = await widgetsResponse(); return { props: { initialSettings: settings, fallback: { + "/api/favicons": favicons, "/api/services": services, "/api/bookmarks": bookmarks, "/api/widgets": widgets, @@ -71,6 +74,7 @@ export async function getStaticProps() { props: { initialSettings: {}, fallback: { + "/api/favicons": [], "/api/services": [], "/api/bookmarks": [], "/api/widgets": [], @@ -180,11 +184,13 @@ function Home({ initialSettings }) { const { data: services } = useSWR("/api/services"); const { data: bookmarks } = useSWR("/api/bookmarks"); + const { data: favicons } = useSWR("/api/favicons"); const { data: widgets } = useSWR("/api/widgets"); const servicesAndBookmarks = [ ...services.map((sg) => sg.services).flat(), ...bookmarks.map((bg) => bg.bookmarks).flat(), + ...favicons.map((fg) => fg.favicons).flat(), ].filter((i) => i?.href); useEffect(() => { @@ -254,7 +260,7 @@ function Home({ initialSettings }) { 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)) + .map((groupName) => services?.find((g) => g.name === groupName) ?? favicons?.find((f) => f.name === groupName) ?? bookmarks?.find((b) => b.name === groupName) ) .filter(tabGroupFilter); if (!settings.layout && JSON.stringify(settings.layout) !== JSON.stringify(initialSettings.layout)) { @@ -264,7 +270,10 @@ function Home({ initialSettings }) { const serviceGroups = services?.filter(tabGroupFilter).filter(undefinedGroupFilter); const bookmarkGroups = bookmarks.filter(tabGroupFilter).filter(undefinedGroupFilter); - + const faviconGroups = favicons.filter(tabGroupFilter).filter(undefinedGroupFilter); + console.log(bookmarkGroups) + console.log('------') + console.log(faviconGroups) return ( <> {tabs.length > 0 && ( @@ -300,6 +309,7 @@ function Home({ initialSettings }) { groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed} /> ) : ( + <> + + ), )} @@ -339,6 +357,19 @@ function Home({ initialSettings }) { ))} )} + {faviconGroups?.length > 0 && ( +
    + {faviconGroups.map((group) => ( + + ))} +
    + )} ); }, [ @@ -346,6 +377,7 @@ function Home({ initialSettings }) { activeTab, services, bookmarks, + favicons, settings.layout, settings.fiveColumns, settings.disableCollapse, diff --git a/src/styles/globals.css b/src/styles/globals.css index f3bfec78..034594f7 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -25,6 +25,12 @@ body { padding: 0; } +.favicon { + display: inline-block; + margin-right: 0.5em; +} + + .light { --bg-color: var(--color-50); --scrollbar-thumb: rgb(var(--color-300)); diff --git a/src/utils/config/api-response.js b/src/utils/config/api-response.js index 97f61ea0..8eb3c1a4 100644 --- a/src/utils/config/api-response.js +++ b/src/utils/config/api-response.js @@ -24,6 +24,54 @@ function compareServices(service1, service2) { return service1.name.localeCompare(service2.name); } + +export async function faviconsResponse() { + checkAndCopyConfig("favicons.yaml"); + + const faviconsYaml = path.join(CONF_DIR, "favicons.yaml"); + const rawFileContents = await fs.readFile(faviconsYaml, "utf8"); + const fileContents = substituteEnvironmentVars(rawFileContents); + const favicons = yaml.load(fileContents); + + if (!favicons) return []; + + let initialSettings; + + try { + initialSettings = await getSettings(); + } catch (e) { + console.error("Failed to load favicons.yaml, please check for errors"); + if (e) console.error(e.toString()); + initialSettings = {}; + } + + // map easy to write YAML objects into easy to consume JS arrays + const faviconsArray = favicons.map((group) => ({ + name: Object.keys(group)[0], + bookmarks: group[Object.keys(group)[0]].map((entries) => ({ + name: Object.keys(entries)[0], + ...entries[Object.keys(entries)[0]][0], + })), + })); + + const sortedGroups = []; + const unsortedGroups = []; + const definedLayouts = initialSettings.layout ? Object.keys(initialSettings.layout) : null; + + faviconsArray.forEach((group) => { + if (definedLayouts) { + const layoutIndex = definedLayouts.findIndex((layout) => layout === group.name); + if (layoutIndex > -1) sortedGroups[layoutIndex] = group; + else unsortedGroups.push(group); + } else { + unsortedGroups.push(group); + } + }); + + return [...sortedGroups.filter((g) => g), ...unsortedGroups]; +} + + export async function bookmarksResponse() { checkAndCopyConfig("bookmarks.yaml");