init: lab project bootstrapped from isletmem-kovakcrm
- CRM domain modules removed (customers, services, software, calendar, tasks, invoices, leads, finance, etc.)
- DLS branding: package name=lab, logo wordmark, sidebar nav, header CTA
- Tenant layer extended with kind dimension (lab|clinic) + requireTenantKind helper
- Schema rewritten for DLS domain: jobs, job_files, job_status_history, prosthetics, connections, finance_entries, notifications
- Onboarding form: clinic/lab account-type selection + auto-generated memberNumber
- Placeholder routes for jobs/{inbound,outbound,new}, products, finance, connections
- PDF spec + spec.md under belgeler/
- db: lab database + 13 collections + indexes + storage bucket (job-files) provisioned via Appwrite MCP
Ref: belgeler/dls-ui-tasarim.pdf
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useRouter } from "next/navigation"
|
||||
import Image from "next/image"
|
||||
|
||||
export function ForbiddenError() {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className='mx-auto flex min-h-dvh flex-col items-center justify-center gap-8 p-8 md:gap-12 md:p-16'>
|
||||
<Image
|
||||
src='https://ui.shadcn.com/placeholder.svg'
|
||||
alt='placeholder image'
|
||||
width={960}
|
||||
height={540}
|
||||
className='aspect-video w-240 rounded-xl object-cover dark:brightness-[0.95] dark:invert'
|
||||
/>
|
||||
<div className='text-center'>
|
||||
<h1 className='mb-4 text-3xl font-bold'>403</h1>
|
||||
<h2 className="mb-3 text-2xl font-semibold">Forbidden</h2>
|
||||
<p>Access to this resource is forbidden. You don't have the necessary permissions to view this page.</p>
|
||||
<div className='mt-6 flex items-center justify-center gap-4 md:mt-8'>
|
||||
<Button className='cursor-pointer' onClick={() => router.push('/dashboard')}>Go Back Home</Button>
|
||||
<Button variant='outline' className='flex cursor-pointer items-center gap-1' onClick={() => router.push('#')}>
|
||||
Contact Us
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ForbiddenError } from "./components/forbidden-error"
|
||||
|
||||
export default function ForbiddenPage() {
|
||||
return <ForbiddenError />
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useRouter } from "next/navigation"
|
||||
import Image from "next/image"
|
||||
|
||||
export function InternalServerError() {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className='mx-auto flex min-h-dvh flex-col items-center justify-center gap-8 p-8 md:gap-12 md:p-16'>
|
||||
<Image
|
||||
src='https://ui.shadcn.com/placeholder.svg'
|
||||
alt='placeholder image'
|
||||
width={960}
|
||||
height={540}
|
||||
className='aspect-video w-240 rounded-xl object-cover dark:brightness-[0.95] dark:invert'
|
||||
/>
|
||||
<div className='text-center'>
|
||||
<h1 className='mb-4 text-3xl font-bold'>500</h1>
|
||||
<h2 className="mb-3 text-2xl font-semibold">Internal Server Error</h2>
|
||||
<p>Something went wrong on our end. We're working to fix the issue. Please try again later.</p>
|
||||
<div className='mt-6 flex items-center justify-center gap-4 md:mt-8'>
|
||||
<Button className='cursor-pointer' onClick={() => router.push('/dashboard')}>Go Back Home</Button>
|
||||
<Button variant='outline' className='flex cursor-pointer items-center gap-1' onClick={() => router.push('#')}>
|
||||
Contact Us
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { InternalServerError } from "./components/internal-server-error"
|
||||
|
||||
export default function InternalServerErrorPage() {
|
||||
return <InternalServerError />
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useRouter } from "next/navigation"
|
||||
import Image from "next/image"
|
||||
|
||||
export function NotFoundError() {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className='mx-auto flex min-h-dvh flex-col items-center justify-center gap-8 p-8 md:gap-12 md:p-16'>
|
||||
<Image
|
||||
src='https://ui.shadcn.com/placeholder.svg'
|
||||
alt='placeholder image'
|
||||
width={960}
|
||||
height={540}
|
||||
className='aspect-video w-240 rounded-xl object-cover dark:brightness-[0.95] dark:invert'
|
||||
/>
|
||||
<div className='text-center'>
|
||||
<h1 className='mb-4 text-3xl font-bold'>404</h1>
|
||||
<h2 className="mb-3 text-2xl font-semibold">Page Not Found</h2>
|
||||
<p>The page you are looking for doesn't exist or has been moved to another location.</p>
|
||||
<div className='mt-6 flex items-center justify-center gap-4 md:mt-8'>
|
||||
<Button className='cursor-pointer' onClick={() => router.push('/dashboard')}>Go Back Home</Button>
|
||||
<Button variant='outline' className='flex cursor-pointer items-center gap-1' onClick={() => router.push('#')}>
|
||||
Contact Us
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { NotFoundError } from "./components/not-found-error"
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return <NotFoundError />
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useRouter } from "next/navigation"
|
||||
import Image from "next/image"
|
||||
|
||||
export function UnauthorizedError() {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className='mx-auto flex min-h-dvh flex-col items-center justify-center gap-8 p-8 md:gap-12 md:p-16'>
|
||||
<Image
|
||||
src='https://ui.shadcn.com/placeholder.svg'
|
||||
alt='placeholder image'
|
||||
width={960}
|
||||
height={540}
|
||||
className='aspect-video w-240 rounded-xl object-cover dark:brightness-[0.95] dark:invert'
|
||||
/>
|
||||
<div className='text-center'>
|
||||
<h1 className='mb-4 text-3xl font-bold'>401</h1>
|
||||
<h2 className="mb-3 text-2xl font-semibold">Unauthorized</h2>
|
||||
<p>You don't have permission to access this resource. Please sign in or contact your administrator.</p>
|
||||
<div className='mt-6 flex items-center justify-center gap-4 md:mt-8'>
|
||||
<Button className='cursor-pointer' onClick={() => router.push('/dashboard')}>Go Back Home</Button>
|
||||
<Button variant='outline' className='flex cursor-pointer items-center gap-1' onClick={() => router.push('#')}>
|
||||
Contact Us
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { UnauthorizedError } from "./components/unauthorized-error"
|
||||
|
||||
export default function UnauthorizedPage() {
|
||||
return <UnauthorizedError />
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useRouter } from "next/navigation"
|
||||
import Image from "next/image"
|
||||
|
||||
export function UnderMaintenanceError() {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className='mx-auto flex min-h-dvh flex-col items-center justify-center gap-8 p-8 md:gap-12 md:p-16'>
|
||||
<Image
|
||||
src='https://ui.shadcn.com/placeholder.svg'
|
||||
alt='placeholder image'
|
||||
width={960}
|
||||
height={540}
|
||||
className='aspect-video w-240 rounded-xl object-cover dark:brightness-[0.95] dark:invert'
|
||||
/>
|
||||
<div className='text-center'>
|
||||
<h1 className='mb-4 text-3xl font-bold'>503</h1>
|
||||
<h2 className="mb-3 text-2xl font-semibold">Under Maintenance</h2>
|
||||
<p>The service is currently unavailable. Please try again later.</p>
|
||||
<div className='mt-6 flex items-center justify-center gap-4 md:mt-8'>
|
||||
<Button className='cursor-pointer' onClick={() => router.push('/dashboard')}>Go Back Home</Button>
|
||||
<Button variant='outline' className='flex cursor-pointer items-center gap-1' onClick={() => router.push('#')}>
|
||||
Contact Us
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { UnderMaintenanceError } from "./components/under-maintenance-error"
|
||||
|
||||
export default function UnderMaintenancePage() {
|
||||
return <UnderMaintenanceError />
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
export function ForgotPasswordForm2({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"form">) {
|
||||
return (
|
||||
<form className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<h1 className="text-2xl font-bold">Forgot your password?</h1>
|
||||
<p className="text-muted-foreground text-sm text-balance">
|
||||
Enter your email address and we'll send you a link to reset your password
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" placeholder="m@example.com" required />
|
||||
</div>
|
||||
<Button type="submit" className="w-full cursor-pointer">
|
||||
Send Reset Link
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-center text-sm">
|
||||
Remember your password?{" "}
|
||||
<a href="/auth/sign-in-2" className="underline underline-offset-4">
|
||||
Back to sign in
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ForgotPasswordForm2 } from "./components/forgot-password-form-2"
|
||||
import { Logo } from "@/components/logo"
|
||||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
|
||||
export default function ForgotPassword2Page() {
|
||||
return (
|
||||
<div className="grid min-h-svh lg:grid-cols-2">
|
||||
<div className="flex flex-col gap-4 p-6 md:p-10">
|
||||
<div className="flex justify-center gap-2 md:justify-start">
|
||||
<Link href="/" className="flex items-center gap-2 font-medium">
|
||||
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md">
|
||||
<Logo size={24} />
|
||||
</div>
|
||||
ShadcnStore
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="w-full max-w-md">
|
||||
<ForgotPasswordForm2 />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-muted relative hidden lg:block">
|
||||
<Image
|
||||
src="https://ui.shadcn.com/placeholder.svg"
|
||||
alt="Image"
|
||||
fill
|
||||
className="object-cover dark:brightness-[0.95] dark:invert"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Logo } from "@/components/logo"
|
||||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
|
||||
export function ForgotPasswordForm3({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card className="overflow-hidden p-0">
|
||||
<CardContent className="grid p-0 md:grid-cols-2">
|
||||
<form className="p-6 md:p-8">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex justify-center mb-2">
|
||||
<Link href="/" className="flex items-center gap-2 font-medium">
|
||||
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md">
|
||||
<Logo size={24} />
|
||||
</div>
|
||||
<span className="text-xl">ShadcnStore</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<h1 className="text-2xl font-bold">Forgot your password?</h1>
|
||||
<p className="text-muted-foreground text-balance">
|
||||
Enter your email to reset your ShadcnStore account password
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full cursor-pointer">
|
||||
Send Reset Link
|
||||
</Button>
|
||||
<div className="text-center text-sm">
|
||||
Remember your password?{" "}
|
||||
<a href="/auth/sign-in-3" className="underline underline-offset-4">
|
||||
Back to sign in
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div className="bg-muted relative hidden md:block">
|
||||
<Image
|
||||
src="https://ui.shadcn.com/placeholder.svg"
|
||||
alt="Image"
|
||||
fill
|
||||
className="object-cover dark:brightness-[0.95] dark:invert"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="text-muted-foreground *:[a]:hover:text-primary text-center text-xs text-balance *:[a]:underline *:[a]:underline-offset-4">
|
||||
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
|
||||
and <a href="#">Privacy Policy</a>.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ForgotPasswordForm3 } from "./components/forgot-password-form-3"
|
||||
|
||||
export default function ForgotPassword3Page() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background p-4">
|
||||
<ForgotPasswordForm3 className="w-full max-w-sm md:max-w-4xl" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useActionState } from "react";
|
||||
import { ArrowLeft, Loader2, MailCheck } 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 { cn } from "@/lib/utils";
|
||||
import { requestPasswordResetAction } from "@/lib/appwrite/password-reset-actions";
|
||||
import { initialAuthState } from "@/lib/appwrite/auth-types";
|
||||
|
||||
export function ForgotPasswordForm1({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const [state, formAction, isPending] = useActionState(requestPasswordResetAction, initialAuthState);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl">Şifremi unuttum</CardTitle>
|
||||
<CardDescription>
|
||||
Email adresinizi girin, sıfırlama bağlantısı gönderelim.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{state.ok ? (
|
||||
<div className="flex flex-col items-center gap-3 py-4 text-center">
|
||||
<div className="bg-primary/10 text-primary flex size-12 items-center justify-center rounded-full">
|
||||
<MailCheck className="size-6" />
|
||||
</div>
|
||||
<p className="text-sm">
|
||||
Sıfırlama kodunuz e-posta adresinize gönderildi. Kodu girerek şifrenizi yenileyebilirsiniz.
|
||||
</p>
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className="text-muted-foreground hover:text-foreground mt-2 flex items-center gap-1 text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
<ArrowLeft className="size-3.5" />
|
||||
Giriş sayfasına dön
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<form action={formAction} className="flex flex-col gap-4">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="ornek@firma.com"
|
||||
autoComplete="email"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{state.error && (
|
||||
<p className="text-destructive text-sm text-center" role="alert">
|
||||
{state.error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Gönderiliyor...
|
||||
</>
|
||||
) : (
|
||||
"Sıfırlama bağlantısı gönder"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className="text-muted-foreground hover:text-foreground flex items-center justify-center gap-1 text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
<ArrowLeft className="size-3.5" />
|
||||
Giriş sayfasına dön
|
||||
</Link>
|
||||
</form>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { ForgotPasswordForm1 } from "./components/forgot-password-form-1";
|
||||
import { Logo } from "@/components/logo";
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
return (
|
||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||
<Link href="/" className="flex items-center gap-2 self-center font-medium">
|
||||
<div className="bg-primary text-primary-foreground flex size-9 items-center justify-center rounded-md">
|
||||
<Logo size={24} />
|
||||
</div>
|
||||
<span className="text-lg font-semibold">DLS</span>
|
||||
</Link>
|
||||
<ForgotPasswordForm1 />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "DLS — Giriş",
|
||||
description: "DLS hesabınıza giriş yapın veya yeni hesap oluşturun.",
|
||||
};
|
||||
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
return <div className="min-h-screen bg-background">{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useActionState } from "react";
|
||||
import { ArrowLeft, Loader2, ShieldCheck } 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 { cn } from "@/lib/utils";
|
||||
import { resetPasswordAction } from "@/lib/appwrite/password-reset-actions";
|
||||
import { initialAuthState } from "@/lib/appwrite/auth-types";
|
||||
|
||||
interface Props extends React.ComponentProps<"div"> {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export function ResetPasswordForm({ token, className, ...props }: Props) {
|
||||
const [state, formAction, isPending] = useActionState(resetPasswordAction, initialAuthState);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<div className="bg-primary/10 text-primary mx-auto mb-2 flex size-12 items-center justify-center rounded-full">
|
||||
<ShieldCheck className="size-6" />
|
||||
</div>
|
||||
<CardTitle className="text-xl">Yeni şifre belirle</CardTitle>
|
||||
<CardDescription>
|
||||
Kod doğrulandı. Yeni şifrenizi girin.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form action={formAction} className="flex flex-col gap-4">
|
||||
<input type="hidden" name="token" value={token} />
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="password">Yeni şifre</Label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="En az 8 karakter"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
minLength={8}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="confirmPassword">Şifre tekrar</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
placeholder="Şifreyi tekrar girin"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{state.error && (
|
||||
<p className="text-destructive text-center text-sm" role="alert">
|
||||
{state.error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Güncelleniyor...
|
||||
</>
|
||||
) : (
|
||||
"Şifreyi güncelle"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className="text-muted-foreground hover:text-foreground flex items-center justify-center gap-1 text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
<ArrowLeft className="size-3.5" />
|
||||
Giriş sayfasına dön
|
||||
</Link>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import Link from "next/link";
|
||||
import { XCircle } from "lucide-react";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { verifyResetToken } from "@/lib/appwrite/password-reset-actions";
|
||||
import { ResetPasswordForm } from "./components/reset-password-form";
|
||||
|
||||
interface Props {
|
||||
searchParams: Promise<{ token?: string }>;
|
||||
}
|
||||
|
||||
export default async function ResetPasswordPage({ searchParams }: Props) {
|
||||
const { token } = await searchParams;
|
||||
|
||||
if (!token) {
|
||||
return <InvalidToken message="Geçersiz bağlantı. Yeni bir sıfırlama kodu talep edin." />;
|
||||
}
|
||||
|
||||
const { valid } = await verifyResetToken(token);
|
||||
|
||||
if (!valid) {
|
||||
return <InvalidToken message="Bu kod geçersiz veya süresi dolmuş. Yeni bir sıfırlama kodu talep edin." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-svh items-center justify-center p-6">
|
||||
<div className="w-full max-w-sm">
|
||||
<ResetPasswordForm token={token} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InvalidToken({ message }: { message: string }) {
|
||||
return (
|
||||
<div className="flex min-h-svh items-center justify-center p-6">
|
||||
<div className="w-full max-w-sm">
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<div className="text-destructive mx-auto mb-2 flex size-12 items-center justify-center rounded-full bg-red-50">
|
||||
<XCircle className="size-6" />
|
||||
</div>
|
||||
<CardTitle className="text-xl">Geçersiz kod</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center gap-4 text-center">
|
||||
<p className="text-muted-foreground text-sm">{message}</p>
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="text-primary text-sm underline underline-offset-4"
|
||||
>
|
||||
Yeni kod talep et
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
export function LoginForm2({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"form">) {
|
||||
return (
|
||||
<form className={cn("flex flex-col gap-6", className)} {...props} action="/dashboard">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<h1 className="text-2xl font-bold">Login to your account</h1>
|
||||
<p className="text-muted-foreground text-sm text-balance">
|
||||
Enter your email below to login to your account
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" placeholder="test@example.com" defaultValue="test@example.com" required />
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<a
|
||||
href="/auth/forgot-password-2"
|
||||
className="ml-auto text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input id="password" type="password" defaultValue="password" required />
|
||||
</div>
|
||||
<Button type="submit" className="w-full cursor-pointer">
|
||||
Login
|
||||
</Button>
|
||||
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
|
||||
<span className="bg-background text-muted-foreground relative z-10 px-2">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
<Button variant="outline" className="w-full cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Login with GitHub
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-center text-sm">
|
||||
Don't have an account?{" "}
|
||||
<a href="/auth/sign-up-2" className="underline underline-offset-4">
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { LoginForm2 } from "./components/login-form-2"
|
||||
import { Logo } from "@/components/logo"
|
||||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="grid min-h-svh lg:grid-cols-2">
|
||||
<div className="flex flex-col gap-4 p-6 md:p-10">
|
||||
<div className="flex justify-center gap-2 md:justify-start">
|
||||
<Link href="/" className="flex items-center gap-2 font-medium">
|
||||
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md">
|
||||
<Logo size={24} />
|
||||
</div>
|
||||
ShadcnStore
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="w-full max-w-md">
|
||||
<LoginForm2 />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-muted relative hidden lg:block">
|
||||
<Image
|
||||
src="https://ui.shadcn.com/placeholder.svg"
|
||||
alt="Image"
|
||||
fill
|
||||
className="object-cover dark:brightness-[0.95] dark:invert"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Logo } from "@/components/logo"
|
||||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
|
||||
export function LoginForm3({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card className="overflow-hidden p-0">
|
||||
<CardContent className="grid p-0 md:grid-cols-2">
|
||||
<form className="p-6 md:p-8" action="/dashboard">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex justify-center mb-2">
|
||||
<Link href="/" className="flex items-center gap-2 font-medium">
|
||||
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md">
|
||||
<Logo size={24} />
|
||||
</div>
|
||||
<span className="text-xl">ShadcnStore</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<h1 className="text-2xl font-bold">Welcome back</h1>
|
||||
<p className="text-muted-foreground text-balance">
|
||||
Login to your ShadcnStore account
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="test@example.com"
|
||||
defaultValue="test@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<a
|
||||
href="/auth/forgot-password-3"
|
||||
className="ml-auto text-sm underline-offset-2 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input id="password" type="password" defaultValue="password" required />
|
||||
</div>
|
||||
<Button type="submit" className="w-full cursor-pointer">
|
||||
Login
|
||||
</Button>
|
||||
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
|
||||
<span className="bg-card text-muted-foreground relative z-10 px-2">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Button variant="outline" type="button" className="w-full cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Login with Apple</span>
|
||||
</Button>
|
||||
<Button variant="outline" type="button" className="w-full cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Login with Google</span>
|
||||
</Button>
|
||||
<Button variant="outline" type="button" className="w-full cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Login with Meta</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-center text-sm">
|
||||
Don't have an account?{" "}
|
||||
<a href="/auth/sign-up-3" className="underline underline-offset-4">
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div className="bg-muted relative hidden md:block">
|
||||
<Image
|
||||
src="https://ui.shadcn.com/placeholder.svg"
|
||||
alt="Image"
|
||||
fill
|
||||
className="object-cover dark:brightness-[0.95] dark:invert"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="text-muted-foreground *:[a]:hover:text-primary text-center text-xs text-balance *:[a]:underline *:[a]:underline-offset-4">
|
||||
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
|
||||
and <a href="#">Privacy Policy</a>.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { LoginForm3 } from "./components/login-form-3"
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm md:max-w-4xl">
|
||||
<LoginForm3 />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useActionState } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Logo } from "@/components/logo";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { signInAction } from "@/lib/appwrite/auth-actions";
|
||||
import { initialAuthState } from "@/lib/appwrite/auth-types";
|
||||
|
||||
export function LoginForm1({
|
||||
className,
|
||||
inviteCode,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { inviteCode?: string }) {
|
||||
const [state, formAction, isPending] = useActionState(signInAction, initialAuthState);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card className="overflow-hidden p-0">
|
||||
<CardContent className="grid p-0 md:grid-cols-2">
|
||||
<form action={formAction} className="p-6 md:p-10">
|
||||
{inviteCode && <input type="hidden" name="inviteCode" value={inviteCode} />}
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex justify-center">
|
||||
<Link href="/" className="flex items-center gap-2 font-medium">
|
||||
<div className="bg-primary text-primary-foreground flex size-9 items-center justify-center rounded-md">
|
||||
<Logo size={22} />
|
||||
</div>
|
||||
<span className="text-xl font-semibold">DLS</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{inviteCode && (
|
||||
<p className="text-muted-foreground rounded-md border bg-muted/50 px-3 py-2 text-center text-xs">
|
||||
Davete katılmak için giriş yapın.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Tekrar hoş geldiniz</h1>
|
||||
<p className="text-muted-foreground text-sm text-balance mt-1">
|
||||
Hesabınıza giriş yaparak işletmenizi yönetmeye devam edin
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="ornek@firma.com"
|
||||
autoComplete="email"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Şifre</Label>
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="ml-auto text-xs text-muted-foreground hover:text-foreground underline-offset-4 hover:underline"
|
||||
>
|
||||
Şifremi unuttum
|
||||
</Link>
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{state.error && (
|
||||
<p className="text-destructive text-sm text-center" role="alert">
|
||||
{state.error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Giriş yapılıyor...
|
||||
</>
|
||||
) : (
|
||||
"Giriş yap"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Hesabınız yok mu?{" "}
|
||||
<Link
|
||||
href="/sign-up"
|
||||
className="text-foreground font-medium underline-offset-4 hover:underline"
|
||||
>
|
||||
Hesap oluştur
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<BrandPanel />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<p className="text-muted-foreground text-center text-xs text-balance">
|
||||
Giriş yaparak{" "}
|
||||
<Link href="#" className="underline-offset-4 hover:underline">
|
||||
Kullanım Şartları
|
||||
</Link>{" "}
|
||||
ve{" "}
|
||||
<Link href="#" className="underline-offset-4 hover:underline">
|
||||
Gizlilik Politikası
|
||||
</Link>
|
||||
'nı kabul etmiş olursunuz.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BrandPanel() {
|
||||
return (
|
||||
<div className="bg-primary text-primary-foreground relative hidden md:flex md:flex-col md:justify-between overflow-hidden p-10">
|
||||
<div
|
||||
className="absolute inset-0 opacity-30"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"radial-gradient(circle at 20% 20%, rgba(255,255,255,0.4) 0%, transparent 40%), radial-gradient(circle at 80% 80%, rgba(255,255,255,0.25) 0%, transparent 45%)",
|
||||
}}
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className="absolute -top-24 -right-24 size-72 rounded-full bg-white/10 blur-3xl"
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className="absolute -bottom-32 -left-20 size-80 rounded-full bg-black/10 blur-3xl"
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
<div className="relative z-10 flex items-center gap-2">
|
||||
<div className="bg-primary-foreground/15 ring-1 ring-primary-foreground/20 backdrop-blur flex size-10 items-center justify-center rounded-md">
|
||||
<Logo size={22} />
|
||||
</div>
|
||||
<span className="text-lg font-medium">DLS</span>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col gap-3">
|
||||
<h2 className="text-3xl font-semibold leading-tight">
|
||||
Müşteriden faturaya, tek panelden işletmenizi yönetin.
|
||||
</h2>
|
||||
<p className="text-primary-foreground/80 text-sm">
|
||||
Müşteriler, hizmetler, takvim, görevler ve finans — hepsi tek yerde, multi-tenant ve ekibinize özel.
|
||||
</p>
|
||||
<div className="text-primary-foreground/70 mt-4 text-xs">Kovak Yazılım tarafından</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { LoginForm1 } from "./components/login-form-1";
|
||||
import { getCurrentUser } from "@/lib/appwrite/server";
|
||||
|
||||
export default async function Page({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ invite?: string; reset?: string }>;
|
||||
}) {
|
||||
const { invite, reset } = await searchParams;
|
||||
const user = await getCurrentUser();
|
||||
if (user) redirect(invite ? `/d/${invite}` : "/dashboard");
|
||||
|
||||
return (
|
||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm md:max-w-4xl">
|
||||
{reset === "success" && (
|
||||
<p className="mb-4 rounded-lg bg-green-50 px-4 py-3 text-center text-sm text-green-700">
|
||||
Şifreniz güncellendi. Giriş yapabilirsiniz.
|
||||
</p>
|
||||
)}
|
||||
<LoginForm1 inviteCode={invite} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
|
||||
export function SignupForm2({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"form">) {
|
||||
return (
|
||||
<form className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<h1 className="text-2xl font-bold">Create your account</h1>
|
||||
<p className="text-muted-foreground text-sm text-balance">
|
||||
Enter your information to create a new account
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-6">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="firstName">First Name</Label>
|
||||
<Input id="firstName" placeholder="John" required />
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="lastName">Last Name</Label>
|
||||
<Input id="lastName" placeholder="Doe" required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" placeholder="m@example.com" required />
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input id="password" type="password" required />
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
||||
<Input id="confirmPassword" type="password" required />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="terms" required />
|
||||
<Label htmlFor="terms" className="text-sm">
|
||||
I agree to the{" "}
|
||||
<a href="#" className="underline underline-offset-4 hover:text-primary">
|
||||
Terms of Service
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a href="#" className="underline underline-offset-4 hover:text-primary">
|
||||
Privacy Policy
|
||||
</a>
|
||||
</Label>
|
||||
</div>
|
||||
<Button type="submit" className="w-full cursor-pointer">
|
||||
Create Account
|
||||
</Button>
|
||||
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
|
||||
<span className="bg-background text-muted-foreground relative z-10 px-2">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
<Button variant="outline" className="w-full cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Sign up with GitHub
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-center text-sm">
|
||||
Already have an account?{" "}
|
||||
<a href="/auth/sign-in-2" className="underline underline-offset-4">
|
||||
Sign in
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { SignupForm2 } from "./components/signup-form-2"
|
||||
import { Logo } from "@/components/logo"
|
||||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
|
||||
export default function SignUp2Page() {
|
||||
return (
|
||||
<div className="grid min-h-svh lg:grid-cols-2">
|
||||
<div className="flex flex-col gap-4 p-6 md:p-10">
|
||||
<div className="flex justify-center gap-2 md:justify-start">
|
||||
<Link href="/" className="flex items-center gap-2 font-medium">
|
||||
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md">
|
||||
<Logo size={24} />
|
||||
</div>
|
||||
ShadcnStore
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="w-full max-w-md">
|
||||
<SignupForm2 />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-muted relative hidden lg:block">
|
||||
<Image
|
||||
src="https://ui.shadcn.com/placeholder.svg"
|
||||
alt="Image"
|
||||
fill
|
||||
className="object-cover dark:brightness-[0.95] dark:invert"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Logo } from "@/components/logo"
|
||||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
|
||||
export function SignupForm3({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card className="overflow-hidden p-0">
|
||||
<CardContent className="grid p-0 md:grid-cols-2">
|
||||
<form className="p-6 md:p-8">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex justify-center mb-2">
|
||||
<Link href="/" className="flex items-center gap-2 font-medium">
|
||||
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md">
|
||||
<Logo size={24} />
|
||||
</div>
|
||||
<span className="text-xl">ShadcnStore</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<h1 className="text-2xl font-bold">Create your account</h1>
|
||||
<p className="text-muted-foreground text-balance">
|
||||
Enter your information to create a new account
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="firstName">First Name</Label>
|
||||
<Input
|
||||
id="firstName"
|
||||
placeholder="John"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="lastName">Last Name</Label>
|
||||
<Input
|
||||
id="lastName"
|
||||
placeholder="Doe"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input id="password" type="password" required />
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
||||
<Input id="confirmPassword" type="password" required />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="terms" required />
|
||||
<Label htmlFor="terms" className="text-sm">
|
||||
I agree to the{" "}
|
||||
<a href="#" className="underline underline-offset-4 hover:text-primary">
|
||||
Terms of Service
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a href="#" className="underline underline-offset-4 hover:text-primary">
|
||||
Privacy Policy
|
||||
</a>
|
||||
</Label>
|
||||
</div>
|
||||
<Button type="submit" className="w-full cursor-pointer">
|
||||
Create Account
|
||||
</Button>
|
||||
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
|
||||
<span className="bg-card text-muted-foreground relative z-10 px-2">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Button variant="outline" type="button" className="w-full cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Sign up with Apple</span>
|
||||
</Button>
|
||||
<Button variant="outline" type="button" className="w-full cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Sign up with Google</span>
|
||||
</Button>
|
||||
<Button variant="outline" type="button" className="w-full cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Sign up with Meta</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-center text-sm">
|
||||
Already have an account?{" "}
|
||||
<a href="/auth/sign-in-3" className="underline underline-offset-4">
|
||||
Sign in
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div className="bg-muted relative hidden md:block">
|
||||
<Image
|
||||
src="https://ui.shadcn.com/placeholder.svg"
|
||||
alt="Image"
|
||||
fill
|
||||
className="object-cover dark:brightness-[0.95] dark:invert"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="text-muted-foreground *:[a]:hover:text-primary text-center text-xs text-balance *:[a]:underline *:[a]:underline-offset-4">
|
||||
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
|
||||
and <a href="#">Privacy Policy</a>.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { SignupForm3 } from "./components/signup-form-3"
|
||||
|
||||
export default function SignUp3Page() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background p-4">
|
||||
<SignupForm3 className="w-full max-w-5xl" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useActionState } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Logo } from "@/components/logo";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { signUpAction } from "@/lib/appwrite/auth-actions";
|
||||
import { initialAuthState } from "@/lib/appwrite/auth-types";
|
||||
|
||||
export function SignupForm1({
|
||||
className,
|
||||
inviteCode,
|
||||
prefilledEmail,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { inviteCode?: string; prefilledEmail?: string }) {
|
||||
const [state, formAction, isPending] = useActionState(signUpAction, initialAuthState);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card className="overflow-hidden p-0">
|
||||
<CardContent className="grid p-0 md:grid-cols-2">
|
||||
<BrandPanel />
|
||||
|
||||
<form action={formAction} className="p-6 md:p-10">
|
||||
{inviteCode && <input type="hidden" name="inviteCode" value={inviteCode} />}
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex justify-center">
|
||||
<Link href="/" className="flex items-center gap-2 font-medium">
|
||||
<div className="bg-primary text-primary-foreground flex size-9 items-center justify-center rounded-md">
|
||||
<Logo size={22} />
|
||||
</div>
|
||||
<span className="text-xl font-semibold">DLS</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
{inviteCode ? "Davete katıl" : "Hesap oluşturun"}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm text-balance mt-1">
|
||||
{inviteCode
|
||||
? "Hesap oluşturduktan sonra çalışma alanına otomatik katılacaksınız"
|
||||
: "Birkaç saniye içinde hesabınız hazır"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="name">Adınız Soyadınız</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Ahmet Yılmaz"
|
||||
autoComplete="name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="ornek@firma.com"
|
||||
autoComplete="email"
|
||||
defaultValue={prefilledEmail}
|
||||
readOnly={Boolean(prefilledEmail)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="password">Şifre</Label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
minLength={8}
|
||||
required
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">En az 8 karakter</p>
|
||||
</div>
|
||||
|
||||
{state.error && (
|
||||
<p className="text-destructive text-sm text-center" role="alert">
|
||||
{state.error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Hesap oluşturuluyor...
|
||||
</>
|
||||
) : (
|
||||
"Hesap oluştur"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Zaten hesabınız var mı?{" "}
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className="text-foreground font-medium underline-offset-4 hover:underline"
|
||||
>
|
||||
Giriş yap
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<p className="text-muted-foreground text-center text-xs text-balance">
|
||||
Hesap oluşturarak{" "}
|
||||
<Link href="#" className="underline-offset-4 hover:underline">
|
||||
Kullanım Şartları
|
||||
</Link>{" "}
|
||||
ve{" "}
|
||||
<Link href="#" className="underline-offset-4 hover:underline">
|
||||
Gizlilik Politikası
|
||||
</Link>
|
||||
'nı kabul etmiş olursunuz.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BrandPanel() {
|
||||
return (
|
||||
<div className="bg-primary text-primary-foreground relative hidden md:flex md:flex-col md:justify-between overflow-hidden p-10">
|
||||
<div
|
||||
className="absolute inset-0 opacity-30"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"radial-gradient(circle at 20% 20%, rgba(255,255,255,0.4) 0%, transparent 40%), radial-gradient(circle at 80% 80%, rgba(255,255,255,0.25) 0%, transparent 45%)",
|
||||
}}
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className="absolute -top-24 -left-24 size-72 rounded-full bg-white/10 blur-3xl"
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className="absolute -bottom-32 -right-20 size-80 rounded-full bg-black/10 blur-3xl"
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
<div className="relative z-10 flex items-center gap-2">
|
||||
<div className="bg-primary-foreground/15 ring-1 ring-primary-foreground/20 backdrop-blur flex size-10 items-center justify-center rounded-md">
|
||||
<Logo size={22} />
|
||||
</div>
|
||||
<span className="text-lg font-medium">DLS</span>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col gap-3">
|
||||
<h2 className="text-3xl font-semibold leading-tight">
|
||||
İşletmenizi büyütecek tek araç.
|
||||
</h2>
|
||||
<p className="text-primary-foreground/80 text-sm">
|
||||
Hesap oluşturduktan sonra çalışma alanınızı kuruyor, ekibinizi davet ediyor ve hemen kullanmaya başlıyorsunuz.
|
||||
</p>
|
||||
<ul className="text-primary-foreground/85 mt-2 space-y-1 text-sm">
|
||||
<li>• Müşteri & hizmet yönetimi</li>
|
||||
<li>• Görev ve takvim</li>
|
||||
<li>• Finans ve fatura</li>
|
||||
</ul>
|
||||
<div className="text-primary-foreground/70 mt-4 text-xs">Kovak Yazılım tarafından</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { SignupForm1 } from "./components/signup-form-1";
|
||||
import { getCurrentUser } from "@/lib/appwrite/server";
|
||||
|
||||
export default async function SignUpPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ invite?: string; email?: string }>;
|
||||
}) {
|
||||
const { invite, email } = await searchParams;
|
||||
const user = await getCurrentUser();
|
||||
if (user) redirect(invite ? `/d/${invite}` : "/dashboard");
|
||||
|
||||
return (
|
||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm md:max-w-4xl">
|
||||
<SignupForm1 inviteCode={invite} prefilledEmail={email} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { requireTenant } from "@/lib/appwrite/tenant-guard";
|
||||
|
||||
export default async function ConnectionsPage() {
|
||||
let ctx;
|
||||
try {
|
||||
ctx = await requireTenant();
|
||||
} catch {
|
||||
redirect("/onboarding");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-6 px-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Bağlantı Kur</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Klinik ve laboratuvar arasında bağlantı taleplerini yönetin.
|
||||
</p>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bağlantı kodunuz</CardTitle>
|
||||
<CardDescription>Karşı taraf bu kodu girerek size bağlantı talebi gönderir.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="bg-muted/40 rounded-md border px-4 py-3 font-mono text-lg tracking-widest">
|
||||
{ctx.settings?.memberNumber ?? "—"}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Yapım aşamasında</CardTitle>
|
||||
<CardDescription>Bağlantı talepleri ve bağlı taraflar listesi sonraki sürümde eklenecek.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { AppSidebar } from "@/components/app-sidebar";
|
||||
import { SiteHeader } from "@/components/site-header";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
|
||||
import { ThemeCustomizer, ThemeCustomizerTrigger } from "@/components/theme-customizer";
|
||||
import { PrefsInitializer } from "@/components/theme-customizer/prefs-initializer";
|
||||
import { useSidebarConfig } from "@/hooks/use-sidebar-config";
|
||||
import type { UserPrefs as ThemePrefs } from "@/lib/appwrite/user-prefs-actions";
|
||||
|
||||
export type ShellUser = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type TenantKind = "lab" | "clinic";
|
||||
|
||||
export type ShellCompany = {
|
||||
id: string;
|
||||
name: string;
|
||||
logoUrl?: string | null;
|
||||
kind: TenantKind;
|
||||
};
|
||||
|
||||
export function DashboardShell({
|
||||
user,
|
||||
company,
|
||||
children,
|
||||
initialPrefs,
|
||||
}: {
|
||||
user: ShellUser;
|
||||
company: ShellCompany;
|
||||
children: React.ReactNode;
|
||||
initialPrefs: ThemePrefs;
|
||||
}) {
|
||||
const [themeCustomizerOpen, setThemeCustomizerOpen] = React.useState(false);
|
||||
const { config } = useSidebarConfig();
|
||||
|
||||
return (
|
||||
<SidebarProvider
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": "16rem",
|
||||
"--sidebar-width-icon": "3rem",
|
||||
"--header-height": "calc(var(--spacing) * 14)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={config.collapsible === "none" ? "sidebar-none-mode" : ""}
|
||||
>
|
||||
<PrefsInitializer prefs={initialPrefs} />
|
||||
|
||||
{config.side === "left" ? (
|
||||
<>
|
||||
<AppSidebar
|
||||
user={user}
|
||||
company={company}
|
||||
variant={config.variant}
|
||||
collapsible={config.collapsible}
|
||||
side={config.side}
|
||||
/>
|
||||
<SidebarInset>
|
||||
<SiteHeader company={company} />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
||||
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
<SiteFooter />
|
||||
</SidebarInset>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SidebarInset>
|
||||
<SiteHeader company={company} />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
||||
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
<SiteFooter />
|
||||
</SidebarInset>
|
||||
<AppSidebar
|
||||
user={user}
|
||||
company={company}
|
||||
variant={config.variant}
|
||||
collapsible={config.collapsible}
|
||||
side={config.side}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ThemeCustomizerTrigger onClick={() => setThemeCustomizerOpen(true)} />
|
||||
<ThemeCustomizer
|
||||
open={themeCustomizerOpen}
|
||||
onOpenChange={setThemeCustomizerOpen}
|
||||
initialPrefs={initialPrefs}
|
||||
/>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { getActiveContext } from "@/lib/appwrite/active-context";
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const ctx = await getActiveContext();
|
||||
if (!ctx) redirect("/onboarding");
|
||||
|
||||
const firstName = ctx.user.name?.split(" ")[0] ?? "";
|
||||
const companyName = ctx.settings?.companyName ?? "Çalışma alanı";
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-6 px-6 pt-0">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-muted-foreground text-sm">{companyName}</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
{firstName ? `Hoş geldiniz, ${firstName}` : "Anasayfa"}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Açık işleri, bildirimleri ve istatistikleri buradan takip edin.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Açık işler</CardTitle>
|
||||
<CardDescription>Gelen ve giden iş özetleri burada listelenecek.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-muted-foreground text-sm">
|
||||
Modül yapım aşamasında.
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>İşlem bekleyen</CardTitle>
|
||||
<CardDescription>Onay/işlem bekleyen kalemler.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-muted-foreground text-sm">
|
||||
Modül yapım aşamasında.
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bildirimler</CardTitle>
|
||||
<CardDescription>Bağlantılarınızdan gelen son bildirimler.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-muted-foreground text-sm">
|
||||
Modül yapım aşamasında.
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
export default function FinancePage() {
|
||||
return (
|
||||
<div className="flex-1 space-y-6 px-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Finans</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Gelen ödemeler, ödenen hesaplar ve bekleyen tahsilatlar.
|
||||
</p>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Yapım aşamasında</CardTitle>
|
||||
<CardDescription>Finans hareketleri, durum takibi ve raporlar sonraki sürümde.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
export default function InboundJobsPage() {
|
||||
return (
|
||||
<div className="flex-1 space-y-6 px-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Gelen İşler</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Bağlı kliniklerden gelen protez işleri burada listelenecek.
|
||||
</p>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Yapım aşamasında</CardTitle>
|
||||
<CardDescription>Gelen iş listesi, filtreleme ve detay görünümü sonraki sürümde eklenecek.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { requireTenant, requireTenantKind } from "@/lib/appwrite/tenant-guard";
|
||||
|
||||
export default async function NewJobPage() {
|
||||
let ctx;
|
||||
try {
|
||||
ctx = await requireTenant();
|
||||
requireTenantKind(ctx, ["clinic"]);
|
||||
} catch {
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-6 px-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Yeni İş Yayınla</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Bağlı laboratuvarınıza yeni bir protez işi gönderin.
|
||||
</p>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Yapım aşamasında</CardTitle>
|
||||
<CardDescription>
|
||||
Form (lab seçimi, hasta kodu, protez türü, renk, dosya yükleme) sonraki sürümde eklenecek.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
export default function OutboundJobsPage() {
|
||||
return (
|
||||
<div className="flex-1 space-y-6 px-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Giden İşler</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Karşı tarafa gönderilen protez işleri burada listelenecek.
|
||||
</p>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Yapım aşamasında</CardTitle>
|
||||
<CardDescription>Giden iş listesi sonraki sürümde eklenecek.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getActiveContext } from "@/lib/appwrite/active-context";
|
||||
import { getLogoUrl } from "@/lib/appwrite/storage";
|
||||
import { getUserPrefs } from "@/lib/appwrite/user-prefs-actions";
|
||||
import type { UserPrefs as ThemePrefs } from "@/lib/appwrite/user-prefs-actions";
|
||||
import { DashboardShell } from "./dashboard-shell";
|
||||
|
||||
export default async function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const ctx = await getActiveContext();
|
||||
if (!ctx) redirect("/onboarding");
|
||||
|
||||
const themePrefs: ThemePrefs = await getUserPrefs();
|
||||
|
||||
const company = {
|
||||
id: ctx.tenantId,
|
||||
name: ctx.settings?.companyName ?? "Çalışma alanı",
|
||||
logoUrl: getLogoUrl(ctx.settings?.logo) ?? null,
|
||||
kind: (ctx.settings?.kind ?? "lab") as "lab" | "clinic",
|
||||
};
|
||||
const user = {
|
||||
id: ctx.user.id,
|
||||
name: ctx.user.name || ctx.user.email,
|
||||
email: ctx.user.email,
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardShell user={user} company={company} initialPrefs={themePrefs}>
|
||||
{children}
|
||||
</DashboardShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { requireTenant, requireTenantKind } from "@/lib/appwrite/tenant-guard";
|
||||
|
||||
export default async function ProductsPage() {
|
||||
let ctx;
|
||||
try {
|
||||
ctx = await requireTenant();
|
||||
requireTenantKind(ctx, ["lab"]);
|
||||
} catch {
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-6 px-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Ürünler</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Sunduğunuz protez türleri ve fiyatlandırma katalogu.
|
||||
</p>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Yapım aşamasında</CardTitle>
|
||||
<CardDescription>Ürün ekleme/düzenleme sonraki sürümde eklenecek.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState, useEffect, useRef } from "react";
|
||||
import { Loader2, Save } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
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 { updateEmailAction } from "@/lib/appwrite/profile-actions";
|
||||
import { initialProfileState } from "@/lib/appwrite/profile-types";
|
||||
|
||||
export function EmailForm({ currentEmail }: { currentEmail: string }) {
|
||||
const [state, formAction, isPending] = useActionState(updateEmailAction, initialProfileState);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.ok) {
|
||||
toast.success("Email güncellendi.");
|
||||
// Clear password field after success
|
||||
formRef.current?.reset();
|
||||
} else if (state.error) {
|
||||
toast.error(state.error);
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Email adresi</CardTitle>
|
||||
<CardDescription>
|
||||
Email değiştirmek için mevcut şifrenizi de girin. Yeni email ile giriş yapmaya devam edersiniz.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form ref={formRef} action={formAction} className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Yeni email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
defaultValue={currentEmail}
|
||||
required
|
||||
/>
|
||||
{state.fieldErrors?.email && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email-password">Şifre (doğrulama)</Label>
|
||||
<Input
|
||||
id="email-password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
/>
|
||||
{state.fieldErrors?.password && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.password}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="md:col-span-2 flex justify-end">
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Güncelleniyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="size-4" />
|
||||
Email'i güncelle
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState, useEffect } from "react";
|
||||
import { Loader2, Save } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
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 { updateNameAction } from "@/lib/appwrite/profile-actions";
|
||||
import { initialProfileState } from "@/lib/appwrite/profile-types";
|
||||
|
||||
export function NameForm({ currentName }: { currentName: string }) {
|
||||
const [state, formAction, isPending] = useActionState(updateNameAction, initialProfileState);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.ok) toast.success("İsim güncellendi.");
|
||||
else if (state.error) toast.error(state.error);
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Görünür isim</CardTitle>
|
||||
<CardDescription>
|
||||
Header'da, davetlerde ve takım listesinde görünecek isim.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form action={formAction} className="grid gap-4 md:grid-cols-[1fr_auto] md:items-end">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">İsim</Label>
|
||||
<Input id="name" name="name" defaultValue={currentName} required maxLength={128} />
|
||||
{state.fieldErrors?.name && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Kaydediliyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="size-4" />
|
||||
Kaydet
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState, useEffect, useRef } from "react";
|
||||
import { KeyRound, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
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 { updatePasswordAction } from "@/lib/appwrite/profile-actions";
|
||||
import { initialProfileState } from "@/lib/appwrite/profile-types";
|
||||
|
||||
export function PasswordForm() {
|
||||
const [state, formAction, isPending] = useActionState(
|
||||
updatePasswordAction,
|
||||
initialProfileState,
|
||||
);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.ok) {
|
||||
toast.success("Şifre değiştirildi.");
|
||||
formRef.current?.reset();
|
||||
} else if (state.error) {
|
||||
toast.error(state.error);
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Şifre</CardTitle>
|
||||
<CardDescription>
|
||||
Şifrenizi değiştirmek için mevcut şifrenizi ve yeni şifreyi iki kez girin.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form ref={formRef} action={formAction} className="grid gap-4 md:grid-cols-3">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="oldPassword">Mevcut şifre</Label>
|
||||
<Input
|
||||
id="oldPassword"
|
||||
name="oldPassword"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
/>
|
||||
{state.fieldErrors?.oldPassword && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.oldPassword}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="newPassword">Yeni şifre</Label>
|
||||
<Input
|
||||
id="newPassword"
|
||||
name="newPassword"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
minLength={8}
|
||||
required
|
||||
/>
|
||||
{state.fieldErrors?.newPassword && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.newPassword}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="confirmPassword">Yeni şifre (tekrar)</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
minLength={8}
|
||||
required
|
||||
/>
|
||||
{state.fieldErrors?.confirmPassword && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.confirmPassword}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="md:col-span-3 flex justify-end">
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Güncelleniyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<KeyRound className="size-4" />
|
||||
Şifreyi değiştir
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { Metadata } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getCurrentUser } from "@/lib/appwrite/server";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
import { NameForm } from "./components/name-form";
|
||||
import { EmailForm } from "./components/email-form";
|
||||
import { PasswordForm } from "./components/password-form";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "DLS — Profil",
|
||||
};
|
||||
|
||||
export default async function AccountSettingsPage() {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) redirect("/sign-in");
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-6 px-6 pt-0">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-muted-foreground text-sm">Profil ayarları</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight">{user.name || user.email}</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Hesap bilgilerinizi ve şifrenizi buradan yönetin.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Hesap bilgileri</CardTitle>
|
||||
<CardDescription>Kayıt tarihi ve hesap durumu</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<dl className="grid gap-4 text-sm md:grid-cols-2">
|
||||
<div>
|
||||
<dt className="text-muted-foreground text-xs uppercase">Hesap ID</dt>
|
||||
<dd className="mt-1 font-mono text-xs">{user.$id}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground text-xs uppercase">Kayıt tarihi</dt>
|
||||
<dd className="mt-1">{formatDateTime(user.registration)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground text-xs uppercase">Email doğrulanmış</dt>
|
||||
<dd className="mt-1">{user.emailVerification ? "Evet" : "Hayır"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground text-xs uppercase">İki faktör (2FA)</dt>
|
||||
<dd className="mt-1">{user.mfa ? "Açık" : "Kapalı"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<NameForm currentName={user.name || ""} />
|
||||
<EmailForm currentEmail={user.email} />
|
||||
<PasswordForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
|
||||
const appearanceFormSchema = z.object({
|
||||
theme: z.enum(["light", "dark"]),
|
||||
fontFamily: z.string().optional(),
|
||||
fontSize: z.string().optional(),
|
||||
sidebarWidth: z.string().optional(),
|
||||
contentWidth: z.string().optional(),
|
||||
})
|
||||
|
||||
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>
|
||||
|
||||
export default function AppearanceSettings() {
|
||||
const form = useForm<AppearanceFormValues>({
|
||||
resolver: zodResolver(appearanceFormSchema),
|
||||
defaultValues: {
|
||||
theme: "dark",
|
||||
fontFamily: "",
|
||||
fontSize: "",
|
||||
sidebarWidth: "",
|
||||
contentWidth: "",
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: AppearanceFormValues) {
|
||||
console.log("Form submitted:", data)
|
||||
// Here you would typically save the data
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 px-4 lg:px-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Appearance</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Customize the appearance of the application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
{/* Theme Section */}
|
||||
<h3 className="text-lg font-medium mb-2">Theme</h3>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="theme"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
className="flex gap-4"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel className="[&:has([data-state=checked])>div]:border-primary cursor-pointer">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="light" className="sr-only" />
|
||||
</FormControl>
|
||||
<div className="rounded-md border-2 border-muted p-4 hover:border-accent transition-colors">
|
||||
<div className="space-y-2">
|
||||
<div className="w-20 h-20 bg-white border rounded-md p-3">
|
||||
<div className="space-y-2">
|
||||
<div className="h-2 bg-gray-200 rounded w-3/4"></div>
|
||||
<div className="h-2 bg-gray-200 rounded w-1/2"></div>
|
||||
<div className="flex space-x-2">
|
||||
<div className="h-2 w-2 bg-gray-300 rounded-full"></div>
|
||||
<div className="h-2 bg-gray-200 rounded flex-1"></div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<div className="h-2 w-2 bg-gray-300 rounded-full"></div>
|
||||
<div className="h-2 bg-gray-200 rounded flex-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm font-medium">Light</span>
|
||||
</div>
|
||||
</div>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<FormLabel className="[&:has([data-state=checked])>div]:border-primary cursor-pointer">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="dark" className="sr-only" />
|
||||
</FormControl>
|
||||
<div className="rounded-md border-2 border-muted p-4 hover:border-accent transition-colors">
|
||||
<div className="space-y-2">
|
||||
<div className="w-20 h-20 bg-gray-900 border border-gray-700 rounded-md p-3">
|
||||
<div className="space-y-2">
|
||||
<div className="h-2 bg-gray-600 rounded w-3/4"></div>
|
||||
<div className="h-2 bg-gray-600 rounded w-1/2"></div>
|
||||
<div className="flex space-x-2">
|
||||
<div className="h-2 w-2 bg-gray-500 rounded-full"></div>
|
||||
<div className="h-2 bg-gray-600 rounded flex-1"></div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<div className="h-2 w-2 bg-gray-500 rounded-full"></div>
|
||||
<div className="h-2 bg-gray-600 rounded flex-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm font-medium">Dark</span>
|
||||
</div>
|
||||
</div>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fontFamily"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Font Family</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="cursor-pointer">
|
||||
<SelectValue placeholder="Select a font" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="inter">Inter</SelectItem>
|
||||
<SelectItem value="roboto">Roboto</SelectItem>
|
||||
<SelectItem value="system">System Default</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fontSize"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Font Size</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="cursor-pointer">
|
||||
<SelectValue placeholder="Select font size" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="small">Small</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="large">Large</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Layout Section */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sidebarWidth"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Sidebar Width</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="cursor-pointer">
|
||||
<SelectValue placeholder="Select sidebar width" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="compact">Compact</SelectItem>
|
||||
<SelectItem value="comfortable">Comfortable</SelectItem>
|
||||
<SelectItem value="spacious">Spacious</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="contentWidth"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Content Width</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="cursor-pointer">
|
||||
<SelectValue placeholder="Select content width" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="fixed">Fixed</SelectItem>
|
||||
<SelectItem value="fluid">Fluid</SelectItem>
|
||||
<SelectItem value="container">Container</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex space-x-2 mt-12">
|
||||
<Button type="submit" className="cursor-pointer">
|
||||
Save Preferences
|
||||
</Button>
|
||||
<Button variant="outline" type="button" className="cursor-pointer">Cancel</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
"use client"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Github, Slack, Twitter, Zap, Globe, Database, Apple, Chrome, Facebook, Instagram, Dribbble } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
export default function ConnectionSettings() {
|
||||
// Controlled state for switches
|
||||
const [appleConnected, setAppleConnected] = useState(true)
|
||||
const [googleConnected, setGoogleConnected] = useState(false)
|
||||
const [githubConnected, setGithubConnected] = useState(true)
|
||||
const [slackConnected, setSlackConnected] = useState(false)
|
||||
const [zapierConnected, setZapierConnected] = useState(true)
|
||||
const [webhooksConnected, setWebhooksConnected] = useState(false)
|
||||
const [dbConnected, setDbConnected] = useState(true)
|
||||
|
||||
return (
|
||||
<div className="space-y-6 px-4 lg:px-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Connections</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Connect your account with third-party services and integrations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 grid-cols-1 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Connected Accounts</CardTitle>
|
||||
<CardDescription>
|
||||
Display content from your connected accounts on your site
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Apple className="h-8 w-8" />
|
||||
<div>
|
||||
<div className="font-medium">Apple</div>
|
||||
<div className="text-sm text-muted-foreground">Calendar and contacts</div>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
className="cursor-pointer"
|
||||
checked={appleConnected}
|
||||
onCheckedChange={setAppleConnected}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Chrome className="h-8 w-8" />
|
||||
<div>
|
||||
<div className="font-medium">Google</div>
|
||||
<div className="text-sm text-muted-foreground">Calendar and contacts</div>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
className="cursor-pointer"
|
||||
checked={googleConnected}
|
||||
onCheckedChange={setGoogleConnected}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Github className="h-8 w-8" />
|
||||
<div>
|
||||
<div className="font-medium">Github</div>
|
||||
<div className="text-sm text-muted-foreground">Manage your Git repositories</div>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
className="cursor-pointer"
|
||||
checked={githubConnected}
|
||||
onCheckedChange={setGithubConnected}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Slack className="h-8 w-8" />
|
||||
<div>
|
||||
<div className="font-medium">Slack</div>
|
||||
<div className="text-sm text-muted-foreground">Communication</div>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
className="cursor-pointer"
|
||||
checked={slackConnected}
|
||||
onCheckedChange={setSlackConnected}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Social Accounts</CardTitle>
|
||||
<CardDescription>
|
||||
Display content from your connected accounts on your site
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Facebook className="h-8 w-8" />
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
Facebook
|
||||
<Badge variant="outline" className="ml-2">Not Connected</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Share updates on Facebook</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" className="cursor-pointer">
|
||||
<Globe className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Twitter className="h-8 w-8" />
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
Twitter
|
||||
<Badge variant="secondary" className="ml-2">connected</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Share updates on Twitter</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" className="cursor-pointer text-destructive">
|
||||
<Globe className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Instagram className="h-8 w-8" />
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
Instagram
|
||||
<Badge variant="secondary" className="ml-2">connected</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Stay connected at Instagram</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" className="cursor-pointer text-destructive">
|
||||
<Globe className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Dribbble className="h-8 w-8" />
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
Dribbble
|
||||
<Badge variant="outline" className="ml-2">Not Connected</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Stay connected at Dribbble</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" className="cursor-pointer">
|
||||
<Globe className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="grid gap-6 grid-cols-1 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>API Integrations</CardTitle>
|
||||
<CardDescription>
|
||||
Configure API connections and webhooks.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Zap className="h-8 w-8" />
|
||||
<div>
|
||||
<div className="font-medium">Zapier</div>
|
||||
<div className="text-sm text-muted-foreground">Automate workflows with Zapier</div>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
className="cursor-pointer"
|
||||
checked={zapierConnected}
|
||||
onCheckedChange={setZapierConnected}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Globe className="h-8 w-8" />
|
||||
<div>
|
||||
<div className="font-medium">Webhooks</div>
|
||||
<div className="text-sm text-muted-foreground">Configure custom webhook endpoints</div>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
className="cursor-pointer"
|
||||
checked={webhooksConnected}
|
||||
onCheckedChange={setWebhooksConnected}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Database className="h-8 w-8" />
|
||||
<div>
|
||||
<div className="font-medium">Database Sync</div>
|
||||
<div className="text-sm text-muted-foreground">Sync data with external databases</div>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
className="cursor-pointer"
|
||||
checked={dbConnected}
|
||||
onCheckedChange={setDbConnected}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>API Keys</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your API keys and access tokens.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||||
<div>
|
||||
<div className="font-medium">Production API Key</div>
|
||||
<div className="text-sm text-muted-foreground font-mono">sk_live_••••••••••••••••••••••••4234</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm" className="cursor-pointer">
|
||||
Regenerate
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="cursor-pointer">
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||||
<div>
|
||||
<div className="font-medium">Development API Key</div>
|
||||
<div className="text-sm text-muted-foreground font-mono">sk_test_••••••••••••••••••••••••5678</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm" className="cursor-pointer">
|
||||
Regenerate
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="cursor-pointer">
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="pt-4">
|
||||
<Button variant="outline" className="cursor-pointer">Add New API Key</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState, useEffect, useRef, useState } from "react";
|
||||
import { Check, Copy, Loader2, UserPlus } 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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { inviteMemberAction } from "@/lib/appwrite/team-actions";
|
||||
import { initialInviteState } from "@/lib/appwrite/team-types";
|
||||
|
||||
export function InviteForm() {
|
||||
const [state, formAction, isPending] = useActionState(inviteMemberAction, initialInviteState);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.ok && formRef.current) {
|
||||
formRef.current.reset();
|
||||
}
|
||||
}, [state.ok, state.shortUrl]);
|
||||
|
||||
const copy = async () => {
|
||||
if (!state.shortUrl) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(state.shortUrl);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Üye davet et</CardTitle>
|
||||
<CardDescription>
|
||||
Email ve rol girin, oluşturulan kısa linki kopyalayıp davet edeceğiniz kişiye gönderin.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form ref={formRef} action={formAction} className="grid gap-4 md:grid-cols-[1fr_180px_auto]">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="ornek@firma.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="role">Rol</Label>
|
||||
<Select name="role" defaultValue="member">
|
||||
<SelectTrigger id="role">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="member">Üye</SelectItem>
|
||||
<SelectItem value="admin">Yönetici</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<Button type="submit" disabled={isPending} className="w-full md:w-auto">
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Gönderiliyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserPlus className="size-4" />
|
||||
Davet et
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{state.error && (
|
||||
<p className="text-destructive mt-3 text-sm" role="alert">
|
||||
{state.error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{state.ok && state.shortUrl && (
|
||||
<div className="bg-muted/50 mt-4 flex flex-col gap-2 rounded-md border p-3">
|
||||
{state.message && (
|
||||
<p className="text-muted-foreground text-xs">{state.message}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Input value={state.shortUrl} readOnly className="font-mono text-xs" />
|
||||
<Button type="button" variant="outline" size="sm" onClick={copy}>
|
||||
{copied ? <Check className="size-4" /> : <Copy className="size-4" />}
|
||||
{copied ? "Kopyalandı" : "Kopyala"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState, useTransition } from "react";
|
||||
import { DoorOpen, Loader2, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
leaveWorkspaceAction,
|
||||
removeMemberAction,
|
||||
updateMemberRoleAction,
|
||||
} from "@/lib/appwrite/team-actions";
|
||||
|
||||
type Member = {
|
||||
id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
joined: string;
|
||||
invited: string;
|
||||
confirm: boolean;
|
||||
};
|
||||
|
||||
const ROLE_LABEL: Record<string, string> = {
|
||||
owner: "Sahip",
|
||||
admin: "Yönetici",
|
||||
member: "Üye",
|
||||
};
|
||||
|
||||
export function MembersTable({
|
||||
members,
|
||||
currentUserId,
|
||||
isOwner,
|
||||
canManage,
|
||||
}: {
|
||||
members: Member[];
|
||||
currentUserId: string;
|
||||
isOwner: boolean;
|
||||
canManage: boolean;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const [, startTransition] = useTransition();
|
||||
const [removing, setRemoving] = useState<Member | null>(null);
|
||||
const [leaving, setLeaving] = useState(false);
|
||||
|
||||
const setRole = (membershipId: string, role: string) => {
|
||||
setBusy(membershipId);
|
||||
startTransition(async () => {
|
||||
const result = await updateMemberRoleAction({ ok: false }, formDataFor({
|
||||
membershipId,
|
||||
role,
|
||||
}));
|
||||
if (result.ok) toast.success("Rol güncellendi.");
|
||||
else toast.error(result.error ?? "Rol güncellenemedi.");
|
||||
setBusy(null);
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemove = () => {
|
||||
if (!removing) return;
|
||||
setBusy(removing.id);
|
||||
startTransition(async () => {
|
||||
const result = await removeMemberAction({ ok: false }, formDataFor({
|
||||
membershipId: removing.id,
|
||||
}));
|
||||
if (result.ok) {
|
||||
toast.success(`${removing.name} ekipten çıkarıldı.`);
|
||||
setRemoving(null);
|
||||
} else {
|
||||
toast.error(result.error ?? "İşlem başarısız.");
|
||||
}
|
||||
setBusy(null);
|
||||
});
|
||||
};
|
||||
|
||||
const handleLeave = () => {
|
||||
setBusy("leave");
|
||||
startTransition(async () => {
|
||||
const result = await leaveWorkspaceAction();
|
||||
if (result.ok) {
|
||||
toast.success("Çalışma alanından ayrıldınız.");
|
||||
setLeaving(false);
|
||||
router.push("/dashboard");
|
||||
} else {
|
||||
toast.error(result.error ?? "Ayrılma başarısız.");
|
||||
}
|
||||
setBusy(null);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Üyeler ({members.length})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>İsim</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Rol</TableHead>
|
||||
<TableHead className="text-right">İşlem</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{members.map((m) => {
|
||||
const isSelf = m.userId === currentUserId;
|
||||
const isMemberOwner = m.role === "owner";
|
||||
return (
|
||||
<TableRow key={m.id}>
|
||||
<TableCell className="font-medium">
|
||||
{m.name}
|
||||
{isSelf && (
|
||||
<Badge variant="secondary" className="ml-2 text-xs">
|
||||
Siz
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{m.email}</TableCell>
|
||||
<TableCell>
|
||||
{isOwner && !isMemberOwner && !isSelf ? (
|
||||
<Select
|
||||
value={m.role}
|
||||
disabled={busy === m.id}
|
||||
onValueChange={(v) => setRole(m.id, v)}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="member">Üye</SelectItem>
|
||||
<SelectItem value="admin">Yönetici</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Badge variant={isMemberOwner ? "default" : "outline"}>
|
||||
{ROLE_LABEL[m.role] ?? m.role}
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{isSelf ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-muted-foreground"
|
||||
disabled={busy === "leave"}
|
||||
onClick={() => setLeaving(true)}
|
||||
>
|
||||
{busy === "leave" ? (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<DoorOpen className="size-3.5" />
|
||||
)}
|
||||
Ayrıl
|
||||
</Button>
|
||||
) : canManage && !isMemberOwner ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive"
|
||||
disabled={busy === m.id}
|
||||
onClick={() => setRemoving(m)}
|
||||
>
|
||||
{busy === m.id ? (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="size-3.5" />
|
||||
)}
|
||||
Çıkar
|
||||
</Button>
|
||||
) : null}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog
|
||||
open={Boolean(removing)}
|
||||
onOpenChange={(v) => !v && busy === null && setRemoving(null)}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Üyeyi ekipten çıkar</DialogTitle>
|
||||
<DialogDescription>
|
||||
{removing && (
|
||||
<>
|
||||
<strong>{removing.name}</strong> ({removing.email}) ekipten çıkarılacak.
|
||||
Verileri silinmez ama bu çalışma alanına erişimi kalkar.
|
||||
</>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setRemoving(null)}
|
||||
disabled={busy !== null}
|
||||
>
|
||||
Vazgeç
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleRemove}
|
||||
disabled={busy !== null}
|
||||
>
|
||||
{busy ? <Loader2 className="size-4 animate-spin" /> : <Trash2 className="size-4" />}
|
||||
Çıkar
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={leaving} onOpenChange={(v) => !v && busy === null && setLeaving(false)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Çalışma alanından ayrıl</DialogTitle>
|
||||
<DialogDescription>
|
||||
Bu çalışma alanındaki tüm verilere erişiminiz kalkar. Tekrar davet edilmedikçe
|
||||
giremezsiniz. Devam etmek istediğinize emin misiniz?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setLeaving(false)}
|
||||
disabled={busy !== null}
|
||||
>
|
||||
Vazgeç
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleLeave}
|
||||
disabled={busy !== null}
|
||||
>
|
||||
{busy ? <Loader2 className="size-4 animate-spin" /> : <DoorOpen className="size-4" />}
|
||||
Ayrıl
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function formDataFor(fields: Record<string, string>): FormData {
|
||||
const fd = new FormData();
|
||||
for (const [k, v] of Object.entries(fields)) fd.set(k, v);
|
||||
return fd;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
"use client";
|
||||
|
||||
import { useTransition, useState } from "react";
|
||||
import { Check, Copy, Loader2, X } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { cancelInviteAction } from "@/lib/appwrite/team-actions";
|
||||
|
||||
type Invite = {
|
||||
id: string;
|
||||
code: string;
|
||||
email: string;
|
||||
role: string;
|
||||
expiresAt?: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export function PendingInvitesTable({
|
||||
invites,
|
||||
canManage,
|
||||
}: {
|
||||
invites: Invite[];
|
||||
canManage: boolean;
|
||||
}) {
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const [, startTransition] = useTransition();
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
|
||||
const baseUrl =
|
||||
typeof window !== "undefined" ? window.location.origin : "";
|
||||
|
||||
const copy = async (code: string, id: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(`${baseUrl}/d/${code}`);
|
||||
setCopiedId(id);
|
||||
setTimeout(() => setCopiedId(null), 2000);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
const cancel = (id: string) => {
|
||||
setBusy(id);
|
||||
startTransition(async () => {
|
||||
const fd = new FormData();
|
||||
fd.set("inviteId", id);
|
||||
await cancelInviteAction({ ok: false }, fd);
|
||||
setBusy(null);
|
||||
});
|
||||
};
|
||||
|
||||
const formatDate = (iso?: string) => {
|
||||
if (!iso) return "—";
|
||||
return new Date(iso).toLocaleDateString("tr-TR", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bekleyen davetler ({invites.length})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Rol</TableHead>
|
||||
<TableHead>Geçerlilik</TableHead>
|
||||
<TableHead className="text-right">İşlem</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{invites.map((inv) => (
|
||||
<TableRow key={inv.id}>
|
||||
<TableCell className="font-medium">{inv.email}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">
|
||||
{inv.role === "admin" ? "Yönetici" : "Üye"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{formatDate(inv.expiresAt)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => copy(inv.code, inv.id)}
|
||||
>
|
||||
{copiedId === inv.id ? (
|
||||
<Check className="size-3.5" />
|
||||
) : (
|
||||
<Copy className="size-3.5" />
|
||||
)}
|
||||
Linki kopyala
|
||||
</Button>
|
||||
{canManage && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={busy === inv.id}
|
||||
onClick={() => cancel(inv.id)}
|
||||
>
|
||||
{busy === inv.id ? (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<X className="size-3.5" />
|
||||
)}
|
||||
İptal
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { Metadata } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Query } from "node-appwrite";
|
||||
|
||||
import { createAdminClient } from "@/lib/appwrite/server";
|
||||
import { DATABASE_ID, TABLES, type InviteLink } from "@/lib/appwrite/schema";
|
||||
import { requireTenant } from "@/lib/appwrite/tenant-guard";
|
||||
import { InviteForm } from "./components/invite-form";
|
||||
import { MembersTable } from "./components/members-table";
|
||||
import { PendingInvitesTable } from "./components/pending-invites-table";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "DLS — Ekip üyeleri",
|
||||
};
|
||||
|
||||
export default async function MembersPage() {
|
||||
let ctx;
|
||||
try {
|
||||
ctx = await requireTenant();
|
||||
} catch {
|
||||
redirect("/onboarding");
|
||||
}
|
||||
|
||||
const canManage = ctx.role === "owner" || ctx.role === "admin";
|
||||
const isOwner = ctx.role === "owner";
|
||||
|
||||
const { teams, tablesDB } = createAdminClient();
|
||||
|
||||
const [memberships, invites] = await Promise.all([
|
||||
teams.listMemberships(ctx.tenantId).catch(() => ({ memberships: [], total: 0 })),
|
||||
tablesDB
|
||||
.listRows({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.inviteLinks,
|
||||
queries: [
|
||||
Query.equal("tenantId", ctx.tenantId),
|
||||
Query.equal("status", "pending"),
|
||||
Query.orderDesc("$createdAt"),
|
||||
Query.limit(50),
|
||||
],
|
||||
})
|
||||
.catch(() => ({ rows: [] as unknown[] })),
|
||||
]);
|
||||
|
||||
const members = memberships.memberships.map((m) => ({
|
||||
id: m.$id,
|
||||
userId: m.userId,
|
||||
name: m.userName || m.userEmail,
|
||||
email: m.userEmail,
|
||||
role: m.roles[0] ?? "member",
|
||||
joined: m.joined,
|
||||
invited: m.invited,
|
||||
confirm: m.confirm,
|
||||
}));
|
||||
|
||||
const pendingInvites = (invites.rows as unknown as InviteLink[]).map((row) => ({
|
||||
id: row.$id,
|
||||
code: row.code,
|
||||
email: row.email,
|
||||
role: row.role ?? "member",
|
||||
expiresAt: row.expiresAt,
|
||||
createdAt: row.$createdAt,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-6 px-6 pt-0">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-muted-foreground text-sm">{ctx.settings?.companyName ?? "Çalışma alanı"}</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight">Ekip üyeleri</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Çalışma alanına üye davet edin, rolleri yönetin.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{canManage ? (
|
||||
<InviteForm />
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Yeni üye davet etmek için yönetici yetkisine ihtiyacınız var.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{pendingInvites.length > 0 && (
|
||||
<PendingInvitesTable invites={pendingInvites} canManage={canManage} />
|
||||
)}
|
||||
|
||||
<MembersTable
|
||||
members={members}
|
||||
currentUserId={ctx.user.id}
|
||||
isOwner={isOwner}
|
||||
canManage={canManage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,669 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Bell, Mail, MessageSquare } from "lucide-react"
|
||||
|
||||
const notificationsFormSchema = z.object({
|
||||
emailSecurity: z.boolean(),
|
||||
emailUpdates: z.boolean(),
|
||||
emailMarketing: z.boolean(),
|
||||
pushMessages: z.boolean(),
|
||||
pushMentions: z.boolean(),
|
||||
pushTasks: z.boolean(),
|
||||
emailFrequency: z.string(),
|
||||
quietHoursStart: z.string(),
|
||||
quietHoursEnd: z.string(),
|
||||
channelEmail: z.boolean(),
|
||||
channelPush: z.boolean(),
|
||||
channelSms: z.boolean(),
|
||||
// New notification table fields
|
||||
orderUpdatesEmail: z.boolean(),
|
||||
orderUpdatesBrowser: z.boolean(),
|
||||
orderUpdatesApp: z.boolean(),
|
||||
invoiceRemindersEmail: z.boolean(),
|
||||
invoiceRemindersBrowser: z.boolean(),
|
||||
invoiceRemindersApp: z.boolean(),
|
||||
promotionalOffersEmail: z.boolean(),
|
||||
promotionalOffersBrowser: z.boolean(),
|
||||
promotionalOffersApp: z.boolean(),
|
||||
systemMaintenanceEmail: z.boolean(),
|
||||
systemMaintenanceBrowser: z.boolean(),
|
||||
systemMaintenanceApp: z.boolean(),
|
||||
notificationTiming: z.string(),
|
||||
})
|
||||
|
||||
type NotificationsFormValues = z.infer<typeof notificationsFormSchema>
|
||||
|
||||
export default function NotificationSettings() {
|
||||
const form = useForm<NotificationsFormValues>({
|
||||
resolver: zodResolver(notificationsFormSchema),
|
||||
defaultValues: {
|
||||
emailSecurity: false,
|
||||
emailUpdates: true,
|
||||
emailMarketing: false,
|
||||
pushMessages: true,
|
||||
pushMentions: true,
|
||||
pushTasks: false,
|
||||
emailFrequency: "instant",
|
||||
quietHoursStart: "22:00",
|
||||
quietHoursEnd: "06:00",
|
||||
channelEmail: true,
|
||||
channelPush: true,
|
||||
channelSms: false,
|
||||
// New notification table defaults
|
||||
orderUpdatesEmail: true,
|
||||
orderUpdatesBrowser: true,
|
||||
orderUpdatesApp: true,
|
||||
invoiceRemindersEmail: true,
|
||||
invoiceRemindersBrowser: false,
|
||||
invoiceRemindersApp: true,
|
||||
promotionalOffersEmail: false,
|
||||
promotionalOffersBrowser: true,
|
||||
promotionalOffersApp: false,
|
||||
systemMaintenanceEmail: true,
|
||||
systemMaintenanceBrowser: true,
|
||||
systemMaintenanceApp: false,
|
||||
notificationTiming: "online",
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: NotificationsFormValues) {
|
||||
console.log("Notifications settings submitted:", data)
|
||||
// Here you would typically save the settings
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 px-4 lg:px-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Notifications</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Configure how you receive notifications.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div className="grid gap-6 grid-cols-1 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Email Notifications</CardTitle>
|
||||
<CardDescription>
|
||||
Choose what email notifications you want to receive.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="emailSecurity"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-3">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1">
|
||||
<FormLabel>Security alerts</FormLabel>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Get notified when there are security events on your account.
|
||||
</p>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="emailUpdates"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-3">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1">
|
||||
<FormLabel>Product updates</FormLabel>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Receive updates about new features and improvements.
|
||||
</p>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="emailMarketing"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-3">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1">
|
||||
<FormLabel>Marketing emails</FormLabel>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Receive emails about our latest offers and promotions.
|
||||
</p>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Push Notifications</CardTitle>
|
||||
<CardDescription>
|
||||
Configure browser and mobile push notifications.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pushMessages"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-3">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1">
|
||||
<FormLabel>New messages</FormLabel>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Get notified when you receive new messages.
|
||||
</p>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pushMentions"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-3">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1">
|
||||
<FormLabel>Mentions</FormLabel>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Get notified when someone mentions you.
|
||||
</p>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pushTasks"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-3">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1">
|
||||
<FormLabel>Task updates</FormLabel>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Get notified about task assignments and updates.
|
||||
</p>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Notification Frequency</CardTitle>
|
||||
<CardDescription>
|
||||
Control how often you receive notifications.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="emailFrequency"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email Frequency</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select frequency" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="instant">Instant</SelectItem>
|
||||
<SelectItem value="hourly">Hourly digest</SelectItem>
|
||||
<SelectItem value="daily">Daily digest</SelectItem>
|
||||
<SelectItem value="weekly">Weekly digest</SelectItem>
|
||||
<SelectItem value="never">Never</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormItem>
|
||||
<FormLabel>Quiet Hours</FormLabel>
|
||||
<div className="flex space-x-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="quietHoursStart"
|
||||
render={({ field }) => (
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-50">
|
||||
<SelectValue placeholder="Start" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="22:00">10:00 PM</SelectItem>
|
||||
<SelectItem value="23:00">11:00 PM</SelectItem>
|
||||
<SelectItem value="00:00">12:00 AM</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
<span className="self-center">to</span>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="quietHoursEnd"
|
||||
render={({ field }) => (
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-50">
|
||||
<SelectValue placeholder="End" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="06:00">6:00 AM</SelectItem>
|
||||
<SelectItem value="07:00">7:00 AM</SelectItem>
|
||||
<SelectItem value="08:00">8:00 AM</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Notification Preferences</CardTitle>
|
||||
<CardDescription>
|
||||
We need permission from your browser to show notifications.{" "}
|
||||
<Button variant="link" className="p-0 h-auto text-primary">
|
||||
Request Permission
|
||||
</Button>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[200px]">TYPE</TableHead>
|
||||
<TableHead className="text-center">EMAIL</TableHead>
|
||||
<TableHead className="text-center">BROWSER</TableHead>
|
||||
<TableHead className="text-center">APP</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">Order updates</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="orderUpdatesEmail"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="orderUpdatesBrowser"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="orderUpdatesApp"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">Invoice reminders</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="invoiceRemindersEmail"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="invoiceRemindersBrowser"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="invoiceRemindersApp"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">Promotional offers</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="promotionalOffersEmail"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="promotionalOffersBrowser"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="promotionalOffersApp"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">System maintenance</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="systemMaintenanceEmail"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="systemMaintenanceBrowser"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="systemMaintenanceApp"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notificationTiming"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>When should we send you notifications?</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-full max-w-sm">
|
||||
<SelectValue placeholder="Select timing" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="online">Only When I'm online</SelectItem>
|
||||
<SelectItem value="always">Always</SelectItem>
|
||||
<SelectItem value="never">Never</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Notification Channels</CardTitle>
|
||||
<CardDescription>
|
||||
Choose your preferred notification channels for different types of alerts.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="channelEmail"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Mail className="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<FormLabel className="font-medium mb-1">Email</FormLabel>
|
||||
<div className="text-sm text-muted-foreground">Receive notifications via email</div>
|
||||
</div>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Separator />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="channelPush"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Bell className="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<FormLabel className="font-medium mb-1">Push Notifications</FormLabel>
|
||||
<div className="text-sm text-muted-foreground">Receive browser push notifications</div>
|
||||
</div>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Separator />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="channelSms"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<MessageSquare className="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<FormLabel className="font-medium mb-1">SMS</FormLabel>
|
||||
<div className="text-sm text-muted-foreground">Receive notifications via SMS</div>
|
||||
</div>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button type="submit" className="cursor-pointer">Save Preferences</Button>
|
||||
<Button variant="outline" type="reset" className="cursor-pointer">Cancel</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent,CardHeader, CardDescription, CardTitle } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Upload } from "lucide-react"
|
||||
import { useRef, useState } from "react"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Logo } from "@/components/logo"
|
||||
|
||||
const userFormSchema = z.object({
|
||||
firstName: z.string().min(1, "First name is required"),
|
||||
lastName: z.string().min(1, "Last name is required"),
|
||||
email: z.string().email("Invalid email address"),
|
||||
phone: z.string().optional(),
|
||||
website: z.string().optional(),
|
||||
location: z.string().optional(),
|
||||
role: z.string().optional(),
|
||||
bio: z.string().optional(),
|
||||
company: z.string().optional(),
|
||||
timezone: z.string().optional(),
|
||||
language: z.string().optional(),
|
||||
})
|
||||
|
||||
type UserFormValues = z.infer<typeof userFormSchema>
|
||||
|
||||
export default function UserSettingsPage() {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [profileImage, setProfileImage] = useState<string | null>(null)
|
||||
const [useDefaultIcon, setUseDefaultIcon] = useState(true)
|
||||
|
||||
const form = useForm<UserFormValues>({
|
||||
resolver: zodResolver(userFormSchema),
|
||||
defaultValues: {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
website: "",
|
||||
location: "",
|
||||
role: "",
|
||||
bio: "",
|
||||
company: "",
|
||||
timezone: "",
|
||||
language: "",
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: UserFormValues) {
|
||||
console.log("Form submitted:", data)
|
||||
// Here you would typically save the data
|
||||
}
|
||||
|
||||
const handleFileUpload = () => {
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (file) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
setProfileImage(e.target?.result as string)
|
||||
setUseDefaultIcon(false)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setProfileImage(null)
|
||||
setUseDefaultIcon(true)
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-4 lg:px-6">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Profile Settings</CardTitle>
|
||||
<CardDescription>Update your personal information and preferences</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Profile Picture Section */}
|
||||
<div className="flex items-center gap-6 ">
|
||||
{useDefaultIcon ? (
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-lg">
|
||||
< Logo size={56} />
|
||||
</div>
|
||||
) : (
|
||||
<Avatar className="h-20 w-20 rounded-lg">
|
||||
<AvatarImage src={profileImage || undefined} />
|
||||
<AvatarFallback>SS</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={handleFileUpload}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
Upload new photo
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Allowed JPG, GIF or PNG. Max size of 800K
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/jpeg,image/gif,image/png"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="mb-10" />
|
||||
{/* Form Fields */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* First Name */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="firstName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>First Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Enter your first name" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Last Name */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Last Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Enter your last name" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Email */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>E-mail</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" placeholder="Enter your email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Company */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="company"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Company</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Enter your company" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Phone Number */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Phone Number</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="tel" placeholder="Enter your phone number" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Location */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="location"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Location</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Enter your location" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Website */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="website"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Website</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="url" placeholder="Enter your website" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Language */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="language"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Language</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select Language" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="english">English</SelectItem>
|
||||
<SelectItem value="spanish">Spanish</SelectItem>
|
||||
<SelectItem value="french">French</SelectItem>
|
||||
<SelectItem value="german">German</SelectItem>
|
||||
<SelectItem value="italian">Italian</SelectItem>
|
||||
<SelectItem value="portuguese">Portuguese</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Role */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="role"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Role</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Enter your role" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Timezone */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="timezone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Timezone</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select Timezone" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="pst">PST (Pacific Standard Time)</SelectItem>
|
||||
<SelectItem value="est">EST (Eastern Standard Time)</SelectItem>
|
||||
<SelectItem value="cst">CST (Central Standard Time)</SelectItem>
|
||||
<SelectItem value="mst">MST (Mountain Standard Time)</SelectItem>
|
||||
<SelectItem value="utc">UTC (Coordinated Universal Time)</SelectItem>
|
||||
<SelectItem value="cet">CET (Central European Time)</SelectItem>
|
||||
<SelectItem value="jst">JST (Japan Standard Time)</SelectItem>
|
||||
<SelectItem value="aest">AEST (Australian Eastern Standard Time)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bio - Full Width */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bio"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bio</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Tell us a little about yourself..."
|
||||
className="min-h-[100px]"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-start gap-3">
|
||||
<Button type="submit" className="cursor-pointer">
|
||||
Save Changes
|
||||
</Button>
|
||||
<Button variant="outline" type="button" className="cursor-pointer">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState, useEffect, useRef, useState, useTransition } from "react";
|
||||
import { Building2, ImagePlus, Loader2, Trash2, Upload } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
removeLogoAction,
|
||||
uploadLogoAction,
|
||||
} from "@/lib/appwrite/logo-actions";
|
||||
import { initialLogoState } from "@/lib/appwrite/logo-types";
|
||||
|
||||
type Props = {
|
||||
canEdit: boolean;
|
||||
currentLogoUrl: string | null;
|
||||
companyName: string;
|
||||
};
|
||||
|
||||
const MAX_BYTES = 2 * 1024 * 1024;
|
||||
const ALLOWED_MIME = ["image/png", "image/jpeg", "image/webp", "image/svg+xml"];
|
||||
|
||||
export function LogoUploader({ canEdit, currentLogoUrl, companyName }: Props) {
|
||||
const [state, formAction, isPending] = useActionState(
|
||||
uploadLogoAction,
|
||||
initialLogoState,
|
||||
);
|
||||
const [removing, startRemove] = useTransition();
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(currentLogoUrl);
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const [selectedName, setSelectedName] = useState<string | null>(null);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setPreviewUrl(currentLogoUrl);
|
||||
}, [currentLogoUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.ok) {
|
||||
toast.success("Logo güncellendi.");
|
||||
setSelectedName(null);
|
||||
} else if (state.error) {
|
||||
toast.error(state.error);
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
const handleFile = (file: File | null) => {
|
||||
if (!file) return;
|
||||
if (!ALLOWED_MIME.includes(file.type)) {
|
||||
toast.error("Sadece PNG, JPG, WebP veya SVG yükleyebilirsiniz.");
|
||||
return;
|
||||
}
|
||||
if (file.size > MAX_BYTES) {
|
||||
toast.error("Dosya 2MB'dan büyük olamaz.");
|
||||
return;
|
||||
}
|
||||
setSelectedName(file.name);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
setPreviewUrl(typeof e.target?.result === "string" ? e.target.result : null);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault();
|
||||
setDragOver(false);
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
if (file && inputRef.current) {
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(file);
|
||||
inputRef.current.files = dt.files;
|
||||
handleFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = () => {
|
||||
startRemove(async () => {
|
||||
const result = await removeLogoAction();
|
||||
if (result.ok) {
|
||||
toast.success("Logo kaldırıldı.");
|
||||
setPreviewUrl(null);
|
||||
setSelectedName(null);
|
||||
if (inputRef.current) inputRef.current.value = "";
|
||||
} else {
|
||||
toast.error(result.error ?? "Logo kaldırılamadı.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const submitDisabled = isPending || removing || !selectedName;
|
||||
const busy = isPending || removing;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Building2 className="size-4" />
|
||||
Logo
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Faturalarda, panel başlığında ve dış paylaşımlarda görünür. PNG, JPG, WebP veya SVG —
|
||||
en fazla 2 MB.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form ref={formRef} action={formAction} className="space-y-4">
|
||||
<div className="grid items-start gap-6 md:grid-cols-[200px_1fr]">
|
||||
<div className="bg-muted flex aspect-square w-full items-center justify-center overflow-hidden rounded-lg border">
|
||||
{previewUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt={`${companyName} logo`}
|
||||
className="size-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-muted-foreground flex flex-col items-center gap-2 p-4 text-center text-xs">
|
||||
<Building2 className="size-8 opacity-40" />
|
||||
<span>Henüz logo yok</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
if (canEdit) setDragOver(true);
|
||||
}}
|
||||
onDragLeave={() => setDragOver(false)}
|
||||
onDrop={canEdit ? handleDrop : undefined}
|
||||
className={cn(
|
||||
"flex min-h-[120px] cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-6 text-center transition-colors",
|
||||
dragOver && "border-primary bg-primary/5",
|
||||
!canEdit && "cursor-not-allowed opacity-60",
|
||||
!dragOver && "hover:bg-muted/30",
|
||||
)}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
name="logo"
|
||||
accept={ALLOWED_MIME.join(",")}
|
||||
className="sr-only"
|
||||
disabled={!canEdit || busy}
|
||||
onChange={(e) => handleFile(e.target.files?.[0] ?? null)}
|
||||
/>
|
||||
<ImagePlus className="text-muted-foreground size-6" />
|
||||
<div className="text-sm font-medium">
|
||||
{selectedName ?? "Logo yüklemek için tıkla veya sürükle bırak"}
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
Önerilen: kare, en az 256×256 px, şeffaf arka plan (PNG/SVG)
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{canEdit && (
|
||||
<Button type="submit" disabled={submitDisabled}>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Yükleniyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Upload className="size-4" />
|
||||
Yükle
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{canEdit && currentLogoUrl && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleRemove}
|
||||
disabled={busy}
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
{removing ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="size-4" />
|
||||
)}
|
||||
Kaldır
|
||||
</Button>
|
||||
)}
|
||||
{!canEdit && (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Logo değiştirmek için yönetici yetkisi gerekli.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState, useEffect } from "react";
|
||||
import { Building2, Coins, Loader2, Save } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
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 { Textarea } from "@/components/ui/textarea";
|
||||
import { updateWorkspaceSettingsAction } from "@/lib/appwrite/workspace-actions";
|
||||
import { initialWorkspaceSettingsState } from "@/lib/appwrite/workspace-types";
|
||||
|
||||
type Defaults = {
|
||||
companyName: string;
|
||||
companyTaxId: string;
|
||||
companyAddress: string;
|
||||
companyEmail: string;
|
||||
companyPhone: string;
|
||||
defaultCurrency: string;
|
||||
kind: "lab" | "clinic" | null;
|
||||
memberNumber: string;
|
||||
};
|
||||
|
||||
export function WorkspaceSettingsForm({
|
||||
canEdit,
|
||||
defaults,
|
||||
}: {
|
||||
canEdit: boolean;
|
||||
defaults: Defaults;
|
||||
}) {
|
||||
const [state, formAction, isPending] = useActionState(
|
||||
updateWorkspaceSettingsAction,
|
||||
initialWorkspaceSettingsState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.ok) toast.success("Bilgiler güncellendi.");
|
||||
else if (state.error) toast.error(state.error);
|
||||
}, [state]);
|
||||
|
||||
const kindLabel = defaults.kind === "lab" ? "Laboratuvar" : defaults.kind === "clinic" ? "Klinik" : "—";
|
||||
|
||||
return (
|
||||
<form action={formAction} className="space-y-6">
|
||||
<fieldset disabled={!canEdit || isPending} className="space-y-6 disabled:opacity-90">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Building2 className="size-4" />
|
||||
Şirket
|
||||
</CardTitle>
|
||||
<CardDescription>Resmi şirket bilgileriniz ve bağlantı kodunuz.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-5 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label>Hesap türü</Label>
|
||||
<div className="bg-muted/50 flex h-9 items-center rounded-md border px-3 text-sm">
|
||||
{kindLabel}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Bağlantı kodu</Label>
|
||||
<div className="bg-muted/50 flex h-9 items-center rounded-md border px-3 font-mono text-sm tracking-widest">
|
||||
{defaults.memberNumber || "—"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 grid gap-2">
|
||||
<Label htmlFor="companyName">Şirket adı *</Label>
|
||||
<Input
|
||||
id="companyName"
|
||||
name="companyName"
|
||||
defaultValue={defaults.companyName}
|
||||
required
|
||||
placeholder="Örn. Atlas Diş Polikliniği"
|
||||
/>
|
||||
{state.fieldErrors?.companyName && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.companyName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="companyTaxId">Vergi numarası</Label>
|
||||
<Input
|
||||
id="companyTaxId"
|
||||
name="companyTaxId"
|
||||
defaultValue={defaults.companyTaxId}
|
||||
inputMode="numeric"
|
||||
placeholder="1234567890"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="companyPhone">Telefon</Label>
|
||||
<Input
|
||||
id="companyPhone"
|
||||
name="companyPhone"
|
||||
type="tel"
|
||||
defaultValue={defaults.companyPhone}
|
||||
placeholder="+90 555 123 45 67"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="companyEmail">Email</Label>
|
||||
<Input
|
||||
id="companyEmail"
|
||||
name="companyEmail"
|
||||
type="email"
|
||||
defaultValue={defaults.companyEmail}
|
||||
placeholder="info@firma.com"
|
||||
/>
|
||||
{state.fieldErrors?.companyEmail && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.companyEmail}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="companyAddress">Adres</Label>
|
||||
<Textarea
|
||||
id="companyAddress"
|
||||
name="companyAddress"
|
||||
rows={2}
|
||||
defaultValue={defaults.companyAddress}
|
||||
placeholder="İl, ilçe, açık adres"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Coins className="size-4" />
|
||||
Finans varsayılanları
|
||||
</CardTitle>
|
||||
<CardDescription>Yeni iş ve faturalarda kullanılan varsayılanlar.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-5 md:grid-cols-3">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="defaultCurrency">Varsayılan para birimi</Label>
|
||||
<Input
|
||||
id="defaultCurrency"
|
||||
name="defaultCurrency"
|
||||
defaultValue={defaults.defaultCurrency}
|
||||
maxLength={8}
|
||||
placeholder="TRY"
|
||||
style={{ textTransform: "uppercase" }}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{canEdit && (
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Kaydediliyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="size-4" />
|
||||
Kaydet
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Metadata } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getLogoUrl } from "@/lib/appwrite/storage";
|
||||
import { requireTenant } from "@/lib/appwrite/tenant-guard";
|
||||
import { LogoUploader } from "./components/logo-uploader";
|
||||
import { WorkspaceSettingsForm } from "./components/workspace-form";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "DLS — Şirket bilgileri",
|
||||
};
|
||||
|
||||
export default async function WorkspaceSettingsPage() {
|
||||
let ctx;
|
||||
try {
|
||||
ctx = await requireTenant();
|
||||
} catch {
|
||||
redirect("/onboarding");
|
||||
}
|
||||
|
||||
const canEdit = ctx.role === "owner" || ctx.role === "admin";
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-6 px-6 pt-0">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-muted-foreground text-sm">{ctx.settings?.companyName ?? "Çalışma alanı"}</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight">Şirket bilgileri</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Faturalarda ve panel başlığında görünecek şirket bilgileri.
|
||||
{!canEdit && " Düzenlemek için yönetici yetkisine ihtiyacınız var."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<LogoUploader
|
||||
canEdit={canEdit}
|
||||
currentLogoUrl={getLogoUrl(ctx.settings?.logo)}
|
||||
companyName={ctx.settings?.companyName ?? "Çalışma alanı"}
|
||||
/>
|
||||
|
||||
<WorkspaceSettingsForm
|
||||
canEdit={canEdit}
|
||||
defaults={{
|
||||
companyName: ctx.settings?.companyName ?? "",
|
||||
companyTaxId: ctx.settings?.companyTaxId ?? "",
|
||||
companyAddress: ctx.settings?.companyAddress ?? "",
|
||||
companyEmail: ctx.settings?.companyEmail ?? "",
|
||||
companyPhone: ctx.settings?.companyPhone ?? "",
|
||||
defaultCurrency: ctx.settings?.defaultCurrency ?? "TRY",
|
||||
kind: ctx.settings?.kind ?? null,
|
||||
memberNumber: ctx.settings?.memberNumber ?? "",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { CheckCircle2, Loader2 } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { acceptInviteAction } from "@/lib/appwrite/team-actions";
|
||||
import { useState } from "react";
|
||||
|
||||
export function AcceptInviteButton({ code }: { code: string }) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const handleAccept = () => {
|
||||
setError(null);
|
||||
startTransition(async () => {
|
||||
const result = await acceptInviteAction(code);
|
||||
if (result.ok) {
|
||||
router.push("/dashboard");
|
||||
} else {
|
||||
setError(result.error ?? "Beklenmeyen hata.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={handleAccept} disabled={isPending} className="w-full">
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Katılınıyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle2 className="size-4" />
|
||||
Daveti kabul et
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{error && (
|
||||
<p className="text-destructive text-center text-sm" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Logo } from "@/components/logo";
|
||||
import { resolveInviteCode } from "@/lib/appwrite/team-actions";
|
||||
import { createAdminClient, getCurrentUser } from "@/lib/appwrite/server";
|
||||
import { DATABASE_ID, TABLES, type TenantSettings } from "@/lib/appwrite/schema";
|
||||
import { AcceptInviteButton } from "./accept-invite-button";
|
||||
import { Query } from "node-appwrite";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "DLS — Davet",
|
||||
};
|
||||
|
||||
async function getCompanyName(tenantId: string): Promise<string | null> {
|
||||
try {
|
||||
const { tablesDB } = createAdminClient();
|
||||
const result = await tablesDB.listRows({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.tenantSettings,
|
||||
queries: [Query.equal("tenantId", tenantId), Query.limit(1)],
|
||||
});
|
||||
return (result.rows[0] as unknown as TenantSettings | undefined)?.companyName ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function InvitePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ code: string }>;
|
||||
}) {
|
||||
const { code } = await params;
|
||||
const invite = await resolveInviteCode(code);
|
||||
const user = await getCurrentUser();
|
||||
|
||||
return (
|
||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
|
||||
<div className="flex w-full max-w-md flex-col gap-6">
|
||||
<Link href="/" className="flex items-center justify-center gap-2 font-medium">
|
||||
<div className="bg-primary text-primary-foreground flex size-9 items-center justify-center rounded-md">
|
||||
<Logo size={22} />
|
||||
</div>
|
||||
<span className="text-xl font-semibold">DLS</span>
|
||||
</Link>
|
||||
|
||||
{!invite || invite.status === "cancelled" ? (
|
||||
<InvalidCard reason="Davet bulunamadı veya iptal edilmiş." />
|
||||
) : invite.status === "accepted" ? (
|
||||
<InvalidCard reason="Bu davet daha önce kabul edilmiş." action="dashboard" />
|
||||
) : invite.expiresAt && new Date(invite.expiresAt).getTime() < Date.now() ? (
|
||||
<InvalidCard reason="Bu davetin süresi dolmuş." />
|
||||
) : (
|
||||
<ValidInvite
|
||||
code={code}
|
||||
email={invite.email}
|
||||
role={invite.role ?? "member"}
|
||||
companyName={(await getCompanyName(invite.tenantId)) ?? "Bir çalışma alanı"}
|
||||
currentUserEmail={user?.email ?? null}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InvalidCard({ reason, action }: { reason: string; action?: "dashboard" }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Davet kullanılamıyor</CardTitle>
|
||||
<CardDescription>{reason}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex justify-center">
|
||||
<Button asChild variant="outline">
|
||||
<Link href={action === "dashboard" ? "/dashboard" : "/sign-in"}>
|
||||
{action === "dashboard" ? "Panele git" : "Giriş yap"}
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function ValidInvite({
|
||||
code,
|
||||
email,
|
||||
role,
|
||||
companyName,
|
||||
currentUserEmail,
|
||||
}: {
|
||||
code: string;
|
||||
email: string;
|
||||
role: string;
|
||||
companyName: string;
|
||||
currentUserEmail: string | null;
|
||||
}) {
|
||||
const roleLabel = role === "admin" ? "Yönetici" : "Üye";
|
||||
const emailMatches = currentUserEmail?.toLowerCase() === email.toLowerCase();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-2xl">{companyName}</CardTitle>
|
||||
<CardDescription>
|
||||
<strong>{email}</strong> olarak <strong>{roleLabel}</strong> rolüyle çalışma alanına davet
|
||||
edildiniz.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
{!currentUserEmail ? (
|
||||
<>
|
||||
<Button asChild className="w-full">
|
||||
<Link href={`/sign-up?invite=${code}&email=${encodeURIComponent(email)}`}>
|
||||
Hesap oluşturup katıl
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="w-full">
|
||||
<Link href={`/sign-in?invite=${code}`}>Zaten hesabım var, giriş yap</Link>
|
||||
</Button>
|
||||
</>
|
||||
) : emailMatches ? (
|
||||
<AcceptInviteButton code={code} />
|
||||
) : (
|
||||
<>
|
||||
<p className="text-destructive text-center text-sm">
|
||||
Şu an <strong>{currentUserEmail}</strong> ile giriş yapmışsınız. Davet{" "}
|
||||
<strong>{email}</strong> içindir. Doğru hesabı kullanın.
|
||||
</p>
|
||||
<Button asChild variant="outline" className="w-full">
|
||||
<Link href={`/sign-in?invite=${code}`}>Farklı hesapla giriş yap</Link>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,172 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--font-sans: var(--font-inter);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure consistent font rendering across different DPI levels */
|
||||
@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) {
|
||||
body {
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fix for sidebar "none" mode height issue */
|
||||
.sidebar-none-mode [data-slot="sidebar"] {
|
||||
height: 100vh !important;
|
||||
min-height: 100vh !important;
|
||||
}
|
||||
|
||||
/* Fix for right-side inset variant support */
|
||||
@media (min-width: 768px) {
|
||||
/* Right sidebar inset variant - margin adjustments */
|
||||
[data-side="right"][data-variant="inset"] ~ [data-slot="sidebar-inset"] {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
/* Right sidebar inset variant - collapsed state margin */
|
||||
[data-side="right"][data-variant="inset"][data-state="collapsed"] ~ [data-slot="sidebar-inset"] {
|
||||
margin-right: 0.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth scrolling for the entire page */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Logo carousel animation */
|
||||
@keyframes logo-scroll {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-144rem); /* 12 items * 12rem = 144rem */
|
||||
}
|
||||
}
|
||||
|
||||
.animate-logo-scroll {
|
||||
animation: logo-scroll 30s linear infinite;
|
||||
}
|
||||
|
||||
.animate-logo-scroll:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { CardDecorator } from '@/components/ui/card-decorator'
|
||||
import { Github, Code, Palette, Layout, Crown } from 'lucide-react'
|
||||
|
||||
const values = [
|
||||
{
|
||||
icon: Code,
|
||||
title: 'Developer First',
|
||||
description: 'Every component is built with the developer experience in mind, ensuring clean code and easy integration.'
|
||||
},
|
||||
{
|
||||
icon: Palette,
|
||||
title: 'Design Excellence',
|
||||
description: 'We maintain the highest design standards, following shadcn/ui principles and modern UI patterns.'
|
||||
},
|
||||
{
|
||||
icon: Layout,
|
||||
title: 'Production Ready',
|
||||
description: 'Battle-tested components used in real applications with proven performance and reliability across different environments.'
|
||||
},
|
||||
{
|
||||
icon: Crown,
|
||||
title: 'Premium Quality',
|
||||
description: 'Hand-crafted with attention to detail and performance optimization, ensuring exceptional user experience and accessibility.'
|
||||
}
|
||||
]
|
||||
|
||||
export function AboutSection() {
|
||||
return (
|
||||
<section id="about" className="py-24 sm:py-32">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Section Header */}
|
||||
<div className="mx-auto max-w-4xl text-center mb-16">
|
||||
<Badge variant="outline" className="mb-4">
|
||||
About ShadcnStore
|
||||
</Badge>
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-6">
|
||||
Built for developers, by developers
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground mb-8">
|
||||
We're passionate about creating the best marketplace for shadcn/ui components and templates.
|
||||
Our mission is to accelerate development and help developers build beautiful admin interfaces faster.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Modern Values Grid with Enhanced Design */}
|
||||
<div className="grid grid-cols-1 gap-x-8 gap-y-12 sm:grid-cols-2 xl:grid-cols-4 mb-12">
|
||||
{values.map((value, index) => (
|
||||
<Card key={index} className='group shadow-xs py-2'>
|
||||
<CardContent className='p-8'>
|
||||
<div className='flex flex-col items-center text-center'>
|
||||
<CardDecorator>
|
||||
<value.icon className='h-6 w-6' aria-hidden />
|
||||
</CardDecorator>
|
||||
<h3 className='mt-6 font-medium text-balance'>{value.title}</h3>
|
||||
<p className='text-muted-foreground mt-3 text-sm'>{value.description}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Call to Action */}
|
||||
<div className="mt-16 text-center">
|
||||
<div className="flex items-center justify-center gap-2 mb-6">
|
||||
<span className="text-muted-foreground">❤️ Made with love for the developer community</span>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button size="lg" className="cursor-pointer" asChild>
|
||||
<a href="https://github.com/silicondeck/shadcn-dashboard-landing-template" target="_blank" rel="noopener noreferrer">
|
||||
<Github className="mr-2 h-4 w-4" />
|
||||
Star on GitHub
|
||||
</a>
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" className="cursor-pointer" asChild>
|
||||
<a href="https://discord.com/invite/XEQhPc9a6p" target="_blank" rel="noopener noreferrer">
|
||||
Join Discord Community
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
"use client"
|
||||
|
||||
import Image from 'next/image'
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
const blogs = [
|
||||
{
|
||||
id: 1,
|
||||
image: 'https://ui.shadcn.com/placeholder.svg',
|
||||
category: 'Technology',
|
||||
title: 'AI Development Catalysts',
|
||||
description:
|
||||
'Exploring how AI-driven tools are transforming software development workflows and accelerating innovation.',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
image: 'https://ui.shadcn.com/placeholder.svg',
|
||||
category: 'Lifestyle',
|
||||
title: 'Minimalist Living Guide',
|
||||
description:
|
||||
'Minimalist living approaches that can help reduce stress and create more meaningful daily experiences.',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
image: 'https://ui.shadcn.com/placeholder.svg',
|
||||
category: 'Design',
|
||||
title: 'Accessible UI Trends',
|
||||
description:
|
||||
'How modern UI trends are embracing accessibility while maintaining sleek, intuitive user experiences.',
|
||||
},
|
||||
]
|
||||
|
||||
export function BlogSection() {
|
||||
return (
|
||||
<section id="blog" className="py-24 sm:py-32 bg-muted/50">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Section Header */}
|
||||
<div className="mx-auto max-w-2xl text-center mb-16">
|
||||
<Badge variant="outline" className="mb-4">Latest Insights</Badge>
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
|
||||
From our blog
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Stay updated with the latest trends, best practices, and insights from our team of experts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Blog Grid */}
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{blogs.map(blog => (
|
||||
<Card key={blog.id} className="overflow-hidden py-0">
|
||||
<CardContent className="px-0">
|
||||
<div className="aspect-video">
|
||||
<Image
|
||||
src={blog.image}
|
||||
alt={blog.title}
|
||||
width={400}
|
||||
height={225}
|
||||
className="size-full object-cover dark:invert dark:brightness-[0.95]"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-3 p-6">
|
||||
<p className="text-muted-foreground text-xs tracking-widest uppercase">
|
||||
{blog.category}
|
||||
</p>
|
||||
<a
|
||||
href="#"
|
||||
onClick={e => e.preventDefault()}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<h3 className="text-xl font-bold hover:text-primary transition-colors">{blog.title}</h3>
|
||||
</a>
|
||||
<p className="text-muted-foreground">{blog.description}</p>
|
||||
<a
|
||||
href="#"
|
||||
onClick={e => e.preventDefault()}
|
||||
className="inline-flex items-center gap-2 text-primary hover:underline cursor-pointer"
|
||||
>
|
||||
Learn More
|
||||
<ArrowRight className="size-4" />
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Mail, MessageCircle, Github, BookOpen } from 'lucide-react'
|
||||
|
||||
const contactFormSchema = z.object({
|
||||
firstName: z.string().min(2, {
|
||||
message: "First name must be at least 2 characters.",
|
||||
}),
|
||||
lastName: z.string().min(2, {
|
||||
message: "Last name must be at least 2 characters.",
|
||||
}),
|
||||
email: z.string().email({
|
||||
message: "Please enter a valid email address.",
|
||||
}),
|
||||
subject: z.string().min(5, {
|
||||
message: "Subject must be at least 5 characters.",
|
||||
}),
|
||||
message: z.string().min(10, {
|
||||
message: "Message must be at least 10 characters.",
|
||||
}),
|
||||
})
|
||||
|
||||
export function ContactSection() {
|
||||
const form = useForm<z.infer<typeof contactFormSchema>>({
|
||||
resolver: zodResolver(contactFormSchema),
|
||||
defaultValues: {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
subject: "",
|
||||
message: "",
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(values: z.infer<typeof contactFormSchema>) {
|
||||
// Here you would typically send the form data to your backend
|
||||
console.log(values)
|
||||
// You could also show a success message or redirect
|
||||
form.reset()
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="contact" className="py-24 sm:py-32">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-2xl text-center mb-16">
|
||||
<Badge variant="outline" className="mb-4">Get In Touch</Badge>
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
|
||||
Need help or have questions?
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Our team is here to help you get the most out of ShadcnStore. Choose the best way to reach out to us.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 lg:grid-cols-3">
|
||||
{/* Contact Options */}
|
||||
<div className="space-y-6 order-2 lg:order-1">
|
||||
<Card className="hover:shadow-md transition-shadow cursor-pointer">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<MessageCircle className="h-5 w-5 text-primary" />
|
||||
Discord Community
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-3">
|
||||
Join our active community for quick help and discussions with other developers.
|
||||
</p>
|
||||
<Button variant="outline" size="sm" className="cursor-pointer" asChild>
|
||||
<a href="https://discord.com/invite/XEQhPc9a6p" target="_blank" rel="noopener noreferrer">
|
||||
Join Discord
|
||||
</a>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:shadow-md transition-shadow cursor-pointer">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Github className="h-5 w-5 text-primary" />
|
||||
GitHub Issues
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-3">
|
||||
Report bugs, request features, or contribute to our open source repository.
|
||||
</p>
|
||||
<Button variant="outline" size="sm" className="cursor-pointer" asChild>
|
||||
<a href="https://github.com/silicondeck/shadcn-dashboard-landing-template/issues" target="_blank" rel="noopener noreferrer">
|
||||
View on GitHub
|
||||
</a>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:shadow-md transition-shadow cursor-pointer">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<BookOpen className="h-5 w-5 text-primary" />
|
||||
Documentation
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-3">
|
||||
Browse our comprehensive guides, tutorials, and component documentation.
|
||||
</p>
|
||||
<Button variant="outline" size="sm" className="cursor-pointer" asChild>
|
||||
<a href="#">
|
||||
View Docs
|
||||
</a>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Contact Form */}
|
||||
<div className="lg:col-span-2 order-1 lg:order-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Mail className="h-5 w-5" />
|
||||
Send us a message
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="firstName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>First name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="John" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Last name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Doe" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" placeholder="john@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subject"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Subject</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Component request, bug report, general inquiry..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="message"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Message</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Tell us how we can help you with ShadcnStore components..."
|
||||
rows={10}
|
||||
className="min-h-50"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full cursor-pointer">
|
||||
Send Message
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
"use client"
|
||||
|
||||
import { ArrowRight, TrendingUp, Package, Github } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
|
||||
export function CTASection() {
|
||||
return (
|
||||
<section className='py-16 lg:py-24 bg-muted/80'>
|
||||
<div className='container mx-auto px-4 lg:px-8'>
|
||||
<div className='mx-auto max-w-4xl'>
|
||||
<div className='text-center'>
|
||||
<div className='space-y-8'>
|
||||
{/* Badge and Stats */}
|
||||
<div className='flex flex-col items-center gap-4'>
|
||||
<Badge variant='outline' className='flex items-center gap-2'>
|
||||
<TrendingUp className='size-3' />
|
||||
Productivity Suite
|
||||
</Badge>
|
||||
|
||||
<div className='text-muted-foreground flex items-center gap-4 text-sm'>
|
||||
<span className='flex items-center gap-1'>
|
||||
<div className='size-2 rounded-full bg-green-500' />
|
||||
150+ Blocks
|
||||
</span>
|
||||
<Separator orientation='vertical' className='!h-4' />
|
||||
<span>25K+ Downloads</span>
|
||||
<Separator orientation='vertical' className='!h-4' />
|
||||
<span>4.9★ Rating</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className='space-y-6'>
|
||||
<h1 className='text-4xl font-bold tracking-tight text-balance sm:text-5xl lg:text-6xl'>
|
||||
Supercharge your team's
|
||||
<span className='flex sm:inline-flex justify-center'>
|
||||
<span className='relative mx-2'>
|
||||
<span className='bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent'>
|
||||
performance
|
||||
</span>
|
||||
<div className='absolute start-0 -bottom-2 h-1 w-full bg-gradient-to-r from-primary/30 to-secondary/30' />
|
||||
</span>
|
||||
today
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className='text-muted-foreground mx-auto max-w-2xl text-balance lg:text-xl'>
|
||||
Stop building from scratch. Get production-ready components, templates and dashboards
|
||||
that integrate seamlessly with your shadcn/ui projects.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className='flex flex-col justify-center gap-4 sm:flex-row sm:gap-6'>
|
||||
<Button size='lg' className='cursor-pointer px-8 py-6 text-lg font-medium' asChild>
|
||||
<a href='https://shadcnstore.com/blocks' target='_blank' rel='noopener noreferrer'>
|
||||
<Package className='me-2 size-5' />
|
||||
Browse Components
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant='outline' size='lg' className='cursor-pointer px-8 py-6 text-lg font-medium group' asChild>
|
||||
<a href='https://github.com/silicondeck/shadcn-dashboard-landing-template' target='_blank' rel='noopener noreferrer'>
|
||||
<Github className='me-2 size-5' />
|
||||
View on GitHub
|
||||
<ArrowRight className='ms-2 size-4 transition-transform group-hover:translate-x-1' />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Trust Indicators */}
|
||||
<div className='text-muted-foreground flex flex-wrap items-center justify-center gap-6 text-sm'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='size-2 rounded-full bg-green-600 dark:bg-green-400 me-1' />
|
||||
|
||||
<span>Free components available</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='size-2 rounded-full bg-blue-600 dark:bg-blue-400 me-1' />
|
||||
|
||||
<span>Commercial license included</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='size-2 rounded-full bg-purple-600 dark:bg-purple-400 me-1' />
|
||||
|
||||
<span>Regular updates & support</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
"use client"
|
||||
|
||||
import { CircleHelp } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
type FaqItem = {
|
||||
value: string
|
||||
question: string
|
||||
answer: string
|
||||
}
|
||||
|
||||
const faqItems: FaqItem[] = [
|
||||
{
|
||||
value: 'item-1',
|
||||
question: 'How do I integrate ShadcnStore components into my project?',
|
||||
answer:
|
||||
'Integration is simple! All our components are built with shadcn/ui and work with React, Next.js, and Vite. Just copy the component code, install any required dependencies, and paste it into your project. Each component comes with detailed installation instructions and examples.',
|
||||
},
|
||||
{
|
||||
value: 'item-2',
|
||||
question: 'What\'s the difference between free and premium components?',
|
||||
answer:
|
||||
'Free components include essential UI elements like buttons, forms, and basic layouts. Premium components offer advanced features like complex data tables, analytics dashboards, authentication flows, and complete admin templates. Premium also includes Figma files, priority support, and commercial licenses.',
|
||||
},
|
||||
{
|
||||
value: 'item-3',
|
||||
question: 'Can I use these components in commercial projects?',
|
||||
answer:
|
||||
'Yes! Free components come with an MIT license for unlimited use. Premium components include a commercial license that allows usage in client projects, SaaS applications, and commercial products without attribution requirements.',
|
||||
},
|
||||
{
|
||||
value: 'item-4',
|
||||
question: 'Do you provide support and updates?',
|
||||
answer:
|
||||
'Absolutely! We provide community support for free components through our Discord server and GitHub issues. Premium subscribers get priority email support, regular component updates, and early access to new releases. We also maintain compatibility with the latest shadcn/ui versions.',
|
||||
},
|
||||
{
|
||||
value: 'item-5',
|
||||
question: 'What frameworks and tools do you support?',
|
||||
answer:
|
||||
'Our components work with React 18+, Next.js 13+, and Vite. We use TypeScript, Tailwind CSS, and follow shadcn/ui conventions. Components are tested with popular tools like React Hook Form, TanStack Query, and Zustand for state management.',
|
||||
},
|
||||
{
|
||||
value: 'item-6',
|
||||
question: 'How often do you release new components?',
|
||||
answer:
|
||||
'We release new components and templates weekly. Premium subscribers get early access to new releases, while free components are updated regularly based on community feedback. You can track our roadmap and request specific components through our GitHub repository.',
|
||||
},
|
||||
]
|
||||
|
||||
const FaqSection = () => {
|
||||
return (
|
||||
<section id="faq" className="py-24 sm:py-32">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Section Header */}
|
||||
<div className="mx-auto max-w-2xl text-center mb-16">
|
||||
<Badge variant="outline" className="mb-4">FAQ</Badge>
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Everything you need to know about ShadcnStore components, licensing, and integration. Still have questions? We're here to help!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* FAQ Content */}
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className='bg-transparent'>
|
||||
<div className='p-0'>
|
||||
<Accordion type='single' collapsible className='space-y-5'>
|
||||
{faqItems.map(item => (
|
||||
<AccordionItem key={item.value} value={item.value} className='rounded-md !border bg-transparent'>
|
||||
<AccordionTrigger className='cursor-pointer items-center gap-4 rounded-none bg-transparent py-2 ps-3 pe-4 hover:no-underline data-[state=open]:border-b'>
|
||||
<div className='flex items-center gap-4'>
|
||||
<div className='bg-primary/10 text-primary flex size-9 shrink-0 items-center justify-center rounded-full'>
|
||||
<CircleHelp className='size-5' />
|
||||
</div>
|
||||
<span className='text-start font-semibold'>{item.question}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className='p-4 bg-transparent'>{item.answer}</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Support CTA */}
|
||||
<div className="text-center mt-12">
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Still have questions? We're here to help.
|
||||
</p>
|
||||
<Button className='cursor-pointer' asChild>
|
||||
<a href="#contact">
|
||||
Contact Support
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export { FaqSection }
|
||||
@@ -0,0 +1,183 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
BarChart3,
|
||||
Zap,
|
||||
Users,
|
||||
ArrowRight,
|
||||
Database,
|
||||
Package,
|
||||
Crown,
|
||||
Layout,
|
||||
Palette
|
||||
} from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Image3D } from '@/components/image-3d'
|
||||
|
||||
const mainFeatures = [
|
||||
{
|
||||
icon: Package,
|
||||
title: 'Curated Component Library',
|
||||
description: 'Hand-picked blocks and templates for quality and reliability.'
|
||||
},
|
||||
{
|
||||
icon: Crown,
|
||||
title: 'Free & Premium Options',
|
||||
description: 'Start free, upgrade to premium collections when you need more.'
|
||||
},
|
||||
{
|
||||
icon: Layout,
|
||||
title: 'Ready-to-Use Templates',
|
||||
description: 'Copy-paste components that just work out of the box.'
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
title: 'Regular Updates',
|
||||
description: 'New blocks and templates added weekly to keep you current.'
|
||||
}
|
||||
]
|
||||
|
||||
const secondaryFeatures = [
|
||||
{
|
||||
icon: BarChart3,
|
||||
title: 'Multiple Frameworks',
|
||||
description: 'React, Next.js, and Vite compatibility for flexible development.'
|
||||
},
|
||||
{
|
||||
icon: Palette,
|
||||
title: 'Modern Tech Stack',
|
||||
description: 'Built with shadcn/ui, Tailwind CSS, and TypeScript.'
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: 'Responsive Design',
|
||||
description: 'Mobile-first components for all screen sizes and devices.'
|
||||
},
|
||||
{
|
||||
icon: Database,
|
||||
title: 'Developer-Friendly',
|
||||
description: 'Clean code, well-documented, easy integration and customization.'
|
||||
}
|
||||
]
|
||||
|
||||
export function FeaturesSection() {
|
||||
return (
|
||||
<section id="features" className="py-24 sm:py-32 bg-muted/30">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Section Header */}
|
||||
<div className="mx-auto max-w-2xl text-center mb-16">
|
||||
<Badge variant="outline" className="mb-4">Marketplace Features</Badge>
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
|
||||
Everything you need to build amazing web applications
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Our marketplace provides curated blocks, templates, landing pages, and admin dashboards to help you build professional applications faster than ever.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* First Feature Section */}
|
||||
<div className="grid items-center gap-12 lg:grid-cols-2 lg:gap-8 xl:gap-16 mb-24">
|
||||
{/* Left Image */}
|
||||
<Image3D
|
||||
lightSrc="/feature-1-light.png"
|
||||
darkSrc="/feature-1-dark.png"
|
||||
alt="Analytics dashboard"
|
||||
direction="left"
|
||||
/>
|
||||
{/* Right Content */}
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-2xl font-semibold tracking-tight text-balance sm:text-3xl">
|
||||
Components that accelerate development
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-base text-pretty">
|
||||
Our curated marketplace offers premium blocks and templates designed to save time and ensure consistency across your admin projects.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul className="grid gap-4 sm:grid-cols-2">
|
||||
{mainFeatures.map((feature, index) => (
|
||||
<li key={index} className="group hover:bg-accent/5 flex items-start gap-3 p-2 rounded-lg transition-colors">
|
||||
<div className="mt-0.5 flex shrink-0 items-center justify-center">
|
||||
<feature.icon className="size-5 text-primary" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-foreground font-medium">{feature.title}</h3>
|
||||
<p className="text-muted-foreground mt-1 text-sm">{feature.description}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 pe-4 pt-2">
|
||||
<Button size="lg" className="cursor-pointer">
|
||||
<a href="https://shadcnstore.com/templates" className='flex items-center'>
|
||||
Browse Templates
|
||||
<ArrowRight className="ms-2 size-4" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" className="cursor-pointer">
|
||||
<a href="https://shadcnstore.com/blocks">
|
||||
View Components
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Second Feature Section - Flipped Layout */}
|
||||
<div className="grid items-center gap-12 lg:grid-cols-2 lg:gap-8 xl:gap-16">
|
||||
{/* Left Content */}
|
||||
<div className="space-y-6 order-2 lg:order-1">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-2xl font-semibold tracking-tight text-balance sm:text-3xl">
|
||||
Built for modern development workflows
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-base text-pretty">
|
||||
Every component follows best practices with TypeScript, responsive design, and clean code architecture that integrates seamlessly into your projects.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul className="grid gap-4 sm:grid-cols-2">
|
||||
{secondaryFeatures.map((feature, index) => (
|
||||
<li key={index} className="group hover:bg-accent/5 flex items-start gap-3 p-2 rounded-lg transition-colors">
|
||||
<div className="mt-0.5 flex shrink-0 items-center justify-center">
|
||||
<feature.icon className="size-5 text-primary" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-foreground font-medium">{feature.title}</h3>
|
||||
<p className="text-muted-foreground mt-1 text-sm">{feature.description}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 pe-4 pt-2">
|
||||
<Button size="lg" className="cursor-pointer">
|
||||
<a href="#" className='flex items-center'>
|
||||
View Documentation
|
||||
<ArrowRight className="ms-2 size-4" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" className="cursor-pointer">
|
||||
<a href="https://github.com/silicondeck/shadcn-dashboard-landing-template" target="_blank" rel="noopener noreferrer">
|
||||
GitHub Repository
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Image */}
|
||||
<Image3D
|
||||
lightSrc="/feature-2-light.png"
|
||||
darkSrc="/feature-2-dark.png"
|
||||
alt="Performance dashboard"
|
||||
direction="right"
|
||||
className="order-1 lg:order-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Logo } from '@/components/logo'
|
||||
import { Github, Twitter, Linkedin, Youtube, Heart } from 'lucide-react'
|
||||
|
||||
const newsletterSchema = z.object({
|
||||
email: z.string().email({
|
||||
message: "Please enter a valid email address.",
|
||||
}),
|
||||
})
|
||||
|
||||
const footerLinks = {
|
||||
product: [
|
||||
{ name: 'Features', href: '#features' },
|
||||
{ name: 'Pricing', href: '#pricing' },
|
||||
{ name: 'API', href: '#api' },
|
||||
{ name: 'Documentation', href: '#docs' },
|
||||
],
|
||||
company: [
|
||||
{ name: 'About', href: '#about' },
|
||||
{ name: 'Blog', href: '#blog' },
|
||||
{ name: 'Careers', href: '#careers' },
|
||||
{ name: 'Press', href: '#press' },
|
||||
],
|
||||
resources: [
|
||||
{ name: 'Help Center', href: '#help' },
|
||||
{ name: 'Community', href: '#community' },
|
||||
{ name: 'Guides', href: '#guides' },
|
||||
{ name: 'Webinars', href: '#webinars' },
|
||||
],
|
||||
legal: [
|
||||
{ name: 'Privacy', href: '#privacy' },
|
||||
{ name: 'Terms', href: '#terms' },
|
||||
{ name: 'Security', href: '#security' },
|
||||
{ name: 'Status', href: '#status' },
|
||||
],
|
||||
}
|
||||
|
||||
const socialLinks = [
|
||||
{ name: 'Twitter', href: '#', icon: Twitter },
|
||||
{ name: 'GitHub', href: 'https://github.com/silicondeck/shadcn-dashboard-landing-template', icon: Github },
|
||||
{ name: 'LinkedIn', href: '#', icon: Linkedin },
|
||||
{ name: 'YouTube', href: '#', icon: Youtube },
|
||||
]
|
||||
|
||||
export function LandingFooter() {
|
||||
const form = useForm<z.infer<typeof newsletterSchema>>({
|
||||
resolver: zodResolver(newsletterSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(values: z.infer<typeof newsletterSchema>) {
|
||||
// Here you would typically send the email to your newsletter service
|
||||
console.log(values)
|
||||
// Show success message and reset form
|
||||
form.reset()
|
||||
}
|
||||
|
||||
return (
|
||||
<footer className="border-t bg-background">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
{/* Newsletter Section */}
|
||||
<div className="mb-16">
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
<h3 className="text-2xl font-bold mb-4">Stay updated</h3>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Get the latest updates, articles, and resources sent to your inbox weekly.
|
||||
</p>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-2 max-w-md mx-auto sm:flex-row">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="cursor-pointer">Subscribe</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Footer Content */}
|
||||
<div className="grid gap-8 grid-cols-4 lg:grid-cols-6">
|
||||
{/* Brand Column */}
|
||||
<div className="col-span-4 lg:col-span-2 max-w-2xl">
|
||||
<div className="flex items-center space-x-2 mb-4 max-lg:justify-center">
|
||||
<a href="https://shadcnstore.com" target='_blank' className="flex items-center space-x-2 cursor-pointer">
|
||||
<Logo size={32} />
|
||||
<span className="font-bold text-xl">ShadcnStore</span>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-muted-foreground mb-6 max-lg:text-center max-lg:flex max-lg:justify-center">
|
||||
Accelerating web development with curated blocks, templates, landing pages, and admin dashboards designed for modern developers.
|
||||
</p>
|
||||
<div className="flex space-x-4 max-lg:justify-center">
|
||||
{socialLinks.map((social) => (
|
||||
<Button key={social.name} variant="ghost" size="icon" asChild>
|
||||
<a
|
||||
href={social.href}
|
||||
aria-label={social.name}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<social.icon className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Links Columns */}
|
||||
<div className='max-md:col-span-2 lg:col-span-1'>
|
||||
<h4 className="font-semibold mb-4">Product</h4>
|
||||
<ul className="space-y-3">
|
||||
{footerLinks.product.map((link) => (
|
||||
<li key={link.name}>
|
||||
<a
|
||||
href={link.href}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className='max-md:col-span-2 lg:col-span-1'>
|
||||
<h4 className="font-semibold mb-4">Company</h4>
|
||||
<ul className="space-y-3">
|
||||
{footerLinks.company.map((link) => (
|
||||
<li key={link.name}>
|
||||
<a
|
||||
href={link.href}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className='max-md:col-span-2 lg:col-span-1'>
|
||||
<h4 className="font-semibold mb-4">Resources</h4>
|
||||
<ul className="space-y-3">
|
||||
{footerLinks.resources.map((link) => (
|
||||
<li key={link.name}>
|
||||
<a
|
||||
href={link.href}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className='max-md:col-span-2 lg:col-span-1'>
|
||||
<h4 className="font-semibold mb-4">Legal</h4>
|
||||
<ul className="space-y-3">
|
||||
{footerLinks.legal.map((link) => (
|
||||
<li key={link.name}>
|
||||
<a
|
||||
href={link.href}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="my-8" />
|
||||
|
||||
{/* Bottom Footer */}
|
||||
<div className="flex flex-col lg:flex-row justify-between items-center gap-2">
|
||||
<div className="flex flex-col sm:flex-row items-center gap-2 text-muted-foreground text-sm">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Made with</span>
|
||||
<Heart className="h-4 w-4 text-red-500 fill-current" />
|
||||
<span>by</span>
|
||||
<a href="https://shadcnstore.com" target='_blank' className="font-semibold text-foreground hover:text-primary transition-colors cursor-pointer">
|
||||
ShadcnStore
|
||||
</a>
|
||||
</div>
|
||||
<span className="hidden sm:inline">•</span>
|
||||
<span>© {new Date().getFullYear()} for the developer community</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 text-sm text-muted-foreground mt-4 md:mt-0">
|
||||
<a href="#privacy" className="hover:text-foreground transition-colors">
|
||||
Privacy Policy
|
||||
</a>
|
||||
<a href="#terms" className="hover:text-foreground transition-colors">
|
||||
Terms of Service
|
||||
</a>
|
||||
<a href="#cookies" className="hover:text-foreground transition-colors">
|
||||
Cookie Policy
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
"use client"
|
||||
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { ArrowRight, Play, Star } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { DotPattern } from '@/components/dot-pattern'
|
||||
|
||||
export function HeroSection() {
|
||||
return (
|
||||
<section id="hero" className="relative overflow-hidden bg-gradient-to-b from-background to-background/80 pt-16 sm:pt-20 pb-16">
|
||||
{/* Background Pattern */}
|
||||
<div className="absolute inset-0">
|
||||
{/* Dot pattern overlay using reusable component */}
|
||||
<DotPattern className="opacity-100" size="md" fadeStyle="ellipse" />
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
{/* Announcement Badge */}
|
||||
<div className="mb-8 flex justify-center">
|
||||
<Badge variant="outline" className="px-4 py-2 border-foreground">
|
||||
<Star className="w-3 h-3 mr-2 fill-current" />
|
||||
New: Premium Template Collection
|
||||
<ArrowRight className="w-3 h-3 ml-2" />
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Main Headline */}
|
||||
<h1 className="mb-6 text-4xl font-bold tracking-tight sm:text-6xl lg:text-7xl">
|
||||
Build Better
|
||||
<span className="bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
|
||||
{" "}Web Applications{" "}
|
||||
</span>
|
||||
with Ready-Made Components
|
||||
</h1>
|
||||
|
||||
{/* Subheading */}
|
||||
<p className="mx-auto mb-10 max-w-2xl text-lg text-muted-foreground sm:text-xl">
|
||||
Accelerate your development with our curated collection of blocks, templates, landing pages,
|
||||
and admin dashboards. From free components to complete solutions, built with shadcn/ui.
|
||||
</p>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||
<Button size="lg" className="text-base cursor-pointer" asChild>
|
||||
<Link href="/auth/sign-up">
|
||||
Get Started Free
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" className="text-base cursor-pointer" asChild>
|
||||
<a href="#">
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Watch Demo
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero Image/Visual */}
|
||||
<div className="mx-auto mt-20 max-w-6xl">
|
||||
<div className="relative group">
|
||||
{/* Top background glow effect - positioned above the image */}
|
||||
<div className="absolute top-2 lg:-top-8 left-1/2 transform -translate-x-1/2 w-[90%] mx-auto h-24 lg:h-80 bg-primary/50 rounded-full blur-3xl"></div>
|
||||
|
||||
<div className="relative rounded-xl border bg-card shadow-2xl">
|
||||
{/* Light mode dashboard image */}
|
||||
<Image
|
||||
src="/dashboard-light.png"
|
||||
alt="Dashboard Preview - Light Mode"
|
||||
width={1200}
|
||||
height={800}
|
||||
className="w-full rounded-xl object-cover block dark:hidden"
|
||||
priority
|
||||
/>
|
||||
|
||||
{/* Dark mode dashboard image */}
|
||||
<Image
|
||||
src="/dashboard-dark.png"
|
||||
alt="Dashboard Preview - Dark Mode"
|
||||
width={1200}
|
||||
height={800}
|
||||
className="w-full rounded-xl object-cover hidden dark:block"
|
||||
priority
|
||||
/>
|
||||
|
||||
{/* Bottom fade effect - gradient overlay that fades the image to background */}
|
||||
<div className="absolute bottom-0 left-0 w-full h-32 md:h-40 lg:h-48 bg-gradient-to-b from-background/0 via-background/70 to-background rounded-b-xl"></div>
|
||||
|
||||
{/* Overlay play button for demo */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Button
|
||||
size="lg"
|
||||
className="rounded-full h-16 w-16 p-0 cursor-pointer hover:scale-105 transition-transform"
|
||||
asChild
|
||||
>
|
||||
<a href="#" aria-label="Watch demo video">
|
||||
<Play className="h-6 w-6 fill-current" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
"use client"
|
||||
|
||||
import React from 'react'
|
||||
import { Palette, RotateCcw, Settings, X, Dices, Upload, ExternalLink, Sun, Moon } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
|
||||
import { useThemeManager } from '@/hooks/use-theme-manager'
|
||||
import { useCircularTransition } from '@/hooks/use-circular-transition'
|
||||
import { colorThemes, tweakcnThemes } from '@/config/theme-data'
|
||||
import { radiusOptions, baseColors } from '@/config/theme-customizer-constants'
|
||||
import { ColorPicker } from '@/components/color-picker'
|
||||
import { ImportModal } from '@/components/theme-customizer/import-modal'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { ImportedTheme } from '@/types/theme-customizer'
|
||||
import "@/components/theme-customizer/circular-transition.css"
|
||||
|
||||
interface LandingThemeCustomizerProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function LandingThemeCustomizer({ open, onOpenChange }: LandingThemeCustomizerProps) {
|
||||
const {
|
||||
applyImportedTheme,
|
||||
isDarkMode,
|
||||
resetTheme,
|
||||
applyRadius,
|
||||
setBrandColorsValues,
|
||||
applyTheme,
|
||||
applyTweakcnTheme,
|
||||
brandColorsValues,
|
||||
handleColorChange
|
||||
} = useThemeManager()
|
||||
|
||||
const { toggleTheme } = useCircularTransition()
|
||||
|
||||
const [selectedTheme, setSelectedTheme] = React.useState("default")
|
||||
const [selectedTweakcnTheme, setSelectedTweakcnTheme] = React.useState("")
|
||||
const [selectedRadius, setSelectedRadius] = React.useState("0.5rem")
|
||||
const [importModalOpen, setImportModalOpen] = React.useState(false)
|
||||
const [importedTheme, setImportedTheme] = React.useState<ImportedTheme | null>(null)
|
||||
|
||||
const handleReset = () => {
|
||||
// Reset all state variables to initial values
|
||||
setSelectedTheme("")
|
||||
setSelectedTweakcnTheme("")
|
||||
setSelectedRadius("0.5rem")
|
||||
setImportedTheme(null)
|
||||
setBrandColorsValues({})
|
||||
|
||||
// Reset theme and radius to defaults
|
||||
resetTheme()
|
||||
applyRadius("0.5rem")
|
||||
}
|
||||
|
||||
const handleImport = (themeData: ImportedTheme) => {
|
||||
setImportedTheme(themeData)
|
||||
// Clear other selections to indicate custom import is active
|
||||
setSelectedTheme("")
|
||||
setSelectedTweakcnTheme("")
|
||||
|
||||
// Apply the imported theme
|
||||
applyImportedTheme(themeData, isDarkMode)
|
||||
}
|
||||
|
||||
const handleImportClick = () => {
|
||||
setImportModalOpen(true)
|
||||
}
|
||||
|
||||
const handleRandomShadcn = () => {
|
||||
// Apply a random shadcn theme
|
||||
const randomTheme = colorThemes[Math.floor(Math.random() * colorThemes.length)]
|
||||
setSelectedTheme(randomTheme.value)
|
||||
setSelectedTweakcnTheme("")
|
||||
setBrandColorsValues({})
|
||||
setImportedTheme(null)
|
||||
applyTheme(randomTheme.value, isDarkMode)
|
||||
}
|
||||
|
||||
const handleRandomTweakcn = () => {
|
||||
// Apply a random tweakcn theme
|
||||
const randomTheme = tweakcnThemes[Math.floor(Math.random() * tweakcnThemes.length)]
|
||||
setSelectedTweakcnTheme(randomTheme.value)
|
||||
setSelectedTheme("")
|
||||
setBrandColorsValues({})
|
||||
setImportedTheme(null)
|
||||
applyTweakcnTheme(randomTheme.preset, isDarkMode)
|
||||
}
|
||||
|
||||
const handleRadiusSelect = (radius: string) => {
|
||||
setSelectedRadius(radius)
|
||||
applyRadius(radius)
|
||||
}
|
||||
|
||||
const handleLightMode = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (isDarkMode === false) return
|
||||
toggleTheme(event)
|
||||
}
|
||||
|
||||
const handleDarkMode = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (isDarkMode === true) return
|
||||
toggleTheme(event)
|
||||
}
|
||||
|
||||
// Re-apply themes when theme mode changes
|
||||
React.useEffect(() => {
|
||||
if (importedTheme) {
|
||||
applyImportedTheme(importedTheme, isDarkMode)
|
||||
} else if (selectedTheme) {
|
||||
applyTheme(selectedTheme, isDarkMode)
|
||||
} else if (selectedTweakcnTheme) {
|
||||
const selectedPreset = tweakcnThemes.find(t => t.value === selectedTweakcnTheme)?.preset
|
||||
if (selectedPreset) {
|
||||
applyTweakcnTheme(selectedPreset, isDarkMode)
|
||||
}
|
||||
}
|
||||
}, [isDarkMode, importedTheme, selectedTheme, selectedTweakcnTheme, applyImportedTheme, applyTheme, applyTweakcnTheme])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sheet open={open} onOpenChange={onOpenChange} modal={false}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
className="w-[400px] p-0 gap-0 pointer-events-auto [&>button]:hidden overflow-hidden flex flex-col"
|
||||
onInteractOutside={(e) => {
|
||||
// Prevent the sheet from closing when dialog is open
|
||||
if (importModalOpen) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SheetHeader className="space-y-0 p-4 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<Settings className="h-4 w-4" />
|
||||
</div>
|
||||
<SheetTitle className="text-lg font-semibold">Theme Customizer</SheetTitle>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" onClick={handleReset} className="cursor-pointer h-8 w-8">
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={() => onOpenChange(false)} className="cursor-pointer h-8 w-8">
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<SheetDescription className="text-sm text-muted-foreground">
|
||||
Customize the theme and colors of your landing page.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
{/* Mode Section */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Mode</Label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
variant={!isDarkMode ? "secondary" : "outline"}
|
||||
size="sm"
|
||||
onClick={handleLightMode}
|
||||
className="cursor-pointer mode-toggle-button relative overflow-hidden"
|
||||
>
|
||||
<Sun className="h-4 w-4 mr-1 transition-transform duration-300" />
|
||||
Light
|
||||
</Button>
|
||||
<Button
|
||||
variant={isDarkMode ? "secondary" : "outline"}
|
||||
size="sm"
|
||||
onClick={handleDarkMode}
|
||||
className="cursor-pointer mode-toggle-button relative overflow-hidden"
|
||||
>
|
||||
<Moon className="h-4 w-4 mr-1 transition-transform duration-300" />
|
||||
Dark
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Shadcn UI Theme Presets */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">Shadcn UI Theme Presets</Label>
|
||||
<Button variant="outline" size="sm" onClick={handleRandomShadcn} className="cursor-pointer">
|
||||
<Dices className="h-3.5 w-3.5 mr-1.5" />
|
||||
Random
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Select value={selectedTheme} onValueChange={(value) => {
|
||||
setSelectedTheme(value)
|
||||
setSelectedTweakcnTheme("")
|
||||
setBrandColorsValues({})
|
||||
setImportedTheme(null)
|
||||
applyTheme(value, isDarkMode)
|
||||
}}>
|
||||
<SelectTrigger className="w-full cursor-pointer">
|
||||
<SelectValue placeholder="Choose Shadcn Theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-60">
|
||||
<div className="p-2">
|
||||
{colorThemes.map((theme) => (
|
||||
<SelectItem key={theme.value} value={theme.value} className="cursor-pointer">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-1">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-border/20"
|
||||
style={{ backgroundColor: theme.preset.styles.light.primary }}
|
||||
/>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-border/20"
|
||||
style={{ backgroundColor: theme.preset.styles.light.secondary }}
|
||||
/>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-border/20"
|
||||
style={{ backgroundColor: theme.preset.styles.light.accent }}
|
||||
/>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-border/20"
|
||||
style={{ backgroundColor: theme.preset.styles.light.muted }}
|
||||
/>
|
||||
</div>
|
||||
<span>{theme.name}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Tweakcn Theme Presets */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">Tweakcn Theme Presets</Label>
|
||||
<Button variant="outline" size="sm" onClick={handleRandomTweakcn} className="cursor-pointer">
|
||||
<Dices className="h-3.5 w-3.5 mr-1.5" />
|
||||
Random
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Select value={selectedTweakcnTheme} onValueChange={(value) => {
|
||||
setSelectedTweakcnTheme(value)
|
||||
setSelectedTheme("")
|
||||
setBrandColorsValues({})
|
||||
setImportedTheme(null)
|
||||
const selectedPreset = tweakcnThemes.find(t => t.value === value)?.preset
|
||||
if (selectedPreset) {
|
||||
applyTweakcnTheme(selectedPreset, isDarkMode)
|
||||
}
|
||||
}}>
|
||||
<SelectTrigger className="w-full cursor-pointer">
|
||||
<SelectValue placeholder="Choose Tweakcn Theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-60">
|
||||
<div className="p-2">
|
||||
{tweakcnThemes.map((theme) => (
|
||||
<SelectItem key={theme.value} value={theme.value} className="cursor-pointer">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-1">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-border/20"
|
||||
style={{ backgroundColor: theme.preset.styles.light.primary }}
|
||||
/>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-border/20"
|
||||
style={{ backgroundColor: theme.preset.styles.light.secondary }}
|
||||
/>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-border/20"
|
||||
style={{ backgroundColor: theme.preset.styles.light.accent }}
|
||||
/>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-border/20"
|
||||
style={{ backgroundColor: theme.preset.styles.light.muted }}
|
||||
/>
|
||||
</div>
|
||||
<span>{theme.name}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Radius Selection */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Radius</Label>
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{radiusOptions.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={`relative cursor-pointer rounded-md p-3 border transition-colors ${
|
||||
selectedRadius === option.value
|
||||
? "border-primary"
|
||||
: "border-border hover:border-border/60"
|
||||
}`}
|
||||
onClick={() => handleRadiusSelect(option.value)}
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="text-xs font-medium">{option.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Import Theme Button */}
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={handleImportClick}
|
||||
className="w-full cursor-pointer"
|
||||
>
|
||||
<Upload className="h-3.5 w-3.5 mr-1.5" />
|
||||
Import Theme
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Brand Colors Section */}
|
||||
<Accordion type="single" collapsible className="w-full border-b rounded-lg">
|
||||
<AccordionItem value="brand-colors" className="border border-border rounded-lg overflow-hidden">
|
||||
<AccordionTrigger className="px-4 py-3 hover:no-underline hover:bg-muted/50 transition-colors">
|
||||
<Label className="text-sm font-medium cursor-pointer">Brand Colors</Label>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-4 pt-2 space-y-3 border-t border-border bg-muted/20">
|
||||
{baseColors.map((color) => (
|
||||
<div key={color.cssVar} className="flex items-center justify-between">
|
||||
<ColorPicker
|
||||
label={color.name}
|
||||
cssVar={color.cssVar}
|
||||
value={brandColorsValues[color.cssVar] || ""}
|
||||
onChange={handleColorChange}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
{/* Tweakcn */}
|
||||
<div className="p-4 bg-muted rounded-lg space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Palette className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm font-medium">Advanced Customization</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
For advanced theme customization with real-time preview, visual color picker, and hundreds of prebuilt themes, visit{" "}
|
||||
<a
|
||||
href="https://tweakcn.com/editor/theme"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline font-medium cursor-pointer"
|
||||
>
|
||||
tweakcn.com
|
||||
</a>
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full cursor-pointer"
|
||||
onClick={() => window.open('https://tweakcn.com/editor/theme', '_blank')}
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5 mr-1.5" />
|
||||
Open Tweakcn
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<ImportModal
|
||||
open={importModalOpen}
|
||||
onOpenChange={setImportModalOpen}
|
||||
onImport={handleImport}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Floating trigger button for landing page
|
||||
export function LandingThemeCustomizerTrigger({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<Button
|
||||
onClick={onClick}
|
||||
size="icon"
|
||||
className={cn(
|
||||
"fixed top-1/2 -translate-y-1/2 h-12 w-12 rounded-full shadow-lg z-50 bg-primary hover:bg-primary/90 text-primary-foreground cursor-pointer right-4"
|
||||
)}
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
"use client"
|
||||
|
||||
import { Card } from '@/components/ui/card'
|
||||
|
||||
// Simple icon component for company logos
|
||||
const SimpleIcon = ({ iconSlug, size = 24 }: { iconSlug: string; size?: number }) => {
|
||||
const iconMap = {
|
||||
shopify:
|
||||
'M15.337 23.979l7.216-1.561s-2.604-17.613-2.625-17.73c-.018-.116-.114-.192-.211-.192s-1.929-.136-1.929-.136-1.275-1.274-1.439-1.411c-.045-.037-.075-.057-.121-.074l-.914 21.104h.023zM11.71 11.305s-.81-.424-1.774-.424c-1.447 0-1.504.906-1.504 1.141 0 1.232 3.24 1.715 3.24 4.629 0 2.295-1.44 3.76-3.406 3.76-2.354 0-3.54-1.465-3.54-1.465l.646-2.086s1.245 1.066 2.28 1.066c.675 0 .975-.545.975-.932 0-1.619-2.654-1.694-2.654-4.359-.034-2.237 1.571-4.416 4.827-4.416 1.257 0 1.875.361 1.875.361l-.945 2.715-.02.01zM11.17.83c.136 0 .271.038.405.135-.984.465-2.064 1.639-2.508 3.992-.656.213-1.293.405-1.889.578C7.697 3.75 8.951.84 11.17.84V.83zm1.235 2.949v.135c-.754.232-1.583.484-2.394.736.466-1.777 1.333-2.645 2.085-2.971.193.501.309 1.176.309 2.1zm.539-2.234c.694.074 1.141.867 1.429 1.755-.349.114-.735.231-1.158.366v-.252c0-.752-.096-1.371-.271-1.871v.002zm2.992 1.289c-.02 0-.06.021-.078.021s-.289.075-.714.21c-.423-1.233-1.176-2.37-2.508-2.37h-.115C12.135.209 11.669 0 11.265 0 8.159 0 6.675 3.877 6.21 5.846c-1.194.365-2.063.636-2.16.674-.675.213-.694.232-.772.87-.075.462-1.83 14.063-1.83 14.063L15.009 24l.927-21.166z',
|
||||
netflix:
|
||||
'm5.398 0 8.348 23.602c2.346.059 4.856.398 4.856.398L10.113 0H5.398zm8.489 0v9.172l4.715 13.33V0h-4.715zM5.398 1.5V24c1.873-.225 2.81-.312 4.715-.398V14.83L5.398 1.5z',
|
||||
spotify:
|
||||
'M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z',
|
||||
airbnb:
|
||||
'M12.001 18.275c-1.353-1.697-2.148-3.184-2.413-4.457-.263-1.027-.16-1.848.291-2.465.477-.71 1.188-1.056 2.121-1.056s1.643.345 2.12 1.063c.446.61.558 1.432.286 2.465-.291 1.298-1.085 2.785-2.412 4.458zm9.601 1.14c-.185 1.246-1.034 2.28-2.2 2.783-2.253.98-4.483-.583-6.392-2.704 3.157-3.951 3.74-7.028 2.385-9.018-.795-1.14-1.933-1.695-3.394-1.695-2.944 0-4.563 2.49-3.927 5.382.37 1.565 1.352 3.343 2.917 5.332-.98 1.085-1.91 1.856-2.732 2.333-.636.344-1.245.558-1.828.609-2.679.399-4.778-2.2-3.825-4.88.132-.345.395-.98.845-1.961l.025-.053c1.464-3.178 3.242-6.79 5.285-10.795l.053-.132.58-1.116c.45-.822.635-1.19 1.351-1.643.346-.21.77-.315 1.246-.315.954 0 1.698.558 2.016 1.007.158.239.345.557.582.953l.558 1.089.08.159c2.041 4.004 3.821 7.608 5.279 10.794l.026.025.533 1.22.318.764c.243.613.294 1.222.213 1.858zm1.22-2.39c-.186-.583-.505-1.271-.9-2.094v-.03c-1.889-4.006-3.642-7.608-5.307-10.844l-.111-.163C15.317 1.461 14.468 0 12.001 0c-2.44 0-3.476 1.695-4.535 3.898l-.081.16c-1.669 3.236-3.421 6.843-5.303 10.847v.053l-.559 1.22c-.21.504-.317.768-.345.847C-.172 20.74 2.611 24 5.98 24c.027 0 .132 0 .265-.027h.372c1.75-.213 3.554-1.325 5.384-3.317 1.829 1.989 3.635 3.104 5.382 3.317h.372c.133.027.239.027.265.027 3.37.003 6.152-3.261 4.802-6.975z',
|
||||
dropbox:
|
||||
'M6 1.807L0 5.629l6 3.822 6.001-3.822L6 1.807zM18 1.807l-6 3.822 6 3.822 6-3.822-6-3.822zM0 13.274l6 3.822 6.001-3.822L6 9.452l-6 3.822zM18 9.452l-6 3.822 6 3.822 6-3.822-6-3.822zM6 18.371l6.001 3.822 6-3.822-6-3.822L6 18.371z',
|
||||
stripe:
|
||||
'M13.976 9.15c-2.172-.806-3.356-1.426-3.356-2.409 0-.831.683-1.305 1.901-1.305 2.227 0 4.515.858 6.09 1.631l.89-5.494C18.252.975 15.697 0 12.165 0 9.667 0 7.589.654 6.104 1.872 4.56 3.147 3.757 4.992 3.757 7.218c0 4.039 2.467 5.76 6.476 7.219 2.585.92 3.445 1.574 3.445 2.583 0 .98-.84 1.545-2.354 1.545-1.875 0-4.965-.921-6.99-2.109l-.9 5.555C5.175 22.99 8.385 24 11.714 24c2.641 0 4.843-.624 6.328-1.813 1.664-1.305 2.525-3.236 2.525-5.732 0-4.128-2.524-5.851-6.594-7.305h.003z',
|
||||
google:
|
||||
'M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z',
|
||||
apple:
|
||||
'M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701',
|
||||
meta: 'M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z',
|
||||
tesla:
|
||||
'M12 5.362l2.475-3.026s4.245.09 8.471 2.054c-1.082 1.636-3.231 2.438-3.231 2.438-.146-1.439-1.154-1.79-4.354-1.79L12 24 8.619 5.034c-3.18 0-4.188.354-4.335 1.792 0 0-2.146-.795-3.229-2.43C5.28 2.431 9.525 2.34 9.525 2.34L12 5.362l-.004.002H12v-.002zm0-3.899c3.415-.03 7.326.528 11.328 2.28.535-.968.672-1.395.672-1.395C19.625.612 15.528.015 12 0 8.472.015 4.375.61 0 2.349c0 0 .195.525.672 1.396C4.674 1.989 8.585 1.435 12 1.46v.003z',
|
||||
salesforce:
|
||||
'M10.006 5.415a4.195 4.195 0 013.045-1.306c1.56 0 2.954.9 3.69 2.205.63-.3 1.35-.45 2.1-.45 2.85 0 5.159 2.34 5.159 5.22s-2.31 5.22-5.176 5.22c-.345 0-.69-.044-1.02-.104a3.75 3.75 0 01-3.3 1.95c-.6 0-1.155-.15-1.65-.375A4.314 4.314 0 018.88 20.4a4.302 4.302 0 01-4.05-2.82c-.27.062-.54.076-.825.076-2.204 0-4.005-1.8-4.005-4.05 0-1.5.811-2.805 2.01-3.51-.255-.57-.39-1.2-.39-1.846 0-2.58 2.1-4.65 4.65-4.65 1.53 0 2.85.705 3.72 1.8',
|
||||
github:
|
||||
'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12',
|
||||
} as const
|
||||
|
||||
const iconPath = iconMap[iconSlug as keyof typeof iconMap]
|
||||
|
||||
if (!iconPath) {
|
||||
return <div className='bg-muted animate-pulse rounded-sm' style={{ width: size, height: size }} />
|
||||
}
|
||||
|
||||
return (
|
||||
<svg role='img' viewBox='0 0 24 24' className='fill-black dark:fill-white' style={{ width: size, height: size }}>
|
||||
<path d={iconPath} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Tech companies data
|
||||
const techCompanies = [
|
||||
{ name: 'Shopify', id: 'shopify' },
|
||||
{ name: 'Netflix', id: 'netflix' },
|
||||
{ name: 'Spotify', id: 'spotify' },
|
||||
{ name: 'Airbnb', id: 'airbnb' },
|
||||
{ name: 'Dropbox', id: 'dropbox' },
|
||||
{ name: 'Stripe', id: 'stripe' },
|
||||
{ name: 'Google', id: 'google' },
|
||||
{ name: 'Apple', id: 'apple' },
|
||||
{ name: 'Meta', id: 'meta' },
|
||||
{ name: 'Tesla', id: 'tesla' },
|
||||
{ name: 'Salesforce', id: 'salesforce' },
|
||||
{ name: 'GitHub', id: 'github' },
|
||||
] as const
|
||||
|
||||
export function LogoCarousel() {
|
||||
return (
|
||||
<section className="pb-12 sm:pb-16 lg:pb-20 pt-12">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-medium text-muted-foreground mb-8">
|
||||
Trusted by leading companies worldwide
|
||||
</p>
|
||||
|
||||
{/* Logo Carousel with Fade Effect */}
|
||||
<div className="relative">
|
||||
{/* Left Fade */}
|
||||
<div className="absolute left-0 top-0 bottom-0 w-20 bg-gradient-to-r from-background to-transparent z-10 pointer-events-none" />
|
||||
|
||||
{/* Right Fade */}
|
||||
<div className="absolute right-0 top-0 bottom-0 w-20 bg-gradient-to-l from-background to-transparent z-10 pointer-events-none" />
|
||||
|
||||
{/* Logo Container */}
|
||||
<div className="overflow-hidden">
|
||||
<div className="flex animate-logo-scroll space-x-8 sm:space-x-12">
|
||||
{/* First set of logos */}
|
||||
{techCompanies.map((company, index) => (
|
||||
<Card
|
||||
key={`first-${index}`}
|
||||
className="flex-shrink-0 flex items-center justify-center h-16 w-40 opacity-60 hover:opacity-100 transition-opacity duration-300 border-0 shadow-none bg-transparent"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<SimpleIcon iconSlug={company.id} size={28} />
|
||||
<span className="text-foreground text-lg font-semibold whitespace-nowrap">
|
||||
{company.name}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
{/* Second set for seamless loop - identical to first */}
|
||||
{techCompanies.map((company, index) => (
|
||||
<Card
|
||||
key={`second-${index}`}
|
||||
className="flex-shrink-0 flex items-center justify-center h-16 w-40 opacity-60 hover:opacity-100 transition-opacity duration-300 border-0 shadow-none bg-transparent"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<SimpleIcon iconSlug={company.id} size={28} />
|
||||
<span className="text-foreground text-lg font-semibold whitespace-nowrap">
|
||||
{company.name}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Menu, Github, LayoutDashboard, ChevronDown, X, Moon, Sun } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
} from '@/components/ui/navigation-menu'
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetTrigger,
|
||||
SheetHeader,
|
||||
SheetTitle
|
||||
} from '@/components/ui/sheet'
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible'
|
||||
import { Logo } from '@/components/logo'
|
||||
import { MegaMenu } from '@/components/landing/mega-menu'
|
||||
import { ModeToggle } from '@/components/mode-toggle'
|
||||
import { useTheme } from '@/hooks/use-theme'
|
||||
|
||||
const navigationItems = [
|
||||
{ name: 'Home', href: '#hero' },
|
||||
{ name: 'Features', href: '#features' },
|
||||
{ name: 'Solutions', href: '#features', hasMegaMenu: true },
|
||||
{ name: 'Team', href: '#team' },
|
||||
{ name: 'Pricing', href: '#pricing' },
|
||||
{ name: 'FAQ', href: '#faq' },
|
||||
{ name: 'Contact', href: '#contact' },
|
||||
]
|
||||
|
||||
// Solutions menu items for mobile
|
||||
const solutionsItems = [
|
||||
{ title: 'Browse Products' },
|
||||
{ name: 'Free Blocks', href: '#free-blocks' },
|
||||
{ name: 'Premium Templates', href: '#premium-templates' },
|
||||
{ name: 'Admin Dashboards', href: '#admin-dashboards' },
|
||||
{ name: 'Landing Pages', href: '#landing-pages' },
|
||||
{ title: 'Categories' },
|
||||
{ name: 'E-commerce', href: '#ecommerce' },
|
||||
{ name: 'SaaS Dashboards', href: '#saas-dashboards' },
|
||||
{ name: 'Analytics', href: '#analytics' },
|
||||
{ name: 'Authentication', href: '#authentication' },
|
||||
{ title: 'Resources' },
|
||||
{ name: 'Documentation', href: '#docs' },
|
||||
{ name: 'Component Showcase', href: '#showcase' },
|
||||
{ name: 'GitHub Repository', href: '#github' },
|
||||
{ name: 'Design System', href: '#design-system' }
|
||||
]
|
||||
|
||||
// Smooth scroll function
|
||||
const smoothScrollTo = (targetId: string) => {
|
||||
if (targetId.startsWith('#')) {
|
||||
const element = document.querySelector(targetId)
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function LandingNavbar() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [solutionsOpen, setSolutionsOpen] = useState(false)
|
||||
const { setTheme, theme } = useTheme()
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/80 backdrop-blur-xl supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 flex h-16 items-center justify-between">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href="https://shadcnstore.com" className="flex items-center space-x-2 cursor-pointer" target='_blank' rel="noopener noreferrer">
|
||||
<Logo size={32} />
|
||||
<span className="font-bold">
|
||||
ShadcnStore
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<NavigationMenu className="hidden xl:flex">
|
||||
<NavigationMenuList>
|
||||
{navigationItems.map((item) => (
|
||||
<NavigationMenuItem key={item.name}>
|
||||
{item.hasMegaMenu ? (
|
||||
<>
|
||||
<NavigationMenuTrigger className="bg-transparent hover:bg-transparent focus:bg-transparent data-[active]:bg-transparent data-[state=open]:bg-transparent px-4 py-2 text-sm font-medium transition-colors hover:text-primary focus:text-primary cursor-pointer">
|
||||
{item.name}
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<MegaMenu />
|
||||
</NavigationMenuContent>
|
||||
</>
|
||||
) : (
|
||||
<NavigationMenuLink
|
||||
className="group inline-flex h-10 w-max items-center justify-center px-4 py-2 text-sm font-medium transition-colors hover:text-primary focus:text-primary focus:outline-none cursor-pointer"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
if (item.href.startsWith('#')) {
|
||||
smoothScrollTo(item.href)
|
||||
} else {
|
||||
window.location.href = item.href
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</NavigationMenuLink>
|
||||
)}
|
||||
</NavigationMenuItem>
|
||||
))}
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
|
||||
{/* Desktop CTA */}
|
||||
<div className="hidden xl:flex items-center space-x-2">
|
||||
<ModeToggle variant="ghost" />
|
||||
<Button variant="ghost" size="icon" asChild className="cursor-pointer">
|
||||
<a href="https://github.com/silicondeck/shadcn-dashboard-landing-template" target="_blank" rel="noopener noreferrer" aria-label="GitHub Repository">
|
||||
<Github className="h-5 w-5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="outline" asChild className="cursor-pointer">
|
||||
<Link href="/dashboard" target="_blank" rel="noopener noreferrer">
|
||||
<LayoutDashboard className="h-4 w-4 mr-2" />
|
||||
Dashboard
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" asChild className="cursor-pointer">
|
||||
<Link href="/auth/sign-in">Sign In</Link>
|
||||
</Button>
|
||||
<Button asChild className="cursor-pointer">
|
||||
<Link href="/auth/sign-up">Get Started</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
||||
<SheetTrigger asChild className="xl:hidden">
|
||||
<Button variant="ghost" size="icon" className="cursor-pointer">
|
||||
<Menu className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="right" className="w-full sm:w-[400px] p-0 gap-0 [&>button]:hidden overflow-hidden flex flex-col">
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<SheetHeader className="space-y-0 p-4 pb-2 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<Logo size={16} />
|
||||
</div>
|
||||
<SheetTitle className="text-lg font-semibold">ShadcnStore</SheetTitle>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
|
||||
className="cursor-pointer h-8 w-8"
|
||||
>
|
||||
<Moon className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Sun className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" asChild className="cursor-pointer h-8 w-8">
|
||||
<a href="https://github.com/silicondeck/shadcn-dashboard-landing-template" target="_blank" rel="noopener noreferrer" aria-label="GitHub Repository">
|
||||
<Github className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={() => setIsOpen(false)} className="cursor-pointer h-8 w-8">
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<nav className="p-6 space-y-1">
|
||||
{navigationItems.map((item) => (
|
||||
<div key={item.name}>
|
||||
{item.hasMegaMenu ? (
|
||||
<Collapsible open={solutionsOpen} onOpenChange={setSolutionsOpen}>
|
||||
<CollapsibleTrigger className="flex items-center justify-between w-full px-4 py-3 text-base font-medium rounded-lg transition-colors hover:bg-accent hover:text-accent-foreground cursor-pointer">
|
||||
{item.name}
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${solutionsOpen ? 'rotate-180' : ''}`} />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pl-4 space-y-1">
|
||||
{solutionsItems.map((solution, index) => (
|
||||
solution.title ? (
|
||||
<div
|
||||
key={`title-${index}`}
|
||||
className="px-4 mt-5 py-2 text-xs font-semibold text-muted-foreground/50 uppercase tracking-wider"
|
||||
>
|
||||
{solution.title}
|
||||
</div>
|
||||
) : (
|
||||
<a
|
||||
key={solution.name}
|
||||
href={solution.href}
|
||||
className="flex items-center px-4 py-2 text-sm rounded-lg transition-colors hover:bg-accent hover:text-accent-foreground cursor-pointer"
|
||||
onClick={(e) => {
|
||||
setIsOpen(false)
|
||||
if (solution.href?.startsWith('#')) {
|
||||
e.preventDefault()
|
||||
setTimeout(() => smoothScrollTo(solution.href), 100)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{solution.name}
|
||||
</a>
|
||||
)
|
||||
))}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
) : (
|
||||
<a
|
||||
href={item.href}
|
||||
className="flex items-center px-4 py-3 text-base font-medium rounded-lg transition-colors hover:bg-accent hover:text-accent-foreground cursor-pointer"
|
||||
onClick={(e) => {
|
||||
setIsOpen(false)
|
||||
if (item.href.startsWith('#')) {
|
||||
e.preventDefault()
|
||||
setTimeout(() => smoothScrollTo(item.href), 100)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="border-t p-6 space-y-4">
|
||||
|
||||
{/* Primary Actions */}
|
||||
<div className="space-y-3">
|
||||
<Button variant="outline" size="lg" asChild className="w-full cursor-pointer">
|
||||
<Link href="/dashboard">
|
||||
<LayoutDashboard className="size-4" />
|
||||
Dashboard
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Button variant="outline" size="lg" asChild className="cursor-pointer">
|
||||
<Link href="/auth/sign-in">Sign In</Link>
|
||||
</Button>
|
||||
<Button asChild size="lg" className="cursor-pointer" >
|
||||
<Link href="/auth/sign-up">Get Started</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Package,
|
||||
Download,
|
||||
Users,
|
||||
Star
|
||||
} from 'lucide-react'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { DotPattern } from '@/components/dot-pattern'
|
||||
|
||||
|
||||
const stats = [
|
||||
{
|
||||
icon: Package,
|
||||
value: '500+',
|
||||
label: 'Components',
|
||||
description: 'Ready-to-use blocks'
|
||||
},
|
||||
{
|
||||
icon: Download,
|
||||
value: '25K+',
|
||||
label: 'Downloads',
|
||||
description: 'Trusted worldwide'
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
value: '10K+',
|
||||
label: 'Developers',
|
||||
description: 'Active community'
|
||||
},
|
||||
{
|
||||
icon: Star,
|
||||
value: '4.9',
|
||||
label: 'Rating',
|
||||
description: 'User satisfaction'
|
||||
}
|
||||
]
|
||||
|
||||
export function StatsSection() {
|
||||
return (
|
||||
<section className="py-12 sm:py-16 relative">
|
||||
{/* Background with transparency */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-primary/8 via-transparent to-secondary/20" />
|
||||
<DotPattern className="opacity-75" size="md" fadeStyle="circle" />
|
||||
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative">
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8">
|
||||
{stats.map((stat, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className="text-center bg-background/60 backdrop-blur-sm border-border/50 py-0"
|
||||
>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="p-3 bg-primary/10 rounded-xl">
|
||||
<stat.icon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-2xl sm:text-3xl font-bold text-foreground">
|
||||
{stat.value}
|
||||
</h3>
|
||||
<p className="font-semibold text-foreground">{stat.label}</p>
|
||||
<p className="text-sm text-muted-foreground">{stat.description}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
"use client"
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { CardDecorator } from '@/components/ui/card-decorator'
|
||||
import { Github, Linkedin, Globe } from 'lucide-react'
|
||||
|
||||
|
||||
const team = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Alexandra Chen',
|
||||
role: 'Founder & CEO',
|
||||
description: 'Former co-founder of TechFlow. Early staff at Microsoft and Google.',
|
||||
image: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?q=60&w=150&auto=format&fit=crop',
|
||||
fallback: 'AC',
|
||||
social: {
|
||||
linkedin: '#',
|
||||
github: '#',
|
||||
website: '#'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Marcus Rodriguez',
|
||||
role: 'Engineering Manager',
|
||||
description: 'Lead engineering teams at Stripe, Discord, and Meta Labs.',
|
||||
image: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=60&w=150&auto=format&fit=crop',
|
||||
fallback: 'MR',
|
||||
social: {
|
||||
linkedin: '#',
|
||||
github: '#',
|
||||
website: '#'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Sophie Laurent',
|
||||
role: 'Product Manager',
|
||||
description: 'Former PM for Linear, Lambda School, and On Deck.',
|
||||
image: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?q=60&w=150&auto=format&fit=crop',
|
||||
fallback: 'SL',
|
||||
social: {
|
||||
linkedin: '#',
|
||||
github: '#',
|
||||
website: '#'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'David Kim',
|
||||
role: 'Frontend Developer',
|
||||
description: 'Former frontend dev for Linear, Coinbase, and PostScript.',
|
||||
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=60&w=150&auto=format&fit=crop',
|
||||
fallback: 'DK',
|
||||
social: {
|
||||
linkedin: '#',
|
||||
github: '#',
|
||||
website: '#'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Emma Thompson',
|
||||
role: 'Backend Developer',
|
||||
description: 'Lead backend dev at Clearbit. Former Clearbit and Loom.',
|
||||
image: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?q=60&w=150&auto=format&fit=crop',
|
||||
fallback: 'ET',
|
||||
social: {
|
||||
linkedin: '#',
|
||||
github: '#',
|
||||
website: '#'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Ryan Mitchell',
|
||||
role: 'Product Designer',
|
||||
description: 'Founding design team at Figma. Former Pleo, Stripe, and Tile.',
|
||||
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?q=60&w=150&auto=format&fit=crop',
|
||||
fallback: 'RM',
|
||||
social: {
|
||||
linkedin: '#',
|
||||
github: '#',
|
||||
website: '#'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'James Anderson',
|
||||
role: 'UX Researcher',
|
||||
description: 'Lead user research for Slack. Contractor for Netflix and Udacity.',
|
||||
image: 'https://images.unsplash.com/photo-1566492031773-4f4e44671d66?q=60&w=150&auto=format&fit=crop',
|
||||
fallback: 'JA',
|
||||
social: {
|
||||
linkedin: '#',
|
||||
github: '#',
|
||||
website: '#'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Isabella Garcia',
|
||||
role: 'Customer Success',
|
||||
description: 'Lead CX at Wealthsimple. Former PagerDuty and Squreen.',
|
||||
image: 'https://images.unsplash.com/photo-1580489944761-15a19d654956?q=60&w=150&auto=format&fit=crop',
|
||||
fallback: 'IG',
|
||||
social: {
|
||||
linkedin: '#',
|
||||
github: '#',
|
||||
website: '#'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export function TeamSection() {
|
||||
return (
|
||||
<section id="team" className="py-24 sm:py-32">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Section Header */}
|
||||
<div className="mx-auto max-w-4xl text-center mb-16">
|
||||
<Badge variant="outline" className="mb-4">
|
||||
Our Team
|
||||
</Badge>
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-6">
|
||||
Meet our team
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground mb-8">
|
||||
We are a passionate team of innovators, builders, and problem-solvers dedicated to creating exceptional digital experiences that make a difference.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Team Grid */}
|
||||
<div className="grid grid-cols-1 gap-x-8 gap-y-12 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{team.map((member) => (
|
||||
<Card key={member.id} className="shadow-xs py-2">
|
||||
<CardContent className="p-4">
|
||||
<div className="text-center">
|
||||
{/* Avatar */}
|
||||
<div className="flex justify-center mb-4">
|
||||
<CardDecorator>
|
||||
<Avatar className="h-24 w-24 border shadow-lg">
|
||||
<AvatarImage
|
||||
src={member.image}
|
||||
alt={member.name}
|
||||
className="object-cover"
|
||||
/>
|
||||
<AvatarFallback className="text-lg font-semibold">
|
||||
{member.fallback}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</CardDecorator>
|
||||
</div>
|
||||
|
||||
{/* Name and Role */}
|
||||
<h3 className="text-lg font-semibold text-foreground mb-1">
|
||||
{member.name}
|
||||
</h3>
|
||||
<p className="text-sm font-medium text-primary mb-3">
|
||||
{member.role}
|
||||
</p>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-sm text-muted-foreground mb-4 leading-relaxed">
|
||||
{member.description}
|
||||
</p>
|
||||
|
||||
{/* Social Links */}
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 cursor-pointer hover:text-primary"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={member.social.linkedin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`${member.name} LinkedIn`}
|
||||
>
|
||||
<Linkedin className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 cursor-pointer hover:text-primary"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={member.social.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`${member.name} GitHub`}
|
||||
>
|
||||
<Github className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 cursor-pointer hover:text-primary"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={member.social.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`${member.name} Website`}
|
||||
>
|
||||
<Globe className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
type Testimonial = {
|
||||
name: string
|
||||
role: string
|
||||
image: string
|
||||
quote: string
|
||||
}
|
||||
|
||||
const testimonials: Testimonial[] = [
|
||||
{
|
||||
name: 'Alexandra Mitchell',
|
||||
role: 'Senior Frontend Developer',
|
||||
image: 'https://notion-avatars.netlify.app/api/avatar?preset=female-1',
|
||||
quote:
|
||||
'This platform has completely transformed our development workflow. The component system is so well-architected that even complex applications feel simple to build.',
|
||||
},
|
||||
{
|
||||
name: 'James Thompson',
|
||||
role: 'Technical Lead',
|
||||
image: 'https://notion-avatars.netlify.app/api/avatar?preset=male-1',
|
||||
quote: 'After trying countless frameworks, this is the one that finally clicked. The documentation is exceptional.',
|
||||
},
|
||||
{
|
||||
name: 'Priya Sharma',
|
||||
role: 'Product Designer',
|
||||
image: 'https://notion-avatars.netlify.app/api/avatar?preset=female-2',
|
||||
quote:
|
||||
'The design system is beautiful and consistent. I can prototype ideas quickly and hand them off to developers with confidence that the implementation will match perfectly.',
|
||||
},
|
||||
{
|
||||
name: 'Robert Kim',
|
||||
role: 'Engineering Manager',
|
||||
image: 'https://notion-avatars.netlify.app/api/avatar?preset=male-2',
|
||||
quote:
|
||||
'We migrated our entire application to this platform in just two weeks. The performance improvements were immediate.',
|
||||
},
|
||||
{
|
||||
name: 'Maria Santos',
|
||||
role: 'Full Stack Engineer',
|
||||
image: 'https://notion-avatars.netlify.app/api/avatar?preset=female-3',
|
||||
quote:
|
||||
'The accessibility features are top-notch. Building inclusive applications has never been easier. Every component follows best practices out of the box, and the automated testing suite ensures we maintain high accessibility standards throughout our development process.',
|
||||
},
|
||||
{
|
||||
name: 'Thomas Anderson',
|
||||
role: 'Solutions Architect',
|
||||
image: 'https://notion-avatars.netlify.app/api/avatar?preset=male-3',
|
||||
quote: 'Scalability was our biggest concern, but this platform handles enterprise-level complexity with ease.',
|
||||
},
|
||||
{
|
||||
name: 'Lisa Chang',
|
||||
role: 'UX Researcher',
|
||||
image: 'https://notion-avatars.netlify.app/api/avatar?preset=female-4',
|
||||
quote:
|
||||
'User testing results have been consistently positive since we adopted this platform. The user experience is intuitive and the performance is stellar. Our user satisfaction scores have increased by 40% since the migration.',
|
||||
},
|
||||
{
|
||||
name: 'Michael Foster',
|
||||
role: 'DevOps Engineer',
|
||||
image: 'https://notion-avatars.netlify.app/api/avatar?preset=male-4',
|
||||
quote: 'Deployment and maintenance are a breeze. The platform integrates seamlessly with our CI/CD pipeline.',
|
||||
},
|
||||
{
|
||||
name: 'Sophie Laurent',
|
||||
role: 'Creative Director',
|
||||
image: 'https://notion-avatars.netlify.app/api/avatar?preset=female-5',
|
||||
quote:
|
||||
'The creative possibilities are endless. We can bring any design concept to life without compromising on technical quality or user experience.',
|
||||
},
|
||||
{
|
||||
name: 'Daniel Wilson',
|
||||
role: 'Backend Developer',
|
||||
image: 'https://notion-avatars.netlify.app/api/avatar?preset=male-5',
|
||||
quote: 'The API design is exceptional. Clean, intuitive, and well-documented.',
|
||||
},
|
||||
{
|
||||
name: 'Natasha Petrov',
|
||||
role: 'Mobile App Developer',
|
||||
image: 'https://notion-avatars.netlify.app/api/avatar?preset=female-6',
|
||||
quote:
|
||||
'Cross-platform development has never been this efficient. One codebase, multiple platforms, consistent user experience. This is the future. The responsive design system ensures our apps look perfect on every device.',
|
||||
},
|
||||
{
|
||||
name: 'Carlos Rivera',
|
||||
role: 'Startup Founder',
|
||||
image: 'https://notion-avatars.netlify.app/api/avatar?preset=male-6',
|
||||
quote: 'As a non-technical founder, this platform gave me the confidence to build our MVP quickly.',
|
||||
},
|
||||
]
|
||||
|
||||
export function TestimonialsSection() {
|
||||
return (
|
||||
<section id="testimonials" className="py-24 sm:py-32">
|
||||
<div className="container mx-auto px-8 sm:px-6">
|
||||
{/* Section Header */}
|
||||
<div className="mx-auto max-w-2xl text-center mb-16">
|
||||
<Badge variant="outline" className="mb-4">Testimonials</Badge>
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
|
||||
Empowering Innovation Worldwide
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Join thousands of developers and teams who trust our platform to build exceptional digital experiences.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Testimonials Masonry Grid */}
|
||||
<div className="columns-1 gap-4 md:columns-2 md:gap-6 lg:columns-3 lg:gap-4">
|
||||
{testimonials.map((testimonial, index) => (
|
||||
<Card key={index} className="mb-6 break-inside-avoid shadow-none lg:mb-4">
|
||||
<CardContent>
|
||||
<div className="flex items-start gap-4">
|
||||
<Avatar className="bg-muted size-12 shrink-0">
|
||||
<AvatarImage
|
||||
alt={testimonial.name}
|
||||
src={testimonial.image}
|
||||
loading="lazy"
|
||||
width="120"
|
||||
height="120"
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{testimonial.name
|
||||
.split(' ')
|
||||
.map(n => n[0])
|
||||
.join('')}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<a href="#" onClick={e => e.preventDefault()} className="cursor-pointer">
|
||||
<h3 className="font-medium hover:text-primary transition-colors">{testimonial.name}</h3>
|
||||
</a>
|
||||
<span className="text-muted-foreground block text-sm tracking-wide">
|
||||
{testimonial.role}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<blockquote className="mt-4">
|
||||
<p className="text-sm leading-relaxed text-balance">{testimonial.quote}</p>
|
||||
</blockquote>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
|
||||
import React from 'react'
|
||||
import { LandingNavbar } from './components/navbar'
|
||||
import { HeroSection } from './components/hero-section'
|
||||
import { LogoCarousel } from './components/logo-carousel'
|
||||
import { StatsSection } from './components/stats-section'
|
||||
import { FeaturesSection } from './components/features-section'
|
||||
import { TeamSection } from './components/team-section'
|
||||
import { TestimonialsSection } from './components/testimonials-section'
|
||||
import { BlogSection } from './components/blog-section'
|
||||
import { CTASection } from './components/cta-section'
|
||||
import { ContactSection } from './components/contact-section'
|
||||
import { FaqSection } from './components/faq-section'
|
||||
import { LandingFooter } from './components/footer'
|
||||
import { LandingThemeCustomizer, LandingThemeCustomizerTrigger } from './components/landing-theme-customizer'
|
||||
import { AboutSection } from './components/about-section'
|
||||
|
||||
export function LandingPageContent() {
|
||||
const [themeCustomizerOpen, setThemeCustomizerOpen] = React.useState(false)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Navigation */}
|
||||
<LandingNavbar />
|
||||
|
||||
{/* Main Content */}
|
||||
<main>
|
||||
<HeroSection />
|
||||
<LogoCarousel />
|
||||
<StatsSection />
|
||||
<AboutSection />
|
||||
<FeaturesSection />
|
||||
<TeamSection />
|
||||
<TestimonialsSection />
|
||||
<BlogSection />
|
||||
<FaqSection />
|
||||
<CTASection />
|
||||
<ContactSection />
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<LandingFooter />
|
||||
|
||||
{/* Theme Customizer */}
|
||||
<LandingThemeCustomizerTrigger onClick={() => setThemeCustomizerOpen(true)} />
|
||||
<LandingThemeCustomizer open={themeCustomizerOpen} onOpenChange={setThemeCustomizerOpen} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { LandingPageContent } from './landing-page-content'
|
||||
|
||||
// Metadata for the landing page
|
||||
export const metadata: Metadata = {
|
||||
title: 'ShadcnStore - Modern Admin Dashboard Template',
|
||||
description: 'A beautiful and comprehensive admin dashboard template built with React, Next.js, TypeScript, and shadcn/ui. Perfect for building modern web applications.',
|
||||
keywords: ['admin dashboard', 'react', 'nextjs', 'typescript', 'shadcn/ui', 'tailwind css'],
|
||||
openGraph: {
|
||||
title: 'ShadcnStore - Modern Admin Dashboard Template',
|
||||
description: 'A beautiful and comprehensive admin dashboard template built with React, Next.js, TypeScript, and shadcn/ui.',
|
||||
type: 'website',
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'ShadcnStore - Modern Admin Dashboard Template',
|
||||
description: 'A beautiful and comprehensive admin dashboard template built with React, Next.js, TypeScript, and shadcn/ui.',
|
||||
},
|
||||
}
|
||||
|
||||
export default function LandingPage() {
|
||||
return <LandingPageContent />
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { SidebarConfigProvider } from "@/contexts/sidebar-context";
|
||||
import { inter } from "@/lib/fonts";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "DLS — Kovak Yazılım",
|
||||
description: "İşletme yönetim yazılımı — Kovak Yazılım ve Medya Ltd. Şti.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="tr" className={`${inter.variable} antialiased`}>
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider defaultTheme="system" storageKey="lab-ui-theme">
|
||||
<SidebarConfigProvider>{children}</SidebarConfigProvider>
|
||||
</ThemeProvider>
|
||||
<Toaster richColors closeButton />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
|
||||
<p className="text-muted-foreground mt-2">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold">404</h1>
|
||||
<p className="text-muted-foreground mt-2">Page not found</p>
|
||||
<Button asChild className="mt-4">
|
||||
<Link href="/dashboard">Go to Dashboard</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState, useState } from "react";
|
||||
import { Building2, Loader2, ShieldCheck, ArrowRight, FlaskConical, Stethoscope } 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 { Logo } from "@/components/logo";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { createWorkspaceAction, importWorkspaceAction } from "@/lib/appwrite/tenant-actions";
|
||||
import { initialWorkspaceState } from "@/lib/appwrite/tenant-types";
|
||||
|
||||
type Kind = "lab" | "clinic";
|
||||
|
||||
interface Props {
|
||||
userName?: string;
|
||||
crossAppTeams?: Array<{ $id: string; name: string }>;
|
||||
}
|
||||
|
||||
export function CreateWorkspaceForm({ userName, crossAppTeams = [] }: Props) {
|
||||
const [kind, setKind] = useState<Kind | null>(null);
|
||||
const [state, formAction, isPending] = useActionState(
|
||||
createWorkspaceAction,
|
||||
initialWorkspaceState,
|
||||
);
|
||||
const [, importFormAction, isImporting] = useActionState(
|
||||
importWorkspaceAction,
|
||||
initialWorkspaceState,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center justify-center gap-2 font-medium">
|
||||
<div className="bg-primary text-primary-foreground flex size-9 items-center justify-center rounded-md">
|
||||
<Logo size={22} />
|
||||
</div>
|
||||
<span className="text-xl font-semibold">DLS</span>
|
||||
</div>
|
||||
|
||||
{crossAppTeams.length > 0 && (
|
||||
<Card className="border-primary/20 bg-primary/5">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base">Mevcut çalışma alanınızı ekleyin</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Diğer bir Kovak Yazılım uygulamasında kayıtlı çalışma alanınızı buraya bağlayın.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
{crossAppTeams.map((team) => (
|
||||
<form key={team.$id} action={importFormAction}>
|
||||
<input type="hidden" name="teamId" value={team.$id} />
|
||||
<input type="hidden" name="kind" value={kind ?? ""} />
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isImporting || !kind}
|
||||
className="flex w-full items-center justify-between rounded-lg border bg-background px-4 py-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Building2 className="size-4 text-muted-foreground" />
|
||||
<span>{team.name}</span>
|
||||
</div>
|
||||
{isImporting ? (
|
||||
<Loader2 className="size-4 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<ArrowRight className="size-4 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{crossAppTeams.length > 0 && (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-muted px-2 text-muted-foreground">veya yeni oluştur</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<div className="bg-primary/10 text-primary mx-auto mb-2 flex size-12 items-center justify-center rounded-full">
|
||||
<Building2 className="size-6" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl">
|
||||
{userName ? `Hoş geldiniz, ${userName}` : "Çalışma alanı oluştur"}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Hesap türünüzü seçin ve şirket bilgilerini girin — birkaç saniyede hazır.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form action={formAction} className="flex flex-col gap-5">
|
||||
<div className="grid gap-3">
|
||||
<Label>Hesap türü *</Label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<KindCard
|
||||
selected={kind === "clinic"}
|
||||
onClick={() => setKind("clinic")}
|
||||
icon={<Stethoscope className="size-5" />}
|
||||
title="Klinik"
|
||||
description="Diş kliniği — lab'a iş gönderir."
|
||||
/>
|
||||
<KindCard
|
||||
selected={kind === "lab"}
|
||||
onClick={() => setKind("lab")}
|
||||
icon={<FlaskConical className="size-5" />}
|
||||
title="Laboratuvar"
|
||||
description="Diş laboratuvarı — iş alır, protez üretir."
|
||||
/>
|
||||
</div>
|
||||
<input type="hidden" name="kind" value={kind ?? ""} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="companyName">Şirket adı *</Label>
|
||||
<Input
|
||||
id="companyName"
|
||||
name="companyName"
|
||||
type="text"
|
||||
placeholder={kind === "lab" ? "Örn. Beyaz Diş Laboratuvarı" : "Örn. Atlas Diş Polikliniği"}
|
||||
autoComplete="organization"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="companyTaxId">
|
||||
Vergi numarası <span className="text-muted-foreground text-xs">(opsiyonel)</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="companyTaxId"
|
||||
name="companyTaxId"
|
||||
type="text"
|
||||
placeholder="1234567890"
|
||||
inputMode="numeric"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="companyPhone">
|
||||
Telefon <span className="text-muted-foreground text-xs">(opsiyonel)</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="companyPhone"
|
||||
name="companyPhone"
|
||||
type="tel"
|
||||
placeholder="+90 555 123 45 67"
|
||||
autoComplete="tel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{state.error && (
|
||||
<p className="text-destructive text-sm text-center" role="alert">
|
||||
{state.error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isPending || !kind}>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Hazırlanıyor...
|
||||
</>
|
||||
) : (
|
||||
"Çalışma alanını oluştur"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<p className="text-muted-foreground flex items-center justify-center gap-1.5 text-xs">
|
||||
<ShieldCheck className="size-3.5" />
|
||||
Verileriniz yalnızca size özel — multi-tenant izolasyon
|
||||
</p>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<p className="text-muted-foreground text-center text-xs">
|
||||
Şirket bilgilerini daha sonra Ayarlar üzerinden düzenleyebilirsiniz.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function KindCard({
|
||||
selected,
|
||||
onClick,
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
selected: boolean;
|
||||
onClick: () => void;
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-pressed={selected}
|
||||
className={cn(
|
||||
"flex flex-col items-start gap-1.5 rounded-lg border bg-background p-3 text-left transition-colors",
|
||||
selected
|
||||
? "border-primary bg-primary/5 ring-1 ring-primary/40"
|
||||
: "hover:bg-accent hover:text-accent-foreground",
|
||||
)}
|
||||
>
|
||||
<span className={cn("flex size-8 items-center justify-center rounded-md", selected ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground")}>{icon}</span>
|
||||
<span className="text-sm font-medium">{title}</span>
|
||||
<span className="text-muted-foreground text-xs">{description}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Metadata } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getCurrentUser } from "@/lib/appwrite/server";
|
||||
import { getUserTeams, getCrossAppTeams } from "@/lib/appwrite/tenant";
|
||||
import { CreateWorkspaceForm } from "./components/create-workspace-form";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "DLS — Çalışma alanı oluştur",
|
||||
description: "DLS için ilk çalışma alanınızı kurun.",
|
||||
};
|
||||
|
||||
export default async function OnboardingPage() {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) redirect("/sign-in");
|
||||
|
||||
const teams = await getUserTeams();
|
||||
if (teams && teams.total > 0) redirect("/dashboard");
|
||||
|
||||
const crossAppTeams = await getCrossAppTeams();
|
||||
|
||||
return (
|
||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-md">
|
||||
<CreateWorkspaceForm
|
||||
userName={user.name?.split(" ")[0]}
|
||||
crossAppTeams={crossAppTeams}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function HomePage() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
router.replace("/dashboard");
|
||||
}, [router]);
|
||||
|
||||
// Show a loading state while redirecting
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
|
||||
<p className="text-muted-foreground mt-2">Redirecting to dashboard...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user