aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--apps/web/app/dashboard/admin/page.tsx11
-rw-r--r--apps/web/app/dashboard/bookmarks/layout.tsx3
-rw-r--r--apps/web/app/dashboard/layout.tsx2
-rw-r--r--apps/web/app/dashboard/lists/page.tsx3
-rw-r--r--apps/web/app/dashboard/search/page.tsx3
-rw-r--r--apps/web/app/dashboard/settings/page.tsx2
-rw-r--r--apps/web/app/dashboard/tags/page.tsx4
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkPreview.tsx11
-rw-r--r--apps/web/components/dashboard/bookmarks/Bookmarks.tsx3
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx2
-rw-r--r--apps/web/components/dashboard/bookmarks/EditorCard.tsx5
-rw-r--r--apps/web/components/dashboard/bookmarks/LinkCard.tsx2
-rw-r--r--apps/web/components/dashboard/bookmarks/TagsEditor.tsx26
-rw-r--r--apps/web/components/dashboard/bookmarks/TextCard.tsx2
-rw-r--r--apps/web/components/dashboard/lists/AllListsView.tsx2
-rw-r--r--apps/web/components/dashboard/settings/ApiKeySettings.tsx3
-rw-r--r--apps/web/components/dashboard/settings/ChangePassword.tsx3
-rw-r--r--apps/web/components/dashboard/sidebar/ModileSidebar.tsx2
-rw-r--r--apps/web/components/dashboard/sidebar/ModileSidebarItem.tsx4
-rw-r--r--apps/web/components/dashboard/sidebar/Sidebar.tsx4
-rw-r--r--apps/web/components/dashboard/sidebar/SidebarItem.tsx4
-rw-r--r--apps/web/components/dashboard/sidebar/SidebarProfileOptions.tsx29
-rw-r--r--apps/web/components/signin/CredentialsForm.tsx6
-rw-r--r--apps/web/components/theme-provider.tsx9
-rw-r--r--apps/web/lib/providers.tsx10
-rw-r--r--apps/web/package.json1
-rw-r--r--docs/docs/01-intro.md1
-rw-r--r--docs/docs/04-screenshots.md4
-rw-r--r--docs/static/img/screenshots/homepage-dark.pngbin0 -> 2905164 bytes
-rw-r--r--pnpm-lock.yaml15
31 files changed, 138 insertions, 39 deletions
diff --git a/README.md b/README.md
index f2582843..8ef91abc 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ A self-hostable bookmark-everything app with a touch of AI for the data hoarders
- ✨ AI-based (aka chatgpt) automatic tagging.
- 🔖 [Chrome plugin](https://chromewebstore.google.com/detail/hoarder/kgcjekpmcjjogibpjebkhaanilehneje) for quick bookmarking.
- 📱 An iOS app that's pending apple's review.
+- 🌙 Dark mode support.
- 💾 Self-hosting first.
- [Planned] Archiving the content for offline reading.
diff --git a/apps/web/app/dashboard/admin/page.tsx b/apps/web/app/dashboard/admin/page.tsx
index 7b4c3cdd..c44d3142 100644
--- a/apps/web/app/dashboard/admin/page.tsx
+++ b/apps/web/app/dashboard/admin/page.tsx
@@ -2,6 +2,7 @@
import { useRouter } from "next/navigation";
import { ActionButton } from "@/components/ui/action-button";
+import { Separator } from "@/components/ui/separator";
import LoadingSpinner from "@/components/ui/spinner";
import {
Table,
@@ -96,7 +97,7 @@ function ServerStatsSection() {
</TableRow>
</TableBody>
</Table>
- <hr />
+ <Separator />
<p className="text-xl">Background Jobs</p>
<Table className="w-1/2">
<TableBody>
@@ -190,13 +191,13 @@ export default function AdminPage() {
}
return (
- <div className="m-4 flex flex-col gap-5 rounded-md border bg-white p-4">
+ <div className="m-4 flex flex-col gap-5 rounded-md border bg-background p-4">
<p className="text-2xl">Admin</p>
- <hr />
+ <Separator />
<ServerStatsSection />
- <hr />
+ <Separator />
<UsersSection />
- <hr />
+ <Separator />
<ActionsSection />
</div>
);
diff --git a/apps/web/app/dashboard/bookmarks/layout.tsx b/apps/web/app/dashboard/bookmarks/layout.tsx
index 8691e822..a2356d23 100644
--- a/apps/web/app/dashboard/bookmarks/layout.tsx
+++ b/apps/web/app/dashboard/bookmarks/layout.tsx
@@ -1,6 +1,7 @@
import React from "react";
import TopNav from "@/components/dashboard/bookmarks/TopNav";
import UploadDropzone from "@/components/dashboard/UploadDropzone";
+import { Separator } from "@/components/ui/separator";
export default function BookmarksLayout({
children,
@@ -13,7 +14,7 @@ export default function BookmarksLayout({
<div>
<TopNav />
</div>
- <hr />
+ <Separator />
<div className="my-4 flex-1 pb-4">{children}</div>
</div>
</UploadDropzone>
diff --git a/apps/web/app/dashboard/layout.tsx b/apps/web/app/dashboard/layout.tsx
index b2d3806b..68c0bfbd 100644
--- a/apps/web/app/dashboard/layout.tsx
+++ b/apps/web/app/dashboard/layout.tsx
@@ -15,7 +15,7 @@ export default async function Dashboard({
<div className="hidden flex-none sm:flex">
<Sidebar />
</div>
- <main className="flex-1 bg-gray-100 sm:overflow-y-auto">
+ <main className="flex-1 bg-muted sm:overflow-y-auto">
{serverConfig.demoMode && <DemoModeBanner />}
<div className="block w-full sm:hidden">
<MobileSidebar />
diff --git a/apps/web/app/dashboard/lists/page.tsx b/apps/web/app/dashboard/lists/page.tsx
index a8c53eb6..d379b1bb 100644
--- a/apps/web/app/dashboard/lists/page.tsx
+++ b/apps/web/app/dashboard/lists/page.tsx
@@ -1,4 +1,5 @@
import AllListsView from "@/components/dashboard/lists/AllListsView";
+import { Separator } from "@/components/ui/separator";
import { api } from "@/server/api/client";
export default async function ListsPage() {
@@ -7,7 +8,7 @@ export default async function ListsPage() {
return (
<div className="container mt-4 flex flex-col gap-3">
<p className="text-2xl">📋 All Lists</p>
- <hr />
+ <Separator />
<AllListsView initialData={lists.lists} />
</div>
);
diff --git a/apps/web/app/dashboard/search/page.tsx b/apps/web/app/dashboard/search/page.tsx
index 26b984a7..f09041f3 100644
--- a/apps/web/app/dashboard/search/page.tsx
+++ b/apps/web/app/dashboard/search/page.tsx
@@ -3,6 +3,7 @@
import { Suspense, useRef } from "react";
import BookmarksGrid from "@/components/dashboard/bookmarks/BookmarksGrid";
import { SearchInput } from "@/components/dashboard/search/SearchInput";
+import { Separator } from "@/components/ui/separator";
import { useBookmarkSearch } from "@/lib/hooks/bookmark-search";
import Loading from "../bookmarks/loading";
@@ -16,7 +17,7 @@ function SearchComp() {
return (
<div className="container flex flex-col gap-3 p-4">
<SearchInput ref={inputRef} autoFocus={true} />
- <hr />
+ <Separator />
{data ? <BookmarksGrid bookmarks={data.bookmarks} /> : <Loading />}
</div>
);
diff --git a/apps/web/app/dashboard/settings/page.tsx b/apps/web/app/dashboard/settings/page.tsx
index 71712eb9..f75bc298 100644
--- a/apps/web/app/dashboard/settings/page.tsx
+++ b/apps/web/app/dashboard/settings/page.tsx
@@ -3,7 +3,7 @@ import { ChangePassword } from "@/components/dashboard/settings/ChangePassword";
export default async function Settings() {
return (
- <div className="m-4 flex flex-col space-y-2 rounded-md border bg-white p-4">
+ <div className="m-4 flex flex-col space-y-2 rounded-md border bg-background p-4">
<p className="text-2xl">Settings</p>
<ChangePassword />
<ApiKeySettings />
diff --git a/apps/web/app/dashboard/tags/page.tsx b/apps/web/app/dashboard/tags/page.tsx
index dec11527..ec09e34e 100644
--- a/apps/web/app/dashboard/tags/page.tsx
+++ b/apps/web/app/dashboard/tags/page.tsx
@@ -7,7 +7,7 @@ import { getServerAuthSession } from "@/server/auth";
function TagPill({ name, count }: { name: string; count: number }) {
return (
<Link
- className="flex gap-2 rounded-md border border-gray-200 bg-white px-2 py-1 text-foreground hover:bg-foreground hover:text-background"
+ className="flex gap-2 rounded-md border border-border bg-background px-2 py-1 text-foreground hover:bg-foreground hover:text-background"
href={`/dashboard/tags/${name}`}
>
{name} <Separator orientation="vertical" /> {count}
@@ -38,7 +38,7 @@ export default async function TagsPage() {
return (
<div className="container mt-2 space-y-3">
<span className="text-2xl">All Tags</span>
- <hr />
+ <Separator />
<div className="flex flex-wrap gap-3">{tagPill}</div>
</div>
);
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkPreview.tsx b/apps/web/components/dashboard/bookmarks/BookmarkPreview.tsx
index 2c393fe7..96e1c19b 100644
--- a/apps/web/components/dashboard/bookmarks/BookmarkPreview.tsx
+++ b/apps/web/components/dashboard/bookmarks/BookmarkPreview.tsx
@@ -3,6 +3,7 @@
import Image from "next/image";
import Link from "next/link";
import { ScrollArea } from "@/components/ui/scroll-area";
+import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import {
Tooltip,
@@ -82,7 +83,7 @@ function LinkHeader({ bookmark }: { bookmark: ZBookmark }) {
<span className="my-auto">View Original</span>
<ExternalLink />
</Link>
- <hr />
+ <Separator />
</div>
);
}
@@ -103,7 +104,7 @@ function TextContentSection({ bookmark }: { bookmark: ZBookmark }) {
dangerouslySetInnerHTML={{
__html: bookmark.content.htmlContent || "",
}}
- className="prose mx-auto"
+ className="prose dark:prose-invert mx-auto"
/>
);
}
@@ -111,7 +112,9 @@ function TextContentSection({ bookmark }: { bookmark: ZBookmark }) {
}
case "text": {
content = (
- <Markdown className="prose mx-auto">{bookmark.content.text}</Markdown>
+ <Markdown className="prose dark:prose-invert mx-auto">
+ {bookmark.content.text}
+ </Markdown>
);
break;
}
@@ -195,7 +198,7 @@ export default function BookmarkPreview({
<div className="row-span-2 h-full w-full overflow-hidden p-2 md:col-span-2 lg:row-auto">
{isBookmarkStillCrawling(bookmark) ? <ContentLoading /> : content}
</div>
- <div className="lg:col-span1 row-span-1 flex flex-col gap-4 bg-gray-100 p-4 lg:row-auto">
+ <div className="lg:col-span1 row-span-1 flex flex-col gap-4 bg-accent p-4 lg:row-auto">
{linkHeader}
<CreationTime createdAt={bookmark.createdAt} />
<div className="flex gap-2">
diff --git a/apps/web/components/dashboard/bookmarks/Bookmarks.tsx b/apps/web/components/dashboard/bookmarks/Bookmarks.tsx
index cee620c9..81dd9361 100644
--- a/apps/web/components/dashboard/bookmarks/Bookmarks.tsx
+++ b/apps/web/components/dashboard/bookmarks/Bookmarks.tsx
@@ -1,4 +1,5 @@
import { redirect } from "next/navigation";
+import { Separator } from "@/components/ui/separator";
import { api } from "@/server/api/client";
import { getServerAuthSession } from "@/server/auth";
@@ -27,7 +28,7 @@ export default async function Bookmarks({
return (
<div className="container flex flex-col gap-3">
{header}
- {showDivider && <hr />}
+ {showDivider && <Separator />}
<UpdatableBookmarksGrid
query={query}
bookmarks={bookmarks}
diff --git a/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx b/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx
index 39638553..048dab85 100644
--- a/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx
+++ b/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx
@@ -16,7 +16,7 @@ import TextCard from "./TextCard";
function BookmarkCard({ children }: { children: React.ReactNode }) {
return (
- <Slot className="border-grey-100 mb-4 border bg-gray-50 duration-300 ease-in hover:shadow-lg hover:transition-all">
+ <Slot className="mb-4 border border-border bg-card duration-300 ease-in hover:shadow-lg hover:transition-all">
{children}
</Slot>
);
diff --git a/apps/web/components/dashboard/bookmarks/EditorCard.tsx b/apps/web/components/dashboard/bookmarks/EditorCard.tsx
index dfcf1f6c..d8a3c117 100644
--- a/apps/web/components/dashboard/bookmarks/EditorCard.tsx
+++ b/apps/web/components/dashboard/bookmarks/EditorCard.tsx
@@ -1,6 +1,7 @@
import type { SubmitErrorHandler, SubmitHandler } from "react-hook-form";
import { ActionButton } from "@/components/ui/action-button";
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
+import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea";
import { toast } from "@/components/ui/use-toast";
import { api } from "@/lib/trpc";
@@ -55,12 +56,12 @@ export default function EditorCard({ className }: { className?: string }) {
<form
className={cn(
className,
- "flex h-96 flex-col gap-2 rounded-xl bg-white p-4",
+ "flex h-96 flex-col gap-2 rounded-xl bg-card p-4",
)}
onSubmit={form.handleSubmit(onSubmit, onError)}
>
<p className="text-sm">NEW ITEM</p>
- <hr />
+ <Separator />
<FormField
control={form.control}
name="text"
diff --git a/apps/web/components/dashboard/bookmarks/LinkCard.tsx b/apps/web/components/dashboard/bookmarks/LinkCard.tsx
index 3451013d..9796ed4f 100644
--- a/apps/web/components/dashboard/bookmarks/LinkCard.tsx
+++ b/apps/web/components/dashboard/bookmarks/LinkCard.tsx
@@ -84,7 +84,7 @@ export default function LinkCard({
<div className="mt-1 flex justify-between text-gray-500">
<div className="my-auto">
<Link
- className="line-clamp-1 hover:text-black"
+ className="line-clamp-1 hover:text-foreground"
href={link.url}
target="_blank"
>
diff --git a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx
index bebd53df..e11410b8 100644
--- a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx
+++ b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx
@@ -99,6 +99,17 @@ export function TagsEditor({ bookmark }: { bookmark: ZBookmark }) {
closeMenuOnSelect={false}
isClearable={false}
isLoading={isExistingTagsLoading || isMutating}
+ theme={(theme) => ({
+ ...theme,
+ // This color scheme doesn't support disabled options.
+ colors: {
+ ...theme.colors,
+ primary: "hsl(var(--accent))",
+ primary50: "hsl(var(--accent))",
+ primary75: "hsl(var(--accent))",
+ primary25: "hsl(var(--accent))",
+ },
+ })}
styles={{
multiValueRemove: () => ({
"background-color": "transparent",
@@ -110,6 +121,14 @@ export function TagsEditor({ bookmark }: { bookmark: ZBookmark }) {
overflowY: "auto",
scrollbarWidth: "none",
}),
+ control: (styles) => ({
+ ...styles,
+ "background-color": "hsl(var(--background))",
+ "border-color": "hsl(var(--border))",
+ ":hover": {
+ "border-color": "hsl(var(--border))",
+ },
+ }),
}}
components={{
MultiValueContainer: ({ children, data }) => (
@@ -118,7 +137,7 @@ export function TagsEditor({ bookmark }: { bookmark: ZBookmark }) {
"flex min-h-8 space-x-1 rounded px-2",
(data as { attachedBy: string }).attachedBy == "ai"
? "bg-gradient-to-tr from-purple-500 to-purple-400 text-white"
- : "bg-gray-200",
+ : "bg-accent",
)}
>
{children}
@@ -137,8 +156,9 @@ export function TagsEditor({ bookmark }: { bookmark: ZBookmark }) {
}}
classNames={{
multiValueRemove: () => "my-auto",
- valueContainer: () => "gap-2",
- menuList: () => "text-sm",
+ valueContainer: () => "gap-2 bg-background",
+ menuList: () => "text-sm bg-background",
+ option: () => "text-red-500",
}}
/>
);
diff --git a/apps/web/components/dashboard/bookmarks/TextCard.tsx b/apps/web/components/dashboard/bookmarks/TextCard.tsx
index 75733063..82664390 100644
--- a/apps/web/components/dashboard/bookmarks/TextCard.tsx
+++ b/apps/web/components/dashboard/bookmarks/TextCard.tsx
@@ -58,7 +58,7 @@ export default function TextCard({
),
)}
>
- <Markdown className="prose grow overflow-hidden">
+ <Markdown className="prose dark:prose-invert grow overflow-hidden">
{bookmarkedText.text}
</Markdown>
<div className="mt-4 flex flex-none flex-wrap gap-1 overflow-hidden">
diff --git a/apps/web/components/dashboard/lists/AllListsView.tsx b/apps/web/components/dashboard/lists/AllListsView.tsx
index acb94edb..4159bc14 100644
--- a/apps/web/components/dashboard/lists/AllListsView.tsx
+++ b/apps/web/components/dashboard/lists/AllListsView.tsx
@@ -20,7 +20,7 @@ function ListItem({
}) {
return (
<Link href={path}>
- <div className="rounded-md border border-gray-200 bg-background px-4 py-2 text-lg">
+ <div className="rounded-md border border-border bg-background px-4 py-2 text-lg">
<p className="text-nowrap">
{icon} {name}
</p>
diff --git a/apps/web/components/dashboard/settings/ApiKeySettings.tsx b/apps/web/components/dashboard/settings/ApiKeySettings.tsx
index a3680863..1455e1b6 100644
--- a/apps/web/components/dashboard/settings/ApiKeySettings.tsx
+++ b/apps/web/components/dashboard/settings/ApiKeySettings.tsx
@@ -1,3 +1,4 @@
+import { Separator } from "@/components/ui/separator";
import {
Table,
TableBody,
@@ -16,7 +17,7 @@ export default async function ApiKeys() {
return (
<div className="pt-4">
<span className="text-xl">API Keys</span>
- <hr className="my-2" />
+ <Separator className="my-2" />
<div className="flex flex-col space-y-3">
<div className="flex flex-1 justify-end">
<AddApiKey />
diff --git a/apps/web/components/dashboard/settings/ChangePassword.tsx b/apps/web/components/dashboard/settings/ChangePassword.tsx
index d976f3e4..a2ca0870 100644
--- a/apps/web/components/dashboard/settings/ChangePassword.tsx
+++ b/apps/web/components/dashboard/settings/ChangePassword.tsx
@@ -11,6 +11,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
+import { Separator } from "@/components/ui/separator";
import { toast } from "@/components/ui/use-toast";
import { api } from "@/lib/trpc";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -55,7 +56,7 @@ export function ChangePassword() {
return (
<div className="w-full pt-4">
<span className="text-xl">Change Password</span>
- <hr className="my-2" />
+ <Separator className="my-2" />
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
diff --git a/apps/web/components/dashboard/sidebar/ModileSidebar.tsx b/apps/web/components/dashboard/sidebar/ModileSidebar.tsx
index 3c68433a..7306308d 100644
--- a/apps/web/components/dashboard/sidebar/ModileSidebar.tsx
+++ b/apps/web/components/dashboard/sidebar/ModileSidebar.tsx
@@ -12,7 +12,7 @@ import SidebarProfileOptions from "./SidebarProfileOptions";
export default async function MobileSidebar() {
return (
<aside className="w-full">
- <ul className="flex justify-between space-x-2 border-b-black bg-gray-100 px-5 py-2 pt-5">
+ <ul className="flex justify-between space-x-2 border-b-black px-5 py-2 pt-5">
<MobileSidebarItem logo={<PackageOpen />} path="/dashboard/bookmarks" />
<MobileSidebarItem logo={<Search />} path="/dashboard/search" />
<MobileSidebarItem logo={<ClipboardList />} path="/dashboard/lists" />
diff --git a/apps/web/components/dashboard/sidebar/ModileSidebarItem.tsx b/apps/web/components/dashboard/sidebar/ModileSidebarItem.tsx
index d2b4aad3..3382f47b 100644
--- a/apps/web/components/dashboard/sidebar/ModileSidebarItem.tsx
+++ b/apps/web/components/dashboard/sidebar/ModileSidebarItem.tsx
@@ -15,8 +15,8 @@ export default function MobileSidebarItem({
return (
<li
className={cn(
- "flex w-full rounded-lg hover:bg-gray-50",
- path == currentPath ? "bg-gray-50" : "",
+ "flex w-full rounded-lg hover:bg-background",
+ path == currentPath ? "bg-background" : "",
)}
>
<Link href={path} className="mx-auto px-3 py-2">
diff --git a/apps/web/components/dashboard/sidebar/Sidebar.tsx b/apps/web/components/dashboard/sidebar/Sidebar.tsx
index 0351b889..1c18e90c 100644
--- a/apps/web/components/dashboard/sidebar/Sidebar.tsx
+++ b/apps/web/components/dashboard/sidebar/Sidebar.tsx
@@ -22,12 +22,12 @@ export default async function Sidebar() {
return (
<aside className="flex h-screen w-60 flex-col gap-5 border-r p-4">
<Link href={"/dashboard/bookmarks"}>
- <div className="flex items-center rounded-lg px-1 text-slate-900">
+ <div className="flex items-center rounded-lg px-1 text-foreground">
<PackageOpen />
<span className="ml-2 text-base font-semibold">Hoarder</span>
</div>
</Link>
- <hr />
+ <Separator />
<div>
<ul className="space-y-2 text-sm font-medium">
<SidebarItem
diff --git a/apps/web/components/dashboard/sidebar/SidebarItem.tsx b/apps/web/components/dashboard/sidebar/SidebarItem.tsx
index 75a1f6ba..7e5eb3bd 100644
--- a/apps/web/components/dashboard/sidebar/SidebarItem.tsx
+++ b/apps/web/components/dashboard/sidebar/SidebarItem.tsx
@@ -19,8 +19,8 @@ export default function SidebarItem({
return (
<li
className={cn(
- "rounded-lg px-3 py-2 hover:bg-slate-100",
- path == currentPath ? "bg-gray-50" : "",
+ "rounded-lg px-3 py-2 hover:bg-accent",
+ path == currentPath ? "bg-accent/50" : "",
className,
)}
>
diff --git a/apps/web/components/dashboard/sidebar/SidebarProfileOptions.tsx b/apps/web/components/dashboard/sidebar/SidebarProfileOptions.tsx
index f931b63e..bf56b805 100644
--- a/apps/web/components/dashboard/sidebar/SidebarProfileOptions.tsx
+++ b/apps/web/components/dashboard/sidebar/SidebarProfileOptions.tsx
@@ -7,8 +7,32 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
-import { LogOut, MoreHorizontal } from "lucide-react";
+import { Slot } from "@radix-ui/react-slot";
+import { LogOut, Moon, MoreHorizontal, Sun } from "lucide-react";
import { signOut } from "next-auth/react";
+import { useTheme } from "next-themes";
+
+function DarkModeToggle() {
+ const { theme, setTheme } = useTheme();
+
+ let comp;
+ if (theme == "dark") {
+ comp = (
+ <button onClick={() => setTheme("light")}>
+ <Sun className="size-4" />
+ <p>Light Mode</p>
+ </button>
+ );
+ } else {
+ comp = (
+ <button onClick={() => setTheme("dark")}>
+ <Moon className="size-4" />
+ <p>Dark Mode</p>
+ </button>
+ );
+ }
+ return <Slot className="flex flex-row gap-2">{comp}</Slot>;
+}
export default function SidebarProfileOptions() {
return (
@@ -19,6 +43,9 @@ export default function SidebarProfileOptions() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-fit">
+ <DropdownMenuItem>
+ <DarkModeToggle />
+ </DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
signOut({
diff --git a/apps/web/components/signin/CredentialsForm.tsx b/apps/web/components/signin/CredentialsForm.tsx
index 5e3b4de9..8d7136cb 100644
--- a/apps/web/components/signin/CredentialsForm.tsx
+++ b/apps/web/components/signin/CredentialsForm.tsx
@@ -53,7 +53,7 @@ function SignIn() {
>
<div className="flex w-full flex-col space-y-2">
{signinError && (
- <p className="w-full text-center text-red-500">
+ <p className="w-full text-center text-destructive">
Incorrect username or password
</p>
)}
@@ -137,7 +137,9 @@ function SignUp() {
>
<div className="flex w-full flex-col space-y-2">
{errorMessage && (
- <p className="w-full text-center text-red-500">{errorMessage}</p>
+ <p className="w-full text-center text-destructive">
+ {errorMessage}
+ </p>
)}
<FormField
control={form.control}
diff --git a/apps/web/components/theme-provider.tsx b/apps/web/components/theme-provider.tsx
new file mode 100644
index 00000000..8efcd93b
--- /dev/null
+++ b/apps/web/components/theme-provider.tsx
@@ -0,0 +1,9 @@
+"use client";
+
+import type { ThemeProviderProps } from "next-themes/dist/types";
+import * as React from "react";
+import { ThemeProvider as NextThemesProvider } from "next-themes";
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
+}
diff --git a/apps/web/lib/providers.tsx b/apps/web/lib/providers.tsx
index ce667f8d..b700d0c1 100644
--- a/apps/web/lib/providers.tsx
+++ b/apps/web/lib/providers.tsx
@@ -2,6 +2,7 @@
import type { Session } from "next-auth";
import React, { useState } from "react";
+import { ThemeProvider } from "@/components/theme-provider";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink, loggerLink } from "@trpc/client";
import { SessionProvider } from "next-auth/react";
@@ -73,7 +74,14 @@ export default function Providers({
<SessionProvider session={session}>
<api.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
- {children}
+ <ThemeProvider
+ attribute="class"
+ defaultTheme="system"
+ enableSystem
+ disableTransitionOnChange
+ >
+ {children}
+ </ThemeProvider>
</QueryClientProvider>
</api.Provider>
</SessionProvider>
diff --git a/apps/web/package.json b/apps/web/package.json
index aa38b882..a81079f5 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -47,6 +47,7 @@
"next": "14.1.4",
"next-auth": "^4.24.5",
"next-pwa": "^5.6.0",
+ "next-themes": "^0.3.0",
"prettier": "^3.2.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
diff --git a/docs/docs/01-intro.md b/docs/docs/01-intro.md
index 4aae4b79..e5eac1dc 100644
--- a/docs/docs/01-intro.md
+++ b/docs/docs/01-intro.md
@@ -18,6 +18,7 @@ Hoarder is an open source "Bookmark Everything" app that uses AI for automatical
- ✨ AI-based (aka chatgpt) automatic tagging.
- 🔖 [Chrome plugin](https://chromewebstore.google.com/detail/hoarder/kgcjekpmcjjogibpjebkhaanilehneje) for quick bookmarking.
- 📱 An iOS app that's pending apple's review.
+- 🌙 Dark mode support.
- 💾 Self-hosting first.
- [Planned] Archiving the content for offline reading.
diff --git a/docs/docs/04-screenshots.md b/docs/docs/04-screenshots.md
index 03137367..4c211276 100644
--- a/docs/docs/04-screenshots.md
+++ b/docs/docs/04-screenshots.md
@@ -4,6 +4,10 @@
![Homepage](/img/screenshots/homepage.png)
+## Homepage (Dark Mode)
+
+![Homepage](/img/screenshots/homepage-dark.png)
+
## Tags
![All Tags](/img/screenshots/all-tags.png)
diff --git a/docs/static/img/screenshots/homepage-dark.png b/docs/static/img/screenshots/homepage-dark.png
new file mode 100644
index 00000000..41279c84
--- /dev/null
+++ b/docs/static/img/screenshots/homepage-dark.png
Binary files differ
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b1ed0ff4..6b87e252 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -430,6 +430,9 @@ importers:
next-pwa:
specifier: ^5.6.0
version: 5.6.0(@babel/core@7.24.0)(next@14.1.4(@babel/core@7.24.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(webpack@5.90.3)
+ next-themes:
+ specifier: ^0.3.0
+ version: 0.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
prettier:
specifier: ^3.2.5
version: 3.2.5
@@ -8466,6 +8469,12 @@ packages:
peerDependencies:
next: '>=9.0.0'
+ next-themes@0.3.0:
+ resolution: {integrity: sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==}
+ peerDependencies:
+ react: ^16.8 || ^17 || ^18
+ react-dom: ^16.8 || ^17 || ^18
+
next-tick@1.1.0:
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
@@ -23692,6 +23701,12 @@ snapshots:
- webpack
dev: false
+ next-themes@0.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
+ dependencies:
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: false
+
next-tick@1.1.0:
dev: true