💄 feat: Support dark mode.

This commit is contained in:
jarvis2f 2025-01-12 18:50:11 +08:00
parent ef5a2d60d4
commit bf3162a9e9
21 changed files with 376 additions and 90 deletions

142
web/package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "telegram-files-web", "name": "telegram-files-web",
"version": "0.1.6", "version": "0.1.9",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "telegram-files-web", "name": "telegram-files-web",
"version": "0.1.6", "version": "0.1.9",
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.2.0", "@dnd-kit/core": "^6.2.0",
"@dnd-kit/modifiers": "^8.0.0", "@dnd-kit/modifiers": "^8.0.0",
@ -25,6 +25,7 @@
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-toggle": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.4", "@radix-ui/react-tooltip": "^1.1.4",
"@t3-oss/env-nextjs": "^0.10.1", "@t3-oss/env-nextjs": "^0.10.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@ -38,6 +39,7 @@
"lucide-react": "^0.462.0", "lucide-react": "^0.462.0",
"motion": "^11.15.0", "motion": "^11.15.0",
"next": "^15.0.1", "next": "^15.0.1",
"next-themes": "^0.4.4",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -2160,6 +2162,88 @@
} }
} }
}, },
"node_modules/@radix-ui/react-toggle": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.1.tgz",
"integrity": "sha512-i77tcgObYr743IonC1hrsnnPmszDRn8p+EGUsUt+5a/JFn28fxaM88Py6V2mc8J5kELMWishI0rLnuGLFD/nnQ==",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-controllable-state": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
"dependencies": {
"@radix-ui/react-slot": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip": { "node_modules/@radix-ui/react-tooltip": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.4.tgz",
@ -5967,6 +6051,15 @@
} }
} }
}, },
"node_modules/next-themes": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz",
"integrity": "sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/next/node_modules/postcss": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@ -9127,6 +9220,45 @@
"@radix-ui/react-visually-hidden": "1.1.0" "@radix-ui/react-visually-hidden": "1.1.0"
} }
}, },
"@radix-ui/react-toggle": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.1.tgz",
"integrity": "sha512-i77tcgObYr743IonC1hrsnnPmszDRn8p+EGUsUt+5a/JFn28fxaM88Py6V2mc8J5kELMWishI0rLnuGLFD/nnQ==",
"requires": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-controllable-state": "1.1.0"
},
"dependencies": {
"@radix-ui/primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="
},
"@radix-ui/react-compose-refs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
"requires": {}
},
"@radix-ui/react-primitive": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
"requires": {
"@radix-ui/react-slot": "1.1.1"
}
},
"@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"requires": {
"@radix-ui/react-compose-refs": "1.1.1"
}
}
}
},
"@radix-ui/react-tooltip": { "@radix-ui/react-tooltip": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.4.tgz",
@ -11725,6 +11857,12 @@
} }
} }
}, },
"next-themes": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz",
"integrity": "sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==",
"requires": {}
},
"normalize-path": { "normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",

View file

@ -33,6 +33,7 @@
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-toggle": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.4", "@radix-ui/react-tooltip": "^1.1.4",
"@t3-oss/env-nextjs": "^0.10.1", "@t3-oss/env-nextjs": "^0.10.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@ -46,6 +47,7 @@
"lucide-react": "^0.462.0", "lucide-react": "^0.462.0",
"motion": "^11.15.0", "motion": "^11.15.0",
"next": "^15.0.1", "next": "^15.0.1",
"next-themes": "^0.4.4",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",

View file

@ -8,6 +8,7 @@ import { SettingsProvider } from "@/hooks/use-settings";
import { WebSocketProvider } from "@/hooks/use-websocket"; import { WebSocketProvider } from "@/hooks/use-websocket";
import { env } from "@/env"; import { env } from "@/env";
import { TelegramAccountProvider } from "@/hooks/use-telegram-account"; import { TelegramAccountProvider } from "@/hooks/use-telegram-account";
import {ThemeProvider} from "@/components/theme-provider";
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
@ -22,7 +23,7 @@ export default async function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<html lang="en"> <html lang="en" suppressHydrationWarning>
<head> <head>
<link <link
rel="icon" rel="icon"
@ -47,14 +48,21 @@ export default async function RootLayout({
)} )}
</head> </head>
<body className={inter.className}> <body className={inter.className}>
<SWRProvider> <ThemeProvider
<WebSocketProvider> attribute="class"
<SettingsProvider> defaultTheme="system"
<TelegramAccountProvider>{children}</TelegramAccountProvider> enableSystem
</SettingsProvider> disableTransitionOnChange
</WebSocketProvider> >
</SWRProvider> <SWRProvider>
<Toaster /> <WebSocketProvider>
<SettingsProvider>
<TelegramAccountProvider>{children}</TelegramAccountProvider>
</SettingsProvider>
</WebSocketProvider>
</SWRProvider>
<Toaster />
</ThemeProvider>
</body> </body>
</html> </html>
); );

View file

@ -79,7 +79,9 @@ export default function About() {
<span>Loading...</span> <span>Loading...</span>
</div> </div>
) : ( ) : (
<p className="rounded bg-gray-100 px-3">{currentVersion}</p> <p className="rounded bg-gray-100 px-3 dark:bg-gray-800">
{currentVersion}
</p>
)} )}
</div> </div>
@ -95,7 +97,7 @@ export default function About() {
<span>Loading...</span> <span>Loading...</span>
</div> </div>
) : ( ) : (
<p className="rounded bg-gray-100 px-3"> <p className="rounded bg-gray-100 px-3 dark:bg-gray-800">
{githubData.tag_name} {githubData.tag_name}
</p> </p>
)} )}

View file

@ -3,7 +3,7 @@ import useSWRMutation from "swr/mutation";
import { request } from "@/lib/api"; import { request } from "@/lib/api";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { useSWRConfig } from "swr"; import { useSWRConfig } from "swr";
import { type FormEvent, useCallback, useEffect, useState } from "react"; import React, { type FormEvent, useCallback, useEffect, useState } from "react";
import { useDebounce } from "use-debounce"; import { useDebounce } from "use-debounce";
import { Ellipsis, LoaderCircle } from "lucide-react"; import { Ellipsis, LoaderCircle } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -22,6 +22,8 @@ import {
} from "@/components/ui/input-otp"; } from "@/components/ui/input-otp";
import { useWebsocket } from "@/hooks/use-websocket"; import { useWebsocket } from "@/hooks/use-websocket";
import { useTelegramAccount } from "@/hooks/use-telegram-account"; import { useTelegramAccount } from "@/hooks/use-telegram-account";
import TGDuck16Plane from "@/components/animations/tg-duck16_plane.json";
import dynamic from "next/dynamic";
interface AccountCreatorProps { interface AccountCreatorProps {
isAdd?: boolean; isAdd?: boolean;
@ -30,6 +32,8 @@ interface AccountCreatorProps {
onLoginSuccess?: () => void; onLoginSuccess?: () => void;
} }
const Lottie = dynamic(() => import("lottie-react"), { ssr: false });
export default function AccountCreator({ export default function AccountCreator({
isAdd, isAdd,
proxyName, proxyName,
@ -153,6 +157,11 @@ export default function AccountCreator({
if (!initSuccessfully) { if (!initSuccessfully) {
return ( return (
<div className="flex flex-col items-center justify-center space-y-4"> <div className="flex flex-col items-center justify-center space-y-4">
<Lottie
className="mb-10 h-28 w-28 md:mb-3"
animationData={TGDuck16Plane}
loop={true}
/>
<Button <Button
className={cn("w-full", debounceIsCreateMutating ? "opacity-50" : "")} className={cn("w-full", debounceIsCreateMutating ? "opacity-50" : "")}
disabled={debounceIsCreateMutating} disabled={debounceIsCreateMutating}

View file

@ -38,7 +38,7 @@ export function AccountDialog({
<DialogTrigger asChild>{children}</DialogTrigger> <DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent <DialogContent
aria-describedby={undefined} aria-describedby={undefined}
className="h-full w-full pb-10 md:h-auto md:min-h-40 md:min-w-[550px]" className="h-full w-full pb-16 md:h-auto md:min-h-40 md:min-w-[550px]"
> >
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
@ -62,7 +62,7 @@ export function AccountDialog({
proxyName={proxyName} proxyName={proxyName}
onProxyNameChange={setProxyName} onProxyNameChange={setProxyName}
enableSelect={true} enableSelect={true}
className="absolute bottom-1 right-1 md:-bottom-9 md:-right-4" className="absolute bottom-1 right-1 md:-bottom-14 md:-right-4"
/> />
</div> </div>
</DialogContent> </DialogContent>

File diff suppressed because one or more lines are too long

View file

@ -70,7 +70,7 @@ export default function AutoDownloadDialog() {
}); });
if (isLoading) { if (isLoading) {
return <div className="h-8 w-32 animate-pulse bg-gray-200"></div>; return <div className="h-8 w-32 animate-pulse bg-gray-200 dark:bg-gray-700"></div>;
} }
return ( return (
@ -151,17 +151,17 @@ export default function AutoDownloadDialog() {
</div> </div>
</div> </div>
)} )}
<div className="space-y-4 rounded-md border border-gray-200 bg-gray-50 p-4"> <div className="space-y-4 rounded-md border border-gray-200 bg-gray-50 p-4 dark:bg-gray-800 dark:border-gray-700">
<div className="flex items-start"> <div className="flex items-start">
<span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-yellow-400"></span> <span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-yellow-400"></span>
<p className="text-sm leading-6 text-gray-700"> <p className="text-sm leading-6 text-gray-700 dark:text-gray-300">
This will disable auto download for this chat. You can always This will disable auto download for this chat. You can always
enable it later. enable it later.
</p> </p>
</div> </div>
<div className="flex items-start"> <div className="flex items-start">
<span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-yellow-400"></span> <span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-yellow-400"></span>
<p className="text-sm leading-6 text-gray-700"> <p className="text-sm leading-6 text-gray-700 dark:text-gray-300">
Files that are being downloaded will be paused and you can Files that are being downloaded will be paused and you can
enable automatic downloading again later. enable automatic downloading again later.
</p> </p>
@ -170,26 +170,26 @@ export default function AutoDownloadDialog() {
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-4 rounded-md border border-gray-200 bg-gray-50 p-4"> <div className="space-y-4 rounded-md border border-gray-200 bg-gray-50 p-4 dark:bg-gray-800 dark:border-gray-700">
<div className="flex items-start"> <div className="flex items-start">
<span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span> <span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span>
<p className="text-sm leading-6 text-gray-700"> <p className="text-sm leading-6 text-gray-700 dark:text-gray-300">
This will enable auto download for this chat. Files will be This will enable auto download for this chat. Files will be
downloaded automatically. downloaded automatically.
</p> </p>
</div> </div>
<div className="flex items-start"> <div className="flex items-start">
<span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span> <span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span>
<p className="text-sm leading-6 text-gray-700"> <p className="text-sm leading-6 text-gray-700 dark:text-gray-300">
Files in historical messages will be downloaded first, and Files in historical messages will be downloaded first, and
then files in new messages will be downloaded automatically. then files in new messages will be downloaded automatically.
</p> </p>
</div> </div>
<div className="flex items-start"> <div className="flex items-start">
<span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span> <span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span>
<p className="text-sm leading-6 text-gray-700"> <p className="text-sm leading-6 text-gray-700 dark:text-gray-300">
Download Order: Download Order:
<span className="ml-1 rounded bg-blue-100 px-2"> <span className="ml-1 rounded bg-blue-100 px-2 dark:bg-blue-800 dark:text-blue-200 text-blue-700">
{"Photo -> Video -> Audio -> File"} {"Photo -> Video -> Audio -> File"}
</span> </span>
</p> </p>

View file

@ -68,8 +68,6 @@ export function EmptyState({
</Button> </Button>
</div> </div>
</AccountDialog> </AccountDialog>
<ProxysDialog />
</div> </div>
</div> </div>

View file

@ -33,7 +33,7 @@ function FileName({ file }: { file: TelegramFile }) {
<SpoiledWrapper hasSensitiveContent={file.hasSensitiveContent}> <SpoiledWrapper hasSensitiveContent={file.hasSensitiveContent}>
<p className="flex items-center gap-2"> <p className="flex items-center gap-2">
<Mountain className="h-4 w-4" /> <Mountain className="h-4 w-4" />
<span className="rounded px-1 text-sm hover:bg-gray-100"> <span className="rounded px-1 text-sm hover:bg-gray-100 dark:hover:bg-gray-800">
{file.fileName} {file.fileName}
</span> </span>
</p> </p>
@ -86,7 +86,7 @@ function FilePath({ file }: { file: TelegramFile }) {
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<FileCheck className="h-4 w-4 flex-shrink-0" /> <FileCheck className="h-4 w-4 flex-shrink-0" />
<p <p
className="group line-clamp-1 cursor-pointer overflow-hidden truncate text-ellipsis text-wrap rounded px-1 hover:bg-gray-100" className="group line-clamp-1 cursor-pointer overflow-hidden truncate text-ellipsis text-wrap rounded px-1 hover:bg-gray-100 dark:hover:bg-gray-800"
onClick={() => copyToClipboard(file.localPath)} onClick={() => copyToClipboard(file.localPath)}
> >
{file.localPath.split("/").pop()} {file.localPath.split("/").pop()}
@ -108,7 +108,7 @@ function FileTime({ file }: { file: TelegramFile }) {
<TooltipTrigger asChild> <TooltipTrigger asChild>
<p className="flex items-center gap-2"> <p className="flex items-center gap-2">
<Clock className="h-4 w-4" /> <Clock className="h-4 w-4" />
<span className="rounded px-1 text-sm text-muted-foreground hover:bg-gray-100"> <span className="rounded px-1 text-sm text-muted-foreground hover:bg-gray-100 dark:hover:bg-gray-800">
{formatDistanceToNow(new Date(file.date * 1000), { {formatDistanceToNow(new Date(file.date * 1000), {
addSuffix: true, addSuffix: true,
})} })}
@ -126,7 +126,7 @@ function FileTime({ file }: { file: TelegramFile }) {
<TooltipTrigger asChild> <TooltipTrigger asChild>
<p className="flex items-center gap-2"> <p className="flex items-center gap-2">
<ClockArrowDown className="h-4 w-4" /> <ClockArrowDown className="h-4 w-4" />
<span className="rounded px-1 text-sm text-muted-foreground hover:bg-gray-100"> <span className="rounded px-1 text-sm text-muted-foreground hover:bg-gray-100 dark:hover:bg-gray-800">
{formatDistanceToNow(new Date(file.completionDate), { {formatDistanceToNow(new Date(file.completionDate), {
addSuffix: true, addSuffix: true,
})} })}

View file

@ -280,7 +280,7 @@ export function FileList({ accountId, chatId }: FileListProps) {
</TableBody> </TableBody>
</Table> </Table>
{isLoading && ( {isLoading && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white bg-opacity-90"> <div className="absolute inset-0 z-10 flex items-center justify-center bg-accent bg-opacity-90">
<LoaderPinwheel <LoaderPinwheel
className="h-8 w-8 animate-spin" className="h-8 w-8 animate-spin"
style={{ strokeWidth: "0.8px" }} style={{ strokeWidth: "0.8px" }}

View file

@ -34,17 +34,17 @@ export default function FileNotFount() {
{/* Inner Circles */} {/* Inner Circles */}
<OrbitingCircles <OrbitingCircles
className="size-[30px] border-none bg-transparent" className="size-[30px] border-none bg-transparent dark:bg-transparent"
duration={20} duration={20}
delay={20} delay={20}
radius={80} radius={80}
> >
<div className="rounded bg-gray-100 p-2"> <div className="rounded bg-gray-100 p-2 dark:bg-gray-800">
<FolderClosed className="h-4 w-4 stroke-1" /> <FolderClosed className="h-4 w-4 stroke-1" />
</div> </div>
</OrbitingCircles> </OrbitingCircles>
<OrbitingCircles <OrbitingCircles
className="size-[30px] border-none bg-transparent" className="size-[30px] border-none bg-transparent dark:bg-transparent"
duration={20} duration={20}
delay={10} delay={10}
radius={80} radius={80}
@ -54,17 +54,17 @@ export default function FileNotFount() {
{/* Outer Circles (reverse) */} {/* Outer Circles (reverse) */}
<OrbitingCircles <OrbitingCircles
className="size-[100px] border-none bg-transparent" className="size-[100px] border-none bg-transparent dark:bg-transparent"
radius={120} radius={120}
duration={20} duration={20}
reverse reverse
> >
<div className="rounded bg-gray-100 p-2"> <div className="rounded bg-gray-100 p-2 dark:bg-gray-800">
<ImageIcon className="h-4 w-4 stroke-1" /> <ImageIcon className="h-4 w-4 stroke-1" />
</div> </div>
</OrbitingCircles> </OrbitingCircles>
<OrbitingCircles <OrbitingCircles
className="size-[120px] border-none bg-transparent" className="size-[120px] border-none bg-transparent dark:bg-transparent"
radius={160} radius={160}
duration={20} duration={20}
delay={20} delay={20}
@ -74,7 +74,7 @@ export default function FileNotFount() {
</OrbitingCircles> </OrbitingCircles>
<OrbitingCircles <OrbitingCircles
className="size-[160px] border-none bg-transparent" className="size-[160px] border-none bg-transparent dark:bg-transparent"
circleClassName="stroke-black/5 stroke-1 dark:stroke-white/5" circleClassName="stroke-black/5 stroke-1 dark:stroke-white/5"
radius={200} radius={200}
duration={20} duration={20}
@ -84,7 +84,7 @@ export default function FileNotFount() {
</OrbitingCircles> </OrbitingCircles>
<OrbitingCircles <OrbitingCircles
className="size-[160px] border-none bg-transparent" className="size-[160px] border-none bg-transparent dark:bg-transparent"
circleClassName="stroke-black/5 stroke-1 dark:stroke-white/5" circleClassName="stroke-black/5 stroke-1 dark:stroke-white/5"
radius={200} radius={200}
duration={20} duration={20}
@ -94,13 +94,13 @@ export default function FileNotFount() {
</OrbitingCircles> </OrbitingCircles>
<OrbitingCircles <OrbitingCircles
className="size-[200px] border-none bg-transparent" className="size-[200px] border-none bg-transparent dark:bg-transparent"
circleClassName="stroke-black/5 stroke-1 dark:stroke-white/5" circleClassName="stroke-black/5 stroke-1 dark:stroke-white/5"
radius={240} radius={240}
> >
</OrbitingCircles> </OrbitingCircles>
<OrbitingCircles <OrbitingCircles
className="size-[280px] border-none bg-transparent" className="size-[280px] border-none bg-transparent dark:bg-transparent"
circleClassName="stroke-black/5 stroke-1 dark:stroke-white/5" circleClassName="stroke-black/5 stroke-1 dark:stroke-white/5"
radius={280} radius={280}
></OrbitingCircles> ></OrbitingCircles>

View file

@ -77,7 +77,7 @@ const FileStatistics: React.FC<FileStatisticsProps> = ({ telegramId }) => {
// Render an error message if the API call fails // Render an error message if the API call fails
if (error) { if (error) {
return ( return (
<div className="flex items-center space-x-2 rounded-lg bg-white p-4 text-red-600 shadow-md"> <div className="flex items-center space-x-2 rounded-lg bg-white p-4 text-red-600 shadow-md dark:bg-red-50 dark:text-red-400">
<AlertTriangle className="h-5 w-5" /> <AlertTriangle className="h-5 w-5" />
<span>Failed to load data.</span> <span>Failed to load data.</span>
</div> </div>
@ -87,7 +87,7 @@ const FileStatistics: React.FC<FileStatisticsProps> = ({ telegramId }) => {
// Render a loading indicator while the data is being fetched // Render a loading indicator while the data is being fetched
if (!data) { if (!data) {
return ( return (
<div className="flex items-center space-x-2 rounded-lg bg-white p-4 text-gray-600 shadow-md"> <div className="flex items-center space-x-2 rounded-lg bg-white p-4 text-gray-600 shadow-md dark:bg-gray-50 dark:text-gray-400">
<LoaderPinwheel <LoaderPinwheel
className="h-5 w-5 animate-spin" className="h-5 w-5 animate-spin"
style={{ strokeWidth: "0.8px" }} style={{ strokeWidth: "0.8px" }}
@ -167,94 +167,109 @@ const FileStatistics: React.FC<FileStatisticsProps> = ({ telegramId }) => {
]; ];
return ( return (
<div className="space-y-6 rounded-lg bg-gray-50 p-2 md:p-6"> <div className="space-y-6 rounded-lg bg-gray-50 p-2 dark:bg-gray-800 md:p-6">
<div className="flex-1 rounded-lg bg-white p-4 shadow-md"> <div className="flex-1 rounded-lg bg-white p-4 shadow-md dark:bg-gray-900">
<div className="flex items-center space-x-3 border-gray-200"> <div className="flex items-center space-x-3 border-gray-200">
<h3 className="text-md flex items-center space-x-2 font-semibold text-gray-700"> <h3 className="text-md flex items-center space-x-2 font-semibold text-gray-700 dark:text-gray-200">
<CloudDownload className="h-5 w-5 text-blue-600" /> <CloudDownload className="h-5 w-5 text-blue-600" />
<span>Download Statistics</span> <span>Download Statistics</span>
</h3> </h3>
</div> </div>
<div className="mt-4 grid grid-cols-2 gap-4 md:grid-cols-5"> <div className="mt-4 grid grid-cols-2 gap-4 md:grid-cols-5">
<div className="rounded-lg bg-gray-50 p-4 shadow-sm"> <div className="rounded-lg bg-gray-50 p-4 shadow-sm dark:bg-gray-800">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<FileText className="h-5 w-5 text-gray-600" /> <FileText className="h-5 w-5 text-gray-600 dark:text-gray-400" />
<span className="text-sm text-gray-600">Total Files</span> <span className="text-sm text-gray-600 dark:text-gray-400">
Total Files
</span>
</div> </div>
<div className="mt-2 text-center text-lg font-semibold text-gray-800"> <div className="mt-2 text-center text-lg font-semibold text-gray-800 dark:text-gray-200">
{total} {total}
</div> </div>
</div> </div>
<div className="rounded-lg bg-gray-50 p-4 shadow-sm"> <div className="rounded-lg bg-gray-50 p-4 shadow-sm dark:bg-gray-800">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Download className="h-5 w-5 text-blue-600" /> <Download className="h-5 w-5 text-blue-600" />
<span className="text-sm text-gray-600">Downloading</span> <span className="text-sm text-gray-600 dark:text-gray-400">
Downloading
</span>
</div> </div>
<div className="mt-2 text-center text-lg font-semibold text-gray-800"> <div className="mt-2 text-center text-lg font-semibold text-gray-800 dark:text-gray-200">
{downloading} {downloading}
</div> </div>
</div> </div>
<div className="rounded-lg bg-gray-50 p-4 shadow-sm"> <div className="rounded-lg bg-gray-50 p-4 shadow-sm dark:bg-gray-800">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<PauseCircle className="h-5 w-5 text-yellow-500" /> <PauseCircle className="h-5 w-5 text-yellow-500" />
<span className="text-sm text-gray-600">Paused</span> <span className="text-sm text-gray-600 dark:text-gray-400">
Paused
</span>
</div> </div>
<div className="mt-2 text-center text-lg font-semibold text-gray-800"> <div className="mt-2 text-center text-lg font-semibold text-gray-800 dark:text-gray-200">
{paused} {paused}
</div> </div>
</div> </div>
<div className="rounded-lg bg-gray-50 p-4 shadow-sm"> <div className="rounded-lg bg-gray-50 p-4 shadow-sm dark:bg-gray-800">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<CheckCircle className="h-5 w-5 text-green-600" /> <CheckCircle className="h-5 w-5 text-green-600" />
<span className="text-sm text-gray-600">Completed</span> <span className="text-sm text-gray-600 dark:text-gray-400">
Completed
</span>
</div> </div>
<div className="mt-2 text-center text-lg font-semibold text-gray-800"> <div className="mt-2 text-center text-lg font-semibold text-gray-800 dark:text-gray-200">
{completed} {completed}
</div> </div>
</div> </div>
<div className="rounded-lg bg-gray-50 p-4 shadow-sm"> <div className="rounded-lg bg-gray-50 p-4 shadow-sm dark:bg-gray-800">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<AlertTriangle className="h-5 w-5 text-red-600" /> <AlertTriangle className="h-5 w-5 text-red-600" />
<span className="text-sm text-gray-600">Error</span> <span className="text-sm text-gray-600 dark:text-gray-400">
Error
</span>
</div> </div>
<div className="mt-2 text-center text-lg font-semibold text-gray-800"> <div className="mt-2 text-center text-lg font-semibold text-gray-800 dark:text-gray-200">
{errorCount} {errorCount}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="flex-1 rounded-lg bg-white p-4 shadow-md"> <div className="flex-1 rounded-lg bg-white p-4 shadow-md dark:bg-gray-900">
<div className="flex items-center space-x-3 border-gray-200"> <div className="flex items-center space-x-3 border-gray-200 dark:border-gray-700">
<h3 className="text-md flex items-center space-x-2 font-semibold text-gray-700"> <h3 className="text-md flex items-center space-x-2 font-semibold text-gray-700 dark:text-gray-200">
<Clock className="h-5 w-5 text-yellow-500" /> <Clock className="h-5 w-5 text-yellow-500" />
<span>Speed Statistics</span> <span>Speed Statistics</span>
<span className="text-sm text-gray-500 font-medium">({data.speedStats.interval / 60} minute interval)</span> <span className="text-sm font-medium text-gray-500 dark:text-gray-400">
({data.speedStats.interval / 60} minute interval)
</span>
</h3> </h3>
</div> </div>
<div className="mt-4 grid grid-cols-2 gap-4 md:grid-cols-4"> <div className="mt-4 grid grid-cols-2 gap-4 md:grid-cols-4">
{avgStatFields.map((stat, index) => ( {avgStatFields.map((stat, index) => (
<div <div
key={index} key={index}
className="flex flex-col space-y-3 rounded-xl border border-gray-100 p-4 transition-colors hover:border-gray-200" className="flex flex-col space-y-3 rounded-xl border border-gray-100 p-4 transition-colors hover:border-gray-200 dark:border-gray-700
dark:hover:border-gray-600"
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className={`rounded-lg p-2 ${stat.bgColor}`}> <div className={`rounded-lg p-2 ${stat.bgColor}`}>
<stat.icon className={`h-5 w-5 ${stat.color}`} /> <stat.icon className={`h-5 w-5 ${stat.color}`} />
</div> </div>
<span className="text-sm font-medium text-gray-500"> <span className="text-sm font-medium text-gray-500 dark:text-gray-400">
{stat.label} {stat.label}
</span> </span>
</div> </div>
<div className="text-lg font-semibold">{stat.value}</div> <div className="text-lg font-semibold text-gray-800 dark:text-gray-200">
{stat.value}
</div>
</div> </div>
))} ))}
</div> </div>
</div> </div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-3"> <div className="grid grid-cols-1 gap-6 md:grid-cols-3">
<div className="flex-1 rounded-lg bg-white p-4 shadow-md md:col-span-2"> <div className="flex-1 rounded-lg bg-white p-4 shadow-md dark:bg-gray-900 md:col-span-2">
<h3 className="text-md flex items-center space-x-2 font-semibold text-gray-700"> <h3 className="text-md flex items-center space-x-2 font-semibold text-gray-700 dark:text-gray-200">
<CheckCircle className="h-5 w-5 text-green-600" /> <CheckCircle className="h-5 w-5 text-green-600" />
<span>Completed by Type</span> <span>Completed by Type</span>
</h3> </h3>
@ -262,13 +277,15 @@ const FileStatistics: React.FC<FileStatisticsProps> = ({ telegramId }) => {
{completedTypes.map((type) => ( {completedTypes.map((type) => (
<li <li
key={type.label} key={type.label}
className="rounded-lg bg-gray-50 p-3 shadow-sm" className="rounded-lg bg-gray-50 p-3 shadow-sm dark:bg-gray-800"
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{type.icon} {type.icon}
<span className="text-sm text-gray-600">{type.label}</span> <span className="text-sm text-gray-600 dark:text-gray-400">
{type.label}
</span>
</div> </div>
<div className="mt-2 text-lg font-semibold text-gray-800"> <div className="mt-2 text-lg font-semibold text-gray-800 dark:text-gray-200">
{type.value} {type.value}
</div> </div>
</li> </li>
@ -276,21 +293,21 @@ const FileStatistics: React.FC<FileStatisticsProps> = ({ telegramId }) => {
</ul> </ul>
</div> </div>
<div className="flex-1 rounded-lg bg-white p-4 shadow-md"> <div className="flex-1 rounded-lg bg-white p-4 shadow-md dark:bg-gray-900">
<h3 className="text-md flex items-center space-x-2 font-semibold text-gray-700"> <h3 className="text-md flex items-center space-x-2 font-semibold text-gray-700 dark:text-gray-200">
<Network className="h-5 w-5 text-gray-500" /> <Network className="h-5 w-5 text-gray-500" />
<span>Network Statistics</span> <span>Network Statistics</span>
</h3> </h3>
<div className="mt-4 h-full"> <div className="mt-4 h-full">
<div className="flex items-center justify-center space-x-2 rounded-lg bg-gray-50 p-4 shadow-sm"> <div className="flex items-center justify-center space-x-2 rounded-lg bg-gray-50 p-4 shadow-sm dark:bg-gray-800">
<Upload className="h-5 w-5 text-yellow-500" /> <Upload className="h-5 w-5 text-yellow-500" />
<span className="text-lg font-semibold text-gray-800"> <span className="text-lg font-semibold text-gray-800 dark:text-gray-200">
{prettyBytes(data.networkStatistics.sentBytes)} {prettyBytes(data.networkStatistics.sentBytes)}
</span> </span>
</div> </div>
<div className="mt-4 flex items-center justify-center space-x-2 rounded-lg bg-gray-50 p-4 shadow-sm"> <div className="mt-4 flex items-center justify-center space-x-2 rounded-lg bg-gray-50 p-4 shadow-sm dark:bg-gray-800">
<Download className="h-5 w-5 text-blue-600" /> <Download className="h-5 w-5 text-blue-600" />
<span className="text-lg font-semibold text-gray-800"> <span className="text-lg font-semibold text-gray-800 dark:text-gray-200">
{prettyBytes(data.networkStatistics.receivedBytes)} {prettyBytes(data.networkStatistics.receivedBytes)}
</span> </span>
</div> </div>

View file

@ -34,6 +34,7 @@ import AutoDownloadDialog from "@/components/auto-download-dialog";
import useIsMobile from "@/hooks/use-is-mobile"; import useIsMobile from "@/hooks/use-is-mobile";
import { useState } from "react"; import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import ThemeToggleButton from "@/components/theme-toggle-button";
export function Header() { export function Header() {
const { isLoading, getAccounts, accountId, account, handleAccountChange } = const { isLoading, getAccounts, accountId, account, handleAccountChange } =
@ -114,7 +115,7 @@ export function Header() {
)} )}
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-2">
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@ -155,6 +156,8 @@ export function Header() {
</TooltipProvider> </TooltipProvider>
)} )}
<ThemeToggleButton />
<SettingsDialog /> <SettingsDialog />
</div> </div>

View file

@ -22,7 +22,7 @@ export default function ProxyPing({ accountId }: { accountId: string }) {
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<div <div
className="flex items-center space-x-2 rounded bg-gray-100 p-1" className="flex items-center space-x-2 rounded bg-gray-100 p-1 dark:bg-gray-800"
onClick={() => { onClick={() => {
if (isLoading) return; if (isLoading) return;
void mutate(undefined, true); void mutate(undefined, true);
@ -30,12 +30,14 @@ export default function ProxyPing({ accountId }: { accountId: string }) {
> >
<ChevronsLeftRightEllipsis className="h-4 w-4 text-gray-500" /> <ChevronsLeftRightEllipsis className="h-4 w-4 text-gray-500" />
{isLoading && ( {isLoading && (
<div className="h-6 w-16 animate-pulse rounded bg-gray-200"></div> <div className="h-6 w-16 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
)} )}
{!isLoading && error && <span>Connection error</span>} {!isLoading && error && <span>Connection error</span>}
{!isLoading && data && ( {!isLoading && data && (
<div className="flex h-6 items-center space-x-2"> <div className="flex h-6 items-center space-x-2">
<span className="text-sm">{(data.ping * 1000).toFixed(0)} ms</span> <span className="text-sm">
{(data.ping * 1000).toFixed(0)} ms
</span>
</div> </div>
)} )}
</div> </div>

View file

@ -224,8 +224,9 @@ export default function Proxys({
))} ))}
</div> </div>
{enableSelect && ( {enableSelect && (
<div className="absolute bottom-0 right-0 flex items-center space-x-4"> <div className="w-full absolute bottom-0 justify-end flex items-center space-x-4">
<Button <Button
className="w-full md:w-auto"
disabled={isToggleProxyMutating} disabled={isToggleProxyMutating}
onClick={() => handleProxySubmit()} onClick={() => handleProxySubmit()}
> >

View file

@ -51,14 +51,14 @@ export default function SettingsForm() {
className="flex h-full flex-col overflow-hidden" className="flex h-full flex-col overflow-hidden"
> >
<div className="flex flex-col space-y-4 overflow-y-scroll"> <div className="flex flex-col space-y-4 overflow-y-scroll">
<p className="rounded-md bg-gray-50 p-2 text-sm text-muted-foreground shadow"> <p className="rounded-md bg-gray-50 p-2 text-sm text-muted-foreground shadow dark:bg-gray-700">
<Bell className="mr-2 inline-block h-4 w-4" /> <Bell className="mr-2 inline-block h-4 w-4" />
These settings will be applied to all accounts. These settings will be applied to all accounts.
</p> </p>
<div className="w-full rounded-md border p-4 shadow"> <div className="w-full rounded-md border p-4 shadow">
<p className="mb-1 text-xs text-muted-foreground">Your root path</p> <p className="mb-1 text-xs text-muted-foreground">Your root path</p>
<div className="flex items-center justify-between space-x-1"> <div className="flex items-center justify-between space-x-1">
<p className="bg-gray-50 rounded-md p-2 text-xs text-muted-foreground"> <p className="bg-gray-50 rounded-md p-2 text-xs text-muted-foreground dark:bg-gray-700">
{account?.rootPath} {account?.rootPath}
</p> </p>
<Button <Button
@ -204,7 +204,7 @@ export default function SettingsForm() {
</div> </div>
</div> </div>
<div className="mt-2 flex flex-1 justify-end"> <div className="mt-2 flex flex-1 justify-end">
<Button type="submit">Submit</Button> <Button className="w-full md:w-auto" type="submit">Submit</Button>
</div> </div>
</form> </form>
); );

View file

@ -1,10 +1,12 @@
import { cn } from "@/lib/utils";
export default function TelegramIcon({ className }: { className?: string }) { export default function TelegramIcon({ className }: { className?: string }) {
return ( return (
<svg <svg
role="img" role="img"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className={className} className={cn(className, "dark:fill-current")}
> >
<title>Telegram</title> <title>Telegram</title>
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" /> <path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />

View file

@ -0,0 +1,21 @@
"use client";
import {
ThemeProvider as NextThemesProvider,
type ThemeProviderProps,
} from "next-themes";
import { useEffect, useState } from "react";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const [isMounted, setIsMounted] = useState<boolean>(false);
useEffect(() => {
setIsMounted(true);
}, []);
if (!isMounted) {
return null;
}
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View file

@ -0,0 +1,37 @@
"use client";
import { Toggle } from "@/components/ui/toggle";
import { Moon, Sun } from "lucide-react";
import {useTheme} from "next-themes";
export default function ThemeToggleButton() {
const { theme, setTheme } = useTheme();
return (
<div>
<Toggle
variant="default"
size="sm"
className="group size-9"
pressed={theme === "dark"}
onPressedChange={() =>
setTheme((prev) => (prev === "dark" ? "light" : "dark"))
}
aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
>
<Moon
size={16}
strokeWidth={2}
className="shrink-0 scale-0 opacity-0 transition-all group-data-[state=on]:scale-100 group-data-[state=on]:opacity-100"
aria-hidden="true"
/>
<Sun
size={16}
strokeWidth={2}
className="absolute shrink-0 scale-100 opacity-100 transition-all group-data-[state=on]:scale-0 group-data-[state=on]:opacity-0"
aria-hidden="true"
/>
</Toggle>
</div>
);
}

View file

@ -0,0 +1,45 @@
"use client";
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3 min-w-10",
sm: "h-9 px-2.5 min-w-9",
lg: "h-11 px-5 min-w-11",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
));
Toggle.displayName = TogglePrimitive.Root.displayName;
export { Toggle, toggleVariants };