feat(calendar): month-view calendar bound to calendar_events
Replaces the template's static-data calendar with a multi-tenant calendar backed by Appwrite calendar_events. Schema/validation: - lib/validation/calendar.ts (calendarEventSchema with cross-field check end >= start) - lib/appwrite/calendar-actions.ts: createCalendarEventAction, updateCalendarEventAction, deleteCalendarEventAction. Date inputs (HTML datetime-local 'YYYY-MM-DDTHH:mm', date 'YYYY-MM-DD') are normalized to ISO 8601 before write. - lib/appwrite/calendar-queries.ts: listCalendarEvents with optional start/end range queries. UI: - /calendar server page: pulls events + customers, hands to CalendarClient. - CalendarClient: month grid (6 rows × 7 cols), Monday-first, today badge, prev/next/Bugün nav. Multi-day events show on every day in their range. Each day cell shows up to 3 event chips with start time prefix; '+N daha' for overflow. Hover reveals a + button to add an event on that day. - EventFormSheet: title, all-day switch (toggles input type between date and datetime-local), start/end with validation, customer FK, color preset (blue/green/amber/red/violet/slate). Sentinel '__none__' for nullable Selects. When editing, footer shows a destructive 'Sil' ghost button on the left that triggers the parent's confirm dialog. Color tokens centralized in COLOR_BG map; falls back to primary tint. Removed all template calendar files (calendars.tsx, calendar-main, etc.) since the data model didn't match.
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState, useTransition } from "react";
|
||||
import { ChevronLeft, ChevronRight, Loader2, Plus, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { deleteCalendarEventAction } from "@/lib/appwrite/calendar-actions";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { EventFormSheet } from "./event-form-sheet";
|
||||
import { COLOR_BG, type Customer, type EventRow } from "./types";
|
||||
|
||||
type Props = {
|
||||
events: EventRow[];
|
||||
customers: Customer[];
|
||||
};
|
||||
|
||||
const WEEKDAYS = ["Pzt", "Sal", "Çar", "Per", "Cum", "Cmt", "Paz"];
|
||||
const MONTH_NAMES = [
|
||||
"Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran",
|
||||
"Temmuz", "Ağustos", "Eylül", "Ekim", "Kasım", "Aralık",
|
||||
];
|
||||
|
||||
function startOfMonthGrid(year: number, month: number): Date {
|
||||
// Monday-first grid; first cell is the Monday on/before the 1st
|
||||
const first = new Date(year, month, 1);
|
||||
const dayIdx = (first.getDay() + 6) % 7; // 0 = Mon
|
||||
return new Date(year, month, 1 - dayIdx);
|
||||
}
|
||||
|
||||
function ymd(d: Date): string {
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||
}
|
||||
|
||||
export function CalendarClient({ events, customers }: Props) {
|
||||
const today = new Date();
|
||||
const [cursor, setCursor] = useState(new Date(today.getFullYear(), today.getMonth(), 1));
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<EventRow | null>(null);
|
||||
const [defaultDate, setDefaultDate] = useState<string | undefined>();
|
||||
const [deleting, setDeleting] = useState<EventRow | null>(null);
|
||||
const [busy, startTransition] = useTransition();
|
||||
|
||||
const eventsByDay = useMemo(() => {
|
||||
const map = new Map<string, EventRow[]>();
|
||||
for (const e of events) {
|
||||
const start = new Date(e.start);
|
||||
const end = new Date(e.end);
|
||||
const cur = new Date(start.getFullYear(), start.getMonth(), start.getDate());
|
||||
const last = new Date(end.getFullYear(), end.getMonth(), end.getDate());
|
||||
while (cur.getTime() <= last.getTime()) {
|
||||
const key = ymd(cur);
|
||||
const arr = map.get(key) ?? [];
|
||||
arr.push(e);
|
||||
map.set(key, arr);
|
||||
cur.setDate(cur.getDate() + 1);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [events]);
|
||||
|
||||
const grid = useMemo(() => {
|
||||
const start = startOfMonthGrid(cursor.getFullYear(), cursor.getMonth());
|
||||
const days: Date[] = [];
|
||||
for (let i = 0; i < 42; i++) {
|
||||
const d = new Date(start);
|
||||
d.setDate(start.getDate() + i);
|
||||
days.push(d);
|
||||
}
|
||||
return days;
|
||||
}, [cursor]);
|
||||
|
||||
const handlePrev = () => setCursor(new Date(cursor.getFullYear(), cursor.getMonth() - 1, 1));
|
||||
const handleNext = () => setCursor(new Date(cursor.getFullYear(), cursor.getMonth() + 1, 1));
|
||||
const handleToday = () => setCursor(new Date(today.getFullYear(), today.getMonth(), 1));
|
||||
|
||||
const handleAddOnDay = (date: Date) => {
|
||||
setEditing(null);
|
||||
setDefaultDate(ymd(date));
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
const handleAddNew = () => {
|
||||
setEditing(null);
|
||||
setDefaultDate(ymd(today));
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (event: EventRow) => {
|
||||
setEditing(event);
|
||||
setDefaultDate(undefined);
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!deleting) return;
|
||||
startTransition(async () => {
|
||||
const fd = new FormData();
|
||||
fd.set("id", deleting.id);
|
||||
const result = await deleteCalendarEventAction(fd);
|
||||
if (result.ok) {
|
||||
toast.success("Etkinlik silindi.");
|
||||
setDeleting(null);
|
||||
} else {
|
||||
toast.error(result.error ?? "Silme başarısız.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const todayKey = ymd(today);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="mb-4 flex flex-col items-center justify-between gap-3 md:flex-row">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" className="size-8" onClick={handlePrev}>
|
||||
<ChevronLeft className="size-4" />
|
||||
</Button>
|
||||
<h2 className="text-lg font-semibold">
|
||||
{MONTH_NAMES[cursor.getMonth()]} {cursor.getFullYear()}
|
||||
</h2>
|
||||
<Button variant="outline" size="icon" className="size-8" onClick={handleNext}>
|
||||
<ChevronRight className="size-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={handleToday}>
|
||||
Bugün
|
||||
</Button>
|
||||
</div>
|
||||
<Button onClick={handleAddNew}>
|
||||
<Plus className="size-4" />
|
||||
Yeni etkinlik
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-7 gap-px overflow-hidden rounded-md border bg-border">
|
||||
{WEEKDAYS.map((wd) => (
|
||||
<div
|
||||
key={wd}
|
||||
className="bg-muted/40 text-muted-foreground py-2 text-center text-xs font-medium"
|
||||
>
|
||||
{wd}
|
||||
</div>
|
||||
))}
|
||||
{grid.map((d) => {
|
||||
const inMonth = d.getMonth() === cursor.getMonth();
|
||||
const key = ymd(d);
|
||||
const isToday = key === todayKey;
|
||||
const dayEvents = eventsByDay.get(key) ?? [];
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={cn(
|
||||
"bg-card group relative flex min-h-[110px] flex-col gap-1 p-1.5",
|
||||
!inMonth && "bg-muted/30",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex size-6 items-center justify-center rounded-full text-xs",
|
||||
isToday && "bg-primary text-primary-foreground font-medium",
|
||||
!inMonth && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{d.getDate()}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleAddOnDay(d)}
|
||||
className="text-muted-foreground hover:text-foreground opacity-0 group-hover:opacity-100"
|
||||
aria-label="Bu güne etkinlik ekle"
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{dayEvents.slice(0, 3).map((e) => (
|
||||
<button
|
||||
key={e.id}
|
||||
type="button"
|
||||
onClick={() => handleEdit(e)}
|
||||
className={cn(
|
||||
"truncate rounded border px-1.5 py-0.5 text-left text-xs",
|
||||
COLOR_BG[e.color] ?? COLOR_BG[""],
|
||||
)}
|
||||
title={e.title}
|
||||
>
|
||||
{!e.allDay && (
|
||||
<span className="opacity-70">
|
||||
{new Date(e.start).toLocaleTimeString("tr-TR", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}{" "}
|
||||
</span>
|
||||
)}
|
||||
{e.title}
|
||||
</button>
|
||||
))}
|
||||
{dayEvents.length > 3 && (
|
||||
<span className="text-muted-foreground px-1 text-xs">
|
||||
+{dayEvents.length - 3} daha
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<EventFormSheet
|
||||
open={formOpen}
|
||||
onOpenChange={(v) => {
|
||||
setFormOpen(v);
|
||||
if (!v) setEditing(null);
|
||||
}}
|
||||
event={editing}
|
||||
defaultDate={defaultDate}
|
||||
customers={customers}
|
||||
onRequestDelete={(e) => {
|
||||
setFormOpen(false);
|
||||
setDeleting(e);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Dialog open={Boolean(deleting)} onOpenChange={(v) => !v && setDeleting(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Etkinliği sil</DialogTitle>
|
||||
<DialogDescription>
|
||||
<strong>{deleting?.title}</strong> kalıcı olarak silinecek.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDeleting(null)} disabled={busy}>
|
||||
Vazgeç
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleDelete} disabled={busy}>
|
||||
{busy ? <Loader2 className="size-4 animate-spin" /> : <Trash2 className="size-4" />}
|
||||
Sil
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Calendar as CalendarIcon,
|
||||
Clock,
|
||||
MapPin,
|
||||
Users,
|
||||
MoreHorizontal,
|
||||
Search,
|
||||
Grid3X3,
|
||||
List,
|
||||
ChevronDown,
|
||||
Menu
|
||||
} from "lucide-react"
|
||||
import { format, addMonths, subMonths, startOfMonth, endOfMonth, eachDayOfInterval, isSameMonth, isToday, isSameDay } from "date-fns"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { type CalendarEvent } from "../types"
|
||||
|
||||
// Import data
|
||||
import eventsData from "../data/events.json"
|
||||
|
||||
interface CalendarMainProps {
|
||||
selectedDate?: Date
|
||||
onDateSelect?: (date: Date) => void
|
||||
onMenuClick?: () => void
|
||||
events?: CalendarEvent[]
|
||||
onEventClick?: (event: CalendarEvent) => void
|
||||
}
|
||||
|
||||
export function CalendarMain({ selectedDate, onDateSelect, onMenuClick, events, onEventClick }: CalendarMainProps) {
|
||||
// Convert JSON events to CalendarEvent objects with proper Date objects, fallback to imported data
|
||||
const sampleEvents: CalendarEvent[] = events || eventsData.map(event => ({
|
||||
...event,
|
||||
date: new Date(event.date),
|
||||
type: event.type as "meeting" | "event" | "personal" | "task" | "reminder"
|
||||
}))
|
||||
|
||||
const [currentDate, setCurrentDate] = useState(selectedDate || new Date())
|
||||
const [viewMode, setViewMode] = useState<"month" | "week" | "day" | "list">("month")
|
||||
const [showEventDialog, setShowEventDialog] = useState(false)
|
||||
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null)
|
||||
|
||||
const monthStart = startOfMonth(currentDate)
|
||||
const monthEnd = endOfMonth(currentDate)
|
||||
|
||||
// Extend to show full weeks (including previous/next month days)
|
||||
const calendarStart = new Date(monthStart)
|
||||
calendarStart.setDate(calendarStart.getDate() - monthStart.getDay())
|
||||
|
||||
const calendarEnd = new Date(monthEnd)
|
||||
calendarEnd.setDate(calendarEnd.getDate() + (6 - monthEnd.getDay()))
|
||||
|
||||
const calendarDays = eachDayOfInterval({ start: calendarStart, end: calendarEnd })
|
||||
|
||||
const getEventsForDay = (date: Date) => {
|
||||
return sampleEvents.filter(event => isSameDay(event.date, date))
|
||||
}
|
||||
|
||||
const navigateMonth = (direction: "prev" | "next") => {
|
||||
setCurrentDate(direction === "prev" ? subMonths(currentDate, 1) : addMonths(currentDate, 1))
|
||||
}
|
||||
|
||||
const goToToday = () => {
|
||||
setCurrentDate(new Date())
|
||||
}
|
||||
|
||||
const handleEventClick = (event: CalendarEvent) => {
|
||||
if (onEventClick) {
|
||||
onEventClick(event)
|
||||
} else {
|
||||
setSelectedEvent(event)
|
||||
setShowEventDialog(true)
|
||||
}
|
||||
}
|
||||
|
||||
const renderCalendarGrid = () => {
|
||||
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-background">
|
||||
{/* Calendar Header */}
|
||||
<div className="grid grid-cols-7 border-b">
|
||||
{weekDays.map(day => (
|
||||
<div key={day} className="p-4 text-center font-medium text-sm text-muted-foreground border-r last:border-r-0">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Calendar Body */}
|
||||
<div className="grid grid-cols-7 flex-1">
|
||||
{calendarDays.map(day => {
|
||||
const dayEvents = getEventsForDay(day)
|
||||
const isCurrentMonth = isSameMonth(day, currentDate)
|
||||
const isDayToday = isToday(day)
|
||||
const isSelected = selectedDate && isSameDay(day, selectedDate)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={day.toISOString()}
|
||||
className={cn(
|
||||
"min-h-[120px] border-r border-b last:border-r-0 p-2 cursor-pointer transition-colors",
|
||||
isCurrentMonth ? "bg-background hover:bg-accent/50" : "bg-muted/30 text-muted-foreground",
|
||||
isSelected && "ring-2 ring-primary ring-inset",
|
||||
isDayToday && "bg-accent/20"
|
||||
)}
|
||||
onClick={() => onDateSelect?.(day)}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className={cn(
|
||||
"text-sm font-medium",
|
||||
isDayToday && "bg-primary text-primary-foreground rounded-md w-6 h-6 flex items-center justify-center text-xs"
|
||||
)}>
|
||||
{format(day, 'd')}
|
||||
</span>
|
||||
{dayEvents.length > 2 && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
+{dayEvents.length - 2}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
{dayEvents.slice(0, 2).map(event => (
|
||||
<div
|
||||
key={event.id}
|
||||
className={cn(
|
||||
"text-xs p-1 rounded-sm text-white cursor-pointer truncate",
|
||||
event.color
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleEventClick(event)
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span className="truncate">{event.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderListView = () => {
|
||||
const upcomingEvents = sampleEvents
|
||||
.filter(event => event.date >= new Date())
|
||||
.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||
|
||||
return (
|
||||
<div className="flex-1 p-6">
|
||||
<div className="space-y-4">
|
||||
{upcomingEvents.map(event => (
|
||||
<Card key={event.id} className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => handleEventClick(event)}>
|
||||
<CardContent className="px-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={cn("w-3 h-3 rounded-full mt-1.5", event.color)} />
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium">{event.title}</h3>
|
||||
<div className="flex items-center gap-4 mt-2 text-sm text-muted-foreground">
|
||||
<div className="flex items-center flex-wrap gap-1">
|
||||
<CalendarIcon className="w-4 h-4" />
|
||||
{format(event.date, 'MMM d, yyyy')}
|
||||
</div>
|
||||
<div className="flex items-center flex-wrap gap-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
{event.time}
|
||||
</div>
|
||||
<div className="flex items-center flex-wrap gap-1">
|
||||
<MapPin className="w-4 h-4" />
|
||||
{event.location}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex -space-x-2">
|
||||
{event.attendees.slice(0, 3).map((attendee, index) => (
|
||||
<Avatar key={index} className="border-2 border-background">
|
||||
<AvatarFallback className="text-xs">{attendee}</AvatarFallback>
|
||||
</Avatar>
|
||||
))}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="cursor-pointer">
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col flex-wrap gap-4 p-6 border-b md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
{/* Mobile Menu Button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="xl:hidden cursor-pointer"
|
||||
onClick={onMenuClick}
|
||||
>
|
||||
<Menu className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => navigateMonth("prev")} className="cursor-pointer">
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => navigateMonth("next")} className="cursor-pointer">
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={goToToday} className="cursor-pointer">
|
||||
Today
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl font-semibold">
|
||||
{format(currentDate, 'MMMM yyyy')}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center">
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground" />
|
||||
<Input placeholder="Search events..." className="pl-10 w-64" />
|
||||
</div>
|
||||
|
||||
{/* View Mode Toggle */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="cursor-pointer">
|
||||
{viewMode === "month" && <Grid3X3 className="w-4 h-4 mr-2" />}
|
||||
{viewMode === "list" && <List className="w-4 h-4 mr-2" />}
|
||||
{viewMode.charAt(0).toUpperCase() + viewMode.slice(1)}
|
||||
<ChevronDown className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={() => setViewMode("month")} className="cursor-pointer">
|
||||
<Grid3X3 className="w-4 h-4 mr-2" />
|
||||
Month
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setViewMode("list")} className="cursor-pointer">
|
||||
<List className="w-4 h-4 mr-2" />
|
||||
List
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calendar Content */}
|
||||
{viewMode === "month" ? renderCalendarGrid() : renderListView()}
|
||||
|
||||
{/* Event Detail Dialog */}
|
||||
<Dialog open={showEventDialog} onOpenChange={setShowEventDialog}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedEvent?.title || "Event Details"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
View and manage this calendar event
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{selectedEvent && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarIcon className="w-4 h-4 text-muted-foreground" />
|
||||
<span>{format(selectedEvent.date, 'EEEE, MMMM d, yyyy')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4 text-muted-foreground" />
|
||||
<span>{selectedEvent.time} ({selectedEvent.duration})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4 text-muted-foreground" />
|
||||
<span>{selectedEvent.location}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-muted-foreground" />
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Attendees:</span>
|
||||
<div className="flex -space-x-2">
|
||||
{selectedEvent.attendees.map((attendee: string, index: number) => (
|
||||
<Avatar key={index} className="w-6 h-6 border-2 border-background">
|
||||
<AvatarFallback className="text-xs">{attendee}</AvatarFallback>
|
||||
</Avatar>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary" className={cn("text-white", selectedEvent.color)}>
|
||||
{selectedEvent.type}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-4">
|
||||
<Button variant="outline" className="flex-1 cursor-pointer" onClick={() => {
|
||||
setShowEventDialog(false)
|
||||
}}>Edit</Button>
|
||||
<Button variant="destructive" className="flex-1 cursor-pointer" onClick={() => {
|
||||
setShowEventDialog(false)
|
||||
}}>Delete</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { Plus } from "lucide-react"
|
||||
|
||||
import { Calendars } from "./calendars"
|
||||
import { DatePicker } from "./date-picker"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
interface CalendarSidebarProps {
|
||||
selectedDate?: Date
|
||||
onDateSelect?: (date: Date) => void
|
||||
onNewCalendar?: () => void
|
||||
onNewEvent?: () => void
|
||||
events?: Array<{ date: Date; count: number }>
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function CalendarSidebar({
|
||||
selectedDate,
|
||||
onDateSelect,
|
||||
onNewCalendar,
|
||||
onNewEvent,
|
||||
events = [],
|
||||
className
|
||||
}: CalendarSidebarProps) {
|
||||
return (
|
||||
<div className={`flex flex-col h-full bg-background rounded-lg ${className}`}>
|
||||
{/* Add New Event Button */}
|
||||
<div className="p-6 border-b">
|
||||
<Button
|
||||
className="w-full cursor-pointer"
|
||||
onClick={onNewEvent}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add New Event
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Date Picker */}
|
||||
<DatePicker
|
||||
selectedDate={selectedDate}
|
||||
onDateSelect={onDateSelect}
|
||||
events={events}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Calendars */}
|
||||
<div className="flex-1 p-4">
|
||||
<Calendars
|
||||
onNewCalendar={onNewCalendar}
|
||||
onCalendarToggle={(calendarId, visible) => {
|
||||
console.log(`Calendar ${calendarId} visibility: ${visible}`)
|
||||
}}
|
||||
onCalendarEdit={(calendarId) => {
|
||||
console.log(`Edit calendar: ${calendarId}`)
|
||||
}}
|
||||
onCalendarDelete={(calendarId) => {
|
||||
console.log(`Delete calendar: ${calendarId}`)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start cursor-pointer"
|
||||
onClick={onNewCalendar}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New Calendar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,381 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Calendar as CalendarIcon,
|
||||
Clock,
|
||||
MapPin,
|
||||
Users,
|
||||
Search,
|
||||
Grid3X3,
|
||||
List,
|
||||
ChevronDown,
|
||||
Menu,
|
||||
Plus
|
||||
} from "lucide-react"
|
||||
import { format, addMonths, subMonths, startOfMonth, endOfMonth, eachDayOfInterval, isSameMonth, isToday, isSameDay } from "date-fns"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { Calendar } from "@/components/ui/calendar"
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { type CalendarEvent } from "../types"
|
||||
|
||||
// Import data
|
||||
import eventsData from "../data/events.json"
|
||||
import calendarsData from "../data/calendars.json"
|
||||
|
||||
interface CalendarMainProps {
|
||||
eventDates?: Array<{ date: Date; count: number }>
|
||||
}
|
||||
|
||||
export function CalendarMain({ eventDates = [] }: CalendarMainProps) {
|
||||
const [selectedDate, setSelectedDate] = useState<Date>(new Date())
|
||||
const [currentDate, setCurrentDate] = useState(new Date())
|
||||
const [viewMode, setViewMode] = useState<"month" | "week" | "day" | "list">("month")
|
||||
const [showEventDialog, setShowEventDialog] = useState(false)
|
||||
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null)
|
||||
const [showCalendarSheet, setShowCalendarSheet] = useState(false)
|
||||
|
||||
// Convert JSON events to CalendarEvent objects with proper Date objects
|
||||
const sampleEvents: CalendarEvent[] = eventsData.map(event => ({
|
||||
...event,
|
||||
date: new Date(event.date),
|
||||
type: event.type as "meeting" | "event" | "personal" | "task" | "reminder"
|
||||
}))
|
||||
|
||||
const monthStart = startOfMonth(currentDate)
|
||||
const monthEnd = endOfMonth(currentDate)
|
||||
|
||||
// Extend to show full weeks (including previous/next month days)
|
||||
const calendarStart = new Date(monthStart)
|
||||
calendarStart.setDate(calendarStart.getDate() - monthStart.getDay())
|
||||
|
||||
const calendarEnd = new Date(monthEnd)
|
||||
calendarEnd.setDate(calendarEnd.getDate() + (6 - monthEnd.getDay()))
|
||||
|
||||
const calendarDays = eachDayOfInterval({ start: calendarStart, end: calendarEnd })
|
||||
|
||||
const getEventsForDay = (date: Date) => {
|
||||
return sampleEvents.filter(event => isSameDay(event.date, date))
|
||||
}
|
||||
|
||||
const navigateMonth = (direction: "prev" | "next") => {
|
||||
setCurrentDate(direction === "prev" ? subMonths(currentDate, 1) : addMonths(currentDate, 1))
|
||||
}
|
||||
|
||||
const goToToday = () => {
|
||||
setCurrentDate(new Date())
|
||||
}
|
||||
|
||||
const handleEventClick = (event: CalendarEvent) => {
|
||||
setSelectedEvent(event)
|
||||
setShowEventDialog(true)
|
||||
}
|
||||
|
||||
const handleDateSelect = (date: Date) => {
|
||||
setSelectedDate(date)
|
||||
}
|
||||
|
||||
const handleNewCalendar = () => {
|
||||
console.log("Creating new calendar")
|
||||
// In a real app, this would open a new calendar form
|
||||
}
|
||||
|
||||
const handleNewEvent = () => {
|
||||
console.log("Creating new event")
|
||||
// In a real app, this would open event form
|
||||
}
|
||||
|
||||
const renderCalendarGrid = () => {
|
||||
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-background">
|
||||
{/* Calendar Header */}
|
||||
<div className="grid grid-cols-7 border-b">
|
||||
{weekDays.map(day => (
|
||||
<div key={day} className="p-4 text-center font-medium text-sm text-muted-foreground border-r last:border-r-0">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Calendar Grid */}
|
||||
<div className="grid grid-cols-7 min-h-[600px]">
|
||||
{calendarDays.map((day) => {
|
||||
const dayEvents = getEventsForDay(day)
|
||||
const isCurrentMonth = isSameMonth(day, currentDate)
|
||||
const isDayToday = isToday(day)
|
||||
const isSelected = isSameDay(day, selectedDate)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={day.toISOString()}
|
||||
className={cn(
|
||||
"relative border-r border-b last:border-r-0 p-2 min-h-[120px] hover:bg-muted/50 cursor-pointer transition-colors",
|
||||
!isCurrentMonth && "text-muted-foreground bg-muted/20",
|
||||
isDayToday && "bg-blue-50 dark:bg-blue-900/20",
|
||||
isSelected && "bg-blue-100 dark:bg-blue-800/30"
|
||||
)}
|
||||
onClick={() => handleDateSelect(day)}
|
||||
>
|
||||
{/* Date Number */}
|
||||
<div className={cn(
|
||||
"text-sm font-medium mb-1",
|
||||
isDayToday && "text-blue-600 dark:text-blue-400"
|
||||
)}>
|
||||
{format(day, 'd')}
|
||||
</div>
|
||||
|
||||
{/* Events */}
|
||||
<div className="space-y-1">
|
||||
{dayEvents.slice(0, 3).map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className={cn(
|
||||
"text-xs px-2 py-1 rounded text-white cursor-pointer hover:opacity-80 transition-opacity truncate",
|
||||
event.color
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleEventClick(event)
|
||||
}}
|
||||
>
|
||||
{event.time} {event.title}
|
||||
</div>
|
||||
))}
|
||||
{dayEvents.length > 3 && (
|
||||
<div className="text-xs text-muted-foreground px-2">
|
||||
+{dayEvents.length - 3} more
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderSidebar = () => (
|
||||
<div className="w-full h-full bg-background border-r">
|
||||
<div className="p-4 border-b">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="font-semibold">Calendar</h2>
|
||||
<Button size="sm" onClick={handleNewEvent}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Event
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Date Picker */}
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={selectedDate}
|
||||
onSelect={(date) => date && handleDateSelect(date)}
|
||||
className="rounded-md border"
|
||||
modifiers={{
|
||||
eventDay: eventDates.map(ed => ed.date)
|
||||
}}
|
||||
modifiersStyles={{
|
||||
eventDay: { fontWeight: 'bold' }
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mini Calendars List */}
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-medium">My Calendars</h3>
|
||||
<Button variant="ghost" size="sm" onClick={handleNewCalendar}>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{calendarsData.map((calendar) => (
|
||||
<div key={calendar.id} className="flex items-center space-x-2">
|
||||
<div className={cn("w-3 h-3 rounded-full", calendar.color)} />
|
||||
<span className="text-sm">{calendar.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg bg-background relative">
|
||||
<div className="flex min-h-[800px]">
|
||||
{/* Desktop Sidebar */}
|
||||
<div className="hidden xl:block w-80 flex-shrink-0">
|
||||
{renderSidebar()}
|
||||
</div>
|
||||
|
||||
{/* Main Calendar Panel */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Calendar Toolbar */}
|
||||
<div className="border-b bg-background px-4 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Mobile Menu Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="xl:hidden"
|
||||
onClick={() => setShowCalendarSheet(true)}
|
||||
>
|
||||
<Menu className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{/* Month Navigation */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => navigateMonth("prev")}>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<h2 className="text-lg font-semibold min-w-[140px] text-center">
|
||||
{format(currentDate, 'MMMM yyyy')}
|
||||
</h2>
|
||||
<Button variant="ghost" size="sm" onClick={() => navigateMonth("next")}>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" size="sm" onClick={goToToday}>
|
||||
Today
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="hidden sm:flex items-center space-x-2">
|
||||
<Button variant="ghost" size="sm" className="text-xs">
|
||||
<Search className="h-4 w-4 mr-1" />
|
||||
Search
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* View Mode Toggle */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<Grid3X3 className="h-4 w-4 mr-1" />
|
||||
{viewMode === "month" ? "Month" : viewMode === "week" ? "Week" : viewMode === "day" ? "Day" : "List"}
|
||||
<ChevronDown className="h-4 w-4 ml-1" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setViewMode("month")}>
|
||||
<Grid3X3 className="h-4 w-4 mr-2" />
|
||||
Month
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setViewMode("week")}>
|
||||
<List className="h-4 w-4 mr-2" />
|
||||
Week
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setViewMode("day")}>
|
||||
<CalendarIcon className="h-4 w-4 mr-2" />
|
||||
Day
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setViewMode("list")}>
|
||||
<List className="h-4 w-4 mr-2" />
|
||||
List
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calendar Content */}
|
||||
{renderCalendarGrid()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile/Tablet Sheet */}
|
||||
<Sheet open={showCalendarSheet} onOpenChange={setShowCalendarSheet}>
|
||||
<SheetContent side="left" className="w-80 p-0">
|
||||
<SheetHeader className="p-4 pb-2">
|
||||
<SheetTitle>Calendar</SheetTitle>
|
||||
<SheetDescription>
|
||||
Browse dates and manage your calendar events
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
{renderSidebar()}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
{/* Event Details Dialog */}
|
||||
<Dialog open={showEventDialog} onOpenChange={setShowEventDialog}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedEvent?.title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Event details and information
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{selectedEvent && (
|
||||
<div className="space-y-4 pt-4">
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{selectedEvent.time} • {selectedEvent.duration}</span>
|
||||
</div>
|
||||
|
||||
{selectedEvent.location && (
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
<MapPin className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{selectedEvent.location}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedEvent.attendees.length > 0 && (
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex space-x-1">
|
||||
{selectedEvent.attendees.map((attendee, index) => (
|
||||
<Avatar key={index} className="h-6 w-6">
|
||||
<AvatarFallback className="text-xs">
|
||||
{attendee}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedEvent.description && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{selectedEvent.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center space-x-2 pt-4">
|
||||
<Badge variant="secondary" className={cn("text-white", selectedEvent.color)}>
|
||||
{selectedEvent.type}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { CalendarSidebar } from "./calendar-sidebar"
|
||||
import { CalendarMain } from "./calendar-main"
|
||||
import { EventForm } from "./event-form"
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
|
||||
import { type CalendarEvent } from "../types"
|
||||
import { useCalendar } from "../use-calendar"
|
||||
|
||||
interface CalendarProps {
|
||||
events: CalendarEvent[]
|
||||
eventDates: Array<{ date: Date; count: number }>
|
||||
}
|
||||
|
||||
export function Calendar({ events, eventDates }: CalendarProps) {
|
||||
const calendar = useCalendar(events)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border rounded-lg bg-background relative">
|
||||
<div className="flex min-h-[800px]">
|
||||
{/* Desktop Sidebar - Hidden on mobile/tablet, shown on extra large screens */}
|
||||
<div className="hidden xl:block w-80 flex-shrink-0 border-r">
|
||||
<CalendarSidebar
|
||||
selectedDate={calendar.selectedDate}
|
||||
onDateSelect={calendar.handleDateSelect}
|
||||
onNewCalendar={calendar.handleNewCalendar}
|
||||
onNewEvent={calendar.handleNewEvent}
|
||||
events={eventDates}
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Calendar Panel */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<CalendarMain
|
||||
selectedDate={calendar.selectedDate}
|
||||
onDateSelect={calendar.handleDateSelect}
|
||||
onMenuClick={() => calendar.setShowCalendarSheet(true)}
|
||||
events={calendar.events}
|
||||
onEventClick={calendar.handleEditEvent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile/Tablet Sheet - Positioned relative to calendar container */}
|
||||
<Sheet open={calendar.showCalendarSheet} onOpenChange={calendar.setShowCalendarSheet}>
|
||||
<SheetContent side="left" className="w-80 p-0" style={{ position: 'absolute' }}>
|
||||
<SheetHeader className="p-4 pb-2">
|
||||
<SheetTitle>Calendar</SheetTitle>
|
||||
<SheetDescription>
|
||||
Browse dates and manage your calendar events
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<CalendarSidebar
|
||||
selectedDate={calendar.selectedDate}
|
||||
onDateSelect={calendar.handleDateSelect}
|
||||
onNewCalendar={calendar.handleNewCalendar}
|
||||
onNewEvent={calendar.handleNewEvent}
|
||||
events={eventDates}
|
||||
className="h-full"
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
||||
{/* Event Form Dialog */}
|
||||
<EventForm
|
||||
event={calendar.editingEvent}
|
||||
open={calendar.showEventForm}
|
||||
onOpenChange={calendar.setShowEventForm}
|
||||
onSave={calendar.handleSaveEvent}
|
||||
onDelete={calendar.handleDeleteEvent}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Check, ChevronRight, Plus, Eye, EyeOff, MoreHorizontal } from "lucide-react"
|
||||
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface CalendarItem {
|
||||
id: string
|
||||
name: string
|
||||
color: string
|
||||
visible: boolean
|
||||
type: "personal" | "work" | "shared"
|
||||
}
|
||||
|
||||
interface CalendarGroup {
|
||||
name: string
|
||||
items: CalendarItem[]
|
||||
}
|
||||
|
||||
interface CalendarsProps {
|
||||
calendars?: {
|
||||
name: string
|
||||
items: string[]
|
||||
}[]
|
||||
onCalendarToggle?: (calendarId: string, visible: boolean) => void
|
||||
onCalendarEdit?: (calendarId: string) => void
|
||||
onCalendarDelete?: (calendarId: string) => void
|
||||
onNewCalendar?: () => void
|
||||
}
|
||||
|
||||
// Enhanced calendar data with colors and visibility
|
||||
const enhancedCalendars: CalendarGroup[] = [
|
||||
{
|
||||
name: "My Calendars",
|
||||
items: [
|
||||
{ id: "personal", name: "Personal", color: "bg-blue-500", visible: true, type: "personal" },
|
||||
{ id: "work", name: "Work", color: "bg-green-500", visible: true, type: "work" },
|
||||
{ id: "family", name: "Family", color: "bg-pink-500", visible: true, type: "personal" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Favorites",
|
||||
items: [
|
||||
{ id: "holidays", name: "Holidays", color: "bg-red-500", visible: true, type: "shared" },
|
||||
{ id: "birthdays", name: "Birthdays", color: "bg-purple-500", visible: true, type: "personal" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Other",
|
||||
items: [
|
||||
{ id: "travel", name: "Travel", color: "bg-orange-500", visible: false, type: "personal" },
|
||||
{ id: "reminders", name: "Reminders", color: "bg-yellow-500", visible: true, type: "personal" },
|
||||
{ id: "deadlines", name: "Deadlines", color: "bg-red-600", visible: true, type: "work" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export function Calendars({
|
||||
onCalendarToggle,
|
||||
onCalendarEdit,
|
||||
onCalendarDelete,
|
||||
onNewCalendar
|
||||
}: CalendarsProps) {
|
||||
const [calendarData, setCalendarData] = useState(enhancedCalendars)
|
||||
|
||||
const handleToggleVisibility = (calendarId: string) => {
|
||||
setCalendarData(prev => prev.map(group => ({
|
||||
...group,
|
||||
items: group.items.map(item =>
|
||||
item.id === calendarId
|
||||
? { ...item, visible: !item.visible }
|
||||
: item
|
||||
)
|
||||
})))
|
||||
|
||||
const calendar = calendarData.flatMap(g => g.items).find(c => c.id === calendarId)
|
||||
if (calendar) {
|
||||
onCalendarToggle?.(calendarId, !calendar.visible)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{calendarData.map((calendar, index) => (
|
||||
<div key={calendar.name}>
|
||||
<Collapsible
|
||||
defaultOpen={index === 0}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<CollapsibleTrigger className="flex items-center justify-between w-full p-2 hover:bg-accent hover:text-accent-foreground rounded-md cursor-pointer">
|
||||
<span className="text-sm font-medium">{calendar.name}</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{index === 0 && (
|
||||
<div
|
||||
className="h-5 w-5 flex items-center justify-center opacity-0 group-hover/collapsible:opacity-100 cursor-pointer hover:bg-accent rounded-sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onNewCalendar?.()
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</div>
|
||||
)}
|
||||
<ChevronRight className="h-4 w-4 transition-transform group-data-[state=open]/collapsible:rotate-90" />
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleContent>
|
||||
<div className="mt-2 space-y-1">
|
||||
{calendar.items.map((item) => (
|
||||
<div key={item.id} className="group/calendar-item">
|
||||
<div className="flex items-center justify-between p-2 hover:bg-accent/50 rounded-md">
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
{/* Calendar Color & Visibility Toggle */}
|
||||
<button
|
||||
onClick={() => handleToggleVisibility(item.id)}
|
||||
className={cn(
|
||||
"flex aspect-square size-4 shrink-0 items-center justify-center rounded-sm border transition-all cursor-pointer",
|
||||
item.visible
|
||||
? cn("border-transparent text-white", item.color)
|
||||
: "border-border bg-transparent"
|
||||
)}
|
||||
>
|
||||
{item.visible && <Check className="size-3" />}
|
||||
</button>
|
||||
|
||||
{/* Calendar Name */}
|
||||
<span
|
||||
className={cn(
|
||||
"flex-1 truncate text-sm cursor-pointer",
|
||||
!item.visible && "text-muted-foreground"
|
||||
)}
|
||||
onClick={() => handleToggleVisibility(item.id)}
|
||||
>
|
||||
{item.name}
|
||||
</span>
|
||||
|
||||
{/* Visibility Icon */}
|
||||
<div className="opacity-0 group-hover/calendar-item:opacity-100">
|
||||
{item.visible ? (
|
||||
<Eye className="h-3 w-3 text-muted-foreground" />
|
||||
) : (
|
||||
<EyeOff className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* More Options */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div
|
||||
className="h-5 w-5 flex items-center justify-center p-0 opacity-0 group-hover/calendar-item:opacity-100 cursor-pointer hover:bg-accent rounded-sm"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<MoreHorizontal className="h-3 w-3" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" side="right">
|
||||
<DropdownMenuItem
|
||||
onClick={() => onCalendarEdit?.(item.id)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Edit calendar
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleToggleVisibility(item.id)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{item.visible ? "Hide" : "Show"} calendar
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => onCalendarDelete?.(item.id)}
|
||||
className="cursor-pointer text-destructive"
|
||||
>
|
||||
Delete calendar
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Calendar } from "@/components/ui/calendar"
|
||||
|
||||
interface DatePickerProps {
|
||||
selectedDate?: Date
|
||||
onDateSelect?: (date: Date) => void
|
||||
events?: Array<{ date: Date; count: number }>
|
||||
}
|
||||
|
||||
export function DatePicker({ selectedDate, onDateSelect, events = [] }: DatePickerProps) {
|
||||
const [date, setDate] = useState<Date | undefined>(selectedDate || new Date())
|
||||
|
||||
const handleDateSelect = (selectedDate: Date | undefined) => {
|
||||
if (selectedDate) {
|
||||
setDate(selectedDate)
|
||||
onDateSelect?.(selectedDate)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a map of dates with events for styling
|
||||
const eventDates = events.reduce((acc, event) => {
|
||||
const dateKey = event.date.toDateString()
|
||||
acc[dateKey] = event.count
|
||||
return acc
|
||||
}, {} as Record<string, number>)
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={handleDateSelect}
|
||||
className="w-full [&_[role=gridcell]_button]:cursor-pointer [&_button]:cursor-pointer"
|
||||
modifiers={{
|
||||
hasEvents: (date) => {
|
||||
const eventCount = eventDates[date.toDateString()]
|
||||
return Boolean(eventCount && eventCount > 0)
|
||||
}
|
||||
}}
|
||||
modifiersClassNames={{
|
||||
hasEvents: "relative after:absolute after:bottom-1 after:right-1 after:w-1.5 after:h-1.5 after:bg-primary after:rounded-full"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState, useEffect, useState } from "react";
|
||||
import { Loader2, Save, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
createCalendarEventAction,
|
||||
updateCalendarEventAction,
|
||||
} from "@/lib/appwrite/calendar-actions";
|
||||
import { initialCalendarState } from "@/lib/appwrite/calendar-types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { COLOR_PRESETS, type Customer, type EventRow } from "./types";
|
||||
|
||||
const NONE = "__none__";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onOpenChange: (v: boolean) => void;
|
||||
event?: EventRow | null;
|
||||
defaultDate?: string; // YYYY-MM-DD for new events
|
||||
customers: Customer[];
|
||||
onRequestDelete?: (event: EventRow) => void;
|
||||
};
|
||||
|
||||
function isoToInput(iso: string, allDay: boolean): string {
|
||||
if (!iso) return "";
|
||||
if (allDay) return iso.slice(0, 10);
|
||||
const d = new Date(iso);
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
export function EventFormSheet({
|
||||
open,
|
||||
onOpenChange,
|
||||
event,
|
||||
defaultDate,
|
||||
customers,
|
||||
onRequestDelete,
|
||||
}: Props) {
|
||||
const isEdit = Boolean(event);
|
||||
const action = isEdit ? updateCalendarEventAction : createCalendarEventAction;
|
||||
const [state, formAction, isPending] = useActionState(action, initialCalendarState);
|
||||
const [allDay, setAllDay] = useState<boolean>(event?.allDay ?? false);
|
||||
|
||||
useEffect(() => {
|
||||
setAllDay(event?.allDay ?? false);
|
||||
}, [event]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.ok) {
|
||||
toast.success(isEdit ? "Etkinlik güncellendi." : "Etkinlik eklendi.");
|
||||
onOpenChange(false);
|
||||
} else if (state.error) {
|
||||
toast.error(state.error);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [state]);
|
||||
|
||||
const startDefault =
|
||||
event?.start
|
||||
? isoToInput(event.start, allDay)
|
||||
: defaultDate
|
||||
? allDay
|
||||
? defaultDate
|
||||
: `${defaultDate}T09:00`
|
||||
: "";
|
||||
const endDefault =
|
||||
event?.end
|
||||
? isoToInput(event.end, allDay)
|
||||
: defaultDate
|
||||
? allDay
|
||||
? defaultDate
|
||||
: `${defaultDate}T10:00`
|
||||
: "";
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent className="flex w-full flex-col gap-0 p-0 sm:max-w-xl">
|
||||
<SheetHeader className="border-b px-6 py-4">
|
||||
<SheetTitle>{isEdit ? "Etkinliği düzenle" : "Yeni etkinlik"}</SheetTitle>
|
||||
<SheetDescription>
|
||||
Tarih, saat ve müşteri bilgileri ile bir takvim girdisi oluşturun.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<form
|
||||
action={(fd) => {
|
||||
["customerId", "color"].forEach((k) => {
|
||||
if (fd.get(k) === NONE) fd.set(k, "");
|
||||
});
|
||||
formAction(fd);
|
||||
}}
|
||||
className="flex flex-1 flex-col"
|
||||
>
|
||||
{isEdit && event && <input type="hidden" name="id" value={event.id} />}
|
||||
|
||||
<div className="flex-1 space-y-4 overflow-y-auto px-6 py-5">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="title">Başlık *</Label>
|
||||
<Input
|
||||
id="title"
|
||||
name="title"
|
||||
defaultValue={event?.title ?? ""}
|
||||
placeholder="Örn. Müşteri toplantısı"
|
||||
required
|
||||
/>
|
||||
{state.fieldErrors?.title && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.title}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div className="grid gap-0.5">
|
||||
<Label htmlFor="allDay" className="cursor-pointer">
|
||||
Tüm gün
|
||||
</Label>
|
||||
<p className="text-muted-foreground text-xs">Saat girmeden gün boyu sürecek.</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="allDay"
|
||||
name="allDay"
|
||||
checked={allDay}
|
||||
onCheckedChange={setAllDay}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="start">Başlangıç *</Label>
|
||||
<Input
|
||||
id="start"
|
||||
name="start"
|
||||
type={allDay ? "date" : "datetime-local"}
|
||||
defaultValue={startDefault}
|
||||
required
|
||||
/>
|
||||
{state.fieldErrors?.start && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.start}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="end">Bitiş *</Label>
|
||||
<Input
|
||||
id="end"
|
||||
name="end"
|
||||
type={allDay ? "date" : "datetime-local"}
|
||||
defaultValue={endDefault}
|
||||
required
|
||||
/>
|
||||
{state.fieldErrors?.end && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.end}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="customerId">Müşteri (opsiyonel)</Label>
|
||||
<Select name="customerId" defaultValue={event?.customerId || NONE}>
|
||||
<SelectTrigger id="customerId">
|
||||
<SelectValue placeholder="Yok" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NONE}>Yok</SelectItem>
|
||||
{customers.map((c) => (
|
||||
<SelectItem key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="color">Renk</Label>
|
||||
<Select name="color" defaultValue={event?.color || NONE}>
|
||||
<SelectTrigger id="color">
|
||||
<SelectValue placeholder="Varsayılan" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NONE}>Varsayılan</SelectItem>
|
||||
{COLOR_PRESETS.map((c) => (
|
||||
<SelectItem key={c.value} value={c.value}>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className={cn("size-3 rounded-full", c.classes)} />
|
||||
{c.label}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">Notlar</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
name="description"
|
||||
rows={3}
|
||||
defaultValue={event?.description ?? ""}
|
||||
placeholder="Açıklama, gündem, vb."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SheetFooter className="border-t bg-muted/30 px-6 py-4">
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<div>
|
||||
{isEdit && event && onRequestDelete && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive"
|
||||
onClick={() => onRequestDelete(event)}
|
||||
disabled={isPending}
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
Sil
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isPending}
|
||||
>
|
||||
Vazgeç
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Kaydediliyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="size-4" />
|
||||
{isEdit ? "Güncelle" : "Kaydet"}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -1,339 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { CalendarIcon, Clock, MapPin, Users, Type, Tag } from "lucide-react"
|
||||
import { format } from "date-fns"
|
||||
|
||||
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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/components/ui/select"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
} from "@/components/ui/popover"
|
||||
import { Calendar } from "@/components/ui/calendar"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { type CalendarEvent } from "../types"
|
||||
|
||||
interface EventFormProps {
|
||||
event?: CalendarEvent | null
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSave: (event: Partial<CalendarEvent>) => void
|
||||
onDelete?: (eventId: number) => void
|
||||
}
|
||||
|
||||
const eventTypes = [
|
||||
{ value: "meeting", label: "Meeting", color: "bg-blue-500" },
|
||||
{ value: "event", label: "Event", color: "bg-green-500" },
|
||||
{ value: "personal", label: "Personal", color: "bg-pink-500" },
|
||||
{ value: "task", label: "Task", color: "bg-orange-500" },
|
||||
{ value: "reminder", label: "Reminder", color: "bg-purple-500" }
|
||||
]
|
||||
|
||||
const timeSlots = [
|
||||
"9:00 AM", "9:30 AM", "10:00 AM", "10:30 AM", "11:00 AM", "11:30 AM",
|
||||
"12:00 PM", "12:30 PM", "1:00 PM", "1:30 PM", "2:00 PM", "2:30 PM",
|
||||
"3:00 PM", "3:30 PM", "4:00 PM", "4:30 PM", "5:00 PM", "5:30 PM",
|
||||
"6:00 PM", "6:30 PM", "7:00 PM", "7:30 PM", "8:00 PM", "8:30 PM"
|
||||
]
|
||||
|
||||
const durationOptions = [
|
||||
"15 min", "30 min", "45 min", "1 hour", "1.5 hours", "2 hours", "3 hours", "All day"
|
||||
]
|
||||
|
||||
export function EventForm({ event, open, onOpenChange, onSave, onDelete }: EventFormProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
title: event?.title || "",
|
||||
date: event?.date || new Date(),
|
||||
time: event?.time || "9:00 AM",
|
||||
duration: event?.duration || "1 hour",
|
||||
type: event?.type || "meeting",
|
||||
location: event?.location || "",
|
||||
description: event?.description || "",
|
||||
attendees: event?.attendees || [],
|
||||
allDay: false,
|
||||
reminder: true
|
||||
})
|
||||
|
||||
const [showCalendar, setShowCalendar] = useState(false)
|
||||
const [newAttendee, setNewAttendee] = useState("")
|
||||
|
||||
const handleSave = () => {
|
||||
const eventData: Partial<CalendarEvent> = {
|
||||
...formData,
|
||||
id: event?.id,
|
||||
color: eventTypes.find(t => t.value === formData.type)?.color || "bg-blue-500"
|
||||
}
|
||||
onSave(eventData)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
if (event?.id && onDelete) {
|
||||
onDelete(event.id)
|
||||
onOpenChange(false)
|
||||
}
|
||||
}
|
||||
|
||||
const addAttendee = () => {
|
||||
if (newAttendee.trim() && !formData.attendees.includes(newAttendee.trim())) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
attendees: [...prev.attendees, newAttendee.trim()]
|
||||
}))
|
||||
setNewAttendee("")
|
||||
}
|
||||
}
|
||||
|
||||
const removeAttendee = (attendee: string) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
attendees: prev.attendees.filter(a => a !== attendee)
|
||||
}))
|
||||
}
|
||||
|
||||
const selectedEventType = eventTypes.find(t => t.value === formData.type)
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<div className={cn("w-3 h-3 rounded-full", selectedEventType?.color)} />
|
||||
{event ? "Edit Event" : "Create New Event"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{event ? "Make changes to this event" : "Add a new event to your calendar"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-4">
|
||||
{/* Event Title */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title" className="flex items-center gap-2">
|
||||
<Type className="w-4 h-4" />
|
||||
Event Title
|
||||
</Label>
|
||||
<Input
|
||||
id="title"
|
||||
placeholder="Enter event title..."
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
||||
className="text-lg font-medium"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Event Type */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<Tag className="w-4 h-4" />
|
||||
Event Type
|
||||
</Label>
|
||||
<Select value={formData.type} onValueChange={(value) => setFormData(prev => ({ ...prev, type: value as CalendarEvent["type"] }))}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{eventTypes.map(type => (
|
||||
<SelectItem key={type.value} value={type.value}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={cn("w-3 h-3 rounded-full", type.color)} />
|
||||
{type.label}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date and Time */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<CalendarIcon className="w-4 h-4" />
|
||||
Date
|
||||
</Label>
|
||||
<Popover open={showCalendar} onOpenChange={setShowCalendar}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="w-full justify-start text-left font-normal">
|
||||
{format(formData.date, "PPP")}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={formData.date}
|
||||
onSelect={(date) => {
|
||||
if (date) {
|
||||
setFormData(prev => ({ ...prev, date }))
|
||||
setShowCalendar(false)
|
||||
}
|
||||
}}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
Time
|
||||
</Label>
|
||||
<Select value={formData.time} onValueChange={(value) => setFormData(prev => ({ ...prev, time: value }))}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{timeSlots.map(time => (
|
||||
<SelectItem key={time} value={time}>{time}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Duration and All Day */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Duration</Label>
|
||||
<Select value={formData.duration} onValueChange={(value) => setFormData(prev => ({ ...prev, duration: value }))}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{durationOptions.map(duration => (
|
||||
<SelectItem key={duration} value={duration}>{duration}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Options</Label>
|
||||
<div className="flex items-center space-x-4 h-10">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="all-day"
|
||||
checked={formData.allDay}
|
||||
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, allDay: checked }))}
|
||||
/>
|
||||
<Label htmlFor="all-day" className="text-sm">All day</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="reminder"
|
||||
checked={formData.reminder}
|
||||
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, reminder: checked }))}
|
||||
/>
|
||||
<Label htmlFor="reminder" className="text-sm">Reminder</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Location */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="location" className="flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4" />
|
||||
Location
|
||||
</Label>
|
||||
<Input
|
||||
id="location"
|
||||
placeholder="Add location..."
|
||||
value={formData.location}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, location: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Attendees */}
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4" />
|
||||
Attendees
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Add attendee..."
|
||||
value={newAttendee}
|
||||
onChange={(e) => setNewAttendee(e.target.value)}
|
||||
onKeyPress={(e) => e.key === "Enter" && addAttendee()}
|
||||
/>
|
||||
<Button onClick={addAttendee} variant="outline" className="cursor-pointer">Add</Button>
|
||||
</div>
|
||||
{formData.attendees.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{formData.attendees.map((attendee, index) => (
|
||||
<Badge key={index} variant="secondary" className="flex items-center gap-2 px-2 py-1">
|
||||
<Avatar className="w-5 h-5">
|
||||
<AvatarFallback className="text-[10px] font-medium">
|
||||
{attendee.split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-sm">{attendee}</span>
|
||||
<button
|
||||
onClick={() => removeAttendee(attendee)}
|
||||
className="text-muted-foreground hover:text-foreground cursor-pointer"
|
||||
type="button"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Add description..."
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3 pt-6">
|
||||
<Button onClick={handleSave} className="flex-1 cursor-pointer">
|
||||
{event ? "Update Event" : "Create Event"}
|
||||
</Button>
|
||||
{event && onDelete && (
|
||||
<Button onClick={handleDelete} variant="destructive" className="cursor-pointer">
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => onOpenChange(false)} variant="outline" className="cursor-pointer">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Clock,
|
||||
Users,
|
||||
Plus,
|
||||
Settings,
|
||||
Download,
|
||||
Share,
|
||||
Bell
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
interface QuickActionsProps {
|
||||
onNewEvent?: () => void
|
||||
onNewMeeting?: () => void
|
||||
onNewReminder?: () => void
|
||||
onSettings?: () => void
|
||||
}
|
||||
|
||||
export function QuickActions({
|
||||
onNewEvent,
|
||||
onNewMeeting,
|
||||
onNewReminder,
|
||||
onSettings
|
||||
}: QuickActionsProps) {
|
||||
const quickStats = [
|
||||
{ label: "Today's Events", value: "3", color: "bg-blue-500" },
|
||||
{ label: "This Week", value: "12", color: "bg-green-500" },
|
||||
{ label: "Pending", value: "2", color: "bg-orange-500" }
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Quick Stats */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium">Overview</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{quickStats.map((stat, index) => (
|
||||
<div key={index} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${stat.color}`} />
|
||||
<span className="text-sm text-muted-foreground">{stat.label}</span>
|
||||
</div>
|
||||
<Badge variant="secondary">{stat.value}</Badge>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium">Quick Actions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start cursor-pointer"
|
||||
onClick={onNewEvent}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New Event
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start cursor-pointer"
|
||||
onClick={onNewMeeting}
|
||||
>
|
||||
<Users className="w-4 h-4 mr-2" />
|
||||
Schedule Meeting
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start cursor-pointer"
|
||||
onClick={onNewReminder}
|
||||
>
|
||||
<Bell className="w-4 h-4 mr-2" />
|
||||
Set Reminder
|
||||
</Button>
|
||||
|
||||
<Separator className="my-3" />
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start cursor-pointer"
|
||||
>
|
||||
<Share className="w-4 h-4 mr-2" />
|
||||
Share Calendar
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start cursor-pointer"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Export
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start cursor-pointer"
|
||||
onClick={onSettings}
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Settings
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Upcoming Events */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
Next Up
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">Team Standup</p>
|
||||
<p className="text-xs text-muted-foreground">9:00 AM • Conference Room A</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-2 h-2 bg-purple-500 rounded-full mt-2" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">Design Review</p>
|
||||
<p className="text-xs text-muted-foreground">2:00 PM • Virtual</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
export type EventRow = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
start: string;
|
||||
end: string;
|
||||
allDay: boolean;
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
export type Customer = { id: string; name: string };
|
||||
|
||||
export const COLOR_PRESETS = [
|
||||
{ value: "blue", label: "Mavi", classes: "bg-blue-500" },
|
||||
{ value: "green", label: "Yeşil", classes: "bg-emerald-500" },
|
||||
{ value: "amber", label: "Amber", classes: "bg-amber-500" },
|
||||
{ value: "red", label: "Kırmızı", classes: "bg-red-500" },
|
||||
{ value: "violet", label: "Mor", classes: "bg-violet-500" },
|
||||
{ value: "slate", label: "Gri", classes: "bg-slate-500" },
|
||||
] as const;
|
||||
|
||||
export const COLOR_BG: Record<string, string> = {
|
||||
blue: "bg-blue-500/15 text-blue-700 dark:text-blue-300 border-blue-500/30",
|
||||
green: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border-emerald-500/30",
|
||||
amber: "bg-amber-500/15 text-amber-800 dark:text-amber-300 border-amber-500/30",
|
||||
red: "bg-red-500/15 text-red-700 dark:text-red-300 border-red-500/30",
|
||||
violet: "bg-violet-500/15 text-violet-700 dark:text-violet-300 border-violet-500/30",
|
||||
slate: "bg-slate-500/15 text-slate-700 dark:text-slate-300 border-slate-500/30",
|
||||
"": "bg-primary/10 text-primary border-primary/20",
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
import { type CalendarEvent, type Calendar } from "./types"
|
||||
|
||||
// Import JSON data
|
||||
import eventsData from "./data/events.json"
|
||||
import eventDatesData from "./data/event-dates.json"
|
||||
import calendarsData from "./data/calendars.json"
|
||||
|
||||
// Convert JSON events to CalendarEvent objects with proper Date objects
|
||||
// Always use current month and year, but preserve day and time from JSON
|
||||
export const events: CalendarEvent[] = eventsData.map(event => {
|
||||
const now = new Date()
|
||||
const currentYear = now.getFullYear()
|
||||
const currentMonth = now.getMonth() // 0-based month
|
||||
|
||||
// Parse the day from the date string (format: "11T09:00:00.000Z")
|
||||
const dayAndTime = event.date.split('T')
|
||||
const day = parseInt(dayAndTime[0])
|
||||
const timeStr = dayAndTime[1] // "09:00:00.000Z"
|
||||
|
||||
// Parse hours and minutes from time string
|
||||
const timeParts = timeStr.split(':')
|
||||
const hours = parseInt(timeParts[0])
|
||||
const minutes = parseInt(timeParts[1])
|
||||
|
||||
// Create date with current year/month but original day and time
|
||||
const eventDate = new Date(currentYear, currentMonth, day, hours, minutes)
|
||||
|
||||
return {
|
||||
...event,
|
||||
date: eventDate,
|
||||
type: event.type as "meeting" | "event" | "personal" | "task" | "reminder"
|
||||
}
|
||||
})
|
||||
|
||||
// Convert event dates for calendar picker - also use current month/year
|
||||
export const eventDates = eventDatesData.map(item => {
|
||||
const now = new Date()
|
||||
const currentYear = now.getFullYear()
|
||||
const currentMonth = now.getMonth()
|
||||
|
||||
// Parse day from date string
|
||||
const day = parseInt(item.date.split('T')[0])
|
||||
const eventDate = new Date(currentYear, currentMonth, day)
|
||||
|
||||
return {
|
||||
date: eventDate,
|
||||
count: item.count
|
||||
}
|
||||
})
|
||||
|
||||
// Calendars data
|
||||
export const calendars: Calendar[] = calendarsData as Calendar[]
|
||||
|
||||
// Export individual collections for convenience
|
||||
export { eventsData, eventDatesData, calendarsData }
|
||||
@@ -1,37 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "personal",
|
||||
"name": "Personal",
|
||||
"color": "bg-blue-500",
|
||||
"visible": true,
|
||||
"type": "personal"
|
||||
},
|
||||
{
|
||||
"id": "work",
|
||||
"name": "Work",
|
||||
"color": "bg-green-500",
|
||||
"visible": true,
|
||||
"type": "work"
|
||||
},
|
||||
{
|
||||
"id": "shared",
|
||||
"name": "Team Calendar",
|
||||
"color": "bg-purple-500",
|
||||
"visible": true,
|
||||
"type": "shared"
|
||||
},
|
||||
{
|
||||
"id": "meetings",
|
||||
"name": "Meetings",
|
||||
"color": "bg-orange-500",
|
||||
"visible": true,
|
||||
"type": "work"
|
||||
},
|
||||
{
|
||||
"id": "events",
|
||||
"name": "Events",
|
||||
"color": "bg-pink-500",
|
||||
"visible": true,
|
||||
"type": "shared"
|
||||
}
|
||||
]
|
||||
@@ -1,30 +0,0 @@
|
||||
[
|
||||
{
|
||||
"date": "11T00:00:00.000Z",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"date": "15T00:00:00.000Z",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"date": "18T00:00:00.000Z",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"date": "20T00:00:00.000Z",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"date": "22T00:00:00.000Z",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"date": "25T00:00:00.000Z",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"date": "27T00:00:00.000Z",
|
||||
"count": 1
|
||||
}
|
||||
]
|
||||
@@ -1,62 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Team Standup",
|
||||
"date": "11T09:00:00.000Z",
|
||||
"time": "9:00 AM",
|
||||
"duration": "30 min",
|
||||
"type": "meeting",
|
||||
"attendees": ["JD", "SM", "AR"],
|
||||
"location": "Conference Room A",
|
||||
"color": "bg-blue-500",
|
||||
"description": "Daily team standup meeting to discuss progress and blockers"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Design Review",
|
||||
"date": "11T14:00:00.000Z",
|
||||
"time": "2:00 PM",
|
||||
"duration": "1 hour",
|
||||
"type": "meeting",
|
||||
"attendees": ["ER", "LC"],
|
||||
"location": "Virtual",
|
||||
"color": "bg-purple-500",
|
||||
"description": "Review new UI designs and provide feedback"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Product Launch",
|
||||
"date": "15T10:00:00.000Z",
|
||||
"time": "10:00 AM",
|
||||
"duration": "2 hours",
|
||||
"type": "event",
|
||||
"attendees": ["TL", "ST"],
|
||||
"location": "Main Hall",
|
||||
"color": "bg-green-500",
|
||||
"description": "Official product launch event with stakeholders"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Client Presentation",
|
||||
"date": "18T15:00:00.000Z",
|
||||
"time": "3:00 PM",
|
||||
"duration": "1 hour",
|
||||
"type": "meeting",
|
||||
"attendees": ["AT", "SM"],
|
||||
"location": "Client Office",
|
||||
"color": "bg-orange-500",
|
||||
"description": "Present project progress to client stakeholders"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Birthday Party 🎉",
|
||||
"date": "20T19:00:00.000Z",
|
||||
"time": "7:00 PM",
|
||||
"duration": "3 hours",
|
||||
"type": "personal",
|
||||
"attendees": ["PB", "VB"],
|
||||
"location": "Home",
|
||||
"color": "bg-pink-500",
|
||||
"description": "Birthday celebration with friends and family"
|
||||
}
|
||||
]
|
||||
@@ -1,10 +1,54 @@
|
||||
import { Calendar } from "./components/calendar"
|
||||
import { events, eventDates } from "./data"
|
||||
import type { Metadata } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { listCalendarEvents } from "@/lib/appwrite/calendar-queries";
|
||||
import { listCustomers } from "@/lib/appwrite/customer-queries";
|
||||
import { requireTenant } from "@/lib/appwrite/tenant-guard";
|
||||
import { CalendarClient } from "./components/calendar-client";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "İşletmem — Takvim",
|
||||
};
|
||||
|
||||
export default async function CalendarPage() {
|
||||
let ctx;
|
||||
try {
|
||||
ctx = await requireTenant();
|
||||
} catch {
|
||||
redirect("/onboarding");
|
||||
}
|
||||
|
||||
const [events, customers] = await Promise.all([
|
||||
listCalendarEvents(ctx.tenantId),
|
||||
listCustomers(ctx.tenantId),
|
||||
]);
|
||||
|
||||
const customerMap = new Map(customers.map((c) => [c.$id, c.name]));
|
||||
|
||||
export default function CalendarPage() {
|
||||
return (
|
||||
<div className="px-4 lg:px-6">
|
||||
<Calendar events={events} eventDates={eventDates} />
|
||||
<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">Takvim</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Toplantılar, randevular ve önemli tarihler.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CalendarClient
|
||||
events={events.map((e) => ({
|
||||
id: e.$id,
|
||||
title: e.title,
|
||||
description: e.description ?? "",
|
||||
start: e.start,
|
||||
end: e.end,
|
||||
allDay: Boolean(e.allDay),
|
||||
customerId: e.customerId ?? "",
|
||||
customerName: e.customerId ? customerMap.get(e.customerId) ?? "" : "",
|
||||
color: e.color ?? "",
|
||||
}))}
|
||||
customers={customers.map((c) => ({ id: c.$id, name: c.name }))}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
export interface CalendarEvent {
|
||||
id: number
|
||||
title: string
|
||||
date: Date
|
||||
time: string
|
||||
duration: string
|
||||
type: "meeting" | "event" | "personal" | "task" | "reminder"
|
||||
attendees: string[]
|
||||
location: string
|
||||
color: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface Calendar {
|
||||
id: string
|
||||
name: string
|
||||
color: string
|
||||
visible: boolean
|
||||
type: "personal" | "work" | "shared"
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useCallback } from "react"
|
||||
import { type CalendarEvent } from "./types"
|
||||
|
||||
export interface UseCalendarState {
|
||||
selectedDate: Date
|
||||
showEventForm: boolean
|
||||
editingEvent: CalendarEvent | null
|
||||
showCalendarSheet: boolean
|
||||
events: CalendarEvent[]
|
||||
}
|
||||
|
||||
export interface UseCalendarActions {
|
||||
setSelectedDate: (date: Date) => void
|
||||
setShowEventForm: (show: boolean) => void
|
||||
setEditingEvent: (event: CalendarEvent | null) => void
|
||||
setShowCalendarSheet: (show: boolean) => void
|
||||
handleDateSelect: (date: Date) => void
|
||||
handleNewEvent: () => void
|
||||
handleNewCalendar: () => void
|
||||
handleSaveEvent: (eventData: Partial<CalendarEvent>) => void
|
||||
handleDeleteEvent: (eventId: number) => void
|
||||
handleEditEvent: (event: CalendarEvent) => void
|
||||
}
|
||||
|
||||
export interface UseCalendarReturn extends UseCalendarState, UseCalendarActions {}
|
||||
|
||||
export function useCalendar(initialEvents: CalendarEvent[] = []): UseCalendarReturn {
|
||||
const [selectedDate, setSelectedDate] = useState<Date>(new Date())
|
||||
const [showEventForm, setShowEventForm] = useState(false)
|
||||
const [editingEvent, setEditingEvent] = useState<CalendarEvent | null>(null)
|
||||
const [showCalendarSheet, setShowCalendarSheet] = useState(false)
|
||||
const [events] = useState<CalendarEvent[]>(initialEvents)
|
||||
|
||||
const handleDateSelect = useCallback((date: Date) => {
|
||||
setSelectedDate(date)
|
||||
// Auto-close mobile sheet when date is selected
|
||||
setShowCalendarSheet(false)
|
||||
}, [])
|
||||
|
||||
const handleNewEvent = useCallback(() => {
|
||||
setEditingEvent(null)
|
||||
setShowEventForm(true)
|
||||
}, [])
|
||||
|
||||
const handleNewCalendar = useCallback(() => {
|
||||
console.log("Creating new calendar")
|
||||
// In a real app, this would open a new calendar form
|
||||
}, [])
|
||||
|
||||
const handleSaveEvent = useCallback((eventData: Partial<CalendarEvent>) => {
|
||||
console.log("Saving event:", eventData)
|
||||
// In a real app, this would save to a backend
|
||||
setShowEventForm(false)
|
||||
setEditingEvent(null)
|
||||
}, [])
|
||||
|
||||
const handleDeleteEvent = useCallback((eventId: number) => {
|
||||
console.log("Deleting event:", eventId)
|
||||
// In a real app, this would delete from backend
|
||||
setShowEventForm(false)
|
||||
setEditingEvent(null)
|
||||
}, [])
|
||||
|
||||
const handleEditEvent = useCallback((event: CalendarEvent) => {
|
||||
setEditingEvent(event)
|
||||
setShowEventForm(true)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
// State
|
||||
selectedDate,
|
||||
showEventForm,
|
||||
editingEvent,
|
||||
showCalendarSheet,
|
||||
events,
|
||||
// Actions
|
||||
setSelectedDate,
|
||||
setShowEventForm,
|
||||
setEditingEvent,
|
||||
setShowCalendarSheet,
|
||||
handleDateSelect,
|
||||
handleNewEvent,
|
||||
handleNewCalendar,
|
||||
handleSaveEvent,
|
||||
handleDeleteEvent,
|
||||
handleEditEvent,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user