diff --git a/app/(dashboard)/calendar/actions.ts b/app/(dashboard)/calendar/actions.ts
new file mode 100644
index 0000000..ef6cc5c
--- /dev/null
+++ b/app/(dashboard)/calendar/actions.ts
@@ -0,0 +1,106 @@
+"use server";
+
+import { createClient } from "@/lib/supabase/server";
+import { revalidatePath } from "next/cache";
+
+const EVENT_TYPES = ["meeting", "focus", "deadline", "personal", "finance"] as const;
+
+function cleanText(value: FormDataEntryValue | null) {
+ const text = typeof value === "string" ? value.trim() : "";
+ return text.length > 0 && text !== "__none" ? text : null;
+}
+
+function readType(value: FormDataEntryValue | null) {
+ const type = typeof value === "string" ? value : "focus";
+ return EVENT_TYPES.includes(type as (typeof EVENT_TYPES)[number]) ? type : "focus";
+}
+
+async function getCurrentUserId() {
+ const supabase = await createClient();
+ const {
+ data: { user },
+ error,
+ } = await supabase.auth.getUser();
+
+ if (error || !user) {
+ throw new Error("Takvim işlemi için giriş yapmış kullanıcı bulunamadı.");
+ }
+
+ return { supabase, userId: user.id };
+}
+
+function readPayload(formData: FormData) {
+ return {
+ title: cleanText(formData.get("title")),
+ description: cleanText(formData.get("description")),
+ type: readType(formData.get("type")),
+ starts_at: cleanText(formData.get("starts_at")),
+ ends_at: cleanText(formData.get("ends_at")),
+ client_id: cleanText(formData.get("client_id")),
+ project_id: cleanText(formData.get("project_id")),
+ task_id: cleanText(formData.get("task_id")),
+ };
+}
+
+export async function createCalendarEventRecord(formData: FormData) {
+ const { supabase, userId } = await getCurrentUserId();
+ const payload = readPayload(formData);
+
+ if (!payload.title || !payload.starts_at) {
+ throw new Error("Etkinlik başlığı ve başlangıç zamanı zorunludur.");
+ }
+
+ const { error } = await supabase.from("calendar_events").insert({
+ user_id: userId,
+ ...payload,
+ });
+
+ if (error) {
+ throw new Error(`Etkinlik eklenemedi: ${error.message}`);
+ }
+
+ revalidatePath("/calendar");
+}
+
+export async function updateCalendarEventRecord(formData: FormData) {
+ const { supabase, userId } = await getCurrentUserId();
+ const id = cleanText(formData.get("id"));
+ const payload = readPayload(formData);
+
+ if (!id || !payload.title || !payload.starts_at) {
+ throw new Error("Etkinlik güncellemek için başlık, başlangıç ve kayıt kimliği zorunludur.");
+ }
+
+ const { error } = await supabase
+ .from("calendar_events")
+ .update(payload)
+ .eq("id", id)
+ .eq("user_id", userId);
+
+ if (error) {
+ throw new Error(`Etkinlik güncellenemedi: ${error.message}`);
+ }
+
+ revalidatePath("/calendar");
+}
+
+export async function deleteCalendarEventRecord(formData: FormData) {
+ const { supabase, userId } = await getCurrentUserId();
+ const id = cleanText(formData.get("id"));
+
+ if (!id) {
+ throw new Error("Silinecek etkinlik bulunamadı.");
+ }
+
+ const { error } = await supabase
+ .from("calendar_events")
+ .delete()
+ .eq("id", id)
+ .eq("user_id", userId);
+
+ if (error) {
+ throw new Error(`Etkinlik silinemedi: ${error.message}`);
+ }
+
+ revalidatePath("/calendar");
+}
diff --git a/app/(dashboard)/calendar/calendar-client.tsx b/app/(dashboard)/calendar/calendar-client.tsx
new file mode 100644
index 0000000..432fb94
--- /dev/null
+++ b/app/(dashboard)/calendar/calendar-client.tsx
@@ -0,0 +1,456 @@
+"use client";
+
+import {
+ createCalendarEventRecord,
+ deleteCalendarEventRecord,
+ updateCalendarEventRecord,
+} from "@/app/(dashboard)/calendar/actions";
+import { Badge, Button, Card, CardContent, Input, Label, Textarea } from "poyraz-ui/atoms";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "poyraz-ui/molecules";
+import { CalendarDays, Clock, Pencil, Plus, Trash2 } from "lucide-react";
+import { useMemo, useState } from "react";
+
+export type CalendarRelationOption = {
+ id: string;
+ name: string;
+};
+
+export type CalendarTaskOption = {
+ id: string;
+ title: string;
+};
+
+export type CalendarEventItem = {
+ id: string;
+ title: string;
+ description: string | null;
+ type: "meeting" | "focus" | "deadline" | "personal" | "finance";
+ starts_at: string;
+ ends_at: string | null;
+ client_id: string | null;
+ project_id: string | null;
+ task_id: string | null;
+ clientName: string | null;
+ projectName: string | null;
+ taskTitle: string | null;
+};
+
+const typeLabels = {
+ meeting: "Toplantı",
+ focus: "Odak",
+ deadline: "Deadline",
+ personal: "Kişisel",
+ finance: "Finans",
+};
+
+const typeClasses = {
+ meeting: "border-blue-200 bg-blue-50 text-blue-700",
+ focus: "border-emerald-200 bg-emerald-50 text-emerald-700",
+ deadline: "border-rose-200 bg-rose-50 text-rose-700",
+ personal: "border-amber-200 bg-amber-50 text-amber-700",
+ finance: "border-primary/20 bg-primary/10 text-primary",
+};
+
+type CalendarClientProps = {
+ events: CalendarEventItem[];
+ clients: CalendarRelationOption[];
+ projects: CalendarRelationOption[];
+ tasks: CalendarTaskOption[];
+};
+
+export function CalendarClient({ events, clients, projects, tasks }: CalendarClientProps) {
+ const [monthDate, setMonthDate] = useState(() => new Date());
+ const [selectedDate, setSelectedDate] = useState(() => toDateKey(new Date()));
+ const days = useMemo(() => buildMonthDays(monthDate), [monthDate]);
+ const eventsByDay = useMemo(() => groupEventsByDay(events), [events]);
+ const selectedEvents = eventsByDay.get(selectedDate) || [];
+ const upcomingEvents = events
+ .filter((event) => new Date(event.starts_at) >= startOfToday())
+ .slice(0, 6);
+
+ function shiftMonth(amount: number) {
+ setMonthDate((current) => new Date(current.getFullYear(), current.getMonth() + amount, 1));
+ }
+
+ return (
+
+
+
+
+
+ Planlama
+
+
+
Takvim
+
+ Toplantı, odak bloğu, deadline, kişisel ve finans etkinliklerini yönet.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatMonth(monthDate)}
+
+
{events.length} etkinlik
+
+
+
+
+
+
+
+
+
+ {["Pzt", "Sal", "Çar", "Per", "Cum", "Cmt", "Paz"].map((day) => (
+
+ {day}
+
+ ))}
+
+
+ {days.map((day) => {
+ const dayEvents = eventsByDay.get(day.key) || [];
+ const isSelected = selectedDate === day.key;
+
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+ {formatDateLabel(selectedDate)}
+
+
+ {selectedEvents.length} etkinlik
+
+
+
+
+
+
+
+
+
+ Yaklaşan etkinlikler
+
+
+
+
+
+
+ );
+}
+
+function EventList({
+ events,
+ clients,
+ projects,
+ tasks,
+ compact = false,
+}: {
+ events: CalendarEventItem[];
+ clients: CalendarRelationOption[];
+ projects: CalendarRelationOption[];
+ tasks: CalendarTaskOption[];
+ compact?: boolean;
+}) {
+ if (events.length === 0) {
+ return Etkinlik yok.
;
+ }
+
+ return (
+
+ {events.map((event) => (
+
+
+
+
{event.title}
+
+
+ {formatTimeRange(event)}
+
+ {!compact ? (
+
+ {event.projectName || event.clientName || event.taskTitle || event.description || "Bağlantı yok"}
+
+ ) : null}
+
+
{typeLabels[event.type]}
+
+ {!compact ? (
+
+
+
+
+ ) : null}
+
+ ))}
+
+ );
+}
+
+function CalendarEventDialog({
+ mode,
+ event,
+ defaultDate,
+ clients,
+ projects,
+ tasks,
+}: {
+ mode: "create" | "edit";
+ event?: CalendarEventItem;
+ defaultDate?: string;
+ clients: CalendarRelationOption[];
+ projects: CalendarRelationOption[];
+ tasks: CalendarTaskOption[];
+}) {
+ const [open, setOpen] = useState(false);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const action = mode === "create" ? createCalendarEventRecord : updateCalendarEventRecord;
+
+ async function handleSubmit(formData: FormData) {
+ setIsSubmitting(true);
+ try {
+ await action(formData);
+ setOpen(false);
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ return (
+
+ );
+}
+
+function EventFormFields({
+ event,
+ defaultDate,
+ clients,
+ projects,
+ tasks,
+}: {
+ event?: CalendarEventItem;
+ defaultDate?: string;
+ clients: CalendarRelationOption[];
+ projects: CalendarRelationOption[];
+ tasks: CalendarTaskOption[];
+}) {
+ const startsAt = event?.starts_at ? toDateTimeLocal(event.starts_at) : `${defaultDate || toDateKey(new Date())}T09:00`;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ Toplantı
+ Odak
+ Deadline
+ Kişisel
+ Finans
+
+
+ Müşteri yok
+ {clients.map((client) => {client.name})}
+
+
+
+
+ Proje yok
+ {projects.map((project) => {project.name})}
+
+
+ Görev yok
+ {tasks.map((task) => {task.title})}
+
+
+
+
+ );
+}
+
+function SelectField({ name, label, defaultValue, children }: { name: string; label: string; defaultValue: string; children: React.ReactNode }) {
+ return (
+
+
+
+
+ );
+}
+
+function buildMonthDays(monthDate: Date) {
+ const year = monthDate.getFullYear();
+ const month = monthDate.getMonth();
+ const firstDay = new Date(year, month, 1);
+ const offset = (firstDay.getDay() + 6) % 7;
+ const start = new Date(year, month, 1 - offset);
+
+ return Array.from({ length: 42 }, (_, index) => {
+ const date = new Date(start);
+ date.setDate(start.getDate() + index);
+ return { date, key: toDateKey(date), inMonth: date.getMonth() === month };
+ });
+}
+
+function groupEventsByDay(events: CalendarEventItem[]) {
+ const map = new Map();
+ for (const event of events) {
+ const key = toDateKey(new Date(event.starts_at));
+ map.set(key, [...(map.get(key) || []), event]);
+ }
+ return map;
+}
+
+function toDateKey(date: Date) {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ return `${year}-${month}-${day}`;
+}
+
+function startOfToday() {
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ return today;
+}
+
+function formatMonth(date: Date) {
+ return new Intl.DateTimeFormat("tr-TR", { month: "long", year: "numeric" }).format(date);
+}
+
+function formatDateLabel(dateKey: string) {
+ return new Intl.DateTimeFormat("tr-TR", { day: "2-digit", month: "long", year: "numeric" }).format(new Date(`${dateKey}T00:00:00`));
+}
+
+function formatTimeRange(event: CalendarEventItem) {
+ const start = new Intl.DateTimeFormat("tr-TR", { hour: "2-digit", minute: "2-digit" }).format(new Date(event.starts_at));
+ const end = event.ends_at ? new Intl.DateTimeFormat("tr-TR", { hour: "2-digit", minute: "2-digit" }).format(new Date(event.ends_at)) : null;
+ return end ? `${start} - ${end}` : start;
+}
+
+function toDateTimeLocal(value: string) {
+ const date = new Date(value);
+ const offsetDate = new Date(date.getTime() - date.getTimezoneOffset() * 60_000);
+ return offsetDate.toISOString().slice(0, 16);
+}
diff --git a/app/(dashboard)/calendar/page.tsx b/app/(dashboard)/calendar/page.tsx
index ce3bc77..1395ffb 100644
--- a/app/(dashboard)/calendar/page.tsx
+++ b/app/(dashboard)/calendar/page.tsx
@@ -1,339 +1,107 @@
-"use client";
+import {
+ CalendarClient,
+ type CalendarEventItem,
+ type CalendarRelationOption,
+ type CalendarTaskOption,
+} from "@/app/(dashboard)/calendar/calendar-client";
+import { createClient } from "@/lib/supabase/server";
-import { useState } from "react";
-import {
- ChevronLeft, ChevronRight, Plus, Search, Calendar as CalendarIcon,
- Clock, Filter, MoreHorizontal, Brain, X, Check, ArrowRight
-} from "lucide-react";
-import { motion, AnimatePresence } from "framer-motion";
-
-// Mock Data
-const categories = [
- { name: "Deep Work", color: "bg-[#6C5BB0]" },
- { name: "Meetings", color: "bg-orange-500" },
- { name: "Project Deadlines", color: "bg-blue-500" },
- { name: "Health & Habits", color: "bg-emerald-500" },
-];
-
-const mockEvents = [
- { day: 2, title: "Q3 Planning", type: "Meetings", time: "10:00 AM", colorClass: "bg-orange-500/10 text-orange-400 border-orange-500/20" },
- { day: 5, title: "Deep Work Session", type: "Deep Work", time: "09:00 AM", colorClass: "bg-[#6C5BB0]/10 text-[#a798e8] border-[#6C5BB0]/20" },
- { day: 5, title: "UI Review", type: "Project Deadlines", time: "02:00 PM", colorClass: "bg-blue-500/10 text-blue-400 border-blue-500/20" },
- { day: 12, title: "Frontend Deploy", type: "Project Deadlines", time: "11:00 AM", colorClass: "bg-blue-500/10 text-blue-400 border-blue-500/20" },
- { day: 15, title: "1:1 with Alex", type: "Meetings", time: "01:30 PM", colorClass: "bg-orange-500/10 text-orange-400 border-orange-500/20" },
- { day: 15, title: "Gym", type: "Health & Habits", time: "06:00 PM", colorClass: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20" },
- { day: 18, title: "Focus Block", type: "Deep Work", time: "08:00 AM", colorClass: "bg-[#6C5BB0]/10 text-[#a798e8] border-[#6C5BB0]/20" },
- { day: 22, title: "Reading", type: "Health & Habits", time: "09:00 PM", colorClass: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20" },
- { day: 25, title: "Client Demo", type: "Project Deadlines", time: "03:00 PM", colorClass: "bg-blue-500/10 text-blue-400 border-blue-500/20" },
- { day: 28, title: "Retrospective", type: "Meetings", time: "04:00 PM", colorClass: "bg-orange-500/10 text-orange-400 border-orange-500/20" },
-];
-
-const daysOfWeek = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
-
-const generateCalendarDays = () => {
- const days = [];
- days.push({ empty: true, key: "empty-0" });
- for (let i = 1; i <= 30; i++) {
- const events = mockEvents.filter((e) => e.day === i);
- days.push({ empty: false, day: i, events, isToday: i === 15 });
- }
- const remaining = 35 - days.length;
- for (let i = 1; i <= remaining; i++) {
- days.push({ empty: true, key: `empty-end-${i}` });
- }
- return days;
+type CalendarEventRow = {
+ id: string;
+ title: string;
+ description: string | null;
+ type: CalendarEventItem["type"];
+ starts_at: string;
+ ends_at: string | null;
+ client_id: string | null;
+ project_id: string | null;
+ task_id: string | null;
+ clients: { name: string } | { name: string }[] | null;
+ projects: { name: string } | { name: string }[] | null;
+ tasks: { title: string } | { title: string }[] | null;
};
-export default function CalendarPage() {
- const [selectedDay, setSelectedDay] = useState(null);
- const [isOptimizing, setIsOptimizing] = useState(false);
- const calendarDays = generateCalendarDays();
+export default async function CalendarPage() {
+ const supabase = await createClient();
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
- const handleOptimize = () => {
- setIsOptimizing(true);
- setTimeout(() => setIsOptimizing(false), 2500);
- };
+ if (!user) {
+ return null;
+ }
+
+ const [{ data: eventRows }, { data: clientRows }, { data: projectRows }, { data: taskRows }] =
+ await Promise.all([
+ supabase
+ .from("calendar_events")
+ .select("id, title, description, type, starts_at, ends_at, client_id, project_id, task_id, clients(name), projects(name), tasks(title)")
+ .eq("user_id", user.id)
+ .order("starts_at", { ascending: true }),
+ supabase
+ .from("clients")
+ .select("id, name")
+ .eq("user_id", user.id)
+ .neq("status", "archived")
+ .order("name", { ascending: true }),
+ supabase
+ .from("projects")
+ .select("id, name")
+ .eq("user_id", user.id)
+ .neq("status", "cancelled")
+ .order("name", { ascending: true }),
+ supabase
+ .from("tasks")
+ .select("id, title")
+ .eq("user_id", user.id)
+ .neq("status", "done")
+ .order("created_at", { ascending: false }),
+ ]);
+
+ const events: CalendarEventItem[] = ((eventRows || []) as unknown as CalendarEventRow[]).map((event) => ({
+ id: event.id,
+ title: event.title,
+ description: event.description,
+ type: normalizeType(event.type),
+ starts_at: event.starts_at,
+ ends_at: event.ends_at,
+ client_id: event.client_id,
+ project_id: event.project_id,
+ task_id: event.task_id,
+ clientName: getRelationName(event.clients),
+ projectName: getRelationName(event.projects),
+ taskTitle: getRelationTitle(event.tasks),
+ }));
return (
-
-
- {/* Optimization Overlay */}
-
- {isOptimizing && (
-
-
-
-
- AI Schedule Optimization
- Analyzing energy trends and focus blocks...
-
-
-
-
- )}
-
-
- {/* Top Header */}
-
-
- Calendar / Monthly View
-
-
-
-
-
-
-
-
-
-
- {/* Main Content Grid */}
-
-
- {/* Left Sidebar Panel */}
-
-
- {/* Mini Calendar Mock */}
-
-
-
-
-
27
- {[...Array(30)].map((_, i) => (
-
- {i + 1}
-
- ))}
-
-
-
- {/* Calendars / Categories */}
-
-
-
My Calendars
-
-
-
- {categories.map((cat, idx) => (
-
- ))}
-
-
-
- {/* AI Optimizer Card */}
-
-
-
-
-
-
-
AI Optimizer
-
-
- Detected focus peak at 10 AM on Thursdays. Suggest moving **UI Review** to maximize alignment.
-
-
-
-
-
-
- {/* Main Calendar View */}
-
-
- {/* Calendar Toolbar */}
-
-
-
May 2026
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Calendar Header */}
-
- {daysOfWeek.map((day) => (
-
- {day}
-
- ))}
-
-
- {/* Calendar Grid */}
-
- {calendarDays.map((cell, idx) => (
-
!cell.empty && setSelectedDay(cell)}
- className={`bg-[#0A0710] p-2 flex flex-col gap-1 transition-all cursor-pointer relative group ${cell.empty ? 'opacity-30 pointer-events-none' : ''}`}
- >
- {!cell.empty && (
-
- {cell.day}
-
- )}
-
-
- {cell.events?.map((event, eIdx) => (
-
- {event.title}
-
- ))}
-
-
- ))}
-
-
-
-
- {/* Quick Entry Sheet */}
-
- {selectedDay && (
- <>
- setSelectedDay(null)}
- className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100]"
- />
-
-
-
-
May {selectedDay.day}, 2026
-
Daily Schedule Overview
-
-
-
-
-
-
-
- Scheduled Events
-
-
- {selectedDay.events?.length > 0 ? (
- selectedDay.events.map((event: any, idx: number) => (
-
-
-
-
-
{event.title}
-
{event.time} • {event.type}
-
-
-
-
- ))
- ) : (
-
No events scheduled for this day.
- )}
-
-
-
-
-
Quick Add Event
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- )}
-
-
-
+
);
}
+
+function getRelationName(relation: CalendarEventRow["clients"] | CalendarEventRow["projects"]) {
+ if (!relation) return null;
+ return Array.isArray(relation) ? relation[0]?.name || null : relation.name;
+}
+
+function getRelationTitle(relation: CalendarEventRow["tasks"]) {
+ if (!relation) return null;
+ return Array.isArray(relation) ? relation[0]?.title || null : relation.title;
+}
+
+function normalizeType(type: string): CalendarEventItem["type"] {
+ if (
+ type === "meeting" ||
+ type === "deadline" ||
+ type === "personal" ||
+ type === "finance"
+ ) {
+ return type;
+ }
+
+ return "focus";
+}