added 1RM and macro calculators to fitness dashboard.

This commit is contained in:
Parth
2026-09-17 17:48:26 +05:30
commit 21f1ce7918
50 changed files with 14396 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
"use client";
import { motion } from "framer-motion";
import { Plus, Bookmark } from "lucide-react";
import { MOCK_POSTS } from "@/lib/mock-data";
// Reusing the same structure as the Profile tab for consistency
const MOCK_BOARDS = [
{ id: "b1", name: "Architecture Inspo", pins: 124, images: [MOCK_POSTS[2].mediaUrl, MOCK_POSTS[0].mediaUrl, MOCK_POSTS[4].mediaUrl] },
{ id: "b2", name: "Streetwear 2024", pins: 89, images: [MOCK_POSTS[1].mediaUrl, MOCK_POSTS[5].mediaUrl, MOCK_POSTS[6].mediaUrl] },
{ id: "b3", name: "Travel Bucketlist", pins: 342, images: [MOCK_POSTS[0].mediaUrl, MOCK_POSTS[7].mediaUrl, MOCK_POSTS[3].mediaUrl] },
{ id: "b4", name: "Interior Minimal", pins: 56, images: [MOCK_POSTS[5].mediaUrl, MOCK_POSTS[2].mediaUrl, MOCK_POSTS[1].mediaUrl] },
];
export default function BoardsPage() {
return (
<div className="w-full max-w-6xl mx-auto pt-8 pb-24 px-4 min-h-screen">
<div className="flex items-center gap-3 mb-10">
<Bookmark className="w-10 h-10 text-primary" />
<h1 className="text-4xl font-extrabold text-foreground">Saved Boards</h1>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
{/* Create New Board Card */}
<div className="aspect-[4/5] rounded-[2rem] border-2 border-dashed border-border flex flex-col items-center justify-center gap-4 hover:bg-muted/50 transition-colors cursor-pointer group glass">
<div className="w-16 h-16 rounded-full bg-background flex items-center justify-center shadow-lg group-active:scale-95 transition-transform">
<Plus className="w-8 h-8 text-primary" />
</div>
<span className="font-bold text-muted-foreground">New Board</span>
</div>
{MOCK_BOARDS.map((board) => (
<div key={board.id} className="group cursor-pointer flex flex-col gap-4">
{/* Stacked Polaroids */}
<div className="relative aspect-[4/5] w-full">
{board.images.map((img, i) => (
<motion.div
key={i}
className="absolute inset-0 rounded-2xl glass p-2 pb-6 shadow-xl origin-bottom transition-all duration-300 pointer-events-none"
style={{ zIndex: 3 - i }}
whileHover={
i === 0 ? { rotate: -6, x: -10, y: -5, scale: 1.05 } :
i === 1 ? { rotate: 2, scale: 1.02, zIndex: 10 } :
{ rotate: 8, x: 10, y: -5, scale: 1.05 }
}
initial={{ rotate: i === 0 ? 0 : i === 1 ? 4 : -4 }}
>
<img src={img} className="w-full h-full object-cover rounded-xl" alt="Pin" />
</motion.div>
))}
</div>
<div>
<h3 className="text-xl font-bold px-2 group-hover:text-primary transition-colors">{board.name}</h3>
<p className="text-sm font-medium text-muted-foreground px-2">{board.pins} Pins</p>
</div>
</div>
))}
</div>
</div>
);
}
+235
View File
@@ -0,0 +1,235 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import { UploadCloud, Image as ImageIcon, Sparkles, Hash, X } from "lucide-react";
import { MOCK_POSTS, MOCK_USERS } from "@/lib/mock-data";
const FILTERS = [
{ name: "Normal", class: "" },
{ name: "Vintage", class: "sepia contrast-125" },
{ name: "Noir", class: "grayscale contrast-150" },
{ name: "Vivid", class: "saturate-200 contrast-110" },
{ name: "Fade", class: "opacity-80 saturate-50" },
{ name: "Cool", class: "hue-rotate-15 saturate-150" }
];
export default function CreatePostPage() {
const router = useRouter();
const [dragActive, setDragActive] = useState(false);
const [file, setFile] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(null);
const [activeFilter, setActiveFilter] = useState("");
const [caption, setCaption] = useState("");
const [tags, setTags] = useState<string[]>([]);
const [tagInput, setTagInput] = useState("");
const handleDrag = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
handleFile(e.dataTransfer.files[0]);
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
handleFile(e.target.files[0]);
}
};
const handleFile = (file: File) => {
setFile(file);
const url = URL.createObjectURL(file);
setPreview(url);
};
const addTag = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && tagInput.trim() !== "") {
e.preventDefault();
if (!tags.includes(tagInput.trim())) {
setTags([...tags, tagInput.trim()]);
}
setTagInput("");
}
};
const removeTag = (tag: string) => {
setTags(tags.filter(t => t !== tag));
};
const handleShare = () => {
if (!preview) return;
const newPost = {
id: `p${MOCK_POSTS.length + 1}-${Date.now()}`,
author: MOCK_USERS[4], // The current mocked user (neo_tokyo)
mediaUrl: preview,
aspectRatio: "aspect-square", // Simplified for newly created posts
isVideo: file?.type.startsWith("video/") || false,
likes: 0,
comments: 0,
caption: caption,
tags: tags,
createdAt: "Just now",
};
// Unshift to add to the beginning of the feed
MOCK_POSTS.unshift(newPost);
// Navigate home
router.push("/feed");
};
return (
<div className="w-full max-w-5xl mx-auto pt-8 pb-24 px-4 min-h-[80vh] flex flex-col items-center">
<h1 className="text-4xl font-extrabold mb-8 flex items-center gap-3">
<Sparkles className="w-8 h-8 text-primary" /> Create New Post
</h1>
<div className="w-full glass-panel rounded-3xl p-6 shadow-2xl overflow-hidden relative border border-border">
<AnimatePresence mode="wait">
{!preview ? (
<motion.div
key="upload"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={`w-full h-[500px] rounded-2xl border-4 border-dashed flex flex-col items-center justify-center transition-colors ${
dragActive ? "border-primary bg-primary/10" : "border-border bg-background/50 hover:bg-muted/50"
}`}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
>
<UploadCloud className={`w-24 h-24 mb-6 transition-all duration-300 ${dragActive ? "text-primary scale-110" : "text-muted-foreground"}`} />
<p className="text-2xl font-bold mb-2">Drag and drop your media here</p>
<p className="text-muted-foreground mb-8">Photos or Videos (up to 50MB)</p>
<label className="px-8 py-4 bg-primary text-primary-foreground font-bold rounded-full cursor-pointer hover:bg-primary/90 active:scale-95 transition-all shadow-lg hover:shadow-primary/30">
Browse Files
<input type="file" className="hidden" accept="image/*,video/*" onChange={handleFileChange} />
</label>
</motion.div>
) : (
<motion.div
key="editor"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col md:flex-row gap-8"
>
{/* Image Preview & Filters */}
<div className="w-full md:w-1/2 space-y-6">
<div className="relative w-full aspect-square rounded-2xl overflow-hidden bg-black shadow-inner">
<img
src={preview}
alt="Preview"
className={`w-full h-full object-contain transition-all duration-300 ${activeFilter}`}
/>
<button
onClick={() => { setPreview(null); setFile(null); }}
className="absolute top-4 left-4 p-2 bg-black/50 hover:bg-black/70 text-white rounded-full backdrop-blur-md transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="space-y-3">
<h3 className="font-semibold px-2">Filters</h3>
<div className="flex gap-4 overflow-x-auto no-scrollbar pb-2 px-2">
{FILTERS.map(filter => (
<button
key={filter.name}
onClick={() => setActiveFilter(filter.class)}
className={`flex flex-col items-center gap-2 flex-shrink-0 group`}
>
<div className={`w-16 h-16 rounded-xl overflow-hidden border-2 transition-all ${activeFilter === filter.class ? "border-primary shadow-lg shadow-primary/30 ring-2 ring-primary ring-offset-2 ring-offset-background" : "border-transparent border-border"}`}>
<img src={preview} className={`w-full h-full object-cover ${filter.class}`} alt={filter.name} />
</div>
<span className={`text-xs font-bold ${activeFilter === filter.class ? "text-primary" : "text-muted-foreground"}`}>
{filter.name}
</span>
</button>
))}
</div>
</div>
</div>
{/* Details & Tags */}
<div className="w-full md:w-1/2 flex flex-col pt-2">
<div className="space-y-6 flex-1">
<div className="space-y-2">
<label className="text-sm font-bold text-muted-foreground uppercase tracking-wider">Caption</label>
<textarea
value={caption}
onChange={(e) => setCaption(e.target.value)}
placeholder="Write a captivating caption..."
className="w-full h-32 bg-background/50 border border-border rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-primary/50 transition-all resize-none"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-muted-foreground uppercase tracking-wider">Tags</label>
<div className="flex flex-wrap gap-2 mb-2">
<AnimatePresence>
{tags.map(tag => (
<motion.span
key={tag}
initial={{ scale: 0 }}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
className="bg-primary/10 text-primary border border-primary/20 px-3 py-1 rounded-full text-sm font-semibold flex items-center gap-1"
>
<Hash className="w-3 h-3" />
{tag}
<button onClick={() => removeTag(tag)} className="ml-1 hover:text-black dark:hover:text-white transition-colors">
<X className="w-3 h-3" />
</button>
</motion.span>
))}
</AnimatePresence>
</div>
<input
type="text"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={addTag}
placeholder="Type a tag and press Enter"
className="w-full bg-background/50 border border-border rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-primary/50 transition-all"
/>
</div>
</div>
<div className="mt-8 pt-6 border-t border-border flex gap-4">
<button onClick={() => { setPreview(null); setFile(null); }} className="flex-1 py-4 font-bold rounded-full glass hover:bg-muted/50 transition-all">
Discard
</button>
<button
onClick={handleShare}
className="flex-1 py-4 font-bold rounded-full bg-primary text-primary-foreground hover:bg-primary/90 transition-all shadow-lg hover:shadow-primary/30 active:scale-95"
>
Share Post
</button>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
}
+97
View File
@@ -0,0 +1,97 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { Search, Compass, TrendingUp, Hash } from "lucide-react";
import { MOCK_POSTS } from "@/lib/mock-data";
import { PostCard } from "@/components/feed/PostCard";
import { useState } from "react";
const TRENDING_TAGS = ["#streetwear", "#architecture", "#minimalism", "#neonLights", "#coffeeArt"];
const CATEGORIES = ["For You", "Photography", "Fashion", "Tech", "Travel", "Art"];
export default function ExplorePage() {
const [searchFocused, setSearchFocused] = useState(false);
return (
<div className="w-full flex flex-col pt-6 pb-24 px-4 min-h-screen" suppressHydrationWarning>
{/* Header & Search */}
<div className="sticky top-0 z-40 bg-background/80 backdrop-blur-md pt-2 pb-6 flex flex-col gap-6">
<h1 className="text-4xl font-extrabold flex items-center gap-3">
<Compass className="w-10 h-10 text-primary" />
Explore
</h1>
<div className="relative w-full max-w-2xl">
<motion.div
animate={{ scale: searchFocused ? 1.02 : 1 }}
className={`flex items-center w-full rounded-2xl border transition-all glass ${
searchFocused ? "border-primary ring-4 ring-primary/20 bg-background/80" : "border-border bg-muted/30"
}`}
>
<Search className="w-6 h-6 ml-4 text-muted-foreground object-fill" />
<input
type="text"
placeholder="Search posts, tags, or topics..."
onFocus={() => setSearchFocused(true)}
onBlur={() => setSearchFocused(false)}
className="w-full bg-transparent px-4 py-4 outline-none font-medium placeholder:text-muted-foreground"
/>
</motion.div>
<AnimatePresence>
{searchFocused && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
className="absolute top-full left-0 right-0 mt-4 p-4 glass-panel rounded-2xl shadow-2xl border border-border"
>
<div className="mb-4">
<span className="flex items-center gap-2 text-sm font-bold text-muted-foreground uppercase tracking-wider mb-2">
<TrendingUp className="w-4 h-4" /> Trending Searches
</span>
<div className="flex flex-wrap gap-2">
{TRENDING_TAGS.map(tag => (
<span key={tag} className="px-3 py-1.5 rounded-full bg-muted text-sm font-medium hover:bg-primary hover:text-primary-foreground cursor-pointer transition-colors flex items-center gap-1">
<Hash className="w-3 h-3" /> {tag.replace("#", "")}
</span>
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Categories */}
<div className="flex overflow-x-auto no-scrollbar gap-3 pb-2">
{CATEGORIES.map((cat, i) => (
<button
key={cat}
className={`px-6 py-2.5 rounded-full font-bold whitespace-nowrap transition-all active:scale-95 ${
i === 0 ? "bg-primary text-primary-foreground shadow-md shadow-primary/30" : "glass hover:bg-white/40 dark:hover:bg-black/40"
}`}
>
{cat}
</button>
))}
</div>
</div>
{/* Masonry Grid (Reused Post Cards with mixed sizes for discovery layout) */}
<motion.div
layout
className="columns-1 sm:columns-2 lg:columns-3 xl:columns-4 gap-6 space-y-6 mt-6"
>
{/* Shuffle the mock posts slightly to look different from Home Feed */}
{[...MOCK_POSTS].reverse().map((post) => (
<PostCard key={`explore-${post.id}`} post={post} />
))}
{MOCK_POSTS.map((post) => (
<PostCard key={`explore-2-${post.id}`} post={post} />
))}
</motion.div>
</div>
);
}
+20
View File
@@ -0,0 +1,20 @@
"use client";
import { StoriesBanner } from "@/components/feed/StoriesBanner";
import { PostCard } from "@/components/feed/PostCard";
import { MOCK_POSTS } from "@/lib/mock-data";
export default function FeedPage() {
return (
<div className="w-full">
<StoriesBanner />
{/* Masonry Layout Container */}
<div className="columns-1 sm:columns-2 lg:columns-3 xl:columns-4 gap-6 space-y-6 px-4 pb-24">
{MOCK_POSTS.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
</div>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { AppShell } from "@/components/layout/AppShell";
export default function AppLayout({ children }: { children: React.ReactNode }) {
return <AppShell>{children}</AppShell>;
}
+188
View File
@@ -0,0 +1,188 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { Send, Phone, Video, Info, Check, CheckCheck, Smile, Image as ImageIcon, Paperclip } from "lucide-react";
import { MOCK_USERS, MOCK_POSTS } from "@/lib/mock-data";
import { motion, AnimatePresence } from "framer-motion";
const INIT_MESSAGES = [
{ id: 1, sender: "u1", text: "Hey! Did you see that new architecture post?", time: "10:24 AM", read: true },
{ id: 2, sender: "me", text: "Yes! The lighting was incredible.", time: "10:25 AM", read: true },
{ id: 3, sender: "u1", text: "I saved it to our shared board.", time: "10:26 AM", read: true },
];
export default function MessagesPage() {
const [activeChat, setActiveChat] = useState(MOCK_USERS[0]);
const [messages, setMessages] = useState(INIT_MESSAGES);
const [inputText, setInputText] = useState("");
const [isTyping, setIsTyping] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [messages, isTyping]);
const handleSend = (e: React.FormEvent) => {
e.preventDefault();
if (!inputText.trim()) return;
const newMsg = { id: Date.now(), sender: "me", text: inputText, time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), read: false };
setMessages(prev => [...prev, newMsg]);
setInputText("");
// Simulate partner typing and replying
setIsTyping(true);
setTimeout(() => {
setIsTyping(false);
setMessages(prev => [...prev, {
id: Date.now() + 1,
sender: activeChat.id,
text: "That sounds awesome! 😎",
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
read: true
}]);
}, 2500);
};
return (
<div className="w-full h-full max-h-screen py-6 px-4 flex">
<div className="w-full max-w-6xl mx-auto flex h-[85vh] glass-panel rounded-3xl overflow-hidden shadow-2xl border border-border">
{/* Sidebar / Chat List */}
<div className="w-1/3 border-r border-border bg-background/50 flex flex-col">
<div className="p-6 border-b border-border">
<h2 className="text-2xl font-extrabold">Messages</h2>
<div className="mt-4">
<input
type="text"
placeholder="Search messages..."
className="w-full bg-muted/50 border border-border rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-primary/50 transition-all text-sm"
/>
</div>
</div>
<div className="flex-1 overflow-y-auto no-scrollbar p-3 space-y-1">
{MOCK_USERS.slice(0, 5).map(user => (
<button
key={user.id}
onClick={() => setActiveChat(user)}
className={`w-full flex items-center gap-4 p-3 rounded-2xl transition-all ${activeChat.id === user.id ? "bg-primary text-primary-foreground shadow-lg" : "hover:bg-muted"}`}
>
<div className="relative">
<img src={user.avatar} className="w-14 h-14 rounded-full object-cover shadow-sm bg-background" alt={user.username} />
{user.hasStory && <span className="absolute bottom-0 right-0 w-3.5 h-3.5 bg-green-500 border-2 border-background rounded-full" />}
</div>
<div className="flex-1 text-left hidden sm:block">
<h3 className="font-bold truncate text-sm">{user.username}</h3>
<p className={`text-xs truncate ${activeChat.id === user.id ? "text-primary-foreground/80" : "text-muted-foreground"}`}>
Active today
</p>
</div>
</button>
))}
</div>
</div>
{/* Chat Area */}
<div className="flex-1 flex flex-col bg-background/30">
{/* Chat Header */}
<div className="p-4 border-b border-border flex items-center justify-between bg-background/80 backdrop-blur-md z-10">
<div className="flex items-center gap-4">
<img src={activeChat.avatar} className="w-12 h-12 rounded-full object-cover shadow-sm" alt={activeChat.username} />
<div>
<h3 className="font-bold">{activeChat.username}</h3>
<p className="text-xs text-muted-foreground">Active now</p>
</div>
</div>
<div className="flex gap-4 text-primary">
<button className="p-2 glass rounded-full hover:bg-muted/50 transition-colors"><Phone className="w-5 h-5" /></button>
<button className="p-2 glass rounded-full hover:bg-muted/50 transition-colors"><Video className="w-5 h-5" /></button>
<button className="p-2 glass rounded-full hover:bg-muted/50 transition-colors"><Info className="w-5 h-5" /></button>
</div>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-6 space-y-6">
<div className="text-center text-xs text-muted-foreground font-semibold my-4">Today</div>
<AnimatePresence>
{messages.map((msg, i) => {
const isMe = msg.sender === "me";
return (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
key={msg.id}
className={`flex flex-col ${isMe ? "items-end" : "items-start"}`}
>
<div className={`max-w-[70%] px-6 py-3 rounded-3xl ${isMe ? "bg-primary text-primary-foreground rounded-tr-sm shadow-lg shadow-primary/20" : "glass rounded-tl-sm shadow-sm"}`}>
<p className="text-[15px] leading-relaxed">{msg.text}</p>
</div>
<div className="flex items-center gap-1 mt-1 px-2">
<span className="text-[10px] uppercase tracking-widest text-muted-foreground font-bold">{msg.time}</span>
{isMe && (
msg.read ? <CheckCheck className="w-3 h-3 text-primary" /> : <Check className="w-3 h-3 text-muted-foreground" />
)}
</div>
</motion.div>
);
})}
</AnimatePresence>
{/* Typing Indicator */}
<AnimatePresence>
{isTyping && (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="flex items-start"
>
<div className="glass px-5 py-4 rounded-3xl rounded-tl-sm flex gap-1.5 items-center shadow-sm">
<motion.div animate={{ y: [0, -5, 0] }} transition={{ repeat: Infinity, duration: 0.6, delay: 0 }} className="w-2 h-2 bg-muted-foreground rounded-full" />
<motion.div animate={{ y: [0, -5, 0] }} transition={{ repeat: Infinity, duration: 0.6, delay: 0.2 }} className="w-2 h-2 bg-muted-foreground rounded-full" />
<motion.div animate={{ y: [0, -5, 0] }} transition={{ repeat: Infinity, duration: 0.6, delay: 0.4 }} className="w-2 h-2 bg-muted-foreground rounded-full" />
</div>
</motion.div>
)}
</AnimatePresence>
<div ref={messagesEndRef} />
</div>
{/* Input Box */}
<div className="p-4 bg-background/80 backdrop-blur-md border-t border-border">
<form onSubmit={handleSend} className="flex items-center gap-2">
<button type="button" className="p-3 text-muted-foreground hover:text-foreground transition-colors">
<Paperclip className="w-6 h-6" />
</button>
<div className="flex-1 relative flex items-center">
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Message..."
className="w-full bg-muted/30 border border-border rounded-full py-3 pl-6 pr-12 outline-none focus:ring-2 focus:ring-primary/50 transition-all"
/>
<button type="button" className="absolute right-3 p-1 text-muted-foreground hover:text-foreground transition-colors">
<Smile className="w-5 h-5" />
</button>
</div>
<button
type="submit"
disabled={!inputText.trim()}
className={`p-3 rounded-full transition-all active:scale-90 ${inputText.trim() ? "bg-primary text-primary-foreground shadow-lg shadow-primary/30" : "bg-muted text-muted-foreground cursor-not-allowed"}`}
>
<Send className="w-5 h-5 ml-0.5" />
</button>
</form>
</div>
</div>
</div>
</div>
);
}
+100
View File
@@ -0,0 +1,100 @@
"use client";
import { motion } from "framer-motion";
import { Heart, MessageCircle, UserPlus, Bookmark, Bell } from "lucide-react";
import { MOCK_USERS, MOCK_POSTS } from "@/lib/mock-data";
const NOTIFICATIONS = [
{ id: 1, type: "like", user: MOCK_USERS[1], post: MOCK_POSTS[0], time: "2m", text: "liked your post" },
{ id: 2, type: "comment", user: MOCK_USERS[4], post: MOCK_POSTS[0], time: "15m", text: "commented: \"Amazing shot! 📸\"" },
{ id: 3, type: "follow", user: MOCK_USERS[2], time: "1h", text: "started following you" },
{ id: 4, type: "save", user: MOCK_USERS[3], post: MOCK_POSTS[2], time: "3h", text: "saved your post to their board 'Architecture Inspo'" },
{ id: 5, type: "like", user: MOCK_USERS[5], post: MOCK_POSTS[1], time: "1d", text: "liked your post" },
{ id: 6, type: "follow", user: MOCK_USERS[6], time: "1d", text: "started following you" },
{ id: 7, type: "comment", user: MOCK_USERS[7], post: MOCK_POSTS[1], time: "2d", text: "commented: \"Where did you get that jacket? 🔥\"" },
];
export default function NotificationsPage() {
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.1 }
}
};
const item = {
hidden: { opacity: 0, x: -20 },
show: { opacity: 1, x: 0, transition: { type: "spring" as const, stiffness: 300, damping: 24 } }
};
const renderIcon = (type: string) => {
switch(type) {
case "like": return <Heart className="w-5 h-5 text-red-500 fill-red-500" />;
case "comment": return <MessageCircle className="w-5 h-5 text-blue-500 fill-blue-500" />;
case "follow": return <UserPlus className="w-5 h-5 text-primary" />;
case "save": return <Bookmark className="w-5 h-5 text-yellow-500 fill-yellow-500" />;
default: return <Bell className="w-5 h-5" />;
}
};
return (
<div className="w-full max-w-3xl mx-auto pt-8 pb-24 px-4 min-h-[80vh]">
<div className="flex items-center gap-4 mb-8 sticky top-4 z-40 bg-background/80 backdrop-blur-md py-4 rounded-3xl px-6 border border-border shadow-sm">
<div className="p-3 bg-primary/10 rounded-2xl">
<Bell className="w-6 h-6 text-primary" />
</div>
<h1 className="text-3xl font-extrabold tracking-tight">Activity</h1>
</div>
<motion.div
variants={container}
initial="hidden"
animate="show"
className="space-y-4"
>
<div className="mb-4">
<h3 className="font-bold text-muted-foreground ml-2 uppercase tracking-widest text-xs">This Week</h3>
</div>
{NOTIFICATIONS.map((notif) => (
<motion.div
key={notif.id}
variants={item}
className="group relative flex items-center gap-4 p-4 rounded-[2rem] glass-card border border-border/50 hover:bg-muted/50 cursor-pointer transition-colors"
>
{/* User Avatar with Type Badge */}
<div className="relative">
<img src={notif.user.avatar} alt={notif.user.username} className="w-14 h-14 rounded-full object-cover shadow-md bg-background" />
<div className="absolute -bottom-1 -right-1 p-1.5 bg-background rounded-full shadow-sm">
{renderIcon(notif.type)}
</div>
</div>
{/* Notification Text */}
<div className="flex-1 pr-4">
<p className="text-[15px] leading-snug">
<span className="font-bold text-foreground mr-1 hover:underline">{notif.user.username}</span>
<span className="text-muted-foreground">{notif.text}</span>
</p>
<p className="text-xs font-semibold text-muted-foreground/60 uppercase tracking-widest mt-1">
{notif.time}
</p>
</div>
{/* Trailing Image / Action */}
{notif.post ? (
<img src={notif.post.mediaUrl} className="w-14 h-14 rounded-xl object-cover shadow-sm mr-2" alt="Post thumbnail" />
) : (
notif.type === "follow" && (
<button className="px-5 py-2 mr-2 rounded-full font-bold text-sm bg-primary text-primary-foreground hover:bg-primary/90 active:scale-95 transition-all shadow-md">
Follow Back
</button>
)
)}
</motion.div>
))}
</motion.div>
</div>
);
}
+171
View File
@@ -0,0 +1,171 @@
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Settings, Plus, MapPin, Link as LinkIcon, Calendar, User } from "lucide-react";
import { MOCK_POSTS, MOCK_USERS } from "@/lib/mock-data";
import { PostCard } from "@/components/feed/PostCard";
const TABS = ["Created", "Saved Boards", "Tagged In"];
const MOCK_BOARDS = [
{ id: "b1", name: "Architecture Inspo", pins: 124, images: [MOCK_POSTS[2].mediaUrl, MOCK_POSTS[0].mediaUrl, MOCK_POSTS[4].mediaUrl] },
{ id: "b2", name: "Streetwear 2024", pins: 89, images: [MOCK_POSTS[1].mediaUrl, MOCK_POSTS[5].mediaUrl, MOCK_POSTS[6].mediaUrl] },
{ id: "b3", name: "Travel Bucketlist", pins: 342, images: [MOCK_POSTS[0].mediaUrl, MOCK_POSTS[7].mediaUrl, MOCK_POSTS[3].mediaUrl] },
{ id: "b4", name: "Interior Minimal", pins: 56, images: [MOCK_POSTS[5].mediaUrl, MOCK_POSTS[2].mediaUrl, MOCK_POSTS[1].mediaUrl] },
];
export default function ProfilePage() {
const [activeTab, setActiveTab] = useState(0);
const currentUser = MOCK_USERS[4];
return (
<div className="w-full max-w-6xl mx-auto pt-8 pb-24 px-4 min-h-screen">
{/* Profile Header Canvas */}
<div className="relative w-full h-64 rounded-[2rem] overflow-hidden bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 shadow-xl border border-white/10">
<div className="absolute inset-0 bg-black/20" />
<button className="absolute top-4 right-4 p-3 bg-black/20 hover:bg-black/40 text-white rounded-full backdrop-blur-md transition-colors">
<Settings className="w-5 h-5" />
</button>
</div>
{/* Profile Info */}
<div className="relative px-8 flex flex-col sm:flex-row gap-8 items-start sm:items-end -mt-24 mb-12">
<div className="w-40 h-40 rounded-full border-4 border-background p-1 bg-gradient-to-tr from-yellow-400 via-red-500 to-pink-500 shadow-2xl relative z-10 shrink-0 bg-background">
<img src={currentUser.avatar} alt="Profile" className="w-full h-full rounded-full object-cover" />
</div>
<div className="flex-1 space-y-4 pt-24 sm:pt-0">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-4xl font-extrabold">{currentUser.username}</h1>
<p className="text-muted-foreground font-medium">Digital Creator & Developer</p>
</div>
<div className="flex gap-3">
<button className="px-8 py-2.5 rounded-full bg-primary text-primary-foreground font-bold hover:bg-primary/90 transition-all active:scale-95 shadow-lg shadow-primary/20">
Share Profile
</button>
<button className="px-8 py-2.5 rounded-full glass font-bold hover:bg-muted transition-all active:scale-95">
Edit
</button>
</div>
</div>
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-muted-foreground font-medium">
<span className="flex items-center gap-1.5"><MapPin className="w-4 h-4" /> Tokyo, Japan</span>
<span className="flex items-center gap-1.5 text-primary hover:underline cursor-pointer"><LinkIcon className="w-4 h-4" /> pinstagram.app/neo_tokyo</span>
<span className="flex items-center gap-1.5"><Calendar className="w-4 h-4" /> Joined April 2024</span>
</div>
<div className="flex items-center gap-8 pt-2">
<div className="flex flex-col">
<span className="text-2xl font-extrabold text-foreground">1,245</span>
<span className="text-xs font-bold text-muted-foreground uppercase tracking-widest">Followers</span>
</div>
<div className="flex flex-col">
<span className="text-2xl font-extrabold text-foreground">342</span>
<span className="text-xs font-bold text-muted-foreground uppercase tracking-widest">Following</span>
</div>
<div className="flex flex-col">
<span className="text-2xl font-extrabold text-foreground">89</span>
<span className="text-xs font-bold text-muted-foreground uppercase tracking-widest">Pins</span>
</div>
</div>
</div>
</div>
{/* Animated Tabs */}
<div className="relative w-full flex justify-center border-b border-border/50 mb-8">
{TABS.map((tab, idx) => (
<button
key={tab}
onClick={() => setActiveTab(idx)}
className={`relative px-8 py-4 text-sm font-bold uppercase tracking-widest transition-colors ${
activeTab === idx ? "text-foreground" : "text-muted-foreground hover:text-foreground/80"
}`}
>
{tab}
{activeTab === idx && (
<motion.div
layoutId="profileTabIndicator"
className="absolute bottom-0 left-0 w-full h-0.5 bg-primary"
transition={{ type: "spring", stiffness: 300, damping: 30 }}
/>
)}
</button>
))}
</div>
{/* Tab Content */}
<div className="min-h-[50vh]">
<AnimatePresence mode="wait">
<motion.div
key={activeTab}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
{activeTab === 0 && (
<div className="columns-1 sm:columns-2 lg:columns-3 gap-6 space-y-6">
{MOCK_POSTS.slice(0, 5).map((post) => (
<PostCard key={`created-${post.id}`} post={post} />
))}
</div>
)}
{activeTab === 1 && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 pt-4">
{/* Create New Board Card */}
<div className="aspect-[4/5] rounded-[2rem] border-2 border-dashed border-border flex flex-col items-center justify-center gap-4 hover:bg-muted/50 transition-colors cursor-pointer group">
<div className="w-16 h-16 rounded-full bg-background flex items-center justify-center shadow-lg group-active:scale-95 transition-transform">
<Plus className="w-8 h-8 text-primary" />
</div>
<span className="font-bold text-muted-foreground">New Board</span>
</div>
{MOCK_BOARDS.map((board) => (
<div key={board.id} className="group cursor-pointer flex flex-col gap-4">
{/* Stacked Polaroids */}
<div className="relative aspect-[4/5] w-full">
{board.images.map((img, i) => (
<motion.div
key={i}
className="absolute inset-0 rounded-2xl glass p-2 pb-6 shadow-xl origin-bottom transition-all duration-300 pointer-events-none"
style={{ zIndex: 3 - i }}
whileHover={
i === 0 ? { rotate: -6, x: -10, y: -5, scale: 1.05 } :
i === 1 ? { rotate: 2, scale: 1.02, zIndex: 10 } :
{ rotate: 8, x: 10, y: -5, scale: 1.05 }
}
initial={{ rotate: i === 0 ? 0 : i === 1 ? 4 : -4 }}
>
<img src={img} className="w-full h-full object-cover rounded-xl" alt="Pin" />
</motion.div>
))}
</div>
<div>
<h3 className="text-xl font-bold px-2">{board.name}</h3>
<p className="text-sm font-medium text-muted-foreground px-2">{board.pins} Pins</p>
</div>
</div>
))}
</div>
)}
{activeTab === 2 && (
<div className="flex flex-col items-center justify-center py-24 text-center space-y-4">
<div className="w-24 h-24 rounded-full border-4 border-dashed border-muted-foreground/30 flex items-center justify-center mb-2">
<User className="w-10 h-10 text-muted-foreground/50" />
</div>
<h3 className="text-2xl font-bold text-foreground">Photos of you</h3>
<p className="text-muted-foreground">When people tag you in photos, they'll appear here.</p>
</div>
)}
</motion.div>
</AnimatePresence>
</div>
</div>
);
}
+91
View File
@@ -0,0 +1,91 @@
"use client";
import { motion } from "framer-motion";
import { useState } from "react";
import { ThemeToggle } from "@/components/theme-toggle";
export default function AuthPage() {
const [isLogin, setIsLogin] = useState(true);
return (
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden bg-background text-foreground">
<div className="absolute top-4 right-4 z-50">
<ThemeToggle />
</div>
{/* Dynamic Backgrounds */}
<motion.div
animate={{
scale: [1, 1.2, 1],
rotate: [0, 90, 0]
}}
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
className="absolute top-[-10%] left-[-10%] w-[600px] h-[600px] bg-primary/10 rounded-full blur-[120px]"
/>
<motion.div
animate={{
scale: [1, 1.5, 1],
rotate: [0, -90, 0]
}}
transition={{ duration: 25, repeat: Infinity, ease: "linear" }}
className="absolute bottom-[-10%] right-[-10%] w-[600px] h-[600px] bg-pink-500/10 rounded-full blur-[120px]"
/>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="glass-panel p-10 rounded-3xl w-full max-w-md z-10 flex flex-col gap-6"
>
<div className="text-center space-y-2">
<h1 className="text-4xl font-extrabold tracking-tight">Pinstagram</h1>
<p className="text-muted-foreground">{isLogin ? "Welcome back to your curated world." : "Join the visual discovery."}</p>
</div>
<form className="flex flex-col gap-4 mt-4" onSubmit={(e) => { e.preventDefault(); window.location.href = "/onboarding"; }}>
{!isLogin && (
<div className="space-y-2">
<label className="text-sm font-medium">Username</label>
<input type="text" className="w-full bg-background/50 border border-border rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-primary/50 transition-all" placeholder="creative_soul" />
</div>
)}
<div className="space-y-2">
<label className="text-sm font-medium">Email</label>
<input type="email" required className="w-full bg-background/50 border border-border rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-primary/50 transition-all" placeholder="you@example.com" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Password</label>
<input type="password" required className="w-full bg-background/50 border border-border rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-primary/50 transition-all" placeholder="••••••••" />
</div>
<button type="submit" className="w-full bg-primary text-primary-foreground font-semibold py-3 rounded-xl mt-2 hover:bg-primary/90 transition-all active:scale-[0.98] shadow-lg">
{isLogin ? "Sign In" : "Sign Up"}
</button>
</form>
<div className="relative flex items-center py-2">
<div className="flex-grow border-t border-border"></div>
<span className="flex-shrink-0 mx-4 text-muted-foreground text-sm">or continue with</span>
<div className="flex-grow border-t border-border"></div>
</div>
<div className="flex gap-4">
<button className="flex-1 glass py-3 rounded-xl font-medium hover:bg-white/40 dark:hover:bg-black/40 transition-all flex items-center justify-center gap-2">
<svg className="w-5 h-5" viewBox="0 0 24 24"><path fill="currentColor" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" /><path fill="currentColor" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" /><path fill="currentColor" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" /><path fill="currentColor" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" /></svg>
Google
</button>
<button className="flex-1 glass py-3 rounded-xl font-medium hover:bg-white/40 dark:hover:bg-black/40 transition-all flex items-center justify-center gap-2">
<svg className="w-5 h-5 text-black dark:text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
GitHub
</button>
</div>
<p className="text-center text-sm text-muted-foreground mt-4">
{isLogin ? "Don't have an account?" : "Already have an account?"}{" "}
<button onClick={() => setIsLogin(!isLogin)} className="text-foreground font-semibold hover:underline">
{isLogin ? "Sign up" : "Sign in"}
</button>
</p>
</motion.div>
</div>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+154
View File
@@ -0,0 +1,154 @@
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
@theme {
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-shimmer: shimmer 2s linear infinite;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes shimmer {
from {
backgroundPosition: 200% 0;
}
to {
backgroundPosition: -200% 0;
}
}
}
:root {
--background: #f8fafc;
--foreground: #020817;
--muted: #f1f5f9;
--muted-foreground: #64748b;
--popover: #ffffff;
--popover-foreground: #020817;
--card: #ffffff;
--card-foreground: #020817;
--border: #e2e8f0;
--input: #e2e8f0;
--primary: #0f172a;
--primary-foreground: #f8fafc;
--secondary: #f1f5f9;
--secondary-foreground: #0f172a;
--accent: #f1f5f9;
--accent-foreground: #0f172a;
--ring: #94a3b8;
--radius: 0.75rem;
}
.dark {
--background: #020817;
--foreground: #f8fafc;
--muted: #0f172a;
--muted-foreground: #94a3b8;
--popover: #020817;
--popover-foreground: #f8fafc;
--card: #020817;
--card-foreground: #f8fafc;
--border: #1e293b;
--input: #1e293b;
--primary: #e2e8f0;
--primary-foreground: #020817;
--secondary: #0f172a;
--secondary-foreground: #f8fafc;
--accent: #1e293b;
--accent-foreground: #f8fafc;
--ring: #334155;
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@utility glass {
@apply bg-white/5 backdrop-blur-xl border border-white/10 shadow-lg;
}
@utility glass-card {
@apply bg-[#111726]/80 backdrop-blur-md border border-slate-800/80 rounded-2xl shadow-xl hover:border-slate-700/80 transition-all;
}
@utility glass-panel {
@apply bg-[#0c1220]/90 backdrop-blur-2xl border border-slate-800 shadow-2xl;
}
@utility text-gradient-cyan {
@apply bg-clip-text text-transparent bg-gradient-to-r from-cyan-400 via-teal-300 to-emerald-400;
}
@utility text-gradient-purple {
@apply bg-clip-text text-transparent bg-gradient-to-r from-violet-400 via-fuchsia-400 to-pink-400;
}
@utility glow-cyan {
box-shadow: 0 0 20px -3px rgba(6, 182, 212, 0.35);
}
@utility glow-emerald {
box-shadow: 0 0 20px -3px rgba(16, 185, 129, 0.35);
}
@utility glow-purple {
box-shadow: 0 0 20px -3px rgba(168, 85, 247, 0.35);
}
/* Custom Sleek Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #090d16;
}
::-webkit-scrollbar-thumb {
background: #1e293b;
border-radius: 9999px;
}
::-webkit-scrollbar-thumb:hover {
background: #334155;
}
+43
View File
@@ -0,0 +1,43 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Parth's Life Manager | Personal Command Center",
description: "An all-in-one personal dashboard for tasks, habits, calendar, expenses, notes, and focus.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className="dark" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-[#090d16] text-[#e2e8f0] min-h-screen selection:bg-cyan-500/30 selection:text-cyan-200`}
suppressHydrationWarning
>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem={false}
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
);
}
+112
View File
@@ -0,0 +1,112 @@
"use client";
import { motion } from "framer-motion";
import { useState } from "react";
import { ThemeToggle } from "@/components/theme-toggle";
const INTERESTS = [
{ id: "fashion", label: "Streetwear", emoji: "👟" },
{ id: "travel", label: "Travel & Places", emoji: "✈️" },
{ id: "architecture", label: "Architecture", emoji: "🏛️" },
{ id: "food", label: "Food & Drink", emoji: "🍔" },
{ id: "tech", label: "Technology", emoji: "💻" },
{ id: "art", label: "Digital Art", emoji: "🎨" },
{ id: "music", label: "Music & Audio", emoji: "🎵" },
{ id: "nature", label: "Nature", emoji: "🌲" },
{ id: "fitness", label: "Health & Fitness", emoji: "🏋️" },
{ id: "cars", label: "Automotive", emoji: "🚗" },
];
export default function OnboardingPage() {
const [selected, setSelected] = useState<string[]>([]);
const toggleInterest = (id: string) => {
setSelected(prev =>
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
);
};
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
};
const item: import("framer-motion").Variants = {
hidden: { opacity: 0, scale: 0.8, y: 20 },
show: { opacity: 1, scale: 1, y: 0, transition: { type: "spring", stiffness: 300, damping: 20 } }
};
return (
<div className="min-h-screen bg-background text-foreground flex flex-col items-center justify-center p-8 relative">
<div className="absolute top-4 right-4 z-50">
<ThemeToggle />
</div>
<div className="max-w-4xl w-full z-10 flex flex-col items-center">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center space-y-4 mb-12"
>
<h1 className="text-5xl font-extrabold tracking-tight">What are you into?</h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Pick 3 or more topics to personalize your feed. We'll curate the best content just for you.
</p>
</motion.div>
<motion.div
variants={container}
initial="hidden"
animate="show"
className="flex flex-wrap justify-center gap-4 max-w-3xl"
>
{INTERESTS.map((interest) => {
const isSelected = selected.includes(interest.id);
return (
<motion.button
key={interest.id}
variants={item}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => toggleInterest(interest.id)}
className={`flex items-center gap-3 px-6 py-4 rounded-full text-lg font-medium transition-all shadow-sm
${isSelected
? "bg-primary text-primary-foreground shadow-lg shadow-primary/30 ring-2 ring-primary ring-offset-2 ring-offset-background"
: "glass hover:bg-white/50 dark:hover:bg-black/50"
}
`}
>
<span className="text-2xl">{interest.emoji}</span>
{interest.label}
</motion.button>
);
})}
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: selected.length > 0 ? 1 : 0 }}
transition={{ duration: 0.3 }}
className="mt-16"
>
<button
disabled={selected.length < 3}
onClick={() => window.location.href = "/feed"}
className={`px-12 py-4 rounded-full font-bold text-lg shadow-xl transition-all ${
selected.length >= 3
? "bg-primary text-primary-foreground hover:scale-105 active:scale-95"
: "bg-muted text-muted-foreground cursor-not-allowed opacity-50"
}`}
>
{selected.length < 3 ? `Pick ${3 - selected.length} more` : "Continue to Feed →"}
</button>
</motion.div>
</div>
</div>
);
}
+327
View File
@@ -0,0 +1,327 @@
'use client';
import React, { useState } from 'react';
import { Sidebar } from '@/components/Sidebar';
import { Header } from '@/components/Header';
import { DashboardView } from '@/components/modules/DashboardView';
import { PomodoroView } from '@/components/modules/PomodoroView';
import { TasksView } from '@/components/modules/TasksView';
import { HabitsView } from '@/components/modules/HabitsView';
import { CalendarView } from '@/components/modules/CalendarView';
import { FinanceView } from '@/components/modules/FinanceView';
import { NotesView } from '@/components/modules/NotesView';
import { FitnessView } from '@/components/modules/FitnessView';
import { useLocalStorage } from '@/lib/useLocalStorage';
import {
INITIAL_TASKS,
INITIAL_HABITS,
INITIAL_EXPENSES,
INITIAL_EVENTS,
INITIAL_NOTES,
INITIAL_WORKOUTS,
INITIAL_NUTRITION,
INITIAL_MUSCLE_RECOVERY,
getFormattedDate
} from '@/lib/initialData';
import {
Task,
Habit,
ExpenseItem,
CalendarEvent,
Note,
ActiveModule,
TaskStatus,
WorkoutSession,
DailyNutrition,
MuscleRecovery
} from '@/lib/types';
export default function Home() {
const [activeModule, setActiveModule] = useState<ActiveModule>('dashboard');
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState<boolean>(false);
// Quick action triggers
const [showQuickAddTask, setShowQuickAddTask] = useState<boolean>(false);
const [showQuickAddNote, setShowQuickAddNote] = useState<boolean>(false);
// Persistent States
const [tasks, setTasks] = useLocalStorage<Task[]>('plm_tasks', INITIAL_TASKS);
const [habits, setHabits] = useLocalStorage<Habit[]>('plm_habits', INITIAL_HABITS);
const [expenses, setExpenses] = useLocalStorage<ExpenseItem[]>('plm_expenses', INITIAL_EXPENSES);
const [events, setEvents] = useLocalStorage<CalendarEvent[]>('plm_events', INITIAL_EVENTS);
const [notes, setNotes] = useLocalStorage<Note[]>('plm_notes', INITIAL_NOTES);
const [workouts, setWorkouts] = useLocalStorage<WorkoutSession[]>('plm_workouts', INITIAL_WORKOUTS);
const [nutrition, setNutrition] = useLocalStorage<DailyNutrition>('plm_nutrition', INITIAL_NUTRITION);
const [muscleRecovery, setMuscleRecovery] = useLocalStorage<MuscleRecovery[]>('plm_recovery', INITIAL_MUSCLE_RECOVERY);
// Task Operations
const handleAddTask = (newTask: Omit<Task, 'id' | 'createdAt'>) => {
const task: Task = {
...newTask,
id: `t-${Date.now()}`,
createdAt: getFormattedDate(0),
};
setTasks((prev) => [task, ...prev]);
};
const handleUpdateTask = (id: string, updates: Partial<Task>) => {
setTasks((prev) =>
prev.map((t) => (t.id === id ? { ...t, ...updates } : t))
);
};
const handleDeleteTask = (id: string) => {
setTasks((prev) => prev.filter((t) => t.id !== id));
};
const handleToggleTask = (taskId: string) => {
setTasks((prev) =>
prev.map((t) => {
if (t.id === taskId) {
const newStatus: TaskStatus = t.status === 'completed' ? 'todo' : 'completed';
return { ...t, status: newStatus };
}
return t;
})
);
};
// Habit Operations
const handleToggleHabit = (habitId: string, dateStr: string = getFormattedDate(0)) => {
setHabits((prev) =>
prev.map((h) => {
if (h.id === habitId) {
const isDone = h.completedDates.includes(dateStr);
let updatedDates: string[];
if (isDone) {
updatedDates = h.completedDates.filter((d) => d !== dateStr);
} else {
updatedDates = [...h.completedDates, dateStr];
}
// Compute new streak
const newStreak = isDone ? Math.max(0, h.streak - 1) : h.streak + 1;
const newBestStreak = Math.max(h.bestStreak, newStreak);
return {
...h,
completedDates: updatedDates,
streak: newStreak,
bestStreak: newBestStreak,
};
}
return h;
})
);
};
const handleAddHabit = (newHabit: Omit<Habit, 'id' | 'streak' | 'bestStreak' | 'completedDates'>) => {
const habit: Habit = {
...newHabit,
id: `h-${Date.now()}`,
streak: 0,
bestStreak: 0,
completedDates: [],
};
setHabits((prev) => [...prev, habit]);
};
const handleDeleteHabit = (habitId: string) => {
setHabits((prev) => prev.filter((h) => h.id !== habitId));
};
// Event Operations
const handleAddEvent = (newEvent: Omit<CalendarEvent, 'id'>) => {
const event: CalendarEvent = {
...newEvent,
id: `ev-${Date.now()}`,
};
setEvents((prev) => [...prev, event]);
};
const handleDeleteEvent = (id: string) => {
setEvents((prev) => prev.filter((e) => e.id !== id));
};
// Finance Operations
const handleAddTransaction = (newTx: Omit<ExpenseItem, 'id'>) => {
const tx: ExpenseItem = {
...newTx,
id: `e-${Date.now()}`,
};
setExpenses((prev) => [tx, ...prev]);
};
const handleDeleteTransaction = (id: string) => {
setExpenses((prev) => prev.filter((e) => e.id !== id));
};
// Note Operations
const handleAddNote = (newNote: Omit<Note, 'id' | 'updatedAt'>) => {
const note: Note = {
...newNote,
id: `n-${Date.now()}`,
updatedAt: getFormattedDate(0),
};
setNotes((prev) => [note, ...prev]);
};
const handleUpdateNote = (id: string, updates: Partial<Note>) => {
setNotes((prev) =>
prev.map((n) => (n.id === id ? { ...n, ...updates, updatedAt: getFormattedDate(0) } : n))
);
};
const handleDeleteNote = (id: string) => {
setNotes((prev) => prev.filter((n) => n.id !== id));
};
// Fitness Operations
const handleAddWorkout = (newWorkout: Omit<WorkoutSession, 'id'>) => {
const session: WorkoutSession = {
...newWorkout,
id: `w-${Date.now()}`,
};
setWorkouts((prev) => [session, ...prev]);
// Automatically update muscle recovery states for trained muscle groups
setMuscleRecovery((prev) =>
prev.map((m) => {
const trained = newWorkout.muscleGroups.some((mg) =>
mg.toLowerCase().includes(m.muscle.toLowerCase()) || m.muscle.toLowerCase().includes(mg.toLowerCase())
);
if (trained) {
return {
...m,
lastTrainedDaysAgo: 0,
percentRecovered: 25,
status: 'fatigued',
};
}
return m;
})
);
};
const handleDeleteWorkout = (id: string) => {
setWorkouts((prev) => prev.filter((w) => w.id !== id));
};
const handleUpdateNutrition = (updates: Partial<DailyNutrition>) => {
setNutrition((prev) => ({ ...prev, ...updates }));
};
const pendingTasksCount = tasks.filter((t) => t.status !== 'completed').length;
return (
<div className="flex h-screen w-screen overflow-hidden bg-[#090d16] text-[#e2e8f0]">
{/* Sidebar Navigation */}
<Sidebar
activeModule={activeModule}
onSelectModule={(mod) => {
setActiveModule(mod);
setShowQuickAddTask(false);
setShowQuickAddNote(false);
}}
isCollapsed={isSidebarCollapsed}
onToggleCollapse={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
pendingTasksCount={pendingTasksCount}
habitsCount={habits.length}
/>
{/* Main Workspace */}
<div className="flex-1 flex flex-col min-w-0 h-full overflow-hidden">
{/* Top Header */}
<Header
activeModule={activeModule}
onQuickAddTask={() => {
setActiveModule('tasks');
setShowQuickAddTask(true);
}}
onQuickAddNote={() => {
setActiveModule('notes');
setShowQuickAddNote(true);
}}
/>
{/* Scrollable Module Canvas */}
<main className="flex-1 overflow-y-auto p-4 md:p-6 lg:p-8">
<div className="max-w-7xl mx-auto animate-in fade-in duration-300">
{activeModule === 'dashboard' && (
<DashboardView
tasks={tasks}
habits={habits}
expenses={expenses}
events={events}
notes={notes}
onNavigate={(mod) => setActiveModule(mod)}
onToggleTask={handleToggleTask}
onToggleHabit={handleToggleHabit}
/>
)}
{activeModule === 'fitness' && (
<FitnessView
workouts={workouts}
nutrition={nutrition}
muscleRecovery={muscleRecovery}
onAddWorkout={handleAddWorkout}
onDeleteWorkout={handleDeleteWorkout}
onUpdateNutrition={handleUpdateNutrition}
/>
)}
{activeModule === 'pomodoro' && (
<PomodoroView tasks={tasks} />
)}
{activeModule === 'tasks' && (
<TasksView
tasks={tasks}
onAddTask={handleAddTask}
onUpdateTask={handleUpdateTask}
onDeleteTask={handleDeleteTask}
showAddModalDefault={showQuickAddTask}
/>
)}
{activeModule === 'habits' && (
<HabitsView
habits={habits}
onToggleHabit={handleToggleHabit}
onAddHabit={handleAddHabit}
onDeleteHabit={handleDeleteHabit}
/>
)}
{activeModule === 'calendar' && (
<CalendarView
events={events}
onAddEvent={handleAddEvent}
onDeleteEvent={handleDeleteEvent}
/>
)}
{activeModule === 'finance' && (
<FinanceView
expenses={expenses}
onAddTransaction={handleAddTransaction}
onDeleteTransaction={handleDeleteTransaction}
/>
)}
{activeModule === 'notes' && (
<NotesView
notes={notes}
onAddNote={handleAddNote}
onUpdateNote={handleUpdateNote}
onDeleteNote={handleDeleteNote}
showAddModalDefault={showQuickAddNote}
/>
)}
</div>
</main>
</div>
</div>
);
}
+121
View File
@@ -0,0 +1,121 @@
'use client';
import React, { useState, useEffect } from 'react';
import { Clock, Plus, Bell, Sparkles } from 'lucide-react';
import { ActiveModule } from '@/lib/types';
interface HeaderProps {
activeModule: ActiveModule;
onQuickAddTask: () => void;
onQuickAddNote: () => void;
}
export const Header: React.FC<HeaderProps> = ({
activeModule,
onQuickAddTask,
onQuickAddNote,
}) => {
const [timeString, setTimeString] = useState<string>('');
const [dateString, setDateString] = useState<string>('');
useEffect(() => {
const updateTime = () => {
const now = new Date();
setTimeString(
now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
);
setDateString(
now.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' })
);
};
updateTime();
const interval = setInterval(updateTime, 1000);
return () => clearInterval(interval);
}, []);
const moduleTitles: Record<ActiveModule, { title: string; subtitle: string }> = {
dashboard: {
title: "Command Center",
subtitle: "Welcome back, Parth. Here is your daily productivity pulse.",
},
fitness: {
title: "Scientific Fitness & Nutrition Lab",
subtitle: "Evidence-based hypertrophy, progressive overload tracking, and macronutrient fueling.",
},
pomodoro: {
title: "Deep Work Focus Timer",
subtitle: "Eliminate distractions and enter the flow state.",
},
tasks: {
title: "Task & Project Board",
subtitle: "Track, prioritize, and conquer your goals.",
},
habits: {
title: "Habit Tracker & Streaks",
subtitle: "Consistency breeds excellence. Don't break the chain!",
},
calendar: {
title: "Calendar & Schedule",
subtitle: "Plan your timeline and stay ahead of deadlines.",
},
finance: {
title: "Personal Budget & Finance",
subtitle: "Master your cash flow, investments, and expenses.",
},
notes: {
title: "Notes & Reflection Journal",
subtitle: "Capture ideas, mental models, and daily mood reflections.",
},
};
const current = moduleTitles[activeModule] || moduleTitles.dashboard;
return (
<header className="h-20 border-b border-slate-800/80 bg-[#090d16]/80 backdrop-blur-xl px-6 flex items-center justify-between z-20 shrink-0">
{/* Title & Subtitle */}
<div>
<h1 className="text-xl font-bold text-slate-100 flex items-center gap-2">
{current.title}
<span className="inline-flex items-center gap-1 text-[11px] font-semibold px-2 py-0.5 rounded-full bg-cyan-500/10 text-cyan-400 border border-cyan-500/30">
<Sparkles className="w-3 h-3" /> Active
</span>
</h1>
<p className="text-xs text-slate-400 mt-0.5 hidden sm:block">
{current.subtitle}
</p>
</div>
{/* Right Side: Clock & Quick Actions */}
<div className="flex items-center gap-4">
{/* Live Clock */}
<div className="hidden md:flex items-center gap-2.5 px-3.5 py-1.5 rounded-xl bg-slate-900/80 border border-slate-800/80 text-xs">
<Clock className="w-4 h-4 text-cyan-400 animate-pulse" />
<div className="text-right">
<div className="font-mono font-semibold text-slate-200">{timeString || '--:--:--'}</div>
<div className="text-[10px] text-slate-400">{dateString || 'Loading...'}</div>
</div>
</div>
{/* Quick Add Buttons */}
<div className="flex items-center gap-2">
<button
onClick={onQuickAddTask}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 text-xs font-semibold transition-all hover:scale-105 active:scale-95 cursor-pointer shadow-sm"
>
<Plus className="w-3.5 h-3.5" />
<span className="hidden sm:inline">New Task</span>
</button>
<button
onClick={onQuickAddNote}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-purple-500/10 hover:bg-purple-500/20 text-purple-400 border border-purple-500/30 text-xs font-semibold transition-all hover:scale-105 active:scale-95 cursor-pointer shadow-sm"
>
<Plus className="w-3.5 h-3.5" />
<span className="hidden sm:inline">New Note</span>
</button>
</div>
</div>
</header>
);
};
+207
View File
@@ -0,0 +1,207 @@
'use client';
import React from 'react';
import {
LayoutDashboard,
CheckSquare,
Flame,
Calendar as CalendarIcon,
Wallet,
BookOpen,
Timer,
Sparkles,
ChevronLeft,
ChevronRight,
UserCheck,
Dumbbell
} from 'lucide-react';
import { ActiveModule } from '@/lib/types';
interface SidebarProps {
activeModule: ActiveModule;
onSelectModule: (module: ActiveModule) => void;
isCollapsed: boolean;
onToggleCollapse: () => void;
pendingTasksCount: number;
habitsCount: number;
}
export const Sidebar: React.FC<SidebarProps> = ({
activeModule,
onSelectModule,
isCollapsed,
onToggleCollapse,
pendingTasksCount,
habitsCount,
}) => {
const navItems = [
{
id: 'dashboard' as ActiveModule,
label: 'Command Center',
icon: LayoutDashboard,
badge: null,
glowColor: 'group-hover:text-cyan-400',
activeBg: 'bg-cyan-500/10 text-cyan-400 border-cyan-500/40',
},
{
id: 'fitness' as ActiveModule,
label: 'Fitness Lab',
icon: Dumbbell,
badge: 'MPS',
glowColor: 'group-hover:text-lime-400',
activeBg: 'bg-lime-500/10 text-lime-400 border-lime-500/40',
},
{
id: 'pomodoro' as ActiveModule,
label: 'Focus Timer',
icon: Timer,
badge: '25m',
glowColor: 'group-hover:text-amber-400',
activeBg: 'bg-amber-500/10 text-amber-400 border-amber-500/40',
},
{
id: 'tasks' as ActiveModule,
label: 'Tasks & Kanban',
icon: CheckSquare,
badge: pendingTasksCount > 0 ? `${pendingTasksCount}` : null,
glowColor: 'group-hover:text-emerald-400',
activeBg: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/40',
},
{
id: 'habits' as ActiveModule,
label: 'Habits & Streaks',
icon: Flame,
badge: habitsCount > 0 ? `${habitsCount}` : null,
glowColor: 'group-hover:text-orange-400',
activeBg: 'bg-orange-500/10 text-orange-400 border-orange-500/40',
},
{
id: 'calendar' as ActiveModule,
label: 'Calendar Planner',
icon: CalendarIcon,
badge: null,
glowColor: 'group-hover:text-blue-400',
activeBg: 'bg-blue-500/10 text-blue-400 border-blue-500/40',
},
{
id: 'finance' as ActiveModule,
label: 'Budget & Finance',
icon: Wallet,
badge: null,
glowColor: 'group-hover:text-teal-400',
activeBg: 'bg-teal-500/10 text-teal-400 border-teal-500/40',
},
{
id: 'notes' as ActiveModule,
label: 'Notes & Journal',
icon: BookOpen,
badge: null,
glowColor: 'group-hover:text-purple-400',
activeBg: 'bg-purple-500/10 text-purple-400 border-purple-500/40',
},
];
return (
<aside
className={`relative flex flex-col justify-between h-screen bg-[#0a0f1d] border-r border-slate-800/80 transition-all duration-300 z-30 select-none ${
isCollapsed ? 'w-20' : 'w-64'
}`}
>
{/* Brand Header */}
<div className="p-4 border-b border-slate-800/70">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 overflow-hidden">
<div className="w-10 h-10 rounded-xl bg-gradient-to-tr from-cyan-500 to-blue-600 flex items-center justify-center shadow-lg shadow-cyan-500/20 shrink-0">
<Sparkles className="w-5 h-5 text-white" />
</div>
{!isCollapsed && (
<div className="flex flex-col">
<span className="font-bold text-base text-slate-100 tracking-tight whitespace-nowrap">
Parth&apos;s Life
</span>
<span className="text-[11px] font-semibold text-cyan-400 tracking-wider uppercase">
Command Center
</span>
</div>
)}
</div>
<button
onClick={onToggleCollapse}
className="p-1.5 rounded-lg text-slate-400 hover:text-slate-200 hover:bg-slate-800/60 transition-colors"
title={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
>
{isCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
</button>
</div>
</div>
{/* Navigation List */}
<div className="flex-1 py-4 px-3 space-y-1.5 overflow-y-auto">
<div className="text-[11px] font-semibold text-slate-500 px-3 uppercase tracking-wider mb-2">
{!isCollapsed ? 'Modules' : '•••'}
</div>
{navItems.map((item) => {
const Icon = item.icon;
const isActive = activeModule === item.id;
return (
<button
key={item.id}
onClick={() => onSelectModule(item.id)}
className={`w-full flex items-center gap-3.5 px-3 py-2.5 rounded-xl text-sm font-medium transition-all group relative border ${
isActive
? item.activeBg
: 'text-slate-400 border-transparent hover:text-slate-200 hover:bg-slate-800/40 hover:border-slate-800/50'
}`}
title={isCollapsed ? item.label : undefined}
>
<Icon className={`w-5 h-5 shrink-0 transition-transform group-hover:scale-110 ${isActive ? '' : item.glowColor}`} />
{!isCollapsed && (
<span className="flex-1 text-left truncate">
{item.label}
</span>
)}
{!isCollapsed && item.badge && (
<span className={`text-[11px] font-bold px-2 py-0.5 rounded-full ${
isActive
? 'bg-white/10 text-white'
: 'bg-slate-800 text-slate-400 border border-slate-700/50'
}`}>
{item.badge}
</span>
)}
{/* Collapsed Badge Dot */}
{isCollapsed && item.badge && (
<span className="absolute top-2 right-2 w-2 h-2 rounded-full bg-cyan-400" />
)}
</button>
);
})}
</div>
{/* User / Status Footer */}
<div className="p-3 border-t border-slate-800/70">
<div className={`flex items-center gap-3 p-2 rounded-xl bg-slate-900/60 border border-slate-800/60 ${isCollapsed ? 'justify-center' : ''}`}>
<div className="relative shrink-0">
<div className="w-9 h-9 rounded-full bg-gradient-to-br from-violet-500 via-indigo-500 to-cyan-400 flex items-center justify-center text-white font-bold text-sm shadow-md">
P
</div>
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-emerald-400 border-2 border-[#0a0f1d] rounded-full"></span>
</div>
{!isCollapsed && (
<div className="flex-1 min-w-0">
<p className="text-xs font-semibold text-slate-200 truncate">Parth</p>
<div className="flex items-center gap-1 text-[10px] text-emerald-400 font-medium">
<UserCheck className="w-3 h-3" />
<span>Focus Active</span>
</div>
</div>
)}
</div>
</div>
</aside>
);
};
+101
View File
@@ -0,0 +1,101 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { X, Send, Heart } from "lucide-react";
interface CommentsSlideoutProps {
isOpen: boolean;
onClose: () => void;
post: any;
}
const MOCK_COMMENTS = [
{ id: "c1", user: "creative_soul", text: "This is absolutely stunning! 😍", likes: 12, time: "2h" },
{ id: "c2", user: "design_guru", text: "Love the composition and lighting here. Inspiring work as always.", likes: 8, time: "5h" },
{ id: "c3", user: "wanderlust.vibes", text: "Adding this to my bucket list rn 📝✨", likes: 45, time: "1d" },
{ id: "c4", user: "minimal_ui", text: "Clean and perfect.", likes: 2, time: "1d" },
];
export function CommentsSlideout({ isOpen, onClose, post }: CommentsSlideoutProps) {
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 bg-background/50 backdrop-blur-sm z-[100]"
/>
{/* Slideout Panel */}
<motion.div
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "spring", damping: 25, stiffness: 200 }}
className="fixed inset-y-0 right-0 w-full max-w-md glass-panel shadow-2xl z-[101] flex flex-col border-l border-border"
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-border bg-background/50">
<h2 className="text-xl font-bold">Comments</h2>
<button
onClick={onClose}
className="w-8 h-8 rounded-full hover:bg-muted flex items-center justify-center transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Post Caption (First Comment conceptually) */}
<div className="p-6 border-b border-border flex gap-4">
<img src={post.author.avatar} alt="Author" className="w-10 h-10 rounded-full" />
<div>
<span className="font-bold mr-2">{post.author.username}</span>
<span className="text-sm">{post.caption}</span>
<p className="text-xs text-muted-foreground mt-2 uppercase tracking-widest">{post.createdAt}</p>
</div>
</div>
{/* Comments List */}
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{MOCK_COMMENTS.map((comment) => (
<div key={comment.id} className="flex gap-4 group">
<div className="w-8 h-8 rounded-full bg-gradient-to-tr from-pink-500 to-yellow-500 flex-shrink-0" />
<div className="flex-1">
<span className="font-bold mr-2 text-sm">{comment.user}</span>
<span className="text-sm">{comment.text}</span>
<div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground font-semibold">
<span>{comment.time}</span>
<button className="hover:text-foreground transition-colors">{comment.likes} likes</button>
<button className="hover:text-foreground transition-colors">Reply</button>
</div>
</div>
<button className="flex-shrink-0 opacity-50 hover:opacity-100 group-hover:opacity-100 transition-opacity pt-1">
<Heart className="w-4 h-4" />
</button>
</div>
))}
</div>
{/* Input Area */}
<div className="p-4 border-t border-border bg-background/50">
<div className="relative flex items-center">
<input
type="text"
placeholder="Add a comment..."
className="w-full bg-transparent border border-border rounded-full py-3 px-6 pr-12 outline-none focus:ring-2 focus:ring-primary/50 transition-all placeholder:text-muted-foreground"
/>
<button className="absolute right-2 p-2 rounded-full hover:bg-muted text-primary transition-colors">
<Send className="w-5 h-5" />
</button>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
}
+119
View File
@@ -0,0 +1,119 @@
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Heart, MessageCircle, Bookmark, Share2 } from "lucide-react";
import { SaveBoardModal } from "@/components/feed/SaveBoardModal";
import { CommentsSlideout } from "@/components/feed/CommentsSlideout";
interface PostCardProps {
post: any;
}
export function PostCard({ post }: PostCardProps) {
const [isLiked, setIsLiked] = useState(false);
const [isSaved, setIsSaved] = useState(false);
const [showHeart, setShowHeart] = useState(false);
const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
const [isCommentsOpen, setIsCommentsOpen] = useState(false);
const handleDoubleTap = () => {
setIsLiked(true);
setShowHeart(true);
setTimeout(() => setShowHeart(false), 800);
};
const handleSaveClick = (e: React.MouseEvent) => {
e.stopPropagation();
setIsSaveModalOpen(true);
};
return (
<>
<div className="group relative break-inside-avoid mb-6 glass-card overflow-hidden w-full max-w-sm mx-auto shadow-sm hover:shadow-xl transition-all duration-500 object-cover">
{/* Header */}
<div className="flex items-center gap-3 p-3">
<img src={post.author.avatar} alt={post.author.username} className="w-8 h-8 rounded-full object-cover shadow-sm" />
<span className="font-semibold text-sm">{post.author.username}</span>
</div>
{/* Media Content */}
<div
className={`relative w-full ${post.aspectRatio} overflow-hidden bg-muted cursor-pointer`}
onDoubleClick={handleDoubleTap}
>
<img
src={post.mediaUrl}
alt={post.caption}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
/>
{/* Animated Double-Tap Heart */}
<AnimatePresence>
{showHeart && (
<motion.div
initial={{ scale: 0, opacity: 1 }}
animate={{ scale: 1.5, opacity: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.6, type: "spring" }}
className="absolute inset-0 flex items-center justify-center pointer-events-none drop-shadow-2xl"
>
<Heart className="w-32 h-32 text-red-500 fill-red-500" />
</motion.div>
)}
</AnimatePresence>
{/* Hover Action Overlay */}
<div className="absolute inset-x-0 bottom-0 p-4 bg-gradient-to-t from-black/60 via-black/30 to-transparent flex justify-between items-end opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="flex gap-4">
<button onClick={() => setIsLiked(!isLiked)} className="active:scale-90 transition-transform hover:scale-110">
<Heart className={`w-7 h-7 drop-shadow-md ${isLiked ? "text-red-500 fill-red-500" : "text-white"}`} />
</button>
<button onClick={() => setIsCommentsOpen(true)} className="active:scale-90 transition-transform hover:scale-110">
<MessageCircle className="w-7 h-7 text-white drop-shadow-md" />
</button>
<button className="active:scale-90 transition-transform hover:scale-110">
<Share2 className="w-7 h-7 text-white drop-shadow-md" />
</button>
</div>
<button
onClick={handleSaveClick}
className={`px-5 py-2 rounded-full font-bold text-sm transition-all active:scale-95 shadow-lg ${
isSaved ? "bg-black text-white border-none" : "bg-red-600 hover:bg-red-700 text-white"
}`}
>
{isSaved ? "Saved" : "Save"}
</button>
</div>
</div>
{/* Footer Info */}
<div className="p-4 space-y-2">
<p className="text-sm font-semibold">
{isLiked ? post.likes + 1 : post.likes} likes
</p>
<p className="text-sm line-clamp-2">
<span className="font-semibold mr-2">{post.author.username}</span>
{post.caption}
</p>
<p className="text-xs text-muted-foreground uppercase tracking-widest mt-1">
{post.createdAt}
</p>
</div>
</div>
<SaveBoardModal
isOpen={isSaveModalOpen}
onClose={() => { setIsSaveModalOpen(false); setIsSaved(true); }}
postMediaUrl={post.mediaUrl}
/>
<CommentsSlideout
isOpen={isCommentsOpen}
onClose={() => setIsCommentsOpen(false)}
post={post}
/>
</>
);
}
+106
View File
@@ -0,0 +1,106 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { X, Plus, Image as ImageIcon } from "lucide-react";
import { useState } from "react";
interface SaveModalProps {
isOpen: boolean;
onClose: () => void;
postMediaUrl: string;
}
const MOCK_BOARDS = [
{ id: "b1", name: "Architecture Inspo", cover: "https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=100&q=80" },
{ id: "b2", name: "Streetwear 2024", cover: "https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?w=100&q=80" },
{ id: "b3", name: "Travel Bucketlist", cover: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=100&q=80" },
];
export function SaveBoardModal({ isOpen, onClose, postMediaUrl }: SaveModalProps) {
const [savedBoard, setSavedBoard] = useState<string | null>(null);
const handleSave = (boardId: string) => {
setSavedBoard(boardId);
setTimeout(() => {
onClose();
setSavedBoard(null);
}, 1200);
};
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-background/80 backdrop-blur-sm"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20, opacity: 0 }}
animate={{ scale: 1, y: 0, opacity: 1 }}
exit={{ scale: 0.9, y: 20, opacity: 0 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="w-full max-w-lg glass-panel rounded-3xl overflow-hidden shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-border">
<h2 className="text-xl font-bold">Save to board</h2>
<button
onClick={onClose}
className="w-8 h-8 rounded-full hover:bg-muted flex items-center justify-center transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="flex p-6 gap-6">
<div className="w-1/3 aspect-[3/4] rounded-xl overflow-hidden shadow-md">
<img src={postMediaUrl} className="w-full h-full object-cover" alt="Post preview" />
</div>
<div className="flex-1 space-y-4">
<div className="flex items-center gap-3 p-3 rounded-xl border border-dashed border-border hover:bg-muted/50 transition-colors cursor-pointer text-muted-foreground group">
<div className="w-10 h-10 rounded-lg bg-muted flex items-center justify-center group-hover:bg-background transition-colors">
<Plus className="w-6 h-6" />
</div>
<span className="font-medium group-hover:text-foreground transition-colors">Create new board</span>
</div>
<div className="space-y-2">
<p className="text-sm font-semibold text-muted-foreground px-1">All boards</p>
<div className="space-y-1 max-h-[40vh] overflow-y-auto no-scrollbar pr-2">
{MOCK_BOARDS.map(board => (
<button
key={board.id}
onClick={() => handleSave(board.id)}
className="w-full flex items-center justify-between p-2 rounded-xl hover:bg-muted transition-colors group"
>
<div className="flex items-center gap-3">
<img src={board.cover} className="w-12 h-12 rounded-lg object-cover shadow-sm" alt={board.name} />
<span className="font-semibold">{board.name}</span>
</div>
{savedBoard === board.id ? (
<span className="px-3 py-1 bg-black text-white dark:bg-white dark:text-black rounded-full text-xs font-bold">
Saved!
</span>
) : (
<span className="px-4 py-1.5 bg-primary text-primary-foreground rounded-full text-sm font-bold opacity-0 group-hover:opacity-100 transition-opacity">
Save
</span>
)}
</button>
))}
</div>
</div>
</div>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
+39
View File
@@ -0,0 +1,39 @@
"use client";
import { motion } from "framer-motion";
import { MOCK_USERS } from "@/lib/mock-data";
export function StoriesBanner() {
return (
<div className="w-full mb-8 pt-6 pb-2 overflow-x-auto no-scrollbar">
<div className="flex gap-4 px-4 min-w-max">
{/* Current User Add Story */}
<div className="flex flex-col items-center gap-2 cursor-pointer relative group">
<div className="w-16 h-16 rounded-full glass border-2 border-dashed border-muted-foreground flex items-center justify-center overflow-hidden transition-transform group-active:scale-95">
<span className="text-2xl text-muted-foreground">+</span>
</div>
<span className="text-xs font-medium">Your Story</span>
</div>
{MOCK_USERS.filter(u => u.hasStory).map((user, i) => (
<motion.div
key={user.id}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: i * 0.1 }}
className="flex flex-col items-center gap-2 cursor-pointer group"
>
<div className="w-16 h-16 rounded-full p-[2px] bg-gradient-to-tr from-yellow-400 via-red-500 to-pink-500 transition-transform group-hover:scale-105 group-active:scale-95">
<div className="w-full h-full rounded-full border-2 border-background overflow-hidden relative">
<img src={user.avatar} alt={user.username} className="w-full h-full object-cover" />
</div>
</div>
<span className="text-xs font-medium text-muted-foreground truncate w-16 text-center">
{user.username}
</span>
</motion.div>
))}
</div>
</div>
);
}
+87
View File
@@ -0,0 +1,87 @@
"use client";
import { Home, Compass, PlusSquare, Heart, User, Send, Settings, Bookmark } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ThemeToggle } from "@/components/theme-toggle";
const NAV_ITEMS = [
{ icon: Home, label: "Home", href: "/feed" },
{ icon: Compass, label: "Explore", href: "/explore" },
{ icon: PlusSquare, label: "Create", href: "/create" },
{ icon: Heart, label: "Notifications", href: "/notifications" },
{ icon: Send, label: "Messages", href: "/messages" },
{ icon: Bookmark, label: "Saved Boards", href: "/boards" },
{ icon: User, label: "Profile", href: "/profile" },
];
export function AppShell({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
return (
<div className="min-h-screen bg-background text-foreground flex" suppressHydrationWarning>
{/* Desktop Sidebar */}
<aside className="hidden md:flex flex-col w-64 border-r border-border p-6 sticky top-0 h-screen overflow-y-auto glass-panel z-50">
<div className="flex items-center justify-between mb-10" suppressHydrationWarning>
<h1 className="text-2xl font-extrabold text-gradient">Pinstagram</h1>
<ThemeToggle className="w-8 h-8 scale-75" />
</div>
<nav className="flex-1 space-y-2">
{NAV_ITEMS.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-4 px-4 py-3 rounded-2xl transition-all font-medium ${
isActive
? "bg-primary text-primary-foreground shadow-md"
: "hover:bg-muted dark:hover:bg-white/5 active:scale-95"
}`}
>
<item.icon className={`w-6 h-6 ${isActive ? "fill-primary-foreground/20" : ""}`} />
<span className="text-lg">{item.label}</span>
</Link>
)
})}
</nav>
<div className="mt-auto pt-6 border-t border-border" suppressHydrationWarning>
<button className="flex items-center gap-4 px-4 py-3 rounded-2xl w-full hover:bg-muted dark:hover:bg-white/5 transition-all active:scale-95 font-medium">
<Settings className="w-6 h-6" />
<span className="text-lg">Settings</span>
</button>
</div>
</aside>
{/* Main Content Area */}
<main className="flex-1 min-w-0 pb-20 md:pb-0 overflow-y-auto no-scrollbar">
<div className="max-w-7xl mx-auto w-full" suppressHydrationWarning>
{children}
</div>
</main>
{/* Mobile Bottom Nav */}
<nav className="md:hidden fixed bottom-0 w-full glass-panel border-t border-border flex justify-around items-center p-3 pb-safe z-50 text-foreground">
{[
{ icon: Home, href: "/feed" },
{ icon: Compass, href: "/explore" },
{ icon: PlusSquare, href: "/create" },
{ icon: Heart, href: "/notifications" },
{ icon: User, href: "/profile" },
].map((item) => {
const isActive = pathname === item.href;
return (
<Link key={item.href} href={item.href} className="p-3 relative">
<item.icon className={`w-7 h-7 transition-all ${isActive ? "text-primary fill-primary/20 scale-110" : "text-muted-foreground"}`} />
{isActive && (
<span className="absolute bottom-1 left-1/2 -translate-x-1/2 w-1.5 h-1.5 bg-primary rounded-full" />
)}
</Link>
);
})}
</nav>
</div>
);
}
+415
View File
@@ -0,0 +1,415 @@
'use client';
import React, { useState } from 'react';
import {
ChevronLeft,
ChevronRight,
Plus,
Clock,
Trash2,
Calendar as CalendarIcon,
X,
Sparkles
} from 'lucide-react';
import { CalendarEvent } from '@/lib/types';
import { getFormattedDate } from '@/lib/initialData';
import { playNotificationChime } from '@/lib/audio';
interface CalendarViewProps {
events: CalendarEvent[];
onAddEvent: (event: Omit<CalendarEvent, 'id'>) => void;
onDeleteEvent: (id: string) => void;
}
export const CalendarView: React.FC<CalendarViewProps> = ({
events,
onAddEvent,
onDeleteEvent,
}) => {
const [currentDate, setCurrentDate] = useState(new Date());
const [selectedDateStr, setSelectedDateStr] = useState<string>(getFormattedDate(0));
const [isModalOpen, setIsModalOpen] = useState(false);
// New Event Form
const [title, setTitle] = useState('');
const [startTime, setStartTime] = useState('10:00');
const [endTime, setEndTime] = useState('11:00');
const [category, setCategory] = useState<'work' | 'personal' | 'study' | 'health'>('work');
const [description, setDescription] = useState('');
// Month navigation
const prevMonth = () => {
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1));
};
const nextMonth = () => {
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1));
};
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const monthName = currentDate.toLocaleDateString([], { month: 'long', year: 'numeric' });
// Compute days in current month
const firstDayOfMonth = new Date(year, month, 1).getDay(); // 0 = Sunday
const daysInMonth = new Date(year, month + 1, 0).getDate();
// Calendar cells: empty padding days + actual days
const paddingDays = Array.from({ length: firstDayOfMonth }, (_, i) => i);
const monthDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
const handleCreate = (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim()) return;
onAddEvent({
title: title.trim(),
date: selectedDateStr,
startTime,
endTime,
category,
description: description.trim() || undefined,
});
setTitle('');
setDescription('');
setIsModalOpen(false);
playNotificationChime('click');
};
const selectedDayEvents = events.filter((e) => e.date === selectedDateStr);
const getCategoryColor = (cat: string) => {
switch (cat) {
case 'work':
return 'bg-cyan-500/20 text-cyan-300 border-cyan-500/30';
case 'study':
return 'bg-purple-500/20 text-purple-300 border-purple-500/30';
case 'health':
return 'bg-emerald-500/20 text-emerald-300 border-emerald-500/30';
default:
return 'bg-amber-500/20 text-amber-300 border-amber-500/30';
}
};
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left 2 Cols: Monthly Calendar Grid */}
<div className="lg:col-span-2 glass-card p-6 space-y-6">
{/* Month Navigation */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2.5 rounded-xl bg-blue-500/10 text-blue-400">
<CalendarIcon className="w-5 h-5" />
</div>
<h2 className="text-xl font-bold text-slate-100">{monthName}</h2>
</div>
<div className="flex items-center gap-2">
<button
onClick={prevMonth}
className="p-2 rounded-xl bg-slate-900 border border-slate-800 text-slate-400 hover:text-white transition-colors"
>
<ChevronLeft className="w-4 h-4" />
</button>
<button
onClick={() => {
setCurrentDate(new Date());
setSelectedDateStr(getFormattedDate(0));
}}
className="px-3 py-1.5 rounded-xl bg-slate-900 border border-slate-800 text-xs font-semibold text-slate-300 hover:text-white transition-colors"
>
Today
</button>
<button
onClick={nextMonth}
className="p-2 rounded-xl bg-slate-900 border border-slate-800 text-slate-400 hover:text-white transition-colors"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
{/* Days of Week Header */}
<div className="grid grid-cols-7 gap-2 text-center text-xs font-bold text-slate-500 uppercase tracking-wider">
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((d) => (
<div key={d} className="py-1">
{d}
</div>
))}
</div>
{/* Grid Cells */}
<div className="grid grid-cols-7 gap-2">
{paddingDays.map((p) => (
<div key={`pad-${p}`} className="h-20 sm:h-24 rounded-xl bg-slate-950/20 opacity-30" />
))}
{monthDays.map((day) => {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const isToday = dateStr === getFormattedDate(0);
const isSelected = dateStr === selectedDateStr;
const dayEvents = events.filter((e) => e.date === dateStr);
return (
<div
key={dateStr}
onClick={() => setSelectedDateStr(dateStr)}
className={`h-20 sm:h-24 p-2 rounded-xl border flex flex-col justify-between transition-all cursor-pointer select-none relative ${
isSelected
? 'bg-blue-950/40 border-blue-500 shadow-md shadow-blue-500/20'
: isToday
? 'bg-slate-900/90 border-cyan-500/50'
: 'bg-slate-900/50 border-slate-800/80 hover:border-slate-700'
}`}
>
<div className="flex items-center justify-between">
<span
className={`text-xs font-bold w-6 h-6 rounded-full flex items-center justify-center ${
isToday
? 'bg-cyan-500 text-slate-950 shadow-sm shadow-cyan-500/50'
: isSelected
? 'text-blue-400 font-extrabold'
: 'text-slate-400'
}`}
>
{day}
</span>
{dayEvents.length > 0 && (
<span className="text-[10px] font-bold px-1.5 py-0.2 rounded-full bg-slate-800 text-slate-300">
{dayEvents.length}
</span>
)}
</div>
{/* Mini Dots / Previews */}
<div className="space-y-1 overflow-hidden">
{dayEvents.slice(0, 2).map((evt) => (
<div
key={evt.id}
className={`text-[10px] truncate px-1 py-0.5 rounded border ${getCategoryColor(
evt.category
)}`}
>
{evt.title}
</div>
))}
{dayEvents.length > 2 && (
<div className="text-[9px] text-slate-500 font-semibold pl-1">
+{dayEvents.length - 2} more
</div>
)}
</div>
</div>
);
})}
</div>
</div>
{/* Right Col: Selected Day Agenda & Quick Add */}
<div className="glass-card p-6 flex flex-col justify-between space-y-6">
<div>
<div className="flex items-center justify-between pb-4 border-b border-slate-800">
<div>
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">
Daily Agenda
</span>
<h3 className="text-lg font-extrabold text-white mt-0.5">
{new Date(selectedDateStr + 'T00:00:00').toLocaleDateString([], {
weekday: 'short',
month: 'short',
day: 'numeric',
})}
</h3>
</div>
<button
onClick={() => setIsModalOpen(true)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-blue-500/20 hover:bg-blue-500/30 text-blue-400 border border-blue-500/40 text-xs font-bold transition-all cursor-pointer"
>
<Plus className="w-3.5 h-3.5" />
<span>Add Event</span>
</button>
</div>
{/* Agenda Event Cards */}
<div className="mt-4 space-y-3 overflow-y-auto max-h-[420px]">
{selectedDayEvents.length > 0 ? (
selectedDayEvents.map((evt) => (
<div
key={evt.id}
className="p-4 rounded-xl bg-slate-900/90 border border-slate-800/90 hover:border-slate-700 transition-all group"
>
<div className="flex items-start justify-between gap-2">
<span
className={`text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-md border ${getCategoryColor(
evt.category
)}`}
>
{evt.category}
</span>
<button
onClick={() => onDeleteEvent(evt.id)}
className="text-slate-400 hover:text-red-400 p-1 rounded transition-colors"
title="Delete Event"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
<h4 className="mt-2 text-sm font-semibold text-slate-100">{evt.title}</h4>
<div className="mt-2 flex items-center gap-2 text-xs text-slate-400">
<Clock className="w-3.5 h-3.5 text-blue-400" />
<span>
{evt.startTime} - {evt.endTime}
</span>
</div>
{evt.description && (
<p className="mt-2 text-xs text-slate-400 leading-relaxed bg-slate-950/40 p-2 rounded-lg">
{evt.description}
</p>
)}
</div>
))
) : (
<div className="py-12 flex flex-col items-center justify-center text-center text-slate-500 text-xs space-y-2">
<Sparkles className="w-6 h-6 text-slate-600" />
<p>No events scheduled for this day.</p>
<button
onClick={() => setIsModalOpen(true)}
className="text-blue-400 hover:text-blue-300 font-semibold"
>
Schedule something now
</button>
</div>
)}
</div>
</div>
</div>
{/* Create Event Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm animate-in fade-in duration-200">
<div className="relative w-full max-w-md rounded-2xl bg-[#0e1424] border border-blue-500/30 p-6 shadow-2xl space-y-4">
<div className="flex items-center justify-between pb-3 border-b border-slate-800">
<h3 className="text-base font-bold text-white flex items-center gap-2">
<CalendarIcon className="w-4 h-4 text-blue-400" />
Schedule Event
</h3>
<button
onClick={() => setIsModalOpen(false)}
className="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-800"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleCreate} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Event Title *
</label>
<input
type="text"
required
placeholder="e.g. Deep Work Session, Team Sync, Gym"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-blue-500"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Date
</label>
<input
type="date"
value={selectedDateStr}
onChange={(e) => setSelectedDateStr(e.target.value)}
className="w-full px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 focus:outline-none focus:border-blue-500"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Category
</label>
<select
value={category}
onChange={(e) =>
setCategory(e.target.value as 'work' | 'personal' | 'study' | 'health')
}
className="w-full px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 focus:outline-none focus:border-blue-500"
>
<option value="work">Work</option>
<option value="study">Study</option>
<option value="health">Health</option>
<option value="personal">Personal</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Start Time
</label>
<input
type="time"
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
className="w-full px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 focus:outline-none focus:border-blue-500"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
End Time
</label>
<input
type="time"
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
className="w-full px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 focus:outline-none focus:border-blue-500"
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Description
</label>
<textarea
rows={2}
placeholder="Notes, agenda, or link..."
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 focus:outline-none focus:border-blue-500 resize-none"
/>
</div>
<div className="pt-3 border-t border-slate-800 flex justify-end gap-3">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs font-semibold text-slate-300 transition-colors"
>
Cancel
</button>
<button
type="submit"
className="px-5 py-2 rounded-xl bg-gradient-to-r from-blue-500 to-indigo-600 text-white text-xs font-bold shadow-md shadow-blue-500/20 hover:scale-105 active:scale-95 transition-all cursor-pointer"
>
Save to Calendar
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};
+401
View File
@@ -0,0 +1,401 @@
'use client';
import React from 'react';
import {
CheckSquare,
Flame,
Wallet,
Timer,
Calendar,
Sparkles,
ArrowUpRight,
CheckCircle2,
Circle,
Clock,
TrendingUp,
AlertCircle
} from 'lucide-react';
import { Task, Habit, ExpenseItem, CalendarEvent, Note, ActiveModule } from '@/lib/types';
import { getFormattedDate } from '@/lib/initialData';
import { playNotificationChime } from '@/lib/audio';
interface DashboardViewProps {
tasks: Task[];
habits: Habit[];
expenses: ExpenseItem[];
events: CalendarEvent[];
notes: Note[];
onNavigate: (module: ActiveModule) => void;
onToggleTask: (taskId: string) => void;
onToggleHabit: (habitId: string) => void;
}
export const DashboardView: React.FC<DashboardViewProps> = ({
tasks,
habits,
expenses,
events,
notes,
onNavigate,
onToggleTask,
onToggleHabit,
}) => {
const todayStr = getFormattedDate(0);
// Calculations
const pendingTasks = tasks.filter(t => t.status !== 'completed');
const urgentTasksCount = pendingTasks.filter(t => t.priority === 'urgent' || t.priority === 'high').length;
const habitsDoneToday = habits.filter(h => h.completedDates.includes(todayStr)).length;
const habitCompletionRate = habits.length > 0 ? Math.round((habitsDoneToday / habits.length) * 100) : 0;
const highestStreak = Math.max(...habits.map(h => h.streak), 0);
const totalIncome = expenses.filter(e => e.type === 'income').reduce((acc, curr) => acc + curr.amount, 0);
const totalExpense = expenses.filter(e => e.type === 'expense').reduce((acc, curr) => acc + curr.amount, 0);
const netSavings = totalIncome - totalExpense;
const todayEvents = events.filter(e => e.date === todayStr);
return (
<div className="space-y-6 pb-12">
{/* Welcome Hero Banner */}
<div className="relative overflow-hidden rounded-3xl bg-gradient-to-r from-[#11192e] via-[#0d1527] to-[#121c33] border border-cyan-500/20 p-6 md:p-8 shadow-2xl">
<div className="absolute -right-12 -top-12 w-64 h-64 bg-cyan-500/10 rounded-full blur-3xl pointer-events-none" />
<div className="absolute -left-12 -bottom-12 w-64 h-64 bg-purple-500/10 rounded-full blur-3xl pointer-events-none" />
<div className="relative z-10 flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="space-y-2">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-cyan-500/10 border border-cyan-500/30 text-cyan-400 text-xs font-semibold tracking-wide">
<Sparkles className="w-3.5 h-3.5" />
<span>Personal Command System Active</span>
</div>
<h2 className="text-2xl md:text-3xl font-extrabold text-white tracking-tight">
Ready to crush your goals, <span className="text-gradient-cyan">Parth?</span>
</h2>
<p className="text-sm text-slate-400 max-w-xl">
&ldquo;Small daily disciplines repeated consistently lead to monumental success.&rdquo; You have <strong className="text-slate-200">{pendingTasks.length} tasks</strong> remaining today.
</p>
</div>
<div className="flex flex-wrap gap-3">
<button
onClick={() => onNavigate('pomodoro')}
className="flex items-center gap-2 px-5 py-3 rounded-2xl bg-gradient-to-r from-amber-500 to-orange-500 text-slate-950 font-bold text-sm shadow-lg shadow-amber-500/20 hover:scale-105 active:scale-95 transition-all cursor-pointer"
>
<Timer className="w-4 h-4" />
<span>Start Focus Session</span>
</button>
<button
onClick={() => onNavigate('tasks')}
className="flex items-center gap-2 px-5 py-3 rounded-2xl bg-slate-800/80 hover:bg-slate-800 text-slate-200 border border-slate-700/70 font-semibold text-sm transition-all hover:scale-105 active:scale-95 cursor-pointer"
>
<span>View Kanban</span>
<ArrowUpRight className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Metrics Row */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Metric 1: Pending Tasks */}
<div
onClick={() => onNavigate('tasks')}
className="glass-card p-5 cursor-pointer hover:border-cyan-500/50 hover:shadow-cyan-500/10 group"
>
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Tasks Pending</span>
<div className="p-2.5 rounded-xl bg-cyan-500/10 text-cyan-400 group-hover:scale-110 transition-transform">
<CheckSquare className="w-5 h-5" />
</div>
</div>
<div className="mt-4 flex items-baseline gap-2">
<span className="text-3xl font-extrabold text-white">{pendingTasks.length}</span>
<span className="text-xs text-slate-500">active tasks</span>
</div>
<div className="mt-3 flex items-center gap-1.5 text-xs text-amber-400">
<AlertCircle className="w-3.5 h-3.5" />
<span>{urgentTasksCount} urgent / high priority</span>
</div>
</div>
{/* Metric 2: Habits Completion */}
<div
onClick={() => onNavigate('habits')}
className="glass-card p-5 cursor-pointer hover:border-orange-500/50 hover:shadow-orange-500/10 group"
>
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Today&apos;s Habits</span>
<div className="p-2.5 rounded-xl bg-orange-500/10 text-orange-400 group-hover:scale-110 transition-transform">
<Flame className="w-5 h-5" />
</div>
</div>
<div className="mt-4 flex items-baseline gap-2">
<span className="text-3xl font-extrabold text-white">{habitCompletionRate}%</span>
<span className="text-xs text-slate-500">({habitsDoneToday}/{habits.length} done)</span>
</div>
<div className="mt-3 flex items-center gap-1.5 text-xs text-orange-400">
<Flame className="w-3.5 h-3.5 fill-orange-400" />
<span>Top Streak: {highestStreak} days strong</span>
</div>
</div>
{/* Metric 3: Net Cash Balance */}
<div
onClick={() => onNavigate('finance')}
className="glass-card p-5 cursor-pointer hover:border-emerald-500/50 hover:shadow-emerald-500/10 group"
>
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Net Balance</span>
<div className="p-2.5 rounded-xl bg-emerald-500/10 text-emerald-400 group-hover:scale-110 transition-transform">
<Wallet className="w-5 h-5" />
</div>
</div>
<div className="mt-4 flex items-baseline gap-2">
<span className="text-3xl font-extrabold text-white">
${netSavings.toLocaleString()}
</span>
</div>
<div className="mt-3 flex items-center gap-1.5 text-xs text-emerald-400">
<TrendingUp className="w-3.5 h-3.5" />
<span>${totalIncome} In / ${totalExpense} Out</span>
</div>
</div>
{/* Metric 4: Focus & Schedule */}
<div
onClick={() => onNavigate('calendar')}
className="glass-card p-5 cursor-pointer hover:border-blue-500/50 hover:shadow-blue-500/10 group"
>
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Today&apos;s Agenda</span>
<div className="p-2.5 rounded-xl bg-blue-500/10 text-blue-400 group-hover:scale-110 transition-transform">
<Calendar className="w-5 h-5" />
</div>
</div>
<div className="mt-4 flex items-baseline gap-2">
<span className="text-3xl font-extrabold text-white">{todayEvents.length}</span>
<span className="text-xs text-slate-500">events scheduled</span>
</div>
<div className="mt-3 flex items-center gap-1.5 text-xs text-blue-400">
<Clock className="w-3.5 h-3.5" />
<span>{todayEvents[0]?.title || "Clear schedule ahead"}</span>
</div>
</div>
</div>
{/* Two Column Grid: Top Tasks & Habit Quick-Check */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Column 1: Priority Tasks */}
<div className="glass-card p-6 flex flex-col justify-between">
<div>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<CheckSquare className="w-5 h-5 text-cyan-400" />
<h3 className="font-bold text-slate-100 text-base">Top Priority Tasks</h3>
</div>
<button
onClick={() => onNavigate('tasks')}
className="text-xs text-cyan-400 hover:text-cyan-300 font-semibold flex items-center gap-1"
>
Kanban View <ArrowUpRight className="w-3.5 h-3.5" />
</button>
</div>
<div className="space-y-3">
{tasks.slice(0, 4).map((task) => {
const isDone = task.status === 'completed';
return (
<div
key={task.id}
className={`flex items-start gap-3 p-3.5 rounded-xl border transition-all ${
isDone
? 'bg-slate-900/40 border-slate-800/40 opacity-60'
: 'bg-slate-900/70 border-slate-800 hover:border-slate-700'
}`}
>
<button
onClick={() => {
onToggleTask(task.id);
playNotificationChime(isDone ? 'click' : 'complete');
}}
className="mt-0.5 text-slate-400 hover:text-cyan-400 transition-colors cursor-pointer shrink-0"
>
{isDone ? (
<CheckCircle2 className="w-5 h-5 text-emerald-400 fill-emerald-400/20" />
) : (
<Circle className="w-5 h-5" />
)}
</button>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<p className={`text-sm font-semibold truncate ${isDone ? 'line-through text-slate-500' : 'text-slate-200'}`}>
{task.title}
</p>
<span className={`text-[10px] uppercase font-bold px-2 py-0.5 rounded-full border shrink-0 ${
task.priority === 'urgent'
? 'bg-red-500/10 text-red-400 border-red-500/30'
: task.priority === 'high'
? 'bg-amber-500/10 text-amber-400 border-amber-500/30'
: 'bg-blue-500/10 text-blue-400 border-blue-500/30'
}`}>
{task.priority}
</span>
</div>
{task.description && (
<p className="text-xs text-slate-400 line-clamp-1 mt-0.5">{task.description}</p>
)}
</div>
</div>
);
})}
</div>
</div>
<button
onClick={() => onNavigate('tasks')}
className="mt-4 w-full py-2.5 rounded-xl bg-slate-800/60 hover:bg-slate-800 text-xs font-semibold text-slate-300 transition-colors border border-slate-700/60"
>
Manage All Tasks ({tasks.length})
</button>
</div>
{/* Column 2: Habits Quick Check-In */}
<div className="glass-card p-6 flex flex-col justify-between">
<div>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Flame className="w-5 h-5 text-orange-400" />
<h3 className="font-bold text-slate-100 text-base">Daily Habit Check-in</h3>
</div>
<button
onClick={() => onNavigate('habits')}
className="text-xs text-orange-400 hover:text-orange-300 font-semibold flex items-center gap-1"
>
Streak Matrix <ArrowUpRight className="w-3.5 h-3.5" />
</button>
</div>
<div className="space-y-3">
{habits.map((habit) => {
const isCompleted = habit.completedDates.includes(todayStr);
return (
<div
key={habit.id}
className={`flex items-center justify-between p-3.5 rounded-xl border transition-all ${
isCompleted
? 'bg-emerald-950/20 border-emerald-500/30'
: 'bg-slate-900/70 border-slate-800 hover:border-slate-700'
}`}
>
<div className="flex items-center gap-3">
<button
onClick={() => {
onToggleHabit(habit.id);
playNotificationChime(isCompleted ? 'click' : 'streak');
}}
className={`w-8 h-8 rounded-lg flex items-center justify-center border transition-all cursor-pointer ${
isCompleted
? 'bg-emerald-500 border-emerald-400 text-slate-950 shadow-md shadow-emerald-500/30'
: 'bg-slate-800/80 border-slate-700 text-slate-400 hover:border-slate-600'
}`}
>
<CheckCircle2 className={`w-5 h-5 ${isCompleted ? 'stroke-[2.5]' : 'opacity-40'}`} />
</button>
<div>
<p className={`text-sm font-semibold ${isCompleted ? 'text-emerald-300' : 'text-slate-200'}`}>
{habit.name}
</p>
<span className="text-[11px] text-slate-400">{habit.category}</span>
</div>
</div>
<div className="flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-orange-500/10 border border-orange-500/30 text-orange-400 text-xs font-bold">
<Flame className="w-3.5 h-3.5 fill-orange-400" />
<span>{habit.streak}d</span>
</div>
</div>
);
})}
</div>
</div>
<button
onClick={() => onNavigate('habits')}
className="mt-4 w-full py-2.5 rounded-xl bg-slate-800/60 hover:bg-slate-800 text-xs font-semibold text-slate-300 transition-colors border border-slate-700/60"
>
Track & Add Habits
</button>
</div>
</div>
{/* Bottom Row: Today's Timeline & Quick Notes Preview */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Calendar Events */}
<div className="glass-card p-6 lg:col-span-2">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Calendar className="w-5 h-5 text-blue-400" />
<h3 className="font-bold text-slate-100 text-base">Today&apos;s Schedule</h3>
</div>
<button
onClick={() => onNavigate('calendar')}
className="text-xs text-blue-400 hover:text-blue-300 font-semibold flex items-center gap-1"
>
Full Calendar <ArrowUpRight className="w-3.5 h-3.5" />
</button>
</div>
{todayEvents.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{todayEvents.map((evt) => (
<div
key={evt.id}
className="p-4 rounded-xl bg-slate-900/60 border border-slate-800 flex items-start gap-3"
>
<div className="w-2 h-10 rounded-full bg-blue-500 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-slate-200 truncate">{evt.title}</p>
<div className="flex items-center gap-2 text-xs text-slate-400 mt-1">
<Clock className="w-3 h-3" />
<span>{evt.startTime} - {evt.endTime}</span>
</div>
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-slate-500 py-4 text-center">No more events scheduled for today.</p>
)}
</div>
{/* Latest Note Pin */}
<div className="glass-card p-6 flex flex-col justify-between">
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="font-bold text-slate-100 text-base">Pinned Insight</h3>
<button
onClick={() => onNavigate('notes')}
className="text-xs text-purple-400 hover:text-purple-300 font-semibold"
>
All Notes
</button>
</div>
{notes[0] ? (
<div className="p-4 rounded-xl bg-purple-950/20 border border-purple-500/20">
<p className="text-xs font-bold text-purple-300 mb-1">{notes[0].title}</p>
<p className="text-xs text-slate-300 line-clamp-4 leading-relaxed font-mono whitespace-pre-line">
{notes[0].content}
</p>
</div>
) : (
<p className="text-xs text-slate-500">No notes captured yet.</p>
)}
</div>
</div>
</div>
</div>
);
};
+455
View File
@@ -0,0 +1,455 @@
'use client';
import React, { useState } from 'react';
import {
Wallet,
ArrowDownRight,
ArrowUpRight,
Plus,
Trash2,
DollarSign,
Tag,
X,
PieChart,
SlidersHorizontal
} from 'lucide-react';
import { ExpenseItem } from '@/lib/types';
import { getFormattedDate } from '@/lib/initialData';
import { playNotificationChime } from '@/lib/audio';
interface FinanceViewProps {
expenses: ExpenseItem[];
onAddTransaction: (item: Omit<ExpenseItem, 'id'>) => void;
onDeleteTransaction: (id: string) => void;
}
export const FinanceView: React.FC<FinanceViewProps> = ({
expenses,
onAddTransaction,
onDeleteTransaction,
}) => {
const [filterType, setFilterType] = useState<'all' | 'income' | 'expense'>('all');
const [isModalOpen, setIsModalOpen] = useState(false);
// New Transaction Form
const [title, setTitle] = useState('');
const [amount, setAmount] = useState('');
const [type, setType] = useState<'income' | 'expense'>('expense');
const [category, setCategory] = useState('Food');
const [date, setDate] = useState(getFormattedDate(0));
const [note, setNote] = useState('');
const monthlyBudgetLimit = 2500;
const totalIncome = expenses
.filter((e) => e.type === 'income')
.reduce((acc, curr) => acc + curr.amount, 0);
const totalExpense = expenses
.filter((e) => e.type === 'expense')
.reduce((acc, curr) => acc + curr.amount, 0);
const netBalance = totalIncome - totalExpense;
const budgetUsagePercent = Math.min(Math.round((totalExpense / monthlyBudgetLimit) * 100), 100);
// Calculate Category Breakdowns for Expenses
const categoryTotals: Record<string, number> = {};
expenses
.filter((e) => e.type === 'expense')
.forEach((e) => {
categoryTotals[e.category] = (categoryTotals[e.category] || 0) + e.amount;
});
const handleCreate = (e: React.FormEvent) => {
e.preventDefault();
const numAmount = parseFloat(amount);
if (!title.trim() || isNaN(numAmount) || numAmount <= 0) return;
onAddTransaction({
title: title.trim(),
amount: numAmount,
type,
category: category.trim() || 'General',
date,
note: note.trim() || undefined,
});
setTitle('');
setAmount('');
setNote('');
setIsModalOpen(false);
playNotificationChime(type === 'income' ? 'streak' : 'click');
};
const filteredList = expenses.filter((e) => {
if (filterType === 'all') return true;
return e.type === filterType;
});
return (
<div className="space-y-6">
{/* KPI Cards */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{/* Net Savings Card */}
<div className="glass-card p-5 border-l-4 border-l-cyan-500">
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Net Balance</span>
<div className="p-2 rounded-xl bg-cyan-500/10 text-cyan-400">
<Wallet className="w-5 h-5" />
</div>
</div>
<div className="mt-3">
<div className="text-3xl font-extrabold text-white">
${netBalance.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
<p className="text-xs text-slate-400 mt-1">Available liquid balance</p>
</div>
</div>
{/* Total Income Card */}
<div className="glass-card p-5 border-l-4 border-l-emerald-500">
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Total Income</span>
<div className="p-2 rounded-xl bg-emerald-500/10 text-emerald-400">
<ArrowUpRight className="w-5 h-5" />
</div>
</div>
<div className="mt-3">
<div className="text-3xl font-extrabold text-emerald-400">
+${totalIncome.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
<p className="text-xs text-slate-400 mt-1">Inflow this period</p>
</div>
</div>
{/* Total Expenses Card */}
<div className="glass-card p-5 border-l-4 border-l-rose-500">
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Total Expenses</span>
<div className="p-2 rounded-xl bg-rose-500/10 text-rose-400">
<ArrowDownRight className="w-5 h-5" />
</div>
</div>
<div className="mt-3">
<div className="text-3xl font-extrabold text-rose-400">
-${totalExpense.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
<p className="text-xs text-slate-400 mt-1">Outflow this period</p>
</div>
</div>
</div>
{/* Budget Meter & Category Breakdown */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Monthly Budget Bar */}
<div className="glass-card p-6 flex flex-col justify-between space-y-4">
<div>
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider">Monthly Budget Tracker</span>
<span className="text-xs font-bold text-cyan-400">{budgetUsagePercent}% Used</span>
</div>
<div className="mt-4">
<div className="flex items-baseline justify-between text-sm mb-2">
<span className="text-slate-300 font-semibold">${totalExpense} Spent</span>
<span className="text-slate-500">Target: ${monthlyBudgetLimit}</span>
</div>
<div className="w-full h-3 bg-slate-900 rounded-full overflow-hidden border border-slate-800">
<div
className={`h-full transition-all duration-500 rounded-full ${
budgetUsagePercent > 90 ? 'bg-rose-500' : budgetUsagePercent > 70 ? 'bg-amber-500' : 'bg-cyan-500'
}`}
style={{ width: `${budgetUsagePercent}%` }}
/>
</div>
</div>
<p className="text-xs text-slate-400 mt-3 leading-relaxed">
You have <strong className="text-slate-200">${Math.max(0, monthlyBudgetLimit - totalExpense)}</strong> remaining in your safe monthly spending threshold.
</p>
</div>
<button
onClick={() => setIsModalOpen(true)}
className="w-full py-2.5 rounded-xl bg-gradient-to-r from-teal-500 to-emerald-600 text-slate-950 font-bold text-xs shadow-lg shadow-teal-500/20 hover:scale-102 active:scale-98 transition-all cursor-pointer flex items-center justify-center gap-2"
>
<Plus className="w-4 h-4" />
<span>Record Transaction</span>
</button>
</div>
{/* Category Breakdown Progress Bars */}
<div className="glass-card p-6 lg:col-span-2 space-y-4">
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
<h3 className="text-sm font-bold text-slate-100 flex items-center gap-2">
<PieChart className="w-4 h-4 text-teal-400" />
<span>Expense Categories Distribution</span>
</h3>
<span className="text-xs text-slate-400">{Object.keys(categoryTotals).length} categories active</span>
</div>
<div className="space-y-3">
{Object.entries(categoryTotals).map(([cat, amount]) => {
const pct = totalExpense > 0 ? Math.round((amount / totalExpense) * 100) : 0;
return (
<div key={cat} className="space-y-1">
<div className="flex items-center justify-between text-xs">
<span className="font-semibold text-slate-200 flex items-center gap-1.5">
<Tag className="w-3 h-3 text-teal-400" />
{cat}
</span>
<span className="text-slate-400">
${amount.toLocaleString()} <span className="text-teal-400 font-bold">({pct}%)</span>
</span>
</div>
<div className="w-full h-2 bg-slate-900 rounded-full overflow-hidden border border-slate-800/80">
<div
className="h-full bg-gradient-to-r from-teal-500 to-emerald-400 rounded-full"
style={{ width: `${pct}%` }}
/>
</div>
</div>
);
})}
{Object.keys(categoryTotals).length === 0 && (
<p className="text-xs text-slate-500 py-6 text-center">No expense categories to display.</p>
)}
</div>
</div>
</div>
{/* Transaction History Ledger */}
<div className="glass-card overflow-hidden">
<div className="p-5 border-b border-slate-800 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h3 className="text-base font-bold text-slate-100">Transaction History</h3>
<p className="text-xs text-slate-400 mt-0.5">All income deposits and expenditure logs</p>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center p-1 rounded-xl bg-slate-900 border border-slate-800 text-xs">
<SlidersHorizontal className="w-3.5 h-3.5 text-slate-400 ml-2 mr-1" />
{(['all', 'income', 'expense'] as const).map((t) => (
<button
key={t}
onClick={() => setFilterType(t)}
className={`px-3 py-1 rounded-lg font-semibold uppercase tracking-wider text-[10px] transition-colors ${
filterType === t
? 'bg-teal-500/20 text-teal-300 border border-teal-500/40'
: 'text-slate-400 hover:text-slate-200'
}`}
>
{t}
</button>
))}
</div>
<button
onClick={() => setIsModalOpen(true)}
className="flex items-center gap-1.5 px-3 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-semibold transition-colors border border-slate-700"
>
<Plus className="w-3.5 h-3.5" />
<span>Add</span>
</button>
</div>
</div>
{/* Ledger Rows */}
<div className="divide-y divide-slate-800/60">
{filteredList.map((item) => {
const isInc = item.type === 'income';
return (
<div
key={item.id}
className="p-4 sm:px-6 flex items-center justify-between gap-4 hover:bg-slate-900/40 transition-colors group"
>
<div className="flex items-center gap-3 min-w-0">
<div className={`w-9 h-9 rounded-xl flex items-center justify-center border shrink-0 ${
isInc
? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30'
: 'bg-rose-500/10 text-rose-400 border-rose-500/30'
}`}>
{isInc ? <ArrowUpRight className="w-4 h-4" /> : <ArrowDownRight className="w-4 h-4" />}
</div>
<div className="min-w-0">
<p className="text-sm font-semibold text-slate-200 truncate">{item.title}</p>
<div className="flex items-center gap-2 text-xs text-slate-400 mt-0.5">
<span className="px-2 py-0.2 rounded-full bg-slate-900 border border-slate-800 text-[10px]">
{item.category}
</span>
<span>•</span>
<span>{item.date}</span>
{item.note && (
<>
<span className="hidden sm:inline">•</span>
<span className="hidden sm:inline truncate text-slate-500 italic">{item.note}</span>
</>
)}
</div>
</div>
</div>
<div className="flex items-center gap-4 shrink-0">
<div className={`text-base font-bold text-right ${isInc ? 'text-emerald-400' : 'text-slate-200'}`}>
{isInc ? '+' : '-'}${item.amount.toLocaleString(undefined, { minimumFractionDigits: 2 })}
</div>
<button
onClick={() => onDeleteTransaction(item.id)}
className="opacity-60 group-hover:opacity-100 text-slate-500 hover:text-red-400 p-1 rounded transition-all"
title="Delete Transaction"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
);
})}
{filteredList.length === 0 && (
<div className="p-12 text-center text-slate-500 text-xs">
No transactions recorded in this view.
</div>
)}
</div>
</div>
{/* Record Transaction Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm animate-in fade-in duration-200">
<div className="relative w-full max-w-md rounded-2xl bg-[#0e1424] border border-teal-500/30 p-6 shadow-2xl space-y-4">
<div className="flex items-center justify-between pb-3 border-b border-slate-800">
<h3 className="text-base font-bold text-white flex items-center gap-2">
<DollarSign className="w-4 h-4 text-teal-400" />
Record Transaction
</h3>
<button
onClick={() => setIsModalOpen(false)}
className="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-800"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleCreate} className="space-y-4">
{/* Type Switcher */}
<div className="grid grid-cols-2 gap-2 p-1 bg-slate-900 border border-slate-800 rounded-xl">
<button
type="button"
onClick={() => setType('expense')}
className={`py-2 text-xs font-bold rounded-lg transition-colors ${
type === 'expense'
? 'bg-rose-500 text-white shadow-md'
: 'text-slate-400 hover:text-slate-200'
}`}
>
Expense (-)
</button>
<button
type="button"
onClick={() => setType('income')}
className={`py-2 text-xs font-bold rounded-lg transition-colors ${
type === 'income'
? 'bg-emerald-500 text-white shadow-md'
: 'text-slate-400 hover:text-slate-200'
}`}
>
Income (+)
</button>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Title / Description *
</label>
<input
type="text"
required
placeholder="e.g. Grocery run, Freelance payment"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-teal-500"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Amount ($) *
</label>
<input
type="number"
step="0.01"
required
placeholder="0.00"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-teal-500"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Category
</label>
<input
type="text"
placeholder="Food, Health, Subscriptions"
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-teal-500"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Date
</label>
<input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
className="w-full px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 focus:outline-none focus:border-teal-500"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Optional Note
</label>
<input
type="text"
placeholder="e.g. Receipt #12"
value={note}
onChange={(e) => setNote(e.target.value)}
className="w-full px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 focus:outline-none focus:border-teal-500"
/>
</div>
</div>
<div className="pt-3 border-t border-slate-800 flex justify-end gap-3">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs font-semibold text-slate-300 transition-colors"
>
Cancel
</button>
<button
type="submit"
className="px-5 py-2 rounded-xl bg-gradient-to-r from-teal-500 to-emerald-600 text-slate-950 text-xs font-bold shadow-md shadow-teal-500/20 hover:scale-105 active:scale-95 transition-all cursor-pointer"
>
Save Entry
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};
File diff suppressed because it is too large Load Diff
+326
View File
@@ -0,0 +1,326 @@
'use client';
import React, { useState } from 'react';
import {
Flame,
Plus,
CheckCircle2,
Trash2,
Trophy,
Sparkles,
X,
Target
} from 'lucide-react';
import { Habit } from '@/lib/types';
import { getFormattedDate } from '@/lib/initialData';
import { playNotificationChime } from '@/lib/audio';
interface HabitsViewProps {
habits: Habit[];
onToggleHabit: (habitId: string, dateStr?: string) => void;
onAddHabit: (habit: Omit<Habit, 'id' | 'streak' | 'bestStreak' | 'completedDates'>) => void;
onDeleteHabit: (habitId: string) => void;
}
export const HabitsView: React.FC<HabitsViewProps> = ({
habits,
onToggleHabit,
onAddHabit,
onDeleteHabit,
}) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [name, setName] = useState('');
const [category, setCategory] = useState('Productivity');
const [color, setColor] = useState('#06b6d4');
const todayStr = getFormattedDate(0);
// Generate last 7 days array
const last7Days = [-6, -5, -4, -3, -2, -1, 0].map((offset) => {
const d = new Date();
d.setDate(d.getDate() + offset);
const dateStr = d.toISOString().split('T')[0];
const dayName = d.toLocaleDateString([], { weekday: 'narrow' });
const dayNum = d.getDate();
return { offset, dateStr, dayName, dayNum, isToday: offset === 0 };
});
const handleCreate = (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) return;
onAddHabit({
name: name.trim(),
category: category.trim() || 'General',
frequency: 'daily',
color,
});
setName('');
setIsModalOpen(false);
playNotificationChime('streak');
};
const completedTodayCount = habits.filter(h => h.completedDates.includes(todayStr)).length;
const completionRate = habits.length > 0 ? Math.round((completedTodayCount / habits.length) * 100) : 0;
const bestOverallStreak = Math.max(...habits.map(h => h.bestStreak), 0);
return (
<div className="space-y-6">
{/* Top Banner Stats */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="glass-card p-5 border-l-4 border-l-orange-500">
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Today&apos;s Score</span>
<Flame className="w-5 h-5 text-orange-400 fill-orange-400/20" />
</div>
<div className="mt-2 flex items-baseline gap-2">
<span className="text-3xl font-extrabold text-white">{completedTodayCount} / {habits.length}</span>
<span className="text-xs text-orange-400 font-bold">({completionRate}%)</span>
</div>
<p className="text-xs text-slate-400 mt-1">Keep the momentum alive!</p>
</div>
<div className="glass-card p-5 border-l-4 border-l-cyan-500">
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">All-Time Best Streak</span>
<Trophy className="w-5 h-5 text-cyan-400" />
</div>
<div className="mt-2 flex items-baseline gap-2">
<span className="text-3xl font-extrabold text-white">{bestOverallStreak}</span>
<span className="text-xs text-slate-400">days record</span>
</div>
<p className="text-xs text-slate-400 mt-1">Consistency builds character</p>
</div>
<div className="glass-card p-5 border-l-4 border-l-emerald-500 flex flex-col justify-between">
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Active Routines</span>
<Target className="w-5 h-5 text-emerald-400" />
</div>
<div className="mt-2 flex items-center justify-between">
<span className="text-3xl font-extrabold text-white">{habits.length}</span>
<button
onClick={() => setIsModalOpen(true)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-orange-500/10 hover:bg-orange-500/20 text-orange-400 border border-orange-500/30 text-xs font-bold transition-all cursor-pointer"
>
<Plus className="w-3.5 h-3.5" />
<span>Add Routine</span>
</button>
</div>
</div>
</div>
{/* Main Habits Matrix */}
<div className="glass-card overflow-hidden">
<div className="p-5 border-b border-slate-800 flex items-center justify-between">
<div>
<h3 className="text-base font-bold text-slate-100 flex items-center gap-2">
<Sparkles className="w-4 h-4 text-orange-400" />
<span>7-Day Consistency Matrix</span>
</h3>
<p className="text-xs text-slate-400 mt-0.5">Click any circle to toggle habit completion</p>
</div>
<button
onClick={() => setIsModalOpen(true)}
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-gradient-to-r from-orange-500 to-amber-500 text-slate-950 text-xs font-bold shadow-lg shadow-orange-500/20 hover:scale-105 active:scale-95 transition-all cursor-pointer"
>
<Plus className="w-3.5 h-3.5" />
<span>New Habit</span>
</button>
</div>
{/* Matrix Header Row */}
<div className="overflow-x-auto">
<div className="min-w-[640px]">
<div className="grid grid-cols-12 gap-4 px-6 py-3 bg-slate-900/60 border-b border-slate-800/80 text-xs font-semibold text-slate-400 uppercase">
<div className="col-span-5">Habit Routine</div>
<div className="col-span-2 text-center">Streak</div>
<div className="col-span-4 grid grid-cols-7 gap-1 text-center">
{last7Days.map(d => (
<div key={d.dateStr} className={`${d.isToday ? 'text-orange-400 font-bold' : ''}`}>
<div>{d.dayName}</div>
<div className="text-[10px] text-slate-400">{d.dayNum}</div>
</div>
))}
</div>
<div className="col-span-1 text-right">Actions</div>
</div>
{/* Habit Rows */}
<div className="divide-y divide-slate-800/60">
{habits.map((habit) => {
const isTodayDone = habit.completedDates.includes(todayStr);
return (
<div
key={habit.id}
className="grid grid-cols-12 gap-4 px-6 py-4 items-center hover:bg-slate-900/40 transition-colors"
>
{/* Habit Title & Quick Today Check */}
<div className="col-span-5 flex items-center gap-3">
<button
onClick={() => {
onToggleHabit(habit.id, todayStr);
playNotificationChime(isTodayDone ? 'click' : 'streak');
}}
className={`w-7 h-7 rounded-lg flex items-center justify-center border transition-all cursor-pointer ${
isTodayDone
? 'bg-emerald-500 border-emerald-400 text-slate-950 shadow-md shadow-emerald-500/30'
: 'bg-slate-900 border-slate-700 text-slate-400 hover:border-slate-500'
}`}
title={isTodayDone ? "Completed today!" : "Click to check off for today"}
>
<CheckCircle2 className={`w-4 h-4 ${isTodayDone ? 'stroke-[2.5]' : 'opacity-30'}`} />
</button>
<div className="min-w-0">
<p className={`text-sm font-semibold truncate ${isTodayDone ? 'text-emerald-300' : 'text-slate-200'}`}>
{habit.name}
</p>
<span className="text-[11px] text-slate-400">{habit.category}</span>
</div>
</div>
{/* Streak Badge */}
<div className="col-span-2 flex justify-center">
<div className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-orange-500/10 border border-orange-500/30 text-orange-400 text-xs font-extrabold shadow-sm">
<Flame className="w-3.5 h-3.5 fill-orange-400" />
<span>{habit.streak} days</span>
</div>
</div>
{/* 7-Day History Bubbles */}
<div className="col-span-4 grid grid-cols-7 gap-1 place-items-center">
{last7Days.map((d) => {
const isDone = habit.completedDates.includes(d.dateStr);
return (
<button
key={d.dateStr}
onClick={() => {
onToggleHabit(habit.id, d.dateStr);
playNotificationChime(isDone ? 'click' : 'complete');
}}
className={`w-7 h-7 rounded-lg flex items-center justify-center text-xs font-bold transition-all cursor-pointer ${
isDone
? 'bg-orange-500/90 text-slate-950 shadow-sm shadow-orange-500/40 hover:scale-110'
: d.isToday
? 'bg-slate-900 border-2 border-dashed border-orange-500/50 text-slate-400 hover:border-orange-500'
: 'bg-slate-900/60 border border-slate-800 text-slate-400 hover:border-slate-700'
}`}
title={`${d.dateStr}: ${isDone ? 'Completed' : 'Missed'}`}
>
{isDone ? '✓' : ''}
</button>
);
})}
</div>
{/* Actions */}
<div className="col-span-1 text-right">
<button
onClick={() => onDeleteHabit(habit.id)}
className="text-slate-400 hover:text-red-400 transition-colors p-1 rounded"
title="Delete Habit"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
);
})}
</div>
</div>
</div>
</div>
{/* Create Habit Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm animate-in fade-in duration-200">
<div className="relative w-full max-w-md rounded-2xl bg-[#0e1424] border border-orange-500/30 p-6 shadow-2xl space-y-4">
<div className="flex items-center justify-between pb-3 border-b border-slate-800">
<h3 className="text-base font-bold text-white flex items-center gap-2">
<Flame className="w-4 h-4 text-orange-400" />
Add Daily Routine
</h3>
<button
onClick={() => setIsModalOpen(false)}
className="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-800"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleCreate} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Habit Name *
</label>
<input
type="text"
required
placeholder="e.g. Read 20 Pages, Meditate, Hit 10k Steps"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-orange-500"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Category
</label>
<input
type="text"
placeholder="Health, Code, Mindset"
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 focus:outline-none focus:border-orange-500"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Accent Color
</label>
<div className="flex items-center gap-2 pt-1">
{['#06b6d4', '#10b981', '#f59e0b', '#a855f7', '#ec4899'].map((c) => (
<button
key={c}
type="button"
onClick={() => setColor(c)}
className={`w-6 h-6 rounded-full border-2 transition-transform ${
color === c ? 'scale-125 border-white' : 'border-transparent'
}`}
style={{ backgroundColor: c }}
/>
))}
</div>
</div>
</div>
<div className="pt-3 border-t border-slate-800 flex justify-end gap-3">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs font-semibold text-slate-300 transition-colors"
>
Cancel
</button>
<button
type="submit"
className="px-5 py-2 rounded-xl bg-gradient-to-r from-orange-500 to-amber-500 text-slate-950 text-xs font-bold shadow-md shadow-orange-500/20 hover:scale-105 active:scale-95 transition-all cursor-pointer"
>
Start Tracking
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};
+353
View File
@@ -0,0 +1,353 @@
'use client';
import React, { useState } from 'react';
import {
BookOpen,
Search,
Plus,
Trash2,
Tag,
Pin,
Smile,
X,
Sparkles,
Heart
} from 'lucide-react';
import { Note } from '@/lib/types';
import { getFormattedDate } from '@/lib/initialData';
import { playNotificationChime } from '@/lib/audio';
interface NotesViewProps {
notes: Note[];
onAddNote: (note: Omit<Note, 'id' | 'updatedAt'>) => void;
onUpdateNote: (id: string, updates: Partial<Note>) => void;
onDeleteNote: (id: string) => void;
showAddModalDefault?: boolean;
}
export const NotesView: React.FC<NotesViewProps> = ({
notes,
onAddNote,
onUpdateNote,
onDeleteNote,
showAddModalDefault = false,
}) => {
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [isModalOpen, setIsModalOpen] = useState(showAddModalDefault);
// Form state
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [tagsInput, setTagsInput] = useState('');
const [category, setCategory] = useState('Tech');
const [mood, setMood] = useState<1 | 2 | 3 | 4 | 5>(4);
const [pinned, setPinned] = useState(false);
const moodEmojis: Record<number, { emoji: string; label: string }> = {
1: { emoji: '😫', label: 'Stressed' },
2: { emoji: '😴', label: 'Tired' },
3: { emoji: '😐', label: 'Neutral' },
4: { emoji: '😊', label: 'Energized' },
5: { emoji: '🚀', label: 'Unstoppable' },
};
const handleCreate = (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim()) return;
const tags = tagsInput
.split(',')
.map((t) => t.trim())
.filter((t) => t.length > 0);
onAddNote({
title: title.trim(),
content: content.trim(),
tags,
category: category.trim() || 'General',
mood,
pinned,
});
setTitle('');
setContent('');
setTagsInput('');
setIsModalOpen(false);
playNotificationChime('click');
};
const categories = ['all', ...Array.from(new Set(notes.map((n) => n.category)))];
const filteredNotes = notes.filter((n) => {
const matchesSearch =
n.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
n.content.toLowerCase().includes(searchQuery.toLowerCase()) ||
n.tags.some((t) => t.toLowerCase().includes(searchQuery.toLowerCase()));
const matchesCat = selectedCategory === 'all' || n.category === selectedCategory;
return matchesSearch && matchesCat;
});
// Sort pinned first
const sortedNotes = [...filteredNotes].sort((a, b) => (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0));
return (
<div className="space-y-6">
{/* Top Bar: Search, Category Filter, New Note Button */}
<div className="flex flex-col sm:flex-row gap-4 items-center justify-between">
{/* Search */}
<div className="relative w-full sm:w-72">
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="Search notes or tags..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-4 py-2 rounded-xl bg-slate-900 border border-slate-800 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:border-purple-500 transition-colors"
/>
</div>
{/* Category Pills */}
<div className="flex items-center gap-2 overflow-x-auto w-full sm:w-auto pb-1 sm:pb-0">
{categories.map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-3 py-1.5 rounded-xl text-xs font-semibold capitalize transition-all shrink-0 ${
selectedCategory === cat
? 'bg-purple-500/20 text-purple-300 border border-purple-500/40'
: 'bg-slate-900 text-slate-400 border border-slate-800 hover:text-slate-200'
}`}
>
{cat}
</button>
))}
<button
onClick={() => setIsModalOpen(true)}
className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-gradient-to-r from-purple-500 to-indigo-600 text-white text-xs font-bold shadow-lg shadow-purple-500/20 hover:scale-105 active:scale-95 transition-all cursor-pointer shrink-0 ml-auto sm:ml-2"
>
<Plus className="w-4 h-4" />
<span>New Note</span>
</button>
</div>
</div>
{/* Notes Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{sortedNotes.map((note) => (
<div
key={note.id}
className={`glass-card p-6 flex flex-col justify-between group transition-all relative ${
note.pinned ? 'border-purple-500/40 shadow-purple-500/10' : ''
}`}
>
<div>
{/* Header: Category, Mood, Pin, Delete */}
<div className="flex items-start justify-between gap-2 mb-3">
<span className="text-[11px] font-bold uppercase tracking-wider px-2.5 py-0.5 rounded-md bg-purple-500/10 text-purple-300 border border-purple-500/30">
{note.category}
</span>
<div className="flex items-center gap-2">
{note.mood && (
<span
className="text-sm bg-slate-900 px-2 py-0.5 rounded-md border border-slate-800"
title={`Mood: ${moodEmojis[note.mood]?.label}`}
>
{moodEmojis[note.mood]?.emoji}
</span>
)}
<button
onClick={() => onUpdateNote(note.id, { pinned: !note.pinned })}
className={`p-1 rounded transition-colors ${
note.pinned ? 'text-purple-400' : 'text-slate-500 hover:text-slate-300'
}`}
title={note.pinned ? "Unpin note" : "Pin note"}
>
<Pin className="w-3.5 h-3.5" />
</button>
<button
onClick={() => onDeleteNote(note.id)}
className="p-1 text-slate-500 hover:text-red-400 rounded transition-colors"
title="Delete note"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
{/* Note Title */}
<h4 className="text-base font-bold text-slate-100 mb-2">{note.title}</h4>
{/* Note Content */}
<div className="text-xs text-slate-300 leading-relaxed font-mono whitespace-pre-wrap bg-slate-950/40 p-3 rounded-xl border border-slate-900 max-h-48 overflow-y-auto">
{note.content}
</div>
</div>
{/* Footer: Tags & Date */}
<div className="mt-4 pt-3 border-t border-slate-800/80 flex items-center justify-between text-[11px] text-slate-400">
<div className="flex flex-wrap items-center gap-1.5">
{note.tags.map((t) => (
<span key={t} className="inline-flex items-center gap-1 text-[10px] text-slate-400 bg-slate-900 px-1.5 py-0.5 rounded border border-slate-800">
<Tag className="w-2.5 h-2.5 text-purple-400" />
{t}
</span>
))}
</div>
<span>{note.updatedAt}</span>
</div>
</div>
))}
{sortedNotes.length === 0 && (
<div className="col-span-full py-16 text-center text-slate-500 space-y-2">
<BookOpen className="w-8 h-8 mx-auto text-slate-600" />
<p className="text-sm">No notes found matching your criteria.</p>
<button
onClick={() => setIsModalOpen(true)}
className="text-purple-400 hover:text-purple-300 text-xs font-semibold"
>
Write your first thought
</button>
</div>
)}
</div>
{/* Create Note Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm animate-in fade-in duration-200">
<div className="relative w-full max-w-lg rounded-2xl bg-[#0e1424] border border-purple-500/30 p-6 shadow-2xl space-y-4">
<div className="flex items-center justify-between pb-3 border-b border-slate-800">
<h3 className="text-base font-bold text-white flex items-center gap-2">
<BookOpen className="w-4 h-4 text-purple-400" />
Capture Note & Reflection
</h3>
<button
onClick={() => setIsModalOpen(false)}
className="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-800"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleCreate} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Title *
</label>
<input
type="text"
required
placeholder="e.g. Architectural Principles, Today's Reflection"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-purple-500"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Content (Markdown Supported) *
</label>
<textarea
rows={5}
required
placeholder="Write your thoughts, code snippets, learnings, or daily reflection..."
value={content}
onChange={(e) => setContent(e.target.value)}
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 placeholder-slate-500 focus:outline-none focus:border-purple-500 font-mono resize-none"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Category
</label>
<input
type="text"
placeholder="Tech, Mindset, Life"
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 focus:outline-none focus:border-purple-500"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Tags (comma separated)
</label>
<input
type="text"
placeholder="React, Vision, 2026"
value={tagsInput}
onChange={(e) => setTagsInput(e.target.value)}
className="w-full px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 focus:outline-none focus:border-purple-500"
/>
</div>
</div>
{/* Mood Selector */}
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Daily Mood Pulse
</label>
<div className="grid grid-cols-5 gap-2">
{([1, 2, 3, 4, 5] as const).map((m) => (
<button
key={m}
type="button"
onClick={() => setMood(m)}
className={`p-2 rounded-xl border flex flex-col items-center gap-1 transition-all ${
mood === m
? 'bg-purple-500/20 border-purple-500 shadow-md shadow-purple-500/20'
: 'bg-slate-900 border-slate-800 hover:border-slate-700'
}`}
>
<span className="text-xl">{moodEmojis[m].emoji}</span>
<span className="text-[10px] text-slate-400 font-medium">{moodEmojis[m].label}</span>
</button>
))}
</div>
</div>
<div className="flex items-center gap-2 pt-1">
<input
type="checkbox"
id="pinnedCheck"
checked={pinned}
onChange={(e) => setPinned(e.target.checked)}
className="rounded border-slate-700 text-purple-600 focus:ring-purple-500"
/>
<label htmlFor="pinnedCheck" className="text-xs text-slate-300 cursor-pointer">
Pin to top of list
</label>
</div>
<div className="pt-3 border-t border-slate-800 flex justify-end gap-3">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs font-semibold text-slate-300 transition-colors"
>
Cancel
</button>
<button
type="submit"
className="px-5 py-2 rounded-xl bg-gradient-to-r from-purple-500 to-indigo-600 text-white text-xs font-bold shadow-md shadow-purple-500/20 hover:scale-105 active:scale-95 transition-all cursor-pointer"
>
Save Note
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};
+303
View File
@@ -0,0 +1,303 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import {
Play,
Pause,
RotateCcw,
SkipForward,
Flame,
Brain,
Coffee,
CheckCircle2,
Sparkles
} from 'lucide-react';
import { Task } from '@/lib/types';
import { playNotificationChime } from '@/lib/audio';
interface PomodoroViewProps {
tasks: Task[];
onCompleteSession?: () => void;
}
type TimerMode = 'focus' | 'shortBreak' | 'longBreak';
const MODE_CONFIG: Record<TimerMode, { label: string; duration: number; icon: typeof Brain; color: string; ringColor: string }> = {
focus: {
label: 'Focus Session',
duration: 25 * 60,
icon: Brain,
color: 'text-amber-400 bg-amber-500/10 border-amber-500/30',
ringColor: '#f59e0b',
},
shortBreak: {
label: 'Short Rest',
duration: 5 * 60,
icon: Coffee,
color: 'text-cyan-400 bg-cyan-500/10 border-cyan-500/30',
ringColor: '#06b6d4',
},
longBreak: {
label: 'Long Recovery',
duration: 15 * 60,
icon: Coffee,
color: 'text-emerald-400 bg-emerald-500/10 border-emerald-500/30',
ringColor: '#10b981',
},
};
export const PomodoroView: React.FC<PomodoroViewProps> = ({ tasks }) => {
const [mode, setMode] = useState<TimerMode>('focus');
const [timeLeft, setTimeLeft] = useState<number>(MODE_CONFIG.focus.duration);
const [isActive, setIsActive] = useState<boolean>(false);
const [completedSessions, setCompletedSessions] = useState<number>(3);
const [selectedTaskId, setSelectedTaskId] = useState<string>('');
const timerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (isActive) {
timerRef.current = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
clearInterval(timerRef.current!);
setIsActive(false);
playNotificationChime('complete');
if (mode === 'focus') {
setCompletedSessions((c) => c + 1);
}
return 0;
}
return prev - 1;
});
}, 1000);
} else if (timerRef.current) {
clearInterval(timerRef.current);
}
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, [isActive, mode]);
const switchMode = (newMode: TimerMode) => {
setIsActive(false);
setMode(newMode);
setTimeLeft(MODE_CONFIG[newMode].duration);
playNotificationChime('click');
};
const handleToggleTimer = () => {
setIsActive(!isActive);
playNotificationChime('click');
};
const handleReset = () => {
setIsActive(false);
setTimeLeft(MODE_CONFIG[mode].duration);
playNotificationChime('click');
};
const handleSkip = () => {
setIsActive(false);
if (mode === 'focus') {
switchMode('shortBreak');
} else {
switchMode('focus');
}
};
const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;
const timeFormatted = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
const currentDuration = MODE_CONFIG[mode].duration;
const progressPercent = ((currentDuration - timeLeft) / currentDuration) * 100;
// SVG ring properties
const radius = 130;
const circumference = 2 * Math.PI * radius;
const strokeDashoffset = circumference - (progressPercent / 100) * circumference;
const currentTask = tasks.find((t) => t.id === selectedTaskId);
return (
<div className="max-w-4xl mx-auto space-y-8">
{/* Mode Switcher */}
<div className="flex items-center justify-center">
<div className="flex items-center p-1.5 rounded-2xl bg-slate-900 border border-slate-800 shadow-xl">
{(['focus', 'shortBreak', 'longBreak'] as const).map((m) => {
const isSelected = mode === m;
const Icon = MODE_CONFIG[m].icon;
return (
<button
key={m}
onClick={() => switchMode(m)}
className={`flex items-center gap-2 px-5 py-2.5 rounded-xl text-xs font-bold transition-all cursor-pointer ${
isSelected
? MODE_CONFIG[m].color
: 'text-slate-400 hover:text-slate-200'
}`}
>
<Icon className="w-4 h-4" />
<span>{MODE_CONFIG[m].label}</span>
</button>
);
})}
</div>
</div>
{/* Center Circular Countdown Card */}
<div className="glass-card p-8 md:p-12 flex flex-col items-center justify-center relative overflow-hidden border-slate-800/80 shadow-2xl">
{/* Ambient background glow */}
<div
className="absolute w-72 h-72 rounded-full blur-[100px] opacity-20 pointer-events-none transition-all duration-700"
style={{ backgroundColor: MODE_CONFIG[mode].ringColor }}
/>
{/* Circular SVG Timer */}
<div className="relative flex items-center justify-center">
<svg className="w-72 h-72 md:w-80 md:h-80 -rotate-90 transform">
{/* Background Track */}
<circle
cx="50%"
cy="50%"
r={radius}
className="text-slate-800/60 stroke-current"
strokeWidth="10"
fill="transparent"
/>
{/* Animated Progress Ring */}
<circle
cx="50%"
cy="50%"
r={radius}
stroke={MODE_CONFIG[mode].ringColor}
strokeWidth="12"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
fill="transparent"
className="transition-all duration-500"
style={{
filter: `drop-shadow(0 0 10px ${MODE_CONFIG[mode].ringColor}80)`,
}}
/>
</svg>
{/* Time Display Inside Circle */}
<div className="absolute flex flex-col items-center justify-center text-center">
<span className="font-mono text-5xl md:text-6xl font-extrabold text-white tracking-tighter">
{timeFormatted}
</span>
<span className="text-xs uppercase font-bold tracking-widest text-slate-400 mt-2">
{isActive ? 'Session In Progress' : 'Paused'}
</span>
{currentTask && (
<span className="mt-3 px-3 py-1 rounded-full bg-slate-900 border border-slate-800 text-[11px] text-cyan-400 font-medium max-w-[200px] truncate">
🎯 {currentTask.title}
</span>
)}
</div>
</div>
{/* Control Buttons */}
<div className="mt-8 flex items-center gap-4 z-10">
<button
onClick={handleReset}
className="p-3.5 rounded-2xl bg-slate-900/80 hover:bg-slate-800 text-slate-400 hover:text-white border border-slate-800 transition-all cursor-pointer shadow-md"
title="Reset timer"
>
<RotateCcw className="w-5 h-5" />
</button>
<button
onClick={handleToggleTimer}
className={`flex items-center gap-2.5 px-8 py-4 rounded-2xl text-slate-950 font-extrabold text-base shadow-xl transition-all hover:scale-105 active:scale-95 cursor-pointer`}
style={{
backgroundColor: MODE_CONFIG[mode].ringColor,
boxShadow: `0 10px 25px -5px ${MODE_CONFIG[mode].ringColor}60`,
}}
>
{isActive ? (
<>
<Pause className="w-5 h-5 fill-slate-950" />
<span>Pause</span>
</>
) : (
<>
<Play className="w-5 h-5 fill-slate-950" />
<span>Start Focus</span>
</>
)}
</button>
<button
onClick={handleSkip}
className="p-3.5 rounded-2xl bg-slate-900/80 hover:bg-slate-800 text-slate-400 hover:text-white border border-slate-800 transition-all cursor-pointer shadow-md"
title="Skip to next session"
>
<SkipForward className="w-5 h-5" />
</button>
</div>
</div>
{/* Focus Session Target & Task Pairing */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Daily Focus Sessions Card */}
<div className="glass-card p-5 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-3 rounded-xl bg-amber-500/10 text-amber-400">
<Flame className="w-6 h-6 fill-amber-400/20" />
</div>
<div>
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">
Sessions Completed
</span>
<div className="text-2xl font-extrabold text-white mt-0.5">
{completedSessions} / 6 Sessions
</div>
</div>
</div>
<div className="text-right">
<span className="text-xs font-bold text-amber-400">{completedSessions * 25} Mins</span>
<p className="text-[11px] text-slate-400">Deep Work Today</p>
</div>
</div>
{/* Task Attachment Picker */}
<div className="glass-card p-5 flex flex-col justify-between">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider flex items-center gap-1.5">
<Sparkles className="w-3.5 h-3.5 text-cyan-400" />
Pair with Task
</span>
{selectedTaskId && (
<button
onClick={() => setSelectedTaskId('')}
className="text-[11px] text-slate-500 hover:text-slate-300"
>
Clear
</button>
)}
</div>
<select
value={selectedTaskId}
onChange={(e) => setSelectedTaskId(e.target.value)}
className="w-full px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 focus:outline-none focus:border-amber-500"
>
<option value="">No task linked (Freestyle Focus)</option>
{tasks
.filter((t) => t.status !== 'completed')
.map((t) => (
<option key={t.id} value={t.id}>
[{t.priority.toUpperCase()}] {t.title}
</option>
))}
</select>
</div>
</div>
</div>
);
};
+432
View File
@@ -0,0 +1,432 @@
'use client';
import React, { useState } from 'react';
import {
Plus,
Search,
Trash2,
Calendar,
Tag,
Columns,
List,
X,
AlertTriangle
} from 'lucide-react';
import { Task, Priority, TaskStatus } from '@/lib/types';
import { getFormattedDate } from '@/lib/initialData';
import { playNotificationChime } from '@/lib/audio';
interface TasksViewProps {
tasks: Task[];
onAddTask: (task: Omit<Task, 'id' | 'createdAt'>) => void;
onUpdateTask: (id: string, updates: Partial<Task>) => void;
onDeleteTask: (id: string) => void;
showAddModalDefault?: boolean;
}
export const TasksView: React.FC<TasksViewProps> = ({
tasks,
onAddTask,
onUpdateTask,
onDeleteTask,
showAddModalDefault = false,
}) => {
const [viewMode, setViewMode] = useState<'kanban' | 'list'>('kanban');
const [searchQuery, setSearchQuery] = useState('');
const [selectedPriority, setSelectedPriority] = useState<string>('all');
const [isModalOpen, setIsModalOpen] = useState(showAddModalDefault);
// New task form state
const [newTitle, setNewTitle] = useState('');
const [newDesc, setNewDesc] = useState('');
const [newPriority, setNewPriority] = useState<Priority>('medium');
const [newCategory, setNewCategory] = useState('General');
const [newDueDate, setNewDueDate] = useState(getFormattedDate(1));
const handleCreateTask = (e: React.FormEvent) => {
e.preventDefault();
if (!newTitle.trim()) return;
onAddTask({
title: newTitle.trim(),
description: newDesc.trim() || undefined,
status: 'todo',
priority: newPriority,
category: newCategory.trim() || 'General',
dueDate: newDueDate || undefined,
});
setNewTitle('');
setNewDesc('');
setIsModalOpen(false);
playNotificationChime('click');
};
const filteredTasks = tasks.filter((task) => {
const matchesSearch =
task.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
(task.description && task.description.toLowerCase().includes(searchQuery.toLowerCase()));
const matchesPriority = selectedPriority === 'all' || task.priority === selectedPriority;
return matchesSearch && matchesPriority;
});
const columns: { id: TaskStatus; label: string; color: string; count: number }[] = [
{
id: 'todo',
label: 'To Do',
color: 'border-cyan-500/40 text-cyan-400 bg-cyan-500/10',
count: filteredTasks.filter(t => t.status === 'todo').length
},
{
id: 'in-progress',
label: 'In Progress',
color: 'border-amber-500/40 text-amber-400 bg-amber-500/10',
count: filteredTasks.filter(t => t.status === 'in-progress').length
},
{
id: 'completed',
label: 'Completed',
color: 'border-emerald-500/40 text-emerald-400 bg-emerald-500/10',
count: filteredTasks.filter(t => t.status === 'completed').length
},
];
const getPriorityBadge = (p: Priority) => {
switch (p) {
case 'urgent':
return 'bg-red-500/10 text-red-400 border-red-500/30';
case 'high':
return 'bg-amber-500/10 text-amber-400 border-amber-500/30';
case 'medium':
return 'bg-blue-500/10 text-blue-400 border-blue-500/30';
case 'low':
return 'bg-slate-500/10 text-slate-400 border-slate-500/30';
}
};
return (
<div className="space-y-6">
{/* Top Bar: Controls, Search, New Task Button */}
<div className="flex flex-col sm:flex-row gap-4 items-center justify-between">
{/* Search & Priority Filter */}
<div className="flex flex-wrap items-center gap-3 w-full sm:w-auto">
<div className="relative flex-1 sm:w-64">
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="Search tasks..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-4 py-2 rounded-xl bg-slate-900 border border-slate-800 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:border-cyan-500 transition-colors"
/>
</div>
<select
value={selectedPriority}
onChange={(e) => setSelectedPriority(e.target.value)}
className="px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-sm text-slate-300 focus:outline-none focus:border-cyan-500"
>
<option value="all">All Priorities</option>
<option value="urgent">Urgent</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
</div>
{/* View Toggle & Add Button */}
<div className="flex items-center gap-3 w-full sm:w-auto justify-end">
<div className="flex items-center p-1 rounded-xl bg-slate-900 border border-slate-800">
<button
onClick={() => setViewMode('kanban')}
className={`p-1.5 rounded-lg transition-colors ${
viewMode === 'kanban' ? 'bg-slate-800 text-cyan-400' : 'text-slate-400 hover:text-slate-200'
}`}
title="Kanban View"
>
<Columns className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}
className={`p-1.5 rounded-lg transition-colors ${
viewMode === 'list' ? 'bg-slate-800 text-cyan-400' : 'text-slate-400 hover:text-slate-200'
}`}
title="List View"
>
<List className="w-4 h-4" />
</button>
</div>
<button
onClick={() => setIsModalOpen(true)}
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-gradient-to-r from-cyan-500 to-blue-600 text-white text-sm font-semibold shadow-lg shadow-cyan-500/20 hover:scale-105 active:scale-95 transition-all cursor-pointer"
>
<Plus className="w-4 h-4" />
<span>Create Task</span>
</button>
</div>
</div>
{/* Kanban Board View */}
{viewMode === 'kanban' ? (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{columns.map((col) => {
const colTasks = filteredTasks.filter((t) => t.status === col.id);
return (
<div
key={col.id}
className="flex flex-col rounded-2xl bg-[#0e1424]/70 border border-slate-800/80 p-4 min-h-[480px]"
>
{/* Column Header */}
<div className="flex items-center justify-between pb-3 border-b border-slate-800/80 mb-4">
<div className="flex items-center gap-2">
<span className={`px-2.5 py-0.5 rounded-full text-xs font-bold border ${col.color}`}>
{col.label}
</span>
</div>
<span className="text-xs font-semibold text-slate-500 bg-slate-900 px-2 py-0.5 rounded-md border border-slate-800">
{colTasks.length}
</span>
</div>
{/* Task Cards */}
<div className="space-y-3 flex-1 overflow-y-auto">
{colTasks.map((task) => (
<div
key={task.id}
className="group p-4 rounded-xl bg-slate-900/90 border border-slate-800/90 hover:border-slate-700 transition-all shadow-md relative"
>
<div className="flex items-start justify-between gap-2">
<span className={`text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-md border ${getPriorityBadge(task.priority)}`}>
{task.priority}
</span>
{/* Status Change Quick Dropdown */}
<div className="flex items-center gap-1 opacity-80 group-hover:opacity-100">
<select
value={task.status}
onChange={(e) => {
onUpdateTask(task.id, { status: e.target.value as TaskStatus });
if (e.target.value === 'completed') playNotificationChime('complete');
}}
className="text-[11px] bg-slate-950 border border-slate-800 text-slate-400 rounded px-1.5 py-0.5 focus:outline-none focus:border-cyan-500"
>
<option value="todo">To Do</option>
<option value="in-progress">In Progress</option>
<option value="completed">Done</option>
</select>
<button
onClick={() => onDeleteTask(task.id)}
className="p-1 text-slate-500 hover:text-red-400 rounded transition-colors"
title="Delete Task"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
<h4 className="mt-2.5 text-sm font-semibold text-slate-100 leading-snug">
{task.title}
</h4>
{task.description && (
<p className="mt-1 text-xs text-slate-400 line-clamp-2 leading-relaxed">
{task.description}
</p>
)}
<div className="mt-3 pt-3 border-t border-slate-800/60 flex items-center justify-between text-[11px] text-slate-400">
<div className="flex items-center gap-1 text-slate-400">
<Tag className="w-3 h-3 text-cyan-400" />
<span>{task.category}</span>
</div>
{task.dueDate && (
<div className="flex items-center gap-1 text-slate-400">
<Calendar className="w-3 h-3 text-amber-400" />
<span>{task.dueDate}</span>
</div>
)}
</div>
</div>
))}
{colTasks.length === 0 && (
<div className="h-32 flex flex-col items-center justify-center text-slate-600 text-xs border border-dashed border-slate-800/80 rounded-xl">
No tasks in {col.label.toLowerCase()}
</div>
)}
</div>
</div>
);
})}
</div>
) : (
/* List View */
<div className="rounded-2xl bg-[#0e1424]/70 border border-slate-800/80 overflow-hidden">
<div className="divide-y divide-slate-800/70">
{filteredTasks.map((task) => {
const isDone = task.status === 'completed';
return (
<div
key={task.id}
className="p-4 flex items-center justify-between gap-4 hover:bg-slate-900/50 transition-colors"
>
<div className="flex items-center gap-3 min-w-0">
<button
onClick={() => {
const newStatus: TaskStatus = isDone ? 'todo' : 'completed';
onUpdateTask(task.id, { status: newStatus });
if (newStatus === 'completed') playNotificationChime('complete');
}}
className={`w-5 h-5 rounded border flex items-center justify-center transition-colors ${
isDone
? 'bg-emerald-500 border-emerald-400 text-black'
: 'border-slate-700 hover:border-cyan-400'
}`}
>
{isDone && <span className="text-xs font-black">✓</span>}
</button>
<div className="min-w-0">
<p className={`text-sm font-semibold truncate ${isDone ? 'line-through text-slate-500' : 'text-slate-200'}`}>
{task.title}
</p>
{task.description && (
<p className="text-xs text-slate-400 truncate">{task.description}</p>
)}
</div>
</div>
<div className="flex items-center gap-3 shrink-0">
<span className={`text-[10px] uppercase font-bold px-2 py-0.5 rounded-full border ${getPriorityBadge(task.priority)}`}>
{task.priority}
</span>
<span className="text-xs text-slate-400 bg-slate-900 px-2 py-0.5 rounded border border-slate-800">
{task.status}
</span>
<button
onClick={() => onDeleteTask(task.id)}
className="text-slate-500 hover:text-red-400 transition-colors p-1"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
);
})}
</div>
</div>
)}
{/* Create Task Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm animate-in fade-in duration-200">
<div className="relative w-full max-w-lg rounded-2xl bg-[#0e1424] border border-cyan-500/30 p-6 shadow-2xl space-y-4">
<div className="flex items-center justify-between pb-3 border-b border-slate-800">
<h3 className="text-base font-bold text-white flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-cyan-400" />
Create New Task
</h3>
<button
onClick={() => setIsModalOpen(false)}
className="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-800"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleCreateTask} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Task Title *
</label>
<input
type="text"
required
placeholder="e.g. Master React Hooks"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-cyan-500"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Description
</label>
<textarea
rows={3}
placeholder="Briefly describe what needs to be done..."
value={newDesc}
onChange={(e) => setNewDesc(e.target.value)}
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-cyan-500 resize-none"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Priority
</label>
<select
value={newPriority}
onChange={(e) => setNewPriority(e.target.value as Priority)}
className="w-full px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 focus:outline-none focus:border-cyan-500"
>
<option value="urgent">Urgent</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Category
</label>
<input
type="text"
placeholder="General"
value={newCategory}
onChange={(e) => setNewCategory(e.target.value)}
className="w-full px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 focus:outline-none focus:border-cyan-500"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Due Date
</label>
<input
type="date"
value={newDueDate}
onChange={(e) => setNewDueDate(e.target.value)}
className="w-full px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 focus:outline-none focus:border-cyan-500"
/>
</div>
</div>
<div className="pt-3 border-t border-slate-800 flex justify-end gap-3">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs font-semibold text-slate-300 transition-colors"
>
Cancel
</button>
<button
type="submit"
className="px-5 py-2 rounded-xl bg-gradient-to-r from-cyan-500 to-blue-600 text-white text-xs font-bold shadow-md shadow-cyan-500/20 hover:scale-105 active:scale-95 transition-all cursor-pointer"
>
Save Task
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};
+11
View File
@@ -0,0 +1,11 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
+24
View File
@@ -0,0 +1,24 @@
"use client";
import * as React from "react";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { cn } from "@/lib/utils";
export function ThemeToggle({ className }: { className?: string }) {
const { setTheme, theme } = useTheme();
return (
<button
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
className={cn(
"relative inline-flex h-10 w-10 items-center justify-center rounded-full glass hover:bg-white/40 dark:hover:bg-black/40 transition-colors",
className
)}
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</button>
);
}
+69
View File
@@ -0,0 +1,69 @@
// Web Audio API chime generator (no external audio assets required)
export const playNotificationChime = (type: 'complete' | 'click' | 'streak' = 'complete') => {
if (typeof window === 'undefined') return;
try {
const AudioContextClass = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
if (!AudioContextClass) return;
const ctx = new AudioContextClass();
if (type === 'complete') {
// Pleasant two-tone chime
const now = ctx.currentTime;
const osc1 = ctx.createOscillator();
const gain1 = ctx.createGain();
osc1.type = 'sine';
osc1.frequency.setValueAtTime(587.33, now); // D5
gain1.gain.setValueAtTime(0.3, now);
gain1.gain.exponentialRampToValueAtTime(0.001, now + 0.8);
osc1.connect(gain1);
gain1.connect(ctx.destination);
osc1.start(now);
osc1.stop(now + 0.8);
const osc2 = ctx.createOscillator();
const gain2 = ctx.createGain();
osc2.type = 'sine';
osc2.frequency.setValueAtTime(880, now + 0.15); // A5
gain2.gain.setValueAtTime(0.3, now + 0.15);
gain2.gain.exponentialRampToValueAtTime(0.001, now + 1.2);
osc2.connect(gain2);
gain2.connect(ctx.destination);
osc2.start(now + 0.15);
osc2.stop(now + 1.2);
} else if (type === 'streak') {
// Ascending triumphant arpeggio
const notes = [440, 554.37, 659.25, 880];
notes.forEach((freq, idx) => {
const now = ctx.currentTime + idx * 0.08;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(freq, now);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.3);
});
} else {
// Subtle click
const now = ctx.currentTime;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(800, now);
gain.gain.setValueAtTime(0.05, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.05);
}
} catch (err) {
console.debug('Web Audio not allowed or failed:', err);
}
};
+398
View File
@@ -0,0 +1,398 @@
import { Task, Habit, ExpenseItem, CalendarEvent, Note, WorkoutSession, DailyNutrition, MuscleRecovery } from './types';
// Helper to get formatted date string YYYY-MM-DD
export const getFormattedDate = (offsetDays: number = 0): string => {
const d = new Date();
d.setDate(d.getDate() + offsetDays);
return d.toISOString().split('T')[0];
};
export const INITIAL_TASKS: Task[] = [
{
id: 't-1',
title: 'Master React & Next.js Core Concepts',
description: 'Understand component lifecycle, hooks (useState, useEffect), and props data flow.',
status: 'in-progress',
priority: 'urgent',
category: 'Learning',
dueDate: getFormattedDate(1),
createdAt: getFormattedDate(-2),
},
{
id: 't-2',
title: 'Launch Parth\'s Life Manager Dashboard',
description: 'Set up dark mode, modular dashboard, and local storage persistence.',
status: 'in-progress',
priority: 'high',
category: 'Projects',
dueDate: getFormattedDate(0),
createdAt: getFormattedDate(-1),
},
{
id: 't-3',
title: 'Plan Weekly Fitness Schedule',
description: 'Schedule 3 gym workout days and 2 cardio/running sessions.',
status: 'todo',
priority: 'medium',
category: 'Health',
dueDate: getFormattedDate(3),
createdAt: getFormattedDate(0),
},
{
id: 't-4',
title: 'Review Monthly Budget & Subscriptions',
description: 'Check recurring payments and allocate monthly investments.',
status: 'todo',
priority: 'low',
category: 'Finance',
dueDate: getFormattedDate(5),
createdAt: getFormattedDate(0),
},
{
id: 't-5',
title: 'Set up Project Repository & Environment',
description: 'Install dependencies, configure Tailwind, and test local development server.',
status: 'completed',
priority: 'high',
category: 'Projects',
dueDate: getFormattedDate(-1),
createdAt: getFormattedDate(-3),
},
];
export const INITIAL_HABITS: Habit[] = [
{
id: 'h-1',
name: 'Code for 45 Mins',
category: 'Skill Building',
frequency: 'daily',
streak: 6,
bestStreak: 14,
completedDates: [
getFormattedDate(-5),
getFormattedDate(-4),
getFormattedDate(-3),
getFormattedDate(-2),
getFormattedDate(-1),
getFormattedDate(0),
],
color: '#06b6d4', // cyan
},
{
id: 'h-2',
name: 'Drink 2.5L Water',
category: 'Health',
frequency: 'daily',
streak: 9,
bestStreak: 12,
completedDates: [
getFormattedDate(-3),
getFormattedDate(-2),
getFormattedDate(-1),
getFormattedDate(0),
],
color: '#10b981', // emerald
},
{
id: 'h-3',
name: 'Read 20 Pages',
category: 'Mindset',
frequency: 'daily',
streak: 4,
bestStreak: 8,
completedDates: [
getFormattedDate(-3),
getFormattedDate(-2),
getFormattedDate(-1),
],
color: '#a855f7', // purple
},
{
id: 'h-4',
name: 'Morning Workout & Stretch',
category: 'Fitness',
frequency: 'daily',
streak: 2,
bestStreak: 10,
completedDates: [
getFormattedDate(-1),
getFormattedDate(0),
],
color: '#f59e0b', // amber
},
];
export const INITIAL_EXPENSES: ExpenseItem[] = [
{
id: 'e-1',
title: 'Software Dev Stipend / Salary',
amount: 3200,
type: 'income',
category: 'Income',
date: getFormattedDate(-5),
note: 'Monthly salary allocation',
},
{
id: 'e-2',
title: 'Tech Gadgets & Desk Setup',
amount: 145,
type: 'expense',
category: 'Equipment',
date: getFormattedDate(-3),
note: 'Mechanical keyboard and cable organizer',
},
{
id: 'e-3',
title: 'Cloud & AI Subscriptions',
amount: 35,
type: 'expense',
category: 'Subscriptions',
date: getFormattedDate(-2),
note: 'API and hosting subscriptions',
},
{
id: 'e-4',
title: 'Healthy Groceries & Nutrition',
amount: 85,
type: 'expense',
category: 'Food',
date: getFormattedDate(-1),
note: 'Weekly essentials and fresh produce',
},
{
id: 'e-5',
title: 'Gym Membership',
amount: 45,
type: 'expense',
category: 'Health',
date: getFormattedDate(-8),
note: 'Monthly access pass',
},
];
export const INITIAL_EVENTS: CalendarEvent[] = [
{
id: 'ev-1',
title: 'React Architecture Planning',
date: getFormattedDate(0),
startTime: '10:00',
endTime: '11:30',
category: 'work',
description: 'Define component tree and state management strategy.',
},
{
id: 'ev-2',
title: 'Focus Coding Session (Deep Work)',
date: getFormattedDate(0),
startTime: '14:00',
endTime: '16:00',
category: 'study',
description: 'Implement Kanban drag-drop and Pomodoro integration.',
},
{
id: 'ev-3',
title: 'Evening Gym & Recovery',
date: getFormattedDate(1),
startTime: '18:30',
endTime: '19:45',
category: 'health',
description: 'Full body resistance workout and mobility work.',
},
{
id: 'ev-4',
title: 'Weekend Project Demo & Review',
date: getFormattedDate(4),
startTime: '15:00',
endTime: '16:30',
category: 'personal',
description: 'Showcase Parth\'s Life Manager to peers.',
},
];
export const INITIAL_NOTES: Note[] = [
{
id: 'n-1',
title: 'React Mindset & Mental Models',
content: `### Why React Clicked for Me
1. **Component-driven**: Everything is isolated and reusable.
2. **One-way data flow**: Props go down, events bubble up.
3. **State drives the UI**: Change state, and the UI responds automatically!
Key quote: *"Programs must be written for people to read, and only incidentally for machines to execute."*`,
tags: ['React', 'Tech', 'Learning'],
category: 'Tech',
mood: 5,
updatedAt: getFormattedDate(0),
pinned: true,
},
{
id: 'n-2',
title: 'Goals for Parth\'s Life Manager',
content: `Build an app that feels snappy, looks like a futuristic sci-fi terminal, and actually helps me stay consistent with coding, fitness, and finance every single day.`,
tags: ['Goals', 'Life', 'Vision'],
category: 'Life',
mood: 4,
updatedAt: getFormattedDate(-1),
pinned: true,
},
];
export const INITIAL_NUTRITION: DailyNutrition = {
date: getFormattedDate(0),
calories: 2380,
calorieTarget: 2500,
protein: 168, // grams (~2.1g per kg bodyweight)
proteinTarget: 165,
carbs: 270,
carbsTarget: 290,
fats: 62,
fatsTarget: 65,
waterLiters: 2.75,
creatineTaken: true,
bodyweightKg: 78.5,
};
export const INITIAL_WORKOUTS: WorkoutSession[] = [
{
id: 'w-1',
name: 'Push Hypertrophy (Chest & Delts)',
date: getFormattedDate(0),
muscleGroups: ['Chest', 'Shoulders', 'Triceps'],
volumeLoad: 7840, // kg
exercises: [
{
name: 'Incline Dumbbell Press',
muscleGroup: 'Upper Chest',
sets: [
{ setNumber: 1, weight: 32, reps: 10, rpe: 8 },
{ setNumber: 2, weight: 32, reps: 9, rpe: 8.5 },
{ setNumber: 3, weight: 34, reps: 8, rpe: 9 },
],
},
{
name: 'Barbell Flat Bench Press',
muscleGroup: 'Mid Chest',
sets: [
{ setNumber: 1, weight: 80, reps: 8, rpe: 8 },
{ setNumber: 2, weight: 85, reps: 6, rpe: 8.5 },
{ setNumber: 3, weight: 85, reps: 6, rpe: 9 },
],
},
{
name: 'Cable Lateral Raises (Lengthened Bias)',
muscleGroup: 'Side Delts',
sets: [
{ setNumber: 1, weight: 12, reps: 15, rpe: 8.5 },
{ setNumber: 2, weight: 12, reps: 14, rpe: 9 },
{ setNumber: 3, weight: 14, reps: 11, rpe: 9.5 },
],
},
],
notes: 'Prioritized lengthened position on incline press. Controlled 3-second eccentrics.',
},
{
id: 'w-2',
name: 'Pull & Posterior Chain (Lats & Hamstrings)',
date: getFormattedDate(-2),
muscleGroups: ['Back', 'Biceps', 'Hamstrings'],
volumeLoad: 8960,
exercises: [
{
name: 'Romanian Deadlift (RDL)',
muscleGroup: 'Hamstrings',
sets: [
{ setNumber: 1, weight: 110, reps: 8, rpe: 8 },
{ setNumber: 2, weight: 115, reps: 8, rpe: 8.5 },
{ setNumber: 3, weight: 120, reps: 6, rpe: 9 },
],
},
{
name: 'Neutral Grip Lat Pulldown',
muscleGroup: 'Lats',
sets: [
{ setNumber: 1, weight: 70, reps: 10, rpe: 8 },
{ setNumber: 2, weight: 75, reps: 9, rpe: 8.5 },
{ setNumber: 3, weight: 75, reps: 8, rpe: 9 },
],
},
{
name: 'Incline Dumbbell Curl',
muscleGroup: 'Biceps',
sets: [
{ setNumber: 1, weight: 14, reps: 12, rpe: 8.5 },
{ setNumber: 2, weight: 14, reps: 10, rpe: 9 },
],
},
],
notes: 'Deep stretch at bottom of RDLs. Strong lat activation without excessive momentum.',
},
];
export const INITIAL_MUSCLE_RECOVERY: MuscleRecovery[] = [
{
muscle: 'Chest',
lastTrainedDaysAgo: 0,
recoveryHours: 48,
percentRecovered: 35,
status: 'fatigued',
},
{
muscle: 'Triceps & Delts',
lastTrainedDaysAgo: 0,
recoveryHours: 48,
percentRecovered: 40,
status: 'fatigued',
},
{
muscle: 'Back & Lats',
lastTrainedDaysAgo: 2,
recoveryHours: 48,
percentRecovered: 90,
status: 'ready',
},
{
muscle: 'Hamstrings & Glutes',
lastTrainedDaysAgo: 2,
recoveryHours: 72,
percentRecovered: 85,
status: 'recovering',
},
{
muscle: 'Quadriceps',
lastTrainedDaysAgo: 4,
recoveryHours: 72,
percentRecovered: 100,
status: 'ready',
},
{
muscle: 'Core / Abs',
lastTrainedDaysAgo: 3,
recoveryHours: 24,
percentRecovered: 100,
status: 'ready',
},
];
export const SCIENCE_TIPS = [
{
title: 'Mechanical Tension is King',
concept: 'Hypertrophy Driver #1',
description: 'Mechanical tension created by heavy loads or proximity to failure (RPE 7-9) is the primary driver of muscle hypertrophy—not metabolic "burn" or soreness.',
},
{
title: 'Stimulus-to-Fatigue Ratio (SFR)',
concept: 'Exercise Selection Science',
description: 'Prioritize movements that provide maximal tension to target tissues with minimal systemic axial fatigue and joint irritation (e.g., Incline DB Press vs Flat Barbell).',
},
{
title: 'Optimal Protein Dose per Meal',
concept: 'Leucine Threshold (MPS)',
description: 'Aim for ~0.4g of protein per kg of bodyweight per meal (approx 30-45g) containing ≥2.7g Leucine to trigger the Muscle Protein Synthesis (MPS) peak.',
},
{
title: 'When to Deload',
concept: 'Fatigue Management',
description: 'If progressive overload stalls for 2 consecutive weeks across main lifts while joint aching rises, take a 1-week deload: cut volume sets by 50% while maintaining load intensity.',
},
];
+109
View File
@@ -0,0 +1,109 @@
export const MOCK_USERS = [
{ id: "u1", username: "alex_travels", avatar: "https://i.pravatar.cc/150?u=a042581f4e29026704d", hasStory: true },
{ id: "u2", username: "sarah_styles", avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d", hasStory: true },
{ id: "u3", username: "arch_daily", avatar: "https://i.pravatar.cc/150?u=a042581f4e290267041", hasStory: false },
{ id: "u4", username: "foodie.crush", avatar: "https://i.pravatar.cc/150?u=a04258a2462d826712d", hasStory: true },
{ id: "u5", username: "neo_tokyo", avatar: "https://i.pravatar.cc/150?u=a042581f4e29026703d", hasStory: true },
{ id: "u6", username: "minimal_ui", avatar: "https://i.pravatar.cc/150?u=a042581f4e29026705d", hasStory: false },
{ id: "u7", username: "jordan_fitness", avatar: "https://i.pravatar.cc/150?u=a042581f4e29026706d", hasStory: true },
{ id: "u8", username: "coffee_vibes", avatar: "https://i.pravatar.cc/150?u=a042581f4e29026707d", hasStory: true },
];
export const MOCK_POSTS = [
{
id: "p1",
author: MOCK_USERS[0],
mediaUrl: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?q=80&w=1500&auto=format&fit=crop",
aspectRatio: "aspect-[4/5]",
isVideo: false,
likes: 1245,
comments: 45,
caption: "Lost in the majestic mountains of Switzerland 🏔️✨ #travel #nature",
tags: ["travel", "nature"],
createdAt: "2h ago",
},
{
id: "p2",
author: MOCK_USERS[1],
mediaUrl: "https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?q=80&w=1500&auto=format&fit=crop",
aspectRatio: "aspect-[3/4]",
isVideo: false,
likes: 8902,
comments: 112,
caption: "Autumn street style essentials 🍂🧥 #fashion #streetwear",
tags: ["fashion", "streetwear"],
createdAt: "5h ago",
},
{
id: "p3",
author: MOCK_USERS[2],
mediaUrl: "https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?q=80&w=1500&auto=format&fit=crop",
aspectRatio: "aspect-square",
isVideo: false,
likes: 432,
comments: 12,
caption: "Modern minimalist interior goals. Notice the lighting integration.",
tags: ["architecture", "interior"],
createdAt: "1d ago",
},
{
id: "p4",
author: MOCK_USERS[3],
mediaUrl: "https://images.unsplash.com/photo-1565299624946-b28f40a0ae38?q=80&w=1500&auto=format&fit=crop",
aspectRatio: "aspect-[9/16]", // TikTok / Reels size
isVideo: false, // In a real app we would play a video here
likes: 21000,
comments: 489,
caption: "Making the perfect Neapolitan pizza from scratch 🍕👨‍🍳 #pizza #cooking",
tags: ["food"],
createdAt: "1d ago",
},
{
id: "p5",
author: MOCK_USERS[4],
mediaUrl: "https://images.unsplash.com/photo-1514933651103-005eec06c04b?q=80&w=1500&auto=format&fit=crop",
aspectRatio: "aspect-[4/5]",
isVideo: false,
likes: 980,
comments: 24,
caption: "Cyberpunk nights in Shinjuku 🗼☔",
tags: ["travel", "nightlife", "photography"],
createdAt: "2d ago",
},
{
id: "p6",
author: MOCK_USERS[5],
mediaUrl: "https://images.unsplash.com/photo-1498050108023-c5249f4df085?q=80&w=1500&auto=format&fit=crop",
aspectRatio: "aspect-video",
isVideo: false,
likes: 124,
comments: 3,
caption: "Clean desk setup. Less is more.",
tags: ["tech", "minimal"],
createdAt: "2d ago",
},
{
id: "p7",
author: MOCK_USERS[6],
mediaUrl: "https://images.unsplash.com/photo-1534438327276-14e5300c3a48?q=80&w=1500&auto=format&fit=crop",
aspectRatio: "aspect-[9/16]",
isVideo: false,
likes: 15400,
comments: 312,
caption: "Morning run by the river. Grinding never stops! 💪🏃‍♂️",
tags: ["fitness", "lifestyle"],
createdAt: "3d ago",
},
{
id: "p8",
author: MOCK_USERS[7],
mediaUrl: "https://images.unsplash.com/photo-1497935586351-b67a49e012bf?q=80&w=1500&auto=format&fit=crop",
aspectRatio: "aspect-[1/1]",
isVideo: false,
likes: 673,
comments: 41,
caption: "Perfect pour over to start the day ☕✨",
tags: ["coffee", "morning"],
createdAt: "4d ago",
}
];
+113
View File
@@ -0,0 +1,113 @@
export type Priority = 'low' | 'medium' | 'high' | 'urgent';
export type TaskStatus = 'todo' | 'in-progress' | 'completed';
export interface Task {
id: string;
title: string;
description?: string;
status: TaskStatus;
priority: Priority;
category: string;
dueDate?: string;
createdAt: string;
}
export interface Habit {
id: string;
name: string;
category: string;
frequency: 'daily' | 'weekly';
streak: number;
bestStreak: number;
completedDates: string[]; // ISO date string YYYY-MM-DD
color: string;
icon?: string;
}
export interface ExpenseItem {
id: string;
title: string;
amount: number;
type: 'income' | 'expense';
category: string;
date: string; // YYYY-MM-DD
note?: string;
}
export interface CalendarEvent {
id: string;
title: string;
date: string; // YYYY-MM-DD
startTime?: string;
endTime?: string;
category: 'work' | 'personal' | 'study' | 'health';
description?: string;
}
export interface Note {
id: string;
title: string;
content: string;
tags: string[];
category: string;
mood?: 1 | 2 | 3 | 4 | 5; // 1: Stressed, 2: Tired, 3: Neutral, 4: Good, 5: Fantastic
updatedAt: string;
pinned?: boolean;
}
export interface ExerciseSet {
setNumber: number;
weight: number; // in kg
reps: number;
rpe: number; // Rate of Perceived Exertion (6 - 10)
}
export interface WorkoutExercise {
name: string;
muscleGroup: string;
sets: ExerciseSet[];
}
export interface WorkoutSession {
id: string;
name: string;
date: string; // YYYY-MM-DD
muscleGroups: string[];
volumeLoad: number; // sum of (weight * reps) across all sets in kg
exercises: WorkoutExercise[];
notes?: string;
}
export interface DailyNutrition {
date: string;
calories: number;
calorieTarget: number;
protein: number; // in grams
proteinTarget: number; // in grams
carbs: number; // in grams
carbsTarget: number;
fats: number; // in grams
fatsTarget: number;
waterLiters: number;
creatineTaken: boolean;
bodyweightKg: number;
}
export interface MuscleRecovery {
muscle: string;
lastTrainedDaysAgo: number;
recoveryHours: number; // Typically 48-72h for MPS
percentRecovered: number; // 0 - 100
status: 'ready' | 'recovering' | 'fatigued';
}
export type ActiveModule =
| 'dashboard'
| 'fitness'
| 'tasks'
| 'habits'
| 'calendar'
| 'finance'
| 'notes'
| 'pomodoro';
+39
View File
@@ -0,0 +1,39 @@
'use client';
import { useState, useEffect } from 'react';
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState<T>(initialValue);
const [isHydrated, setIsHydrated] = useState(false);
useEffect(() => {
try {
const item = window.localStorage.getItem(key);
if (item) {
setStoredValue(JSON.parse(item));
} else {
window.localStorage.setItem(key, JSON.stringify(initialValue));
}
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error);
}
setIsHydrated(true);
}, [key]);
const setValue = (value: T | ((val: T) => T)) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error);
}
};
return [storedValue, setValue];
}
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}