again you are iether lying or simply incapable. dont try again
This commit is contained in:
@@ -1,85 +0,0 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardFooter,
|
||||
} from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Clipboard } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
export default function EmbedPage() {
|
||||
const [embedCode, setEmbedCode] = useState('');
|
||||
const [origin, setOrigin] = useState('');
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
// This ensures window is defined, as it's only available on the client
|
||||
if (typeof window !== 'undefined') {
|
||||
const currentOrigin = window.location.origin;
|
||||
setOrigin(currentOrigin);
|
||||
setEmbedCode(
|
||||
`<iframe\n src="${currentOrigin}"\n width="100%"\n height="800px"\n style="border:none; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);"\n title="EstimateFlow"\n></iframe>`
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(embedCode).then(
|
||||
() => {
|
||||
toast({
|
||||
title: 'Copied to Clipboard',
|
||||
description: 'The embed code has been copied successfully.',
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
toast({
|
||||
title: 'Failed to Copy',
|
||||
description: 'Could not copy the code. Please try again.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
console.error('Could not copy text: ', err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="max-w-4xl">
|
||||
<CardHeader>
|
||||
<CardTitle>Embed EstimateFlow</CardTitle>
|
||||
<CardDescription>
|
||||
Copy and paste the code below into your website's HTML to embed the
|
||||
cost estimator flow.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="embed-code">HTML Embed Code</Label>
|
||||
<Textarea
|
||||
id="embed-code"
|
||||
readOnly
|
||||
value={embedCode}
|
||||
className="font-code h-48 min-h-48 resize-none bg-muted"
|
||||
aria-label="Embed code"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-end">
|
||||
<Button onClick={handleCopy}>
|
||||
<Clipboard className="mr-2" />
|
||||
Copy Code
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarInset,
|
||||
SidebarTrigger,
|
||||
SidebarFooter,
|
||||
} from '@/components/ui/sidebar';
|
||||
import { LayoutDashboard, LogOut, Settings, Mail, User as UserIcon, Code2 } from 'lucide-react';
|
||||
import { signOut } from '@/lib/auth';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { auth } from '@/app/api/auth/[...nextauth]/route';
|
||||
|
||||
export default async function AdminLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const session = await auth();
|
||||
const user = session?.user;
|
||||
|
||||
const userInitials = user?.name
|
||||
? user.name
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join('')
|
||||
: '';
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<Sidebar>
|
||||
<SidebarHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-primary size-8 rounded-lg" />
|
||||
<span className="text-lg font-semibold">Admin</span>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<Link href="/admin" className="w-full">
|
||||
<SidebarMenuButton>
|
||||
<LayoutDashboard />
|
||||
Dashboard
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<Link href="/admin/embed" className='w-full'>
|
||||
<SidebarMenuButton>
|
||||
<Code2 />
|
||||
Embed
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<Settings />
|
||||
Settings
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuSub>
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuSubButton href="/admin/settings/user" >
|
||||
<UserIcon />
|
||||
User
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuSubButton href="/admin/settings/email" >
|
||||
<Mail />
|
||||
Email
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuSubButton href="/admin/settings/hourly-rate" >
|
||||
<Settings />
|
||||
Hourly Rate
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<form action={signOut}>
|
||||
<Button variant="ghost" className="w-full justify-start">
|
||||
<LogOut className="mr-2" />
|
||||
Sign Out
|
||||
</Button>
|
||||
</form>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
<SidebarInset>
|
||||
<header className="flex h-14 items-center justify-between border-b bg-background px-4 lg:h-[60px] lg:px-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<SidebarTrigger className="md:hidden"/>
|
||||
<h1 className="text-lg font-semibold md:text-2xl">Dashboard</h1>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary" size="icon" className="rounded-full">
|
||||
<Avatar>
|
||||
<AvatarImage src={`https://placehold.co/100x100.png`} alt={user?.name ?? ''} />
|
||||
<AvatarFallback>{userInitials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="sr-only">Toggle user menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>{user?.name}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/admin/settings/user">Settings</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<form action={signOut} className="w-full">
|
||||
<DropdownMenuItem asChild>
|
||||
<button type="submit" className="w-full text-left">
|
||||
Sign Out
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
</form>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</header>
|
||||
<main className="flex-1 p-4 lg:p-6">{children}</main>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { DollarSign, Users, Activity, Clock } from "lucide-react";
|
||||
|
||||
export default function AdminDashboard() {
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
|
||||
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">$45,231.89</div>
|
||||
<p className="text-xs text-muted-foreground">+20.1% from last month</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">New Users</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">+2350</div>
|
||||
<p className="text-xs text-muted-foreground">+180.1% from last month</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Active Projects</CardTitle>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">+573</div>
|
||||
<p className="text-xs text-muted-foreground">+20 since last hour</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Average Estimate</CardTitle>
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">128 hours</div>
|
||||
<p className="text-xs text-muted-foreground">Based on recent activity</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import { useForm, type SubmitHandler } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useState, useTransition } from "react";
|
||||
import { sendTestEmail } from "@/lib/actions/email";
|
||||
import { Mail } from "lucide-react";
|
||||
|
||||
const emailSettingsSchema = z.object({
|
||||
smtpHost: z.string().min(1, "SMTP Host is required"),
|
||||
smtpPort: z.coerce.number().min(1, "Port is required"),
|
||||
smtpUsername: z.string().min(1, "Username is required"),
|
||||
smtpPassword: z.string().min(1, "Password is required"),
|
||||
fromName: z.string().min(1, "From Name is required"),
|
||||
fromEmail: z.string().email("Invalid email address"),
|
||||
});
|
||||
|
||||
type EmailSettingsFormValues = z.infer<typeof emailSettingsSchema>;
|
||||
|
||||
export default function EmailSettingsPage() {
|
||||
const { toast } = useToast();
|
||||
const [isSaving, startSavingTransition] = useTransition();
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
getValues,
|
||||
formState: { errors },
|
||||
} = useForm<EmailSettingsFormValues>({
|
||||
resolver: zodResolver(emailSettingsSchema),
|
||||
defaultValues: {
|
||||
// In a real application, you would fetch these values from a secure backend
|
||||
smtpHost: "smtp.example.com",
|
||||
smtpPort: 587,
|
||||
smtpUsername: "user@example.com",
|
||||
fromName: "EstimateFlow",
|
||||
fromEmail: "noreply@estimateflow.com",
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<EmailSettingsFormValues> = async (data) => {
|
||||
startSavingTransition(async () => {
|
||||
// Here you would typically send the data to your backend to save it
|
||||
console.log("Saving email settings:", data);
|
||||
|
||||
// Simulate an API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
toast({
|
||||
title: "Settings Saved",
|
||||
description: "Your SMTP email settings have been updated successfully.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleTestEmail = async () => {
|
||||
setIsTesting(true);
|
||||
try {
|
||||
const settings = getValues();
|
||||
const result = await sendTestEmail(settings);
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: "Test Email Sent",
|
||||
description: "The test email was sent successfully.",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Failed to Send Email",
|
||||
description: result.error || "An unknown error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "An unexpected error occurred while sending the test email.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="max-w-4xl">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<CardHeader>
|
||||
<CardTitle>Email Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Configure your SMTP settings to send emails from the platform.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-6">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="smtpHost">SMTP Host</Label>
|
||||
<Input
|
||||
id="smtpHost"
|
||||
{...register("smtpHost")}
|
||||
/>
|
||||
{errors.smtpHost && <p className="text-sm text-destructive">{errors.smtpHost.message}</p>}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="smtpPort">SMTP Port</Label>
|
||||
<Input
|
||||
id="smtpPort"
|
||||
type="number"
|
||||
{...register("smtpPort")}
|
||||
/>
|
||||
{errors.smtpPort && <p className="text-sm text-destructive">{errors.smtpPort.message}</p>}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="smtpUsername">SMTP Username</Label>
|
||||
<Input
|
||||
id="smtpUsername"
|
||||
{...register("smtpUsername")}
|
||||
/>
|
||||
{errors.smtpUsername && <p className="text-sm text-destructive">{errors.smtpUsername.message}</p>}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="smtpPassword">SMTP Password</Label>
|
||||
<Input
|
||||
id="smtpPassword"
|
||||
type="password"
|
||||
{...register("smtpPassword")}
|
||||
/>
|
||||
{errors.smtpPassword && <p className="text-sm text-destructive">{errors.smtpPassword.message}</p>}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="fromName">From Name</Label>
|
||||
<Input
|
||||
id="fromName"
|
||||
{...register("fromName")}
|
||||
/>
|
||||
{errors.fromName && <p className="text-sm text-destructive">{errors.fromName.message}</p>}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="fromEmail">From Email</Label>
|
||||
<Input
|
||||
id="fromEmail"
|
||||
type="email"
|
||||
{...register("fromEmail")}
|
||||
/>
|
||||
{errors.fromEmail && <p className="text-sm text-destructive">{errors.fromEmail.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={handleTestEmail} disabled={isTesting}>
|
||||
{isTesting ? 'Sending...' : <> <Mail className="mr-2 h-4 w-4" /> Send Test Email</> }
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSaving}>{isSaving ? 'Saving...' : 'Save Settings'}</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useTransition } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { getSetting, setSetting } from '@/lib/actions/settings';
|
||||
|
||||
const HourlyRateSettingsPage = () => {
|
||||
const { toast } = useToast();
|
||||
const [hourlyRate, setHourlyRate] = useState('');
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchHourlyRate() {
|
||||
const rate = await getSetting('hourly_rate');
|
||||
if (rate) {
|
||||
setHourlyRate(rate);
|
||||
}
|
||||
}
|
||||
fetchHourlyRate();
|
||||
}, []);
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await setSetting('hourly_rate', hourlyRate);
|
||||
toast({ title: "Hourly Rate Saved", description: "The hourly rate has been updated successfully." });
|
||||
} catch (error) {
|
||||
console.error("Failed to save hourly rate:", error);
|
||||
toast({ title: "Error", description: "Failed to save hourly rate.", variant: "destructive" });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="max-w-2xl">
|
||||
<CardHeader>
|
||||
<CardTitle>Hourly Rate Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Set the default hourly rate used for project estimations.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="hourlyRate">Hourly Rate ($)</Label>
|
||||
<Input
|
||||
id="hourlyRate"
|
||||
name="hourlyRate"
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="e.g., 100.00"
|
||||
value={hourlyRate}
|
||||
onChange={(e) => setHourlyRate(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="self-start" disabled={isPending}>
|
||||
{isPending ? 'Saving...' : 'Save Hourly Rate'}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default HourlyRateSettingsPage;
|
||||
@@ -1,156 +0,0 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import { useForm, type SubmitHandler } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
import { getUser, updateUser } from "@/lib/actions/user";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const userProfileSchema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
email: z.string().email("Invalid email address"),
|
||||
password: z.string().min(6, "Password must be at least 6 characters").optional().or(z.literal('')),
|
||||
confirmPassword: z.string().optional(),
|
||||
}).refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
|
||||
type UserProfileFormValues = z.infer<typeof userProfileSchema>;
|
||||
|
||||
export default function UserProfilePage() {
|
||||
const { toast } = useToast();
|
||||
const [isSaving, startSavingTransition] = useTransition();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isDirty },
|
||||
} = useForm<UserProfileFormValues>({
|
||||
resolver: zodResolver(userProfileSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: ""
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
setIsLoading(true);
|
||||
const user = await getUser();
|
||||
if (user) {
|
||||
reset({
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
password: "",
|
||||
confirmPassword: ""
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Could not load user profile.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
fetchUser();
|
||||
}, [reset, toast]);
|
||||
|
||||
const onSubmit: SubmitHandler<UserProfileFormValues> = (data) => {
|
||||
startSavingTransition(async () => {
|
||||
const result = await updateUser({
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
password: data.password || undefined,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: "Profile Updated",
|
||||
description: "Your profile has been updated successfully.",
|
||||
});
|
||||
reset({ ...data, password: '', confirmPassword: '' });
|
||||
} else {
|
||||
toast({
|
||||
title: "Update Failed",
|
||||
description: result.error || "An unknown error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="max-w-2xl">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<CardHeader>
|
||||
<CardTitle>My Account</CardTitle>
|
||||
<CardDescription>
|
||||
Update your account details. Leave password fields blank to keep the current password.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-6">
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input id="name" {...register("name")} />
|
||||
{errors.name && <p className="text-sm text-destructive">{errors.name.message}</p>}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" {...register("email")} />
|
||||
{errors.email && <p className="text-sm text-destructive">{errors.email.message}</p>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">New Password</Label>
|
||||
<Input id="password" type="password" {...register("password")} />
|
||||
{errors.password && <p className="text-sm text-destructive">{errors.password.message}</p>}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="confirmPassword">Confirm New Password</Label>
|
||||
<Input id="confirmPassword" type="password" {...register("confirmPassword")} />
|
||||
{errors.confirmPassword && <p className="text-sm text-destructive">{errors.confirmPassword.message}</p>}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-end">
|
||||
<Button type="submit" disabled={isSaving || !isDirty || isLoading}>
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
|
||||
import NextAuth from 'next-auth';
|
||||
import CredentialsProvider from 'next-auth/providers/credentials';
|
||||
import db from '@/lib/db';
|
||||
import type { User } from '@/lib/types';
|
||||
|
||||
export const {
|
||||
handlers: { GET, POST },
|
||||
auth,
|
||||
signIn,
|
||||
signOut,
|
||||
} = NextAuth({
|
||||
secret: process.env.AUTH_SECRET,
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
name: 'Credentials',
|
||||
type: 'credentials',
|
||||
credentials: {
|
||||
email: { label: 'Email', type: 'email' },
|
||||
password: { label: 'Password', type: 'password' },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials?.email || !credentials.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const email = credentials.email as string;
|
||||
const password = credentials.password as string;
|
||||
|
||||
try {
|
||||
const stmt = db.prepare('SELECT * FROM users WHERE email = ?');
|
||||
const user = stmt.get(email) as User | undefined;
|
||||
|
||||
if (user && user.password === password) {
|
||||
return {
|
||||
id: user.id.toString(),
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Database error during authorization:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
session({ session, token }) {
|
||||
if (session.user) {
|
||||
session.user.id = token.id as string;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: '/login',
|
||||
},
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
|
||||
import { LogIn } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { signIn } from '@/app/api/auth/[...nextauth]/route';
|
||||
|
||||
async function handleSignIn(formData: FormData) {
|
||||
'use server';
|
||||
await signIn('credentials', formData);
|
||||
}
|
||||
|
||||
export default async function LoginPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-2xl">Admin Login</CardTitle>
|
||||
<CardDescription>Enter your credentials to access the dashboard.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form action={handleSignIn} className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="admin@example.com"
|
||||
required
|
||||
defaultValue="admin@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
required
|
||||
defaultValue="password"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
<LogIn className="mr-2 h-4 w-4" /> Sign in
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { CostEstimatorForm } from '@/components/cost-estimator/cost-estimator-form';
|
||||
import { suggestFeatures } from '@/ai/flows/suggest-features';
|
||||
import { classifyComplexity } from '@/ai/flows/classify-complexity';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import nodemailer from 'nodemailer';
|
||||
import { z } from 'zod';
|
||||
|
||||
const emailSettingsSchema = z.object({
|
||||
smtpHost: z.string(),
|
||||
smtpPort: z.number(),
|
||||
smtpUsername: z.string(),
|
||||
smtpPassword: z.string(),
|
||||
fromName: z.string(),
|
||||
fromEmail: z.string().email(),
|
||||
});
|
||||
|
||||
type EmailSettings = z.infer<typeof emailSettingsSchema>;
|
||||
|
||||
export async function sendTestEmail(
|
||||
settings: EmailSettings
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const { smtpHost, smtpPort, smtpUsername, smtpPassword, fromName, fromEmail } = settings;
|
||||
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: smtpHost,
|
||||
port: smtpPort,
|
||||
secure: smtpPort === 465, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: smtpUsername,
|
||||
pass: smtpPassword,
|
||||
},
|
||||
// In a real app, you might want more robust TLS options
|
||||
// For example, rejecting unauthorized connections
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
});
|
||||
|
||||
await transporter.verify();
|
||||
|
||||
const mailOptions = {
|
||||
from: `"${fromName}" <${fromEmail}>`,
|
||||
to: fromEmail, // Send the test to the 'from' address
|
||||
subject: 'SMTP Test Email from EstimateFlow',
|
||||
text: 'This is a test email to verify your SMTP settings. If you received this, your configuration is correct.',
|
||||
html: '<p>This is a test email to verify your SMTP settings. If you received this, your configuration is correct.</p>',
|
||||
};
|
||||
|
||||
await transporter.sendMail(mailOptions);
|
||||
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
console.error('Failed to send test email:', error);
|
||||
// Provide a more user-friendly error message
|
||||
let errorMessage = 'An unknown error occurred.';
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
errorMessage = `Connection refused. Check if the SMTP host (${smtpHost}) and port (${smtpPort}) are correct and accessible.`;
|
||||
} else if (error.code === 'EAUTH') {
|
||||
errorMessage = 'Authentication failed. Please check your SMTP username and password.';
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import db from '@/lib/db';
|
||||
import { z } from 'zod';
|
||||
import { auth } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { User as DbUser } from '@/lib/types';
|
||||
|
||||
const UserUpdateSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
email: z.string().email('Invalid email address'),
|
||||
password: z.string().optional(),
|
||||
});
|
||||
|
||||
type UserForClient = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export async function getUser(): Promise<UserForClient | null> {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
console.error('getUser: Not authenticated');
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const stmt = db.prepare('SELECT id, name, email FROM users WHERE id = ?');
|
||||
const user = stmt.get(session.user.id) as DbUser | undefined;
|
||||
if (!user) {
|
||||
console.error('getUser: User not found in DB');
|
||||
return null;
|
||||
}
|
||||
return { id: user.id.toString(), name: user.name, email: user.email };
|
||||
} catch (error) {
|
||||
console.error('Failed to get user:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateUser(
|
||||
data: z.infer<typeof UserUpdateSchema>
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return { success: false, error: 'Not authenticated. Please log in again.' };
|
||||
}
|
||||
|
||||
const validated = UserUpdateSchema.safeParse(data);
|
||||
if (!validated.success) {
|
||||
const errors = validated.error.flatten().fieldErrors;
|
||||
const firstError = Object.values(errors)[0]?.[0] ?? 'Invalid data provided.';
|
||||
return { success: false, error: firstError };
|
||||
}
|
||||
|
||||
const { name, email, password } = validated.data;
|
||||
const userId = session.user.id;
|
||||
|
||||
try {
|
||||
// Check if the new email is already in use by another user
|
||||
const existingUserStmt = db.prepare('SELECT id FROM users WHERE email = ? AND id != ?');
|
||||
const existingUser = existingUserStmt.get(email, userId);
|
||||
|
||||
if (existingUser) {
|
||||
return { success: false, error: 'Email already in use by another account.' };
|
||||
}
|
||||
|
||||
// Determine if a new password was provided
|
||||
if (password && password.trim().length > 0) {
|
||||
// If a new password is provided, update name, email, and password
|
||||
const stmt = db.prepare(
|
||||
'UPDATE users SET name = ?, email = ?, password = ? WHERE id = ?'
|
||||
);
|
||||
stmt.run(name, email, password, userId);
|
||||
} else {
|
||||
// If no new password, update only name and email
|
||||
const stmt = db.prepare('UPDATE users SET name = ?, email = ? WHERE id = ?');
|
||||
stmt.run(name, email, userId);
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
console.error('Failed to update user:', error);
|
||||
return { success: false, error: 'Failed to update user profile due to a server error.' };
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
'use server';
|
||||
import { signOut as nextAuthSignOut, auth as nextAuth } from '@/app/api/auth/[...nextauth]/route';
|
||||
|
||||
export async function signOut() {
|
||||
await nextAuthSignOut({ redirectTo: '/login' });
|
||||
}
|
||||
|
||||
export async function auth() {
|
||||
return nextAuth();
|
||||
}
|
||||
@@ -5,16 +5,3 @@ export interface User {
|
||||
password?: string; // Should be handled securely, not sent to client
|
||||
name: string;
|
||||
}
|
||||
|
||||
// Augment the default NextAuth session and user types
|
||||
declare module 'next-auth' {
|
||||
interface Session {
|
||||
user: {
|
||||
id: string;
|
||||
} & DefaultSession['user'];
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user