| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>YearEndQuest Tracker</title> |
| <link rel="icon" type="image/x-icon" href="/static/favicon.ico"> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script> |
| tailwind.config = { |
| darkMode: ["class"], |
| theme: { |
| extend: { |
| colors: { |
| primary: { |
| 50: '#f0f9ff', |
| 100: '#e0f2fe', |
| 200: '#bae6fd', |
| 300: '#7dd3fc', |
| 400: '#38bdf8', |
| 500: '#0ea5e9', |
| 600: '#0284c7', |
| 700: '#0369a1', |
| 800: '#075985', |
| 900: '#0c4a6e', |
| }, |
| secondary: { |
| 50: '#faf7ff', |
| 100: '#f2e9ff', |
| 200: '#e4d4ff', |
| 300: '#d0b4ff', |
| 400: '#b990ff', |
| 500: '#a855f7', |
| 600: '#9333ea', |
| 700: '#7c3aed', |
| 800: '#6b21a8', |
| 900: '#581c87', |
| } |
| } |
| } |
| } |
| } |
| </script> |
| <script src="https://unpkg.com/react@18/umd/react.development.js"></script> |
| <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> |
| <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> |
| <script src="https://unpkg.com/framer-motion@10.16.4/dist/framer-motion.js"></script> |
| <script src="https://unpkg.com/recharts/umd/Recharts.js"></script> |
| <script src="https://unpkg.com/lucide-react@0.263.1/dist/umd/lucide-react.js"></script> |
| <script src="https://unpkg.com/feather-icons"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); |
| |
| * { |
| font-family: 'Inter', sans-serif; |
| } |
| |
| .shadcn-button { |
| display: inline-flex; |
| align-items: center; |
| justify-content: center; |
| border-radius: 0.375rem; |
| font-weight: 500; |
| transition: all 0.2s; |
| outline: none; |
| } |
| |
| .shadcn-button:focus-visible { |
| outline: 2px solid rgb(14 165 233); |
| outline-offset: 2px; |
| } |
| |
| .shadcn-button-primary { |
| background-color: rgb(14 165 233); |
| color: white; |
| } |
| |
| .shadcn-button-primary:hover { |
| background-color: rgb(3 105 161); |
| } |
| |
| .shadcn-button-secondary { |
| background-color: rgb(168 85 247); |
| color: white; |
| } |
| |
| .shadcn-button-secondary:hover { |
| background-color: rgb(126 58 237); |
| } |
| |
| .shadcn-input { |
| display: flex; |
| height: 2.5rem; |
| width: 100%; |
| border-radius: 0.375rem; |
| border: 1px solid rgb(229 231 235); |
| padding: 0.5rem 0.75rem; |
| font-size: 0.875rem; |
| transition: border-color 0.2s; |
| } |
| |
| .shadcn-input:focus { |
| outline: none; |
| border-color: rgb(14 165 233); |
| } |
| |
| .shadcn-card { |
| border-radius: 0.5rem; |
| border: 1px solid rgb(229 231 235); |
| background-color: white; |
| padding: 1.5rem; |
| box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); |
| } |
| |
| .dark .shadcn-card { |
| background-color: rgb(31 41 55); |
| border-color: rgb(55 65 81); |
| } |
| |
| .dark .shadcn-input { |
| background-color: rgb(31 41 55); |
| border-color: rgb(55 65 81); |
| color: white; |
| } |
| |
| .grid-square { |
| width: 1.25rem; |
| height: 1.25rem; |
| border-radius: 0.25rem; |
| margin: 0.125rem; |
| } |
| |
| .fade-in { |
| animation: fadeIn 0.5s ease-in-out; |
| } |
| |
| @keyframes fadeIn { |
| from { opacity: 0; } |
| to { opacity: 1; } |
| } |
| |
| .scale-up { |
| animation: scaleUp 0.3s ease-out; |
| } |
| |
| @keyframes scaleUp { |
| from { transform: scale(0.8); } |
| to { transform: scale(1); } |
| } |
| |
| .progress-bar { |
| transition: width 0.5s ease-in-out; |
| } |
| |
| .streak-badge { |
| animation: pulse 2s infinite; |
| } |
| |
| @keyframes pulse { |
| 0% { box-shadow: 0 0 0 0 rgba(14, 165, 233, 0.7); } |
| 70% { box-shadow: 0 0 0 10px rgba(14, 165, 233, 0); } |
| 100% { box-shadow: 0 0 0 0 rgba(14, 165, 233, 0); } |
| } |
| |
| .completed-task { |
| text-decoration: line-through; |
| opacity: 0.7; |
| transition: all 0.3s ease; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors duration-200"> |
| <div id="root"></div> |
|
|
| <script type="text/babel"> |
| const { useState, useEffect, useRef } = React; |
| const { motion, AnimatePresence } = FramerMotion; |
| const { LineChart, Line, BarChart, Bar, PieChart, Pie, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell } = Recharts; |
| const { Check, Plus, X, Calendar, Target, BarChart3, Sun, Moon, Trophy, Flame } = LucideReact; |
| |
| |
| function useLocalStorage(key, initialValue) { |
| const [storedValue, setStoredValue] = useState(() => { |
| try { |
| const item = window.localStorage.getItem(key); |
| return item ? JSON.parse(item) : initialValue; |
| } catch (error) { |
| console.error(error); |
| return initialValue; |
| } |
| }); |
| |
| const setValue = (value) => { |
| try { |
| const valueToStore = value instanceof Function ? value(storedValue) : value; |
| setStoredValue(valueToStore); |
| window.localStorage.setItem(key, JSON.stringify(valueToStore)); |
| } catch (error) { |
| console.error(error); |
| } |
| }; |
| |
| return [storedValue, setValue]; |
| } |
| |
| |
| const utils = { |
| getDaysRemaining: () => { |
| const now = new Date(); |
| const endOfYear = new Date(now.getFullYear(), 11, 31, 23, 59, 59); |
| const diff = endOfYear - now; |
| |
| const days = Math.floor(diff / (1000 * 60 * 60 * 24)); |
| const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); |
| const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); |
| const seconds = Math.floor((diff % (1000 * 60)) / 1000); |
| |
| return { days, hours, minutes, seconds, totalSeconds: diff / 1000 }; |
| }, |
| |
| getYearProgress: () => { |
| const now = new Date(); |
| const startOfYear = new Date(now.getFullYear(), 0, 1); |
| const endOfYear = new Date(now.getFullYear(), 11, 31, 23, 59, 59); |
| const total = endOfYear - startOfYear; |
| const elapsed = now - startOfYear; |
| |
| return (elapsed / total) * 100; |
| }, |
| |
| getDaysInYear: () => { |
| const now = new Date(); |
| const startOfYear = new Date(now.getFullYear(), 0, 1); |
| const endOfYear = new Date(now.getFullYear(), 11, 31, 23, 59, 59); |
| return Math.ceil((endOfYear - startOfYear) / (1000 * 60 * 60 * 24)); |
| }, |
| |
| getDayOfYear: () => { |
| const now = new Date(); |
| const start = new Date(now.getFullYear(), 0, 0); |
| const diff = now - start; |
| const oneDay = 1000 * 60 * 60 * 24; |
| return Math.floor(diff / oneDay); |
| }, |
| |
| formatDate: (date) => { |
| return date.toLocaleDateString('en-US', { |
| weekday: 'short', |
| year: 'numeric', |
| month: 'short', |
| day: 'numeric' |
| }); |
| } |
| }; |
| |
| |
| function Countdown() { |
| const [timeLeft, setTimeLeft] = useState(utils.getDaysRemaining()); |
| |
| useEffect(() => { |
| const timer = setInterval(() => { |
| setTimeLeft(utils.getDaysRemaining()); |
| }, 1000); |
| |
| return () => clearInterval(timer); |
| }, []); |
| |
| return ( |
| <div className="text-center mb-8"> |
| <h2 className="text-2xl font-bold mb-4">Time Until New Year</h2> |
| <div className="flex justify-center space-x-4"> |
| <div className="bg-primary-100 dark:bg-primary-900 rounded-lg p-4 min-w-[80px]"> |
| <div className="text-3xl font-bold">{timeLeft.days}</div> |
| <div className="text-sm">Days</div> |
| </div> |
| <div className="bg-primary-100 dark:bg-primary-900 rounded-lg p-4 min-w-[80px]"> |
| <div className="text-3xl font-bold">{timeLeft.hours}</div> |
| <div className="text-sm">Hours</div> |
| </div> |
| <div className="bg-primary-100 dark:bg-primary-900 rounded-lg p-4 min-w-[80px]"> |
| <div className="text-3xl font-bold">{timeLeft.minutes}</div> |
| <div className="text-sm">Minutes</div> |
| </div> |
| <div className="bg-primary-100 dark:bg-primary-900 rounded-lg p-4 min-w-[80px]"> |
| <div className="text-3xl font-bold">{timeLeft.seconds}</div> |
| <div className="text-sm">Seconds</div> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
| |
| |
| function YearGrid({ dailyTasks }) { |
| const daysInYear = utils.getDaysInYear(); |
| const dayOfYear = utils.getDayOfYear(); |
| const daysRemaining = daysInYear - dayOfYear; |
| |
| const grid = []; |
| for (let i = 1; i <= daysInYear; i++) { |
| const isFuture = i > dayOfYear; |
| const isCompleted = dailyTasks.some(task => |
| task.completed && new Date(task.date).getDate() === new Date().getDate() && |
| new Date(task.date).getMonth() === new Date().getMonth() && |
| new Date(task.date).getFullYear() === new Date().getFullYear() |
| ); |
| |
| grid.push( |
| <motion.div |
| key={i} |
| className={`grid-square ${isFuture ? 'bg-primary-300' : isCompleted ? 'bg-primary-600' : 'bg-gray-300 dark:bg-gray-600'}`} |
| initial={{ opacity: 0, scale: 0.8 }} |
| animate={{ opacity: 1, scale: 1 }} |
| transition={{ delay: i * 0.002 }} |
| title={`Day ${i}`} |
| /> |
| ); |
| } |
| |
| return ( |
| <div className="mb-8"> |
| <h2 className="text-2xl font-bold mb-4">Year Progress</h2> |
| <div className="flex flex-wrap justify-center max-w-3xl mx-auto"> |
| {grid} |
| </div> |
| </div> |
| ); |
| } |
| |
| |
| function ProgressBar() { |
| const [progress, setProgress] = useState(0); |
| |
| useEffect(() => { |
| setProgress(utils.getYearProgress()); |
| }, []); |
| |
| return ( |
| <div className="w-full bg-gray-200 rounded-full h-4 mb-8 dark:bg-gray-700"> |
| <motion.div |
| className="progress-bar h-4 rounded-full bg-primary-500" |
| initial={{ width: 0 }} |
| animate={{ width: `${progress}%` }} |
| transition={{ duration: 1, ease: "easeOut" }} |
| /> |
| <div className="text-xs mt-1 text-right">{progress.toFixed(1)}% of year completed</div> |
| </div> |
| ); |
| } |
| |
| |
| function TaskManager() { |
| const [dailyTasks, setDailyTasks] = useLocalStorage('dailyTasks', []); |
| const [yearlyGoals, setYearlyGoals] = useLocalStorage('yearlyGoals', []); |
| const [showAddDaily, setShowAddDaily] = useState(false); |
| const [showAddYearly, setShowAddYearly] = useState(false); |
| const [newDailyTask, setNewDailyTask] = useState(''); |
| const [newYearlyGoal, setNewYearlyGoal] = useState(''); |
| |
| const addDailyTask = () => { |
| if (newDailyTask.trim()) { |
| setDailyTasks([...dailyTasks, { |
| id: Date.now(), |
| text: newDailyTask, |
| completed: false, |
| date: new Date().toISOString(), |
| streak: 0 |
| }]); |
| setNewDailyTask(''); |
| setShowAddDaily(false); |
| } |
| }; |
| |
| const addYearlyGoal = () => { |
| if (newYearlyGoal.trim()) { |
| setYearlyGoals([...yearlyGoals, { |
| id: Date.now(), |
| text: newYearlyGoal, |
| completed: false |
| }]); |
| setNewYearlyGoal(''); |
| setShowAddYearly(false); |
| } |
| }; |
| |
| const toggleDailyTask = (id) => { |
| setDailyTasks(dailyTasks.map(task => |
| task.id === id ? { ...task, completed: !task.completed } : task |
| )); |
| }; |
| |
| const toggleYearlyGoal = (id) => { |
| setYearlyGoals(yearlyGoals.map(goal => |
| goal.id === id ? { ...goal, completed: !goal.completed } : goal |
| )); |
| }; |
| |
| const deleteDailyTask = (id) => { |
| setDailyTasks(dailyTasks.filter(task => task.id !== id)); |
| }; |
| |
| const deleteYearlyGoal = (id) => { |
| setYearlyGoals(yearlyGoals.filter(goal => goal.id !== id)); |
| }; |
| |
| |
| const streak = dailyTasks.filter(task => { |
| const taskDate = new Date(task.date); |
| const today = new Date(); |
| return task.completed && taskDate.toDateString() === today.toDateString(); |
| }).length > 0 ? (parseInt(localStorage.getItem('streak') || 0)) : 0; |
| |
| useEffect(() => { |
| |
| const lastReset = localStorage.getItem('lastReset'); |
| const today = new Date().toDateString(); |
| |
| if (lastReset !== today) { |
| |
| setDailyTasks(prev => prev.map(task => ({ ...task, completed: false }))); |
| localStorage.setItem('lastReset', today); |
| |
| |
| const completedYesterday = dailyTasks.some(task => { |
| const taskDate = new Date(task.date); |
| const yesterday = new Date(); |
| yesterday.setDate(yesterday.getDate() - 1); |
| return task.completed && taskDate.toDateString() === yesterday.toDateString(); |
| }); |
| |
| if (completedYesterday) { |
| const newStreak = streak + 1; |
| localStorage.setItem('streak', newStreak); |
| } else { |
| localStorage.setItem('streak', 0); |
| } |
| } |
| }, [dailyTasks, setDailyTasks, streak]); |
| |
| return ( |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8"> |
| {/* Daily Tasks */} |
| <div className="shadcn-card"> |
| <div className="flex justify-between items-center mb-4"> |
| <h2 className="text-xl font-bold flex items-center"> |
| <Calendar className="mr-2" size={20} /> |
| Daily Goals |
| {streak > 0 && ( |
| <span className="streak-badge ml-2 bg-primary-100 text-primary-800 text-xs font-medium px-2.5 py-0.5 rounded-full dark:bg-primary-900 dark:text-primary-300 flex items-center"> |
| <Flame size={12} className="mr-1" /> {streak} day streak |
| </span> |
| )} |
| </h2> |
| <button |
| className="shadcn-button shadcn-button-primary p-2" |
| onClick={() => setShowAddDaily(true)} |
| > |
| <Plus size={16} /> |
| </button> |
| </div> |
| |
| {showAddDaily && ( |
| <motion.div |
| className="mb-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg" |
| initial={{ opacity: 0, height: 0 }} |
| animate={{ opacity: 1, height: 'auto' }} |
| exit={{ opacity: 0, height: 0 }} |
| > |
| <input |
| type="text" |
| className="shadcn-input mb-2" |
| placeholder="Enter daily task" |
| value={newDailyTask} |
| onChange={(e) => setNewDailyTask(e.target.value)} |
| onKeyPress={(e) => e.key === 'Enter' && addDailyTask()} |
| /> |
| <div className="flex space-x-2"> |
| <button |
| className="shadcn-button shadcn-button-primary flex-1" |
| onClick={addDailyTask} |
| > |
| Add Task |
| </button> |
| <button |
| className="shadcn-button bg-gray-300 dark:bg-gray-600 flex-1" |
| onClick={() => setShowAddDaily(false)} |
| > |
| Cancel |
| </button> |
| </div> |
| </motion.div> |
| )} |
| |
| <AnimatePresence> |
| {dailyTasks.filter(task => !task.completed).map((task) => ( |
| <motion.div |
| key={task.id} |
| className="flex items-center justify-between p-3 mb-2 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700" |
| initial={{ opacity: 0, y: -10 }} |
| animate={{ opacity: 1, y: 0 }} |
| exit={{ opacity: 0, y: -10 }} |
| transition={{ duration: 0.2 }} |
| > |
| <div className="flex items-center"> |
| <button |
| className="w-5 h-5 rounded border border-gray-300 dark:border-gray-600 mr-3 flex items-center justify-center hover:bg-primary-50 dark:hover:bg-primary-900" |
| onClick={() => toggleDailyTask(task.id)} |
| > |
| {task.completed && <Check size={14} className="text-primary-600" />} |
| </button> |
| <span>{task.text}</span> |
| </div> |
| <button |
| className="text-gray-400 hover:text-red-500" |
| onClick={() => deleteDailyTask(task.id)} |
| > |
| <X size={16} /> |
| </button> |
| </motion.div> |
| ))} |
| </AnimatePresence> |
| |
| {dailyTasks.filter(task => task.completed).length > 0 && ( |
| <div className="mt-4"> |
| <h3 className="text-sm font-medium mb-2 text-gray-500 dark:text-gray-400">Completed Today</h3> |
| <AnimatePresence> |
| {dailyTasks.filter(task => task.completed).map((task) => ( |
| <motion.div |
| key={task.id} |
| className="flex items-center justify-between p-3 mb-2 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800" |
| initial={{ opacity: 0, height: 0 }} |
| animate={{ opacity: 1, height: 'auto' }} |
| exit={{ opacity: 0, height: 0 }} |
| transition={{ duration: 0.3 }} |
| > |
| <div className="flex items-center"> |
| <Check size={16} className="text-green-600 mr-3" /> |
| <span className="completed-task">{task.text}</span> |
| </div> |
| <button |
| className="text-gray-400 hover:text-red-500" |
| onClick={() => deleteDailyTask(task.id)} |
| > |
| <X size={16} /> |
| </button> |
| </motion.div> |
| ))} |
| </AnimatePresence> |
| </div> |
| )} |
| </div> |
| |
| {/* Yearly Goals */} |
| <div className="shadcn-card"> |
| <div className="flex justify-between items-center mb-4"> |
| <h2 className="text-xl font-bold flex items-center"> |
| <Target className="mr-2" size={20} /> |
| End-of-Year Goals |
| </h2> |
| <button |
| className="shadcn-button shadcn-button-secondary p-2" |
| onClick={() => setShowAddYearly(true)} |
| > |
| <Plus size={16} /> |
| </button> |
| </div> |
| |
| {showAddYearly && ( |
| <motion.div |
| className="mb-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg" |
| initial={{ opacity: 0, height: 0 }} |
| animate={{ opacity: 1, height: 'auto' }} |
| exit={{ opacity: 0, height: 0 }} |
| > |
| <input |
| type="text" |
| className="shadcn-input mb-2" |
| placeholder="Enter yearly goal" |
| value={newYearlyGoal} |
| onChange={(e) => setNewYearlyGoal(e.target.value)} |
| onKeyPress={(e) => e.key === 'Enter' && addYearlyGoal()} |
| /> |
| <div className="flex space-x-2"> |
| <button |
| className="shadcn-button shadcn-button-secondary flex-1" |
| onClick={addYearlyGoal} |
| > |
| Add Goal |
| </button> |
| <button |
| className="shadcn-button bg-gray-300 dark:bg-gray-600 flex-1" |
| onClick={() => setShowAddYearly(false)} |
| > |
| Cancel |
| </button> |
| </div> |
| </motion.div> |
| )} |
| |
| <AnimatePresence> |
| {yearlyGoals.filter(goal => !goal.completed).map((goal) => ( |
| <motion.div |
| key={goal.id} |
| className="flex items-center justify-between p-3 mb-2 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700" |
| initial={{ opacity: 0, y: -10 }} |
| animate={{ opacity: 1, y: 0 }} |
| exit={{ opacity: 0, y: -10 }} |
| transition={{ duration: 0.2 }} |
| > |
| <div className="flex items-center"> |
| <button |
| className="w-5 h-5 rounded border border-gray-300 dark:border-gray-600 mr-3 flex items-center justify-center hover:bg-primary-50 dark:hover:bg-primary-900" |
| onClick={() => toggleYearlyGoal(goal.id)} |
| > |
| {goal.completed && <Check size={14} className="text-primary-600" />} |
| </button> |
| <span>{goal.text}</span> |
| </div> |
| <button |
| className="text-gray-400 hover:text-red-500" |
| onClick={() => deleteYearlyGoal(goal.id)} |
| > |
| <X size={16} /> |
| </button> |
| </motion.div> |
| ))} |
| </AnimatePresence> |
| |
| {yearlyGoals.filter(goal => goal.completed).length > 0 && ( |
| <div className="mt-4"> |
| <h3 className="text-sm font-medium mb-2 text-gray-500 dark:text-gray-400 flex items-center"> |
| <Trophy className="mr-1" size={16} /> Completed Goals |
| </h3> |
| <AnimatePresence> |
| {yearlyGoals.filter(goal => goal.completed).map((goal) => ( |
| <motion.div |
| key={goal.id} |
| className="flex items-center justify-between p-3 mb-2 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800" |
| initial={{ opacity: 0, height: 0 }} |
| animate={{ opacity: 1, height: 'auto' }} |
| exit={{ opacity: 0, height: 0 }} |
| transition={{ duration: 0.3 }} |
| > |
| <div className="flex items-center"> |
| <Check size={16} className="text-yellow-600 mr-3" /> |
| <span className="completed-task">{goal.text}</span> |
| </div> |
| <button |
| className="text-gray-400 hover:text-red-500" |
| onClick={() => deleteYearlyGoal(goal.id)} |
| > |
| <X size={16} /> |
| </button> |
| </motion.div> |
| ))} |
| </AnimatePresence> |
| </div> |
| )} |
| </div> |
| </div> |
| ); |
| } |
| |
| |
| function Analytics({ dailyTasks, yearlyGoals }) { |
| const completedYearly = yearlyGoals.filter(g => g.completed).length; |
| const totalYearly = yearlyGoals.length; |
| const yearlyPercentage = totalYearly > 0 ? (completedYearly / totalYearly) * 100 : 0; |
| |
| |
| const last7Days = Array.from({ length: 7 }, (_, i) => { |
| const date = new Date(); |
| date.setDate(date.getDate() - i); |
| return date.toDateString(); |
| }); |
| |
| const weeklyData = last7Days.map(date => { |
| const completed = dailyTasks.filter(task => |
| new Date(task.date).toDateString() === date && task.completed |
| ).length; |
| const total = dailyTasks.filter(task => |
| new Date(task.date).toDateString() === date |
| ).length; |
| |
| return { |
| date: date.slice(0, 3), |
| completionRate: total > 0 ? (completed / total) * 100 : 0, |
| completed, |
| total |
| }; |
| }).reverse(); |
| |
| const motivationalMessages = [ |
| "You're on a roll! Keep it up!", |
| "Consistency is key - great job!", |
| "Every task completed brings you closer to your goals!", |
| "You're making amazing progress!", |
| "Just a few more tasks to complete your day!", |
| "Your dedication is inspiring!", |
| "Small steps lead to big achievements!" |
| ]; |
| |
| const randomMessage = motivationalMessages[Math.floor(Math.random() * motivationalMessages.length)]; |
| |
| return ( |
| <div className="shadcn-card mb-8"> |
| <h2 className="text-xl font-bold mb-4 flex items-center"> |
| <BarChart3 className="mr-2" size={20} /> |
| Progress Analytics |
| </h2> |
| |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6"> |
| {/* Yearly Goals Progress */} |
| <div> |
| <h3 className="text-lg font-semibold mb-2">Yearly Goals</h3> |
| <div className="w-full bg-gray-200 rounded-full h-4 dark:bg-gray-700 mb-2"> |
| <div |
| className="h-4 rounded-full bg-primary-500 transition-all duration-500" |
| style={{ width: `${yearlyPercentage}%` }} |
| /> |
| </div> |
| <p className="text-sm text-gray-600 dark:text-gray-400"> |
| {completedYearly} of {totalYearly} goals completed ({yearlyPercentage.toFixed(1)}%) |
| </p> |
| </div> |
| |
| {/* Weekly Completion */} |
| <div> |
| <h3 className="text-lg font-semibold mb-2">Weekly Performance</h3> |
| <ResponsiveContainer width="100%" height={100}> |
| <BarChart data={weeklyData}> |
| <Bar dataKey="completionRate" fill="#0ea5e9" radius={[4, 4, 0, 0]} /> |
| <XAxis dataKey="date" /> |
| <YAxis domain={[0, 100]} /> |
| </BarChart> |
| </ResponsiveContainer> |
| </div> |
| </div> |
| |
| {/* Motivational Message */} |
| <div className="bg-primary-50 dark:bg-primary-900/30 p-4 rounded-lg border border-primary-200 dark:border-primary-800"> |
| <p className="text-primary-800 dark:text-primary-200 text-center italic"> |
| {randomMessage} |
| </p> |
| </div> |
| </div> |
| ); |
| } |
| |
| |
| function ThemeToggle() { |
| const [isDark, setIsDark] = useState(() => { |
| return localStorage.getItem('theme') === 'dark' || |
| (window.matchMedia('(prefers-color-scheme: dark)').matches && !localStorage.getItem('theme')); |
| }); |
| |
| useEffect(() => { |
| if (isDark) { |
| document.documentElement.classList.add('dark'); |
| localStorage.setItem('theme', 'dark'); |
| } else { |
| document.documentElement.classList.remove('dark'); |
| localStorage.setItem('theme', 'light'); |
| } |
| }, [isDark]); |
| |
| return ( |
| <button |
| className="fixed top-4 right-4 p-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-lg" |
| onClick={() => setIsDark(!isDark)} |
| > |
| {isDark ? <Sun size={20} /> : <Moon size={20} />} |
| </button> |
| ); |
| } |
| |
| |
| function App() { |
| const [dailyTasks] = useLocalStorage('dailyTasks', []); |
| const [yearlyGoals] = useLocalStorage('yearlyGoals', []); |
| |
| return ( |
| <div className="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors duration-200"> |
| <div className="container mx-auto px-4 py-8 max-w-6xl"> |
| <motion.header |
| className="text-center mb-8" |
| initial={{ opacity: 0, y: -20 }} |
| animate={{ opacity: 1, y: 0 }} |
| transition={{ duration: 0.5 }} |
| > |
| <h1 className="text-4xl font-bold mb-2 bg-gradient-to-r from-primary-600 to-secondary-600 bg-clip-text text-transparent"> |
| YearEndQuest Tracker |
| </h1> |
| <p className="text-gray-600 dark:text-gray-400 mb-4"> |
| Make the most of the remaining days this year |
| </p> |
| <div className="flex justify-center space-x-4"> |
| <a href="portfolio.html" className="shadcn-button shadcn-button-primary px-6 py-3"> |
| View Portfolio |
| </a> |
| </motion.header> |
| |
| <Countdown /> |
| <ProgressBar /> |
| <YearGrid dailyTasks={dailyTasks} /> |
| <TaskManager /> |
| <Analytics dailyTasks={dailyTasks} yearlyGoals={yearlyGoals} /> |
| <ThemeToggle /> |
| </div> |
| </div> |
| ); |
| } |
| |
| // Render the app |
| const root = ReactDOM.createRoot(document.getElementById('root')); |
| root.render(<App />); |
| |
| // Initialize feather icons |
| feather.replace(); |
| </script> |
| </body> |
| </html> |
| </body> |
| </html> |