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
+88 -24
View File
@@ -1,7 +1,8 @@
"use client";
import { useState } from "react";
import { MoreHorizontal, Plus, Pencil, Trash2, CheckCircle } from "lucide-react";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { DotsThree, Plus, PencilSimple, Trash, CheckCircle, List, CalendarDots } from '@/lib/icons';
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
@@ -17,9 +18,13 @@ import {
deleteActivityAction,
} from "@/lib/appwrite/activity-actions";
import { ActivityFormSheet } from "./activity-form-sheet";
import { ActivityCalendar } from "./activity-calendar";
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
import type { Activity, Customer, Property } from "@/lib/appwrite/schema";
import { ACTIVITY_TYPE_LABELS } from "@/lib/appwrite/schema";
type ViewMode = "list" | "calendar";
interface ActivitiesClientProps {
initialActivities: Activity[];
customers: Customer[];
@@ -31,9 +36,12 @@ export function ActivitiesClient({
customers,
properties,
}: ActivitiesClientProps) {
const router = useRouter();
const [activities, setActivities] = useState(initialActivities);
const [sheetOpen, setSheetOpen] = useState(false);
const [editing, setEditing] = useState<Activity | null>(null);
const [deleteTarget, setDeleteTarget] = useState<Activity | null>(null);
const [viewMode, setViewMode] = useState<ViewMode>("list");
function customerName(id?: string | null) {
if (!id) return "—";
@@ -45,15 +53,19 @@ export function ActivitiesClient({
return properties.find((p) => p.$id === id)?.title ?? "—";
}
function openCreate() {
setEditing(null);
setSheetOpen(true);
}
useEffect(() => {
const open = () => { setEditing(null); setSheetOpen(true); };
const close = () => setSheetOpen(false);
window.addEventListener("kovak:open-form-activities", open);
window.addEventListener("kovak:close-form-activities", close);
return () => {
window.removeEventListener("kovak:open-form-activities", open);
window.removeEventListener("kovak:close-form-activities", close);
};
}, []);
function openEdit(a: Activity) {
setEditing(a);
setSheetOpen(true);
}
function openCreate() { setEditing(null); setSheetOpen(true); }
function openEdit(a: Activity) { setEditing(a); setSheetOpen(true); }
async function handleComplete(a: Activity) {
const result = await completeActivityAction(a.$id);
@@ -67,11 +79,12 @@ export function ActivitiesClient({
}
}
async function handleDelete(a: Activity) {
if (!confirm("Bu aktivite silinsin mi?")) return;
const result = await deleteActivityAction(a.$id);
async function doDelete() {
if (!deleteTarget) return;
const result = await deleteActivityAction(deleteTarget.$id);
if (result.ok) {
setActivities((prev) => prev.filter((x) => x.$id !== a.$id));
setActivities((prev) => prev.filter((x) => x.$id !== deleteTarget.$id));
setDeleteTarget(null);
toast.success("Aktivite silindi.");
} else {
toast.error(result.error ?? "Silinemedi.");
@@ -80,15 +93,57 @@ export function ActivitiesClient({
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<div className="flex items-center justify-between gap-3 flex-wrap">
<h1 className="text-2xl font-bold">Aktiviteler</h1>
<Button onClick={openCreate} size="sm">
<Plus className="mr-1.5 size-4" />
Yeni Aktivite
</Button>
<div className="flex items-center gap-2">
{/* View toggle */}
<div className="flex rounded-md border overflow-hidden">
<button
type="button"
onClick={() => setViewMode("list")}
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors ${
viewMode === "list"
? "bg-primary text-primary-foreground"
: "bg-background text-muted-foreground hover:text-foreground"
}`}
>
<List className="size-3.5" />
Liste
</button>
<button
type="button"
onClick={() => setViewMode("calendar")}
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors ${
viewMode === "calendar"
? "bg-primary text-primary-foreground"
: "bg-background text-muted-foreground hover:text-foreground"
}`}
>
<CalendarDots className="size-3.5" />
Takvim
</button>
</div>
<Button onClick={openCreate} size="sm" data-tour="activities-add">
<Plus className="mr-1.5 size-4" />
Yeni Aktivite
</Button>
</div>
</div>
<div className="rounded-md border">
{/* Calendar view */}
{viewMode === "calendar" && (
<ActivityCalendar
activities={activities}
customers={customers}
properties={properties}
onEdit={openEdit}
onComplete={handleComplete}
/>
)}
{/* List view */}
{viewMode === "list" && (
<div data-tour="activities-table" className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
@@ -133,7 +188,7 @@ export function ActivitiesClient({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="size-8">
<MoreHorizontal className="size-4" />
<DotsThree className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
@@ -144,14 +199,14 @@ export function ActivitiesClient({
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => openEdit(a)}>
<Pencil className="mr-2 size-4" />
<PencilSimple className="mr-2 size-4" />
Düzenle
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDelete(a)}
onClick={() => setDeleteTarget(a)}
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 size-4" />
<Trash className="mr-2 size-4" />
Sil
</DropdownMenuItem>
</DropdownMenuContent>
@@ -162,6 +217,7 @@ export function ActivitiesClient({
</TableBody>
</Table>
</div>
)}
<ActivityFormSheet
open={sheetOpen}
@@ -169,6 +225,14 @@ export function ActivitiesClient({
activity={editing}
customers={customers}
properties={properties}
onSuccess={() => router.refresh()}
/>
<DeleteConfirmDialog
open={!!deleteTarget}
onOpenChange={(v) => { if (!v) setDeleteTarget(null); }}
title="Bu aktivite silinsin mi?"
description="Bu aktivite kalıcı olarak silinecek ve geri alınamaz."
onConfirm={doDelete}
/>
</div>
);