Merge pull request #386 from treyg/global-theming-2.4

Global theming support
This commit is contained in:
Daniel
2024-01-02 07:30:01 -05:00
committed by GitHub
8 changed files with 612 additions and 464 deletions
+15 -15
View File
@@ -26,12 +26,12 @@ export default function Navbar() {
const { width } = useWindowDimensions();
const handleToggle = () => {
if (settings.theme === "dark") {
updateSettings({ theme: "light" });
} else {
updateSettings({ theme: "dark" });
}
const [colorTheme, mode] = (settings.theme || "default-light").split('-');
const newMode = mode === "dark" ? "light" : "dark";
const newTheme = `${colorTheme}-${newMode}`;
updateSettings({ theme: newTheme });
};
useEffect(() => {
setSidebar(false);
@@ -135,16 +135,16 @@ export default function Navbar() {
</Link>
</li>
<li>
<div
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
handleToggle();
}}
tabIndex={0}
role="button"
>
Switch to {settings.theme === "light" ? "Dark" : "Light"}
</div>
<div
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
handleToggle();
}}
tabIndex={0}
role="button"
>
Switch to {(settings.theme || "default-light").endsWith("-dark") ? "Light" : "Dark"}
</div>
</li>
<li>
<div
+35 -28
View File
@@ -2,39 +2,46 @@ import useLocalSettingsStore from "@/store/localSettings";
import { useEffect, useState } from "react";
type Props = {
className?: string;
className?: string;
};
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) => {
setTheme(e.target.checked ? "dark" : "light");
};
const handleToggle = () => {
const [currentColorTheme, currentMode] = theme.split('-');
const newMode = currentMode === 'light' ? 'dark' : 'light';
const newTheme = `${currentColorTheme}-${newMode}`;
useEffect(() => {
updateSettings({ theme: theme as string });
}, [theme]);
setTheme(newTheme);
localStorage.setItem("theme", newTheme);
document.documentElement.setAttribute('data-theme', newTheme);
updateSettings({ theme: newTheme });
console.log("New theme set:", newTheme);
};
return (
<div
className="tooltip tooltip-bottom"
data-tip={`Switch to ${settings.theme === "light" ? "Dark" : "Light"}`}
>
<label
className={`swap swap-rotate btn-square text-neutral btn btn-ghost btn-sm ${className}`}
>
<input
type="checkbox"
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>
);
const isDarkMode = theme.endsWith('-dark');
return (
<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}`}>
<input type="checkbox" onChange={handleToggle} className="theme-controller" checked={isDarkMode} />
<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 "bootstrap-icons/font/bootstrap-icons.css";
import { SessionProvider } from "next-auth/react";
@@ -9,50 +9,62 @@ import { Toaster } from "react-hot-toast";
import { Session } from "next-auth";
export default function App({
Component,
pageProps,
Component,
pageProps,
}: AppProps<{
session: Session;
session: Session;
}>) {
return (
<SessionProvider
session={pageProps.session}
refetchOnWindowFocus={false}
basePath="/api/v1/auth"
>
<Head>
<title>Linkwarden</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
</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>
);
useEffect(() => {
let theme = localStorage.getItem("theme");
if (!theme || !theme.includes("-")) {
theme = "default-dark"; // Default theme
localStorage.setItem("theme", theme);
}
document.documentElement.setAttribute('data-theme', theme);
}, []);
return (
<SessionProvider
session={pageProps.session}
refetchOnWindowFocus={false}
basePath="/api/v1/auth"
>
<Head>
<title>Linkwarden</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
</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 (
<MainLayout>
<div
className="h-[60rem] p-5 flex gap-3 flex-col"
style={{
backgroundImage: `linear-gradient(${activeCollection?.color}20 10%, ${
settings.theme === "dark" ? "#262626" : "#f3f4f6"
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
}}
className="h-[60rem] p-5 flex gap-3 flex-col"
style={{
backgroundImage: `linear-gradient(${activeCollection?.color}20 10%, ${
(settings.theme || "default-light").endsWith("-dark") ? "#262626" : "#f3f4f6"
} 13rem, ${
(settings.theme || "default-light").endsWith("-dark") ? "#171717" : "#ffffff"
} 100%)`,
}}
>
{activeCollection && (
<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";
export default function Dashboard() {
const { collections } = useCollectionStore();
const { links } = useLinkStore();
const { tags } = useTagStore();
const { collections } = useCollectionStore();
const { links } = useLinkStore();
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(() => {
setNumberOfLinks(
collections.reduce(
(accumulator, collection) =>
accumulator + (collection._count as any).links,
0
)
);
}, [collections]);
useEffect(() => {
setNumberOfLinks(
collections.reduce(
(accumulator, collection) =>
accumulator + (collection._count as any).links,
0
)
);
}, [collections]);
const handleNumberOfLinksToShow = () => {
if (window.innerWidth > 1900) {
setShowLinks(8);
} else if (window.innerWidth > 1280) {
setShowLinks(6);
} else if (window.innerWidth > 650) {
setShowLinks(4);
} else setShowLinks(3);
};
const handleNumberOfLinksToShow = () => {
if (window.innerWidth > 1900) {
setShowLinks(8);
} else if (window.innerWidth > 1280) {
setShowLinks(6);
} else if (window.innerWidth > 650) {
setShowLinks(4);
} else setShowLinks(3);
};
const { width } = useWindowDimensions();
const { width } = useWindowDimensions();
useEffect(() => {
handleNumberOfLinksToShow();
}, [width]);
useEffect(() => {
handleNumberOfLinksToShow();
}, [width]);
const importBookmarks = async (e: any, format: MigrationFormat) => {
const file: File = e.target.files[0];
const importBookmarks = async (e: any, format: MigrationFormat) => {
const file: File = e.target.files[0];
if (file) {
var reader = new FileReader();
reader.readAsText(file, "UTF-8");
reader.onload = async function (e) {
const load = toast.loading("Importing...");
if (file) {
var reader = new FileReader();
reader.readAsText(file, "UTF-8");
reader.onload = async function (e) {
const load = toast.loading("Importing...");
const request: string = e.target?.result as string;
const request: string = e.target?.result as string;
const body: MigrationRequest = {
format,
data: request,
};
const body: MigrationRequest = {
format,
data: request,
};
const response = await fetch("/api/v1/migration", {
method: "POST",
body: JSON.stringify(body),
});
const response = await fetch("/api/v1/migration", {
method: "POST",
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(() => {
location.reload();
}, 2000);
};
reader.onerror = function (e) {
console.log("Error:", e);
};
}
};
setTimeout(() => {
location.reload();
}, 2000);
};
reader.onerror = function (e) {
console.log("Error:", e);
};
}
};
const [newLinkModal, setNewLinkModal] = useState(false);
const [newLinkModal, setNewLinkModal] = useState(false);
const [viewMode, setViewMode] = useState<string>(
localStorage.getItem("viewMode") || ViewMode.Card
);
const [viewMode, setViewMode] = useState<string>(
localStorage.getItem("viewMode") || ViewMode.Card
);
const linkView = {
[ViewMode.Card]: CardView,
// [ViewMode.Grid]: GridView,
[ViewMode.List]: ListView,
};
const linkView = {
[ViewMode.Card]: CardView,
// [ViewMode.Grid]: GridView,
[ViewMode.List]: ListView,
};
// @ts-ignore
const LinkComponent = linkView[viewMode];
// @ts-ignore
const LinkComponent = linkView[viewMode];
return (
<MainLayout>
<div style={{ flex: "1 1 auto" }} className="p-5 flex flex-col gap-5">
<div className="flex items-center justify-between">
<PageHeader
icon={"bi-house "}
title={"Dashboard"}
description={"A brief overview of your data"}
/>
<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)} />
return (
<MainLayout>
<div style={{ flex: "1 1 auto" }} className="p-5 flex flex-col gap-5">
<div className="flex items-center justify-between">
<PageHeader
icon={"bi-house "}
title={"Dashboard"}
description={"A brief overview of your data"}
/>
<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-neutral gap-2 cursor-pointer"
>
View All
<i className="bi-chevron-right text-sm"></i>
</Link>
</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"
style={{ flex: "0 1 auto" }}
className="flex flex-col 2xl:flex-row items-start 2xl:gap-2"
>
<p className="text-center text-2xl">
View Your Recently Added Links Here!
</p>
<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>
{links[0] ? (
<div className="w-full">
<LinkComponent links={links.slice(0, showLinks)} />
</div>
<ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60">
<li>
<label
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
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">
View Your Recently Added Links Here!
</p>
<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="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-black/75 dark:text-white/75 gap-2 cursor-pointer"
>
View All
<i className="bi-chevron-right text-sm "></i>
</Link>
</div>
<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
style={{ flex: "1 1 auto" }}
className="flex flex-col 2xl:flex-row items-start 2xl:gap-2"
>
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
<div className="w-full">
<div
className={`grid min-[1900px]:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5 w-full`}
>
{links
.filter((e) => e.pinnedBy && e.pinnedBy[0])
.map((e, i) => <LinkCard key={i} link={e} count={i} />)
.slice(0, showLinks)}
</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>
<ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60">
<li>
<label
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 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
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"
style={{ flex: "1 1 auto" }}
className="flex flex-col 2xl:flex-row items-start 2xl:gap-2"
>
<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>
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
<div className="w-full">
<div
className={`grid min-[1900px]:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5 w-full`}
>
{links
.filter((e) => e.pinnedBy && e.pinnedBy[0])
.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>
{newLinkModal ? (
<NewLinkModal onClose={() => setNewLinkModal(false)} />
) : undefined}
</MainLayout>
);
</div>
{newLinkModal ? (
<NewLinkModal onClose={() => setNewLinkModal(false)} />
) : undefined}
</MainLayout>
);
}
+96 -77
View File
@@ -1,106 +1,125 @@
import SettingsLayout from "@/layouts/SettingsLayout";
import { useState, useEffect } from "react";
import useAccountStore from "@/store/account";
import { AccountSettings } from "@/types/global";
import { toast } from "react-hot-toast";
import React from "react";
import useLocalSettingsStore from "@/store/localSettings";
export default function Appearance() {
const { updateSettings } = useLocalSettingsStore();
const submit = async () => {
const { updateSettings } = useLocalSettingsStore();
const { account, updateAccount } = useAccountStore();
const submit = async () => {
setSubmitLoader(true);
const load = toast.loading("Applying...");
const response = await updateAccount({
...user,
});
toast.dismiss(load);
if (response.ok) {
toast.success("Settings Applied!");
toast.success("Settings Applied!");
} 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>(
!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)
);
useEffect(() => {
if (!objectIsEmpty(account)) setUser({ ...account });
}, [account]);
function objectIsEmpty(obj: object) {
return Object.keys(obj).length === 0;
}
const handleThemeChange = (newThemePart: string, isColorTheme: boolean) => {
const currentTheme = localStorage.getItem("theme") || "default-light";
const [currentColorTheme, currentMode] = currentTheme.split('-');
const newTheme = isColorTheme ? `${newThemePart}-${currentMode}` : `${currentColorTheme}-${newThemePart}`;
useEffect(() => {
if (!objectIsEmpty(account)) setUser({ ...account });
}, [account]);
localStorage.setItem("theme", newTheme);
document.documentElement.setAttribute('data-theme', newTheme);
updateSettings({ theme: newTheme });
return (
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">Appearance</p>
// Update the theme state
setTheme(newTheme);
};
<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
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
onClick={submit}
loading={submitLoader}
label="Save"
className="mt-2 mx-auto lg:mx-0"
/> */}
</div>
</SettingsLayout>
);
<div>
<p className="mb-3">Select Color Theme</p>
<div className="grid grid-cols-4 gap-3">
{["default", "red", "green", "orange"].map((colorTheme) => (
<button
key={colorTheme}
onClick={() => handleThemeChange(colorTheme, true)}
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>
);
}
+22 -17
View File
@@ -18,34 +18,39 @@ const useLocalSettingsStore = create<LocalSettingsStore>((set) => ({
viewMode: "",
},
updateSettings: async (newSettings) => {
if (
newSettings.theme &&
newSettings.theme !== localStorage.getItem("theme")
) {
if (newSettings.theme) {
localStorage.setItem("theme", newSettings.theme);
const localTheme = localStorage.getItem("theme") || "";
document.querySelector("html")?.setAttribute("data-theme", localTheme);
document.documentElement.setAttribute('data-theme', newSettings.theme);
if (newSettings.theme.endsWith("-dark")) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
if (
newSettings.viewMode &&
newSettings.viewMode !== localStorage.getItem("viewMode")
) {
if (newSettings.viewMode) {
localStorage.setItem("viewMode", newSettings.viewMode);
// const localTheme = localStorage.getItem("viewMode") || "";
}
set((state) => ({ settings: { ...state.settings, ...newSettings } }));
},
setSettings: async () => {
if (!localStorage.getItem("theme")) {
localStorage.setItem("theme", "dark");
let theme = localStorage.getItem("theme");
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) => ({
settings: { ...state.settings, theme: localTheme },
+121 -18
View File
@@ -5,7 +5,7 @@ module.exports = {
daisyui: {
themes: [
{
light: {
"default-light": {
primary: "#0369a1",
secondary: "#0891b2",
accent: "#6d28d9",
@@ -21,7 +21,7 @@ module.exports = {
},
},
{
dark: {
"default-dark": {
primary: "#7dd3fc",
secondary: "#22d3ee",
accent: "#6d28d9",
@@ -36,21 +36,124 @@ module.exports = {
error: "#f1293c",
},
},
],
},
darkMode: ["class", '[data-theme="dark"]'],
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
// Red Light Theme
{
"red-light": {
primary: "#ef4444",
secondary: "#dc2626",
accent: "#6d28d9",
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
"./layouts/**/*.{js,ts,jsx,tsx}",
],
plugins: [
require("daisyui"),
plugin(({ addVariant }) => {
addVariant("dark", '&[data-theme="dark"]');
}),
],
};
"./layouts/**/*.{js,ts,jsx,tsx}",
],
plugins: [
require("daisyui"),
plugin(({ addVariant }) => {
addVariant("dark", '&[data-theme="dark"]');
}),
],
};