/* 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 (
< div className = "flex items-center justify-center h-screen" >
< div className = "w-24 h-24 border-2 border-theme-400 border-solid rounded-full animate-spin border-t-transparent" / >
< / div >
) ;
}
if ( errorsData && errorsData . length > 0 ) {
return (
< div className = "w-full h-screen container m-auto justify-center p-10 pointer-events-none" >
< div className = "flex flex-col" >
{ errorsData . map ( ( error , i ) => (
< div
className = "basis-1/2 bg-theme-500 dark:bg-theme-600 text-theme-600 dark:text-theme-300 m-2 rounded-md font-mono shadow-md border-4 border-transparent"
key = { i }
>
< div className = "bg-amber-200 text-amber-800 dark:text-amber-200 dark:bg-amber-800 p-2 rounded-md font-bold" >
< BiError className = "float-right w-6 h-6" / >
{ error . config }
< / div >
< div className = "p-2 text-theme-100 dark:text-theme-200" >
< pre className = "opacity-50 font-bold pb-2" > { error . reason } < / pre >
< pre className = "text-sm" > { error . mark . snippet } < / pre >
< / div >
< / div >
) ) }
< / div >
< / div >
) ;
}
return (
< SWRConfig value = { { fallback , fetcher : ( resource , init ) => fetch ( resource , init ) . then ( ( res ) => res . json ( ) ) } } >
< ErrorBoundary >
< Home initialSettings = { initialSettings } / >
< / ErrorBoundary >
< / SWRConfig >
) ;
}
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 < div / > ;
}
const serviceGroups = services ? . filter ( tabGroupFilter ) . filter ( undefinedGroupFilter ) ;
const bookmarkGroups = bookmarks . filter ( tabGroupFilter ) . filter ( undefinedGroupFilter ) ;
return (
< >
{ tabs . length > 0 && (
< div key = "tabs" id = "tabs" className = "m-6 sm:m-9 sm:mt-4 sm:mb-0" >
< ul
className = { classNames (
"sm:flex rounded-md bg-theme-100/20 dark:bg-white/5" ,
settings . cardBlur !== undefined &&
` backdrop-blur ${ settings . cardBlur . length ? "-" : "" } ${ settings . cardBlur } ` ,
) }
id = "myTab"
data - tabs - toggle = "#myTabContent"
role = "tablist"
>
{ tabs . map ( ( tab ) => (
< Tab key = { tab } tab = { tab } / >
) ) }
< / ul >
< / div >
) }
{ layoutGroups . length > 0 && (
< div key = "layoutGroups" id = "layout-groups" className = "flex flex-wrap m-4 sm:m-8 sm:mt-4 items-start mb-2" >
{ layoutGroups . map ( ( group ) =>
group . services ? (
< ServicesGroup
key = { group . name }
group = { group . name }
services = { group }
layout = { settings . layout ? . [ group . name ] }
fiveColumns = { settings . fiveColumns }
disableCollapse = { settings . disableCollapse }
useEqualHeights = { settings . useEqualHeights }
/ >
) : (
< BookmarksGroup
key = { group . name }
bookmarks = { group }
layout = { settings . layout ? . [ group . name ] }
disableCollapse = { settings . disableCollapse }
/ >
) ,
) }
< / div >
) }
{ serviceGroups ? . length > 0 && (
< div key = "services" id = "services" className = "flex flex-wrap m-4 sm:m-8 sm:mt-4 items-start mb-2" >
{ serviceGroups . map ( ( group ) => (
< ServicesGroup
key = { group . name }
group = { group . name }
services = { group }
layout = { settings . layout ? . [ group . name ] }
fiveColumns = { settings . fiveColumns }
disableCollapse = { settings . disableCollapse }
/ >
) ) }
< / div >
) }
{ bookmarkGroups ? . length > 0 && (
< div key = "bookmarks" id = "bookmarks" className = "flex flex-wrap m-4 sm:m-8 sm:mt-4 items-start mb-2" >
{ bookmarkGroups . map ( ( group ) => (
< BookmarksGroup
key = { group . name }
bookmarks = { group }
layout = { settings . layout ? . [ group . name ] }
disableCollapse = { settings . disableCollapse }
/ >
) ) }
< / div >
) }
< / >
) ;
} , [
tabs ,
activeTab ,
services ,
bookmarks ,
settings . layout ,
settings . fiveColumns ,
settings . disableCollapse ,
settings . useEqualHeights ,
settings . cardBlur ,
initialSettings . layout ,
] ) ;
return (
< >
< Head >
< title > { settings . title || "Homepage" } < / title >
{ settings . base && < base href = { settings . base } / > }
{ settings . favicon ? (
< >
< link rel = "icon" href = { settings . favicon } / >
< link rel = "apple-touch-icon" sizes = "180x180" href = { settings . favicon } / >
< / >
) : (
< >
< link rel = "apple-touch-icon" sizes = "180x180" href = "/apple-touch-icon.png?v=4" / >
< link rel = "shortcut icon" href = "/homepage.ico" / >
< link rel = "icon" type = "image/png" sizes = "32x32" href = "/favicon-32x32.png?v=4" / >
< link rel = "icon" type = "image/png" sizes = "16x16" href = "/favicon-16x16.png?v=4" / >
< link rel = "mask-icon" href = "/safari-pinned-tab.svg?v=4" color = "#1e9cd7" / >
< / >
) }
< meta name = "msapplication-TileColor" content = { themes [ settings . color || "slate" ] [ settings . theme || "dark" ] } / >
< meta name = "theme-color" content = { themes [ settings . color || "slate" ] [ settings . theme || "dark" ] } / >
< / Head >
< link rel = "preload" href = "/api/config/custom.css" as = "fetch" crossOrigin = "anonymous" / >
< style data - name = "custom.css" >
< FileContent
path = "custom.css"
loadingValue = "/* Loading custom CSS... */"
errorValue = "/* Failed to load custom CSS... */"
emptyValue = "/* No custom CSS */"
/ >
< / style >
< link rel = "preload" href = "/api/config/custom.js" as = "fetch" crossOrigin = "anonymous" / >
< script data - name = "custom.js" src = "/api/config/custom.js" async / >
< div className = "relative container m-auto flex flex-col justify-start z-10 h-full" >
< QuickLaunch
servicesAndBookmarks = { servicesAndBookmarks }
searchString = { searchString }
setSearchString = { setSearchString }
isOpen = { searching }
close = { setSearching }
searchProvider = { settings . quicklaunch ? . hideInternetSearch ? null : searchProvider }
/ >
< div
id = "information-widgets"
className = { classNames (
"flex flex-row flex-wrap justify-between" ,
headerStyles [ headerStyle ] ,
settings . cardBlur !== undefined &&
headerStyle === "boxed" &&
` backdrop-blur ${ settings . cardBlur . length ? "-" : "" } ${ settings . cardBlur } ` ,
) }
>
< div
id = "widgets-wrap"
style = { { width : "calc(100% + 1rem)" } }
className = { classNames ( "flex flex-row w-full flex-wrap justify-between -ml-2 -mr-2" ) }
>
{ widgets && (
< >
{ widgets
. filter ( ( widget ) => ! rightAlignedWidgets . includes ( widget . type ) )
. map ( ( widget , i ) => (
< Widget
key = { i }
widget = { widget }
style = { { header : headerStyle , isRightAligned : false , cardBlur : settings . cardBlur } }
/ >
) ) }
< div
id = "information-widgets-right"
className = { classNames (
"m-auto flex flex-wrap grow sm:basis-auto justify-between md:justify-end" ,
headerStyle === "boxedWidgets" ? "sm:ml-4" : "sm:ml-2" ,
) }
>
{ widgets
. filter ( ( widget ) => rightAlignedWidgets . includes ( widget . type ) )
. map ( ( widget , i ) => (
< Widget
key = { i }
widget = { widget }
style = { { header : headerStyle , isRightAligned : true , cardBlur : settings . cardBlur } }
/ >
) ) }
< / div >
< / >
) }
< / div >
< / div >
{ servicesAndBookmarksGroups }
< div id = "footer" className = "flex flex-col mt-auto p-8 w-full" >
< div id = "style" className = "flex w-full justify-end" >
{ ! settings ? . color && < ColorToggle / > }
< Revalidate / >
{ ! settings . theme && < ThemeToggle / > }
< / div >
< div id = "version" className = "flex mt-4 w-full justify-end" >
{ ! settings . hideVersion && < Version / > }
< / div >
< / div >
< / div >
< / >
) ;
}
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 (
< div
id = "page_wrapper"
className = { classNames (
"relative" ,
initialSettings . theme && initialSettings . theme ,
initialSettings . color && ` theme- ${ initialSettings . color } ` ,
) }
>
< div
id = "page_container"
className = "fixed overflow-auto w-full h-full bg-theme-50 dark:bg-theme-800 transition-all"
style = { wrappedStyle }
>
< div
id = "inner_wrapper"
tabIndex = "-1"
className = { classNames (
"fixed overflow-auto w-full h-full" ,
backgroundBlur &&
` backdrop-blur ${ initialSettings . background . blur . length ? "-" : "" } ${ initialSettings . background . blur } ` ,
backgroundSaturate && ` backdrop-saturate- ${ initialSettings . background . saturate } ` ,
backgroundBrightness && ` backdrop-brightness- ${ initialSettings . background . brightness } ` ,
) }
>
< Index initialSettings = { initialSettings } fallback = { fallback } / >
< / div >
< / div >
< / div >
) ;
}