diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fd3dbb5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/README.md b/README.md
index ba38467..2b3e915 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,119 @@
-# Lifestock
\ No newline at end of file
+# LifeStock Dashboard
+
+A Next.js 14 dashboard that tracks your life pillars (Knowledge, Skills, Health, Discipline, Mood) as if they were moving a stock price. Features real-time candlestick charts, daily entries, and weekly performance reports.
+
+## Features
+
+- 👤 Personalized user name on first launch
+- 📊 Real-time candlestick chart visualization
+- 📝 Daily pillar tracking (0-10 scale for each pillar)
+- 📈 Automatic price calculations based on pillar averages
+- 📅 Weekly earnings reports with streak tracking
+- 💾 Local data persistence using localStorage
+- 🌙 Dark theme UI with responsive design
+- 📱 Mobile and desktop friendly
+
+## Tech Stack
+
+- **Next.js 14** with App Router
+- **TypeScript** for type safety
+- **Tailwind CSS** for styling
+- **lightweight-charts** for candlestick visualization
+- **localStorage** for data persistence
+
+## Installation
+
+1. Clone the repository:
+```bash
+git clone https://github.com/kyzen-dev7/Lifestock.git
+cd Lifestock
+```
+
+2. Install dependencies:
+```bash
+npm install
+```
+
+3. Run the development server:
+```bash
+npm run dev
+```
+
+4. Open [http://localhost:3000](http://localhost:3000) in your browser.
+
+## Building for Production
+
+Build the application:
+```bash
+npm run build
+```
+
+Start the production server:
+```bash
+npm start
+```
+
+## How It Works
+
+### The Formula
+
+1. **Average Calculation**: `Avg = (Knowledge + Skills + Health + Discipline + Mood) / 5`
+2. **Daily Change**: `DailyChange% = (Avg - 5) * 4`
+ - When Avg = 10 → +20%
+ - When Avg = 5 → 0%
+ - When Avg = 0 → -20%
+3. **Price Calculation**: `NewClose = OldClose * (1 + DailyChange%/100)`
+4. **Candlestick Wicks**: Discipline affects volatility
+ - Higher discipline = smaller wicks
+ - Lower discipline = larger wicks
+
+### Market Trends
+
+- **Bull Market**: Average ≥ 6.5
+- **Stable Market**: 4.8 ≤ Average ≤ 6.4
+- **Bear Market**: Average < 4.8
+
+## Pages
+
+### Dashboard (/)
+- Real-time candlestick chart
+- Daily entry form with 5 pillar sliders
+- Preview of projected price changes
+- Recent entries table
+- Current market trend indicator
+
+### Weekly Report (/weekly)
+- Last 7 days performance summary
+- Weekly % change
+- Best and weakest pillar analysis
+- Green streak counter (consecutive days with Avg ≥ 5)
+- Biggest up/down days
+
+## Data Storage
+
+All data is stored locally in your browser's localStorage. No backend or external API required.
+
+## Project Structure
+
+```
+Lifestock/
+├── app/
+│ ├── layout.tsx # Root layout
+│ ├── page.tsx # Dashboard page
+│ ├── weekly/
+│ │ └── page.tsx # Weekly report page
+│ └── globals.css # Global styles
+├── components/
+│ └── Chart.tsx # Candlestick chart component
+├── lib/
+│ └── lifestock.ts # Utility functions and formulas
+├── package.json
+├── tsconfig.json
+├── tailwind.config.ts
+├── postcss.config.js
+└── next.config.js
+```
+
+## License
+
+MIT
\ No newline at end of file
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000..1181442
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,20 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ --background: #000000;
+ --foreground: #ffffff;
+}
+
+body {
+ color: var(--foreground);
+ background: var(--background);
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+@layer utilities {
+ .text-balance {
+ text-wrap: balance;
+ }
+}
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..6e5cf90
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,21 @@
+import type { Metadata } from "next";
+import "./globals.css";
+
+export const metadata: Metadata = {
+ title: "LifeStock Dashboard",
+ description: "Track your life pillars as a stock portfolio",
+};
+
+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..e679f7a
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,349 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import Link from 'next/link';
+import Chart from '@/components/Chart';
+import {
+ DailyEntry,
+ Pillars,
+ loadEntries,
+ saveEntries,
+ todayISO,
+ createEntry,
+ getMarketTrend,
+ getTrendColor,
+ round2,
+ calculateAvg,
+ calculateDailyChangePct,
+ calculateNewClose,
+ getUserName,
+ saveUserName,
+} from '@/lib/lifestock';
+
+export default function Dashboard() {
+ const [entries, setEntries] = useState([]);
+ const [selectedDate, setSelectedDate] = useState(todayISO());
+ const [pillars, setPillars] = useState({
+ knowledge: 5,
+ skills: 5,
+ health: 5,
+ discipline: 5,
+ mood: 5,
+ });
+ const [mounted, setMounted] = useState(false);
+ const [userName, setUserName] = useState(null);
+ const [nameInput, setNameInput] = useState('');
+
+ // Load entries and user name on mount
+ useEffect(() => {
+ setMounted(true);
+ const loaded = loadEntries();
+ setEntries(loaded);
+
+ const savedName = getUserName();
+ setUserName(savedName);
+
+ // If there's an entry for the selected date, load its values
+ const existingEntry = loaded.find(e => e.dateISO === selectedDate);
+ if (existingEntry) {
+ setPillars(existingEntry.pillars);
+ }
+ }, []);
+
+ // Update pillars when date changes
+ useEffect(() => {
+ if (!mounted) return;
+ const existingEntry = entries.find(e => e.dateISO === selectedDate);
+ if (existingEntry) {
+ setPillars(existingEntry.pillars);
+ } else {
+ // Reset to default
+ setPillars({
+ knowledge: 5,
+ skills: 5,
+ health: 5,
+ discipline: 5,
+ mood: 5,
+ });
+ }
+ }, [selectedDate, entries, mounted]);
+
+ const handleSaveDay = () => {
+ // Get previous close
+ const sortedEntries = [...entries].sort((a, b) => a.dateISO.localeCompare(b.dateISO));
+
+ // Find the entry just before the selected date
+ const beforeEntries = sortedEntries.filter(e => e.dateISO < selectedDate);
+ const previousClose = beforeEntries.length > 0
+ ? beforeEntries[beforeEntries.length - 1].priceClose
+ : 100;
+
+ // Create new entry
+ const newEntry = createEntry(selectedDate, pillars, previousClose);
+
+ // Remove existing entry with same date
+ const filteredEntries = entries.filter(e => e.dateISO !== selectedDate);
+
+ // Add new entry
+ const updatedEntries = [...filteredEntries, newEntry];
+
+ // Recalculate all entries after this date
+ const sorted = updatedEntries.sort((a, b) => a.dateISO.localeCompare(b.dateISO));
+ const recalculated: DailyEntry[] = [];
+
+ for (let i = 0; i < sorted.length; i++) {
+ if (i === 0) {
+ // First entry
+ const entry = createEntry(sorted[i].dateISO, sorted[i].pillars, 100);
+ recalculated.push(entry);
+ } else {
+ // Use previous close
+ const entry = createEntry(sorted[i].dateISO, sorted[i].pillars, recalculated[i - 1].priceClose);
+ recalculated.push(entry);
+ }
+ }
+
+ setEntries(recalculated);
+ saveEntries(recalculated);
+ };
+
+ const handleReset = () => {
+ if (confirm('Are you sure you want to clear all data? This cannot be undone.')) {
+ setEntries([]);
+ saveEntries([]);
+ }
+ };
+
+ const handleNameSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (nameInput.trim()) {
+ saveUserName(nameInput.trim());
+ setUserName(nameInput.trim());
+ }
+ };
+
+ // Get latest entry for display
+ const latestEntry = entries.length > 0
+ ? [...entries].sort((a, b) => b.dateISO.localeCompare(a.dateISO))[0]
+ : null;
+
+ // Calculate preview values
+ const previewAvg = round2(calculateAvg(pillars));
+ const previewDailyChangePct = round2(calculateDailyChangePct(previewAvg));
+
+ const sortedEntries = [...entries].sort((a, b) => a.dateISO.localeCompare(b.dateISO));
+ const beforeEntries = sortedEntries.filter(e => e.dateISO < selectedDate);
+ const previewOpen = beforeEntries.length > 0
+ ? beforeEntries[beforeEntries.length - 1].priceClose
+ : 100;
+ const previewClose = round2(calculateNewClose(previewOpen, previewDailyChangePct));
+
+ // Recent entries (last 10-12)
+ const recentEntries = [...entries]
+ .sort((a, b) => b.dateISO.localeCompare(a.dateISO))
+ .slice(0, 12);
+
+ if (!mounted) {
+ return (
+
+ );
+ }
+
+ // Show name input if no name is saved
+ if (!userName) {
+ return (
+
+
+
+
Welcome to LifeStock
+
+ Track your life pillars as a stock portfolio
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+ LifeStock: {userName.toUpperCase()} LTD. (
+ {userName.substring(0, 4).toUpperCase()}X)
+
+
+
+ ${latestEntry ? latestEntry.priceClose.toFixed(2) : '100.00'}
+
+ {latestEntry && (
+
+ {getMarketTrend(latestEntry.avg)}
+
+ )}
+
+
+
+ {/* Chart */}
+
+
+
+
+ {/* Main content grid */}
+
+ {/* Daily Entry Card */}
+
+
Daily Entry
+
+ {/* Date picker */}
+
+
+ setSelectedDate(e.target.value)}
+ className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-white"
+ />
+
+
+ {/* Pillar sliders */}
+
+ {(['knowledge', 'skills', 'health', 'discipline', 'mood'] as const).map((pillar) => (
+
+
+
+ {pillars[pillar]}
+
+
setPillars({ ...pillars, [pillar]: Number(e.target.value) })}
+ className="w-full accent-blue-500"
+ />
+
+ ))}
+
+
+ {/* Save button */}
+
+
+
+ {/* Preview Card */}
+
+
Preview
+
+
+ Open
+ ${previewOpen.toFixed(2)}
+
+
+ Close (Projected)
+ ${previewClose.toFixed(2)}
+
+
+ Average
+ {previewAvg.toFixed(2)}
+
+
+ Daily Change
+ = 0 ? 'text-green-500' : 'text-red-500'}`}>
+ {previewDailyChangePct >= 0 ? '+' : ''}{previewDailyChangePct.toFixed(2)}%
+
+
+
+
+ {getMarketTrend(previewAvg)}
+
+
+
+
+
+
+ {/* Recent entries */}
+
+
Recent Days
+ {recentEntries.length === 0 ? (
+
No entries yet
+ ) : (
+
+
+
+
+ | Date |
+ Close |
+ Change |
+ Avg |
+ Trend |
+
+
+
+ {recentEntries.map((entry) => (
+
+ | {entry.dateISO} |
+ ${entry.priceClose.toFixed(2)} |
+ = 0 ? 'text-green-500' : 'text-red-500'}`}>
+ {entry.dailyChangePct >= 0 ? '+' : ''}{entry.dailyChangePct.toFixed(2)}%
+ |
+ {entry.avg.toFixed(2)} |
+
+ {getMarketTrend(entry.avg)}
+ |
+
+ ))}
+
+
+
+ )}
+
+
+ {/* Actions */}
+
+
+ View Weekly Report
+
+
+
+
+
+ );
+}
diff --git a/app/weekly/page.tsx b/app/weekly/page.tsx
new file mode 100644
index 0000000..0dc16a0
--- /dev/null
+++ b/app/weekly/page.tsx
@@ -0,0 +1,222 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import Link from 'next/link';
+import { DailyEntry, loadEntries, round2 } from '@/lib/lifestock';
+
+export default function WeeklyReport() {
+ const [entries, setEntries] = useState([]);
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ const loaded = loadEntries();
+ setEntries(loaded);
+ }, []);
+
+ if (!mounted) {
+ return (
+
+ );
+ }
+
+ // Get last 7 days (or fewer if not available)
+ const sortedEntries = [...entries].sort((a, b) => b.dateISO.localeCompare(a.dateISO));
+ const last7Days = sortedEntries.slice(0, 7);
+
+ if (last7Days.length === 0) {
+ return (
+
+
+
Weekly Earnings Report
+
+
No data available for weekly report
+
+ Back to Dashboard
+
+
+
+
+ );
+ }
+
+ // Sort by date ascending for calculations
+ const weekData = [...last7Days].sort((a, b) => a.dateISO.localeCompare(b.dateISO));
+
+ // Weekly % change from first open to last close
+ const firstOpen = weekData[0].priceOpen;
+ const lastClose = weekData[weekData.length - 1].priceClose;
+ const weeklyChangePct = round2(((lastClose - firstOpen) / firstOpen) * 100);
+
+ // Best and weakest pillar
+ const pillarAverages: Record = {
+ knowledge: 0,
+ skills: 0,
+ health: 0,
+ discipline: 0,
+ mood: 0,
+ };
+
+ weekData.forEach((entry) => {
+ pillarAverages.knowledge += entry.pillars.knowledge;
+ pillarAverages.skills += entry.pillars.skills;
+ pillarAverages.health += entry.pillars.health;
+ pillarAverages.discipline += entry.pillars.discipline;
+ pillarAverages.mood += entry.pillars.mood;
+ });
+
+ Object.keys(pillarAverages).forEach((key) => {
+ pillarAverages[key] = round2(pillarAverages[key] / weekData.length);
+ });
+
+ const sortedPillars = Object.entries(pillarAverages).sort((a, b) => b[1] - a[1]);
+ const bestPillar = sortedPillars[0];
+ const weakestPillar = sortedPillars[sortedPillars.length - 1];
+
+ // Streak count: consecutive days with Avg >= 5
+ let currentStreak = 0;
+ for (let i = weekData.length - 1; i >= 0; i--) {
+ if (weekData[i].avg >= 5) {
+ currentStreak++;
+ } else {
+ break;
+ }
+ }
+
+ // Biggest up day and down day
+ const sortedByChange = [...weekData].sort((a, b) => b.dailyChangePct - a.dailyChangePct);
+ const biggestUpDay = sortedByChange[0];
+ const biggestDownDay = sortedByChange[sortedByChange.length - 1];
+
+ return (
+
+
+ {/* Header */}
+
+
Weekly Earnings Report
+
+ Last {weekData.length} day{weekData.length !== 1 ? 's' : ''} • {weekData[0].dateISO} to {weekData[weekData.length - 1].dateISO}
+
+
+
+ {/* Summary Cards */}
+
+ {/* Weekly Change */}
+
+
Weekly Change
+
= 0 ? 'text-green-500' : 'text-red-500'}`}>
+ {weeklyChangePct >= 0 ? '+' : ''}{weeklyChangePct.toFixed(2)}%
+
+
+ ${firstOpen.toFixed(2)} → ${lastClose.toFixed(2)}
+
+
+
+ {/* Streak */}
+
+
Green Streak
+
+ {currentStreak} day{currentStreak !== 1 ? 's' : ''}
+
+
+ Consecutive days with Avg ≥ 5
+
+
+
+
+ {/* Pillar Performance */}
+
+
Pillar Performance
+
+
+
+ Best Pillar
+ {bestPillar[0]}
+
+
+
+
{bestPillar[1].toFixed(1)}
+
+
+
+
+
+ Weakest Pillar
+ {weakestPillar[0]}
+
+
+
+
{weakestPillar[1].toFixed(1)}
+
+
+
+
+ {/* All pillars */}
+
+
All Pillars (Weekly Avg)
+
+ {sortedPillars.map(([name, avg]) => (
+
+
{name}
+
+
{avg.toFixed(1)}
+
+ ))}
+
+
+
+
+ {/* Best and Worst Days */}
+
+ {/* Biggest Up Day */}
+
+
Biggest Up Day
+
+ +{biggestUpDay.dailyChangePct.toFixed(2)}%
+
+
{biggestUpDay.dateISO}
+
Avg: {biggestUpDay.avg.toFixed(2)}
+
+
+ {/* Biggest Down Day */}
+
+
Biggest Down Day
+
+ {biggestDownDay.dailyChangePct.toFixed(2)}%
+
+
{biggestDownDay.dateISO}
+
Avg: {biggestDownDay.avg.toFixed(2)}
+
+
+
+ {/* Back button */}
+
+ ← Back to Dashboard
+
+
+
+ );
+}
diff --git a/components/Chart.tsx b/components/Chart.tsx
new file mode 100644
index 0000000..07cd888
--- /dev/null
+++ b/components/Chart.tsx
@@ -0,0 +1,94 @@
+'use client';
+
+import { useEffect, useRef } from 'react';
+import { createChart, IChartApi, ISeriesApi, CandlestickData, Time } from 'lightweight-charts';
+import { DailyEntry } from '@/lib/lifestock';
+
+interface ChartProps {
+ entries: DailyEntry[];
+}
+
+export default function Chart({ entries }: ChartProps) {
+ const chartContainerRef = useRef(null);
+ const chartRef = useRef(null);
+ const seriesRef = useRef | null>(null);
+
+ useEffect(() => {
+ if (!chartContainerRef.current) return;
+
+ // Create chart
+ const chart = createChart(chartContainerRef.current, {
+ width: chartContainerRef.current.clientWidth,
+ height: 400,
+ layout: {
+ background: { color: '#000000' },
+ textColor: '#d1d5db',
+ },
+ grid: {
+ vertLines: { color: '#1f2937' },
+ horzLines: { color: '#1f2937' },
+ },
+ timeScale: {
+ borderColor: '#374151',
+ },
+ rightPriceScale: {
+ borderColor: '#374151',
+ },
+ });
+
+ chartRef.current = chart;
+
+ // Create candlestick series
+ const candlestickSeries = chart.addCandlestickSeries({
+ upColor: '#10b981',
+ downColor: '#ef4444',
+ borderUpColor: '#10b981',
+ borderDownColor: '#ef4444',
+ wickUpColor: '#10b981',
+ wickDownColor: '#ef4444',
+ });
+
+ seriesRef.current = candlestickSeries;
+
+ // Handle resize
+ const handleResize = () => {
+ if (chartContainerRef.current && chartRef.current) {
+ chartRef.current.applyOptions({
+ width: chartContainerRef.current.clientWidth,
+ });
+ }
+ };
+
+ window.addEventListener('resize', handleResize);
+
+ return () => {
+ window.removeEventListener('resize', handleResize);
+ chart.remove();
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!seriesRef.current) return;
+
+ // Convert entries to candlestick data
+ const data: CandlestickData