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
16 changed files with 628 additions and 479 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
NEXTAUTH_SECRET=very_sensitive_secret NEXTAUTH_SECRET=very_sensitive_secret
NEXTAUTH_URL=http://localhost:3000/api/v1/auth NEXTAUTH_URL=http://localhost:3000
# Manual installation database settings # Manual installation database settings
DATABASE_URL=postgresql://user:password@localhost:5432/linkwarden DATABASE_URL=postgresql://user:password@localhost:5432/linkwarden
+1 -1
View File
@@ -9,7 +9,7 @@ WORKDIR /data
COPY ./package.json ./yarn.lock ./playwright.config.ts ./ COPY ./package.json ./yarn.lock ./playwright.config.ts ./
# Increase timeout to pass github actions arm64 build # Increase timeout to pass github actions arm64 build
RUN --mount=type=cache,sharing=locked,target=/usr/local/share/.cache/yarn yarn install --network-timeout 10000000 RUN yarn install --network-timeout 10000000
RUN npx playwright install-deps && \ RUN npx playwright install-deps && \
apt-get clean && \ apt-get clean && \
+1 -3
View File
@@ -17,9 +17,7 @@
## Intro & motivation ## Intro & motivation
**Linkwarden is a self-hosted, open-source collaborative bookmark manager to collect, organize and archive webpages.** **Linkwarden is a self-hosted, open-source collaborative bookmark manager to collect, organize and archive webpages.** The objective is to organize useful webpages and articles you find across the web in one place, and since useful webpages can go away (see the inevitability of [Link Rot](https://www.howtogeek.com/786227/what-is-link-rot-and-how-does-it-threaten-the-web/)), Linkwarden also saves a copy of each webpage as a Screenshot and PDF, ensuring accessibility even if the original content is no longer available.
The objective is to organize useful webpages and articles you find across the web in one place, and since useful webpages can go away (see the inevitability of [Link Rot](https://www.howtogeek.com/786227/what-is-link-rot-and-how-does-it-threaten-the-web/)), Linkwarden also saves a copy of each webpage as a Screenshot and PDF, ensuring accessibility even if the original content is no longer available.
Additionally, Linkwarden is designed with collaboration in mind, sharing links with the public and/or allowing multiple users to work together seamlessly. Additionally, Linkwarden is designed with collaboration in mind, sharing links with the public and/or allowing multiple users to work together seamlessly.
@@ -69,13 +69,11 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
const isReady = () => { const isReady = () => {
return ( return (
collectionOwner.archiveAsScreenshot ===
(link && link.pdf && link.pdf !== "pending") &&
collectionOwner.archiveAsPDF ===
(link && link.pdf && link.pdf !== "pending") &&
link && link &&
(collectionOwner.archiveAsScreenshot === true
? link.pdf && link.pdf !== "pending"
: true) &&
(collectionOwner.archiveAsPDF === true
? link.pdf && link.pdf !== "pending"
: true) &&
link.readable && link.readable &&
link.readable !== "pending" link.readable !== "pending"
); );
+15 -15
View File
@@ -26,12 +26,12 @@ 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);
@@ -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
+1 -1
View File
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
export default function SettingsSidebar({ className }: { className?: string }) { export default function SettingsSidebar({ className }: { className?: string }) {
const LINKWARDEN_VERSION = "v2.4.9"; const LINKWARDEN_VERSION = "v2.4.7";
const { collections } = useCollectionStore(); const { collections } = useCollectionStore();
+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>
);
} }
+1 -2
View File
@@ -43,8 +43,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
? await validateUrlSize(link.url) ? await validateUrlSize(link.url)
: undefined; : undefined;
if (validatedUrl === null) if (validatedUrl === null) throw "File is too large to be stored.";
throw "Something went wrong while retrieving the file size.";
const contentType = validatedUrl?.get("content-type"); const contentType = validatedUrl?.get("content-type");
let linkType = "url"; let linkType = "url";
+6
View File
@@ -67,6 +67,12 @@ export default async function postLink(
const validatedUrl = link.url ? await validateUrlSize(link.url) : undefined; const validatedUrl = link.url ? await validateUrlSize(link.url) : undefined;
if (validatedUrl === null)
return {
response: "Something went wrong while retrieving the file size.",
status: 400,
};
const contentType = validatedUrl?.get("content-type"); const contentType = validatedUrl?.get("content-type");
let linkType = "url"; let linkType = "url";
let imageExtension = "png"; let imageExtension = "png";
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "linkwarden", "name": "linkwarden",
"version": "2.4.9", "version": "2.4.7",
"main": "index.js", "main": "index.js",
"repository": "https://github.com/linkwarden/linkwarden.git", "repository": "https://github.com/linkwarden/linkwarden.git",
"author": "Daniel31X13 <daniel31x13@gmail.com>", "author": "Daniel31X13 <daniel31x13@gmail.com>",
+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>
);
} }
+96 -77
View File
@@ -1,106 +1,125 @@
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({
...user, ...user,
}); });
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>
);
} }
+22 -17
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.querySelector("html")?.setAttribute("data-theme", localTheme); document.documentElement.classList.add("dark");
} 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"]');
}), }),
], ],
}; };