Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -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.
|
||||
@@ -1,19 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
import { db } from "@/lib/db";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { analyzeJournalWithLocalAI } from "@/lib/ai";
|
||||
import { db } from "@/lib/db";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
@@ -31,22 +31,23 @@ export default function JournalPage() {
|
||||
const [energy, setEnergy] = useState("3");
|
||||
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(() =>
|
||||
db.journals.orderBy("date").reverse().toArray(),
|
||||
);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!content.trim()) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const entryId = uuidv4();
|
||||
const currentContent = content; // Analiz için metni kopyala
|
||||
const currentContent = content;
|
||||
|
||||
try {
|
||||
// 1. Veriyi veritabanına hemen ekle (Kullanıcı beklemesin)
|
||||
// Veriyi önce kaydet, AI analizini daha sonra arka planda tamamla.
|
||||
await db.journals.add({
|
||||
id: entryId,
|
||||
date: now.split("T")[0],
|
||||
@@ -57,12 +58,10 @@ export default function JournalPage() {
|
||||
updated_at: now,
|
||||
});
|
||||
|
||||
// UI'ı Sıfırla
|
||||
setContent("");
|
||||
setEnergy("3");
|
||||
setMood("happy");
|
||||
|
||||
// 2. Arka planda AI Analizi başlat
|
||||
try {
|
||||
const aiResult = await analyzeJournalWithLocalAI(currentContent);
|
||||
if (aiResult) {
|
||||
@@ -72,8 +71,7 @@ export default function JournalPage() {
|
||||
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 && aiResult.suggested_tasks.length > 0) {
|
||||
if (aiResult.suggested_tasks?.length) {
|
||||
for (const taskTitle of aiResult.suggested_tasks) {
|
||||
await db.tasks.add({
|
||||
id: uuidv4(),
|
||||
@@ -87,8 +85,8 @@ export default function JournalPage() {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (aiErr) {
|
||||
console.error("Arka plan AI analizi hatası:", aiErr);
|
||||
} catch (aiError) {
|
||||
console.error("Arka plan AI analizi hatası:", aiError);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Günlük kaydedilemedi:", error);
|
||||
@@ -104,7 +102,7 @@ export default function JournalPage() {
|
||||
};
|
||||
|
||||
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>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Günlük</h1>
|
||||
<p className="text-muted-foreground">
|
||||
@@ -119,27 +117,28 @@ export default function JournalPage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<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">
|
||||
<Label>Ruh Hali</Label>
|
||||
<div className="flex gap-2">
|
||||
{moods.map((m) => (
|
||||
{moods.map((currentMood) => (
|
||||
<button
|
||||
key={m.id}
|
||||
key={currentMood.id}
|
||||
type="button"
|
||||
onClick={() => setMood(m.id)}
|
||||
className={`flex-1 py-2 px-1 rounded-md border text-xl flex items-center justify-center transition-colors ${
|
||||
mood === m.id
|
||||
? "bg-primary/20 border-primary"
|
||||
: "bg-card border-border hover:bg-muted"
|
||||
onClick={() => setMood(currentMood.id)}
|
||||
className={`flex flex-1 items-center justify-center rounded-md border px-1 py-2 text-xl transition-colors ${
|
||||
mood === currentMood.id
|
||||
? "border-primary bg-primary/20"
|
||||
: "border-border bg-card hover:bg-muted"
|
||||
}`}
|
||||
title={m.label}
|
||||
title={currentMood.label}
|
||||
>
|
||||
{m.emoji}
|
||||
{currentMood.emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Enerji Seviyesi (1-5): {energy}</Label>
|
||||
<Input
|
||||
@@ -148,7 +147,7 @@ export default function JournalPage() {
|
||||
max="5"
|
||||
step="1"
|
||||
value={energy}
|
||||
onChange={(e) => setEnergy(e.target.value)}
|
||||
onChange={(event) => setEnergy(event.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -159,7 +158,7 @@ export default function JournalPage() {
|
||||
<Textarea
|
||||
id="content"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onChange={(event) => setContent(event.target.value)}
|
||||
className="min-h-[150px] resize-y"
|
||||
placeholder="Örneğin: Bugün toplantıda işler ters gitti..."
|
||||
/>
|
||||
@@ -190,13 +189,14 @@ export default function JournalPage() {
|
||||
<Card key={journal.id}>
|
||||
<CardHeader className="pb-2">
|
||||
<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>
|
||||
{moods.find((m) => m.id === journal.mood)?.emoji}
|
||||
{moods.find((currentMood) => currentMood.id === journal.mood)
|
||||
?.emoji ?? "📝"}
|
||||
</span>
|
||||
<span>{journal.date}</span>
|
||||
</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
|
||||
</span>
|
||||
</div>
|
||||
@@ -206,30 +206,30 @@ export default function JournalPage() {
|
||||
{journal.content}
|
||||
</p>
|
||||
|
||||
{/* AI Sonuçlarını Göster */}
|
||||
{journal.ai_tags && (
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{journal.ai_tags.map((tag) => (
|
||||
<span
|
||||
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}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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">
|
||||
" {journal.ai_summary} "
|
||||
<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}”
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end mt-4">
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
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)}
|
||||
>
|
||||
Sil
|
||||
|
||||
+42
-11
@@ -1,20 +1,51 @@
|
||||
import { Sidebar } from "@/components/layout/sidebar";
|
||||
import { Header } from "@/components/layout/header";
|
||||
import { DashboardShell } from "@/components/layout/dashboard-shell";
|
||||
import { createClient } from "@/lib/supabase/server";
|
||||
|
||||
export default function DashboardLayout({
|
||||
export default async function DashboardLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
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 (
|
||||
<div className="flex min-h-screen bg-background text-foreground">
|
||||
<Sidebar />
|
||||
<div className="flex flex-col flex-1 h-screen overflow-hidden">
|
||||
<Header />
|
||||
<main className="flex-1 overflow-y-auto p-6 md:p-8">
|
||||
<DashboardShell
|
||||
user={{
|
||||
email: user?.email ?? "bilinmiyor@mindspace.local",
|
||||
displayName,
|
||||
shortName,
|
||||
avatarUrl:
|
||||
profile?.avatar_url ||
|
||||
user?.user_metadata?.avatar_url ||
|
||||
user?.user_metadata?.picture ||
|
||||
null,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardShell>
|
||||
);
|
||||
}
|
||||
|
||||
+79
-90
@@ -1,23 +1,27 @@
|
||||
"use client";
|
||||
|
||||
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 { db } from "@/lib/db";
|
||||
import { Activity, Brain, CheckCircle, PenTool } from "lucide-react";
|
||||
import {
|
||||
LineChart,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Legend,
|
||||
Line,
|
||||
LineChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
BarChart,
|
||||
Bar,
|
||||
Legend,
|
||||
} 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> = {
|
||||
happy: 4,
|
||||
neutral: 3,
|
||||
@@ -26,65 +30,71 @@ const moodScores: Record<string, number> = {
|
||||
};
|
||||
|
||||
export default function DashboardPage() {
|
||||
const journals =
|
||||
useLiveQuery(() => db.journals.orderBy("date").toArray()) || [];
|
||||
const tasks = useLiveQuery(() => db.tasks.toArray()) || [];
|
||||
const liveJournals = useLiveQuery<Journal[]>(() =>
|
||||
db.journals.orderBy("date").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 todaysJournal = journals.find((j) => j.date === today);
|
||||
const todaysJournal = journals.find((journal) => journal.date === today);
|
||||
const todaysEnergy = todaysJournal ? todaysJournal.energy : "--";
|
||||
|
||||
// 1. Grafik: Son 7 günün Ruh Hali ve Enerji trendi
|
||||
const trendData = useMemo(() => {
|
||||
const dataMap: Record<
|
||||
string,
|
||||
{ date: string; moodSum: number; energySum: number; count: number }
|
||||
> = {};
|
||||
|
||||
journals.forEach((j) => {
|
||||
if (!dataMap[j.date]) {
|
||||
dataMap[j.date] = { date: j.date, moodSum: 0, energySum: 0, count: 0 };
|
||||
journals.forEach((journal) => {
|
||||
if (!dataMap[journal.date]) {
|
||||
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[j.date].count += 1;
|
||||
|
||||
dataMap[journal.date].moodSum += moodScores[journal.mood] || 3;
|
||||
dataMap[journal.date].energySum += journal.energy;
|
||||
dataMap[journal.date].count += 1;
|
||||
});
|
||||
|
||||
return Object.values(dataMap)
|
||||
.map((d) => ({
|
||||
date: d.date.slice(5), // Sadece MM-DD formatı alalım
|
||||
Mood: Number((d.moodSum / d.count).toFixed(1)),
|
||||
Enerji: Number((d.energySum / d.count).toFixed(1)),
|
||||
.map((entry) => ({
|
||||
date: entry.date.slice(5),
|
||||
Mood: Number((entry.moodSum / entry.count).toFixed(1)),
|
||||
Enerji: Number((entry.energySum / entry.count).toFixed(1)),
|
||||
}))
|
||||
.slice(-7); // Sadece son 7 günü göster
|
||||
.slice(-7);
|
||||
}, [journals]);
|
||||
|
||||
// 2. Grafik: En çok kullanılan AI etiketleri
|
||||
const tagData = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
journals.forEach((j) => {
|
||||
if (j.ai_tags) {
|
||||
j.ai_tags.forEach((tag) => {
|
||||
|
||||
journals.forEach((journal) => {
|
||||
journal.ai_tags?.forEach((tag) => {
|
||||
counts[tag] = (counts[tag] || 0) + 1;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Object.entries(counts)
|
||||
.map(([name, value]) => ({ name, Değer: value }))
|
||||
.sort((a, b) => b.Değer - a.Değer)
|
||||
.slice(0, 5); // En çok geçen 5 etiket
|
||||
.slice(0, 5);
|
||||
}, [journals]);
|
||||
|
||||
// Son günlüğe ait AI Summary
|
||||
const lastInsight = [...journals]
|
||||
.reverse()
|
||||
.find((j) => j.ai_summary)?.ai_summary;
|
||||
.find((journal) => journal.ai_summary)?.ai_summary;
|
||||
|
||||
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">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Bugün Nasılsın?</h1>
|
||||
<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">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Günlük Kayıtları
|
||||
</CardTitle>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Günlük Kayıtları</CardTitle>
|
||||
<PenTool className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<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
|
||||
? "Henüz kayıt girilmedi"
|
||||
: "Toplam kayıt eklendi"}
|
||||
@@ -111,58 +119,49 @@ export default function DashboardPage() {
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Aktif Görevler
|
||||
</CardTitle>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Aktif Görevler</CardTitle>
|
||||
<CheckCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<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>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Bugünkü Enerji
|
||||
</CardTitle>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Bugünkü Enerji</CardTitle>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{todaysEnergy}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{todaysJournal ? "/ 5 Seviyesinde" : "Kayıt bekleniyor"}
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{todaysJournal ? "/ 5 seviyesinde" : "Kayıt bekleniyor"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Local AI Durumu
|
||||
</CardTitle>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Local AI Durumu</CardTitle>
|
||||
<Brain className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<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
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7 ">
|
||||
{/* Line Chart */}
|
||||
<Card className="lg:col-span-4 p-6 flex flex-col justify-between">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
|
||||
<Card className="flex flex-col justify-between p-6 lg:col-span-4">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-medium mb-1">
|
||||
Ruh Hali & Enerji Trendi
|
||||
</h3>
|
||||
<h3 className="mb-1 text-lg font-medium">Ruh Hali ve Enerji Trendi</h3>
|
||||
<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>
|
||||
</div>
|
||||
<div className="h-[250px] w-full">
|
||||
@@ -196,9 +195,7 @@ export default function DashboardPage() {
|
||||
}}
|
||||
itemStyle={{ fontWeight: "500" }}
|
||||
/>
|
||||
<Legend
|
||||
wrapperStyle={{ paddingTop: "10px", fontSize: "14px" }}
|
||||
/>
|
||||
<Legend wrapperStyle={{ paddingTop: "10px", fontSize: "14px" }} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="Mood"
|
||||
@@ -218,32 +215,25 @@ export default function DashboardPage() {
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center border-2 border-dashed rounded-md bg-muted/20">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Veri bekleniyor...
|
||||
</p>
|
||||
<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">Veri bekleniyor...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Bar Chart & Insight */}
|
||||
<div className="lg:col-span-3 space-y-4 flex flex-col">
|
||||
<Card className="p-6 flex-1 flex flex-col">
|
||||
<div className="flex flex-col space-y-4 lg:col-span-3">
|
||||
<Card className="flex flex-1 flex-col p-6">
|
||||
<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">
|
||||
Günlüklerinden çıkarılan en sık 5 etiket
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-[150px] w-full mt-auto">
|
||||
<div className="mt-auto h-[150px] w-full">
|
||||
{tagData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={tagData}
|
||||
layout="vertical"
|
||||
margin={{ left: -20 }}
|
||||
>
|
||||
<BarChart data={tagData} layout="vertical" margin={{ left: -20 }}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
horizontal={false}
|
||||
@@ -276,20 +266,19 @@ export default function DashboardPage() {
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center border-2 border-dashed rounded-md bg-muted/20">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Yeterli etiket yok.
|
||||
</p>
|
||||
<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">Yeterli etiket yok.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-primary/5 border-primary/20 flex-1">
|
||||
<h3 className="text-lg font-medium mb-3 flex items-center gap-2">
|
||||
<Brain className="w-5 h-5 text-primary" /> Son AI İçgörüsü
|
||||
<Card className="flex-1 border-primary/20 bg-primary/5 p-6">
|
||||
<h3 className="mb-3 flex items-center gap-2 text-lg font-medium">
|
||||
<Brain className="h-5 w-5 text-primary" />
|
||||
Son AI İçgörüsü
|
||||
</h3>
|
||||
<p className="text-sm text-foreground/80 leading-relaxed italic">
|
||||
<p className="text-sm italic leading-relaxed text-foreground/80">
|
||||
{lastInsight
|
||||
? `"${lastInsight}"`
|
||||
: "Henüz bir içgörü oluşmadı. Biraz günlük yaz, AI analiz yapsın."}
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
'use server'
|
||||
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
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) {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
return { error: 'Kullanıcı bulunamadı.' }
|
||||
}
|
||||
@@ -15,31 +25,30 @@ export async function updateProfile(formData: FormData) {
|
||||
const lastName = formData.get('lastName') as string
|
||||
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) {
|
||||
const fileExt = avatarFile.name.split('.').pop()
|
||||
const fileName = `${user.id}/${Math.random()}.${fileExt}`
|
||||
|
||||
const { error: uploadError, data: uploadData } = await supabase.storage
|
||||
const { error: uploadError } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(fileName, avatarFile, { upsert: true })
|
||||
|
||||
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 { data: { publicUrl } } = supabase.storage
|
||||
.from('avatars')
|
||||
.getPublicUrl(fileName)
|
||||
const {
|
||||
data: { publicUrl },
|
||||
} = supabase.storage.from('avatars').getPublicUrl(fileName)
|
||||
|
||||
avatarUrl = publicUrl
|
||||
}
|
||||
|
||||
// Update profile
|
||||
const updateData: any = {
|
||||
const updateData: ProfileUpdateData = {
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
}
|
||||
@@ -48,13 +57,13 @@ export async function updateProfile(formData: FormData) {
|
||||
updateData.avatar_url = avatarUrl
|
||||
}
|
||||
|
||||
// Upsert profile in case it doesn't exist yet
|
||||
const { error } = await supabase
|
||||
.from('profiles')
|
||||
.upsert({ id: user.id, ...updateData })
|
||||
const { error } = await supabase.from('profiles').upsert({
|
||||
id: user.id,
|
||||
...updateData,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
return { error: 'Profil güncellenirken hata oluştu: ' + error.message }
|
||||
return { error: `Profil güncellenirken hata oluştu: ${error.message}` }
|
||||
}
|
||||
|
||||
revalidatePath('/settings')
|
||||
@@ -69,12 +78,10 @@ export async function updatePassword(formData: FormData) {
|
||||
return { error: 'Şifre en az 6 karakter olmalıdır.' }
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
password: password
|
||||
})
|
||||
const { error } = await supabase.auth.updateUser({ password })
|
||||
|
||||
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 }
|
||||
|
||||
@@ -1,114 +1,161 @@
|
||||
"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 { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Bot, Save, User, KeyRound } from "lucide-react";
|
||||
import { createClient } from "@/lib/supabase/client";
|
||||
import { updateProfile, updatePassword } from "./actions";
|
||||
|
||||
type AiProvider = "groq" | "ollama" | "openai";
|
||||
|
||||
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() {
|
||||
const [aiProvider, setAiProvider] = useState("groq");
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [aiProvider, setAiProvider] = useState<AiProvider>(getInitialAiProvider);
|
||||
const [apiKey, setApiKey] = useState(getInitialApiKey);
|
||||
const [saveStatus, setSaveStatus] = useState("");
|
||||
|
||||
// Profile state
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [avatarUrl, setAvatarUrl] = useState("");
|
||||
const [profileSaveStatus, setProfileSaveStatus] = useState("");
|
||||
const [passwordSaveStatus, setPasswordSaveStatus] = useState("");
|
||||
|
||||
const [supabase] = useState(() => createClient());
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
const supabase = createClient();
|
||||
|
||||
useEffect(() => {
|
||||
const savedProvider = localStorage.getItem("mindspace_ai_provider");
|
||||
const savedApiKey = localStorage.getItem("mindspace_api_key");
|
||||
if (savedProvider) setAiProvider(savedProvider);
|
||||
if (savedApiKey) setApiKey(savedApiKey);
|
||||
let isActive = true;
|
||||
|
||||
const fetchProfile = async () => {
|
||||
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 || "");
|
||||
setLastName(data.last_name || "");
|
||||
setAvatarUrl(data.avatar_url || "");
|
||||
}
|
||||
}
|
||||
}
|
||||
fetchProfile();
|
||||
}, []);
|
||||
};
|
||||
|
||||
void fetchProfile();
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, [supabase]);
|
||||
|
||||
const handleSaveAI = () => {
|
||||
localStorage.setItem("mindspace_ai_provider", aiProvider);
|
||||
localStorage.setItem("mindspace_api_key", apiKey);
|
||||
|
||||
setSaveStatus("Ayarlar başarıyla kaydedildi!");
|
||||
setTimeout(() => setSaveStatus(""), 3000);
|
||||
window.setTimeout(() => setSaveStatus(""), 3000);
|
||||
};
|
||||
|
||||
const handleProfileAction = async (formData: FormData) => {
|
||||
const res = await updateProfile(formData);
|
||||
if (res?.error) {
|
||||
setProfileSaveStatus("Hata: " + res.error);
|
||||
const response = await updateProfile(formData);
|
||||
|
||||
if (response?.error) {
|
||||
setProfileSaveStatus(`Hata: ${response.error}`);
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
}
|
||||
setTimeout(() => setProfileSaveStatus(""), 3000);
|
||||
|
||||
window.setTimeout(() => setProfileSaveStatus(""), 3000);
|
||||
};
|
||||
|
||||
const handlePasswordAction = async (formData: FormData) => {
|
||||
const res = await updatePassword(formData);
|
||||
if (res?.error) {
|
||||
setPasswordSaveStatus("Hata: " + res.error);
|
||||
const response = await updatePassword(formData);
|
||||
|
||||
if (response?.error) {
|
||||
setPasswordSaveStatus(`Hata: ${response.error}`);
|
||||
} else {
|
||||
setPasswordSaveStatus("Şifre başarıyla güncellendi!");
|
||||
formRef.current?.reset();
|
||||
}
|
||||
setTimeout(() => setPasswordSaveStatus(""), 3000);
|
||||
|
||||
window.setTimeout(() => setPasswordSaveStatus(""), 3000);
|
||||
};
|
||||
|
||||
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>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Profile Settings */}
|
||||
<div className="bg-card border border-border rounded-xl p-6 shadow-sm space-y-6 flex flex-col">
|
||||
<div className="flex flex-col space-y-6 rounded-xl border border-border bg-card p-6 shadow-sm">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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 ? (
|
||||
<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">
|
||||
<User className="w-8 h-8 text-muted-foreground" />
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full border border-border bg-muted">
|
||||
<User className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1 flex-1">
|
||||
|
||||
<div className="flex-1 space-y-1">
|
||||
<Label htmlFor="avatar">Profil Fotoğrafı Yükle</Label>
|
||||
<Input id="avatar" name="avatar" type="file" accept="image/*" />
|
||||
</div>
|
||||
@@ -117,21 +164,38 @@ export default function SettingsPage() {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<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 className="space-y-2">
|
||||
<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 className="flex items-center gap-4 mt-4">
|
||||
<div className="mt-4 flex items-center gap-4">
|
||||
<Button type="submit" className="w-max">
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
Profili Kaydet
|
||||
</Button>
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
@@ -139,26 +203,38 @@ export default function SettingsPage() {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Password Settings */}
|
||||
<div className="bg-card border border-border rounded-xl p-6 shadow-sm space-y-6 flex flex-col">
|
||||
<div className="flex flex-col space-y-6 rounded-xl border border-border bg-card p-6 shadow-sm">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<form ref={formRef} action={handlePasswordAction} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<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 className="flex items-center gap-4 mt-4">
|
||||
<div className="mt-4 flex items-center gap-4">
|
||||
<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
|
||||
</Button>
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
@@ -166,31 +242,34 @@ export default function SettingsPage() {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* AI Settings */}
|
||||
<div className="bg-card border border-border rounded-xl p-6 shadow-sm space-y-6 flex flex-col">
|
||||
<div className="flex flex-col space-y-6 rounded-xl border border-border bg-card p-6 shadow-sm">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<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">
|
||||
<SelectValue placeholder="Sağlayıcı seçin" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ollama">
|
||||
Ollama (Yerel & Gizlilik Odaklı)
|
||||
Ollama (Yerel ve Gizlilik Odaklı)
|
||||
</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>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
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>
|
||||
</div>
|
||||
|
||||
@@ -200,19 +279,19 @@ export default function SettingsPage() {
|
||||
<Input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
onChange={(event) => setApiKey(event.target.value)}
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Anahtarınız sadece tarayıcınızın kendi local hafızasında güvenle
|
||||
saklanır, hiçbir sunucuya kaydedilmez.
|
||||
Anahtarınız sadece tarayıcınızın kendi local hafızasında saklanır;
|
||||
herhangi bir sunucuya kaydedilmez.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4 mt-4">
|
||||
<Button onClick={handleSaveAI} className="w-max">
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
<div className="mt-4 flex items-center gap-4">
|
||||
<Button type="button" onClick={handleSaveAI} className="w-max">
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
Kaydet
|
||||
</Button>
|
||||
{saveStatus && (
|
||||
|
||||
+73
-24
@@ -1,14 +1,53 @@
|
||||
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) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { provider, apiKey, userMessageContent } = body;
|
||||
const body = (await request.json()) as ChatRequestBody;
|
||||
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 = "";
|
||||
|
||||
if (provider === "groq") {
|
||||
const res = await fetch(
|
||||
const response = await fetch(
|
||||
"https://api.groq.com/openai/v1/chat/completions",
|
||||
{
|
||||
method: "POST",
|
||||
@@ -30,14 +69,15 @@ export async function POST(request: Request) {
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json();
|
||||
throw new Error(errorData.error?.message || "Groq API Hatası");
|
||||
if (!response.ok) {
|
||||
const errorData = (await response.json()) as ProviderErrorResponse;
|
||||
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") {
|
||||
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
||||
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"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();
|
||||
assistantReply = data.choices[0].message.content;
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("OpenAI API hatası");
|
||||
}
|
||||
|
||||
const data = (await response.json()) as ChatCompletionResponse;
|
||||
assistantReply = data.choices?.[0]?.message?.content ?? "";
|
||||
} else {
|
||||
// Varsayılan: Yerel Ollama
|
||||
const res = await fetch("http://127.0.0.1:11434/api/generate", {
|
||||
const response = await fetch("http://127.0.0.1:11434/api/generate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
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:`,
|
||||
stream: false,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error("Ollama API yanıt vermedi.");
|
||||
const data = await res.json();
|
||||
assistantReply = data.response;
|
||||
if (!response.ok) {
|
||||
throw new Error("Ollama API yanıt vermedi.");
|
||||
}
|
||||
|
||||
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 });
|
||||
} catch (error: any) {
|
||||
console.error("API Route Hatası:", error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || "Bilinmeyen Sunucu Hatası" },
|
||||
{ status: 500 },
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const message = getErrorMessage(error);
|
||||
console.error("API route hatası:", error);
|
||||
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,3 +39,12 @@ export async function signup(formData: FormData) {
|
||||
revalidatePath('/', 'layout')
|
||||
redirect('/')
|
||||
}
|
||||
|
||||
export async function signOut() {
|
||||
const supabase = await createClient()
|
||||
|
||||
await supabase.auth.signOut()
|
||||
|
||||
revalidatePath('/', 'layout')
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
@@ -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]"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -10,6 +10,10 @@ import type {
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
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 & {
|
||||
id: string
|
||||
@@ -18,13 +22,6 @@ type ToasterToast = ToastProps & {
|
||||
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
|
||||
|
||||
function genId() {
|
||||
@@ -32,23 +29,21 @@ function genId() {
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
type: typeof ADD_TOAST
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
type: typeof UPDATE_TOAST
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
type: typeof DISMISS_TOAST
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
type: typeof REMOVE_TOAST
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
@@ -66,7 +61,7 @@ const addToRemoveQueue = (toastId: string) => {
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
type: REMOVE_TOAST,
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
@@ -76,13 +71,13 @@ const addToRemoveQueue = (toastId: string) => {
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
case ADD_TOAST:
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
case UPDATE_TOAST:
|
||||
return {
|
||||
...state,
|
||||
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
|
||||
|
||||
// ! 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) {
|
||||
return {
|
||||
...state,
|
||||
@@ -126,6 +121,8 @@ export const reducer = (state: State, action: Action): State => {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,13 +144,13 @@ function toast({ ...props }: Toast) {
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
type: UPDATE_TOAST,
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
const dismiss = () => dispatch({ type: DISMISS_TOAST, toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
type: ADD_TOAST,
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
@@ -182,12 +179,12 @@ function useToast() {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
}, [])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
dismiss: (toastId?: string) => dispatch({ type: DISMISS_TOAST, toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function updateSession(request: NextRequest) {
|
||||
return request.cookies.getAll()
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
|
||||
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
|
||||
supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function createClient() {
|
||||
cookiesToSet.forEach(({ name, value, options }) => {
|
||||
cookieStore.set(name, value, options)
|
||||
})
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// The `set` method was called from a Server Component.
|
||||
// This can be ignored if you have middleware refreshing
|
||||
// user sessions.
|
||||
|
||||
@@ -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)$).*)',
|
||||
],
|
||||
}
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <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
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.4.1",
|
||||
@@ -28,7 +28,7 @@
|
||||
"dexie-react-hooks": "^4.4.0",
|
||||
"framer-motion": "^11.18.2",
|
||||
"lucide-react": "^1.14.0",
|
||||
"next": "^15.1.6",
|
||||
"next": "^16.2.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.0.0",
|
||||
@@ -49,7 +49,7 @@
|
||||
"@types/uuid": "^10.0.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-next": "^15.1.6",
|
||||
"eslint-config-next": "^16.2.6",
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.3"
|
||||
|
||||
Generated
+123
-67
@@ -66,8 +66,8 @@ importers:
|
||||
specifier: ^1.14.0
|
||||
version: 1.14.0(react@19.2.6)
|
||||
next:
|
||||
specifier: ^15.1.6
|
||||
version: 15.5.16(@babel/core@7.29.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
specifier: ^16.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:
|
||||
specifier: ^0.4.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
|
||||
version: 9.39.4(jiti@1.21.7)
|
||||
eslint-config-next:
|
||||
specifier: ^15.1.6
|
||||
version: 15.5.16(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)
|
||||
specifier: ^16.2.6
|
||||
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:
|
||||
specifier: ^8.5.1
|
||||
version: 8.5.14
|
||||
@@ -618,56 +618,56 @@ packages:
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
'@next/env@15.5.16':
|
||||
resolution: {integrity: sha512-9QMKolCl+JnJtaRAQSXy4RQrhgfe8W7/G1+Hl3QSB/HZY7zQMzTwPDdTRwwio8BS96ps1MHpHhbS8qxoNV3JIQ==}
|
||||
'@next/env@16.2.6':
|
||||
resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==}
|
||||
|
||||
'@next/eslint-plugin-next@15.5.16':
|
||||
resolution: {integrity: sha512-pXa+4smRrgzea94YeAR8txf2CYg4pc1HkcoLUigrE5a0j70dVdUYMKfsOGCe8ulDSLvqnm2keMoxKss5RxHokg==}
|
||||
'@next/eslint-plugin-next@16.2.6':
|
||||
resolution: {integrity: sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==}
|
||||
|
||||
'@next/swc-darwin-arm64@15.5.16':
|
||||
resolution: {integrity: sha512-wzdER4JZj+31vNkhaZ1Ght3IsNI8DMwj7VqadfIOqJB5sh8FiOqNSopYADQn6mgEPomzDd/DHqBcfo2fmVMYtg==}
|
||||
'@next/swc-darwin-arm64@16.2.6':
|
||||
resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@15.5.16':
|
||||
resolution: {integrity: sha512-PPTo+cvcanxkuDEuDyZGk28ntmu0WjfkxqlG7hw9Mhsiribs4x1C6h2Culn0cJKqsne1gFjjZRK3ax7WYlSxgg==}
|
||||
'@next/swc-darwin-x64@16.2.6':
|
||||
resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.5.16':
|
||||
resolution: {integrity: sha512-Jl0IL9P7S8uNl5oI1TqrQmfmLp7OqjWM58000pVnUVIsHrvPP6m9QDW/uNWYUbmd+8IYvc6MTeZKICstBMBpew==}
|
||||
'@next/swc-linux-arm64-gnu@16.2.6':
|
||||
resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.5.16':
|
||||
resolution: {integrity: sha512-Zf0BIqv/o5uOWfyRkzgGhyV2Tky7HLt0bG+w7XWdaU1JpyX0tltM3TrSfa/Y9c597SJG4CzN47+u2InhgZZ4vg==}
|
||||
'@next/swc-linux-arm64-musl@16.2.6':
|
||||
resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.5.16':
|
||||
resolution: {integrity: sha512-HCDDU1TRLeUDV180QQTWrs5Oa4lIcI7XH9nF0UVUVmYLN/boZ6LqyFtm3814gc1fv+lOVyKaw5B6bVC9BpXTSQ==}
|
||||
'@next/swc-linux-x64-gnu@16.2.6':
|
||||
resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-musl@15.5.16':
|
||||
resolution: {integrity: sha512-kvXUY1dn5wxKuMkXxQRUbPjEnKxW1PR9uKOm0zpIpj3574+cFfaePhYFmBVtrOuwt+w34OdDzNaJr5Iixf+HBQ==}
|
||||
'@next/swc-linux-x64-musl@16.2.6':
|
||||
resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.5.16':
|
||||
resolution: {integrity: sha512-zpOQuF+eyENMXRjglp2hZCIrUjTdO37suEBnDn1mX4PXSuetXZDMLpjKOh4dYSw3SiDTnOoOUwBl5i5Elr6nnQ==}
|
||||
'@next/swc-win32-arm64-msvc@16.2.6':
|
||||
resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.5.16':
|
||||
resolution: {integrity: sha512-LnwKYpiSmIzXlTq76hMeeIzZoDcFwu848p6H+QBkGFJIbZphgzNUPdHruJcHM/bFnaFeco0l1Frie5I27VKglA==}
|
||||
'@next/swc-win32-x64-msvc@16.2.6':
|
||||
resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -1453,9 +1453,6 @@ packages:
|
||||
'@rtsao/scc@1.1.0':
|
||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||
|
||||
'@rushstack/eslint-patch@1.16.1':
|
||||
resolution: {integrity: sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==}
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1':
|
||||
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
||||
|
||||
@@ -1546,6 +1543,9 @@ packages:
|
||||
'@types/node@22.19.17':
|
||||
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':
|
||||
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
||||
peerDependencies:
|
||||
@@ -2264,10 +2264,10 @@ packages:
|
||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
eslint-config-next@15.5.16:
|
||||
resolution: {integrity: sha512-9fi/wwYuBQSm2vHDVE8PMPsKIR/xXVOlwMrRp16qra6S/LQVhZ452cjnkPZb6PN/SZ3yJUQp1L4bTYoublvBKw==}
|
||||
eslint-config-next@16.2.6:
|
||||
resolution: {integrity: sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q==}
|
||||
peerDependencies:
|
||||
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
|
||||
eslint: '>=9.0.0'
|
||||
typescript: '>=3.3.1'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
@@ -2326,11 +2326,11 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9
|
||||
|
||||
eslint-plugin-react-hooks@5.2.0:
|
||||
resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==}
|
||||
engines: {node: '>=10'}
|
||||
eslint-plugin-react-hooks@7.1.1:
|
||||
resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==}
|
||||
engines: {node: '>=18'}
|
||||
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:
|
||||
resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==}
|
||||
@@ -2612,6 +2612,10 @@ packages:
|
||||
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
globals@16.4.0:
|
||||
resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
globalthis@1.0.4:
|
||||
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2662,6 +2666,12 @@ packages:
|
||||
headers-polyfill@5.0.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
@@ -3115,9 +3125,9 @@ packages:
|
||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
next@15.5.16:
|
||||
resolution: {integrity: sha512-aZExBk/V6JCu3NCFc90twdj9L/M3y0+ukeQwUAZbOiqRhAX+h2oMEa0NZFhcpj6HYRYjVS3V2/3xvyOpNnmw7A==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
next@16.2.6:
|
||||
resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.1.0
|
||||
@@ -3878,6 +3888,13 @@ packages:
|
||||
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -4049,6 +4066,12 @@ packages:
|
||||
peerDependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
@@ -4579,34 +4602,34 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.10.2
|
||||
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:
|
||||
fast-glob: 3.3.1
|
||||
|
||||
'@next/swc-darwin-arm64@15.5.16':
|
||||
'@next/swc-darwin-arm64@16.2.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@15.5.16':
|
||||
'@next/swc-darwin-x64@16.2.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.5.16':
|
||||
'@next/swc-linux-arm64-gnu@16.2.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.5.16':
|
||||
'@next/swc-linux-arm64-musl@16.2.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.5.16':
|
||||
'@next/swc-linux-x64-gnu@16.2.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@15.5.16':
|
||||
'@next/swc-linux-x64-musl@16.2.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.5.16':
|
||||
'@next/swc-win32-arm64-msvc@16.2.6':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.5.16':
|
||||
'@next/swc-win32-x64-msvc@16.2.6':
|
||||
optional: true
|
||||
|
||||
'@noble/ciphers@1.3.0': {}
|
||||
@@ -5425,8 +5448,6 @@ snapshots:
|
||||
|
||||
'@rtsao/scc@1.1.0': {}
|
||||
|
||||
'@rushstack/eslint-patch@1.16.1': {}
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1': {}
|
||||
|
||||
'@sindresorhus/merge-streams@4.0.0': {}
|
||||
@@ -5527,6 +5548,10 @@ snapshots:
|
||||
dependencies:
|
||||
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)':
|
||||
dependencies:
|
||||
'@types/react': 19.2.14
|
||||
@@ -5547,7 +5572,7 @@ snapshots:
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
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)':
|
||||
dependencies:
|
||||
@@ -6278,22 +6303,22 @@ snapshots:
|
||||
|
||||
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:
|
||||
'@next/eslint-plugin-next': 15.5.16
|
||||
'@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)
|
||||
'@next/eslint-plugin-next': 16.2.6
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
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-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-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:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- '@typescript-eslint/parser'
|
||||
- eslint-import-resolver-webpack
|
||||
- eslint-plugin-import-x
|
||||
- supports-color
|
||||
@@ -6380,9 +6405,16 @@ snapshots:
|
||||
safe-regex-test: 1.1.0
|
||||
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:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/parser': 7.29.3
|
||||
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)):
|
||||
dependencies:
|
||||
@@ -6741,6 +6773,8 @@ snapshots:
|
||||
|
||||
globals@14.0.0: {}
|
||||
|
||||
globals@16.4.0: {}
|
||||
|
||||
globalthis@1.0.4:
|
||||
dependencies:
|
||||
define-properties: 1.2.1
|
||||
@@ -6783,6 +6817,12 @@ snapshots:
|
||||
'@types/set-cookie-parser': 2.4.10
|
||||
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: {}
|
||||
|
||||
http-errors@2.0.1:
|
||||
@@ -7182,24 +7222,25 @@ snapshots:
|
||||
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:
|
||||
'@next/env': 15.5.16
|
||||
'@next/env': 16.2.6
|
||||
'@swc/helpers': 0.5.15
|
||||
baseline-browser-mapping: 2.10.27
|
||||
caniuse-lite: 1.0.30001792
|
||||
postcss: 8.4.31
|
||||
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)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 15.5.16
|
||||
'@next/swc-darwin-x64': 15.5.16
|
||||
'@next/swc-linux-arm64-gnu': 15.5.16
|
||||
'@next/swc-linux-arm64-musl': 15.5.16
|
||||
'@next/swc-linux-x64-gnu': 15.5.16
|
||||
'@next/swc-linux-x64-musl': 15.5.16
|
||||
'@next/swc-win32-arm64-msvc': 15.5.16
|
||||
'@next/swc-win32-x64-msvc': 15.5.16
|
||||
'@next/swc-darwin-arm64': 16.2.6
|
||||
'@next/swc-darwin-x64': 16.2.6
|
||||
'@next/swc-linux-arm64-gnu': 16.2.6
|
||||
'@next/swc-linux-arm64-musl': 16.2.6
|
||||
'@next/swc-linux-x64-gnu': 16.2.6
|
||||
'@next/swc-linux-x64-musl': 16.2.6
|
||||
'@next/swc-win32-arm64-msvc': 16.2.6
|
||||
'@next/swc-win32-x64-msvc': 16.2.6
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
@@ -8163,6 +8204,17 @@ snapshots:
|
||||
possible-typed-array-names: 1.1.0
|
||||
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: {}
|
||||
|
||||
unbox-primitive@1.1.0:
|
||||
@@ -8356,6 +8408,10 @@ snapshots:
|
||||
dependencies:
|
||||
zod: 3.25.76
|
||||
|
||||
zod-validation-error@4.0.2(zod@4.4.3):
|
||||
dependencies:
|
||||
zod: 4.4.3
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
||||
zod@4.4.3: {}
|
||||
|
||||
@@ -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
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -11,7 +15,7 @@
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
@@ -20,9 +24,19 @@
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user