Compare commits

...

3 Commits

Author SHA1 Message Date
Daniel ae6656e0ec Merge pull request #386 from treyg/global-theming-2.4
Global theming support
2024-01-02 07:30:01 -05:00
Trey Gordon 7e9eae0ef2 style: change to neutral to handle new themes 2023-12-29 12:29:10 -05:00
Trey Gordon 6b28abc405 feat: add new theming options 2023-12-29 12:28:43 -05:00
8 changed files with 612 additions and 464 deletions
+15 -15
View File
@@ -26,13 +26,13 @@ export default function Navbar() {
const { width } = useWindowDimensions(); const { width } = useWindowDimensions();
const handleToggle = () => { const handleToggle = () => {
if (settings.theme === "dark") { const [colorTheme, mode] = (settings.theme || "default-light").split('-');
updateSettings({ theme: "light" }); const newMode = mode === "dark" ? "light" : "dark";
} else { const newTheme = `${colorTheme}-${newMode}`;
updateSettings({ theme: "dark" }); updateSettings({ theme: newTheme });
}
}; };
useEffect(() => { useEffect(() => {
setSidebar(false); setSidebar(false);
}, [width]); }, [width]);
@@ -135,16 +135,16 @@ export default function Navbar() {
</Link> </Link>
</li> </li>
<li> <li>
<div <div
onClick={() => { onClick={() => {
(document?.activeElement as HTMLElement)?.blur(); (document?.activeElement as HTMLElement)?.blur();
handleToggle(); handleToggle();
}} }}
tabIndex={0} tabIndex={0}
role="button" role="button"
> >
Switch to {settings.theme === "light" ? "Dark" : "Light"} Switch to {(settings.theme || "default-light").endsWith("-dark") ? "Light" : "Dark"}
</div> </div>
</li> </li>
<li> <li>
<div <div
+35 -28
View File
@@ -2,39 +2,46 @@ import useLocalSettingsStore from "@/store/localSettings";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
type Props = { type Props = {
className?: string; className?: string;
}; };
export default function ToggleDarkMode({ className }: Props) { export default function ToggleDarkMode({ className }: Props) {
const { settings, updateSettings } = useLocalSettingsStore(); const { updateSettings } = useLocalSettingsStore();
const [theme, setTheme] = useState('default-light');
const [theme, setTheme] = useState(localStorage.getItem("theme")); useEffect(() => {
const storedTheme = localStorage.getItem("theme");
if (storedTheme) {
setTheme(storedTheme);
} else {
// Default theme if not set in localStorage
localStorage.setItem("theme", "default-light");
setTheme("default-light");
}
console.log("Initial theme from localStorage:", storedTheme || "default-light");
}, []);
const handleToggle = (e: any) => { const handleToggle = () => {
setTheme(e.target.checked ? "dark" : "light"); const [currentColorTheme, currentMode] = theme.split('-');
}; const newMode = currentMode === 'light' ? 'dark' : 'light';
const newTheme = `${currentColorTheme}-${newMode}`;
useEffect(() => { setTheme(newTheme);
updateSettings({ theme: theme as string }); localStorage.setItem("theme", newTheme);
}, [theme]); document.documentElement.setAttribute('data-theme', newTheme);
updateSettings({ theme: newTheme });
console.log("New theme set:", newTheme);
};
return ( const isDarkMode = theme.endsWith('-dark');
<div
className="tooltip tooltip-bottom" return (
data-tip={`Switch to ${settings.theme === "light" ? "Dark" : "Light"}`} <div className="tooltip tooltip-bottom" data-tip={`Switch to ${isDarkMode ? "Light" : "Dark"}`}>
> <label className={`swap swap-rotate btn-square text-neutral btn btn-ghost btn-sm ${className}`}>
<label <input type="checkbox" onChange={handleToggle} className="theme-controller" checked={isDarkMode} />
className={`swap swap-rotate btn-square text-neutral btn btn-ghost btn-sm ${className}`} <i className="bi-sun-fill text-xl swap-on"></i>
> <i className="bi-moon-fill text-xl swap-off"></i>
<input </label>
type="checkbox" </div>
onChange={handleToggle} );
className="theme-controller"
checked={localStorage.getItem("theme") === "light" ? false : true}
/>
<i className="bi-sun-fill text-xl swap-on"></i>
<i className="bi-moon-fill text-xl swap-off"></i>
</label>
</div>
);
} }
+57 -45
View File
@@ -1,4 +1,4 @@
import React from "react"; import React, { useEffect } from "react";
import "@/styles/globals.css"; import "@/styles/globals.css";
import "bootstrap-icons/font/bootstrap-icons.css"; import "bootstrap-icons/font/bootstrap-icons.css";
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from "next-auth/react";
@@ -9,50 +9,62 @@ import { Toaster } from "react-hot-toast";
import { Session } from "next-auth"; import { Session } from "next-auth";
export default function App({ export default function App({
Component, Component,
pageProps, pageProps,
}: AppProps<{ }: AppProps<{
session: Session; session: Session;
}>) { }>) {
return (
<SessionProvider useEffect(() => {
session={pageProps.session} let theme = localStorage.getItem("theme");
refetchOnWindowFocus={false} if (!theme || !theme.includes("-")) {
basePath="/api/v1/auth" theme = "default-dark"; // Default theme
> localStorage.setItem("theme", theme);
<Head> }
<title>Linkwarden</title>
<meta name="viewport" content="width=device-width, initial-scale=1" /> document.documentElement.setAttribute('data-theme', theme);
<link }, []);
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png" return (
/> <SessionProvider
<link session={pageProps.session}
rel="icon" refetchOnWindowFocus={false}
type="image/png" basePath="/api/v1/auth"
sizes="32x32" >
href="/favicon-32x32.png" <Head>
/> <title>Linkwarden</title>
<link <meta name="viewport" content="width=device-width, initial-scale=1" />
rel="icon" <link
type="image/png" rel="apple-touch-icon"
sizes="16x16" sizes="180x180"
href="/favicon-16x16.png" href="/apple-touch-icon.png"
/> />
<link rel="manifest" href="/site.webmanifest" /> <link
</Head> rel="icon"
<AuthRedirect> type="image/png"
<Toaster sizes="32x32"
position="top-center" href="/favicon-32x32.png"
reverseOrder={false} />
toastOptions={{ <link
className: rel="icon"
"border border-sky-100 dark:border-neutral-700 dark:bg-neutral-800 dark:text-white", type="image/png"
}} sizes="16x16"
/> href="/favicon-16x16.png"
<Component {...pageProps} /> />
</AuthRedirect> <link rel="manifest" href="/site.webmanifest" />
</SessionProvider> </Head>
); <AuthRedirect>
<Toaster
position="top-center"
reverseOrder={false}
toastOptions={{
className:
"border border-sky-100 dark:border-neutral-700 dark:bg-neutral-800 dark:text-white",
}}
/>
<Component {...pageProps} />
</AuthRedirect>
</SessionProvider>
);
} }
+8 -6
View File
@@ -101,12 +101,14 @@ export default function Index() {
return ( return (
<MainLayout> <MainLayout>
<div <div
className="h-[60rem] p-5 flex gap-3 flex-col" className="h-[60rem] p-5 flex gap-3 flex-col"
style={{ style={{
backgroundImage: `linear-gradient(${activeCollection?.color}20 10%, ${ backgroundImage: `linear-gradient(${activeCollection?.color}20 10%, ${
settings.theme === "dark" ? "#262626" : "#f3f4f6" (settings.theme || "default-light").endsWith("-dark") ? "#262626" : "#f3f4f6"
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`, } 13rem, ${
}} (settings.theme || "default-light").endsWith("-dark") ? "#171717" : "#ffffff"
} 100%)`,
}}
> >
{activeCollection && ( {activeCollection && (
<div className="flex gap-3 items-start justify-between"> <div className="flex gap-3 items-start justify-between">
+258 -258
View File
@@ -19,293 +19,293 @@ import ViewDropdown from "@/components/ViewDropdown";
// import GridView from "@/components/LinkViews/Layouts/GridView"; // import GridView from "@/components/LinkViews/Layouts/GridView";
export default function Dashboard() { export default function Dashboard() {
const { collections } = useCollectionStore(); const { collections } = useCollectionStore();
const { links } = useLinkStore(); const { links } = useLinkStore();
const { tags } = useTagStore(); const { tags } = useTagStore();
const [numberOfLinks, setNumberOfLinks] = useState(0); const [numberOfLinks, setNumberOfLinks] = useState(0);
const [showLinks, setShowLinks] = useState(3); const [showLinks, setShowLinks] = useState(3);
useLinks({ pinnedOnly: true, sort: 0 }); useLinks({ pinnedOnly: true, sort: 0 });
useEffect(() => { useEffect(() => {
setNumberOfLinks( setNumberOfLinks(
collections.reduce( collections.reduce(
(accumulator, collection) => (accumulator, collection) =>
accumulator + (collection._count as any).links, accumulator + (collection._count as any).links,
0 0
) )
); );
}, [collections]); }, [collections]);
const handleNumberOfLinksToShow = () => { const handleNumberOfLinksToShow = () => {
if (window.innerWidth > 1900) { if (window.innerWidth > 1900) {
setShowLinks(8); setShowLinks(8);
} else if (window.innerWidth > 1280) { } else if (window.innerWidth > 1280) {
setShowLinks(6); setShowLinks(6);
} else if (window.innerWidth > 650) { } else if (window.innerWidth > 650) {
setShowLinks(4); setShowLinks(4);
} else setShowLinks(3); } else setShowLinks(3);
}; };
const { width } = useWindowDimensions(); const { width } = useWindowDimensions();
useEffect(() => { useEffect(() => {
handleNumberOfLinksToShow(); handleNumberOfLinksToShow();
}, [width]); }, [width]);
const importBookmarks = async (e: any, format: MigrationFormat) => { const importBookmarks = async (e: any, format: MigrationFormat) => {
const file: File = e.target.files[0]; const file: File = e.target.files[0];
if (file) { if (file) {
var reader = new FileReader(); var reader = new FileReader();
reader.readAsText(file, "UTF-8"); reader.readAsText(file, "UTF-8");
reader.onload = async function (e) { reader.onload = async function (e) {
const load = toast.loading("Importing..."); const load = toast.loading("Importing...");
const request: string = e.target?.result as string; const request: string = e.target?.result as string;
const body: MigrationRequest = { const body: MigrationRequest = {
format, format,
data: request, data: request,
}; };
const response = await fetch("/api/v1/migration", { const response = await fetch("/api/v1/migration", {
method: "POST", method: "POST",
body: JSON.stringify(body), body: JSON.stringify(body),
}); });
const data = await response.json(); const data = await response.json();
toast.dismiss(load); toast.dismiss(load);
toast.success("Imported the Bookmarks! Reloading the page..."); toast.success("Imported the Bookmarks! Reloading the page...");
setTimeout(() => { setTimeout(() => {
location.reload(); location.reload();
}, 2000); }, 2000);
}; };
reader.onerror = function (e) { reader.onerror = function (e) {
console.log("Error:", e); console.log("Error:", e);
}; };
} }
}; };
const [newLinkModal, setNewLinkModal] = useState(false); const [newLinkModal, setNewLinkModal] = useState(false);
const [viewMode, setViewMode] = useState<string>( const [viewMode, setViewMode] = useState<string>(
localStorage.getItem("viewMode") || ViewMode.Card localStorage.getItem("viewMode") || ViewMode.Card
); );
const linkView = { const linkView = {
[ViewMode.Card]: CardView, [ViewMode.Card]: CardView,
// [ViewMode.Grid]: GridView, // [ViewMode.Grid]: GridView,
[ViewMode.List]: ListView, [ViewMode.List]: ListView,
}; };
// @ts-ignore // @ts-ignore
const LinkComponent = linkView[viewMode]; const LinkComponent = linkView[viewMode];
return ( return (
<MainLayout> <MainLayout>
<div style={{ flex: "1 1 auto" }} className="p-5 flex flex-col gap-5"> <div style={{ flex: "1 1 auto" }} className="p-5 flex flex-col gap-5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<PageHeader <PageHeader
icon={"bi-house "} icon={"bi-house "}
title={"Dashboard"} title={"Dashboard"}
description={"A brief overview of your data"} description={"A brief overview of your data"}
/> />
<ViewDropdown viewMode={viewMode} setViewMode={setViewMode} /> <ViewDropdown viewMode={viewMode} setViewMode={setViewMode} />
</div>
<div>
<div className="flex justify-evenly flex-col xl:flex-row xl:items-center gap-2 xl:w-full h-full rounded-2xl p-8 border border-neutral-content bg-base-200">
<DashboardItem
name={numberOfLinks === 1 ? "Link" : "Links"}
value={numberOfLinks}
icon={"bi-link-45deg"}
/>
<div className="divider xl:divider-horizontal"></div>
<DashboardItem
name={collections.length === 1 ? "Collection" : "Collections"}
value={collections.length}
icon={"bi-folder"}
/>
<div className="divider xl:divider-horizontal"></div>
<DashboardItem
name={tags.length === 1 ? "Tag" : "Tags"}
value={tags.length}
icon={"bi-hash"}
/>
</div>
</div>
<div className="flex justify-between items-center">
<div className="flex gap-2 items-center">
<PageHeader
icon={"bi-clock-history"}
title={"Recent"}
description={"Recently added Links"}
/>
</div>
<Link
href="/links"
className="flex items-center text-sm text-black/75 dark:text-white/75 gap-2 cursor-pointer"
>
View All
<i className="bi-chevron-right text-sm"></i>
</Link>
</div>
<div
style={{ flex: "0 1 auto" }}
className="flex flex-col 2xl:flex-row items-start 2xl:gap-2"
>
{links[0] ? (
<div className="w-full">
<LinkComponent links={links.slice(0, showLinks)} />
</div> </div>
) : (
<div>
<div className="flex justify-evenly flex-col xl:flex-row xl:items-center gap-2 xl:w-full h-full rounded-2xl p-8 border border-neutral-content bg-base-200">
<DashboardItem
name={numberOfLinks === 1 ? "Link" : "Links"}
value={numberOfLinks}
icon={"bi-link-45deg"}
/>
<div className="divider xl:divider-horizontal"></div>
<DashboardItem
name={collections.length === 1 ? "Collection" : "Collections"}
value={collections.length}
icon={"bi-folder"}
/>
<div className="divider xl:divider-horizontal"></div>
<DashboardItem
name={tags.length === 1 ? "Tag" : "Tags"}
value={tags.length}
icon={"bi-hash"}
/>
</div>
</div>
<div className="flex justify-between items-center">
<div className="flex gap-2 items-center">
<PageHeader
icon={"bi-clock-history"}
title={"Recent"}
description={"Recently added Links"}
/>
</div>
<Link
href="/links"
className="flex items-center text-sm text-neutral gap-2 cursor-pointer"
>
View All
<i className="bi-chevron-right text-sm"></i>
</Link>
</div>
<div <div
style={{ flex: "1 1 auto" }} style={{ flex: "0 1 auto" }}
className="sky-shadow flex flex-col justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200" className="flex flex-col 2xl:flex-row items-start 2xl:gap-2"
> >
<p className="text-center text-2xl"> {links[0] ? (
View Your Recently Added Links Here! <div className="w-full">
</p> <LinkComponent links={links.slice(0, showLinks)} />
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm mt-2">
This section will view your latest added Links across every
Collections you have access to.
</p>
<div className="text-center w-full mt-4 flex flex-wrap gap-4 justify-center">
<div
onClick={() => {
setNewLinkModal(true);
}}
className="inline-flex items-center gap-2 text-sm btn btn-accent dark:border-violet-400 text-white"
>
<i className="bi-plus-lg text-xl duration-100"></i>
<span className="group-hover:opacity-0 text-right duration-100">
Add New Link
</span>
</div>
<div className="dropdown dropdown-bottom">
<div
tabIndex={0}
role="button"
className="inline-flex items-center gap-2 text-sm btn btn-outline btn-neutral"
id="import-dropdown"
>
<i className="bi-cloud-upload text-xl duration-100"></i>
<p>Import From</p>
</div> </div>
<ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60"> ) : (
<li> <div
<label style={{ flex: "1 1 auto" }}
tabIndex={0} className="sky-shadow flex flex-col justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200"
role="button" >
htmlFor="import-linkwarden-file" <p className="text-center text-2xl">
title="JSON File" View Your Recently Added Links Here!
> </p>
From Linkwarden <p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm mt-2">
<input This section will view your latest added Links across every
type="file" Collections you have access to.
name="photo" </p>
id="import-linkwarden-file"
accept=".json"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.linkwarden)
}
/>
</label>
</li>
<li>
<label
tabIndex={0}
role="button"
htmlFor="import-html-file"
title="HTML File"
>
From Bookmarks HTML file
<input
type="file"
name="photo"
id="import-html-file"
accept=".html"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.htmlFile)
}
/>
</label>
</li>
</ul>
</div>
</div>
</div>
)}
</div>
<div className="flex justify-between items-center"> <div className="text-center w-full mt-4 flex flex-wrap gap-4 justify-center">
<div className="flex gap-2 items-center"> <div
<PageHeader onClick={() => {
icon={"bi-pin-angle"} setNewLinkModal(true);
title={"Pinned"} }}
description={"Your pinned Links"} className="inline-flex items-center gap-2 text-sm btn btn-accent dark:border-violet-400 text-white"
/> >
</div> <i className="bi-plus-lg text-xl duration-100"></i>
<Link <span className="group-hover:opacity-0 text-right duration-100">
href="/links/pinned" Add New Link
className="flex items-center text-sm text-black/75 dark:text-white/75 gap-2 cursor-pointer" </span>
> </div>
View All
<i className="bi-chevron-right text-sm "></i>
</Link>
</div>
<div <div className="dropdown dropdown-bottom">
style={{ flex: "1 1 auto" }} <div
className="flex flex-col 2xl:flex-row items-start 2xl:gap-2" tabIndex={0}
> role="button"
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? ( className="inline-flex items-center gap-2 text-sm btn btn-outline btn-neutral"
<div className="w-full"> id="import-dropdown"
<div >
className={`grid min-[1900px]:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5 w-full`} <i className="bi-cloud-upload text-xl duration-100"></i>
> <p>Import From</p>
{links </div>
.filter((e) => e.pinnedBy && e.pinnedBy[0]) <ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60">
.map((e, i) => <LinkCard key={i} link={e} count={i} />) <li>
.slice(0, showLinks)} <label
</div> tabIndex={0}
role="button"
htmlFor="import-linkwarden-file"
title="JSON File"
>
From Linkwarden
<input
type="file"
name="photo"
id="import-linkwarden-file"
accept=".json"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.linkwarden)
}
/>
</label>
</li>
<li>
<label
tabIndex={0}
role="button"
htmlFor="import-html-file"
title="HTML File"
>
From Bookmarks HTML file
<input
type="file"
name="photo"
id="import-html-file"
accept=".html"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.htmlFile)
}
/>
</label>
</li>
</ul>
</div>
</div>
</div>
)}
</div> </div>
) : (
<div className="flex justify-between items-center">
<div className="flex gap-2 items-center">
<PageHeader
icon={"bi-pin-angle"}
title={"Pinned"}
description={"Your pinned Links"}
/>
</div>
<Link
href="/links/pinned"
className="flex items-center text-sm text-neutral gap-2 cursor-pointer"
>
View All
<i className="bi-chevron-right text-sm "></i>
</Link>
</div>
<div <div
style={{ flex: "1 1 auto" }} style={{ flex: "1 1 auto" }}
className="sky-shadow flex flex-col justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200" className="flex flex-col 2xl:flex-row items-start 2xl:gap-2"
> >
<p className="text-center text-2xl"> {links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
Pin Your Favorite Links Here! <div className="w-full">
</p> <div
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm mt-2"> className={`grid min-[1900px]:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5 w-full`}
You can Pin your favorite Links by clicking on the three dots on >
each Link and clicking{" "} {links
<span className="font-semibold">Pin to Dashboard</span>. .filter((e) => e.pinnedBy && e.pinnedBy[0])
</p> .map((e, i) => <LinkCard key={i} link={e} count={i} />)
.slice(0, showLinks)}
</div>
</div>
) : (
<div
style={{ flex: "1 1 auto" }}
className="sky-shadow flex flex-col justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200"
>
<p className="text-center text-2xl">
Pin Your Favorite Links Here!
</p>
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm mt-2">
You can Pin your favorite Links by clicking on the three dots on
each Link and clicking{" "}
<span className="font-semibold">Pin to Dashboard</span>.
</p>
</div>
)}
</div> </div>
)} </div>
</div> {newLinkModal ? (
</div> <NewLinkModal onClose={() => setNewLinkModal(false)} />
{newLinkModal ? ( ) : undefined}
<NewLinkModal onClose={() => setNewLinkModal(false)} /> </MainLayout>
) : undefined} );
</MainLayout>
);
} }
+95 -76
View File
@@ -1,16 +1,16 @@
import SettingsLayout from "@/layouts/SettingsLayout"; import SettingsLayout from "@/layouts/SettingsLayout";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import useAccountStore from "@/store/account"; import useAccountStore from "@/store/account";
import { AccountSettings } from "@/types/global"; import { AccountSettings } from "@/types/global";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import React from "react";
import useLocalSettingsStore from "@/store/localSettings"; import useLocalSettingsStore from "@/store/localSettings";
export default function Appearance() { export default function Appearance() {
const { updateSettings } = useLocalSettingsStore(); const { updateSettings } = useLocalSettingsStore();
const submit = async () => { const { account, updateAccount } = useAccountStore();
const submit = async () => {
setSubmitLoader(true); setSubmitLoader(true);
const load = toast.loading("Applying..."); const load = toast.loading("Applying...");
const response = await updateAccount({ const response = await updateAccount({
@@ -18,89 +18,108 @@ export default function Appearance() {
}); });
toast.dismiss(load); toast.dismiss(load);
if (response.ok) { if (response.ok) {
toast.success("Settings Applied!"); toast.success("Settings Applied!");
} else toast.error(response.data as string); } else toast.error(response.data as string);
setSubmitLoader(false); setSubmitLoader(false);
}; };
const [submitLoader, setSubmitLoader] = useState(false);
const [user, setUser] = useState<AccountSettings>(
!objectIsEmpty(account)
? account
: ({
// @ts-ignore
id: null,
name: "",
username: "",
email: "",
emailVerified: null,
blurredFavicons: null,
image: "",
isPrivate: true,
// @ts-ignore
createdAt: null,
whitelistedUsers: [],
} as unknown as AccountSettings)
);
const [submitLoader, setSubmitLoader] = useState(false); // Combine colorTheme and mode into a single state
const [theme, setTheme] = useState(localStorage.getItem("theme") || "default-dark");
const { account, updateAccount } = useAccountStore(); function objectIsEmpty(obj: object) {
return Object.keys(obj).length === 0;
}
const [user, setUser] = useState<AccountSettings>( useEffect(() => {
!objectIsEmpty(account) if (!objectIsEmpty(account)) setUser({ ...account });
? account }, [account]);
: ({
// @ts-ignore
id: null,
name: "",
username: "",
email: "",
emailVerified: null,
blurredFavicons: null,
image: "",
isPrivate: true,
// @ts-ignore
createdAt: null,
whitelistedUsers: [],
} as unknown as AccountSettings)
);
function objectIsEmpty(obj: object) { const handleThemeChange = (newThemePart: string, isColorTheme: boolean) => {
return Object.keys(obj).length === 0; const currentTheme = localStorage.getItem("theme") || "default-light";
} const [currentColorTheme, currentMode] = currentTheme.split('-');
const newTheme = isColorTheme ? `${newThemePart}-${currentMode}` : `${currentColorTheme}-${newThemePart}`;
useEffect(() => { localStorage.setItem("theme", newTheme);
if (!objectIsEmpty(account)) setUser({ ...account }); document.documentElement.setAttribute('data-theme', newTheme);
}, [account]); updateSettings({ theme: newTheme });
return ( // Update the theme state
<SettingsLayout> setTheme(newTheme);
<p className="capitalize text-3xl font-thin inline">Appearance</p> };
<div className="divider my-3"></div>
<div className="flex flex-col gap-5">
<div>
<p className="mb-3">Select Theme</p>
<div className="flex gap-3 w-full">
<div
className={`w-full text-center outline-solid outline-neutral-content outline dark:outline-neutral-700 h-36 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-black ${
localStorage.getItem("theme") === "dark"
? "dark:outline-primary text-primary"
: "text-white"
}`}
onClick={() => updateSettings({ theme: "dark" })}
>
<i className="bi-moon-fill text-6xl"></i>
<p className="ml-2 text-2xl">Dark</p>
{/* <hr className="my-3 outline-1 outline-neutral-content dark:outline-neutral-700" /> */}
return (
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">Appearance</p>
<div className="divider my-3"></div>
<div className="flex flex-col gap-5">
<div>
<p className="mb-3">Select Mode</p>
<div className="grid grid-cols-2 gap-3">
{["light", "dark"].map((modeOption) => (
<button
key={modeOption}
onClick={() => handleThemeChange(modeOption, false)}
className={`w-full text-center h-36 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none ${theme.endsWith(modeOption) ? "ring-2 ring-primary" : "ring-2 ring-neutral"}`}
>
{modeOption === 'light' ?
<i className={`bi-sun-fill text-6xl ${theme.endsWith(modeOption) ? "text-primary" : ""}`}></i> :
<i className={`bi-moon-fill text-6xl ${theme.endsWith(modeOption) ? "text-primary" : ""}`}></i>}
<p className="ml-2 text-2xl">{modeOption.charAt(0).toUpperCase() + modeOption.slice(1)}</p>
</button>
))}
</div>
</div> </div>
<div
className={`w-full text-center outline-solid outline-neutral-content outline dark:outline-neutral-700 h-36 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-white ${
localStorage.getItem("theme") === "light"
? "outline-primary text-primary"
: "text-black"
}`}
onClick={() => updateSettings({ theme: "light" })}
>
<i className="bi-sun-fill text-6xl"></i>
<p className="ml-2 text-2xl">Light</p>
{/* <hr className="my-3 outline-1 outline-neutral-content dark:outline-neutral-700" /> */}
</div>
</div>
</div>
{/* <SubmitButton <div>
onClick={submit} <p className="mb-3">Select Color Theme</p>
loading={submitLoader} <div className="grid grid-cols-4 gap-3">
label="Save" {["default", "red", "green", "orange"].map((colorTheme) => (
className="mt-2 mx-auto lg:mx-0" <button
/> */} key={colorTheme}
</div> onClick={() => handleThemeChange(colorTheme, true)}
</SettingsLayout> className={`w-full text-center h-36 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none ${theme.startsWith(colorTheme) ? "ring-2 ring-primary" : "ring-2 ring-neutral"}`}
); >
{colorTheme.charAt(0).toUpperCase() + colorTheme.slice(1)}
</button>
))}
</div>
</div>
<button
onClick={submit}
disabled={submitLoader}
className="mt-2 mx-auto lg:mx-0 bg-primary text-white rounded-md px-4 py-2"
>
{submitLoader ? "Saving..." : "Save"}
</button>
</div>
</SettingsLayout>
);
} }
+21 -16
View File
@@ -18,34 +18,39 @@ const useLocalSettingsStore = create<LocalSettingsStore>((set) => ({
viewMode: "", viewMode: "",
}, },
updateSettings: async (newSettings) => { updateSettings: async (newSettings) => {
if ( if (newSettings.theme) {
newSettings.theme &&
newSettings.theme !== localStorage.getItem("theme")
) {
localStorage.setItem("theme", newSettings.theme); localStorage.setItem("theme", newSettings.theme);
document.documentElement.setAttribute('data-theme', newSettings.theme);
const localTheme = localStorage.getItem("theme") || ""; if (newSettings.theme.endsWith("-dark")) {
document.documentElement.classList.add("dark");
document.querySelector("html")?.setAttribute("data-theme", localTheme); } else {
document.documentElement.classList.remove("dark");
}
} }
if ( if (newSettings.viewMode) {
newSettings.viewMode &&
newSettings.viewMode !== localStorage.getItem("viewMode")
) {
localStorage.setItem("viewMode", newSettings.viewMode); localStorage.setItem("viewMode", newSettings.viewMode);
// const localTheme = localStorage.getItem("viewMode") || "";
} }
set((state) => ({ settings: { ...state.settings, ...newSettings } })); set((state) => ({ settings: { ...state.settings, ...newSettings } }));
}, },
setSettings: async () => { setSettings: async () => {
if (!localStorage.getItem("theme")) { let theme = localStorage.getItem("theme");
localStorage.setItem("theme", "dark"); if (!theme || !theme.includes("-")) {
theme = "default-dark"; // Default theme
localStorage.setItem("theme", theme);
} }
const localTheme = localStorage.getItem("theme") || ""; const localTheme = theme;
document.documentElement.setAttribute('data-theme', localTheme);
if (localTheme.endsWith("-dark")) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
set((state) => ({ set((state) => ({
settings: { ...state.settings, theme: localTheme }, settings: { ...state.settings, theme: localTheme },
+121 -18
View File
@@ -5,7 +5,7 @@ module.exports = {
daisyui: { daisyui: {
themes: [ themes: [
{ {
light: { "default-light": {
primary: "#0369a1", primary: "#0369a1",
secondary: "#0891b2", secondary: "#0891b2",
accent: "#6d28d9", accent: "#6d28d9",
@@ -21,7 +21,7 @@ module.exports = {
}, },
}, },
{ {
dark: { "default-dark": {
primary: "#7dd3fc", primary: "#7dd3fc",
secondary: "#22d3ee", secondary: "#22d3ee",
accent: "#6d28d9", accent: "#6d28d9",
@@ -36,21 +36,124 @@ module.exports = {
error: "#f1293c", error: "#f1293c",
}, },
}, },
], // Red Light Theme
}, {
darkMode: ["class", '[data-theme="dark"]'], "red-light": {
content: [ primary: "#ef4444",
"./app/**/*.{js,ts,jsx,tsx}", secondary: "#dc2626",
"./pages/**/*.{js,ts,jsx,tsx}", accent: "#6d28d9",
"./components/**/*.{js,ts,jsx,tsx}", neutral: "#6b7280",
"neutral-content": "#d1d5db",
"base-100": "#ffffff",
"base-200": "#f3f4f6",
"base-content": "#0a0a0a",
info: "#a5f3fc",
success: "#22c55e",
warning: "#facc15",
error: "#dc2626",
},
},
// Red Dark Theme
{
"red-dark": {
primary: "#ef4444",
secondary: "#dc2626",
accent: "#6d28d9",
neutral: "#9ca3af",
"neutral-content": "#404040",
"base-100": "#171717",
"base-200": "#262626",
"base-content": "#fafafa",
info: "#009ee4",
success: "#00b17d",
warning: "#eac700",
error: "#f1293c",
},
},
// Green Light Theme
{
"green-light": {
primary: "#22c55e",
secondary: "#16a34a",
accent: "#6d28d9",
neutral: "#6b7280",
"neutral-content": "#d1d5db",
"base-100": "#ffffff",
"base-200": "#f3f4f6",
"base-content": "#0a0a0a",
info: "#a5f3fc",
success: "#22c55e",
warning: "#facc15",
error: "#dc2626",
},
},
// Green Dark Theme
{
"green-dark": {
primary: "#22c55e",
secondary: "#16a34a",
accent: "#6d28d9",
neutral: "#9ca3af",
"neutral-content": "#404040",
"base-100": "#171717",
"base-200": "#262626",
"base-content": "#fafafa",
info: "#009ee4",
success: "#00b17d",
warning: "#eac700",
error: "#f1293c",
},
},
// Orange Light Theme
{
"orange-light": {
primary: "#f97316",
secondary: "#ea580c",
accent: "#6d28d9",
neutral: "#9ca3af",
"neutral-content": "#404040",
"base-100": "#171717",
"base-200": "#262626",
"base-content": "#fafafa",
info: "#009ee4",
success: "#00b17d",
warning: "#eac700",
error: "#f1293c",
},
},
// Orange Dark Theme
{
"orange-dark": {
primary: "#f97316",
secondary: "#ea580c",
accent: "#6d28d9",
neutral: "#9ca3af",
"neutral-content": "#404040",
"base-100": "#171717",
"base-200": "#262626",
"base-content": "#fafafa",
info: "#009ee4",
success: "#00b17d",
warning: "#eac700",
error: "#f1293c",
},
},
],
},
darkMode: ["class", '[data-theme="dark"]'],
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
// For the "layouts" directory // For the "layouts" directory
"./layouts/**/*.{js,ts,jsx,tsx}", "./layouts/**/*.{js,ts,jsx,tsx}",
], ],
plugins: [ plugins: [
require("daisyui"), require("daisyui"),
plugin(({ addVariant }) => { plugin(({ addVariant }) => {
addVariant("dark", '&[data-theme="dark"]'); addVariant("dark", '&[data-theme="dark"]');
}), }),
], ],
}; };