refactor: remove legacy layout components and unused mood tracker page
This commit is contained in:
@@ -1,442 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import Image from "next/image";
|
|
||||||
import logo from "../assets/logo.png";
|
|
||||||
import {
|
|
||||||
Cell,
|
|
||||||
Legend,
|
|
||||||
Line,
|
|
||||||
LineChart,
|
|
||||||
Pie,
|
|
||||||
PieChart,
|
|
||||||
ResponsiveContainer,
|
|
||||||
Tooltip,
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
} from "recharts";
|
|
||||||
import { FormEvent, useEffect, useMemo, useState } from "react";
|
|
||||||
|
|
||||||
type Mood = "happy" | "neutral" | "sad" | "angry";
|
|
||||||
type MoodScore = 4 | 3 | 2 | 1;
|
|
||||||
type Energy = 1 | 2 | 3 | 4 | 5;
|
|
||||||
|
|
||||||
type MoodEntry = {
|
|
||||||
id: string;
|
|
||||||
date: string;
|
|
||||||
mood: Mood;
|
|
||||||
mood_score: MoodScore;
|
|
||||||
energy: Energy;
|
|
||||||
};
|
|
||||||
|
|
||||||
const STORAGE_KEY = "mood-tracker-entries";
|
|
||||||
|
|
||||||
const moodConfig: Record<
|
|
||||||
Mood,
|
|
||||||
{ label: string; icon: string; score: MoodScore; color: string }
|
|
||||||
> = {
|
|
||||||
happy: { label: "Mutlu", icon: "😊", score: 4, color: "#2f7d63" },
|
|
||||||
neutral: { label: "Nötr", icon: "😐", score: 3, color: "#4f7fbf" },
|
|
||||||
sad: { label: "Üzgün", icon: "😔", score: 2, color: "#e3aa38" },
|
|
||||||
angry: { label: "Sinirli", icon: "😠", score: 1, color: "#d8644a" },
|
|
||||||
};
|
|
||||||
|
|
||||||
const moodOptions = Object.entries(moodConfig) as Array<
|
|
||||||
[Mood, (typeof moodConfig)[Mood]]
|
|
||||||
>;
|
|
||||||
|
|
||||||
const today = () => new Date().toISOString().slice(0, 10);
|
|
||||||
|
|
||||||
const createId = () =>
|
|
||||||
typeof crypto !== "undefined" && "randomUUID" in crypto
|
|
||||||
? crypto.randomUUID()
|
|
||||||
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
const [entries, setEntries] = useState<MoodEntry[]>([]);
|
|
||||||
const [date, setDate] = useState(today);
|
|
||||||
const [mood, setMood] = useState<Mood>("happy");
|
|
||||||
const [energy, setEnergy] = useState<Energy>(3);
|
|
||||||
const [message, setMessage] = useState("");
|
|
||||||
const [isLoaded, setIsLoaded] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const rawEntries = window.localStorage.getItem(STORAGE_KEY);
|
|
||||||
|
|
||||||
if (!rawEntries) {
|
|
||||||
setIsLoaded(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsedEntries = JSON.parse(rawEntries) as MoodEntry[];
|
|
||||||
if (Array.isArray(parsedEntries)) {
|
|
||||||
setEntries(sortEntries(parsedEntries));
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
window.localStorage.removeItem(STORAGE_KEY);
|
|
||||||
} finally {
|
|
||||||
setIsLoaded(true);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLoaded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
|
|
||||||
}, [entries, isLoaded]);
|
|
||||||
|
|
||||||
const lineData = useMemo(
|
|
||||||
() =>
|
|
||||||
entries.map((entry) => ({
|
|
||||||
date: formatShortDate(entry.date),
|
|
||||||
mood: entry.mood_score,
|
|
||||||
energy: entry.energy,
|
|
||||||
})),
|
|
||||||
[entries],
|
|
||||||
);
|
|
||||||
|
|
||||||
const pieData = useMemo(
|
|
||||||
() =>
|
|
||||||
moodOptions
|
|
||||||
.map(([key, config]) => ({
|
|
||||||
name: config.label,
|
|
||||||
value: entries.filter((entry) => entry.mood === key).length,
|
|
||||||
color: config.color,
|
|
||||||
}))
|
|
||||||
.filter((item) => item.value > 0),
|
|
||||||
[entries],
|
|
||||||
);
|
|
||||||
|
|
||||||
const insights = useMemo(() => buildInsights(entries), [entries]);
|
|
||||||
|
|
||||||
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (!date) {
|
|
||||||
setMessage("Lütfen tarih seç.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mood) {
|
|
||||||
setMessage("Lütfen ruh halini seç.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (energy < 1 || energy > 5) {
|
|
||||||
setMessage("Enerji seviyesi 1 ile 5 arasında olmalı.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const moodScore = moodConfig[mood].score;
|
|
||||||
|
|
||||||
setEntries((currentEntries) => {
|
|
||||||
const existingEntry = currentEntries.find((entry) => entry.date === date);
|
|
||||||
const nextEntry: MoodEntry = {
|
|
||||||
id: existingEntry?.id ?? createId(),
|
|
||||||
date,
|
|
||||||
mood,
|
|
||||||
mood_score: moodScore,
|
|
||||||
energy,
|
|
||||||
};
|
|
||||||
|
|
||||||
const nextEntries = existingEntry
|
|
||||||
? currentEntries.map((entry) =>
|
|
||||||
entry.date === date ? nextEntry : entry,
|
|
||||||
)
|
|
||||||
: [...currentEntries, nextEntry];
|
|
||||||
|
|
||||||
return sortEntries(nextEntries);
|
|
||||||
});
|
|
||||||
|
|
||||||
setMessage("Kayıt kaydedildi. Dashboard güncellendi.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="mx-auto flex min-h-screen w-full max-w-7xl flex-col gap-6 px-4 py-6 sm:px-6 lg:px-8">
|
|
||||||
<header className="flex flex-col gap-2 border-b border-ink/10 pb-5 sm:flex-row sm:items-end sm:justify-between">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Image
|
|
||||||
src={logo}
|
|
||||||
alt="Mood Tracker logo"
|
|
||||||
className="h-16 w-16 rounded-md object-contain sm:h-20 sm:w-20"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<h1 className="text-3xl font-bold text-ink sm:text-4xl">
|
|
||||||
Günlük ruh hali dashboard'u
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-md border border-ink/10 bg-white px-4 py-3 text-sm text-ink/70 shadow-soft">
|
|
||||||
{entries.length} kayıt
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section className="grid gap-5 lg:grid-cols-[360px_1fr]">
|
|
||||||
<motion.form
|
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.35 }}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
className="rounded-lg border border-ink/10 bg-white p-5 shadow-soft"
|
|
||||||
>
|
|
||||||
<div className="mb-5">
|
|
||||||
<h2 className="text-xl font-semibold text-ink">Bugünün kaydı</h2>
|
|
||||||
<p className="mt-1 text-sm text-ink/60">
|
|
||||||
Aynı tarih tekrar kaydedilirse eski kayıt güncellenir.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label className="block text-sm font-medium text-ink" htmlFor="date">
|
|
||||||
Tarih
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="date"
|
|
||||||
type="date"
|
|
||||||
value={date}
|
|
||||||
onChange={(event) => setDate(event.target.value)}
|
|
||||||
className="mt-2 w-full rounded-md border border-ink/15 bg-mist px-3 py-2 outline-none transition focus:border-leaf focus:ring-2 focus:ring-leaf/20"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<fieldset className="mt-5">
|
|
||||||
<legend className="text-sm font-medium text-ink">Ruh hali</legend>
|
|
||||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
|
||||||
{moodOptions.map(([key, config]) => (
|
|
||||||
<button
|
|
||||||
key={key}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setMood(key)}
|
|
||||||
className={`flex min-h-20 flex-col items-center justify-center rounded-md border px-3 py-3 text-center transition ${
|
|
||||||
mood === key
|
|
||||||
? "border-leaf bg-leaf text-white"
|
|
||||||
: "border-ink/10 bg-mist text-ink hover:border-leaf/60"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="text-2xl" aria-hidden="true">
|
|
||||||
{config.icon}
|
|
||||||
</span>
|
|
||||||
<span className="mt-1 text-sm font-semibold">
|
|
||||||
{config.label}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<label
|
|
||||||
className="mt-5 block text-sm font-medium text-ink"
|
|
||||||
htmlFor="energy"
|
|
||||||
>
|
|
||||||
Enerji seviyesi: {energy}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="energy"
|
|
||||||
type="range"
|
|
||||||
min="1"
|
|
||||||
max="5"
|
|
||||||
step="1"
|
|
||||||
value={energy}
|
|
||||||
onChange={(event) => setEnergy(Number(event.target.value) as Energy)}
|
|
||||||
className="mt-3 w-full accent-leaf"
|
|
||||||
/>
|
|
||||||
<div className="mt-1 flex justify-between text-xs text-ink/50">
|
|
||||||
<span>1</span>
|
|
||||||
<span>2</span>
|
|
||||||
<span>3</span>
|
|
||||||
<span>4</span>
|
|
||||||
<span>5</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="mt-6 w-full rounded-md bg-ink px-4 py-3 text-sm font-semibold text-white transition hover:bg-leaf"
|
|
||||||
>
|
|
||||||
Kaydet
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{message ? (
|
|
||||||
<p className="mt-3 rounded-md bg-mist px-3 py-2 text-sm text-ink/70">
|
|
||||||
{message}
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
</motion.form>
|
|
||||||
|
|
||||||
<div className="grid gap-5">
|
|
||||||
<ChartCard title="Mood ve enerji trendi">
|
|
||||||
{entries.length > 0 ? (
|
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
|
||||||
<LineChart data={lineData} margin={{ left: 0, right: 16 }}>
|
|
||||||
<XAxis dataKey="date" tickLine={false} axisLine={false} />
|
|
||||||
<YAxis
|
|
||||||
domain={[1, 5]}
|
|
||||||
tickCount={5}
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
/>
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line
|
|
||||||
type="monotone"
|
|
||||||
dataKey="mood"
|
|
||||||
name="Mood score"
|
|
||||||
stroke="#2f7d63"
|
|
||||||
strokeWidth={3}
|
|
||||||
dot={{ r: 4 }}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
type="monotone"
|
|
||||||
dataKey="energy"
|
|
||||||
name="Enerji"
|
|
||||||
stroke="#d8644a"
|
|
||||||
strokeWidth={3}
|
|
||||||
dot={{ r: 4 }}
|
|
||||||
/>
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
) : (
|
|
||||||
<EmptyState text="Trend grafiği için ilk mood kaydını ekle." />
|
|
||||||
)}
|
|
||||||
</ChartCard>
|
|
||||||
|
|
||||||
<div className="grid gap-5 xl:grid-cols-[1fr_1fr]">
|
|
||||||
<ChartCard title="Mood dağılımı">
|
|
||||||
{pieData.length > 0 ? (
|
|
||||||
<ResponsiveContainer width="100%" height={260}>
|
|
||||||
<PieChart>
|
|
||||||
<Pie
|
|
||||||
data={pieData}
|
|
||||||
dataKey="value"
|
|
||||||
nameKey="name"
|
|
||||||
innerRadius={56}
|
|
||||||
outerRadius={92}
|
|
||||||
paddingAngle={3}
|
|
||||||
>
|
|
||||||
{pieData.map((item) => (
|
|
||||||
<Cell key={item.name} fill={item.color} />
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
) : (
|
|
||||||
<EmptyState text="Dağılım grafiği kayıt eklendikten sonra görünür." />
|
|
||||||
)}
|
|
||||||
</ChartCard>
|
|
||||||
|
|
||||||
<ChartCard title="Insight">
|
|
||||||
{insights.length > 0 ? (
|
|
||||||
<ul className="space-y-3">
|
|
||||||
{insights.map((insight) => (
|
|
||||||
<li
|
|
||||||
key={insight}
|
|
||||||
className="rounded-md border border-ink/10 bg-mist px-3 py-3 text-sm leading-6 text-ink/75"
|
|
||||||
>
|
|
||||||
{insight}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<EmptyState text="Insight üretmek için en az bir kayıt ekle." />
|
|
||||||
)}
|
|
||||||
</ChartCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ChartCard({
|
|
||||||
title,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<motion.section
|
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.35 }}
|
|
||||||
className="rounded-lg border border-ink/10 bg-white p-5 shadow-soft"
|
|
||||||
>
|
|
||||||
<h2 className="mb-4 text-lg font-semibold text-ink">{title}</h2>
|
|
||||||
{children}
|
|
||||||
</motion.section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyState({ text }: { text: string }) {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-56 items-center justify-center rounded-md border border-dashed border-ink/15 bg-mist px-4 text-center text-sm text-ink/55">
|
|
||||||
{text}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortEntries(entries: MoodEntry[]) {
|
|
||||||
return [...entries].sort((a, b) => a.date.localeCompare(b.date));
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatShortDate(date: string) {
|
|
||||||
return new Intl.DateTimeFormat("tr-TR", {
|
|
||||||
day: "2-digit",
|
|
||||||
month: "short",
|
|
||||||
}).format(new Date(`${date}T00:00:00`));
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildInsights(entries: MoodEntry[]) {
|
|
||||||
if (entries.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestSeven = [...entries]
|
|
||||||
.sort((a, b) => b.date.localeCompare(a.date))
|
|
||||||
.slice(0, 7);
|
|
||||||
const moodAverage = average(latestSeven.map((entry) => entry.mood_score));
|
|
||||||
const energyAverage = average(latestSeven.map((entry) => entry.energy));
|
|
||||||
const mostFrequentMood = getMostFrequentMood(entries);
|
|
||||||
const insights = [
|
|
||||||
moodAverage >= 3
|
|
||||||
? "Son 7 kayıtta genel ruh halin pozitif görünüyor."
|
|
||||||
: "Son 7 kayıtta ruh hali ortalaman düşük görünüyor.",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (energyAverage < 3) {
|
|
||||||
insights.push("Enerji seviyen son kayıtlarda düşük seyrediyor.");
|
|
||||||
} else {
|
|
||||||
insights.push("Enerji seviyen son kayıtlarda dengeli görünüyor.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mostFrequentMood) {
|
|
||||||
insights.push(
|
|
||||||
`En sık görülen ruh halin: ${moodConfig[mostFrequentMood].label}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return insights;
|
|
||||||
}
|
|
||||||
|
|
||||||
function average(values: number[]) {
|
|
||||||
return values.reduce((total, value) => total + value, 0) / values.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMostFrequentMood(entries: MoodEntry[]) {
|
|
||||||
const counts = entries.reduce<Record<Mood, number>>(
|
|
||||||
(currentCounts, entry) => {
|
|
||||||
currentCounts[entry.mood] += 1;
|
|
||||||
return currentCounts;
|
|
||||||
},
|
|
||||||
{ happy: 0, neutral: 0, sad: 0, angry: 0 },
|
|
||||||
);
|
|
||||||
|
|
||||||
return moodOptions.reduce<Mood | null>((winner, [moodKey]) => {
|
|
||||||
if (!winner || counts[moodKey] > counts[winner]) {
|
|
||||||
return moodKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
return winner;
|
|
||||||
}, null);
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { User } from "lucide-react";
|
|
||||||
|
|
||||||
export function Header() {
|
|
||||||
return (
|
|
||||||
<header className="h-16 border-b border-border/40 bg-background/40 backdrop-blur-md flex items-center justify-between px-6 shrink-0 sticky top-0 z-10">
|
|
||||||
<div className="text-sm text-muted-foreground font-medium">
|
|
||||||
Kişisel Odak Alanı
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="text-sm font-medium">Lokal Kullanıcı</div>
|
|
||||||
<div className="flex items-center justify-center h-9 w-9 bg-secondary text-secondary-foreground rounded-full border border-border/50">
|
|
||||||
<User className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import {
|
|
||||||
LayoutDashboard,
|
|
||||||
Book,
|
|
||||||
CheckSquare,
|
|
||||||
MessageCircle,
|
|
||||||
Settings,
|
|
||||||
BrainCircuit,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const routes = [
|
|
||||||
{ name: "Dashboard", path: "/", icon: LayoutDashboard },
|
|
||||||
{ name: "Günlük", path: "/journal", icon: Book },
|
|
||||||
{ name: "Görevler", path: "/tasks", icon: CheckSquare },
|
|
||||||
{ name: "Sohbet", path: "/chat", icon: MessageCircle },
|
|
||||||
{ name: "Ayarlar", path: "/settings", icon: Settings },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Sidebar() {
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<aside className="border-r border-border bg-secondary min-h-screen flex flex-col shrink-0">
|
|
||||||
<div className="h-16 flex items-center px-6 border-b border-border text-primary">
|
|
||||||
<BrainCircuit className="h-6 w-6 mr-2" />
|
|
||||||
<h2 className="text-xl font-bold tracking-tight">MindSpace</h2>
|
|
||||||
</div>
|
|
||||||
<nav className="flex-1 p-4 space-y-1">
|
|
||||||
{routes.map((route) => {
|
|
||||||
const isActive =
|
|
||||||
pathname === route.path || pathname?.startsWith(route.path + "/");
|
|
||||||
// Home route exact match check
|
|
||||||
const isExactActive =
|
|
||||||
route.path === "/" ? pathname === "/" : isActive;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={route.path}
|
|
||||||
href={route.path}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors text-sm font-medium",
|
|
||||||
isExactActive
|
|
||||||
? "bg-primary text-primary-foreground"
|
|
||||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<route.icon className="h-5 w-5" />
|
|
||||||
{route.name}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
<div className="p-4 border-t border-border/50 text-xs text-muted-foreground/60 text-center">
|
|
||||||
Local-First AI Workspace
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user