Add Turso/Vercel deployment configuration

- Updated database connection to support Turso auth token
- Added vercel.json with bun build configuration
- Updated environment schema for production deployment
- Added new features and components for production readiness
This commit is contained in:
2025-07-12 01:42:43 -04:00
parent 2d217fab47
commit a1b40e7a9c
75 changed files with 8821 additions and 1803 deletions
+65 -26
View File
@@ -1,8 +1,8 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useState, Suspense } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button";
@@ -11,8 +11,10 @@ import { toast } from "sonner";
import { Logo } from "~/components/logo";
import { User, Mail, Lock, ArrowRight } from "lucide-react";
export default function RegisterPage() {
function RegisterForm() {
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard";
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
@@ -25,17 +27,21 @@ export default function RegisterPage() {
const res = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
firstName,
lastName,
email,
password
body: JSON.stringify({
firstName,
lastName,
email,
password,
}),
});
setLoading(false);
if (res.ok) {
toast.success("Account created successfully! Please sign in.");
router.push("/auth/signin");
const signInUrl =
callbackUrl !== "/dashboard"
? `/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
: "/auth/signin";
router.push(signInUrl);
} else {
const error = await res.text();
toast.error(error || "Failed to create account");
@@ -43,21 +49,25 @@ export default function RegisterPage() {
}
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100 flex items-center justify-center p-4">
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-green-50 to-emerald-100 p-4">
<div className="w-full max-w-md space-y-8">
{/* Logo and Welcome */}
<div className="text-center space-y-4">
<div className="space-y-4 text-center">
<Logo size="lg" className="mx-auto" />
<div>
<h1 className="text-2xl font-bold text-gray-900">Join beenvoice</h1>
<p className="text-gray-600 mt-2">Create your account to get started</p>
<p className="mt-2 text-gray-600">
Create your account to get started
</p>
</div>
</div>
{/* Registration Form */}
<Card className="shadow-xl border-0">
<Card className="border-0 shadow-xl">
<CardHeader className="space-y-1">
<CardTitle className="text-xl text-center">Create Account</CardTitle>
<CardTitle className="text-center text-xl">
Create Account
</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleRegister} className="space-y-4">
@@ -65,12 +75,12 @@ export default function RegisterPage() {
<div className="space-y-2">
<Label htmlFor="firstName">First Name</Label>
<div className="relative">
<User className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<User className="absolute top-3 left-3 h-4 w-4 text-gray-400" />
<Input
id="firstName"
type="text"
value={firstName}
onChange={e => setFirstName(e.target.value)}
onChange={(e) => setFirstName(e.target.value)}
required
autoFocus
className="pl-10"
@@ -81,12 +91,12 @@ export default function RegisterPage() {
<div className="space-y-2">
<Label htmlFor="lastName">Last Name</Label>
<div className="relative">
<User className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<User className="absolute top-3 left-3 h-4 w-4 text-gray-400" />
<Input
id="lastName"
type="text"
value={lastName}
onChange={e => setLastName(e.target.value)}
onChange={(e) => setLastName(e.target.value)}
required
className="pl-10"
placeholder="Last name"
@@ -97,12 +107,12 @@ export default function RegisterPage() {
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="relative">
<Mail className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Mail className="absolute top-3 left-3 h-4 w-4 text-gray-400" />
<Input
id="email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
onChange={(e) => setEmail(e.target.value)}
required
className="pl-10"
placeholder="Enter your email"
@@ -112,19 +122,21 @@ export default function RegisterPage() {
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Lock className="absolute top-3 left-3 h-4 w-4 text-gray-400" />
<Input
id="password"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
onChange={(e) => setPassword(e.target.value)}
required
minLength={6}
className="pl-10"
placeholder="Create a password"
/>
</div>
<p className="text-xs text-gray-500">Must be at least 6 characters</p>
<p className="text-xs text-gray-500">
Must be at least 6 characters
</p>
</div>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? (
@@ -139,7 +151,10 @@ export default function RegisterPage() {
</form>
<div className="mt-6 text-center text-sm">
<span className="text-gray-600">Already have an account? </span>
<Link href="/auth/signin" className="text-green-600 hover:text-green-700 font-medium">
<Link
href="/auth/signin"
className="font-medium text-green-600 hover:text-green-700"
>
Sign in here
</Link>
</div>
@@ -147,7 +162,7 @@ export default function RegisterPage() {
</Card>
{/* Features */}
<div className="text-center space-y-4">
<div className="space-y-4 text-center">
<p className="text-sm text-gray-500">Start invoicing like a pro</p>
<div className="flex justify-center space-x-6 text-xs text-gray-400">
<span> Free to start</span>
@@ -158,4 +173,28 @@ export default function RegisterPage() {
</div>
</div>
);
}
}
export default function RegisterPage() {
return (
<Suspense
fallback={
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-green-50 to-emerald-100 p-4">
<div className="w-full max-w-md space-y-8">
<div className="space-y-4 text-center">
<Logo size="lg" className="mx-auto" />
<div>
<h1 className="text-2xl font-bold text-gray-900">
Join beenvoice
</h1>
<p className="mt-2 text-gray-600">Loading...</p>
</div>
</div>
</div>
</div>
}
>
<RegisterForm />
</Suspense>
);
}
+55 -20
View File
@@ -1,8 +1,8 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useState, Suspense } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { signIn } from "next-auth/react";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Input } from "~/components/ui/input";
@@ -12,8 +12,10 @@ import { toast } from "sonner";
import { Logo } from "~/components/logo";
import { Mail, Lock, ArrowRight } from "lucide-react";
export default function SignInPage() {
function SignInForm() {
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard";
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
@@ -21,7 +23,7 @@ export default function SignInPage() {
async function handleSignIn(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
const result = await signIn("credentials", {
email,
password,
@@ -29,44 +31,46 @@ export default function SignInPage() {
});
setLoading(false);
if (result?.error) {
toast.error("Invalid email or password");
} else {
toast.success("Signed in successfully!");
router.push("/dashboard");
router.push(callbackUrl);
router.refresh();
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100 flex items-center justify-center p-4">
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-green-50 to-emerald-100 p-4">
<div className="w-full max-w-md space-y-8">
{/* Logo and Welcome */}
<div className="text-center space-y-4">
<div className="space-y-4 text-center">
<Logo size="lg" className="mx-auto" />
<div>
<h1 className="text-2xl font-bold text-gray-900">Welcome back</h1>
<p className="text-gray-600 mt-2">Sign in to your beenvoice account</p>
<p className="mt-2 text-gray-600">
Sign in to your beenvoice account
</p>
</div>
</div>
{/* Sign In Form */}
<Card className="shadow-xl border-0">
<Card className="border-0 shadow-xl">
<CardHeader className="space-y-1">
<CardTitle className="text-xl text-center">Sign In</CardTitle>
<CardTitle className="text-center text-xl">Sign In</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSignIn} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="relative">
<Mail className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Mail className="absolute top-3 left-3 h-4 w-4 text-gray-400" />
<Input
id="email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
onChange={(e) => setEmail(e.target.value)}
required
autoFocus
className="pl-10"
@@ -77,12 +81,12 @@ export default function SignInPage() {
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Lock className="absolute top-3 left-3 h-4 w-4 text-gray-400" />
<Input
id="password"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
onChange={(e) => setPassword(e.target.value)}
required
className="pl-10"
placeholder="Enter your password"
@@ -101,8 +105,13 @@ export default function SignInPage() {
</Button>
</form>
<div className="mt-6 text-center text-sm">
<span className="text-gray-600">Don&apos;t have an account? </span>
<Link href="/auth/register" className="text-green-600 hover:text-green-700 font-medium">
<span className="text-gray-600">
Don&apos;t have an account?{" "}
</span>
<Link
href="/auth/register"
className="font-medium text-green-600 hover:text-green-700"
>
Create one now
</Link>
</div>
@@ -110,8 +119,10 @@ export default function SignInPage() {
</Card>
{/* Features */}
<div className="text-center space-y-4">
<p className="text-sm text-gray-500">Simple invoicing for freelancers and small businesses</p>
<div className="space-y-4 text-center">
<p className="text-sm text-gray-500">
Simple invoicing for freelancers and small businesses
</p>
<div className="flex justify-center space-x-6 text-xs text-gray-400">
<span> Easy client management</span>
<span> Professional invoices</span>
@@ -121,4 +132,28 @@ export default function SignInPage() {
</div>
</div>
);
}
}
export default function SignInPage() {
return (
<Suspense
fallback={
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-green-50 to-emerald-100 p-4">
<div className="w-full max-w-md space-y-8">
<div className="space-y-4 text-center">
<Logo size="lg" className="mx-auto" />
<div>
<h1 className="text-2xl font-bold text-gray-900">
Welcome back
</h1>
<p className="mt-2 text-gray-600">Loading...</p>
</div>
</div>
</div>
</div>
}
>
<SignInForm />
</Suspense>
);
}
+20
View File
@@ -0,0 +1,20 @@
import { Navbar } from "~/components/Navbar";
import { Sidebar } from "~/components/Sidebar";
export default function ClientsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<Navbar />
<div className="flex">
<Sidebar />
<main className="flex-1 min-h-screen bg-background">
{children}
</main>
</div>
</>
);
}
+42
View File
@@ -0,0 +1,42 @@
import Link from "next/link";
import { auth } from "~/server/auth";
import { api, HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { ClientList } from "~/components/client-list";
import { Plus } from "lucide-react";
export default async function ClientsPage() {
const session = await auth();
if (!session?.user) {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">Access Denied</h1>
<p className="text-muted-foreground mb-8">Please sign in to view clients</p>
<Link href="/api/auth/signin">
<Button size="lg">Sign In</Button>
</Link>
</div>
</div>
);
}
// Prefetch clients data
void api.clients.getAll.prefetch();
return (
<HydrateClient>
<div className="p-6">
<div className="mb-8">
<h2 className="text-3xl font-bold mb-2">Clients</h2>
<p className="text-muted-foreground">
Manage your client relationships
</p>
</div>
<ClientList />
</div>
</HydrateClient>
);
}
@@ -0,0 +1,243 @@
"use client";
import { api } from "~/trpc/react";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import {
Users,
FileText,
TrendingUp,
Calendar,
Plus,
ArrowRight
} from "lucide-react";
import Link from "next/link";
import { DashboardStatsSkeleton, DashboardActivitySkeleton } from "~/components/ui/skeleton";
// Client component for dashboard stats
export function DashboardStats() {
const { data: clients, isLoading: clientsLoading } = api.clients.getAll.useQuery();
const { data: invoices, isLoading: invoicesLoading } = api.invoices.getAll.useQuery();
if (clientsLoading || invoicesLoading) {
return <DashboardStatsSkeleton />;
}
const totalClients = clients?.length ?? 0;
const totalInvoices = invoices?.length ?? 0;
const totalRevenue = invoices?.reduce((sum, invoice) => sum + invoice.totalAmount, 0) ?? 0;
const pendingInvoices = invoices?.filter(invoice => invoice.status === "sent" || invoice.status === "draft").length ?? 0;
// Calculate month-over-month changes (simplified)
const lastMonthClients = 0; // This would need historical data
const lastMonthInvoices = 0;
const lastMonthRevenue = 0;
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-700">Total Clients</CardTitle>
<div className="p-2 bg-emerald-100 rounded-lg">
<Users className="h-4 w-4 text-emerald-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-emerald-600">{totalClients}</div>
<p className="text-xs text-gray-500">
{totalClients > lastMonthClients ? "+" : ""}{totalClients - lastMonthClients} from last month
</p>
</CardContent>
</Card>
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-700">Total Invoices</CardTitle>
<div className="p-2 bg-blue-100 rounded-lg">
<FileText className="h-4 w-4 text-blue-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-blue-600">{totalInvoices}</div>
<p className="text-xs text-gray-500">
{totalInvoices > lastMonthInvoices ? "+" : ""}{totalInvoices - lastMonthInvoices} from last month
</p>
</CardContent>
</Card>
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-700">Revenue</CardTitle>
<div className="p-2 bg-teal-100 rounded-lg">
<TrendingUp className="h-4 w-4 text-teal-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-teal-600">${totalRevenue.toFixed(2)}</div>
<p className="text-xs text-gray-500">
{totalRevenue > lastMonthRevenue ? "+" : ""}{((totalRevenue - lastMonthRevenue) / (lastMonthRevenue || 1) * 100).toFixed(1)}% from last month
</p>
</CardContent>
</Card>
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-700">Pending Invoices</CardTitle>
<div className="p-2 bg-orange-100 rounded-lg">
<Calendar className="h-4 w-4 text-orange-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-orange-600">{pendingInvoices}</div>
<p className="text-xs text-gray-500">
Due this month
</p>
</CardContent>
</Card>
</div>
);
}
// Client component for dashboard cards
export function DashboardCards() {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700">
<div className="p-2 bg-emerald-100 rounded-lg">
<Users className="h-5 w-5" />
</div>
Manage Clients
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-gray-600">
Add new clients and manage your existing client relationships.
</p>
<div className="flex gap-3">
<Button
asChild
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
>
<Link href="/dashboard/clients/new">
<Plus className="mr-2 h-4 w-4" />
Add Client
</Link>
</Button>
<Button
variant="outline"
asChild
className="border-gray-300 text-gray-700 hover:bg-gray-50 font-medium"
>
<Link href="/dashboard/clients">
View All Clients
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</CardContent>
</Card>
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700">
<div className="p-2 bg-emerald-100 rounded-lg">
<FileText className="h-5 w-5" />
</div>
Create Invoices
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-gray-600">
Generate professional invoices and track payments.
</p>
<div className="flex gap-3">
<Button
asChild
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
>
<Link href="/dashboard/invoices/new">
<Plus className="mr-2 h-4 w-4" />
New Invoice
</Link>
</Button>
<Button
variant="outline"
asChild
className="border-gray-300 text-gray-700 hover:bg-gray-50 font-medium"
>
<Link href="/dashboard/invoices">
View All Invoices
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
);
}
// Client component for recent activity
export function DashboardActivity() {
const { data: invoices, isLoading } = api.invoices.getAll.useQuery();
if (isLoading) {
return <DashboardActivitySkeleton />;
}
const recentInvoices = invoices?.slice(0, 5) ?? [];
return (
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
<CardHeader>
<CardTitle className="text-emerald-700">Recent Activity</CardTitle>
</CardHeader>
<CardContent>
{recentInvoices.length === 0 ? (
<div className="text-center py-12 text-gray-500">
<div className="p-4 bg-gray-100 rounded-full w-20 h-20 mx-auto mb-4 flex items-center justify-center">
<FileText className="h-8 w-8 text-gray-400" />
</div>
<p className="text-lg font-medium mb-2">No recent activity</p>
<p className="text-sm">Start by adding your first client or creating an invoice</p>
</div>
) : (
<div className="space-y-4">
{recentInvoices.map((invoice) => (
<div key={invoice.id} className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<div className="flex items-center gap-3">
<div className="p-2 bg-emerald-100 rounded-lg">
<FileText className="h-4 w-4 text-emerald-600" />
</div>
<div>
<p className="font-medium text-gray-900">Invoice #{invoice.invoiceNumber}</p>
<p className="text-sm text-gray-500">
{invoice.client?.name ?? "Unknown Client"} ${invoice.totalAmount.toFixed(2)}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
invoice.status === "paid" ? "bg-green-100 text-green-800" :
invoice.status === "sent" ? "bg-blue-100 text-blue-800" :
invoice.status === "overdue" ? "bg-red-100 text-red-800" :
"bg-gray-100 text-gray-800"
}`}>
{invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
</span>
<Button variant="ghost" size="sm" asChild>
<Link href={`/dashboard/invoices/${invoice.id}`}>
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
);
}
@@ -0,0 +1,11 @@
"use client";
import { BusinessForm } from "~/components/business-form";
import { useParams } from "next/navigation";
export default function EditBusinessPage() {
const params = useParams();
const businessId = Array.isArray(params?.id) ? params.id[0] : params?.id;
if (!businessId) return null;
return <BusinessForm businessId={businessId} mode="edit" />;
}
@@ -0,0 +1,15 @@
"use client";
import { api } from "~/trpc/react";
import { UniversalTable } from "~/components/ui/universal-table";
import { TableSkeleton } from "~/components/ui/skeleton";
export function BusinessesTable() {
const { isLoading } = api.businesses.getAll.useQuery();
if (isLoading) {
return <TableSkeleton rows={8} />;
}
return <UniversalTable resource="businesses" />;
}
@@ -0,0 +1,5 @@
import { BusinessForm } from "~/components/business-form";
export default function NewBusinessPage() {
return <BusinessForm mode="create" />;
}
+35
View File
@@ -0,0 +1,35 @@
import Link from "next/link";
import { api, HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { Plus } from "lucide-react";
import { BusinessesTable } from "./_components/businesses-table";
export default async function BusinessesPage() {
return (
<div>
<div className="mb-8 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h1 className="bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-3xl font-bold text-transparent">
Businesses
</h1>
<p className="mt-1 text-lg text-gray-600">
Manage your businesses and their information.
</p>
</div>
<Button
asChild
size="lg"
className="bg-gradient-to-r from-emerald-600 to-teal-600 font-medium text-white shadow-lg hover:from-emerald-700 hover:to-teal-700 hover:shadow-xl"
>
<Link href="/dashboard/businesses/new">
<Plus className="mr-2 h-5 w-5" /> Add Business
</Link>
</Button>
</div>
<HydrateClient>
<BusinessesTable />
</HydrateClient>
</div>
);
}
+7 -26
View File
@@ -1,45 +1,26 @@
import { auth } from "~/server/auth";
import { HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { ClientForm } from "~/components/client-form";
import Link from "next/link";
interface EditClientPageProps {
params: Promise<{ id: string }>;
}
export default async function EditClientPage({ params }: EditClientPageProps) {
const session = await auth();
const { id } = await params;
if (!session?.user) {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-emerald-50 via-white to-teal-50">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4 bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">Access Denied</h1>
<p className="text-muted-foreground mb-8">Please sign in to edit clients</p>
<Link href="/api/auth/signin">
<Button
size="lg"
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
>
Sign In
</Button>
</Link>
</div>
</div>
);
}
return (
<div>
<div className="mb-8">
<h1 className="text-3xl font-bold bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">Edit Client</h1>
<p className="text-gray-600 mt-1 text-lg">Update client information below.</p>
<h1 className="bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-3xl font-bold text-transparent">
Edit Client
</h1>
<p className="mt-1 text-lg text-gray-600">
Update client information below.
</p>
</div>
<HydrateClient>
<ClientForm mode="edit" clientId={id} />
</HydrateClient>
</div>
);
}
}
@@ -0,0 +1,15 @@
"use client";
import { api } from "~/trpc/react";
import { UniversalTable } from "~/components/ui/universal-table";
import { TableSkeleton } from "~/components/ui/skeleton";
export function ClientsTable() {
const { isLoading } = api.clients.getAll.useQuery();
if (isLoading) {
return <TableSkeleton rows={8} />;
}
return <UniversalTable resource="clients" />;
}
+7 -27
View File
@@ -1,40 +1,20 @@
import { auth } from "~/server/auth";
import { HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { ClientForm } from "~/components/client-form";
import Link from "next/link";
export default async function NewClientPage() {
const session = await auth();
if (!session?.user) {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-emerald-50 via-white to-teal-50">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4 bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">Access Denied</h1>
<p className="text-muted-foreground mb-8">Please sign in to create clients</p>
<Link href="/api/auth/signin">
<Button
size="lg"
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
>
Sign In
</Button>
</Link>
</div>
</div>
);
}
return (
<div>
<div className="mb-8">
<h1 className="text-3xl font-bold bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">Add Client</h1>
<p className="text-gray-600 mt-1 text-lg">Enter client details below to add a new client.</p>
<h1 className="bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-3xl font-bold text-transparent">
Add Client
</h1>
<p className="mt-1 text-lg text-gray-600">
Enter client details below to add a new client.
</p>
</div>
<HydrateClient>
<ClientForm mode="create" />
</HydrateClient>
</div>
);
}
}
+16 -32
View File
@@ -1,51 +1,35 @@
import Link from "next/link";
import { auth } from "~/server/auth";
import { api, HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { UniversalTable } from "~/components/ui/universal-table";
import { Plus } from "lucide-react";
import { ClientsTable } from "./_components/clients-table";
export default async function ClientsPage() {
const session = await auth();
if (!session?.user) {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-emerald-50 via-white to-teal-50">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4 bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">Access Denied</h1>
<p className="text-muted-foreground mb-8">Please sign in to view clients</p>
<Link href="/api/auth/signin">
<Button
size="lg"
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
>
Sign In
</Button>
</Link>
</div>
</div>
);
}
// Prefetch clients data
void api.clients.getAll.prefetch();
return (
<div>
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-8 gap-4">
<div className="mb-8 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">Clients</h1>
<p className="text-gray-600 mt-1 text-lg">Manage your clients and their information.</p>
<h1 className="bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-3xl font-bold text-transparent">
Clients
</h1>
<p className="mt-1 text-lg text-gray-600">
Manage your clients and their information.
</p>
</div>
<Button asChild size="lg" className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl">
<Button
asChild
size="lg"
className="bg-gradient-to-r from-emerald-600 to-teal-600 font-medium text-white shadow-lg hover:from-emerald-700 hover:to-teal-700 hover:shadow-xl"
>
<Link href="/dashboard/clients/new">
<Plus className="mr-2 h-5 w-5" /> Add Client
</Link>
</Button>
</div>
<HydrateClient>
<UniversalTable resource="clients" />
<ClientsTable />
</HydrateClient>
</div>
);
}
}
@@ -0,0 +1,26 @@
"use client";
import { InvoiceView } from "~/components/invoice-view";
import { InvoiceForm } from "~/components/invoice-form";
interface UnifiedInvoicePageProps {
invoiceId: string;
mode: string;
}
export function UnifiedInvoicePage({
invoiceId,
mode,
}: UnifiedInvoicePageProps) {
return (
<div>
{/* Always render InvoiceForm to preserve state, but hide when in view mode */}
<div className={mode === "edit" ? "block" : "hidden"}>
<InvoiceForm invoiceId={invoiceId} />
</div>
{/* Show InvoiceView only when in view mode */}
{mode === "view" && <InvoiceView invoiceId={invoiceId} />}
</div>
);
}
@@ -1,53 +0,0 @@
import { auth } from "~/server/auth";
import { api, HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { InvoiceForm } from "~/components/invoice-form";
import Link from "next/link";
import { notFound } from "next/navigation";
interface EditInvoicePageProps {
params: Promise<{ id: string }>;
}
export default async function EditInvoicePage({ params }: EditInvoicePageProps) {
const session = await auth();
const { id } = await params;
if (!session?.user) {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-emerald-50 via-white to-teal-50">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4 bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">Access Denied</h1>
<p className="text-muted-foreground mb-8">Please sign in to edit invoices</p>
<Link href="/api/auth/signin">
<Button
size="lg"
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
>
Sign In
</Button>
</Link>
</div>
</div>
);
}
// Prefetch invoice data
try {
await api.invoices.getById.prefetch({ id: id });
} catch (error) {
notFound();
}
return (
<div>
<div className="mb-8">
<h1 className="text-3xl font-bold bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">Edit Invoice</h1>
<p className="text-gray-600 mt-1 text-lg">Update the invoice details below.</p>
</div>
<HydrateClient>
<InvoiceForm invoiceId={id} />
</HydrateClient>
</div>
);
}
+54 -45
View File
@@ -1,63 +1,72 @@
import { auth } from "~/server/auth";
import { api, HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { InvoiceView } from "~/components/invoice-view";
import { InvoiceForm } from "~/components/invoice-form";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Edit } from "lucide-react";
import { Edit, Eye, ArrowLeft } from "lucide-react";
import { UnifiedInvoicePage } from "./_components/unified-invoice-page";
interface InvoicePageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ mode?: string }>;
}
export default async function InvoicePage({ params }: InvoicePageProps) {
const session = await auth();
export default async function InvoicePage({
params,
searchParams,
}: InvoicePageProps) {
const { id } = await params;
if (!session?.user) {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-emerald-50 via-white to-teal-50">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4 bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">Access Denied</h1>
<p className="text-muted-foreground mb-8">Please sign in to view invoices</p>
<Link href="/api/auth/signin">
<Button
size="lg"
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
>
Sign In
</Button>
</Link>
</div>
</div>
);
}
// Prefetch invoice data
try {
await api.invoices.getById.prefetch({ id: id });
} catch (error) {
notFound();
}
const { mode = "view" } = await searchParams;
return (
<div>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8 gap-4">
<div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">Invoice Details</h1>
<p className="text-gray-600 mt-1 text-lg">View and manage invoice information.</p>
</div>
<div className="flex gap-3">
<Button asChild variant="outline" size="lg" className="bg-white/80 border-gray-200 hover:bg-gray-50 text-gray-700 font-medium shadow-lg hover:shadow-xl">
<Link href={`/dashboard/invoices/${id}/edit`}>
<Edit className="mr-2 h-5 w-5" /> Edit Invoice
<div className="mb-6">
<div className="mb-4 flex items-center justify-between">
<div>
<h1 className="bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-3xl font-bold text-transparent">
Invoice Details
</h1>
<p className="mt-1 text-lg text-gray-600">
View and manage invoice information.
</p>
</div>
<div className="relative flex rounded-lg border border-gray-200 bg-gray-100 p-1">
<div
className={`absolute top-1 bottom-1 rounded-md bg-white shadow-sm transition-all duration-300 ease-in-out ${
mode === "view" ? "left-1 w-10" : "left-11 w-10"
}`}
/>
<Link
href={`/dashboard/invoices/${id}?mode=view`}
className={`relative z-10 rounded-md px-3 py-2 transition-all duration-200 ${
mode === "view"
? "text-emerald-600"
: "text-gray-600 hover:bg-gray-50 hover:text-gray-800"
}`}
>
<Eye className="h-4 w-4" />
</Link>
</Button>
<Link
href={`/dashboard/invoices/${id}?mode=edit`}
className={`relative z-10 rounded-md px-3 py-2 transition-all duration-200 ${
mode === "edit"
? "text-emerald-600"
: "text-gray-600 hover:bg-gray-50 hover:text-gray-800"
}`}
>
<Edit className="h-4 w-4" />
</Link>
</div>
</div>
<div className="mt-4">
<HydrateClient>
<UnifiedInvoicePage invoiceId={id} mode={mode} />
</HydrateClient>
</div>
</div>
<HydrateClient>
<InvoiceView invoiceId={id} />
</HydrateClient>
</div>
);
}
}
@@ -0,0 +1,15 @@
"use client";
import { api } from "~/trpc/react";
import { UniversalTable } from "~/components/ui/universal-table";
import { TableSkeleton } from "~/components/ui/skeleton";
export function InvoicesTable() {
const { isLoading } = api.invoices.getAll.useQuery();
if (isLoading) {
return <TableSkeleton rows={8} />;
}
return <UniversalTable resource="invoices" />;
}
+8 -28
View File
@@ -1,36 +1,16 @@
import Link from "next/link";
import { auth } from "~/server/auth";
import { api, HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { HydrateClient } from "~/trpc/server";
import { CSVImportPage } from "~/components/csv-import-page";
export default async function ImportPage() {
const session = await auth();
if (!session?.user) {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-emerald-50 via-white to-teal-50">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4 bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">Access Denied</h1>
<p className="text-muted-foreground mb-8">Please sign in to import invoices</p>
<Link href="/api/auth/signin">
<Button
size="lg"
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
>
Sign In
</Button>
</Link>
</div>
</div>
);
}
return (
<div>
<div className="mb-8">
<h1 className="text-3xl font-bold bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">Import Invoices</h1>
<p className="text-gray-600 mt-1 text-lg">Upload CSV files to create invoices in batch.</p>
<h1 className="bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-3xl font-bold text-transparent">
Import Invoices
</h1>
<p className="mt-1 text-lg text-gray-600">
Upload CSV files to create invoices in batch.
</p>
</div>
<HydrateClient>
@@ -38,4 +18,4 @@ export default async function ImportPage() {
</HydrateClient>
</div>
);
}
}
+7 -27
View File
@@ -1,40 +1,20 @@
import { auth } from "~/server/auth";
import { HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import Link from "next/link";
import { InvoiceForm } from "~/components/invoice-form";
export default async function NewInvoicePage() {
const session = await auth();
if (!session?.user) {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-emerald-50 via-white to-teal-50">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4 bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">Access Denied</h1>
<p className="text-muted-foreground mb-8">Please sign in to create invoices</p>
<Link href="/api/auth/signin">
<Button
size="lg"
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
>
Sign In
</Button>
</Link>
</div>
</div>
);
}
return (
<div>
<div className="mb-8">
<h1 className="text-3xl font-bold bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">Create Invoice</h1>
<p className="text-gray-600 mt-1 text-lg">Fill out the details below to create a new invoice.</p>
<h1 className="bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-3xl font-bold text-transparent">
Create Invoice
</h1>
<p className="mt-1 text-lg text-gray-600">
Fill out the details below to create a new invoice.
</p>
</div>
<HydrateClient>
<InvoiceForm />
</HydrateClient>
</div>
);
}
}
+22 -33
View File
@@ -1,49 +1,38 @@
import Link from "next/link";
import { auth } from "~/server/auth";
import { api, HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { UniversalTable } from "~/components/ui/universal-table";
import { Plus, Upload } from "lucide-react";
import { InvoicesTable } from "./_components/invoices-table";
export default async function InvoicesPage() {
const session = await auth();
if (!session?.user) {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-emerald-50 via-white to-teal-50">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4 bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">Access Denied</h1>
<p className="text-muted-foreground mb-8">Please sign in to view invoices</p>
<Link href="/api/auth/signin">
<Button
size="lg"
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
>
Sign In
</Button>
</Link>
</div>
</div>
);
}
// Prefetch invoices data
void api.invoices.getAll.prefetch();
return (
<div>
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-8 gap-4">
<div className="mb-8 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">Invoices</h1>
<p className="text-gray-600 mt-1 text-lg">Manage your invoices and payments.</p>
<h1 className="bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-3xl font-bold text-transparent">
Invoices
</h1>
<p className="mt-1 text-lg text-gray-600">
Manage your invoices and payments.
</p>
</div>
<div className="flex gap-3">
<Button asChild variant="outline" size="lg" className="bg-white/80 border-gray-200 hover:bg-gray-50 text-gray-700 font-medium shadow-lg hover:shadow-xl">
<Button
asChild
variant="outline"
size="lg"
className="border-gray-200 bg-white/80 font-medium text-gray-700 shadow-lg hover:bg-gray-50 hover:shadow-xl"
>
<Link href="/dashboard/invoices/import">
<Upload className="mr-2 h-5 w-5" /> Import CSV
</Link>
</Button>
<Button asChild size="lg" className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl">
<Button
asChild
size="lg"
className="bg-gradient-to-r from-emerald-600 to-teal-600 font-medium text-white shadow-lg hover:from-emerald-700 hover:to-teal-700 hover:shadow-xl"
>
<Link href="/dashboard/invoices/new">
<Plus className="mr-2 h-5 w-5" /> Add Invoice
</Link>
@@ -51,8 +40,8 @@ export default async function InvoicesPage() {
</div>
</div>
<HydrateClient>
<UniversalTable resource="invoices" />
<InvoicesTable />
</HydrateClient>
</div>
);
}
}
+9 -1
View File
@@ -7,7 +7,15 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
<>
<Navbar />
<Sidebar />
<main className="min-h-screen pt-24 ml-70">
{/* Mobile layout - no left margin */}
<main className="min-h-screen pt-24 md:hidden">
<div className="px-4 sm:px-6 pt-4 pb-6">
<DashboardBreadcrumbs />
{children}
</div>
</main>
{/* Desktop layout - with sidebar margin */}
<main className="min-h-screen pt-24 hidden md:block ml-70">
<div className="px-8 pt-6 pb-6">
<DashboardBreadcrumbs />
{children}
+15 -175
View File
@@ -1,191 +1,31 @@
import { redirect } from "next/navigation";
import { auth } from "~/server/auth";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import {
Users,
FileText,
TrendingUp,
Calendar,
Plus,
ArrowRight
} from "lucide-react";
import Link from "next/link";
import { api, HydrateClient } from "~/trpc/server";
import {
DashboardStats,
DashboardCards,
DashboardActivity,
} from "./_components/dashboard-components";
export default async function DashboardPage() {
const session = await auth();
if (!session?.user) {
redirect("/auth/signin");
}
return (
<div>
{/* Header */}
<div className="mb-8">
<h1 className="text-4xl font-bold bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">
Welcome back, {session.user.name?.split(" ")[0] ?? "User"}!
<h1 className="bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-4xl font-bold text-transparent">
Welcome back, {session?.user?.name?.split(" ")[0] ?? "User"}!
</h1>
<p className="text-gray-600 mt-2 text-lg">
<p className="mt-2 text-lg text-gray-600">
Here&apos;s what&apos;s happening with your invoicing business
</p>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-700">Total Clients</CardTitle>
<div className="p-2 bg-emerald-100 rounded-lg">
<Users className="h-4 w-4 text-emerald-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-emerald-600">0</div>
<p className="text-xs text-gray-500">
+0 from last month
</p>
</CardContent>
</Card>
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-700">Total Invoices</CardTitle>
<div className="p-2 bg-blue-100 rounded-lg">
<FileText className="h-4 w-4 text-blue-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-blue-600">0</div>
<p className="text-xs text-gray-500">
+0 from last month
</p>
</CardContent>
</Card>
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-700">Revenue</CardTitle>
<div className="p-2 bg-teal-100 rounded-lg">
<TrendingUp className="h-4 w-4 text-teal-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-teal-600">$0</div>
<p className="text-xs text-gray-500">
+0% from last month
</p>
</CardContent>
</Card>
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-700">Pending Invoices</CardTitle>
<div className="p-2 bg-orange-100 rounded-lg">
<Calendar className="h-4 w-4 text-orange-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-orange-600">0</div>
<p className="text-xs text-gray-500">
Due this month
</p>
</CardContent>
</Card>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700">
<div className="p-2 bg-emerald-100 rounded-lg">
<Users className="h-5 w-5" />
</div>
Manage Clients
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-gray-600">
Add new clients and manage your existing client relationships.
</p>
<div className="flex gap-3">
<Button
asChild
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
>
<Link href="/dashboard/clients/new">
<Plus className="mr-2 h-4 w-4" />
Add Client
</Link>
</Button>
<Button
variant="outline"
asChild
className="border-gray-300 text-gray-700 hover:bg-gray-50 font-medium"
>
<Link href="/dashboard/clients">
View All Clients
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</CardContent>
</Card>
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700">
<div className="p-2 bg-emerald-100 rounded-lg">
<FileText className="h-5 w-5" />
</div>
Create Invoices
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-gray-600">
Generate professional invoices and track payments.
</p>
<div className="flex gap-3">
<Button
asChild
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
>
<Link href="/dashboard/invoices/new">
<Plus className="mr-2 h-4 w-4" />
New Invoice
</Link>
</Button>
<Button
variant="outline"
asChild
className="border-gray-300 text-gray-700 hover:bg-gray-50 font-medium"
>
<Link href="/dashboard/invoices">
View All Invoices
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
{/* Recent Activity */}
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
<CardHeader>
<CardTitle className="text-emerald-700">Recent Activity</CardTitle>
</CardHeader>
<CardContent>
<div className="text-center py-12 text-gray-500">
<div className="p-4 bg-gray-100 rounded-full w-20 h-20 mx-auto mb-4 flex items-center justify-center">
<FileText className="h-8 w-8 text-gray-400" />
</div>
<p className="text-lg font-medium mb-2">No recent activity</p>
<p className="text-sm">Start by adding your first client or creating an invoice</p>
</div>
</CardContent>
</Card>
<HydrateClient>
<DashboardStats />
<DashboardCards />
<DashboardActivity />
</HydrateClient>
</div>
);
}
}
+20
View File
@@ -0,0 +1,20 @@
import { Navbar } from "~/components/Navbar";
import { Sidebar } from "~/components/Sidebar";
export default function InvoicesLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<Navbar />
<div className="flex">
<Sidebar />
<main className="flex-1 min-h-screen bg-background">
{children}
</main>
</div>
</>
);
}
+42
View File
@@ -0,0 +1,42 @@
import Link from "next/link";
import { auth } from "~/server/auth";
import { api, HydrateClient } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { InvoiceList } from "~/components/invoice-list";
import { Plus } from "lucide-react";
export default async function InvoicesPage() {
const session = await auth();
if (!session?.user) {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">Access Denied</h1>
<p className="text-muted-foreground mb-8">Please sign in to view invoices</p>
<Link href="/api/auth/signin">
<Button size="lg">Sign In</Button>
</Link>
</div>
</div>
);
}
// Prefetch invoices data
void api.invoices.getAll.prefetch();
return (
<HydrateClient>
<div className="p-6">
<div className="mb-8">
<h2 className="text-3xl font-bold mb-2">Invoices</h2>
<p className="text-muted-foreground">
Manage your invoices and payments
</p>
</div>
<InvoiceList />
</div>
</HydrateClient>
);
}