Refactor code structure for improved readability and maintainability

This commit is contained in:
Poyraz Avsever
2026-05-08 11:18:55 +03:00
parent e98972597d
commit 9adc34418e
21 changed files with 1194 additions and 393 deletions
View File
+19
View File
@@ -0,0 +1,19 @@
> mood-tracker-mvp@0.1.0 dev D:\Poyraz\kodlama\Introduction-to-Data-Visualization-Project-Assignment
> next dev "--port" "3010"
▲ Next.js 16.2.6 (Turbopack)
- Local: http://localhost:3010
- Network: http://192.168.0.114:3010
- Environments: .env.local
✓ Ready in 1070ms
Creating turbopack project {
dir: 'D:\\Poyraz\\kodlama\\Introduction-to-Data-Visualization-Project-Assignment',
testMode: true
}
○ Compiling /login ...
GET /login 200 in 8.7s (next.js: 7.6s, proxy.ts: 335ms, application-code: 738ms)
GET /register 200 in 8.4s (next.js: 8.0s, proxy.ts: 10ms, application-code: 395ms)
[?25h
ELIFECYCLE Command failed with exit code 1.
+38 -38
View File
@@ -1,19 +1,19 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { v4 as uuidv4 } from "uuid";
import { useLiveQuery } from "dexie-react-hooks"; import { useLiveQuery } from "dexie-react-hooks";
import { db } from "@/lib/db"; import { v4 as uuidv4 } from "uuid";
import { analyzeJournalWithLocalAI } from "@/lib/ai"; import { analyzeJournalWithLocalAI } from "@/lib/ai";
import { db } from "@/lib/db";
import { Button } from "@/components/ui/button";
import { import {
Card, Card,
CardContent, CardContent,
CardDescription,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription,
CardFooter,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
@@ -31,22 +31,23 @@ export default function JournalPage() {
const [energy, setEnergy] = useState("3"); const [energy, setEnergy] = useState("3");
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
// Dexie.js üzerinden günlükleri tarih sırasına göre çekiyoruz (en yeni en üstte) // Dexie.js üzerinden günlükleri tarih sırasına göre çekiyoruz (en yeni en üstte).
const journals = useLiveQuery(() => const journals = useLiveQuery(() =>
db.journals.orderBy("date").reverse().toArray(), db.journals.orderBy("date").reverse().toArray(),
); );
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (event: React.FormEvent) => {
e.preventDefault(); event.preventDefault();
if (!content.trim()) return; if (!content.trim()) return;
setIsSubmitting(true); setIsSubmitting(true);
const now = new Date().toISOString(); const now = new Date().toISOString();
const entryId = uuidv4(); const entryId = uuidv4();
const currentContent = content; // Analiz için metni kopyala const currentContent = content;
try { try {
// 1. Veriyi veritabanına hemen ekle (Kullanıcı beklemesin) // Veriyi önce kaydet, AI analizini daha sonra arka planda tamamla.
await db.journals.add({ await db.journals.add({
id: entryId, id: entryId,
date: now.split("T")[0], date: now.split("T")[0],
@@ -57,12 +58,10 @@ export default function JournalPage() {
updated_at: now, updated_at: now,
}); });
// UI'ı Sıfırla
setContent(""); setContent("");
setEnergy("3"); setEnergy("3");
setMood("happy"); setMood("happy");
// 2. Arka planda AI Analizi başlat
try { try {
const aiResult = await analyzeJournalWithLocalAI(currentContent); const aiResult = await analyzeJournalWithLocalAI(currentContent);
if (aiResult) { if (aiResult) {
@@ -72,8 +71,7 @@ export default function JournalPage() {
ai_summary: aiResult.ai_summary, ai_summary: aiResult.ai_summary,
}); });
// Eğer AI görev önerdiyse Görevler tablosuna at (pending / onay bekliyor yapısı eklenebilir ama direkt atalım) if (aiResult.suggested_tasks?.length) {
if (aiResult.suggested_tasks && aiResult.suggested_tasks.length > 0) {
for (const taskTitle of aiResult.suggested_tasks) { for (const taskTitle of aiResult.suggested_tasks) {
await db.tasks.add({ await db.tasks.add({
id: uuidv4(), id: uuidv4(),
@@ -87,8 +85,8 @@ export default function JournalPage() {
} }
} }
} }
} catch (aiErr) { } catch (aiError) {
console.error("Arka plan AI analizi hatası:", aiErr); console.error("Arka plan AI analizi hatası:", aiError);
} }
} catch (error) { } catch (error) {
console.error("Günlük kaydedilemedi:", error); console.error("Günlük kaydedilemedi:", error);
@@ -104,7 +102,7 @@ export default function JournalPage() {
}; };
return ( return (
<div className="space-y-6 max-w-4xl mx-auto animate-in fade-in slide-in-from-bottom-4 duration-500"> <div className="mx-auto max-w-4xl animate-in space-y-6 fade-in slide-in-from-bottom-4 duration-500">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Günlük</h1> <h1 className="text-3xl font-bold tracking-tight">Günlük</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
@@ -119,27 +117,28 @@ export default function JournalPage() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="space-y-2"> <div className="space-y-2">
<Label>Ruh Hali</Label> <Label>Ruh Hali</Label>
<div className="flex gap-2"> <div className="flex gap-2">
{moods.map((m) => ( {moods.map((currentMood) => (
<button <button
key={m.id} key={currentMood.id}
type="button" type="button"
onClick={() => setMood(m.id)} onClick={() => setMood(currentMood.id)}
className={`flex-1 py-2 px-1 rounded-md border text-xl flex items-center justify-center transition-colors ${ className={`flex flex-1 items-center justify-center rounded-md border px-1 py-2 text-xl transition-colors ${
mood === m.id mood === currentMood.id
? "bg-primary/20 border-primary" ? "border-primary bg-primary/20"
: "bg-card border-border hover:bg-muted" : "border-border bg-card hover:bg-muted"
}`} }`}
title={m.label} title={currentMood.label}
> >
{m.emoji} {currentMood.emoji}
</button> </button>
))} ))}
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>Enerji Seviyesi (1-5): {energy}</Label> <Label>Enerji Seviyesi (1-5): {energy}</Label>
<Input <Input
@@ -148,7 +147,7 @@ export default function JournalPage() {
max="5" max="5"
step="1" step="1"
value={energy} value={energy}
onChange={(e) => setEnergy(e.target.value)} onChange={(event) => setEnergy(event.target.value)}
className="w-full" className="w-full"
/> />
</div> </div>
@@ -159,7 +158,7 @@ export default function JournalPage() {
<Textarea <Textarea
id="content" id="content"
value={content} value={content}
onChange={(e) => setContent(e.target.value)} onChange={(event) => setContent(event.target.value)}
className="min-h-[150px] resize-y" className="min-h-[150px] resize-y"
placeholder="Örneğin: Bugün toplantıda işler ters gitti..." placeholder="Örneğin: Bugün toplantıda işler ters gitti..."
/> />
@@ -190,13 +189,14 @@ export default function JournalPage() {
<Card key={journal.id}> <Card key={journal.id}>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2"> <CardTitle className="flex items-center gap-2 text-base">
<span> <span>
{moods.find((m) => m.id === journal.mood)?.emoji} {moods.find((currentMood) => currentMood.id === journal.mood)
?.emoji ?? "📝"}
</span> </span>
<span>{journal.date}</span> <span>{journal.date}</span>
</CardTitle> </CardTitle>
<span className="text-xs px-2 py-1 bg-muted rounded-md font-medium text-muted-foreground"> <span className="rounded-md bg-muted px-2 py-1 text-xs font-medium text-muted-foreground">
Enerji: {journal.energy}/5 Enerji: {journal.energy}/5
</span> </span>
</div> </div>
@@ -206,30 +206,30 @@ export default function JournalPage() {
{journal.content} {journal.content}
</p> </p>
{/* AI Sonuçlarını Göster */}
{journal.ai_tags && ( {journal.ai_tags && (
<div className="mt-4 flex flex-wrap gap-2"> <div className="mt-4 flex flex-wrap gap-2">
{journal.ai_tags.map((tag) => ( {journal.ai_tags.map((tag) => (
<span <span
key={tag} key={tag}
className="text-xs px-2 py-1 bg-primary/10 text-primary rounded-md" className="rounded-md bg-primary/10 px-2 py-1 text-xs text-primary"
> >
{tag} {tag}
</span> </span>
))} ))}
</div> </div>
)} )}
{journal.ai_summary && ( {journal.ai_summary && (
<div className="mt-3 p-3 bg-muted/40 rounded-lg border text-sm text-muted-foreground italic border-l-2 border-l-primary"> <div className="mt-3 rounded-lg border border-l-2 border-l-primary bg-muted/40 p-3 text-sm italic text-muted-foreground">
" {journal.ai_summary} " &ldquo;{journal.ai_summary}&rdquo;
</div> </div>
)} )}
<div className="flex justify-end mt-4"> <div className="mt-4 flex justify-end">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-red-500 hover:text-red-600 hover:bg-red-500/10" className="text-red-500 hover:bg-red-500/10 hover:text-red-600"
onClick={() => handleDelete(journal.id)} onClick={() => handleDelete(journal.id)}
> >
Sil Sil
+42 -11
View File
@@ -1,20 +1,51 @@
import { Sidebar } from "@/components/layout/sidebar"; import { DashboardShell } from "@/components/layout/dashboard-shell";
import { Header } from "@/components/layout/header"; import { createClient } from "@/lib/supabase/server";
export default function DashboardLayout({ export default async function DashboardLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
const { data: profile } = user
? await supabase
.from("profiles")
.select("first_name, last_name, avatar_url")
.eq("id", user.id)
.maybeSingle()
: { data: null };
const fallbackName = user?.email?.split("@")[0] ?? "MindSpace Kullanıcısı";
const displayName =
[profile?.first_name, profile?.last_name].filter(Boolean).join(" ") ||
fallbackName;
const shortName = displayName
.split(" ")
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join("")
.slice(0, 2) || "MS";
return ( return (
<div className="flex min-h-screen bg-background text-foreground"> <DashboardShell
<Sidebar /> user={{
<div className="flex flex-col flex-1 h-screen overflow-hidden"> email: user?.email ?? "bilinmiyor@mindspace.local",
<Header /> displayName,
<main className="flex-1 overflow-y-auto p-6 md:p-8"> shortName,
avatarUrl:
profile?.avatar_url ||
user?.user_metadata?.avatar_url ||
user?.user_metadata?.picture ||
null,
}}
>
{children} {children}
</main> </DashboardShell>
</div>
</div>
); );
} }
+78 -89
View File
@@ -1,23 +1,27 @@
"use client"; "use client";
import { useMemo } from "react"; import { useMemo } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Activity, Brain, PenTool, CheckCircle } from "lucide-react";
import { useLiveQuery } from "dexie-react-hooks"; import { useLiveQuery } from "dexie-react-hooks";
import { db } from "@/lib/db"; import { Activity, Brain, CheckCircle, PenTool } from "lucide-react";
import { import {
LineChart, Bar,
BarChart,
CartesianGrid,
Legend,
Line, Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis, XAxis,
YAxis, YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
BarChart,
Bar,
Legend,
} from "recharts"; } from "recharts";
import { db, type Journal, type Task } from "@/lib/db";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
const EMPTY_JOURNALS: Journal[] = [];
const EMPTY_TASKS: Task[] = [];
const moodScores: Record<string, number> = { const moodScores: Record<string, number> = {
happy: 4, happy: 4,
neutral: 3, neutral: 3,
@@ -26,65 +30,71 @@ const moodScores: Record<string, number> = {
}; };
export default function DashboardPage() { export default function DashboardPage() {
const journals = const liveJournals = useLiveQuery<Journal[]>(() =>
useLiveQuery(() => db.journals.orderBy("date").toArray()) || []; db.journals.orderBy("date").toArray(),
const tasks = useLiveQuery(() => db.tasks.toArray()) || []; );
const liveTasks = useLiveQuery<Task[]>(() => db.tasks.toArray());
const pendingTasksCount = tasks.filter((t) => t.status === "todo").length; const journals = liveJournals ?? EMPTY_JOURNALS;
const tasks = liveTasks ?? EMPTY_TASKS;
const pendingTasksCount = tasks.filter((task) => task.status === "todo").length;
// Bugüne ait enerji seviyesi
const today = new Date().toISOString().split("T")[0]; const today = new Date().toISOString().split("T")[0];
const todaysJournal = journals.find((j) => j.date === today); const todaysJournal = journals.find((journal) => journal.date === today);
const todaysEnergy = todaysJournal ? todaysJournal.energy : "--"; const todaysEnergy = todaysJournal ? todaysJournal.energy : "--";
// 1. Grafik: Son 7 günün Ruh Hali ve Enerji trendi
const trendData = useMemo(() => { const trendData = useMemo(() => {
const dataMap: Record< const dataMap: Record<
string, string,
{ date: string; moodSum: number; energySum: number; count: number } { date: string; moodSum: number; energySum: number; count: number }
> = {}; > = {};
journals.forEach((j) => { journals.forEach((journal) => {
if (!dataMap[j.date]) { if (!dataMap[journal.date]) {
dataMap[j.date] = { date: j.date, moodSum: 0, energySum: 0, count: 0 }; dataMap[journal.date] = {
date: journal.date,
moodSum: 0,
energySum: 0,
count: 0,
};
} }
dataMap[j.date].moodSum += moodScores[j.mood] || 3;
dataMap[j.date].energySum += j.energy; dataMap[journal.date].moodSum += moodScores[journal.mood] || 3;
dataMap[j.date].count += 1; dataMap[journal.date].energySum += journal.energy;
dataMap[journal.date].count += 1;
}); });
return Object.values(dataMap) return Object.values(dataMap)
.map((d) => ({ .map((entry) => ({
date: d.date.slice(5), // Sadece MM-DD formatı alalım date: entry.date.slice(5),
Mood: Number((d.moodSum / d.count).toFixed(1)), Mood: Number((entry.moodSum / entry.count).toFixed(1)),
Enerji: Number((d.energySum / d.count).toFixed(1)), Enerji: Number((entry.energySum / entry.count).toFixed(1)),
})) }))
.slice(-7); // Sadece son 7 günü göster .slice(-7);
}, [journals]); }, [journals]);
// 2. Grafik: En çok kullanılan AI etiketleri
const tagData = useMemo(() => { const tagData = useMemo(() => {
const counts: Record<string, number> = {}; const counts: Record<string, number> = {};
journals.forEach((j) => {
if (j.ai_tags) { journals.forEach((journal) => {
j.ai_tags.forEach((tag) => { journal.ai_tags?.forEach((tag) => {
counts[tag] = (counts[tag] || 0) + 1; counts[tag] = (counts[tag] || 0) + 1;
}); });
}
}); });
return Object.entries(counts) return Object.entries(counts)
.map(([name, value]) => ({ name, Değer: value })) .map(([name, value]) => ({ name, Değer: value }))
.sort((a, b) => b.Değer - a.Değer) .sort((a, b) => b.Değer - a.Değer)
.slice(0, 5); // En çok geçen 5 etiket .slice(0, 5);
}, [journals]); }, [journals]);
// Son günlüğe ait AI Summary
const lastInsight = [...journals] const lastInsight = [...journals]
.reverse() .reverse()
.find((j) => j.ai_summary)?.ai_summary; .find((journal) => journal.ai_summary)?.ai_summary;
return ( return (
<div className="space-y-6 max-w-6xl mx-auto animate-in fade-in slide-in-from-bottom-4 duration-500"> <div className="mx-auto max-w-6xl animate-in space-y-6 fade-in slide-in-from-bottom-4 duration-500">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h1 className="text-3xl font-bold tracking-tight">Bugün Nasılsın?</h1> <h1 className="text-3xl font-bold tracking-tight">Bugün Nasılsın?</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
@@ -94,15 +104,13 @@ export default function DashboardPage() {
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> <CardTitle className="text-sm font-medium">Günlük Kayıtları</CardTitle>
Günlük Kayıtları
</CardTitle>
<PenTool className="h-4 w-4 text-muted-foreground" /> <PenTool className="h-4 w-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{journals.length}</div> <div className="text-2xl font-bold">{journals.length}</div>
<p className="text-xs text-muted-foreground mt-1"> <p className="mt-1 text-xs text-muted-foreground">
{journals.length === 0 {journals.length === 0
? "Henüz kayıt girilmedi" ? "Henüz kayıt girilmedi"
: "Toplam kayıt eklendi"} : "Toplam kayıt eklendi"}
@@ -111,43 +119,37 @@ export default function DashboardPage() {
</Card> </Card>
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> <CardTitle className="text-sm font-medium">Aktif Görevler</CardTitle>
Aktif Görevler
</CardTitle>
<CheckCircle className="h-4 w-4 text-muted-foreground" /> <CheckCircle className="h-4 w-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{pendingTasksCount}</div> <div className="text-2xl font-bold">{pendingTasksCount}</div>
<p className="text-xs text-muted-foreground mt-1">Bekleyen görev</p> <p className="mt-1 text-xs text-muted-foreground">Bekleyen görev</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> <CardTitle className="text-sm font-medium">Bugünkü Enerji</CardTitle>
Bugünkü Enerji
</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" /> <Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{todaysEnergy}</div> <div className="text-2xl font-bold">{todaysEnergy}</div>
<p className="text-xs text-muted-foreground mt-1"> <p className="mt-1 text-xs text-muted-foreground">
{todaysJournal ? "/ 5 Seviyesinde" : "Kayıt bekleniyor"} {todaysJournal ? "/ 5 seviyesinde" : "Kayıt bekleniyor"}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> <CardTitle className="text-sm font-medium">Local AI Durumu</CardTitle>
Local AI Durumu
</CardTitle>
<Brain className="h-4 w-4 text-muted-foreground" /> <Brain className="h-4 w-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold text-green-500">Hazır</div> <div className="text-2xl font-bold text-green-500">Hazır</div>
<p className="text-xs text-muted-foreground mt-1"> <p className="mt-1 text-xs text-muted-foreground">
Ollama / Veri bekliyor Ollama / Veri bekliyor
</p> </p>
</CardContent> </CardContent>
@@ -155,14 +157,11 @@ export default function DashboardPage() {
</div> </div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
{/* Line Chart */} <Card className="flex flex-col justify-between p-6 lg:col-span-4">
<Card className="lg:col-span-4 p-6 flex flex-col justify-between">
<div className="mb-4"> <div className="mb-4">
<h3 className="text-lg font-medium mb-1"> <h3 className="mb-1 text-lg font-medium">Ruh Hali ve Enerji Trendi</h3>
Ruh Hali & Enerji Trendi
</h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Son 7 günlük ortalamalar (1-5 Arası Puanlama) Son 7 günlük ortalamalar (1-5 arası puanlama)
</p> </p>
</div> </div>
<div className="h-[250px] w-full"> <div className="h-[250px] w-full">
@@ -196,9 +195,7 @@ export default function DashboardPage() {
}} }}
itemStyle={{ fontWeight: "500" }} itemStyle={{ fontWeight: "500" }}
/> />
<Legend <Legend wrapperStyle={{ paddingTop: "10px", fontSize: "14px" }} />
wrapperStyle={{ paddingTop: "10px", fontSize: "14px" }}
/>
<Line <Line
type="monotone" type="monotone"
dataKey="Mood" dataKey="Mood"
@@ -218,32 +215,25 @@ export default function DashboardPage() {
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
) : ( ) : (
<div className="h-full flex items-center justify-center border-2 border-dashed rounded-md bg-muted/20"> <div className="flex h-full items-center justify-center rounded-md border-2 border-dashed bg-muted/20">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">Veri bekleniyor...</p>
Veri bekleniyor...
</p>
</div> </div>
)} )}
</div> </div>
</Card> </Card>
{/* Bar Chart & Insight */} <div className="flex flex-col space-y-4 lg:col-span-3">
<div className="lg:col-span-3 space-y-4 flex flex-col"> <Card className="flex flex-1 flex-col p-6">
<Card className="p-6 flex-1 flex flex-col">
<div className="mb-4"> <div className="mb-4">
<h3 className="text-lg font-medium mb-1">AI Konu Dağılımı</h3> <h3 className="mb-1 text-lg font-medium">AI Konu Dağılımı</h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Günlüklerinden çıkarılan en sık 5 etiket Günlüklerinden çıkarılan en sık 5 etiket
</p> </p>
</div> </div>
<div className="h-[150px] w-full mt-auto"> <div className="mt-auto h-[150px] w-full">
{tagData.length > 0 ? ( {tagData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<BarChart <BarChart data={tagData} layout="vertical" margin={{ left: -20 }}>
data={tagData}
layout="vertical"
margin={{ left: -20 }}
>
<CartesianGrid <CartesianGrid
strokeDasharray="3 3" strokeDasharray="3 3"
horizontal={false} horizontal={false}
@@ -276,20 +266,19 @@ export default function DashboardPage() {
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
) : ( ) : (
<div className="h-full flex items-center justify-center border-2 border-dashed rounded-md bg-muted/20"> <div className="flex h-full items-center justify-center rounded-md border-2 border-dashed bg-muted/20">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">Yeterli etiket yok.</p>
Yeterli etiket yok.
</p>
</div> </div>
)} )}
</div> </div>
</Card> </Card>
<Card className="p-6 bg-primary/5 border-primary/20 flex-1"> <Card className="flex-1 border-primary/20 bg-primary/5 p-6">
<h3 className="text-lg font-medium mb-3 flex items-center gap-2"> <h3 className="mb-3 flex items-center gap-2 text-lg font-medium">
<Brain className="w-5 h-5 text-primary" /> Son AI İçgörüsü <Brain className="h-5 w-5 text-primary" />
Son AI İçgörüsü
</h3> </h3>
<p className="text-sm text-foreground/80 leading-relaxed italic"> <p className="text-sm italic leading-relaxed text-foreground/80">
{lastInsight {lastInsight
? `"${lastInsight}"` ? `"${lastInsight}"`
: "Henüz bir içgörü oluşmadı. Biraz günlük yaz, AI analiz yapsın."} : "Henüz bir içgörü oluşmadı. Biraz günlük yaz, AI analiz yapsın."}
+28 -21
View File
@@ -1,12 +1,22 @@
'use server' 'use server'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache' import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'
type ProfileUpdateData = {
first_name: string
last_name: string
avatar_url?: string
}
export async function updateProfile(formData: FormData) { export async function updateProfile(formData: FormData) {
const supabase = await createClient() const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser() const {
data: { user },
} = await supabase.auth.getUser()
if (!user) { if (!user) {
return { error: 'Kullanıcı bulunamadı.' } return { error: 'Kullanıcı bulunamadı.' }
} }
@@ -15,31 +25,30 @@ export async function updateProfile(formData: FormData) {
const lastName = formData.get('lastName') as string const lastName = formData.get('lastName') as string
const avatarFile = formData.get('avatar') as File | null const avatarFile = formData.get('avatar') as File | null
let avatarUrl = undefined let avatarUrl: string | undefined
// Upload avatar if a new file is provided
if (avatarFile && avatarFile.size > 0) { if (avatarFile && avatarFile.size > 0) {
const fileExt = avatarFile.name.split('.').pop() const fileExt = avatarFile.name.split('.').pop()
const fileName = `${user.id}/${Math.random()}.${fileExt}` const fileName = `${user.id}/${Math.random()}.${fileExt}`
const { error: uploadError, data: uploadData } = await supabase.storage const { error: uploadError } = await supabase.storage
.from('avatars') .from('avatars')
.upload(fileName, avatarFile, { upsert: true }) .upload(fileName, avatarFile, { upsert: true })
if (uploadError) { if (uploadError) {
return { error: 'Profil fotoğrafı yüklenirken hata oluştu: ' + uploadError.message } return {
error: `Profil fotoğrafı yüklenirken hata oluştu: ${uploadError.message}`,
}
} }
// Get public URL const {
const { data: { publicUrl } } = supabase.storage data: { publicUrl },
.from('avatars') } = supabase.storage.from('avatars').getPublicUrl(fileName)
.getPublicUrl(fileName)
avatarUrl = publicUrl avatarUrl = publicUrl
} }
// Update profile const updateData: ProfileUpdateData = {
const updateData: any = {
first_name: firstName, first_name: firstName,
last_name: lastName, last_name: lastName,
} }
@@ -48,13 +57,13 @@ export async function updateProfile(formData: FormData) {
updateData.avatar_url = avatarUrl updateData.avatar_url = avatarUrl
} }
// Upsert profile in case it doesn't exist yet const { error } = await supabase.from('profiles').upsert({
const { error } = await supabase id: user.id,
.from('profiles') ...updateData,
.upsert({ id: user.id, ...updateData }) })
if (error) { if (error) {
return { error: 'Profil güncellenirken hata oluştu: ' + error.message } return { error: `Profil güncellenirken hata oluştu: ${error.message}` }
} }
revalidatePath('/settings') revalidatePath('/settings')
@@ -69,12 +78,10 @@ export async function updatePassword(formData: FormData) {
return { error: 'Şifre en az 6 karakter olmalıdır.' } return { error: 'Şifre en az 6 karakter olmalıdır.' }
} }
const { error } = await supabase.auth.updateUser({ const { error } = await supabase.auth.updateUser({ password })
password: password
})
if (error) { if (error) {
return { error: 'Şifre güncellenirken hata oluştu: ' + error.message } return { error: `Şifre güncellenirken hata oluştu: ${error.message}` }
} }
return { success: true } return { success: true }
+150 -71
View File
@@ -1,114 +1,161 @@
"use client"; "use client";
import { useState, useEffect, useRef } from "react"; import { useEffect, useRef, useState } from "react";
import { Bot, KeyRound, Save, User } from "lucide-react";
import { updatePassword, updateProfile } from "./actions";
import { createClient } from "@/lib/supabase/client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
Select, Select,
SelectTrigger,
SelectValue,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Bot, Save, User, KeyRound } from "lucide-react";
import { createClient } from "@/lib/supabase/client"; type AiProvider = "groq" | "ollama" | "openai";
import { updateProfile, updatePassword } from "./actions";
function getInitialAiProvider(): AiProvider {
if (typeof window === "undefined") {
return "groq";
}
const savedProvider = localStorage.getItem("mindspace_ai_provider");
return savedProvider === "ollama" || savedProvider === "openai"
? savedProvider
: "groq";
}
function getInitialApiKey() {
if (typeof window === "undefined") {
return "";
}
return localStorage.getItem("mindspace_api_key") ?? "";
}
export default function SettingsPage() { export default function SettingsPage() {
const [aiProvider, setAiProvider] = useState("groq"); const [aiProvider, setAiProvider] = useState<AiProvider>(getInitialAiProvider);
const [apiKey, setApiKey] = useState(""); const [apiKey, setApiKey] = useState(getInitialApiKey);
const [saveStatus, setSaveStatus] = useState(""); const [saveStatus, setSaveStatus] = useState("");
// Profile state
const [firstName, setFirstName] = useState(""); const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState(""); const [lastName, setLastName] = useState("");
const [avatarUrl, setAvatarUrl] = useState(""); const [avatarUrl, setAvatarUrl] = useState("");
const [profileSaveStatus, setProfileSaveStatus] = useState(""); const [profileSaveStatus, setProfileSaveStatus] = useState("");
const [passwordSaveStatus, setPasswordSaveStatus] = useState(""); const [passwordSaveStatus, setPasswordSaveStatus] = useState("");
const [supabase] = useState(() => createClient());
const formRef = useRef<HTMLFormElement>(null); const formRef = useRef<HTMLFormElement>(null);
const supabase = createClient();
useEffect(() => { useEffect(() => {
const savedProvider = localStorage.getItem("mindspace_ai_provider"); let isActive = true;
const savedApiKey = localStorage.getItem("mindspace_api_key");
if (savedProvider) setAiProvider(savedProvider); const fetchProfile = async () => {
if (savedApiKey) setApiKey(savedApiKey); const {
data: { user },
} = await supabase.auth.getUser();
if (!user || !isActive) {
return;
}
const { data } = await supabase
.from("profiles")
.select("*")
.eq("id", user.id)
.single();
if (!data || !isActive) {
return;
}
// Fetch user profile
async function fetchProfile() {
const { data: { user } } = await supabase.auth.getUser();
if (user) {
const { data } = await supabase.from("profiles").select("*").eq("id", user.id).single();
if (data) {
setFirstName(data.first_name || ""); setFirstName(data.first_name || "");
setLastName(data.last_name || ""); setLastName(data.last_name || "");
setAvatarUrl(data.avatar_url || ""); setAvatarUrl(data.avatar_url || "");
} };
}
} void fetchProfile();
fetchProfile();
}, []); return () => {
isActive = false;
};
}, [supabase]);
const handleSaveAI = () => { const handleSaveAI = () => {
localStorage.setItem("mindspace_ai_provider", aiProvider); localStorage.setItem("mindspace_ai_provider", aiProvider);
localStorage.setItem("mindspace_api_key", apiKey); localStorage.setItem("mindspace_api_key", apiKey);
setSaveStatus("Ayarlar başarıyla kaydedildi!"); setSaveStatus("Ayarlar başarıyla kaydedildi!");
setTimeout(() => setSaveStatus(""), 3000); window.setTimeout(() => setSaveStatus(""), 3000);
}; };
const handleProfileAction = async (formData: FormData) => { const handleProfileAction = async (formData: FormData) => {
const res = await updateProfile(formData); const response = await updateProfile(formData);
if (res?.error) {
setProfileSaveStatus("Hata: " + res.error); if (response?.error) {
setProfileSaveStatus(`Hata: ${response.error}`);
} else { } else {
setProfileSaveStatus("Profil başarıyla güncellendi!"); setProfileSaveStatus("Profil başarıyla güncellendi!");
if (formData.get("avatar") && (formData.get("avatar") as File).size > 0) { const avatar = formData.get("avatar");
if (avatar instanceof File && avatar.size > 0) {
window.location.reload(); window.location.reload();
} }
} }
setTimeout(() => setProfileSaveStatus(""), 3000);
window.setTimeout(() => setProfileSaveStatus(""), 3000);
}; };
const handlePasswordAction = async (formData: FormData) => { const handlePasswordAction = async (formData: FormData) => {
const res = await updatePassword(formData); const response = await updatePassword(formData);
if (res?.error) {
setPasswordSaveStatus("Hata: " + res.error); if (response?.error) {
setPasswordSaveStatus(`Hata: ${response.error}`);
} else { } else {
setPasswordSaveStatus("Şifre başarıyla güncellendi!"); setPasswordSaveStatus("Şifre başarıyla güncellendi!");
formRef.current?.reset(); formRef.current?.reset();
} }
setTimeout(() => setPasswordSaveStatus(""), 3000);
window.setTimeout(() => setPasswordSaveStatus(""), 3000);
}; };
return ( return (
<div className="flex flex-col gap-6 p-4 max-w-2xl mx-auto w-full"> <div className="mx-auto flex w-full max-w-2xl flex-col gap-6 p-4">
<div> <div>
<h1 className="text-3xl font-bold text-foreground">Ayarlar</h1> <h1 className="text-3xl font-bold text-foreground">Ayarlar</h1>
<p className="text-muted-foreground mt-1"> <p className="mt-1 text-muted-foreground">
Kullanıcı profili ve yapay zeka asistanı yapılandırmanızı yönetin. Kullanıcı profili ve yapay zeka asistanı yapılandırmanızı yönetin.
</p> </p>
</div> </div>
{/* Profile Settings */} <div className="flex flex-col space-y-6 rounded-xl border border-border bg-card p-6 shadow-sm">
<div className="bg-card border border-border rounded-xl p-6 shadow-sm space-y-6 flex flex-col">
<div className="flex items-center gap-3 border-b border-border/50 pb-4"> <div className="flex items-center gap-3 border-b border-border/50 pb-4">
<User className="w-6 h-6 text-primary" /> <User className="h-6 w-6 text-primary" />
<h2 className="text-xl font-semibold">Kullanıcı Profili</h2> <h2 className="text-xl font-semibold">Kullanıcı Profili</h2>
</div> </div>
<form action={handleProfileAction} className="space-y-4"> <form action={handleProfileAction} className="space-y-4">
<div className="flex items-center gap-4 mb-6"> <div className="mb-6 flex items-center gap-4">
{avatarUrl ? ( {avatarUrl ? (
<img src={avatarUrl} alt="Avatar" className="w-16 h-16 rounded-full object-cover border border-border" /> <>
{/* Avatar URL dış kaynaklı olduğu için bu önizlemede native img kullanıyoruz. */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={avatarUrl}
alt="Avatar"
className="h-16 w-16 rounded-full border border-border object-cover"
/>
</>
) : ( ) : (
<div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center border border-border"> <div className="flex h-16 w-16 items-center justify-center rounded-full border border-border bg-muted">
<User className="w-8 h-8 text-muted-foreground" /> <User className="h-8 w-8 text-muted-foreground" />
</div> </div>
)} )}
<div className="space-y-1 flex-1">
<div className="flex-1 space-y-1">
<Label htmlFor="avatar">Profil Fotoğrafı Yükle</Label> <Label htmlFor="avatar">Profil Fotoğrafı Yükle</Label>
<Input id="avatar" name="avatar" type="file" accept="image/*" /> <Input id="avatar" name="avatar" type="file" accept="image/*" />
</div> </div>
@@ -117,21 +164,38 @@ export default function SettingsPage() {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="firstName">Ad</Label> <Label htmlFor="firstName">Ad</Label>
<Input id="firstName" name="firstName" value={firstName} onChange={(e) => setFirstName(e.target.value)} /> <Input
id="firstName"
name="firstName"
value={firstName}
onChange={(event) => setFirstName(event.target.value)}
/>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="lastName">Soyad</Label> <Label htmlFor="lastName">Soyad</Label>
<Input id="lastName" name="lastName" value={lastName} onChange={(e) => setLastName(e.target.value)} /> <Input
id="lastName"
name="lastName"
value={lastName}
onChange={(event) => setLastName(event.target.value)}
/>
</div> </div>
</div> </div>
<div className="flex items-center gap-4 mt-4"> <div className="mt-4 flex items-center gap-4">
<Button type="submit" className="w-max"> <Button type="submit" className="w-max">
<Save className="w-4 h-4 mr-2" /> <Save className="mr-2 h-4 w-4" />
Profili Kaydet Profili Kaydet
</Button> </Button>
{profileSaveStatus && ( {profileSaveStatus && (
<span className={`text-sm ${profileSaveStatus.startsWith("Hata") ? "text-red-500" : "text-green-600 dark:text-green-400"}`}> <span
className={`text-sm ${
profileSaveStatus.startsWith("Hata")
? "text-red-500"
: "text-green-600 dark:text-green-400"
}`}
>
{profileSaveStatus} {profileSaveStatus}
</span> </span>
)} )}
@@ -139,26 +203,38 @@ export default function SettingsPage() {
</form> </form>
</div> </div>
{/* Password Settings */} <div className="flex flex-col space-y-6 rounded-xl border border-border bg-card p-6 shadow-sm">
<div className="bg-card border border-border rounded-xl p-6 shadow-sm space-y-6 flex flex-col">
<div className="flex items-center gap-3 border-b border-border/50 pb-4"> <div className="flex items-center gap-3 border-b border-border/50 pb-4">
<KeyRound className="w-6 h-6 text-primary" /> <KeyRound className="h-6 w-6 text-primary" />
<h2 className="text-xl font-semibold">Şifre Değiştir</h2> <h2 className="text-xl font-semibold">Şifre Değiştir</h2>
</div> </div>
<form ref={formRef} action={handlePasswordAction} className="space-y-4"> <form ref={formRef} action={handlePasswordAction} className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="password">Yeni Şifre</Label> <Label htmlFor="password">Yeni Şifre</Label>
<Input id="password" name="password" type="password" minLength={6} placeholder="En az 6 karakter" required /> <Input
id="password"
name="password"
type="password"
minLength={6}
placeholder="En az 6 karakter"
required
/>
</div> </div>
<div className="flex items-center gap-4 mt-4"> <div className="mt-4 flex items-center gap-4">
<Button type="submit" className="w-max"> <Button type="submit" className="w-max">
<Save className="w-4 h-4 mr-2" /> <Save className="mr-2 h-4 w-4" />
Şifreyi Güncelle Şifreyi Güncelle
</Button> </Button>
{passwordSaveStatus && ( {passwordSaveStatus && (
<span className={`text-sm ${passwordSaveStatus.startsWith("Hata") ? "text-red-500" : "text-green-600 dark:text-green-400"}`}> <span
className={`text-sm ${
passwordSaveStatus.startsWith("Hata")
? "text-red-500"
: "text-green-600 dark:text-green-400"
}`}
>
{passwordSaveStatus} {passwordSaveStatus}
</span> </span>
)} )}
@@ -166,31 +242,34 @@ export default function SettingsPage() {
</form> </form>
</div> </div>
{/* AI Settings */} <div className="flex flex-col space-y-6 rounded-xl border border-border bg-card p-6 shadow-sm">
<div className="bg-card border border-border rounded-xl p-6 shadow-sm space-y-6 flex flex-col">
<div className="flex items-center gap-3 border-b border-border/50 pb-4"> <div className="flex items-center gap-3 border-b border-border/50 pb-4">
<Bot className="w-6 h-6 text-primary" /> <Bot className="h-6 w-6 text-primary" />
<h2 className="text-xl font-semibold">Terapist (AI) Ayarları</h2> <h2 className="text-xl font-semibold">Terapist (AI) Ayarları</h2>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>AI Sağlayıcısı</Label> <Label>AI Sağlayıcısı</Label>
<Select value={aiProvider} onValueChange={setAiProvider}> <Select
value={aiProvider}
onValueChange={(value) => setAiProvider(value as AiProvider)}
>
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
<SelectValue placeholder="Sağlayıcı seçin" /> <SelectValue placeholder="Sağlayıcı seçin" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="ollama"> <SelectItem value="ollama">
Ollama (Yerel & Gizlilik Odaklı) Ollama (Yerel ve Gizlilik Odaklı)
</SelectItem> </SelectItem>
<SelectItem value="openai">OpenAI (GPT-4o vb.)</SelectItem> <SelectItem value="openai">OpenAI (GPT-4o vb.)</SelectItem>
<SelectItem value="groq">Groq (Llama-3 Bulut)</SelectItem> <SelectItem value="groq">Groq (Llama 3 Bulut)</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Gizliliğiniz için yerel Ollama önerilir. Sunucunuzda çalışmayan Gizliliğiniz için yerel Ollama önerilir. Sunucunuzda çalışmayan
durumlarda OpenAI veya Groq gibi bulut çözümlerine geçebilirsiniz. durumlarda OpenAI veya Groq gibi bulut çözümlerine
geçebilirsiniz.
</p> </p>
</div> </div>
@@ -200,19 +279,19 @@ export default function SettingsPage() {
<Input <Input
type="password" type="password"
value={apiKey} value={apiKey}
onChange={(e) => setApiKey(e.target.value)} onChange={(event) => setApiKey(event.target.value)}
placeholder="sk-..." placeholder="sk-..."
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Anahtarınız sadece tarayıcınızın kendi local hafızasında güvenle Anahtarınız sadece tarayıcınızın kendi local hafızasında saklanır;
saklanır, hiçbir sunucuya kaydedilmez. herhangi bir sunucuya kaydedilmez.
</p> </p>
</div> </div>
)} )}
<div className="flex items-center gap-4 mt-4"> <div className="mt-4 flex items-center gap-4">
<Button onClick={handleSaveAI} className="w-max"> <Button type="button" onClick={handleSaveAI} className="w-max">
<Save className="w-4 h-4 mr-2" /> <Save className="mr-2 h-4 w-4" />
Kaydet Kaydet
</Button> </Button>
{saveStatus && ( {saveStatus && (
+73 -24
View File
@@ -1,14 +1,53 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
type ChatProvider = "groq" | "openai" | "ollama";
type ChatRequestBody = {
provider?: ChatProvider;
apiKey?: string;
userMessageContent?: string;
};
type ProviderErrorResponse = {
error?: {
message?: string;
};
};
type ChatCompletionResponse = {
choices?: Array<{
message?: {
content?: string;
};
}>;
};
type OllamaResponse = {
response?: string;
};
function getErrorMessage(error: unknown) {
return error instanceof Error ? error.message : "Bilinmeyen sunucu hatası";
}
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
const body = await request.json(); const body = (await request.json()) as ChatRequestBody;
const { provider, apiKey, userMessageContent } = body; const provider = body.provider ?? "ollama";
const apiKey = body.apiKey ?? "";
const userMessageContent = body.userMessageContent?.trim();
if (!userMessageContent) {
return NextResponse.json(
{ error: "Mesaj içeriği boş olamaz." },
{ status: 400 },
);
}
let assistantReply = ""; let assistantReply = "";
if (provider === "groq") { if (provider === "groq") {
const res = await fetch( const response = await fetch(
"https://api.groq.com/openai/v1/chat/completions", "https://api.groq.com/openai/v1/chat/completions",
{ {
method: "POST", method: "POST",
@@ -30,14 +69,15 @@ export async function POST(request: Request) {
}, },
); );
if (!res.ok) { if (!response.ok) {
const errorData = await res.json(); const errorData = (await response.json()) as ProviderErrorResponse;
throw new Error(errorData.error?.message || "Groq API Hatası"); throw new Error(errorData.error?.message || "Groq API hatası");
} }
const data = await res.json();
assistantReply = data.choices[0].message.content; const data = (await response.json()) as ChatCompletionResponse;
assistantReply = data.choices?.[0]?.message?.content ?? "";
} else if (provider === "openai") { } else if (provider === "openai") {
const res = await fetch("https://api.openai.com/v1/chat/completions", { const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -55,32 +95,41 @@ export async function POST(request: Request) {
], ],
}), }),
}); });
if (!res.ok) throw new Error("OpenAI API Hatası");
const data = await res.json(); if (!response.ok) {
assistantReply = data.choices[0].message.content; throw new Error("OpenAI API hatası");
}
const data = (await response.json()) as ChatCompletionResponse;
assistantReply = data.choices?.[0]?.message?.content ?? "";
} else { } else {
// Varsayılan: Yerel Ollama const response = await fetch("http://127.0.0.1:11434/api/generate", {
const res = await fetch("http://127.0.0.1:11434/api/generate", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
model: "llama3", // veya mistral model: "llama3",
prompt: `Sen MindSpace adlı kullanıcının kişisel yapay zeka terapistisin ve sırdaşısın. Şefkatli ve destekleyici cevap ver.\n\nKullanıcı: ${userMessageContent}\nTerapist:`, prompt: `Sen MindSpace adlı kullanıcının kişisel yapay zeka terapistisin ve sırdaşısın. Şefkatli ve destekleyici cevap ver.\n\nKullanıcı: ${userMessageContent}\nTerapist:`,
stream: false, stream: false,
}), }),
}); });
if (!res.ok) throw new Error("Ollama API yanıt vermedi."); if (!response.ok) {
const data = await res.json(); throw new Error("Ollama API yanıt vermedi.");
assistantReply = data.response; }
const data = (await response.json()) as OllamaResponse;
assistantReply = data.response ?? "";
}
if (!assistantReply) {
throw new Error("Model geçerli bir yanıt üretmedi.");
} }
return NextResponse.json({ reply: assistantReply }); return NextResponse.json({ reply: assistantReply });
} catch (error: any) { } catch (error: unknown) {
console.error("API Route Hatası:", error); const message = getErrorMessage(error);
return NextResponse.json( console.error("API route hatası:", error);
{ error: error.message || "Bilinmeyen Sunucu Hatası" },
{ status: 500 }, return NextResponse.json({ error: message }, { status: 500 });
);
} }
} }
+9
View File
@@ -39,3 +39,12 @@ export async function signup(formData: FormData) {
revalidatePath('/', 'layout') revalidatePath('/', 'layout')
redirect('/') redirect('/')
} }
export async function signOut() {
const supabase = await createClient()
await supabase.auth.signOut()
revalidatePath('/', 'layout')
redirect('/login')
}
+545
View File
@@ -0,0 +1,545 @@
"use client";
import { signOut } from "@/app/login/actions";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "framer-motion";
import {
BookOpenText,
CheckSquare2,
ChevronRight,
LogOut,
Menu,
MessageCircleHeart,
PanelLeftClose,
Settings2,
Sparkles,
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useMemo, useState } from "react";
type DashboardShellProps = {
children: React.ReactNode;
user: {
email: string;
displayName: string;
shortName: string;
avatarUrl: string | null;
};
};
type RouteMeta = {
eyebrow: string;
title: string;
description: string;
};
const routes = [
{
label: "Dashboard",
href: "/",
icon: Sparkles,
description: "Günün ritmi ve genel görünüm",
},
{
label: "Günlük",
href: "/journal",
icon: BookOpenText,
description: "Düşünceler, duygu ve enerji kayıtları",
},
{
label: "Görevler",
href: "/tasks",
icon: CheckSquare2,
description: "Planlar, öncelikler ve aksiyonlar",
},
{
label: "Sohbet",
href: "/chat",
icon: MessageCircleHeart,
description: "AI ile bağlamsal yansıma alanı",
},
{
label: "Ayarlar",
href: "/settings",
icon: Settings2,
description: "Profil, güvenlik ve AI tercihleri",
},
] as const;
const routeMeta: Record<string, RouteMeta> = {
"/": {
eyebrow: "MindSpace Workspace",
title: "Günün Nabzı",
description: "Verilerini, odağını ve kişisel akışını tek panelden yönet.",
},
"/journal": {
eyebrow: "Journal",
title: "Düşünce Akışı",
description: "Bugünün zihinsel yükünü yaz, duygu ve enerji izlerini kaydet.",
},
"/tasks": {
eyebrow: "Tasks",
title: "Aksiyon Katmanı",
description: "Önceliklerini sadeleştir, küçük ama net adımlar belirle.",
},
"/chat": {
eyebrow: "Reflective AI",
title: "Yansıtıcı Sohbet",
description: "Geçmiş kayıtlarınla bağ kuran özel AI eşlikçin burada.",
},
"/settings": {
eyebrow: "Preferences",
title: "Hesap ve Kontroller",
description: "Profilini, güvenliği ve AI ayarlarını tek yerden yönet.",
},
};
export function DashboardShell({ children, user }: DashboardShellProps) {
const pathname = usePathname();
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
const closeMobileSidebar = () => setIsMobileSidebarOpen(false);
const currentMeta = useMemo(() => {
if (pathname === "/") return routeMeta["/"];
if (pathname?.startsWith("/journal")) return routeMeta["/journal"];
if (pathname?.startsWith("/tasks")) return routeMeta["/tasks"];
if (pathname?.startsWith("/chat")) return routeMeta["/chat"];
if (pathname?.startsWith("/settings")) return routeMeta["/settings"];
return routeMeta["/"];
}, [pathname]);
const todayLabel = useMemo(
() =>
new Intl.DateTimeFormat("tr-TR", {
weekday: "long",
day: "numeric",
month: "long",
}).format(new Date()),
[],
);
return (
<div className="relative min-h-screen overflow-hidden bg-background text-foreground">
<AmbientBackdrop />
<div className="relative z-10 flex min-h-screen">
<div className="hidden shrink-0 lg:block lg:w-[320px]">
<div className="sticky top-0 h-screen p-4 pr-0">
<SidebarPanel pathname={pathname} user={user} desktop />
</div>
</div>
<AnimatePresence>
{isMobileSidebarOpen ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex lg:hidden"
>
<button
type="button"
aria-label="Menüyü kapat"
className="absolute inset-0 bg-background/75 backdrop-blur-sm"
onClick={closeMobileSidebar}
/>
<motion.div
initial={{ x: -48, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: -48, opacity: 0 }}
transition={{ duration: 0.28, ease: [0.22, 1, 0.36, 1] }}
className="relative z-10 h-full w-[min(88vw,340px)] p-4 pr-2"
>
<SidebarPanel
pathname={pathname}
user={user}
onNavigate={closeMobileSidebar}
/>
</motion.div>
</motion.div>
) : null}
</AnimatePresence>
<div className="flex min-h-screen min-w-0 flex-1 flex-col">
<header className="sticky top-0 z-30 px-4 pb-3 pt-4 md:px-6 lg:px-8">
<TopBar
user={user}
currentMeta={currentMeta}
todayLabel={todayLabel}
onOpenSidebar={() => setIsMobileSidebarOpen(true)}
/>
</header>
<main className="flex-1 px-4 pb-6 md:px-6 md:pb-8 lg:px-8">
<div className="min-h-[calc(100vh-7rem)] rounded-[28px] border border-white/8 bg-[linear-gradient(180deg,rgba(31,23,43,0.84),rgba(18,13,28,0.72))] shadow-[0_30px_120px_rgba(7,5,14,0.45)] ring-1 ring-white/6 backdrop-blur-xl">
<div className="h-full p-4 md:p-6 lg:p-8">{children}</div>
</div>
</main>
</div>
</div>
</div>
);
}
function SidebarPanel({
pathname,
user,
desktop = false,
onNavigate,
}: {
pathname: string;
user: DashboardShellProps["user"];
desktop?: boolean;
onNavigate?: () => void;
}) {
return (
<aside className="flex h-full flex-col overflow-hidden rounded-[30px] border border-white/10 bg-[linear-gradient(180deg,rgba(31,23,43,0.96),rgba(18,13,28,0.88))] shadow-[0_24px_100px_rgba(7,5,14,0.52)] ring-1 ring-white/6 backdrop-blur-xl">
<div className="relative overflow-hidden border-b border-white/8 px-5 pb-5 pt-6">
<div className="absolute -left-8 top-0 h-24 w-24 rounded-full bg-primary/20 blur-3xl" />
<div className="absolute right-0 top-8 h-16 w-16 rounded-full bg-white/10 blur-2xl" />
<Link href="/" className="relative flex items-center gap-4" onClick={onNavigate}>
<div className="relative flex h-14 w-14 items-center justify-center rounded-2xl border border-white/10 bg-white/5 shadow-[0_12px_40px_rgba(0,0,0,0.22)]">
<Image
src="/logo/logo.png"
alt="MindSpace Logo"
width={40}
height={40}
className="h-10 w-10 object-contain"
priority
/>
</div>
<div className="min-w-0">
<div className="text-[11px] font-medium uppercase tracking-[0.28em] text-primary/80">
Private Space
</div>
<div className="truncate text-xl font-semibold tracking-tight text-foreground">
MindSpace
</div>
<div className="text-sm text-muted-foreground">
Dingin, güvenli ve odaklı çalışma alanı
</div>
</div>
</Link>
</div>
<div className="px-4 pt-4">
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3 text-sm text-muted-foreground shadow-[inset_0_1px_0_rgba(255,255,255,0.05)]">
<div className="mb-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-primary/70">
Focus Layer
</div>
Yaz, gözlemle, planla. Tüm akışı sade ama güçlü bir panelde tut.
</div>
</div>
<nav className="flex-1 space-y-2 overflow-y-auto px-4 py-5">
{routes.map((route) => {
const isActive =
route.href === "/"
? pathname === "/"
: pathname === route.href || pathname.startsWith(route.href + "/");
return (
<Link
key={route.href}
href={route.href}
className="block"
onClick={onNavigate}
>
<motion.div
whileHover={{ x: 6 }}
transition={{ duration: 0.22, ease: "easeOut" }}
className={cn(
"group relative overflow-hidden rounded-2xl border px-4 py-3.5 transition-all",
isActive
? "border-primary/35 bg-primary/[0.14] shadow-[0_16px_40px_rgba(108,91,176,0.22)]"
: "border-white/8 bg-white/[0.03] hover:border-white/16 hover:bg-white/[0.06]",
)}
>
{isActive ? (
<motion.div
layoutId="dashboard-nav-active"
className="absolute inset-0 rounded-2xl bg-[linear-gradient(135deg,rgba(108,91,176,0.2),rgba(108,91,176,0.08),transparent)]"
/>
) : null}
<div className="relative flex items-center gap-3">
<div
className={cn(
"flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border transition-all",
isActive
? "border-primary/30 bg-primary/20 text-primary-foreground shadow-[0_12px_28px_rgba(108,91,176,0.22)]"
: "border-white/8 bg-white/5 text-muted-foreground group-hover:border-primary/20 group-hover:bg-primary/10 group-hover:text-foreground",
)}
>
<route.icon className="h-5 w-5" />
</div>
<div className="min-w-0 flex-1">
<div
className={cn(
"flex items-center gap-2 text-sm font-semibold",
isActive ? "text-foreground" : "text-foreground/90",
)}
>
{route.label}
<ChevronRight
className={cn(
"h-4 w-4 transition-all",
isActive
? "translate-x-0 text-primary"
: "translate-x-[-4px] opacity-0 group-hover:translate-x-0 group-hover:opacity-100",
)}
/>
</div>
<div className="mt-1 line-clamp-2 text-xs leading-5 text-muted-foreground">
{route.description}
</div>
</div>
</div>
</motion.div>
</Link>
);
})}
</nav>
<div className="border-t border-white/8 p-4 pt-5">
<div className="mb-4 rounded-2xl border border-white/8 bg-white/5 p-3">
<div className="flex items-center gap-3">
<Avatar user={user} size="sm" />
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-foreground">
{user.displayName}
</div>
<div className="truncate text-xs text-muted-foreground">
{user.email}
</div>
</div>
</div>
</div>
<form action={signOut}>
<Button
type="submit"
variant="outline"
className={cn(
"h-12 w-full justify-start gap-3 rounded-2xl border-white/10 bg-white/[0.04] px-4 text-sm font-semibold text-foreground shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] transition-all",
"hover:border-destructive/30 hover:bg-destructive/10 hover:text-destructive",
)}
>
<LogOut className="h-4 w-4" />
Çıkış Yap
</Button>
</form>
{!desktop ? (
<div className="mt-3 text-center text-[11px] uppercase tracking-[0.24em] text-muted-foreground/60">
Secure workspace
</div>
) : null}
</div>
</aside>
);
}
function TopBar({
user,
currentMeta,
todayLabel,
onOpenSidebar,
}: {
user: DashboardShellProps["user"];
currentMeta: RouteMeta;
todayLabel: string;
onOpenSidebar: () => void;
}) {
return (
<div className="overflow-hidden rounded-[26px] border border-white/10 bg-[linear-gradient(180deg,rgba(31,23,43,0.82),rgba(18,13,28,0.68))] px-4 py-4 shadow-[0_20px_80px_rgba(7,5,14,0.3)] ring-1 ring-white/6 backdrop-blur-xl md:px-5">
<div className="flex items-start justify-between gap-4">
<div className="flex min-w-0 items-start gap-3">
<Button
type="button"
variant="outline"
size="icon-lg"
className="mt-0.5 shrink-0 rounded-2xl border-white/10 bg-white/[0.04] lg:hidden"
onClick={onOpenSidebar}
>
<Menu className="h-5 w-5" />
</Button>
<div className="min-w-0">
<div className="mb-1 text-[11px] font-semibold uppercase tracking-[0.26em] text-primary/80">
{currentMeta.eyebrow}
</div>
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
<h1 className="text-2xl font-semibold tracking-tight text-foreground md:text-[2rem]">
{currentMeta.title}
</h1>
<div className="rounded-full border border-white/8 bg-white/[0.04] px-3 py-1 text-xs font-medium text-muted-foreground">
{todayLabel}
</div>
</div>
<p className="mt-2 max-w-2xl text-sm leading-6 text-muted-foreground">
{currentMeta.description}
</p>
</div>
</div>
<div className="hidden items-center gap-3 md:flex">
<div className="rounded-full border border-white/8 bg-white/[0.04] px-3 py-1.5 text-xs font-medium text-muted-foreground">
Protected session
</div>
<UserMenu user={user} />
</div>
</div>
<div className="mt-4 flex items-center justify-end md:hidden">
<UserMenu user={user} compact />
</div>
</div>
);
}
function UserMenu({
user,
compact = false,
}: {
user: DashboardShellProps["user"];
compact?: boolean;
}) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className={cn(
"group flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] p-1.5 text-left shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] transition-all outline-none hover:border-white/16 hover:bg-white/[0.07]",
compact ? "w-full justify-between" : "min-w-[220px]",
)}
>
<div className="flex items-center gap-3">
<Avatar user={user} />
<div className={cn("min-w-0", compact ? "max-w-[160px]" : "max-w-[180px]")}>
<div className="truncate text-sm font-semibold text-foreground">
{user.displayName}
</div>
<div className="truncate text-xs text-muted-foreground">
{user.email}
</div>
</div>
</div>
<div className="mr-1 flex h-8 w-8 items-center justify-center rounded-xl bg-white/5 text-muted-foreground transition-all group-hover:bg-white/10 group-hover:text-foreground">
<PanelLeftClose className="h-4 w-4 rotate-[-90deg]" />
</div>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-72 rounded-2xl border border-white/10 bg-[linear-gradient(180deg,rgba(31,23,43,0.98),rgba(18,13,28,0.94))] p-2 text-foreground shadow-[0_20px_60px_rgba(7,5,14,0.45)] backdrop-blur-xl"
>
<DropdownMenuLabel className="px-3 py-2 text-foreground">
<div className="flex items-center gap-3">
<Avatar user={user} />
<div className="min-w-0">
<div className="truncate text-sm font-semibold">{user.displayName}</div>
<div className="truncate text-xs font-normal text-muted-foreground">
{user.email}
</div>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator className="bg-white/8" />
<DropdownMenuItem asChild className="rounded-xl px-3 py-2.5">
<Link href="/settings" className="flex items-center gap-2">
<Settings2 className="h-4 w-4" />
Ayarlar
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="rounded-xl px-3 py-2.5">
<Link href="/journal" className="flex items-center gap-2">
<BookOpenText className="h-4 w-4" />
Günlük Alanı
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator className="bg-white/8" />
<div className="px-3 py-2 text-xs leading-5 text-muted-foreground">
Bu menü profil ve hızlı erişim içindir. Ana çıkış aksiyonu soldaki
panelin altındadır.
</div>
</DropdownMenuContent>
</DropdownMenu>
);
}
function Avatar({
user,
size = "md",
}: {
user: DashboardShellProps["user"];
size?: "sm" | "md";
}) {
const dimensions = size === "sm" ? "h-11 w-11 text-sm" : "h-12 w-12 text-sm";
if (user.avatarUrl) {
return (
<div
className={cn(
"overflow-hidden rounded-2xl border border-white/10 bg-white/5 shadow-[0_12px_36px_rgba(0,0,0,0.18)]",
dimensions,
)}
>
{/* Avatar URL dış kaynaklı olduğu için bu önizlemede native img kullanıyoruz. */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={user.avatarUrl}
alt={user.displayName}
className="h-full w-full object-cover"
/>
</div>
);
}
return (
<div
className={cn(
"flex items-center justify-center rounded-2xl border border-white/10 bg-[linear-gradient(135deg,rgba(108,91,176,0.32),rgba(255,255,255,0.08))] font-semibold text-foreground shadow-[0_12px_36px_rgba(0,0,0,0.18)]",
dimensions,
)}
>
{user.shortName}
</div>
);
}
function AmbientBackdrop() {
return (
<>
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(108,91,176,0.18),transparent_34%),radial-gradient(circle_at_85%_14%,rgba(255,255,255,0.08),transparent_18%),radial-gradient(circle_at_bottom_right,rgba(108,91,176,0.14),transparent_28%)]" />
<motion.div
animate={{ x: [0, 28, 0], y: [0, -20, 0] }}
transition={{ duration: 14, repeat: Infinity, ease: "easeInOut" }}
className="pointer-events-none absolute left-[-8rem] top-10 h-64 w-64 rounded-full bg-primary/10 blur-[120px]"
/>
<motion.div
animate={{ x: [0, -32, 0], y: [0, 24, 0] }}
transition={{ duration: 17, repeat: Infinity, ease: "easeInOut" }}
className="pointer-events-none absolute bottom-0 right-[-8rem] h-72 w-72 rounded-full bg-white/5 blur-[130px]"
/>
</>
);
}
+12
View File
@@ -0,0 +1,12 @@
import nextCoreWebVitals from 'eslint-config-next/core-web-vitals'
import nextTypeScript from 'eslint-config-next/typescript'
const config = [
...nextCoreWebVitals,
...nextTypeScript,
{
ignores: ['**/*.old.*'],
},
]
export default config
+20 -23
View File
@@ -10,6 +10,10 @@ import type {
const TOAST_LIMIT = 1 const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000 const TOAST_REMOVE_DELAY = 1000000
const ADD_TOAST = "ADD_TOAST"
const UPDATE_TOAST = "UPDATE_TOAST"
const DISMISS_TOAST = "DISMISS_TOAST"
const REMOVE_TOAST = "REMOVE_TOAST"
type ToasterToast = ToastProps & { type ToasterToast = ToastProps & {
id: string id: string
@@ -18,13 +22,6 @@ type ToasterToast = ToastProps & {
action?: ToastActionElement action?: ToastActionElement
} }
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0 let count = 0
function genId() { function genId() {
@@ -32,23 +29,21 @@ function genId() {
return count.toString() return count.toString()
} }
type ActionType = typeof actionTypes
type Action = type Action =
| { | {
type: ActionType["ADD_TOAST"] type: typeof ADD_TOAST
toast: ToasterToast toast: ToasterToast
} }
| { | {
type: ActionType["UPDATE_TOAST"] type: typeof UPDATE_TOAST
toast: Partial<ToasterToast> toast: Partial<ToasterToast>
} }
| { | {
type: ActionType["DISMISS_TOAST"] type: typeof DISMISS_TOAST
toastId?: ToasterToast["id"] toastId?: ToasterToast["id"]
} }
| { | {
type: ActionType["REMOVE_TOAST"] type: typeof REMOVE_TOAST
toastId?: ToasterToast["id"] toastId?: ToasterToast["id"]
} }
@@ -66,7 +61,7 @@ const addToRemoveQueue = (toastId: string) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
toastTimeouts.delete(toastId) toastTimeouts.delete(toastId)
dispatch({ dispatch({
type: "REMOVE_TOAST", type: REMOVE_TOAST,
toastId: toastId, toastId: toastId,
}) })
}, TOAST_REMOVE_DELAY) }, TOAST_REMOVE_DELAY)
@@ -76,13 +71,13 @@ const addToRemoveQueue = (toastId: string) => {
export const reducer = (state: State, action: Action): State => { export const reducer = (state: State, action: Action): State => {
switch (action.type) { switch (action.type) {
case "ADD_TOAST": case ADD_TOAST:
return { return {
...state, ...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
} }
case "UPDATE_TOAST": case UPDATE_TOAST:
return { return {
...state, ...state,
toasts: state.toasts.map((t) => toasts: state.toasts.map((t) =>
@@ -90,7 +85,7 @@ export const reducer = (state: State, action: Action): State => {
), ),
} }
case "DISMISS_TOAST": { case DISMISS_TOAST: {
const { toastId } = action const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action, // ! Side effects ! - This could be extracted into a dismissToast() action,
@@ -115,7 +110,7 @@ export const reducer = (state: State, action: Action): State => {
), ),
} }
} }
case "REMOVE_TOAST": case REMOVE_TOAST:
if (action.toastId === undefined) { if (action.toastId === undefined) {
return { return {
...state, ...state,
@@ -126,6 +121,8 @@ export const reducer = (state: State, action: Action): State => {
...state, ...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId), toasts: state.toasts.filter((t) => t.id !== action.toastId),
} }
default:
return state
} }
} }
@@ -147,13 +144,13 @@ function toast({ ...props }: Toast) {
const update = (props: ToasterToast) => const update = (props: ToasterToast) =>
dispatch({ dispatch({
type: "UPDATE_TOAST", type: UPDATE_TOAST,
toast: { ...props, id }, toast: { ...props, id },
}) })
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) const dismiss = () => dispatch({ type: DISMISS_TOAST, toastId: id })
dispatch({ dispatch({
type: "ADD_TOAST", type: ADD_TOAST,
toast: { toast: {
...props, ...props,
id, id,
@@ -182,12 +179,12 @@ function useToast() {
listeners.splice(index, 1) listeners.splice(index, 1)
} }
} }
}, [state]) }, [])
return { return {
...state, ...state,
toast, toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), dismiss: (toastId?: string) => dispatch({ type: DISMISS_TOAST, toastId }),
} }
} }
+1 -1
View File
@@ -15,7 +15,7 @@ export async function updateSession(request: NextRequest) {
return request.cookies.getAll() return request.cookies.getAll()
}, },
setAll(cookiesToSet) { setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value)) cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
supabaseResponse = NextResponse.next({ supabaseResponse = NextResponse.next({
request, request,
}) })
+1 -1
View File
@@ -17,7 +17,7 @@ export async function createClient() {
cookiesToSet.forEach(({ name, value, options }) => { cookiesToSet.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options) cookieStore.set(name, value, options)
}) })
} catch (error) { } catch {
// The `set` method was called from a Server Component. // The `set` method was called from a Server Component.
// This can be ignored if you have middleware refreshing // This can be ignored if you have middleware refreshing
// user sessions. // user sessions.
-19
View File
@@ -1,19 +0,0 @@
import { type NextRequest } from 'next/server'
import { updateSession } from '@/lib/supabase/middleware'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* Feel free to modify this pattern to include more paths.
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" /> import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+3 -3
View File
@@ -6,7 +6,7 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "eslint ."
}, },
"dependencies": { "dependencies": {
"@base-ui/react": "^1.4.1", "@base-ui/react": "^1.4.1",
@@ -28,7 +28,7 @@
"dexie-react-hooks": "^4.4.0", "dexie-react-hooks": "^4.4.0",
"framer-motion": "^11.18.2", "framer-motion": "^11.18.2",
"lucide-react": "^1.14.0", "lucide-react": "^1.14.0",
"next": "^15.1.6", "next": "^16.2.6",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "^19.0.0", "react": "^19.0.0",
@@ -49,7 +49,7 @@
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^9.18.0", "eslint": "^9.18.0",
"eslint-config-next": "^15.1.6", "eslint-config-next": "^16.2.6",
"postcss": "^8.5.1", "postcss": "^8.5.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5.7.3" "typescript": "^5.7.3"
+123 -67
View File
@@ -66,8 +66,8 @@ importers:
specifier: ^1.14.0 specifier: ^1.14.0
version: 1.14.0(react@19.2.6) version: 1.14.0(react@19.2.6)
next: next:
specifier: ^15.1.6 specifier: ^16.2.6
version: 15.5.16(@babel/core@7.29.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) version: 16.2.6(@babel/core@7.29.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
next-themes: next-themes:
specifier: ^0.4.6 specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) version: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
@@ -124,8 +124,8 @@ importers:
specifier: ^9.18.0 specifier: ^9.18.0
version: 9.39.4(jiti@1.21.7) version: 9.39.4(jiti@1.21.7)
eslint-config-next: eslint-config-next:
specifier: ^15.1.6 specifier: ^16.2.6
version: 15.5.16(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) version: 16.2.6(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)
postcss: postcss:
specifier: ^8.5.1 specifier: ^8.5.1
version: 8.5.14 version: 8.5.14
@@ -618,56 +618,56 @@ packages:
'@napi-rs/wasm-runtime@0.2.12': '@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
'@next/env@15.5.16': '@next/env@16.2.6':
resolution: {integrity: sha512-9QMKolCl+JnJtaRAQSXy4RQrhgfe8W7/G1+Hl3QSB/HZY7zQMzTwPDdTRwwio8BS96ps1MHpHhbS8qxoNV3JIQ==} resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==}
'@next/eslint-plugin-next@15.5.16': '@next/eslint-plugin-next@16.2.6':
resolution: {integrity: sha512-pXa+4smRrgzea94YeAR8txf2CYg4pc1HkcoLUigrE5a0j70dVdUYMKfsOGCe8ulDSLvqnm2keMoxKss5RxHokg==} resolution: {integrity: sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==}
'@next/swc-darwin-arm64@15.5.16': '@next/swc-darwin-arm64@16.2.6':
resolution: {integrity: sha512-wzdER4JZj+31vNkhaZ1Ght3IsNI8DMwj7VqadfIOqJB5sh8FiOqNSopYADQn6mgEPomzDd/DHqBcfo2fmVMYtg==} resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@next/swc-darwin-x64@15.5.16': '@next/swc-darwin-x64@16.2.6':
resolution: {integrity: sha512-PPTo+cvcanxkuDEuDyZGk28ntmu0WjfkxqlG7hw9Mhsiribs4x1C6h2Culn0cJKqsne1gFjjZRK3ax7WYlSxgg==} resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@next/swc-linux-arm64-gnu@15.5.16': '@next/swc-linux-arm64-gnu@16.2.6':
resolution: {integrity: sha512-Jl0IL9P7S8uNl5oI1TqrQmfmLp7OqjWM58000pVnUVIsHrvPP6m9QDW/uNWYUbmd+8IYvc6MTeZKICstBMBpew==} resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@next/swc-linux-arm64-musl@15.5.16': '@next/swc-linux-arm64-musl@16.2.6':
resolution: {integrity: sha512-Zf0BIqv/o5uOWfyRkzgGhyV2Tky7HLt0bG+w7XWdaU1JpyX0tltM3TrSfa/Y9c597SJG4CzN47+u2InhgZZ4vg==} resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@next/swc-linux-x64-gnu@15.5.16': '@next/swc-linux-x64-gnu@16.2.6':
resolution: {integrity: sha512-HCDDU1TRLeUDV180QQTWrs5Oa4lIcI7XH9nF0UVUVmYLN/boZ6LqyFtm3814gc1fv+lOVyKaw5B6bVC9BpXTSQ==} resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@next/swc-linux-x64-musl@15.5.16': '@next/swc-linux-x64-musl@16.2.6':
resolution: {integrity: sha512-kvXUY1dn5wxKuMkXxQRUbPjEnKxW1PR9uKOm0zpIpj3574+cFfaePhYFmBVtrOuwt+w34OdDzNaJr5Iixf+HBQ==} resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@next/swc-win32-arm64-msvc@15.5.16': '@next/swc-win32-arm64-msvc@16.2.6':
resolution: {integrity: sha512-zpOQuF+eyENMXRjglp2hZCIrUjTdO37suEBnDn1mX4PXSuetXZDMLpjKOh4dYSw3SiDTnOoOUwBl5i5Elr6nnQ==} resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@next/swc-win32-x64-msvc@15.5.16': '@next/swc-win32-x64-msvc@16.2.6':
resolution: {integrity: sha512-LnwKYpiSmIzXlTq76hMeeIzZoDcFwu848p6H+QBkGFJIbZphgzNUPdHruJcHM/bFnaFeco0l1Frie5I27VKglA==} resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@@ -1453,9 +1453,6 @@ packages:
'@rtsao/scc@1.1.0': '@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
'@rushstack/eslint-patch@1.16.1':
resolution: {integrity: sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==}
'@sec-ant/readable-stream@0.4.1': '@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
@@ -1546,6 +1543,9 @@ packages:
'@types/node@22.19.17': '@types/node@22.19.17':
resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==}
'@types/node@22.19.18':
resolution: {integrity: sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ==}
'@types/react-dom@19.2.3': '@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies: peerDependencies:
@@ -2264,10 +2264,10 @@ packages:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'} engines: {node: '>=10'}
eslint-config-next@15.5.16: eslint-config-next@16.2.6:
resolution: {integrity: sha512-9fi/wwYuBQSm2vHDVE8PMPsKIR/xXVOlwMrRp16qra6S/LQVhZ452cjnkPZb6PN/SZ3yJUQp1L4bTYoublvBKw==} resolution: {integrity: sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q==}
peerDependencies: peerDependencies:
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 eslint: '>=9.0.0'
typescript: '>=3.3.1' typescript: '>=3.3.1'
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
@@ -2326,11 +2326,11 @@ packages:
peerDependencies: peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9
eslint-plugin-react-hooks@5.2.0: eslint-plugin-react-hooks@7.1.1:
resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==}
engines: {node: '>=10'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0
eslint-plugin-react@7.37.5: eslint-plugin-react@7.37.5:
resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==}
@@ -2612,6 +2612,10 @@ packages:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
globals@16.4.0:
resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==}
engines: {node: '>=18'}
globalthis@1.0.4: globalthis@1.0.4:
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2662,6 +2666,12 @@ packages:
headers-polyfill@5.0.1: headers-polyfill@5.0.1:
resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==}
hermes-estree@0.25.1:
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
hermes-parser@0.25.1:
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
hono@4.12.18: hono@4.12.18:
resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==} resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==}
engines: {node: '>=16.9.0'} engines: {node: '>=16.9.0'}
@@ -3115,9 +3125,9 @@ packages:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
next@15.5.16: next@16.2.6:
resolution: {integrity: sha512-aZExBk/V6JCu3NCFc90twdj9L/M3y0+ukeQwUAZbOiqRhAX+h2oMEa0NZFhcpj6HYRYjVS3V2/3xvyOpNnmw7A==} resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} engines: {node: '>=20.9.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@opentelemetry/api': ^1.1.0 '@opentelemetry/api': ^1.1.0
@@ -3878,6 +3888,13 @@ packages:
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
typescript-eslint@8.59.2:
resolution: {integrity: sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
typescript@5.9.3: typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
@@ -4049,6 +4066,12 @@ packages:
peerDependencies: peerDependencies:
zod: ^3.25.28 || ^4 zod: ^3.25.28 || ^4
zod-validation-error@4.0.2:
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
engines: {node: '>=18.0.0'}
peerDependencies:
zod: ^3.25.0 || ^4.0.0
zod@3.25.76: zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
@@ -4579,34 +4602,34 @@ snapshots:
'@tybys/wasm-util': 0.10.2 '@tybys/wasm-util': 0.10.2
optional: true optional: true
'@next/env@15.5.16': {} '@next/env@16.2.6': {}
'@next/eslint-plugin-next@15.5.16': '@next/eslint-plugin-next@16.2.6':
dependencies: dependencies:
fast-glob: 3.3.1 fast-glob: 3.3.1
'@next/swc-darwin-arm64@15.5.16': '@next/swc-darwin-arm64@16.2.6':
optional: true optional: true
'@next/swc-darwin-x64@15.5.16': '@next/swc-darwin-x64@16.2.6':
optional: true optional: true
'@next/swc-linux-arm64-gnu@15.5.16': '@next/swc-linux-arm64-gnu@16.2.6':
optional: true optional: true
'@next/swc-linux-arm64-musl@15.5.16': '@next/swc-linux-arm64-musl@16.2.6':
optional: true optional: true
'@next/swc-linux-x64-gnu@15.5.16': '@next/swc-linux-x64-gnu@16.2.6':
optional: true optional: true
'@next/swc-linux-x64-musl@15.5.16': '@next/swc-linux-x64-musl@16.2.6':
optional: true optional: true
'@next/swc-win32-arm64-msvc@15.5.16': '@next/swc-win32-arm64-msvc@16.2.6':
optional: true optional: true
'@next/swc-win32-x64-msvc@15.5.16': '@next/swc-win32-x64-msvc@16.2.6':
optional: true optional: true
'@noble/ciphers@1.3.0': {} '@noble/ciphers@1.3.0': {}
@@ -5425,8 +5448,6 @@ snapshots:
'@rtsao/scc@1.1.0': {} '@rtsao/scc@1.1.0': {}
'@rushstack/eslint-patch@1.16.1': {}
'@sec-ant/readable-stream@0.4.1': {} '@sec-ant/readable-stream@0.4.1': {}
'@sindresorhus/merge-streams@4.0.0': {} '@sindresorhus/merge-streams@4.0.0': {}
@@ -5527,6 +5548,10 @@ snapshots:
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
'@types/node@22.19.18':
dependencies:
undici-types: 6.21.0
'@types/react-dom@19.2.3(@types/react@19.2.14)': '@types/react-dom@19.2.3(@types/react@19.2.14)':
dependencies: dependencies:
'@types/react': 19.2.14 '@types/react': 19.2.14
@@ -5547,7 +5572,7 @@ snapshots:
'@types/ws@8.18.1': '@types/ws@8.18.1':
dependencies: dependencies:
'@types/node': 22.19.17 '@types/node': 22.19.18
'@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)':
dependencies: dependencies:
@@ -6278,22 +6303,22 @@ snapshots:
escape-string-regexp@4.0.0: {} escape-string-regexp@4.0.0: {}
eslint-config-next@15.5.16(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3): eslint-config-next@16.2.6(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3):
dependencies: dependencies:
'@next/eslint-plugin-next': 15.5.16 '@next/eslint-plugin-next': 16.2.6
'@rushstack/eslint-patch': 1.16.1
'@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)
eslint: 9.39.4(jiti@1.21.7) eslint: 9.39.4(jiti@1.21.7)
eslint-import-resolver-node: 0.3.10 eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@1.21.7))
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@1.21.7))
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@1.21.7))
globals: 16.4.0
typescript-eslint: 8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)
optionalDependencies: optionalDependencies:
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- '@typescript-eslint/parser'
- eslint-import-resolver-webpack - eslint-import-resolver-webpack
- eslint-plugin-import-x - eslint-plugin-import-x
- supports-color - supports-color
@@ -6380,9 +6405,16 @@ snapshots:
safe-regex-test: 1.1.0 safe-regex-test: 1.1.0
string.prototype.includes: 2.0.1 string.prototype.includes: 2.0.1
eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@1.21.7)): eslint-plugin-react-hooks@7.1.1(eslint@9.39.4(jiti@1.21.7)):
dependencies: dependencies:
'@babel/core': 7.29.0
'@babel/parser': 7.29.3
eslint: 9.39.4(jiti@1.21.7) eslint: 9.39.4(jiti@1.21.7)
hermes-parser: 0.25.1
zod: 4.4.3
zod-validation-error: 4.0.2(zod@4.4.3)
transitivePeerDependencies:
- supports-color
eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@1.21.7)): eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@1.21.7)):
dependencies: dependencies:
@@ -6741,6 +6773,8 @@ snapshots:
globals@14.0.0: {} globals@14.0.0: {}
globals@16.4.0: {}
globalthis@1.0.4: globalthis@1.0.4:
dependencies: dependencies:
define-properties: 1.2.1 define-properties: 1.2.1
@@ -6783,6 +6817,12 @@ snapshots:
'@types/set-cookie-parser': 2.4.10 '@types/set-cookie-parser': 2.4.10
set-cookie-parser: 3.1.0 set-cookie-parser: 3.1.0
hermes-estree@0.25.1: {}
hermes-parser@0.25.1:
dependencies:
hermes-estree: 0.25.1
hono@4.12.18: {} hono@4.12.18: {}
http-errors@2.0.1: http-errors@2.0.1:
@@ -7182,24 +7222,25 @@ snapshots:
react: 19.2.6 react: 19.2.6
react-dom: 19.2.6(react@19.2.6) react-dom: 19.2.6(react@19.2.6)
next@15.5.16(@babel/core@7.29.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
dependencies: dependencies:
'@next/env': 15.5.16 '@next/env': 16.2.6
'@swc/helpers': 0.5.15 '@swc/helpers': 0.5.15
baseline-browser-mapping: 2.10.27
caniuse-lite: 1.0.30001792 caniuse-lite: 1.0.30001792
postcss: 8.4.31 postcss: 8.4.31
react: 19.2.6 react: 19.2.6
react-dom: 19.2.6(react@19.2.6) react-dom: 19.2.6(react@19.2.6)
styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.6) styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.6)
optionalDependencies: optionalDependencies:
'@next/swc-darwin-arm64': 15.5.16 '@next/swc-darwin-arm64': 16.2.6
'@next/swc-darwin-x64': 15.5.16 '@next/swc-darwin-x64': 16.2.6
'@next/swc-linux-arm64-gnu': 15.5.16 '@next/swc-linux-arm64-gnu': 16.2.6
'@next/swc-linux-arm64-musl': 15.5.16 '@next/swc-linux-arm64-musl': 16.2.6
'@next/swc-linux-x64-gnu': 15.5.16 '@next/swc-linux-x64-gnu': 16.2.6
'@next/swc-linux-x64-musl': 15.5.16 '@next/swc-linux-x64-musl': 16.2.6
'@next/swc-win32-arm64-msvc': 15.5.16 '@next/swc-win32-arm64-msvc': 16.2.6
'@next/swc-win32-x64-msvc': 15.5.16 '@next/swc-win32-x64-msvc': 16.2.6
sharp: 0.34.5 sharp: 0.34.5
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
@@ -8163,6 +8204,17 @@ snapshots:
possible-typed-array-names: 1.1.0 possible-typed-array-names: 1.1.0
reflect.getprototypeof: 1.0.10 reflect.getprototypeof: 1.0.10
typescript-eslint@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3)
'@typescript-eslint/utils': 8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)
eslint: 9.39.4(jiti@1.21.7)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
typescript@5.9.3: {} typescript@5.9.3: {}
unbox-primitive@1.1.0: unbox-primitive@1.1.0:
@@ -8356,6 +8408,10 @@ snapshots:
dependencies: dependencies:
zod: 3.25.76 zod: 3.25.76
zod-validation-error@4.0.2(zod@4.4.3):
dependencies:
zod: 4.4.3
zod@3.25.76: {} zod@3.25.76: {}
zod@4.4.3: {} zod@4.4.3: {}
+13
View File
@@ -0,0 +1,13 @@
import { type NextRequest } from 'next/server'
import { updateSession } from '@/lib/supabase/middleware'
export async function proxy(request: NextRequest) {
return updateSession(request)
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
+19 -5
View File
@@ -1,7 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": false, "allowJs": false,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -11,7 +15,7 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "react-jsx",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
{ {
@@ -20,9 +24,19 @@
], ],
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./*"] "@/*": [
"./*"
]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": [
"exclude": ["node_modules"] "next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
} }
+1 -1
View File
File diff suppressed because one or more lines are too long