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:
kovakmedya
2026-05-21 18:28:38 +03:00
commit cb150f7a24
215 changed files with 54262 additions and 0 deletions
@@ -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&apos;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>
)
}
+5
View File
@@ -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&apos;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&apos;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>
)
}
+5
View File
@@ -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&apos;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&apos;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>
)
}
+34
View File
@@ -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>
);
}
+20
View File
@@ -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>
);
}
+10
View File
@@ -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>
);
}
+58
View File
@@ -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&apos;t have an account?{" "}
<a href="/auth/sign-up-2" className="underline underline-offset-4">
Sign up
</a>
</div>
</form>
)
}
+34
View File
@@ -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&apos;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>
)
}
+11
View File
@@ -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>
&apos;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>
);
}
+27
View File
@@ -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>
)
}
+34
View File
@@ -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>
)
}
+9
View File
@@ -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>
&apos;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>
);
}
+22
View File
@@ -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>
);
}
+42
View File
@@ -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>
);
}
+104
View File
@@ -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>
);
}
+56
View File
@@ -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 ö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>
);
}
+21
View File
@@ -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>
);
}
+21
View File
@@ -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 listesi, filtreleme ve detay görünümü sonraki sürümde eklenecek.</CardDescription>
</CardHeader>
<CardContent />
</Card>
</div>
);
}
+34
View File
@@ -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 listesi sonraki sürümde eklenecek.</CardDescription>
</CardHeader>
<CardContent />
</Card>
</div>
);
}
+36
View File
@@ -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>
);
}
+32
View File
@@ -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&apos;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>
)
}
+361
View File
@@ -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 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>
);
}
+50
View File
@@ -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>
)}
</>
);
}
+141
View File
@@ -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

+172
View File
@@ -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&apos;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&apos;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>
)
}
+107
View File
@@ -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&apos;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&apos;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>
)
}
+234
View File
@@ -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>
)
}
+110
View File
@@ -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>
)
}
+274
View File
@@ -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>
)
}
+226
View File
@@ -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>
)
}
+50
View File
@@ -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>
)
}
+23
View File
@@ -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 />
}
+29
View File
@@ -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>
);
}
+10
View File
@@ -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>
)
}
+16
View File
@@ -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>
);
}
+32
View File
@@ -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>
);
}
+22
View File
@@ -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>
);
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+126
View File
@@ -0,0 +1,126 @@
"use client";
import * as React from "react";
import {
Inbox,
LayoutDashboard,
Link2,
Package,
Send,
Settings,
Wallet,
} from "lucide-react";
import Link from "next/link";
import { Logo } from "@/components/logo";
import { NavMain } from "@/components/nav-main";
import { NavUser } from "@/components/nav-user";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import type { ShellCompany, ShellUser } from "@/app/(dashboard)/dashboard-shell";
type NavItem = {
title: string;
url: string;
icon?: typeof LayoutDashboard;
};
type NavGroup = {
label: string;
items: NavItem[];
};
function buildNavGroups(kind: ShellCompany["kind"]): NavGroup[] {
const isLab = kind === "lab";
const operationsItems: NavItem[] = [
{ title: "Gelen İşler", url: "/jobs/inbound", icon: Inbox },
{ title: "Giden İşler", url: "/jobs/outbound", icon: Send },
];
if (isLab) {
operationsItems.push({ title: "Ürünler", url: "/products", icon: Package });
}
return [
{
label: "Genel",
items: [{ title: "Anasayfa", url: "/dashboard", icon: LayoutDashboard }],
},
{
label: "Operasyon",
items: operationsItems,
},
{
label: "Finans",
items: [{ title: "Finans", url: "/finance", icon: Wallet }],
},
{
label: "Hesap",
items: [
{ title: "Bağlantı Kur", url: "/connections", icon: Link2 },
{ title: "Ayarlar", url: "/settings/workspace", icon: Settings },
],
},
];
}
export function AppSidebar({
user,
company,
...props
}: React.ComponentProps<typeof Sidebar> & {
user: ShellUser;
company: ShellCompany;
}) {
const navGroups = buildNavGroups(company.kind);
return (
<Sidebar {...props}>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" asChild>
<Link href="/dashboard">
{company.logoUrl ? (
<div className="bg-background flex aspect-square size-8 items-center justify-center overflow-hidden rounded-lg border">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={company.logoUrl}
alt={`${company.name} logo`}
className="size-full object-contain"
/>
</div>
) : (
<div className="bg-primary text-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<Logo size={20} className="text-current" />
</div>
)}
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">DLS</span>
<span className="text-muted-foreground truncate text-xs">{company.name}</span>
</div>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
{navGroups.map((group) => (
<NavMain key={group.label} label={group.label} items={group.items} />
))}
</SidebarContent>
<SidebarFooter>
<NavUser user={user} />
</SidebarFooter>
</Sidebar>
);
}
+81
View File
@@ -0,0 +1,81 @@
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
interface ColorPickerProps {
label: string
cssVar: string
value: string
onChange: (cssVar: string, value: string) => void
}
export function ColorPicker({ label, cssVar, value, onChange }: ColorPickerProps) {
const [localValue, setLocalValue] = React.useState(value)
React.useEffect(() => {
setLocalValue(value)
}, [value])
const handleColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newColor = e.target.value
setLocalValue(newColor)
onChange(cssVar, newColor)
}
const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value
setLocalValue(newValue)
onChange(cssVar, newValue)
}
// Get current computed color for display
const displayColor = React.useMemo(() => {
if (localValue && localValue.startsWith('#')) {
return localValue
}
// Try to get computed value from CSS
const computed = getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim()
if (computed && computed.startsWith('#')) {
return computed
}
return '#000000'
}, [localValue, cssVar])
return (
<div className="space-y-2">
<Label htmlFor={`color-${cssVar}`} className="text-xs font-medium">
{label}
</Label>
<div className="flex items-start gap-2">
<div className="relative">
<Button
type="button"
variant="outline"
className="h-8 w-8 p-0 overflow-hidden cursor-pointer"
style={{ backgroundColor: displayColor }}
>
<input
type="color"
id={`color-${cssVar}`}
value={displayColor}
onChange={handleColorChange}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
</Button>
</div>
<Input
type="text"
placeholder={`${cssVar} value`}
value={localValue}
onChange={handleTextChange}
className="h-8 text-xs flex-1"
/>
</div>
</div>
)
}
+125
View File
@@ -0,0 +1,125 @@
"use client"
import { cn } from "@/lib/utils"
interface DotPatternProps {
className?: string
size?: "sm" | "md" | "lg"
opacity?: "low" | "medium" | "high"
fadeStyle?: "ellipse" | "circle" | "none"
}
export function DotPattern({
className,
size = "md",
opacity = "medium",
fadeStyle = "ellipse"
}: DotPatternProps) {
// Use predefined Tailwind classes instead of template literals for Next.js compatibility
const sizeMap = {
sm: "[background-size:12px_12px]",
md: "[background-size:16px_16px]",
lg: "[background-size:20px_20px]"
}
const opacityMap = {
low: "opacity-30",
medium: "opacity-50",
high: "opacity-70"
}
const fadeMap = {
ellipse: "[mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]",
circle: "[mask-image:radial-gradient(circle_at_50%_50%,#000_70%,transparent_100%)]",
none: ""
}
return (
<div
className={cn(
"absolute inset-0 bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] dark:bg-[radial-gradient(#374151_1px,transparent_1px)]",
sizeMap[size],
fadeMap[fadeStyle],
opacityMap[opacity],
className
)}
/>
)
}
// Alternative themed variants
export function DotPatternLight({
className,
size = "md",
opacity = "medium",
fadeStyle = "ellipse"
}: DotPatternProps) {
// Use predefined Tailwind classes instead of template literals for Next.js compatibility
const sizeMap = {
sm: "[background-size:12px_12px]",
md: "[background-size:16px_16px]",
lg: "[background-size:20px_20px]"
}
const opacityMap = {
low: "opacity-20",
medium: "opacity-40",
high: "opacity-60"
}
const fadeMap = {
ellipse: "[mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]",
circle: "[mask-image:radial-gradient(circle_at_50%_50%,#000_70%,transparent_100%)]",
none: ""
}
return (
<div
className={cn(
"absolute inset-0 bg-[radial-gradient(#d1d5db_1px,transparent_1px)]",
sizeMap[size],
fadeMap[fadeStyle],
opacityMap[opacity],
className
)}
/>
)
}
export function DotPatternDark({
className,
size = "md",
opacity = "medium",
fadeStyle = "ellipse"
}: DotPatternProps) {
// Use predefined Tailwind classes instead of template literals for Next.js compatibility
const sizeMap = {
sm: "[background-size:12px_12px]",
md: "[background-size:16px_16px]",
lg: "[background-size:20px_20px]"
}
const opacityMap = {
low: "opacity-30",
medium: "opacity-50",
high: "opacity-70"
}
const fadeMap = {
ellipse: "[mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]",
circle: "[mask-image:radial-gradient(circle_at_50%_50%,#000_70%,transparent_100%)]",
none: ""
}
return (
<div
className={cn(
"absolute inset-0 bg-[radial-gradient(#4b5563_1px,transparent_1px)]",
sizeMap[size],
fadeMap[fadeStyle],
opacityMap[opacity],
className
)}
/>
)
}
+13
View File
@@ -0,0 +1,13 @@
import dynamic from 'next/dynamic'
import React from 'react'
// Heavy components that should be dynamically imported
export const DynamicThemeCustomizer = dynamic(() => import('./theme-customizer').then(mod => ({ default: mod.ThemeCustomizer })), {
ssr: false,
loading: () => React.createElement('div', { className: "h-8 w-8 animate-pulse bg-muted rounded" })
})
export const DynamicColorPicker = dynamic(() => import('./color-picker').then(mod => ({ default: mod.ColorPicker })), {
ssr: false,
loading: () => React.createElement('div', { className: "h-8 w-8 animate-pulse bg-muted rounded" })
})
+87
View File
@@ -0,0 +1,87 @@
"use client"
import Image from 'next/image'
import { cn } from "@/lib/utils"
interface Image3DProps {
lightSrc: string
darkSrc: string
alt: string
className?: string
direction?: "left" | "right"
}
export function Image3D({
lightSrc,
darkSrc,
alt,
className,
direction = "left"
}: Image3DProps) {
const isRight = direction === "right"
return (
<div className={cn("group relative aspect-[4/3] w-full", className)}>
<div className="perspective-distant transform-3d">
{/* Animated background glow */}
<div className="absolute sm:-inset-8 rounded-3xl bg-gradient-to-r from-primary/10 via-blue-500/10 to-purple-500/10 opacity-0 group-hover:opacity-100 transition-all duration-1000 blur-2xl" />
{/* Main 3D container */}
<div className="relative size-full transform-3d group-hover:rotate-x-8 group-hover:rotate-y-12 group-hover:translate-z-16 transition-all duration-700 ease-out">
{/* Depth layers for 3D effect */}
<div className="absolute inset-0 translate-y-4 translate-x-2 -translate-z-8 rounded-2xl">
<div className="size-full rounded-2xl bg-gradient-to-br from-primary/10 via-background/40 to-secondary/10 shadow-xl" />
</div>
{/* Main image container */}
<div className="relative z-10 size-full rounded-2xl overflow-hidden shadow-2xl shadow-primary/20">
{/* Shimmer effect */}
<div className={cn(
"absolute inset-0 z-20 bg-gradient-to-r from-transparent via-white/20 to-transparent -skew-x-12 transition-transform duration-1000 ease-out pointer-events-none",
isRight
? "translate-x-full group-hover:-translate-x-full"
: "-translate-x-full group-hover:translate-x-full"
)} />
{/* Content fade mask */}
<div className={cn(
"absolute inset-0 z-15 pointer-events-none",
isRight
? "bg-linear-to-l from-background from-0% via-background/85 via-15% to-transparent to-40%"
: "bg-linear-to-r from-background from-0% via-background/85 via-15% to-transparent to-40%"
)} />
{/* Theme-aware images */}
<Image
src={lightSrc}
alt={`${alt} - Light Mode`}
width={800}
height={600}
className={cn(
"block size-full object-cover dark:hidden transition-transform duration-700 group-hover:scale-105",
isRight ? "object-center" : "object-left"
)}
loading="lazy"
/>
<Image
src={darkSrc}
alt={`${alt} - Dark Mode`}
width={800}
height={600}
className={cn(
"hidden dark:block size-full object-cover transition-transform duration-700 group-hover:scale-105",
isRight ? "object-center" : "object-left"
)}
loading="lazy"
/>
{/* Border highlight */}
<div className="absolute inset-0 rounded-2xl ring-1 ring-white/20 dark:ring-white/10 group-hover:ring-primary/40 transition-all duration-500" />
</div>
</div>
</div>
</div>
)
}
+143
View File
@@ -0,0 +1,143 @@
"use client"
import {
Shield,
BarChart3,
Database,
Building2,
Rocket,
Settings,
Zap,
Package,
Layout,
Crown,
Palette
} from 'lucide-react'
const menuSections = [
{
title: 'Browse Products',
items: [
{
title: 'Free Blocks',
description: 'Essential UI components and sections',
icon: Package,
href: '#free-blocks'
},
{
title: 'Premium Templates',
description: 'Complete page templates and layouts',
icon: Crown,
href: '#premium-templates'
},
{
title: 'Admin Dashboards',
description: 'Full-featured dashboard solutions',
icon: BarChart3,
href: '#admin-dashboards'
},
{
title: 'Landing Pages',
description: 'Marketing and product landing templates',
icon: Layout,
href: '#landing-pages'
}
]
},
{
title: 'Categories',
items: [
{
title: 'E-commerce',
description: 'Online store admin panels and components',
icon: Building2,
href: '#ecommerce'
},
{
title: 'SaaS Dashboards',
description: 'Application admin interfaces',
icon: Rocket,
href: '#saas-dashboards'
},
{
title: 'Analytics',
description: 'Data visualization and reporting templates',
icon: BarChart3,
href: '#analytics'
},
{
title: 'Authentication',
description: 'Login, signup, and user management pages',
icon: Shield,
href: '#authentication'
}
]
},
{
title: 'Resources',
items: [
{
title: 'Documentation',
description: 'Integration guides and setup instructions',
icon: Database,
href: '#docs'
},
{
title: 'Component Showcase',
description: 'Interactive preview of all components',
icon: Palette,
href: '#showcase'
},
{
title: 'GitHub Repository',
description: 'Open source foundation and community',
icon: Settings,
href: '#github'
},
{
title: 'Design System',
description: 'shadcn/ui standards and customization',
icon: Zap,
href: '#design-system'
}
]
}
]
export function MegaMenu() {
return (
<div className="w-[700px] max-w-[95vw] p-4 sm:p-6 lg:p-8 bg-background">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8 lg:gap-12">
{menuSections.map((section) => (
<div key={section.title} className="space-y-4 lg:space-y-6">
{/* Section Header */}
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
{section.title}
</h3>
{/* Section Links */}
<div className="space-y-3 lg:space-y-4">
{section.items.map((item) => (
<a
key={item.title}
href={item.href}
className="group block space-y-1 lg:space-y-2 hover:bg-accent rounded-md p-2 lg:p-3 -mx-2 lg:-mx-3 transition-colors my-0"
>
<div className="flex items-center gap-2 lg:gap-3">
<item.icon className="w-4 h-4 text-muted-foreground group-hover:text-primary transition-colors" />
<span className="text-sm font-medium text-foreground group-hover:text-primary transition-colors">
{item.title}
</span>
</div>
<p className="text-xs text-muted-foreground leading-relaxed ml-6 lg:ml-7">
{item.description}
</p>
</a>
))}
</div>
</div>
))}
</div>
</div>
)
}
+32
View File
@@ -0,0 +1,32 @@
import * as React from "react";
interface LogoProps extends React.SVGProps<SVGSVGElement> {
size?: number;
}
export function Logo({ size = 24, className, ...props }: LogoProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
{...props}
>
<text
x="16"
y="21"
textAnchor="middle"
fontFamily="system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif"
fontWeight="800"
fontSize="13"
letterSpacing="-0.5"
fill="currentColor"
>
DLS
</text>
</svg>
);
}
+70
View File
@@ -0,0 +1,70 @@
"use client"
import * as React from "react"
import { Moon, Sun } from "lucide-react"
import { Button } from "@/components/ui/button"
import { useTheme } from "@/hooks/use-theme"
import { useCircularTransition } from "@/hooks/use-circular-transition"
import "./theme-customizer/circular-transition.css"
interface ModeToggleProps {
variant?: "outline" | "ghost" | "default"
}
export function ModeToggle({ variant = "outline" }: ModeToggleProps) {
const { theme } = useTheme()
const { toggleTheme } = useCircularTransition()
// Simple, reliable dark mode detection with re-sync
const [isDarkMode, setIsDarkMode] = React.useState(false)
React.useEffect(() => {
const updateMode = () => {
if (theme === "dark") {
setIsDarkMode(true)
} else if (theme === "light") {
setIsDarkMode(false)
} else {
setIsDarkMode(typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches)
}
}
updateMode()
// Listen for system theme changes
const mediaQuery = typeof window !== "undefined" ? window.matchMedia("(prefers-color-scheme: dark)") : null
if (mediaQuery) {
mediaQuery.addEventListener("change", updateMode)
}
return () => {
if (mediaQuery) {
mediaQuery.removeEventListener("change", updateMode)
}
}
}, [theme])
const handleToggle = (event: React.MouseEvent<HTMLButtonElement>) => {
toggleTheme(event)
}
return (
<Button
variant={variant}
size="icon"
onClick={handleToggle}
className="cursor-pointer mode-toggle-button relative overflow-hidden"
>
{/* Show the icon for the mode you can switch TO */}
{isDarkMode ? (
<Sun className="h-[1.2rem] w-[1.2rem] transition-transform duration-300 rotate-0 scale-100" />
) : (
<Moon className="h-[1.2rem] w-[1.2rem] transition-transform duration-300 rotate-0 scale-100" />
)}
<span className="sr-only">
Switch to {isDarkMode ? "light" : "dark"} mode
</span>
</Button>
)
}
+101
View File
@@ -0,0 +1,101 @@
"use client"
import { ChevronRight, type LucideIcon } from "lucide-react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/components/ui/sidebar"
export function NavMain({
label,
items,
}: {
label: string
items: {
title: string
url: string
icon?: LucideIcon
isActive?: boolean
items?: {
title: string
url: string
isActive?: boolean
}[]
}[]
}) {
const pathname = usePathname()
// Check if any subitem is active to determine if parent should be open
const shouldBeOpen = (item: typeof items[0]) => {
if (item.isActive) return true
return item.items?.some(subItem => pathname === subItem.url) || false
}
return (
<SidebarGroup>
<SidebarGroupLabel>{label}</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={shouldBeOpen(item)}
className="group/collapsible"
>
<SidebarMenuItem>
{item.items?.length ? (
<>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title} className="cursor-pointer">
{item.icon && <item.icon />}
<span>{item.title}</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild className="cursor-pointer" isActive={pathname === subItem.url}>
<Link
href={subItem.url}
target={(item.title === "Auth Pages" || item.title === "Errors") ? "_blank" : undefined}
rel={(item.title === "Auth Pages" || item.title === "Errors") ? "noopener noreferrer" : undefined}
>
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
) : (
<SidebarMenuButton asChild tooltip={item.title} className="cursor-pointer" isActive={pathname === item.url}>
<Link href={item.url}>
{item.icon && <item.icon />}
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
)}
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
)
}
+41
View File
@@ -0,0 +1,41 @@
import * as React from "react"
import { type LucideIcon } from "lucide-react"
import Link from "next/link"
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
export function NavSecondary({
items,
...props
}: {
items: {
title: string
url: string
icon: LucideIcon
}[]
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
return (
<SidebarGroup {...props}>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild size="sm" className="cursor-pointer">
<Link href={item.url}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)
}
+120
View File
@@ -0,0 +1,120 @@
"use client";
import { useTransition } from "react";
import {
BellDot,
CircleUser,
CreditCard,
EllipsisVertical,
LogOut,
} from "lucide-react";
import Link from "next/link";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import { signOutAction } from "@/lib/appwrite/auth-actions";
function initials(name: string) {
const parts = name.trim().split(/\s+/).slice(0, 2);
return parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
}
export function NavUser({
user,
}: {
user: { name: string; email: string };
}) {
const { isMobile } = useSidebar();
const [isPending, startTransition] = useTransition();
const handleSignOut = () => {
startTransition(async () => {
await signOutAction();
});
};
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground cursor-pointer"
>
<div className="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg text-sm font-medium">
{initials(user.name)}
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="text-muted-foreground truncate text-xs">{user.email}</span>
</div>
<EllipsisVertical className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<div className="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg text-sm font-medium">
{initials(user.name)}
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="text-muted-foreground truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem asChild className="cursor-pointer">
<Link href="/settings/account">
<CircleUser />
Profil
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="cursor-pointer">
<Link href="/settings/billing">
<CreditCard />
Plan & Faturalama
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="cursor-pointer">
<Link href="/settings/notifications">
<BellDot />
Bildirimler
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleSignOut}
disabled={isPending}
className="cursor-pointer"
>
<LogOut />
{isPending ? "Çıkış yapılıyor..." : "Çıkış yap"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}
+58
View File
@@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import { X } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Logo } from "./logo"
export function SidebarNotification() {
const [isVisible, setIsVisible] = React.useState(true)
if (!isVisible) return null
return (
<Card className="mb-3 py-0 border-neutral-200 bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-800">
<CardContent className="p-4 relative">
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2 h-6 w-6 p-0 hover:bg-neutral-200 dark:hover:bg-neutral-700"
onClick={() => setIsVisible(false)}
>
<X className="h-3 w-3" />
<span className="sr-only">Close notification</span>
</Button>
<div className="pr-6">
<h3 className="flex items-center gap-3 font-semibold text-neutral-900 dark:text-neutral-100 mb-2 mt-1">
<Logo size={42} className="-mt-1" />
<div>
Welcome to{" "}
<a
href="https://shadcnstore.com"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
ShadcnStore
</a>
</div>
</h3>
<p className="text-sm text-muted-foreground dark:text-neutral-400 leading-relaxed">
Explore our premium Shadcn UI{" "}
<a
href="https://shadcnstore.com/blocks"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline"
>
blocks
</a>{" "}
to build your next project faster.
</p>
</div>
</CardContent>
</Card>
)
}
+34
View File
@@ -0,0 +1,34 @@
import Link from "next/link"
export function SiteFooter() {
const year = new Date().getFullYear()
return (
<footer className="bg-background border-t">
<div className="px-4 py-4 lg:px-6">
<div className="text-muted-foreground flex flex-col items-center justify-between gap-2 text-xs sm:flex-row">
<p>
© {year} DLS bir{" "}
<Link
href="https://kovakyazilim.com"
target="_blank"
rel="noopener noreferrer"
className="text-foreground hover:text-primary font-medium transition-colors"
>
Kovak Yazılım
</Link>{" "}
ürünüdür.
</p>
<div className="flex items-center gap-3">
<Link href="#" className="hover:text-foreground transition-colors">
Kullanım şartları
</Link>
<span aria-hidden>·</span>
<Link href="#" className="hover:text-foreground transition-colors">
Gizlilik
</Link>
</div>
</div>
</div>
</footer>
)
}
+47
View File
@@ -0,0 +1,47 @@
"use client";
import * as React from "react";
import { Building2, Plus } from "lucide-react";
import Link from "next/link";
import { ModeToggle } from "@/components/mode-toggle";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { SidebarTrigger } from "@/components/ui/sidebar";
import type { ShellCompany } from "@/app/(dashboard)/dashboard-shell";
export function SiteHeader({ company }: { company?: ShellCompany }) {
const showNewJobCta = company?.kind === "clinic";
return (
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
<div className="flex w-full items-center gap-1 px-4 py-3 lg:gap-2 lg:px-6">
<SidebarTrigger className="-ml-1" />
<Separator
orientation="vertical"
className="mx-2 data-[orientation=vertical]:h-4"
/>
{company && (
<div className="text-muted-foreground hidden items-center gap-1.5 text-sm md:flex">
<Building2 className="size-3.5" />
<span className="max-w-[260px] truncate">{company.name}</span>
</div>
)}
<div className="ml-auto flex items-center gap-2">
{showNewJobCta && (
<Button asChild size="sm">
<Link href="/jobs/new">
<Plus className="size-4" />
Yeni İş Yayınla
</Link>
</Button>
)}
<ModeToggle />
</div>
</div>
</header>
);
}
+3
View File
@@ -0,0 +1,3 @@
"use client"
// Re-export the main components from the modular structure
export { ThemeCustomizer, ThemeCustomizerTrigger } from './theme-customizer/main'
@@ -0,0 +1,115 @@
/* View Transition Circular Effect - Based on tweakcn implementation */
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-old(root) {
/* Ensure the outgoing view (old theme) is beneath */
z-index: 0;
}
::view-transition-new(root) {
/* Ensure the incoming view (new theme) is always on top */
z-index: 1;
}
@keyframes reveal {
from {
/* Use CSS variables for the origin, defaulting to center if not set */
clip-path: circle(0% at var(--x, 50%) var(--y, 50%));
opacity: 0.7;
}
to {
/* Use CSS variables for the origin, defaulting to center if not set */
clip-path: circle(150% at var(--x, 50%) var(--y, 50%));
opacity: 1;
}
}
::view-transition-new(root) {
/* Apply the reveal animation */
animation: reveal 0.4s ease-in-out forwards;
}
/* Fallback for reduced motion */
@media (prefers-reduced-motion: reduce) {
::view-transition-new(root) {
animation: none;
}
}
/* Button styling for mode toggles */
.mode-toggle-button {
position: relative;
overflow: hidden;
transition: all 0.2s ease-in-out;
}
.mode-toggle-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.mode-toggle-button svg {
transition: transform 0.3s ease-in-out;
}
/* Enhanced mode toggle button with ripple effect */
.mode-toggle-button {
position: relative;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.mode-toggle-button::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: currentColor;
opacity: 0.1;
transform: translate(-50%, -50%);
transition: all 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.mode-toggle-button.animating::before {
width: 200px;
height: 200px;
opacity: 0;
}
/* Improved focus and accessibility */
.mode-toggle-button:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
/* Support for reduced motion */
@media (prefers-reduced-motion: reduce) {
.theme-transition-overlay.active,
.theme-transition-overlay.reverse {
animation: none;
opacity: 1;
transform: scale(1);
transition: opacity 0.2s ease;
}
.mode-toggle-button::before {
transition: none;
}
}
/* Dark mode specific styling for the overlay */
.dark .theme-transition-overlay {
background: var(--background);
}
/* Light mode specific styling for the overlay */
.light .theme-transition-overlay {
background: var(--background);
}
@@ -0,0 +1,108 @@
"use client"
import React from 'react'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Textarea } from '@/components/ui/textarea'
import type { ImportedTheme } from '@/types/theme-customizer'
interface ImportModalProps {
open: boolean
onOpenChange: (open: boolean) => void
onImport: (theme: ImportedTheme) => void
}
export function ImportModal({ open, onOpenChange, onImport }: ImportModalProps) {
const [importText, setImportText] = React.useState("")
const processImport = () => {
try {
if (!importText.trim()) {
console.error("No CSS content provided")
return
}
// Parse CSS content into light and dark theme variables
const lightTheme: Record<string, string> = {}
const darkTheme: Record<string, string> = {}
// Split CSS into sections
const cssText = importText.replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments
// Extract :root section (light theme)
const rootMatch = cssText.match(/:root\s*\{([^}]+)\}/)
if (rootMatch) {
const rootContent = rootMatch[1]
const variableMatches = rootContent.matchAll(/--([^:]+):\s*([^;]+);/g)
for (const match of variableMatches) {
const [, variable, value] = match
lightTheme[variable.trim()] = value.trim()
}
}
// Extract .dark section (dark theme)
const darkMatch = cssText.match(/\.dark\s*\{([^}]+)\}/)
if (darkMatch) {
const darkContent = darkMatch[1]
const variableMatches = darkContent.matchAll(/--([^:]+):\s*([^;]+);/g)
for (const match of variableMatches) {
const [, variable, value] = match
darkTheme[variable.trim()] = value.trim()
}
}
// Store the imported theme
const importedThemeData = { light: lightTheme, dark: darkTheme }
onImport(importedThemeData)
onOpenChange(false)
setImportText("")
} catch (error) {
console.error("Error importing theme:", error)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange} modal={true}>
<DialogContent className="max-w-4xl w-[90vw]">
<DialogHeader>
<DialogTitle>Tema CSS'i içe aktar</DialogTitle>
<DialogDescription>
CSS temasını aşağıya yapıştırın. Açık tema için <code>:root</code> ve koyu tema için{" "}
<code>.dark</code> bölümleri olmalı; <code>--primary</code>, <code>--background</code> gibi CSS değişkenlerini içerir. Açık/koyu mod arasında otomatik geçiş yapılır.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Textarea
id="theme-css"
className="flex w-full rounded-md border border-input bg-transparent px-3 py-2 shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm max-h-[400px] min-h-[300px] font-mono text-sm text-foreground overflow-y-auto resize-none"
placeholder={`:root {
--background: 0 0% 100%;
--foreground: oklch(0.52 0.13 144.17);
--primary: #3e2723;
/* And more */
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: hsl(37.50 36.36% 95.69%);
--primary: rgb(46, 125, 50);
/* And more */
}`}
value={importText}
onChange={(e) => setImportText(e.target.value)}
/>
</div>
<div className="flex gap-2 justify-end">
<Button variant="outline" onClick={() => onOpenChange(false)} className="cursor-pointer">
Vazgeç
</Button>
<Button onClick={processImport} disabled={!importText.trim()} className="cursor-pointer">
İçe aktar
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
}
+227
View File
@@ -0,0 +1,227 @@
"use client"
import React from 'react'
import { Layout, Palette, RotateCcw, Settings, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { useThemeManager } from '@/hooks/use-theme-manager'
import { useTheme } from '@/hooks/use-theme'
import { useSidebarConfig } from '@/contexts/sidebar-context'
import { tweakcnThemes } from '@/config/theme-data'
import { toast } from 'sonner'
import { saveUserPrefsAction } from '@/lib/appwrite/user-prefs-actions'
import type { UserPrefs as ThemePrefs } from '@/lib/appwrite/user-prefs-actions'
import { getLocalThemePrefs, saveLocalThemePrefs } from '@/lib/local-theme-prefs'
import { ThemeTab } from './theme-tab'
import { LayoutTab } from './layout-tab'
import { ImportModal } from './import-modal'
import { cn } from '@/lib/utils'
import type { ImportedTheme } from '@/types/theme-customizer'
interface ThemeCustomizerProps {
open: boolean
onOpenChange: (open: boolean) => void
initialPrefs: ThemePrefs
}
export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCustomizerProps) {
const { applyImportedTheme, isDarkMode, resetTheme, applyRadius, setBrandColorsValues, applyTheme, applyTweakcnTheme } = useThemeManager()
const { theme } = useTheme()
const { config: sidebarConfig, updateConfig: updateSidebarConfig } = useSidebarConfig()
const [activeTab, setActiveTab] = React.useState("theme")
const [selectedTheme, setSelectedTheme] = React.useState(() => {
const local = getLocalThemePrefs()
return local.colorTheme ?? initialPrefs.colorTheme ?? "default"
})
const [selectedTweakcnTheme, setSelectedTweakcnTheme] = React.useState(() => {
const local = getLocalThemePrefs()
return local.tweakcnTheme ?? initialPrefs.tweakcnTheme ?? ""
})
const [selectedRadius, setSelectedRadius] = React.useState(() => {
const local = getLocalThemePrefs()
return local.radius ?? initialPrefs.radius ?? "0.5rem"
})
const [importModalOpen, setImportModalOpen] = React.useState(false)
const [importedTheme, setImportedTheme] = React.useState<ImportedTheme | null>(null)
// Sidebar config → sadece localStorage (Appwrite tetiklemesi döngüye yol açıyordu:
// PrefsInitializer mount'ta updateConfig çağırıyor → bu effect server action tetikliyor
// → Next.js router cache revalidation → remount → döngü)
const sidebarMountRef = React.useRef(false)
React.useEffect(() => {
if (!sidebarMountRef.current) { sidebarMountRef.current = true; return }
saveLocalThemePrefs({
sidebarVariant: sidebarConfig.variant,
sidebarCollapsible: sidebarConfig.collapsible,
sidebarSide: sidebarConfig.side,
})
}, [sidebarConfig.variant, sidebarConfig.collapsible, sidebarConfig.side])
const savePrefs = (update: Parameters<typeof saveUserPrefsAction>[0]) => {
saveUserPrefsAction(update).then((res) => {
if (!res.ok) toast.error("Tercihler kaydedilemedi: " + res.error)
})
}
const handleReset = () => {
setSelectedTheme("default")
setSelectedTweakcnTheme("")
setSelectedRadius("0.5rem")
setImportedTheme(null)
setBrandColorsValues({})
resetTheme()
applyRadius("0.5rem")
updateSidebarConfig({ variant: "inset", collapsible: "offcanvas", side: "left" })
saveLocalThemePrefs({ colorTheme: "default", tweakcnTheme: "", radius: "0.5rem", sidebarVariant: "inset", sidebarCollapsible: "offcanvas", sidebarSide: "left" })
savePrefs({ colorTheme: "default", tweakcnTheme: "", radius: "0.5rem", sidebarVariant: "inset", sidebarCollapsible: "offcanvas", sidebarSide: "left" })
}
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)
}
// 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={sidebarConfig.side === "left" ? "right" : "left"}
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">Görünüm</SheetTitle>
<div className="ml-auto flex items-center gap-2">
<Button
variant="outline"
size="icon"
onClick={handleReset}
className="cursor-pointer h-8 w-8"
aria-label="Varsayılana dön"
title="Varsayılana dön"
>
<RotateCcw className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
onClick={() => onOpenChange(false)}
className="cursor-pointer h-8 w-8"
aria-label="Kapat"
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
<SheetDescription className="text-sm text-muted-foreground sr-only">
Panelin renklerini ve düzenini özelleştirin.
</SheetDescription>
</SheetHeader>
<div className="flex-1 overflow-y-auto">
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex flex-col">
<div className="py-2">
<TabsList className="grid w-full grid-cols-2 rounded-none h-12 p-1.5">
<TabsTrigger value="theme" className="cursor-pointer data-[state=active]:bg-background">
<Palette className="h-4 w-4 mr-1" /> Renk
</TabsTrigger>
<TabsTrigger value="layout" className="cursor-pointer data-[state=active]:bg-background">
<Layout className="h-4 w-4 mr-1" /> Düzen
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="theme" className="flex-1 mt-0">
<ThemeTab
selectedTheme={selectedTheme}
setSelectedTheme={(value) => {
setSelectedTheme(value)
saveLocalThemePrefs({ colorTheme: value })
savePrefs({ colorTheme: value })
}}
selectedTweakcnTheme={selectedTweakcnTheme}
setSelectedTweakcnTheme={(value) => {
setSelectedTweakcnTheme(value)
saveLocalThemePrefs({ tweakcnTheme: value })
savePrefs({ tweakcnTheme: value })
}}
selectedRadius={selectedRadius}
setSelectedRadius={(value) => {
setSelectedRadius(value)
saveLocalThemePrefs({ radius: value })
savePrefs({ radius: value })
}}
setImportedTheme={setImportedTheme}
onImportClick={handleImportClick}
onThemeModeChange={(mode) => savePrefs({ theme: mode })}
/>
</TabsContent>
<TabsContent value="layout" className="flex-1 mt-0">
<LayoutTab />
</TabsContent>
</Tabs>
</div>
</SheetContent>
</Sheet>
<ImportModal
open={importModalOpen}
onOpenChange={setImportModalOpen}
onImport={handleImport}
/>
</>
)
}
// Floating trigger button - positioned dynamically based on sidebar side
export function ThemeCustomizerTrigger({ onClick }: { onClick: () => void }) {
const { config: sidebarConfig } = useSidebarConfig()
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",
sidebarConfig.side === "left" ? "right-4" : "left-4"
)}
>
<Settings className="h-5 w-5" />
</Button>
)
}

Some files were not shown because too many files have changed in this diff Show More