Skip to content

Commit 1e7f453

Browse files
authored
feat: add dark mode (#105)
1 parent e18bbf9 commit 1e7f453

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+640
-374
lines changed

package-lock.json

Lines changed: 120 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/dashboard/layout.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BackgroundWrapper } from "@/components/background-wrapper";
22
import { DashboardNavigation } from "@/components/dashboard-navigation";
33
import { Footer } from "@/components/footer";
44
import { Header } from "@/components/header";
5+
import { Button } from "@/components/ui/button";
56
import { getCurrentSession } from "@/server/auth";
67
import { PlusCircle } from "lucide-react";
78
import Link from "next/link";
@@ -24,13 +25,12 @@ export default async function DashboardLayout({
2425
<main className="flex-grow flex flex-col w-full max-w-sm sm:max-w-2xl md:max-w-4xl lg:max-w-6xl xl:max-w-[72rem] mx-auto px-4 sm:px-6 lg:px-8 py-8 z-10">
2526
<div className="flex justify-between items-center mb-8">
2627
<h1 className="mb-2 text-4xl font-bold tracking-tight">Dashboard</h1>
27-
<Link
28-
href="/invoices/create"
29-
className="bg-black hover:bg-zinc-800 text-white transition-colors px-4 py-2 rounded-md flex items-center"
30-
>
31-
<PlusCircle className="mr-2 h-4 w-4" />
32-
Create Invoice
33-
</Link>
28+
<Button asChild variant="default" className="flex items-center gap-2">
29+
<Link href="/invoices/create">
30+
<PlusCircle className="mr-2 h-4 w-4" />
31+
Create Invoice
32+
</Link>
33+
</Button>
3434
</div>
3535
<DashboardNavigation />
3636
{children}

src/app/i/[id]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default async function InvoiceMePage({
4444
<div className="flex items-center mb-8">
4545
<Link
4646
href="/"
47-
className="text-zinc-600 hover:text-black transition-colors mr-4"
47+
className="text-muted-foreground hover:text-foreground transition-colors mr-4"
4848
>
4949
<ArrowLeft className="h-6 w-6" />
5050
</Link>

src/app/invoices/[ID]/page.tsx

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -52,35 +52,37 @@ export default async function PaymentPage({
5252
<PaymentSection serverInvoice={invoice} />
5353

5454
{/* Invoice Preview */}
55-
<Card className="w-full bg-white shadow-sm border-0">
55+
<Card className="w-full shadow-sm border-0">
5656
<CardContent className="p-8">
5757
{/* Header Section */}
5858
<div className="mb-12">
5959
<div className="grid grid-cols-12 gap-4">
6060
<div className="col-span-4">
61-
<div className="text-xs text-neutral-500 mb-1">
61+
<div className="text-xs text-muted-foreground mb-1">
6262
INVOICE NO
6363
</div>
6464
<div className="text-sm font-medium">
6565
{invoice.invoiceNumber}
6666
</div>
6767
</div>
6868
<div className="col-span-4">
69-
<div className="text-xs text-neutral-500 mb-1">ISSUED</div>
69+
<div className="text-xs text-muted-foreground mb-1">
70+
ISSUED
71+
</div>
7072
<div className="text-sm">
7173
{formatDate(invoice.issuedDate)}
7274
</div>
7375
</div>
7476
<div className="col-span-4">
75-
<div className="text-xs text-neutral-500 mb-1">
77+
<div className="text-xs text-muted-foreground mb-1">
7678
DUE DATE
7779
</div>
7880
<div className="text-sm">{formatDate(invoice.dueDate)}</div>
7981
</div>
8082
</div>
8183
{invoice.recurrence && (
8284
<div className="mt-4">
83-
<div className="text-xs text-neutral-500 mb-1">
85+
<div className="text-xs text-muted-foreground mb-1">
8486
RECURRING
8587
</div>
8688
<div className="text-sm flex items-center gap-1">
@@ -108,23 +110,23 @@ export default async function PaymentPage({
108110
{/* From/To Section */}
109111
<div className="grid grid-cols-2 gap-16 mb-12">
110112
<div>
111-
<div className="text-xs text-neutral-500 mb-3">FROM</div>
113+
<div className="text-xs text-muted-foreground mb-3">FROM</div>
112114
<div className="space-y-1">
113115
<div className="text-sm">{invoice.creatorName}</div>
114-
<div className="text-sm text-neutral-600">
116+
<div className="text-sm text-muted-foreground">
115117
{invoice.creatorEmail}
116118
</div>
117119
<div className="text-sm mt-4">PAYABLE TO:</div>
118-
<div className="text-sm text-neutral-600 font-mono break-all">
120+
<div className="text-sm text-muted-foreground font-mono break-all">
119121
{invoice.payee}
120122
</div>
121123
</div>
122124
</div>
123125
<div>
124-
<div className="text-xs text-neutral-500 mb-3">TO</div>
126+
<div className="text-xs text-muted-foreground mb-3">TO</div>
125127
<div className="space-y-1">
126128
<div className="text-sm">{invoice.clientName}</div>
127-
<div className="text-sm text-neutral-600">
129+
<div className="text-sm text-muted-foreground">
128130
{invoice.clientEmail}
129131
</div>
130132
</div>
@@ -136,21 +138,21 @@ export default async function PaymentPage({
136138
<table className="w-full">
137139
<thead>
138140
<tr>
139-
<th className="text-xs text-neutral-500 text-left pb-3">
141+
<th className="text-xs text-muted-foreground text-left pb-3">
140142
DESCRIPTION
141143
</th>
142-
<th className="text-xs text-neutral-500 text-right pb-3">
144+
<th className="text-xs text-muted-foreground text-right pb-3">
143145
QTY
144146
</th>
145-
<th className="text-xs text-neutral-500 text-right pb-3">
147+
<th className="text-xs text-muted-foreground text-right pb-3">
146148
PRICE
147149
</th>
148-
<th className="text-xs text-neutral-500 text-right pb-3">
150+
<th className="text-xs text-muted-foreground text-right pb-3">
149151
AMOUNT
150152
</th>
151153
</tr>
152154
</thead>
153-
<tbody className="border-y border-neutral-200">
155+
<tbody className="border-y border-border">
154156
{(invoice.items as InvoiceItem[]).map((item, index) => (
155157
<tr key={`invoice-item-${item.description}-${index}`}>
156158
<td className="py-3">
@@ -174,18 +176,20 @@ export default async function PaymentPage({
174176
<div className="flex justify-end mt-6">
175177
<div className="w-48">
176178
<div className="flex justify-between py-2">
177-
<span className="text-sm text-neutral-600">Subtotal</span>
179+
<span className="text-sm text-muted-foreground">
180+
Subtotal
181+
</span>
178182
<span className="text-sm">
179183
{Number(invoice.amount).toString()}
180184
</span>
181185
</div>
182-
<div className="flex justify-between py-2 border-t border-neutral-200">
186+
<div className="flex justify-between py-2 border-t border-border">
183187
<span className="text-sm font-medium">Total</span>
184188
<div>
185189
<div className="text-sm text-right font-medium">
186190
{Number(invoice.amount).toString()}
187191
</div>
188-
<div className="text-xs text-neutral-500">
192+
<div className="text-xs text-muted-foreground">
189193
{formatCurrencyLabel(invoice.invoiceCurrency)}
190194
</div>
191195
</div>
@@ -197,7 +201,7 @@ export default async function PaymentPage({
197201
{/* Payment Details and Notes Section */}
198202
<div className="grid grid-cols-2 gap-16">
199203
<div>
200-
<div className="text-xs text-neutral-500 mb-1">
204+
<div className="text-xs text-muted-foreground mb-1">
201205
PAYABLE IN
202206
</div>
203207
<div className="text-sm">
@@ -206,7 +210,7 @@ export default async function PaymentPage({
206210
</div>
207211
{paymentDetailsData?.paymentDetails ? (
208212
<div>
209-
<div className="text-xs text-neutral-500 mb-1">
213+
<div className="text-xs text-muted-foreground mb-1">
210214
BANK ACCOUNT DETAILS
211215
</div>
212216
{paymentDetailsData.paymentDetails.accountName && (
@@ -245,10 +249,10 @@ export default async function PaymentPage({
245249
</div>
246250
) : (
247251
<div>
248-
<div className="text-xs text-neutral-500 mb-1">
252+
<div className="text-xs text-muted-foreground mb-1">
249253
BANK ACCOUNT DETAILS
250254
</div>
251-
<div className="text-sm text-neutral-500">
255+
<div className="text-sm text-muted-foreground">
252256
{!invoice.paymentDetailsId
253257
? "No payment details available"
254258
: "Unable to load payment details"}
@@ -257,8 +261,10 @@ export default async function PaymentPage({
257261
)}
258262
{invoice.notes && (
259263
<div>
260-
<div className="text-xs text-neutral-500 mb-1">NOTES</div>
261-
<div className="text-sm text-neutral-600 whitespace-pre-wrap break-words max-w-[300px]">
264+
<div className="text-xs text-muted-foreground mb-1">
265+
NOTES
266+
</div>
267+
<div className="text-sm text-muted-foreground whitespace-pre-wrap break-words max-w-[300px]">
262268
{invoice.notes}
263269
</div>
264270
</div>

src/app/invoices/create/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default async function CreateInvoicePage() {
2727
<div className="flex items-center mb-8">
2828
<Link
2929
href="/dashboard"
30-
className="text-zinc-600 hover:text-black transition-colors mr-4"
30+
className="text-muted-foreground hover:text-foreground transition-colors mr-4"
3131
>
3232
<ArrowLeft className="h-6 w-6" />
3333
</Link>

src/app/layout.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { Metadata } from "next";
99
import localFont from "next/font/local";
1010
import { cookies } from "next/headers";
1111
import "./globals.css";
12+
import { ThemeProvider } from "next-themes";
1213

1314
const geistSans = localFont({
1415
src: "./fonts/GeistVF.woff",
@@ -32,20 +33,28 @@ export default function RootLayout({
3233
children: React.ReactNode;
3334
}>) {
3435
return (
35-
<html lang="en">
36-
<GoogleTagManager gtmId={process.env.NEXT_PUBLIC_GTM_ID as string} />
36+
<html lang="en" suppressHydrationWarning>
3737
<body
3838
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
3939
>
40-
<AppKit>
41-
<TooltipProvider>
42-
<TRPCReactProvider cookies={cookies().toString()}>
43-
<BackgroundWrapper>{children}</BackgroundWrapper>
44-
</TRPCReactProvider>
45-
<Toaster />
46-
</TooltipProvider>
47-
</AppKit>
48-
<VersionDisplay githubRelease="https://github.com/RequestNetwork/easy-invoice/releases" />
40+
<GoogleTagManager gtmId={process.env.NEXT_PUBLIC_GTM_ID as string} />
41+
<ThemeProvider
42+
attribute="class"
43+
defaultTheme="system"
44+
enableSystem
45+
enableColorScheme
46+
disableTransitionOnChange
47+
>
48+
<AppKit>
49+
<TooltipProvider>
50+
<TRPCReactProvider cookies={cookies().toString()}>
51+
<BackgroundWrapper>{children}</BackgroundWrapper>
52+
</TRPCReactProvider>
53+
<Toaster />
54+
</TooltipProvider>
55+
</AppKit>
56+
<VersionDisplay githubRelease="https://github.com/RequestNetwork/easy-invoice/releases" />
57+
</ThemeProvider>
4958
</body>
5059
</html>
5160
);

src/app/subscription-plans/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export default async function SubscriptionPlansPage() {
4646
<div className="flex items-center mb-8">
4747
<Link
4848
href="/dashboard"
49-
className="text-zinc-600 hover:text-black transition-colors mr-4"
49+
className="text-muted-foreground hover:text-foreground transition-colors mr-4"
5050
>
5151
<ArrowLeft className="h-6 w-6" />
5252
</Link>

src/components/background-wrapper.tsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
"use client";
2+
import { useTheme } from "next-themes";
3+
import { useEffect, useState } from "react";
14
import type { ReactNode } from "react";
25

36
interface BackgroundWrapperProps {
@@ -23,6 +26,13 @@ export function BackgroundWrapper({
2326
to: "zinc-200",
2427
},
2528
}: BackgroundWrapperProps) {
29+
const { resolvedTheme } = useTheme();
30+
const [isMounted, setIsMounted] = useState(false);
31+
32+
useEffect(() => {
33+
setIsMounted(true);
34+
}, []);
35+
2636
// Convert Tailwind color names to CSS variables or hex values
2737
const getTailwindColor = (colorName: string): string => {
2838
const colors: Record<string, string> = {
@@ -42,24 +52,39 @@ export function BackgroundWrapper({
4252
"zinc-100": "#f4f4f5",
4353
"zinc-200": "#e4e4e7",
4454

55+
// Dark mode colors
56+
"zinc-800": "#27272a",
57+
"zinc-900": "#18181b",
58+
"slate-800": "#1e293b",
59+
"slate-900": "#0f172a",
60+
4561
// Add any other colors you need here
4662
};
4763

4864
return colors[colorName] || "#f4f4f5"; // Default to zinc-100 if color not found
4965
};
5066

67+
// Only trust theme after mount to keep SSR/CSR output consistent
68+
const isDark = isMounted && resolvedTheme === "dark";
69+
5170
return (
52-
<div className="min-h-screen relative overflow-hidden bg-[#FAFAFA]">
53-
{/* Decorative elements */}
54-
<div className="absolute top-0 right-0 w-[600px] h-[600px] -translate-y-1/2 translate-x-1/2">
71+
<div className="min-h-screen relative overflow-hidden bg-background">
72+
{/* Decorative elements: keep DOM shape stable; toggle visibility */}
73+
<div
74+
className="absolute top-0 right-0 w-[600px] h-[600px] -translate-y-1/2 translate-x-1/2"
75+
style={{ display: isMounted && !isDark ? "block" : "none" }}
76+
>
5577
<div
5678
className="w-full h-full rounded-full opacity-30 blur-3xl"
5779
style={{
5880
background: `linear-gradient(to bottom right, ${getTailwindColor(topGradient.from)}, ${getTailwindColor(topGradient.to)})`,
5981
}}
6082
/>
6183
</div>
62-
<div className="absolute bottom-0 left-0 w-[600px] h-[600px] translate-y-1/2 -translate-x-1/2">
84+
<div
85+
className="absolute bottom-0 left-0 w-[600px] h-[600px] translate-y-1/2 -translate-x-1/2"
86+
style={{ display: isMounted && !isDark ? "block" : "none" }}
87+
>
6388
<div
6489
className="w-full h-full rounded-full opacity-30 blur-3xl"
6590
style={{
@@ -73,7 +98,7 @@ export function BackgroundWrapper({
7398
className="absolute inset-0"
7499
style={{
75100
backgroundImage:
76-
"radial-gradient(circle at 1px 1px, #e5e5e5 1px, transparent 0)",
101+
"radial-gradient(circle at 1px 1px, hsl(var(--muted)) 1px, transparent 0)",
77102
backgroundSize: "40px 40px",
78103
}}
79104
/>

0 commit comments

Comments
 (0)