feat: desktop image thumbnails, gallery lightbox portal, client-side compression, clickable table rows, fix header gap

This commit is contained in:
egecankomur
2026-05-12 04:49:36 +03:00
parent 3cce632eb3
commit 3554b39800
134 changed files with 7736 additions and 1913 deletions
@@ -1,22 +1,16 @@
"use client";
import { useActionState, useEffect } from "react";
import { Loader2 } from "lucide-react";
import { CircleNotch } from '@/lib/icons';
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { ResponsiveSheet, FormWizard } from "@/components/ui/responsive-sheet";
import { createCustomerAction, updateCustomerAction } from "@/lib/appwrite/customer-actions";
import type { Customer } from "@/lib/appwrite/schema";
import { CUSTOMER_STAGE_LABELS, CUSTOMER_SOURCE_LABELS } from "@/lib/appwrite/schema";
type ActionState = { ok: boolean; error?: string; fieldErrors?: Record<string, string[]> };
const INITIAL: ActionState = { ok: false };
@@ -47,55 +41,103 @@ export function CustomerFormSheet({ open, onOpenChange, customer, onSuccess }: C
const fe = state.fieldErrors ?? {};
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="sm:max-w-md">
<SheetHeader>
<SheetTitle>{customer ? "Müşteriyi Düzenle" : "Yeni Müşteri"}</SheetTitle>
</SheetHeader>
<form action={formAction} className="mt-4 space-y-4 pb-6">
const steps = [
{
label: "Kimlik",
content: (
<>
<div className="grid gap-1.5">
<Label htmlFor="name">Ad Soyad *</Label>
<Input id="name" name="name" defaultValue={customer?.name} placeholder="Ahmet Yılmaz" />
{fe.name && <p className="text-destructive text-xs">{fe.name[0]}</p>}
</div>
<div className="grid gap-1.5">
<Label>Müşteri tipi *</Label>
<select name="type" defaultValue={customer?.type ?? "alici"}
className="border-input bg-background h-9 rounded-md border px-3 text-sm">
<option value="alici">Alıcı</option>
<option value="kiraci">Kiracı</option>
<option value="yatirimci">Yatırımcı</option>
</select>
{fe.type && <p className="text-destructive text-xs">{fe.type[0]}</p>}
<div className="grid grid-cols-2 gap-3">
<div className="grid gap-1.5">
<Label>Müşteri tipi *</Label>
<select name="type" defaultValue={customer?.type ?? "alici"}
className="border-input bg-background h-9 rounded-md border px-3 text-sm">
<option value="alici">Alıcı</option>
<option value="kiraci">Kiracı</option>
<option value="yatirimci">Yatırımcı</option>
</select>
{fe.type && <p className="text-destructive text-xs">{fe.type[0]}</p>}
</div>
<div className="grid gap-1.5">
<Label>Aşama</Label>
<select name="stage" defaultValue={customer?.stage ?? "ilk_temas"}
className="border-input bg-background h-9 rounded-md border px-3 text-sm">
{Object.entries(CUSTOMER_STAGE_LABELS).map(([v, l]) => (
<option key={v} value={v}>{l}</option>
))}
</select>
</div>
</div>
<div className="grid gap-1.5">
<Label htmlFor="phone">Telefon</Label>
<Input id="phone" name="phone" type="tel" defaultValue={customer?.phone ?? ""} placeholder="+90 555 123 45 67" />
</>
),
},
{
label: "İletişim",
content: (
<>
<div className="grid grid-cols-2 gap-3">
<div className="grid gap-1.5">
<Label htmlFor="phone">Telefon</Label>
<Input id="phone" name="phone" type="tel" defaultValue={customer?.phone ?? ""} placeholder="+90 555 123 45 67" />
</div>
<div className="grid gap-1.5">
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" type="email" defaultValue={customer?.email ?? ""} placeholder="ahmet@example.com" />
{fe.email && <p className="text-destructive text-xs">{fe.email[0]}</p>}
</div>
</div>
<div className="grid gap-1.5">
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" type="email" defaultValue={customer?.email ?? ""} placeholder="ahmet@example.com" />
{fe.email && <p className="text-destructive text-xs">{fe.email[0]}</p>}
<div className="grid grid-cols-2 gap-3">
<div className="grid gap-1.5">
<Label>Kaynak</Label>
<select name="source" defaultValue={customer?.source ?? ""}
className="border-input bg-background h-9 rounded-md border px-3 text-sm">
<option value="">Seçiniz</option>
{Object.entries(CUSTOMER_SOURCE_LABELS).map(([v, l]) => (
<option key={v} value={v}>{l}</option>
))}
</select>
</div>
<div className="grid gap-1.5">
<Label htmlFor="nextFollowUpDate">Takip tarihi</Label>
<Input id="nextFollowUpDate" name="nextFollowUpDate" type="date"
defaultValue={customer?.nextFollowUpDate ? customer.nextFollowUpDate.split("T")[0] : ""} />
</div>
</div>
</>
),
},
{
label: "Not",
content: (
<div className="grid gap-1.5">
<Label htmlFor="notes">Notlar</Label>
<Textarea id="notes" name="notes" rows={4} defaultValue={customer?.notes ?? ""} placeholder="Müşteri hakkında notlar..." />
</div>
),
},
];
<div className="grid gap-1.5">
<Label htmlFor="notes">Notlar</Label>
<Textarea id="notes" name="notes" rows={3} defaultValue={customer?.notes ?? ""} placeholder="Müşteri hakkında notlar..." />
</div>
<SheetFooter>
<Button type="submit" disabled={isPending} className="w-full">
{isPending && <Loader2 className="mr-2 size-4 animate-spin" />}
{customer ? "Güncelle" : "Oluştur"}
</Button>
</SheetFooter>
</form>
</SheetContent>
</Sheet>
return (
<ResponsiveSheet
open={open}
onOpenChange={onOpenChange}
title={customer ? "Müşteriyi Düzenle" : "Yeni Müşteri"}
maxWidth="sm:max-w-lg"
>
<form
action={formAction}
onKeyDown={(e) => {
if (e.key === "Enter" && (e.target as HTMLElement).tagName !== "TEXTAREA") {
e.preventDefault();
}
}}
>
<FormWizard steps={steps} isPending={isPending} submitLabel={customer ? "Güncelle" : "Oluştur"} />
</form>
</ResponsiveSheet>
);
}