feat(dashboard): implement daily journal functionality with CRUD operations

- Added actions for creating, updating, and deleting daily log records in `actions.ts`.
- Introduced `journal-client.tsx` for rendering the journal UI, including log entries and statistics.
- Refactored `page.tsx` to fetch user-specific daily logs from Supabase and display them using the new client component.
- Implemented form handling for daily log creation and updates, including mood and energy scoring.
This commit is contained in:
Poyraz Avsever
2026-06-04 19:53:51 +03:00
parent 7ee3104158
commit dd5ae0617e
6 changed files with 1421 additions and 676 deletions
+106
View File
@@ -0,0 +1,106 @@
"use server";
import { createClient } from "@/lib/supabase/server";
import { revalidatePath } from "next/cache";
function cleanText(value: FormDataEntryValue | null) {
const text = typeof value === "string" ? value.trim() : "";
return text.length > 0 ? text : null;
}
function readScore(value: FormDataEntryValue | null) {
const score = Number(typeof value === "string" ? value : value?.toString());
return Number.isInteger(score) && score >= 1 && score <= 5 ? score : null;
}
async function getCurrentUserId() {
const supabase = await createClient();
const {
data: { user },
error,
} = await supabase.auth.getUser();
if (error || !user) {
throw new Error("Günlük kaydı için giriş yapmış kullanıcı bulunamadı.");
}
return { supabase, userId: user.id };
}
function readPayload(formData: FormData) {
return {
log_date: cleanText(formData.get("log_date")) || new Date().toISOString().slice(0, 10),
mood_score: readScore(formData.get("mood_score")),
energy_score: readScore(formData.get("energy_score")),
work_satisfaction_score: readScore(formData.get("work_satisfaction_score")),
note: cleanText(formData.get("note")),
};
}
export async function createDailyLogRecord(formData: FormData) {
const { supabase, userId } = await getCurrentUserId();
const payload = readPayload(formData);
if (!payload.mood_score || !payload.energy_score) {
throw new Error("Mood ve enerji skorları zorunludur.");
}
const { error } = await supabase
.from("daily_logs")
.upsert(
{
user_id: userId,
...payload,
},
{ onConflict: "user_id,log_date" },
);
if (error) {
throw new Error(`Günlük kaydı eklenemedi: ${error.message}`);
}
revalidatePath("/journal");
}
export async function updateDailyLogRecord(formData: FormData) {
const { supabase, userId } = await getCurrentUserId();
const id = cleanText(formData.get("id"));
const payload = readPayload(formData);
if (!id || !payload.mood_score || !payload.energy_score) {
throw new Error("Günlük kaydını güncellemek için kayıt kimliği, mood ve enerji skorları zorunludur.");
}
const { error } = await supabase
.from("daily_logs")
.update(payload)
.eq("id", id)
.eq("user_id", userId);
if (error) {
throw new Error(`Günlük kaydı güncellenemedi: ${error.message}`);
}
revalidatePath("/journal");
}
export async function deleteDailyLogRecord(formData: FormData) {
const { supabase, userId } = await getCurrentUserId();
const id = cleanText(formData.get("id"));
if (!id) {
throw new Error("Silinecek günlük kaydı bulunamadı.");
}
const { error } = await supabase
.from("daily_logs")
.delete()
.eq("id", id)
.eq("user_id", userId);
if (error) {
throw new Error(`Günlük kaydı silinemedi: ${error.message}`);
}
revalidatePath("/journal");
}
+511
View File
@@ -0,0 +1,511 @@
"use client";
import {
createDailyLogRecord,
deleteDailyLogRecord,
updateDailyLogRecord,
} from "@/app/(dashboard)/journal/actions";
import { Badge, Button, Card, CardContent, Input, Label, Textarea } from "poyraz-ui/atoms";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "poyraz-ui/molecules";
import {
Activity,
Battery,
CalendarDays,
LineChart as LineChartIcon,
Pencil,
Plus,
Smile,
Trash2,
} from "lucide-react";
import {
CartesianGrid,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import type { ReactNode } from "react";
import { useMemo, useState } from "react";
export type DailyLogItem = {
id: string;
log_date: string;
mood_score: number;
energy_score: number;
work_satisfaction_score: number | null;
note: string | null;
};
type JournalClientProps = {
logs: DailyLogItem[];
};
const scoreLabels: Record<number, string> = {
1: "Çok düşük",
2: "Düşük",
3: "Orta",
4: "İyi",
5: "Çok iyi",
};
export function JournalClient({ logs }: JournalClientProps) {
const [monthFilter, setMonthFilter] = useState(() => new Date().toISOString().slice(0, 7));
const filteredLogs = logs.filter((log) => log.log_date.startsWith(monthFilter));
const summary = useMemo(() => calculateSummary(filteredLogs), [filteredLogs]);
const chartData = useMemo(
() =>
[...filteredLogs]
.sort((a, b) => a.log_date.localeCompare(b.log_date))
.map((log) => ({
date: formatShortDate(log.log_date),
mood: log.mood_score,
energy: log.energy_score,
satisfaction: log.work_satisfaction_score,
})),
[filteredLogs],
);
return (
<div className="mx-auto flex max-w-7xl flex-col gap-6">
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Activity className="h-4 w-4" />
Günlük durum
</div>
<div>
<h1 className="text-3xl font-semibold tracking-normal text-foreground">
Mood ve enerji
</h1>
<p className="mt-1 max-w-2xl text-sm text-muted-foreground">
Günlük ruh hali, enerji ve çalışma memnuniyetini takip ederek kişisel kapasite trendini gör.
</p>
</div>
</div>
<div className="flex flex-col gap-2 sm:flex-row">
<Input
type="month"
value={monthFilter}
onChange={(event) => setMonthFilter(event.target.value)}
className="sm:w-44"
/>
<DailyLogDialog mode="create" />
</div>
</div>
<div className="grid gap-3 md:grid-cols-4">
<StatCard
label="Ortalama mood"
value={summary.moodAverage ? summary.moodAverage.toFixed(1) : "-"}
icon={<Smile className="h-5 w-5" />}
tone="primary"
/>
<StatCard
label="Ortalama enerji"
value={summary.energyAverage ? summary.energyAverage.toFixed(1) : "-"}
icon={<Battery className="h-5 w-5" />}
tone="green"
/>
<StatCard
label="Memnuniyet"
value={summary.satisfactionAverage ? summary.satisfactionAverage.toFixed(1) : "-"}
icon={<LineChartIcon className="h-5 w-5" />}
tone="blue"
/>
<StatCard
label="Kayıtlı gün"
value={String(filteredLogs.length)}
icon={<CalendarDays className="h-5 w-5" />}
tone="amber"
/>
</div>
<div className="grid gap-6 lg:grid-cols-[1fr_360px]">
<Card>
<CardContent className="space-y-4 p-4">
<div>
<h2 className="text-base font-semibold text-foreground">Aylık trend</h2>
<p className="text-sm text-muted-foreground">
Mood, enerji ve çalışma memnuniyetinin günlük değişimi.
</p>
</div>
{chartData.length > 0 ? (
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData} margin={{ left: -16, right: 16, top: 12, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis dataKey="date" tickLine={false} axisLine={false} fontSize={12} />
<YAxis domain={[1, 5]} tickCount={5} tickLine={false} axisLine={false} fontSize={12} />
<Tooltip
contentStyle={{
border: "1px solid hsl(var(--border))",
borderRadius: 4,
boxShadow: "0 10px 30px rgba(15, 23, 42, 0.08)",
}}
/>
<Line type="monotone" dataKey="mood" name="Mood" stroke="#dc2626" strokeWidth={3} dot={{ r: 3 }} />
<Line type="monotone" dataKey="energy" name="Enerji" stroke="#059669" strokeWidth={3} dot={{ r: 3 }} />
<Line
type="monotone"
dataKey="satisfaction"
name="Memnuniyet"
stroke="#2563eb"
strokeWidth={3}
dot={{ r: 3 }}
connectNulls
/>
</LineChart>
</ResponsiveContainer>
</div>
) : (
<EmptyState />
)}
</CardContent>
</Card>
<Card>
<CardContent className="space-y-4 p-4">
<div>
<h2 className="text-base font-semibold text-foreground">Kapasite sinyali</h2>
<p className="text-sm text-muted-foreground">Bu ayki günlük kayıtlardan kısa okuma.</p>
</div>
<div className="space-y-3 text-sm text-muted-foreground">
{summary.insights.map((insight) => (
<div key={insight} className="rounded-sm border border-border bg-muted/20 p-3">
{insight}
</div>
))}
</div>
</CardContent>
</Card>
</div>
<Card>
<CardContent className="space-y-4 p-4">
<div className="flex items-center justify-between gap-3">
<div>
<h2 className="text-base font-semibold text-foreground">Günlük kayıtlar</h2>
<p className="text-sm text-muted-foreground">{filteredLogs.length} kayıt görüntüleniyor.</p>
</div>
</div>
{filteredLogs.length > 0 ? (
<div className="overflow-hidden rounded-sm border border-border">
<div className="hidden grid-cols-[0.7fr_0.7fr_0.7fr_1.8fr_0.8fr] gap-4 border-b border-border bg-muted/40 px-4 py-3 text-xs font-medium uppercase text-muted-foreground lg:grid">
<span>Tarih</span>
<span>Mood</span>
<span>Enerji</span>
<span>Not</span>
<span className="text-right">İşlem</span>
</div>
<div className="divide-y divide-border">
{filteredLogs.map((log) => (
<DailyLogRow key={log.id} log={log} />
))}
</div>
</div>
) : (
<EmptyState />
)}
</CardContent>
</Card>
</div>
);
}
function DailyLogRow({ log }: { log: DailyLogItem }) {
return (
<div className="grid gap-4 px-4 py-4 lg:grid-cols-[0.7fr_0.7fr_0.7fr_1.8fr_0.8fr] lg:items-center">
<div>
<div className="font-medium text-foreground">{formatDate(log.log_date)}</div>
<div className="text-xs text-muted-foreground">{formatWeekday(log.log_date)}</div>
</div>
<ScoreBadge score={log.mood_score} tone="primary" />
<ScoreBadge score={log.energy_score} tone="green" />
<div className="min-w-0 text-sm text-muted-foreground">
<p className="line-clamp-2">{log.note || "Not eklenmedi."}</p>
{log.work_satisfaction_score ? (
<p className="mt-1 text-xs">Çalışma memnuniyeti: {log.work_satisfaction_score}/5</p>
) : null}
</div>
<div className="flex justify-start gap-2 lg:justify-end">
<DailyLogDialog mode="edit" log={log} />
<form action={deleteDailyLogRecord}>
<input type="hidden" name="id" value={log.id} />
<Button type="submit" variant="outline" className="h-9 gap-2 text-rose-600">
<Trash2 className="h-4 w-4" />
Sil
</Button>
</form>
</div>
</div>
);
}
function DailyLogDialog({ mode, log }: { mode: "create" | "edit"; log?: DailyLogItem }) {
const [open, setOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const action = mode === "create" ? createDailyLogRecord : updateDailyLogRecord;
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
try {
await action(formData);
setOpen(false);
} finally {
setIsSubmitting(false);
}
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant={mode === "create" ? "default" : "outline"} className="h-9 gap-2">
{mode === "create" ? <Plus className="h-4 w-4" /> : <Pencil className="h-4 w-4" />}
{mode === "create" ? "Günlük ekle" : "Düzenle"}
</Button>
</DialogTrigger>
<DialogContent className="max-h-[min(640px,calc(100dvh-6rem))] overflow-hidden sm:max-w-xl data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95">
<form action={handleSubmit} className="flex max-h-[min(600px,calc(100dvh-9rem))] flex-col">
{log ? <input type="hidden" name="id" value={log.id} /> : null}
<DialogHeader className="shrink-0 pb-5">
<DialogTitle>{mode === "create" ? "Yeni günlük kayıt" : "Günlük kaydı düzenle"}</DialogTitle>
<DialogDescription>
Günün mood, enerji ve çalışma memnuniyeti skorlarını kaydet.
</DialogDescription>
</DialogHeader>
<div className="tiny-scrollbar min-h-0 flex-1 overflow-y-auto pr-2">
<DailyLogFormFields log={log} />
</div>
<DialogFooter className="shrink-0 border-t border-border pt-5">
<Button type="submit" disabled={isSubmitting} className="gap-2">
{isSubmitting ? "Kaydediliyor" : mode === "create" ? "Kaydı ekle" : "Değişiklikleri kaydet"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
function DailyLogFormFields({ log }: { log?: DailyLogItem }) {
const [moodScore, setMoodScore] = useState(log?.mood_score || 3);
const [energyScore, setEnergyScore] = useState(log?.energy_score || 3);
const [satisfactionScore, setSatisfactionScore] = useState(log?.work_satisfaction_score || 3);
return (
<div className="grid gap-5">
<div className="grid gap-2">
<Label>Tarih</Label>
<Input
name="log_date"
type="date"
defaultValue={log?.log_date || new Date().toISOString().slice(0, 10)}
/>
</div>
<ScorePicker
name="mood_score"
label="Mood skoru"
value={moodScore}
onChange={setMoodScore}
tone="primary"
/>
<ScorePicker
name="energy_score"
label="Enerji skoru"
value={energyScore}
onChange={setEnergyScore}
tone="green"
/>
<ScorePicker
name="work_satisfaction_score"
label="Çalışma memnuniyeti"
value={satisfactionScore}
onChange={setSatisfactionScore}
tone="blue"
/>
<div className="grid gap-2">
<Label>Not</Label>
<Textarea
name="note"
defaultValue={log?.note || ""}
rows={4}
placeholder="Bugün nasıl geçti, enerjini etkileyen şeyler nelerdi?"
/>
</div>
</div>
);
}
function ScorePicker({
name,
label,
value,
onChange,
tone,
}: {
name: string;
label: string;
value: number;
onChange: (value: number) => void;
tone: "primary" | "green" | "blue";
}) {
return (
<div className="grid gap-2">
<div className="flex items-center justify-between gap-3">
<Label>{label}</Label>
<span className="text-sm text-muted-foreground">{scoreLabels[value]}</span>
</div>
<input type="hidden" name={name} value={value} />
<div className="grid grid-cols-5 gap-2">
{[1, 2, 3, 4, 5].map((score) => (
<button
key={score}
type="button"
onClick={() => onChange(score)}
className={`h-10 rounded-sm border text-sm font-semibold transition-colors ${
value === score
? getScoreActiveClass(tone)
: "border-border bg-background text-muted-foreground hover:border-primary/40"
}`}
>
{score}
</button>
))}
</div>
</div>
);
}
function ScoreBadge({ score, tone }: { score: number; tone: "primary" | "green" }) {
const className =
tone === "green"
? "border-emerald-200 bg-emerald-50 text-emerald-700"
: "border-primary/20 bg-primary/10 text-primary";
return <Badge className={className}>{score}/5 · {scoreLabels[score]}</Badge>;
}
function StatCard({
label,
value,
icon,
tone,
}: {
label: string;
value: string;
icon: ReactNode;
tone: "primary" | "green" | "blue" | "amber";
}) {
const toneClass = {
primary: "bg-primary/10 text-primary",
green: "bg-emerald-50 text-emerald-700",
blue: "bg-blue-50 text-blue-700",
amber: "bg-amber-50 text-amber-700",
}[tone];
return (
<Card>
<CardContent className="flex items-center justify-between gap-3 p-4">
<div>
<p className="text-sm text-muted-foreground">{label}</p>
<p className="mt-1 text-2xl font-semibold text-foreground">{value}</p>
</div>
<div className={`flex h-10 w-10 items-center justify-center rounded-sm ${toneClass}`}>
{icon}
</div>
</CardContent>
</Card>
);
}
function EmptyState() {
return (
<div className="flex min-h-72 flex-col items-center justify-center rounded-sm border border-dashed border-border bg-muted/20 p-8 text-center">
<Activity className="h-10 w-10 text-muted-foreground" />
<h3 className="mt-4 text-lg font-semibold text-foreground">Bu ay günlük kayıt yok</h3>
<p className="mt-2 max-w-md text-sm text-muted-foreground">
Mood ve enerji trendini görmek için ilk günlük kaydını ekle.
</p>
</div>
);
}
function calculateSummary(logs: DailyLogItem[]) {
const moodAverage = average(logs.map((log) => log.mood_score));
const energyAverage = average(logs.map((log) => log.energy_score));
const satisfactionAverage = average(
logs
.map((log) => log.work_satisfaction_score)
.filter((score): score is number => typeof score === "number"),
);
const insights = [];
if (logs.length === 0) {
insights.push("Bu ay için henüz okunabilir bir trend yok.");
} else {
insights.push(`Bu ay ${logs.length} günlük kayıt var.`);
insights.push(
energyAverage && energyAverage < 3
? "Enerji ortalaması düşük. Dashboard raporlarında geciken işler ile birlikte okunmalı."
: "Enerji ortalaması dengeli görünüyor.",
);
insights.push(
moodAverage && moodAverage >= 4
? "Mood seviyesi güçlü. Yüksek odak isteyen işler için iyi bir dönem olabilir."
: "Mood trendi izlenmeli. Not alanı hangi günlerin zor geçtiğini anlamak için önemli.",
);
}
return { moodAverage, energyAverage, satisfactionAverage, insights };
}
function average(values: number[]) {
if (values.length === 0) return 0;
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
function getScoreActiveClass(tone: "primary" | "green" | "blue") {
if (tone === "green") return "border-emerald-600 bg-emerald-600 text-white";
if (tone === "blue") return "border-blue-600 bg-blue-600 text-white";
return "border-primary bg-primary text-primary-foreground";
}
function formatDate(value: string) {
return new Intl.DateTimeFormat("tr-TR", {
day: "2-digit",
month: "short",
year: "numeric",
}).format(new Date(`${value}T00:00:00`));
}
function formatShortDate(value: string) {
return new Intl.DateTimeFormat("tr-TR", {
day: "2-digit",
month: "short",
}).format(new Date(`${value}T00:00:00`));
}
function formatWeekday(value: string) {
return new Intl.DateTimeFormat("tr-TR", {
weekday: "long",
}).format(new Date(`${value}T00:00:00`));
}
+34 -287
View File
@@ -1,294 +1,41 @@
"use client";
import { JournalClient, type DailyLogItem } from "@/app/(dashboard)/journal/journal-client";
import { createClient } from "@/lib/supabase/server";
import { useState } from "react";
import {
Plus, Search, Calendar, Clock, Brain, MessageSquare,
Smile, Frown, Meh, Star, MoreHorizontal, X,
Zap, Save, Trash2, Edit3, Image as ImageIcon, Link as LinkIcon,
ChevronLeft, ChevronRight, Activity, Filter, AlignLeft, Hash, ArrowRight
} from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
type DailyLogRow = {
id: string;
log_date: string;
mood_score: number;
energy_score: number;
work_satisfaction_score: number | null;
note: string | null;
};
// Mock Data
const journalEntries = [
{
id: 1,
date: "May 15, 2026",
title: "Deep Work Breakthrough",
excerpt: "Today I finally cracked the multi-tenant architecture logic for the Cognis core. Energy was high after the morning focus block.",
sentiment: "Great",
moodScore: 92,
tags: ["Productivity", "Coding"],
content: "The morning started with a 4-hour deep work block. I avoided all notifications and focused purely on the database schema. The multi-tenant logic is now solid. I feel a huge weight off my shoulders. Physical energy was 9/10 thanks to the 7am gym session."
},
{
id: 2,
date: "May 14, 2026",
title: "Project Risk Discussion",
excerpt: "Met with Alex regarding the Infrastructure delays. Felt a bit anxious about the timeline but the AI risk report helped us focus.",
sentiment: "Neutral",
moodScore: 65,
tags: ["Meeting", "Stress"],
content: "Alex and I went through the infrastructure roadmap. We are indeed behind on the database migration. The stress is real, but we have a plan now. AI suggests prioritizing the migration scripts. Note for tomorrow: focus on script optimization."
},
{ id: 3, date: "May 12, 2026", title: "Creative Flow", excerpt: "Spent the afternoon in Figma. The new 'Cyber-Lavender' palette is looking stunning in the dark mode previews.", sentiment: "Great", moodScore: 88, tags: ["Design", "Creative"] },
];
export default async function JournalPage() {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
export default function JournalPage() {
const [selectedEntry, setSelectedEntry] = useState<any>(null);
const [isCreating, setIsCreating] = useState(false);
if (!user) {
return null;
}
return (
<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 pb-12 relative">
{/* Top Header */}
<div className="flex items-center justify-between pb-4 border-b border-white/5 mt-4 shrink-0">
<div className="flex items-center gap-4">
<h1 className="text-lg font-medium text-muted-foreground">
<span className="text-foreground">Mindset</span> / Strategic Journal
</h1>
<div className="h-4 w-px bg-white/10" />
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-primary">
<Edit3 className="h-3 w-3" /> 128 ENTRIES RECORDED
</div>
</div>
<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 thoughts..."
className="bg-transparent border-none outline-none text-xs w-48 placeholder:text-muted-foreground/50 text-foreground"
/>
</div>
<button
onClick={() => setIsCreating(true)}
className="bg-primary hover:bg-primary/90 text-primary-foreground border border-primary/20 px-5 py-2 rounded-sm text-xs font-black tracking-widest flex items-center gap-2 transition-all shadow-lg shadow-primary/30 active:scale-95"
>
<Plus className="h-4 w-4" />
NEW ENTRY
</button>
</div>
</div>
const { data: logRows } = await supabase
.from("daily_logs")
.select("id, log_date, mood_score, energy_score, work_satisfaction_score, note")
.eq("user_id", user.id)
.order("log_date", { ascending: false })
.limit(180);
<div className="flex-1 flex gap-8 min-h-0">
{/* Left Sidebar: Entries List */}
<div className="w-full max-w-sm flex flex-col gap-4 overflow-y-auto tiny-scrollbar pr-2">
<div className="flex items-center justify-between px-2 mb-2">
<h3 className="text-[10px] font-black uppercase tracking-[0.3em] text-muted-foreground">Recent Reflections</h3>
<button className="text-[10px] font-bold text-muted-foreground hover:text-foreground flex items-center gap-1">
<Filter className="h-3 w-3" /> FILTER
</button>
</div>
<div className="space-y-4">
{journalEntries.map(entry => (
<motion.div
key={entry.id}
whileHover={{ x: 4 }}
onClick={() => setSelectedEntry(entry)}
className={`p-5 rounded-sm border cursor-pointer transition-all ${selectedEntry?.id === entry.id ? 'bg-[#1F172B] border-primary/40 shadow-xl' : 'bg-[#0A0710] border-white/5 hover:border-white/10'}`}
>
<div className="flex justify-between items-start mb-3">
<span className="text-[10px] font-black uppercase tracking-widest text-muted-foreground">{entry.date}</span>
<SentimentIcon sentiment={entry.sentiment} />
</div>
<h3 className={`text-sm font-black mb-2 transition-colors ${selectedEntry?.id === entry.id ? 'text-primary' : 'text-foreground'}`}>{entry.title}</h3>
<p className="text-[11px] text-muted-foreground leading-relaxed line-clamp-2 italic">"{entry.excerpt}"</p>
<div className="flex gap-2 mt-4">
{entry.tags.map(tag => (
<span key={tag} className="text-[8px] font-black uppercase tracking-widest px-2 py-0.5 rounded-sm bg-white/5 text-muted-foreground">#{tag}</span>
))}
</div>
</motion.div>
))}
</div>
</div>
const logs: DailyLogItem[] = ((logRows || []) as DailyLogRow[]).map((log) => ({
id: log.id,
log_date: log.log_date,
mood_score: Number(log.mood_score),
energy_score: Number(log.energy_score),
work_satisfaction_score:
typeof log.work_satisfaction_score === "number" ? Number(log.work_satisfaction_score) : null,
note: log.note,
}));
{/* Main Content: Entry Viewer/Editor */}
<div className="flex-1 bg-[#0A0710] border border-white/5 rounded-sm flex flex-col relative overflow-hidden shadow-2xl">
<AnimatePresence mode="wait">
{selectedEntry ? (
<motion.div
key={selectedEntry.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="flex-1 flex flex-col"
>
<div className="p-10 border-b border-white/5 flex items-center justify-between bg-primary/5">
<div className="space-y-1">
<div className="flex items-center gap-3">
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-primary">{selectedEntry.date}</span>
<div className="w-1.5 h-1.5 rounded-full bg-white/20" />
<div className="flex items-center gap-1.5 text-[10px] font-black text-emerald-400">
<Activity className="h-3 w-3" /> MOOD SCORE: {selectedEntry.moodScore}%
</div>
</div>
<h2 className="text-3xl font-black tracking-tighter text-foreground">{selectedEntry.title}</h2>
</div>
<div className="flex gap-2">
<button className="p-2.5 hover:bg-white/5 rounded-sm text-muted-foreground transition-colors"><Edit3 className="h-5 w-5" /></button>
<button className="p-2.5 hover:bg-white/5 rounded-sm text-rose-500 transition-colors"><Trash2 className="h-5 w-5" /></button>
</div>
</div>
<div className="flex-1 p-10 overflow-y-auto tiny-scrollbar space-y-12">
{/* AI Psychological Insight */}
<div className="rounded-sm border border-primary/20 bg-primary/5 p-8 space-y-4 relative overflow-hidden">
<div className="absolute -right-4 -top-4 opacity-5">
<Brain className="h-24 w-24 text-primary" />
</div>
<h3 className="text-[10px] font-black uppercase tracking-[0.2em] text-primary flex items-center gap-2">
<Brain className="h-4 w-4" /> Cognitive Analysis
</h3>
<p className="text-sm font-medium text-foreground/90 leading-relaxed italic">
"I've noticed that your mood scores peak when you mention 'Deep Work' blocks in the morning. However, meeting-heavy days seem to correlate with anxious sentiment. Consider restructuring 'Infrastructure' discussions for early PM."
</p>
</div>
{/* Body Content */}
<div className="space-y-6">
<p className="text-lg text-foreground/90 leading-[1.8] font-medium tracking-tight">
{selectedEntry.content || "No detailed content available for this entry."}
</p>
</div>
{/* Strategic Connections */}
<div className="pt-10 border-t border-white/5 space-y-6">
<h3 className="text-[10px] font-black uppercase tracking-[0.2em] text-primary">Connected Strategics</h3>
<div className="grid grid-cols-2 gap-4">
<div className="p-4 rounded-sm bg-[#150F1D] border border-white/5 flex items-center justify-between group cursor-pointer hover:border-primary/30 transition-all">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-sm"><Zap className="h-4 w-4 text-primary" /></div>
<div>
<div className="text-xs font-bold text-foreground">Goal: Scaling Backend</div>
<div className="text-[9px] text-muted-foreground uppercase mt-1">Directly Referenced</div>
</div>
</div>
<ArrowRight className="h-3 w-3 text-muted-foreground group-hover:text-primary transition-all" />
</div>
<div className="p-4 rounded-sm bg-[#150F1D] border border-white/5 flex items-center justify-between group cursor-pointer hover:border-primary/30 transition-all">
<div className="flex items-center gap-3">
<div className="p-2 bg-emerald-500/10 rounded-sm"><Clock className="h-4 w-4 text-emerald-500" /></div>
<div>
<div className="text-xs font-bold text-foreground">Habit: 7AM Gym</div>
<div className="text-[9px] text-muted-foreground uppercase mt-1">Impact Observed</div>
</div>
</div>
<ArrowRight className="h-3 w-3 text-muted-foreground group-hover:text-primary transition-all" />
</div>
</div>
</div>
</div>
<div className="p-8 border-t border-white/5 bg-[#0F0B15]/40 flex justify-between items-center">
<div className="flex -space-x-2">
{[1, 2, 3].map(i => <div key={i} className="h-8 w-8 rounded-full border-2 border-[#0A0710] bg-[#1F172B] flex items-center justify-center text-[10px] font-bold">A{i}</div>)}
<div className="h-8 w-8 rounded-full border-2 border-[#0A0710] bg-primary/20 flex items-center justify-center text-[10px] font-bold text-primary">+2</div>
</div>
<button className="text-[10px] font-black text-primary uppercase tracking-widest flex items-center gap-2 hover:bg-primary/10 px-4 py-2 rounded-sm transition-all">
<MessageSquare className="h-4 w-4" /> 12 COMMENTS
</button>
</div>
</motion.div>
) : (
<div className="flex-1 flex flex-col items-center justify-center p-20 text-center space-y-6">
<div className="p-6 bg-white/5 rounded-full">
<AlignLeft className="h-12 w-12 text-muted-foreground/30" />
</div>
<div>
<h2 className="text-xl font-black uppercase tracking-widest text-muted-foreground">No Entry Selected</h2>
<p className="text-sm text-muted-foreground mt-2 max-w-xs mx-auto italic">Select a reflection from the sidebar or create a new strategic entry to begin.</p>
</div>
<button
onClick={() => setIsCreating(true)}
className="bg-primary/10 text-primary border border-primary/20 px-6 py-2.5 rounded-sm text-xs font-black tracking-widest uppercase hover:bg-primary/20 transition-all"
>
Create Your First Entry
</button>
</div>
)}
</AnimatePresence>
</div>
</div>
{/* New Entry Modal */}
<AnimatePresence>
{isCreating && (
<>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={() => setIsCreating(false)} className="fixed inset-0 bg-black/90 backdrop-blur-xl z-[100]" />
<motion.div initial={{ scale: 0.95, opacity: 0, y: 20 }} animate={{ scale: 1, opacity: 1, y: 0 }} exit={{ scale: 0.95, opacity: 0, y: 20 }} className="fixed inset-0 m-auto w-full max-w-4xl h-[85vh] bg-[#0A0710] border border-white/10 z-[101] shadow-2xl flex flex-col rounded-sm overflow-hidden">
<div className="p-8 border-b border-white/5 flex items-center justify-between bg-primary/10">
<div className="flex items-center gap-4">
<div className="p-2.5 bg-primary rounded-sm shadow-xl shadow-primary/20"><Edit3 className="h-5 w-5 text-primary-foreground" /></div>
<div>
<h2 className="text-xl font-black uppercase tracking-tight">New Reflection</h2>
<p className="text-[10px] font-black text-primary tracking-[0.3em] uppercase">Documenting Strategic Growth</p>
</div>
</div>
<button onClick={() => setIsCreating(false)} className="p-2 hover:bg-white/10 rounded-sm text-muted-foreground transition-colors"><X className="h-7 w-7" /></button>
</div>
<div className="flex-1 flex flex-col p-10 space-y-8 overflow-y-auto tiny-scrollbar">
<div className="space-y-2">
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-[0.2em]">Title of Reflection</label>
<input type="text" placeholder="e.g., Q3 Breakthrough or Team Alignment thoughts..." className="w-full bg-transparent border-none text-3xl font-black placeholder:text-muted-foreground/20 outline-none" />
</div>
<div className="flex gap-6 pb-6 border-b border-white/5">
<div className="flex items-center gap-3 bg-white/5 px-4 py-2 rounded-sm border border-white/5">
<Calendar className="h-4 w-4 text-primary" />
<span className="text-xs font-bold">May 15, 2026</span>
</div>
<div className="flex items-center gap-4">
<span className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Sentiment:</span>
<div className="flex gap-2">
<button className="p-2 rounded-sm bg-white/5 hover:bg-emerald-500/20 hover:text-emerald-500 transition-all"><Smile className="h-5 w-5" /></button>
<button className="p-2 rounded-sm bg-white/5 hover:bg-primary/20 hover:text-primary transition-all"><Meh className="h-5 w-5" /></button>
<button className="p-2 rounded-sm bg-white/5 hover:bg-rose-500/20 hover:text-rose-500 transition-all"><Frown className="h-5 w-5" /></button>
</div>
</div>
</div>
<div className="flex-1 min-h-[300px]">
<textarea
placeholder="Write your strategic reflections here... Use # to link goals or tasks."
className="w-full h-full bg-transparent border-none outline-none text-lg leading-relaxed text-foreground/80 resize-none placeholder:text-muted-foreground/10 font-medium"
/>
</div>
<div className="flex items-center gap-4 pt-6 border-t border-white/5">
<button className="p-2 text-muted-foreground hover:text-primary transition-colors"><ImageIcon className="h-5 w-5" /></button>
<button className="p-2 text-muted-foreground hover:text-primary transition-colors"><LinkIcon className="h-5 w-5" /></button>
<button className="p-2 text-muted-foreground hover:text-primary transition-colors"><Hash className="h-5 w-5" /></button>
<div className="ml-auto flex items-center gap-2 text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest">
<Clock className="h-3 w-3" /> Auto-saving to Cloud...
</div>
</div>
</div>
<div className="p-10 border-t border-white/5 flex gap-6 bg-[#0F0B15]/60">
<button className="flex-1 bg-primary hover:bg-primary/90 text-primary-foreground py-5 rounded-sm text-[11px] font-black uppercase tracking-[0.3em] shadow-2xl shadow-primary/40 transition-all active:scale-[0.98] flex items-center justify-center gap-3">
<Save className="h-5 w-5" /> PUBLISH REFLECTION
</button>
<button onClick={() => setIsCreating(false)} className="px-10 border border-white/10 rounded-sm text-[11px] font-black uppercase tracking-[0.2em] text-muted-foreground hover:bg-white/5 transition-all">
DISCARD
</button>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</div>
);
}
function SentimentIcon({ sentiment }: { sentiment: string }) {
if (sentiment === "Great") return <Smile className="h-4 w-4 text-emerald-500" />;
if (sentiment === "Neutral") return <Meh className="h-4 w-4 text-primary" />;
return <Frown className="h-4 w-4 text-rose-500" />;
return <JournalClient logs={logs} />;
}