From 2d217985a2c904922aed4b7617244f566794d7a5 Mon Sep 17 00:00:00 2001 From: Poyraz Avsever Date: Mon, 27 Apr 2026 23:36:52 +0300 Subject: [PATCH] Add layout and main page components for Mood Tracker application --- app/layout.tsx | 19 +++ app/page.tsx | 437 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 456 insertions(+) create mode 100644 app/layout.tsx create mode 100644 app/page.tsx diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..2a836e4 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Mood Tracker MVP", + description: "LocalStorage tabanli minimal mood dashboard uygulamasi.", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..f071c30 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,437 @@ +"use client"; + +import { motion } from "framer-motion"; +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([]); + const [date, setDate] = useState(today); + const [mood, setMood] = useState("happy"); + const [energy, setEnergy] = useState(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) { + 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 ( +
+
+
+

+ Mood Tracker +

+

+ Günlük ruh hali dashboard'u +

+
+
+ {entries.length} kayıt +
+
+ +
+ +
+

Bugünün kaydı

+

+ Aynı tarih tekrar kaydedilirse eski kayıt güncellenir. +

+
+ + + 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" + /> + +
+ Ruh hali +
+ {moodOptions.map(([key, config]) => ( + + ))} +
+
+ + + setEnergy(Number(event.target.value) as Energy)} + className="mt-3 w-full accent-leaf" + /> +
+ 1 + 2 + 3 + 4 + 5 +
+ + + + {message ? ( +

+ {message} +

+ ) : null} +
+ +
+ + {entries.length > 0 ? ( + + + + + + + + + + + ) : ( + + )} + + +
+ + {pieData.length > 0 ? ( + + + + {pieData.map((item) => ( + + ))} + + + + + + ) : ( + + )} + + + + {insights.length > 0 ? ( +
    + {insights.map((insight) => ( +
  • + {insight} +
  • + ))} +
+ ) : ( + + )} +
+
+
+
+
+ ); +} + +function ChartCard({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( + +

{title}

+ {children} +
+ ); +} + +function EmptyState({ text }: { text: string }) { + return ( +
+ {text} +
+ ); +} + +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>( + (currentCounts, entry) => { + currentCounts[entry.mood] += 1; + return currentCounts; + }, + { happy: 0, neutral: 0, sad: 0, angry: 0 }, + ); + + return moodOptions.reduce((winner, [moodKey]) => { + if (!winner || counts[moodKey] > counts[winner]) { + return moodKey; + } + + return winner; + }, null); +}