First commit for custom styles and JS

This commit is contained in:
Yann Le Vagueres 2023-09-09 14:56:38 +02:00
parent 63f952509e
commit 213e7fdd28
No known key found for this signature in database
GPG Key ID: EF41B255CD47BC5E
36 changed files with 160 additions and 50 deletions

12
package-lock.json generated
View File

@ -19,6 +19,7 @@
"js-yaml": "^4.1.0",
"json-rpc-2.0": "^1.4.1",
"memory-cache": "^0.2.0",
"mime": "^3.0.0",
"minecraft-ping-js": "^1.0.2",
"next": "^12.3.1",
"next-i18next": "^12.0.1",
@ -4166,6 +4167,17 @@
"node": ">=8.6"
}
},
"node_modules/mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",

View File

@ -21,6 +21,7 @@
"js-yaml": "^4.1.0",
"json-rpc-2.0": "^1.4.1",
"memory-cache": "^0.2.0",
"mime": "^3.0.0",
"minecraft-ping-js": "^1.0.2",
"next": "^12.3.1",
"next-i18next": "^12.0.1",

View File

@ -38,6 +38,9 @@ dependencies:
memory-cache:
specifier: ^0.2.0
version: 0.2.0
mime:
specifier: ^3.0.0
version: 3.0.0
minecraft-ping-js:
specifier: ^1.0.2
version: 1.0.2
@ -2698,6 +2701,12 @@ packages:
mime-db: 1.52.0
dev: false
/mime@3.0.0:
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
engines: {node: '>=10.0.0'}
hasBin: true
dev: false
/mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}

View File

@ -0,0 +1,12 @@
import useSWR from "swr"
export default function FileContent({ path, loadingValue, errorValue, emptyValue = '', refresh = 1500 }) {
const fetcher = (url) => fetch(url).then((res) => res.text())
const { data, error, isLoading } = useSWR(`/api/config/${ path }`, fetcher, {
refreshInterval: refresh,
})
if (error) return (errorValue)
if (isLoading) return (loadingValue)
return (data || emptyValue)
}

View File

@ -38,7 +38,7 @@ export default function ColorToggle() {
}
return (
<div className="w-full self-center">
<div id="color" className="w-full self-center">
<Popover className="relative flex items-center">
<Popover.Button className="outline-none">
<IoColorPalette

View File

@ -10,7 +10,7 @@ export default function Revalidate() {
};
return (
<div className="rounded-full flex align-middle self-center mr-3">
<div id="revalidate" className="rounded-full flex align-middle self-center mr-3">
<MdRefresh onClick={() => revalidate()} className="text-theme-800 dark:text-theme-200 w-6 h-6 cursor-pointer" />
</div>
);

View File

@ -11,7 +11,7 @@ export default function ThemeToggle() {
}
return (
<div className="rounded-full flex self-end">
<div id="theme" className="rounded-full flex self-end">
<MdLightMode className="text-theme-800 dark:text-theme-200 w-5 h-5 m-1.5" />
{theme === "dark" ? (
<MdToggleOn

View File

@ -25,7 +25,7 @@ export default function Version() {
const latestRelease = releaseData?.[0];
return (
<div className="flex flex-row items-center">
<div id="version" className="flex flex-row items-center">
<span className="text-xs text-theme-500 dark:text-theme-400">
{version === "main" || version === "dev" || version === "nightly" ? (
<>

View File

@ -30,7 +30,7 @@ export default function DateTime({ options }) {
}, [date, setDate, dateLocale, format]);
return (
<Container options={options}>
<Container options={options} additionalClassNames="information-widget-datetime">
<Raw>
<div className="flex flex-row items-center grow justify-end">
<span className={`text-theme-800 dark:text-theme-200 tabular-nums ${textSizes[textSize || "lg"]}`}>

View File

@ -3,6 +3,7 @@ import { useContext } from "react";
import { FaMemory, FaRegClock, FaThermometerHalf } from "react-icons/fa";
import { FiCpu, FiHardDrive } from "react-icons/fi";
import { useTranslation } from "next-i18next";
import classNames from "classnames";
import Error from "../widget/error";
import Resource from "../widget/resource";
@ -32,7 +33,7 @@ export default function Widget({ options }) {
}
if (!data) {
return <Resources options={options}>
return <Resources options={options} additionalClassNames="information-widget-glances">
{ options.cpu !== false && <Resource icon={FiCpu} label={t("glances.wait")} percentage="0" /> }
{ options.mem !== false && <Resource icon={FaMemory} label={t("glances.wait")} percentage="0" /> }
{ options.cputemp && <Resource icon={FaThermometerHalf} label={t("glances.wait")} percentage="0" /> }
@ -69,8 +70,10 @@ export default function Widget({ options }) {
: [data.fs.find((d) => d.mnt_point === options.disk)].filter((d) => d);
}
const addedClasses = classNames('information-widget-glances', { 'information-widget-expanded': options.expanded })
return (
<Resources options={options} target={settings.target ?? "_blank"}>
<Resources options={options} target={settings.target ?? "_blank"} additionalClassNames={addedClasses}>
{options.cpu !== false && <Resource
icon={FiCpu}
value={t("common.number", {

View File

@ -14,7 +14,7 @@ const textSizes = {
export default function Greeting({ options }) {
if (options.text) {
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-greeting">
<Raw>
<span className={`text-theme-800 dark:text-theme-200 mr-3 ${textSizes[options.text_size || "xl"]}`}>
{options.text}

View File

@ -36,7 +36,7 @@ export default function Widget({ options }) {
}
if (!data) {
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-kubernetes">
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{cluster.show &&
@ -50,7 +50,7 @@ export default function Widget({ options }) {
</Container>;
}
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-kubernetes">
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{cluster.show &&

View File

@ -5,14 +5,14 @@ import ResolvedIcon from "components/resolvedicon"
export default function Logo({ options }) {
return (
<Container options={options}>
<Container options={options} additionalClassNames={`information-widget-logo ${ options.icon ? 'resolved' : 'fallback'}`}>
<Raw>
{options.icon ?
<div className="mr-3">
<div className="resolved mr-3">
<ResolvedIcon icon={options.icon} width={48} height={48} />
</div> :
// fallback to homepage logo
<div className="w-12 h-12">
<div className="fallback w-12 h-12">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"

View File

@ -17,14 +17,14 @@ export default function Longhorn({ options }) {
}
if (!data) {
return <Container options={options}>
return <Container options={options} additionalClassNames="infomation-widget-longhorn">
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between" />
</Raw>
</Container>;
}
return <Container options={options}>
return <Container options={options} additionalClassNames="infomation-widget-longhorn">
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{data.nodes

View File

@ -8,6 +8,7 @@ export default function Node({ data, expanded, labels }) {
const { t } = useTranslation();
return <Resource
additionalClassNames="information-widget-node"
icon={FaThermometerHalf}
value={t("common.bytes", { value: data.node.available })}
label={t("resources.free")}

View File

@ -24,7 +24,7 @@ function Widget({ options }) {
}
if (!data) {
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-openmeteo">
<PrimaryText>{t("weather.updating")}</PrimaryText>
<SecondaryText>{t("weather.wait")}</SecondaryText>
<WidgetIcon icon={WiCloudDown} size="l" />
@ -35,7 +35,7 @@ function Widget({ options }) {
const condition = data.current_weather.weathercode;
const timeOfDay = data.current_weather.time > data.daily.sunrise[0] && data.current_weather.time < data.daily.sunset[0] ? "day" : "night";
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-openmeteo">
<PrimaryText>
{options.label && `${options.label}, `}
{t("common.number", {
@ -81,7 +81,7 @@ export default function OpenMeteo({ options }) {
// if (!requesting && !location) requestLocation();
if (!location) {
return <ContainerButton options={options} callback={requestLocation} >
return <ContainerButton options={options} callback={requestLocation} additionalClassNames="information-widget-openmeteo-location-button">
<PrimaryText>{t("weather.current")}</PrimaryText>
<SecondaryText>{t("weather.allow")}</SecondaryText>
<WidgetIcon icon={ requesting ? MdLocationSearching : MdLocationDisabled} size="m" pulse />

View File

@ -24,7 +24,7 @@ function Widget({ options }) {
}
if (!data) {
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-openweathermap">
<PrimaryText>{t("weather.updating")}</PrimaryText>
<SecondaryText>{t("weather.wait")}</SecondaryText>
<WidgetIcon icon={WiCloudDown} size="l" />
@ -36,7 +36,7 @@ function Widget({ options }) {
const condition = data.weather[0].id;
const timeOfDay = data.dt > data.sys.sunrise && data.dt < data.sys.sunset ? "day" : "night";
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-openweathermap">
<PrimaryText>{options.label && `${options.label}, ` }{t("common.number", { value: data.main.temp, style: "unit", unit })}</PrimaryText>
<SecondaryText>{data.weather[0].description}</SecondaryText>
<WidgetIcon icon={mapIcon(condition, timeOfDay)} size="xl" />

View File

@ -1,6 +1,6 @@
export default function UsageBar({ percent }) {
export default function UsageBar({ percent, additionalClassNames='' }) {
return (
<div className="mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-theme-200/20">
<div className={`mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-theme-200/20 ${ additionalClassNames}`}>
<div
className="bg-theme-800/70 h-1 rounded-full dark:bg-theme-200/50 transition-all duration-1000"
style={{

View File

@ -103,7 +103,7 @@ export default function Search({ options }) {
localStorage.setItem(localStorageKey, provider.name);
}
return <ContainerForm options={options} callback={submitCallback} additionalClassNames="grow" >
return <ContainerForm options={options} callback={submitCallback} additionalClassNames="grow information-widget-search" >
<Raw>
<div className="flex-col relative h-8 my-4 min-w-fit">
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-white" />

View File

@ -25,7 +25,7 @@ export default function Widget({ options }) {
const defaultSite = options.site ? statsData?.data.find(s => s.desc === options.site) : statsData?.data?.find(s => s.name === "default");
if (!defaultSite) {
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-unify-console">
<PrimaryText>{t("unifi.wait")}</PrimaryText>
<WidgetIcon icon={SiUbiquiti} />
</Container>;
@ -43,7 +43,7 @@ export default function Widget({ options }) {
const dataEmpty = !(wan.show || lan.show || wlan.show || uptime);
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-unify-console">
<Raw>
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<div className="flex flex-col">

View File

@ -24,7 +24,7 @@ function Widget({ options }) {
}
if (!data) {
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-weather">
<PrimaryText>{t("weather.updating")}</PrimaryText>
<SecondaryText>{t("weather.wait")}</SecondaryText>
<WidgetIcon icon={WiCloudDown} size="l" />
@ -35,7 +35,7 @@ function Widget({ options }) {
const condition = data.current.condition.code;
const timeOfDay = data.current.is_day ? "day" : "night";
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-weather">
<PrimaryText>
{options.label && `${options.label}, `}
{t("common.number", {

View File

@ -35,9 +35,9 @@ export function getAllClasses(options, additionalClassNames = '') {
export function getInnerBlock(children) {
// children won't be an array if it's Raw component
return Array.isArray(children) && <div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">{children.find(child => child.type === WidgetIcon)}</div>
<div className="flex flex-col ml-3 text-left">
return Array.isArray(children) && <div className="flex flex-row items-center justify-end widget-inner">
<div className="flex flex-col items-center widget-inner-icon">{children.find(child => child.type === WidgetIcon)}</div>
<div className="flex flex-col ml-3 text-left widget-inner-text">
{children.find(child => child.type === PrimaryText)}
{children.find(child => child.type === SecondaryText)}
</div>
@ -54,7 +54,7 @@ export function getBottomBlock(children) {
export default function Container({ children = [], options, additionalClassNames = '' }) {
return (
<div className={getAllClasses(options, additionalClassNames)}>
<div className={getAllClasses(options, `${ additionalClassNames } widget-container`)}>
{getInnerBlock(children)}
{getBottomBlock(children)}
</div>

View File

@ -2,7 +2,7 @@ import { getAllClasses, getInnerBlock, getBottomBlock } from "./container";
export default function ContainerButton ({ children = [], options, additionalClassNames = '', callback }) {
return (
<button type="button" onClick={callback} className={getAllClasses(options, additionalClassNames)}>
<button type="button" onClick={callback} className={`${ getAllClasses(options, additionalClassNames) } information-widget-container-button`}>
{getInnerBlock(children)}
{getBottomBlock(children)}
</button>

View File

@ -2,7 +2,7 @@ import { getAllClasses, getInnerBlock, getBottomBlock } from "./container";
export default function ContainerForm ({ children = [], options, additionalClassNames = '', callback }) {
return (
<form type="button" onSubmit={callback} className={getAllClasses(options, additionalClassNames)}>
<form type="button" onSubmit={callback} className={`${ getAllClasses(options, additionalClassNames) } information-widget-form`}>
{getInnerBlock(children)}
{getBottomBlock(children)}
</form>

View File

@ -2,7 +2,7 @@ import { getAllClasses, getInnerBlock, getBottomBlock } from "./container";
export default function ContainerLink ({ children = [], options, additionalClassNames = '', target }) {
return (
<a href={options.url} target={target} className={getAllClasses(options, additionalClassNames)}>
<a href={options.url} target={target} className={`${ getAllClasses(options, additionalClassNames) } information-widget-link`}>
{getInnerBlock(children)}
{getBottomBlock(children)}
</a>

View File

@ -8,7 +8,7 @@ import WidgetIcon from "./widget_icon";
export default function Error({ options }) {
const { t } = useTranslation();
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-error">
<PrimaryText>{t("widget.api_error")}</PrimaryText>
<WidgetIcon icon={BiError} size="l" />
</Container>;

View File

@ -1,5 +1,5 @@
export default function PrimaryText({ children }) {
return (
<span className="text-theme-800 dark:text-theme-200 text-sm">{children}</span>
<span className="primary-text text-theme-800 dark:text-theme-200 text-sm">{children}</span>
);
}

View File

@ -1,11 +1,11 @@
import UsageBar from "../resources/usage-bar";
export default function Resource({ children, icon, value, label, expandedValue = "", expandedLabel = "", percentage, expanded = false }) {
export default function Resource({ children, icon, value, label, expandedValue = "", expandedLabel = "", percentage, expanded = false, additionalClassNames='' }) {
const Icon = icon;
return <div className="flex-none flex flex-row items-center mr-3 py-1.5">
<Icon className="text-theme-800 dark:text-theme-200 w-5 h-5"/>
<div className="flex flex-col ml-3 text-left min-w-[85px]">
return <div className={`flex-none flex flex-row items-center mr-3 py-1.5 information-widget-resource ${ additionalClassNames }`}>
<Icon className="text-theme-800 dark:text-theme-200 w-5 h-5 resource-icon"/>
<div className={ `flex flex-col ml-3 text-left min-w-[85px] resource-label${ expanded ? ' expanded' : ''}`}>
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5">{value}</div>
<div className="pr-1">{label}</div>
@ -15,7 +15,7 @@ export default function Resource({ children, icon, value, label, expandedValue =
<div className="pr-1">{expandedLabel}</div>
</div>
}
{ percentage >= 0 && <UsageBar percent={percentage} /> }
{ percentage >= 0 && <UsageBar percent={percentage} additionalClassNames="resource-usage" /> }
{ children }
</div>
</div>;

View File

@ -1,12 +1,15 @@
import classNames from "classnames";
import ContainerLink from "./container_link";
import Resource from "./resource";
import Raw from "./raw";
import WidgetLabel from "./widget_label";
export default function Resources({ options, children, target }) {
export default function Resources({ options, children, target, additionalClassNames }) {
const widgetParts = [].concat(...children);
const addedClassNames = classNames('information-widget-resources', additionalClassNames);
return <ContainerLink options={options} target={target}>
return <ContainerLink options={options} target={target} additionalClassNames={ addedClassNames }>
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{ widgetParts.filter(child => child && child.type === Resource) }

View File

@ -1,5 +1,5 @@
export default function SecondaryText({ children }) {
return (
<span className="text-theme-800 dark:text-theme-200 text-xs">{children}</span>
<span className="secondary-text text-theme-800 dark:text-theme-200 text-xs">{children}</span>
);
}

View File

@ -1,6 +1,6 @@
export default function WidgetIcon({ icon, size = "s", pulse = false }) {
const Icon = icon;
let additionalClasses = "text-theme-800 dark:text-theme-200 ";
let additionalClasses = "information-widget-icon text-theme-800 dark:text-theme-200 ";
switch (size) {
case "m": additionalClasses += "w-6 h-6 "; break;

View File

@ -1,3 +1,3 @@
export default function WidgetLabel({ label = "" }) {
return <div className="pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{label}</div>
return <div className="information-widget-label pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{label}</div>
}

View File

@ -0,0 +1,51 @@
import path from "path";
import fs from "fs";
import mime from "mime";
import { CONF_DIR } from "utils/config/config";
import createLogger from "utils/logger";
const logger = createLogger("configFileService");
/**
* Verifies that the config file paths are in subdirectory
* @param {string} parent Parent initial folder
* @param {string} child Supposed child path
* @returns {boolean} true if in a subdirectory
*/
function isSubDirectory(parent, child) {
return path.relative(child, parent).startsWith('..');
}
/**
* @param {import("next").NextApiRequest} req
* @param {import("next").NextApiResponse} res
*/
export default async function handler(req, res) {
const { path: relativePath } = req.query;
const filePath = path.join(CONF_DIR, relativePath);
if(!isSubDirectory(CONF_DIR, filePath))
{
logger.error(`Forbidden access to parent file: ${ filePath }`);
res.status(403).end('Forbidden access to parent file');
}
const mimeType = mime.getType(relativePath);
try {
// Read the content of the CSS file
const fileContent = fs.readFileSync(filePath, 'utf-8');
// Set the response header to indicate that this is a CSS file
res.setHeader('Content-Type', mimeType);
// Send the CSS content as the API response
res.status(200).send(fileContent);
} catch (error) {
logger.error(error);
res.status(500).end('Internal Server Error');
}
}

View File

@ -8,6 +8,7 @@ import { useEffect, useContext, useState, useMemo } from "react";
import { BiError } from "react-icons/bi";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import FileContent from "components/filecontent";
import ServicesGroup from "components/services/group";
import BookmarksGroup from "components/bookmarks/group";
import Widget from "components/widgets/widget";
@ -311,6 +312,22 @@ function Home({ initialSettings }) {
<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>
<style alt="custom">
<FileContent path="custom.css"
loadingValue="/* Loading custom CSS... */"
errorValue="/* Failed to load custom CSS... */"
emptyValue="/* No custom CSS */"
/>
</style>
<script alt="custom">
<FileContent path="custom.js"
loadingValue="/* Loading custom JS... */"
errorValue="/* Failed to load custom JS... */"
emptyValue="/* No custom JS */"
/>
</script>
<div className="relative container m-auto flex flex-col justify-start z-10 h-full">
<QuickLaunch
servicesAndBookmarks={servicesAndBookmarks}
@ -321,6 +338,7 @@ function Home({ initialSettings }) {
searchProvider={settings.quicklaunch?.hideInternetSearch ? null : searchProvider}
/>
<div
id="information-widgets"
className={classNames(
"flex flex-row flex-wrap justify-between",
headerStyles[headerStyle],
@ -335,7 +353,7 @@ function Home({ initialSettings }) {
<Widget key={i} widget={widget} style={{ header: headerStyle, isRightAligned: false, cardBlur: settings.cardBlur }} />
))}
<div className={classNames(
<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"
)}>
@ -351,14 +369,14 @@ function Home({ initialSettings }) {
{servicesAndBookmarksGroups}
<div className="flex flex-col mt-auto p-8 w-full">
<div className="flex w-full justify-end">
<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 className="flex mt-4 w-full justify-end">
<div id="version" className="flex mt-4 w-full justify-end">
{!settings.hideVersion && <Version />}
</div>
</div>

0
src/skeleton/custom.css Normal file
View File

0
src/skeleton/custom.js Normal file
View File