Spaces:
Running
Running
osanseviero
commited on
Commit
•
9d1a465
1
Parent(s):
a13a90f
Upload 56 files
Browse files- app/(main)/layout.tsx +8 -3
- app/(main)/page.tsx +15 -16
- app/api/generateCode/route.ts +3 -0
- app/layout.tsx +4 -1
- components/DarkModeToggle.tsx +47 -0
- components/Header.tsx +2 -2
- components/ThemeProvider.tsx +48 -0
- components/ThemeToggle.tsx +47 -0
- hooks/useDarkMode.tsx +50 -0
- package.json +3 -3
- tailwind.config.ts +7 -1
app/(main)/layout.tsx
CHANGED
@@ -2,6 +2,7 @@ import Image from "next/image";
|
|
2 |
import bgImg from "@/public/halo.png";
|
3 |
import Footer from "@/components/Footer";
|
4 |
import Header from "@/components/Header";
|
|
|
5 |
|
6 |
export default function Layout({
|
7 |
children,
|
@@ -9,18 +10,22 @@ export default function Layout({
|
|
9 |
children: React.ReactNode;
|
10 |
}>) {
|
11 |
return (
|
12 |
-
<body className="bg-brand antialiased">
|
|
|
13 |
<div className="absolute inset-x-0 flex justify-center">
|
14 |
<Image
|
15 |
src={bgImg}
|
16 |
alt=""
|
17 |
-
className="w-full max-w-[1200px] mix-blend-screen"
|
18 |
priority
|
19 |
/>
|
20 |
</div>
|
21 |
|
22 |
-
<div className="isolate">
|
23 |
<div className="mx-auto flex min-h-screen max-w-7xl flex-col items-center justify-center py-2">
|
|
|
|
|
|
|
24 |
<Header />
|
25 |
{children}
|
26 |
<Footer />
|
|
|
2 |
import bgImg from "@/public/halo.png";
|
3 |
import Footer from "@/components/Footer";
|
4 |
import Header from "@/components/Header";
|
5 |
+
import ThemeToggle from "@/components/ThemeToggle";
|
6 |
|
7 |
export default function Layout({
|
8 |
children,
|
|
|
10 |
children: React.ReactNode;
|
11 |
}>) {
|
12 |
return (
|
13 |
+
<body className="bg-brand dark:bg-dark antialiased dark:text-gray-100">
|
14 |
+
<div className="absolute inset-0 dark:bg-dark-radial" />
|
15 |
<div className="absolute inset-x-0 flex justify-center">
|
16 |
<Image
|
17 |
src={bgImg}
|
18 |
alt=""
|
19 |
+
className="w-full max-w-[1200px] mix-blend-screen dark:mix-blend-plus-lighter dark:opacity-10"
|
20 |
priority
|
21 |
/>
|
22 |
</div>
|
23 |
|
24 |
+
<div className="isolate relative">
|
25 |
<div className="mx-auto flex min-h-screen max-w-7xl flex-col items-center justify-center py-2">
|
26 |
+
<div className="fixed right-4 top-4 z-50">
|
27 |
+
<ThemeToggle />
|
28 |
+
</div>
|
29 |
<Header />
|
30 |
{children}
|
31 |
<Footer />
|
app/(main)/page.tsx
CHANGED
@@ -39,7 +39,6 @@ export default function Home() {
|
|
39 |
let [generatedCode, setGeneratedCode] = useState("");
|
40 |
let [initialAppConfig, setInitialAppConfig] = useState({
|
41 |
model: "",
|
42 |
-
shadcn: true,
|
43 |
});
|
44 |
let [ref, scrollTo] = useScrollTo();
|
45 |
let [messages, setMessages] = useState<{ role: string; content: string }[]>(
|
@@ -106,7 +105,7 @@ export default function Home() {
|
|
106 |
return (
|
107 |
<main className="mt-12 flex w-full flex-1 flex-col items-center px-4 text-center sm:mt-1">
|
108 |
<a
|
109 |
-
className="mb-4 inline-flex h-7 shrink-0 items-center gap-[9px] rounded-[50px] border-[0.5px] border-solid border-[#E6E6E6] bg-[rgba(234,238,255,0.65)] bg-gray-
|
110 |
href="https://ai.google.dev/gemini-api/docs"
|
111 |
target="_blank"
|
112 |
>
|
@@ -114,7 +113,7 @@ export default function Home() {
|
|
114 |
Powered by <span className="font-medium">Gemini API</span>
|
115 |
</span>
|
116 |
</a>
|
117 |
-
<h1 className="my-6 max-w-3xl text-4xl font-bold text-gray-800 sm:text-6xl">
|
118 |
Turn your <span className="text-blue-600">idea</span>
|
119 |
<br /> into an <span className="text-blue-600">app</span>
|
120 |
</h1>
|
@@ -122,8 +121,8 @@ export default function Home() {
|
|
122 |
<form className="w-full max-w-xl" onSubmit={createApp}>
|
123 |
<fieldset disabled={loading} className="disabled:opacity-75">
|
124 |
<div className="relative mt-5">
|
125 |
-
<div className="absolute -inset-2 rounded-[32px] bg-gray-300/50" />
|
126 |
-
<div className="relative flex rounded-3xl bg-white shadow-sm">
|
127 |
<div className="relative flex flex-grow items-stretch focus-within:z-10">
|
128 |
<textarea
|
129 |
rows={3}
|
@@ -131,14 +130,14 @@ export default function Home() {
|
|
131 |
value={prompt}
|
132 |
onChange={(e) => setPrompt(e.target.value)}
|
133 |
name="prompt"
|
134 |
-
className="w-full resize-none rounded-l-3xl bg-transparent px-6 py-5 text-lg focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-500"
|
135 |
placeholder="Build me a calculator app..."
|
136 |
/>
|
137 |
</div>
|
138 |
<button
|
139 |
type="submit"
|
140 |
disabled={loading}
|
141 |
-
className="relative -ml-px inline-flex items-center gap-x-1.5 rounded-r-3xl px-3 py-2 text-sm font-semibold text-blue-500 hover:text-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-500 disabled:text-gray-900"
|
142 |
>
|
143 |
{status === "creating" ? (
|
144 |
<LoadingDots color="black" style="large" />
|
@@ -150,30 +149,30 @@ export default function Home() {
|
|
150 |
</div>
|
151 |
<div className="mt-6 flex flex-col justify-center gap-4 sm:flex-row sm:items-center sm:gap-8">
|
152 |
<div className="flex items-center justify-between gap-3 sm:justify-center">
|
153 |
-
<p className="text-gray-500 sm:text-xs">Model:</p>
|
154 |
<Select.Root
|
155 |
name="model"
|
156 |
disabled={loading}
|
157 |
value={model}
|
158 |
onValueChange={(value) => setModel(value)}
|
159 |
>
|
160 |
-
<Select.Trigger className="group flex w-60 max-w-xs items-center rounded-2xl border-[6px] border-gray-300 bg-white px-4 py-2 text-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-500">
|
161 |
<Select.Value />
|
162 |
<Select.Icon className="ml-auto">
|
163 |
-
<ChevronDownIcon className="size-6 text-gray-300 group-focus-visible:text-gray-500 group-enabled:group-hover:text-gray-500" />
|
164 |
</Select.Icon>
|
165 |
</Select.Trigger>
|
166 |
<Select.Portal>
|
167 |
-
<Select.Content className="overflow-hidden rounded-md bg-white shadow-lg">
|
168 |
<Select.Viewport className="p-2">
|
169 |
{models.map((model) => (
|
170 |
<Select.Item
|
171 |
key={model.value}
|
172 |
value={model.value}
|
173 |
-
className="flex cursor-pointer items-center rounded-md px-3 py-2 text-sm data-[highlighted]:bg-gray-100 data-[highlighted]:outline-none"
|
174 |
>
|
175 |
<Select.ItemText asChild>
|
176 |
-
<span className="inline-flex items-center gap-2 text-gray-500">
|
177 |
<div className="size-2 rounded-full bg-green-500" />
|
178 |
{model.label}
|
179 |
</span>
|
@@ -194,7 +193,7 @@ export default function Home() {
|
|
194 |
</fieldset>
|
195 |
</form>
|
196 |
|
197 |
-
<hr className="border-1 mb-20 h-px bg-gray-700 dark:bg-gray-700" />
|
198 |
|
199 |
{status !== "initial" && (
|
200 |
<motion.div
|
@@ -226,9 +225,9 @@ export default function Home() {
|
|
226 |
duration: 0.85,
|
227 |
delay: 0.5,
|
228 |
}}
|
229 |
-
className="absolute inset-x-0 bottom-0 top-1/2 flex items-center justify-center rounded-r border border-gray-400 bg-gradient-to-br from-gray-100 to-gray-300 md:inset-y-0 md:left-1/2 md:right-0"
|
230 |
>
|
231 |
-
<p className="animate-pulse text-3xl font-bold">
|
232 |
{status === "creating"
|
233 |
? "Building your app..."
|
234 |
: "Updating your app..."}
|
|
|
39 |
let [generatedCode, setGeneratedCode] = useState("");
|
40 |
let [initialAppConfig, setInitialAppConfig] = useState({
|
41 |
model: "",
|
|
|
42 |
});
|
43 |
let [ref, scrollTo] = useScrollTo();
|
44 |
let [messages, setMessages] = useState<{ role: string; content: string }[]>(
|
|
|
105 |
return (
|
106 |
<main className="mt-12 flex w-full flex-1 flex-col items-center px-4 text-center sm:mt-1">
|
107 |
<a
|
108 |
+
className="mb-4 inline-flex h-7 shrink-0 items-center gap-[9px] rounded-[50px] border-[0.5px] border-solid border-[#E6E6E6] bg-[rgba(234,238,255,0.65)] dark:bg-[rgba(30,41,59,0.5)] dark:border-gray-700 px-7 py-5 shadow-[0px_1px_1px_0px_rgba(0,0,0,0.25)]"
|
109 |
href="https://ai.google.dev/gemini-api/docs"
|
110 |
target="_blank"
|
111 |
>
|
|
|
113 |
Powered by <span className="font-medium">Gemini API</span>
|
114 |
</span>
|
115 |
</a>
|
116 |
+
<h1 className="my-6 max-w-3xl text-4xl font-bold text-gray-800 dark:text-white sm:text-6xl">
|
117 |
Turn your <span className="text-blue-600">idea</span>
|
118 |
<br /> into an <span className="text-blue-600">app</span>
|
119 |
</h1>
|
|
|
121 |
<form className="w-full max-w-xl" onSubmit={createApp}>
|
122 |
<fieldset disabled={loading} className="disabled:opacity-75">
|
123 |
<div className="relative mt-5">
|
124 |
+
<div className="absolute -inset-2 rounded-[32px] bg-gray-300/50 dark:bg-gray-800/50" />
|
125 |
+
<div className="relative flex rounded-3xl bg-white dark:bg-[#1E293B] shadow-sm">
|
126 |
<div className="relative flex flex-grow items-stretch focus-within:z-10">
|
127 |
<textarea
|
128 |
rows={3}
|
|
|
130 |
value={prompt}
|
131 |
onChange={(e) => setPrompt(e.target.value)}
|
132 |
name="prompt"
|
133 |
+
className="w-full resize-none rounded-l-3xl bg-transparent px-6 py-5 text-lg focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-500 dark:text-gray-100 dark:placeholder-gray-400"
|
134 |
placeholder="Build me a calculator app..."
|
135 |
/>
|
136 |
</div>
|
137 |
<button
|
138 |
type="submit"
|
139 |
disabled={loading}
|
140 |
+
className="relative -ml-px inline-flex items-center gap-x-1.5 rounded-r-3xl px-3 py-2 text-sm font-semibold text-blue-500 hover:text-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-500 disabled:text-gray-900 dark:disabled:text-gray-400"
|
141 |
>
|
142 |
{status === "creating" ? (
|
143 |
<LoadingDots color="black" style="large" />
|
|
|
149 |
</div>
|
150 |
<div className="mt-6 flex flex-col justify-center gap-4 sm:flex-row sm:items-center sm:gap-8">
|
151 |
<div className="flex items-center justify-between gap-3 sm:justify-center">
|
152 |
+
<p className="text-gray-500 dark:text-gray-400 sm:text-xs">Model:</p>
|
153 |
<Select.Root
|
154 |
name="model"
|
155 |
disabled={loading}
|
156 |
value={model}
|
157 |
onValueChange={(value) => setModel(value)}
|
158 |
>
|
159 |
+
<Select.Trigger className="group flex w-60 max-w-xs items-center rounded-2xl border-[6px] border-gray-300 dark:border-gray-700 bg-white dark:bg-[#1E293B] px-4 py-2 text-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-500">
|
160 |
<Select.Value />
|
161 |
<Select.Icon className="ml-auto">
|
162 |
+
<ChevronDownIcon className="size-6 text-gray-300 group-focus-visible:text-gray-500 group-enabled:group-hover:text-gray-500 dark:text-gray-600 dark:group-focus-visible:text-gray-400 dark:group-enabled:group-hover:text-gray-400" />
|
163 |
</Select.Icon>
|
164 |
</Select.Trigger>
|
165 |
<Select.Portal>
|
166 |
+
<Select.Content className="overflow-hidden rounded-md bg-white dark:bg-[#1E293B] shadow-lg">
|
167 |
<Select.Viewport className="p-2">
|
168 |
{models.map((model) => (
|
169 |
<Select.Item
|
170 |
key={model.value}
|
171 |
value={model.value}
|
172 |
+
className="flex cursor-pointer items-center rounded-md px-3 py-2 text-sm data-[highlighted]:bg-gray-100 dark:data-[highlighted]:bg-gray-800 data-[highlighted]:outline-none"
|
173 |
>
|
174 |
<Select.ItemText asChild>
|
175 |
+
<span className="inline-flex items-center gap-2 text-gray-500 dark:text-gray-400">
|
176 |
<div className="size-2 rounded-full bg-green-500" />
|
177 |
{model.label}
|
178 |
</span>
|
|
|
193 |
</fieldset>
|
194 |
</form>
|
195 |
|
196 |
+
<hr className="border-1 mb-20 h-px bg-gray-700 dark:bg-gray-700/30" />
|
197 |
|
198 |
{status !== "initial" && (
|
199 |
<motion.div
|
|
|
225 |
duration: 0.85,
|
226 |
delay: 0.5,
|
227 |
}}
|
228 |
+
className="absolute inset-x-0 bottom-0 top-1/2 flex items-center justify-center rounded-r border border-gray-400 dark:border-gray-700 bg-gradient-to-br from-gray-100 to-gray-300 dark:from-[#1E293B] dark:to-gray-800 md:inset-y-0 md:left-1/2 md:right-0"
|
229 |
>
|
230 |
+
<p className="animate-pulse text-3xl font-bold dark:text-gray-100">
|
231 |
{status === "creating"
|
232 |
? "Building your app..."
|
233 |
: "Updating your app..."}
|
app/api/generateCode/route.ts
CHANGED
@@ -32,6 +32,8 @@ export async function POST(req: Request) {
|
|
32 |
messages[0].content + systemPrompt + "\nPlease ONLY return code, NO backticks or language names. Don't start with \`\`\`typescript or \`\`\`javascript or \`\`\`tsx or \`\`\`."
|
33 |
);
|
34 |
|
|
|
|
|
35 |
const readableStream = new ReadableStream({
|
36 |
async start(controller) {
|
37 |
for await (const chunk of geminiStream.stream) {
|
@@ -64,6 +66,7 @@ function getSystemPrompt() {
|
|
64 |
systemPrompt += `
|
65 |
NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
|
66 |
`;
|
|
|
67 |
return dedent(systemPrompt);
|
68 |
}
|
69 |
|
|
|
32 |
messages[0].content + systemPrompt + "\nPlease ONLY return code, NO backticks or language names. Don't start with \`\`\`typescript or \`\`\`javascript or \`\`\`tsx or \`\`\`."
|
33 |
);
|
34 |
|
35 |
+
console.log(messages[0].content + systemPrompt + "\nPlease ONLY return code, NO backticks or language names. Don't start with \`\`\`typescript or \`\`\`javascript or \`\`\`tsx or \`\`\`.")
|
36 |
+
|
37 |
const readableStream = new ReadableStream({
|
38 |
async start(controller) {
|
39 |
for await (const chunk of geminiStream.stream) {
|
|
|
66 |
systemPrompt += `
|
67 |
NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
|
68 |
`;
|
69 |
+
|
70 |
return dedent(systemPrompt);
|
71 |
}
|
72 |
|
app/layout.tsx
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
import type { Metadata } from "next";
|
2 |
import "./globals.css";
|
|
|
3 |
|
4 |
let title = "Gemini Coder – AI Code Generator";
|
5 |
let description = "Generate your next app with Gemini";
|
@@ -38,7 +39,9 @@ export default function RootLayout({
|
|
38 |
}>) {
|
39 |
return (
|
40 |
<html lang="en" className="h-full">
|
41 |
-
|
|
|
|
|
42 |
</html>
|
43 |
);
|
44 |
}
|
|
|
1 |
import type { Metadata } from "next";
|
2 |
import "./globals.css";
|
3 |
+
import { ThemeProvider } from "@/components/ThemeProvider";
|
4 |
|
5 |
let title = "Gemini Coder – AI Code Generator";
|
6 |
let description = "Generate your next app with Gemini";
|
|
|
39 |
}>) {
|
40 |
return (
|
41 |
<html lang="en" className="h-full">
|
42 |
+
<ThemeProvider>
|
43 |
+
{children}
|
44 |
+
</ThemeProvider>
|
45 |
</html>
|
46 |
);
|
47 |
}
|
components/DarkModeToggle.tsx
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import { useDarkMode } from '@/hooks/useDarkMode';
|
4 |
+
|
5 |
+
export function DarkModeToggle() {
|
6 |
+
const { isDarkMode, toggleDarkMode } = useDarkMode();
|
7 |
+
|
8 |
+
return (
|
9 |
+
<button
|
10 |
+
onClick={toggleDarkMode}
|
11 |
+
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
12 |
+
aria-label="Toggle dark mode"
|
13 |
+
>
|
14 |
+
{isDarkMode ? (
|
15 |
+
<svg
|
16 |
+
xmlns="http://www.w3.org/2000/svg"
|
17 |
+
fill="none"
|
18 |
+
viewBox="0 0 24 24"
|
19 |
+
strokeWidth={1.5}
|
20 |
+
stroke="currentColor"
|
21 |
+
className="w-5 h-5"
|
22 |
+
>
|
23 |
+
<path
|
24 |
+
strokeLinecap="round"
|
25 |
+
strokeLinejoin="round"
|
26 |
+
d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
|
27 |
+
/>
|
28 |
+
</svg>
|
29 |
+
) : (
|
30 |
+
<svg
|
31 |
+
xmlns="http://www.w3.org/2000/svg"
|
32 |
+
fill="none"
|
33 |
+
viewBox="0 0 24 24"
|
34 |
+
strokeWidth={1.5}
|
35 |
+
stroke="currentColor"
|
36 |
+
className="w-5 h-5"
|
37 |
+
>
|
38 |
+
<path
|
39 |
+
strokeLinecap="round"
|
40 |
+
strokeLinejoin="round"
|
41 |
+
d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"
|
42 |
+
/>
|
43 |
+
</svg>
|
44 |
+
)}
|
45 |
+
</button>
|
46 |
+
);
|
47 |
+
}
|
components/Header.tsx
CHANGED
@@ -15,9 +15,9 @@ export default function Header() {
|
|
15 |
<a
|
16 |
href="https://github.com/osanseviero/geminicoder"
|
17 |
target="_blank"
|
18 |
-
className="ml-auto hidden items-center gap-3 rounded-2xl bg-white px-6 py-2 sm:flex"
|
19 |
>
|
20 |
-
<GithubIcon className="h-4 w-4" />
|
21 |
<span>GitHub Repo</span>
|
22 |
</a>
|
23 |
</header>
|
|
|
15 |
<a
|
16 |
href="https://github.com/osanseviero/geminicoder"
|
17 |
target="_blank"
|
18 |
+
className="ml-auto hidden items-center gap-3 rounded-2xl bg-white dark:bg-[#1E293B] dark:text-gray-100 px-6 py-2 sm:flex border border-gray-200 dark:border-gray-700"
|
19 |
>
|
20 |
+
<GithubIcon className="h-4 w-4 dark:text-gray-100" />
|
21 |
<span>GitHub Repo</span>
|
22 |
</a>
|
23 |
</header>
|
components/ThemeProvider.tsx
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
|
3 |
+
import { createContext, useContext, useEffect, useState } from "react";
|
4 |
+
|
5 |
+
type Theme = "light" | "dark";
|
6 |
+
|
7 |
+
type ThemeContextType = {
|
8 |
+
theme: Theme;
|
9 |
+
toggleTheme: () => void;
|
10 |
+
};
|
11 |
+
|
12 |
+
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
13 |
+
|
14 |
+
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
15 |
+
const [theme, setTheme] = useState<Theme>("light");
|
16 |
+
|
17 |
+
useEffect(() => {
|
18 |
+
const savedTheme = localStorage.getItem("theme") as Theme;
|
19 |
+
if (savedTheme) {
|
20 |
+
setTheme(savedTheme);
|
21 |
+
document.documentElement.classList.toggle("dark", savedTheme === "dark");
|
22 |
+
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
23 |
+
setTheme("dark");
|
24 |
+
document.documentElement.classList.add("dark");
|
25 |
+
}
|
26 |
+
}, []);
|
27 |
+
|
28 |
+
const toggleTheme = () => {
|
29 |
+
const newTheme = theme === "light" ? "dark" : "light";
|
30 |
+
setTheme(newTheme);
|
31 |
+
localStorage.setItem("theme", newTheme);
|
32 |
+
document.documentElement.classList.toggle("dark", newTheme === "dark");
|
33 |
+
};
|
34 |
+
|
35 |
+
return (
|
36 |
+
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
37 |
+
{children}
|
38 |
+
</ThemeContext.Provider>
|
39 |
+
);
|
40 |
+
}
|
41 |
+
|
42 |
+
export function useTheme() {
|
43 |
+
const context = useContext(ThemeContext);
|
44 |
+
if (context === undefined) {
|
45 |
+
throw new Error("useTheme must be used within a ThemeProvider");
|
46 |
+
}
|
47 |
+
return context;
|
48 |
+
}
|
components/ThemeToggle.tsx
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
|
3 |
+
import { useTheme } from "./ThemeProvider";
|
4 |
+
|
5 |
+
export default function ThemeToggle() {
|
6 |
+
const { theme, toggleTheme } = useTheme();
|
7 |
+
|
8 |
+
return (
|
9 |
+
<button
|
10 |
+
onClick={toggleTheme}
|
11 |
+
className="flex h-8 w-8 items-center justify-center rounded-lg bg-gray-200 transition-colors hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600"
|
12 |
+
aria-label="Toggle dark mode"
|
13 |
+
>
|
14 |
+
{theme === "light" ? (
|
15 |
+
<svg
|
16 |
+
xmlns="http://www.w3.org/2000/svg"
|
17 |
+
fill="none"
|
18 |
+
viewBox="0 0 24 24"
|
19 |
+
strokeWidth={1.5}
|
20 |
+
stroke="currentColor"
|
21 |
+
className="h-5 w-5"
|
22 |
+
>
|
23 |
+
<path
|
24 |
+
strokeLinecap="round"
|
25 |
+
strokeLinejoin="round"
|
26 |
+
d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"
|
27 |
+
/>
|
28 |
+
</svg>
|
29 |
+
) : (
|
30 |
+
<svg
|
31 |
+
xmlns="http://www.w3.org/2000/svg"
|
32 |
+
fill="none"
|
33 |
+
viewBox="0 0 24 24"
|
34 |
+
strokeWidth={1.5}
|
35 |
+
stroke="currentColor"
|
36 |
+
className="h-5 w-5"
|
37 |
+
>
|
38 |
+
<path
|
39 |
+
strokeLinecap="round"
|
40 |
+
strokeLinejoin="round"
|
41 |
+
d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
|
42 |
+
/>
|
43 |
+
</svg>
|
44 |
+
)}
|
45 |
+
</button>
|
46 |
+
);
|
47 |
+
}
|
hooks/useDarkMode.tsx
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import { createContext, useContext, useEffect, useState } from 'react';
|
4 |
+
|
5 |
+
type DarkModeContextType = {
|
6 |
+
isDarkMode: boolean;
|
7 |
+
toggleDarkMode: () => void;
|
8 |
+
};
|
9 |
+
|
10 |
+
const DarkModeContext = createContext<DarkModeContextType | undefined>(undefined);
|
11 |
+
|
12 |
+
export function DarkModeProvider({ children }: { children: React.ReactNode }) {
|
13 |
+
const [isDarkMode, setIsDarkMode] = useState(false);
|
14 |
+
|
15 |
+
useEffect(() => {
|
16 |
+
// Check if user has dark mode preference
|
17 |
+
const isDark = localStorage.getItem('darkMode') === 'true';
|
18 |
+
setIsDarkMode(isDark);
|
19 |
+
if (isDark) {
|
20 |
+
document.documentElement.classList.add('dark');
|
21 |
+
}
|
22 |
+
}, []);
|
23 |
+
|
24 |
+
const toggleDarkMode = () => {
|
25 |
+
setIsDarkMode((prev) => {
|
26 |
+
const newValue = !prev;
|
27 |
+
localStorage.setItem('darkMode', String(newValue));
|
28 |
+
if (newValue) {
|
29 |
+
document.documentElement.classList.add('dark');
|
30 |
+
} else {
|
31 |
+
document.documentElement.classList.remove('dark');
|
32 |
+
}
|
33 |
+
return newValue;
|
34 |
+
});
|
35 |
+
};
|
36 |
+
|
37 |
+
return (
|
38 |
+
<DarkModeContext.Provider value={{ isDarkMode, toggleDarkMode }}>
|
39 |
+
{children}
|
40 |
+
</DarkModeContext.Provider>
|
41 |
+
);
|
42 |
+
}
|
43 |
+
|
44 |
+
export function useDarkMode() {
|
45 |
+
const context = useContext(DarkModeContext);
|
46 |
+
if (context === undefined) {
|
47 |
+
throw new Error('useDarkMode must be used within a DarkModeProvider');
|
48 |
+
}
|
49 |
+
return context;
|
50 |
+
}
|
package.json
CHANGED
@@ -3,9 +3,9 @@
|
|
3 |
"version": "0.1.0",
|
4 |
"private": true,
|
5 |
"scripts": {
|
6 |
-
"dev": "
|
7 |
-
"build": "next build",
|
8 |
-
"start": "
|
9 |
"lint": "next lint"
|
10 |
},
|
11 |
"dependencies": {
|
|
|
3 |
"version": "0.1.0",
|
4 |
"private": true,
|
5 |
"scripts": {
|
6 |
+
"dev": "next dev",
|
7 |
+
"build": "prisma generate && prisma migrate deploy && next build",
|
8 |
+
"start": "next start",
|
9 |
"lint": "next lint"
|
10 |
},
|
11 |
"dependencies": {
|
tailwind.config.ts
CHANGED
@@ -3,6 +3,7 @@ import colors from "tailwindcss/colors";
|
|
3 |
import defaultTheme from "tailwindcss/defaultTheme";
|
4 |
|
5 |
const config: Config = {
|
|
|
6 |
content: [
|
7 |
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
8 |
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
@@ -14,7 +15,12 @@ const config: Config = {
|
|
14 |
brand: "#E1E7EC",
|
15 |
gray: colors.slate,
|
16 |
},
|
17 |
-
|
|
|
|
|
|
|
|
|
|
|
18 |
fontFamily: {
|
19 |
sans: ['"Aeonik"', ...defaultTheme.fontFamily.sans],
|
20 |
},
|
|
|
3 |
import defaultTheme from "tailwindcss/defaultTheme";
|
4 |
|
5 |
const config: Config = {
|
6 |
+
darkMode: "class",
|
7 |
content: [
|
8 |
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
9 |
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
|
15 |
brand: "#E1E7EC",
|
16 |
gray: colors.slate,
|
17 |
},
|
18 |
+
backgroundColor: {
|
19 |
+
dark: "#0B1120",
|
20 |
+
},
|
21 |
+
backgroundImage: {
|
22 |
+
'dark-radial': 'radial-gradient(circle at center, rgba(255,255,255,0.1) 0%, transparent 70%)',
|
23 |
+
},
|
24 |
fontFamily: {
|
25 |
sans: ['"Aeonik"', ...defaultTheme.fontFamily.sans],
|
26 |
},
|