feat: implement dashboard layout with sidebar navigation, finance page, and supporting UI architecture
This commit is contained in:
+134
-228
@@ -1,245 +1,151 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Plus, Search, Filter, Brain, PenTool, Calendar, AlignLeft, Tag, Lock, Sparkles, Image as ImageIcon } from "lucide-react";
|
||||
|
||||
import { analyzeJournalWithLocalAI } from "@/lib/ai";
|
||||
import { db } from "@/lib/db";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
const moods = [
|
||||
{ id: "happy", label: "Mutlu", emoji: "😊" },
|
||||
{ id: "neutral", label: "Nötr", emoji: "😐" },
|
||||
{ id: "sad", label: "Üzgün", emoji: "😔" },
|
||||
{ id: "angry", label: "Sinirli", emoji: "😠" },
|
||||
// Mock Data
|
||||
const journalEntries = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Breakthrough in System Design",
|
||||
preview: "Today we finally cracked the database architecture. By moving to a distributed model...",
|
||||
date: "Today, 10:45 AM",
|
||||
tags: ["Work", "Idea", "Win"],
|
||||
sentiment: "Positive"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Feeling a bit overwhelmed",
|
||||
preview: "The Q3 deadlines are approaching fast. I need to make sure I'm prioritizing...",
|
||||
date: "Yesterday",
|
||||
tags: ["Mental Health", "Vent"],
|
||||
sentiment: "Anxious"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Morning Routine Reflection",
|
||||
preview: "Woke up at 6 AM. Did 20 minutes of meditation. It really sets the tone for...",
|
||||
date: "May 06, 2026",
|
||||
tags: ["Habits", "Morning"],
|
||||
sentiment: "Calm"
|
||||
}
|
||||
];
|
||||
|
||||
export default function JournalPage() {
|
||||
const [content, setContent] = useState("");
|
||||
const [mood, setMood] = useState("happy");
|
||||
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).
|
||||
const journals = useLiveQuery(() =>
|
||||
db.journals.orderBy("date").reverse().toArray(),
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
try {
|
||||
// Veriyi önce kaydet, AI analizini daha sonra arka planda tamamla.
|
||||
await db.journals.add({
|
||||
id: entryId,
|
||||
date: now.split("T")[0],
|
||||
mood,
|
||||
energy: parseInt(energy, 10),
|
||||
content: currentContent,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
});
|
||||
|
||||
setContent("");
|
||||
setEnergy("3");
|
||||
setMood("happy");
|
||||
|
||||
try {
|
||||
const aiResult = await analyzeJournalWithLocalAI(currentContent);
|
||||
if (aiResult) {
|
||||
await db.journals.update(entryId, {
|
||||
ai_tags: aiResult.ai_tags,
|
||||
ai_sentiment_score: aiResult.ai_sentiment_score,
|
||||
ai_summary: aiResult.ai_summary,
|
||||
});
|
||||
|
||||
if (aiResult.suggested_tasks?.length) {
|
||||
for (const taskTitle of aiResult.suggested_tasks) {
|
||||
await db.tasks.add({
|
||||
id: uuidv4(),
|
||||
journal_id: entryId,
|
||||
title: `AI Önerisi: ${taskTitle}`,
|
||||
status: "todo",
|
||||
ai_generated: true,
|
||||
date: now.split("T")[0],
|
||||
created_at: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (aiError) {
|
||||
console.error("Arka plan AI analizi hatası:", aiError);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Günlük kaydedilemedi:", error);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (confirm("Bu günlüğü silmek istediğinden emin misin?")) {
|
||||
await db.journals.delete(id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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">
|
||||
Zihnini boşalt, hislerini kaydet. Verilerin sadece cihazında kalır.
|
||||
</p>
|
||||
<div className="mx-auto max-w-7xl animate-in fade-in slide-in-from-bottom-4 duration-500 h-full flex flex-col text-foreground font-sans space-y-6">
|
||||
|
||||
{/* Top Header */}
|
||||
<div className="flex items-center justify-between pb-4 border-b border-white/5 mt-4">
|
||||
<h1 className="text-lg font-medium text-muted-foreground">
|
||||
<span className="text-foreground">Personal</span> / Daily Journal
|
||||
</h1>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-[#150F1D] border border-white/5 rounded-sm px-3 py-1.5 flex items-center gap-2">
|
||||
<Search className="h-4 w-4 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search memories..."
|
||||
className="bg-transparent border-none outline-none text-xs w-48 placeholder:text-muted-foreground/50 text-foreground"
|
||||
/>
|
||||
</div>
|
||||
<button className="bg-primary hover:bg-primary/90 text-primary-foreground border border-primary/20 px-4 py-1.5 rounded-sm text-xs font-semibold flex items-center gap-2 transition-colors">
|
||||
<Plus className="h-4 w-4" />
|
||||
NEW ENTRY
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Yeni Kayıt</CardTitle>
|
||||
<CardDescription>Bugün nasıl hissediyorsun?</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-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((currentMood) => (
|
||||
<button
|
||||
key={currentMood.id}
|
||||
type="button"
|
||||
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={currentMood.label}
|
||||
>
|
||||
{currentMood.emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Main Layout */}
|
||||
<div className="flex flex-col lg:flex-row gap-6 flex-1 min-h-0">
|
||||
|
||||
{/* Left Panel: Entry List & AI */}
|
||||
<div className="w-full lg:w-80 flex flex-col gap-6 shrink-0">
|
||||
|
||||
{/* AI Sentiment Analysis */}
|
||||
<div className="rounded-sm border border-primary/20 bg-[linear-gradient(135deg,rgba(108,91,176,0.1)_0%,rgba(10,7,16,0)_100%)] p-5">
|
||||
<h3 className="text-sm font-semibold text-primary mb-2 flex items-center gap-2">
|
||||
<Brain className="h-4 w-4" /> AI Reflection Insight
|
||||
</h3>
|
||||
<p className="text-[11px] text-foreground/80 leading-relaxed mb-3">
|
||||
Over the last 14 days, your entries tagged with <strong>"Work"</strong> show a 30% increase in stress vocabulary. However, entries after <strong>Morning Meditation</strong> are consistently highly positive.
|
||||
</p>
|
||||
<button className="bg-primary/20 text-primary hover:bg-primary/30 border border-primary/30 px-3 py-1.5 rounded-sm text-[10px] font-bold uppercase tracking-wider transition-colors w-full">
|
||||
Generate Weekly Summary
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Enerji Seviyesi (1-5): {energy}</Label>
|
||||
<Input
|
||||
type="range"
|
||||
min="1"
|
||||
max="5"
|
||||
step="1"
|
||||
value={energy}
|
||||
onChange={(event) => setEnergy(event.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
{/* Entries List */}
|
||||
<div className="flex-1 rounded-sm border border-white/5 bg-[#0A0710] flex flex-col overflow-hidden">
|
||||
<div className="p-4 border-b border-white/5 flex justify-between items-center">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Recent Entries</span>
|
||||
<Filter className="h-4 w-4 text-muted-foreground hover:text-foreground cursor-pointer" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="content">Düşüncelerini buraya dök...</Label>
|
||||
<Textarea
|
||||
id="content"
|
||||
value={content}
|
||||
onChange={(event) => setContent(event.target.value)}
|
||||
className="min-h-[150px] resize-y"
|
||||
placeholder="Örneğin: Bugün toplantıda işler ters gitti..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full md:w-auto"
|
||||
>
|
||||
{isSubmitting ? "Kaydediliyor..." : "Kaydet"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="space-y-4 pt-4">
|
||||
<h2 className="text-xl font-bold">Geçmiş Kayıtlar</h2>
|
||||
{!journals ? (
|
||||
<p className="text-sm text-muted-foreground">Yükleniyor...</p>
|
||||
) : journals.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Henüz kayıt bulunmuyor.
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{journals.map((journal) => (
|
||||
<Card key={journal.id}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<span>
|
||||
{moods.find((currentMood) => currentMood.id === journal.mood)
|
||||
?.emoji ?? "📝"}
|
||||
</span>
|
||||
<span>{journal.date}</span>
|
||||
</CardTitle>
|
||||
<span className="rounded-md bg-muted px-2 py-1 text-xs font-medium text-muted-foreground">
|
||||
Enerji: {journal.energy}/5
|
||||
<div className="flex-1 overflow-y-auto tiny-scrollbar p-2 space-y-1">
|
||||
{journalEntries.map((entry, idx) => (
|
||||
<div key={entry.id} className={`p-3 rounded-sm cursor-pointer transition-colors ${idx === 0 ? 'bg-[#1F172B] border border-primary/30' : 'hover:bg-white/5 border border-transparent'}`}>
|
||||
<h4 className="text-sm font-semibold text-foreground mb-1 truncate">{entry.title}</h4>
|
||||
<p className="text-[11px] text-muted-foreground line-clamp-2 leading-relaxed mb-2">
|
||||
{entry.preview}
|
||||
</p>
|
||||
<div className="flex justify-between items-center text-[10px]">
|
||||
<span className="text-primary font-medium">{entry.date}</span>
|
||||
<span className="flex gap-1">
|
||||
{entry.tags.map(tag => (
|
||||
<span key={tag} className="bg-white/10 px-1.5 py-0.5 rounded-sm">{tag}</span>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-2">
|
||||
<p className="whitespace-pre-wrap text-sm leading-relaxed">
|
||||
{journal.content}
|
||||
</p>
|
||||
|
||||
{journal.ai_tags && (
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{journal.ai_tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-md bg-primary/10 px-2 py-1 text-xs text-primary"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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="mt-4 flex justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-red-500 hover:bg-red-500/10 hover:text-red-600"
|
||||
onClick={() => handleDelete(journal.id)}
|
||||
>
|
||||
Sil
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Panel: Editor Mock */}
|
||||
<div className="flex-1 rounded-sm border border-white/5 bg-[#0A0710] flex flex-col relative overflow-hidden group">
|
||||
|
||||
{/* Editor Header */}
|
||||
<div className="p-4 border-b border-white/5 flex justify-between items-center bg-[#0F0B15]/50">
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-1.5"><Calendar className="h-4 w-4" /> Today, 10:45 AM</span>
|
||||
<span className="flex items-center gap-1.5"><Tag className="h-4 w-4" /> 3 Tags</span>
|
||||
<span className="flex items-center gap-1.5"><Lock className="h-4 w-4" /> Private</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="p-1.5 hover:bg-white/10 rounded-sm text-muted-foreground transition-colors"><AlignLeft className="h-4 w-4" /></button>
|
||||
<button className="p-1.5 hover:bg-white/10 rounded-sm text-muted-foreground transition-colors"><ImageIcon className="h-4 w-4" /></button>
|
||||
<button className="p-1.5 hover:bg-primary/20 bg-primary/10 rounded-sm text-primary transition-colors flex items-center gap-1">
|
||||
<Sparkles className="h-4 w-4" /> <span className="text-[10px] font-bold">Ask AI</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor Body */}
|
||||
<div className="flex-1 p-8 md:px-12 md:py-10 overflow-y-auto tiny-scrollbar">
|
||||
<input
|
||||
type="text"
|
||||
value="Breakthrough in System Design"
|
||||
readOnly
|
||||
className="w-full bg-transparent border-none outline-none text-3xl font-bold text-foreground mb-6"
|
||||
/>
|
||||
<div className="text-foreground/80 leading-loose space-y-6 text-sm">
|
||||
<p>
|
||||
Today we finally cracked the database architecture. By moving to a distributed model, we've essentially solved the latency issues we were seeing during peak load. It feels incredibly satisfying to see weeks of research finally click into place.
|
||||
</p>
|
||||
<p>
|
||||
I was discussing this with Alex earlier, and he pointed out that this structure directly mirrors the microservices split we planned for Q4. This means we are actually ahead of schedule!
|
||||
</p>
|
||||
<div className="pl-4 border-l-2 border-primary text-primary italic">
|
||||
AI Note: You've mentioned "latency issues" in 3 previous entries. Consider documenting this final solution in the Engineering Wiki for future reference.
|
||||
</div>
|
||||
<p>
|
||||
Gonna take the rest of the evening off to recharge. Need to keep this momentum going without burning out.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user