Finalizing Version 1.0.0

This commit is contained in:
Anoy Roy Chowdhury
2025-07-22 15:51:47 +05:30
parent d96814b13f
commit 12db63d90f
16 changed files with 710 additions and 176 deletions
+226 -22
View File
@@ -1,36 +1,240 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
# 🎯 Priospace
## Getting Started
A beautiful, modern productivity app that combines task management with Pomodoro timer functionality and habit tracking. Built with Next.js, React, and Framer Motion for smooth animations and premium user experience.
First, run the development server:
![Todo Timer](https://img.shields.io/badge/Version-2.0-blue)
![Next.js](https://img.shields.io/badge/Next.js-15-black)
![React](https://img.shields.io/badge/React-19-blue)
![TypeScript](https://img.shields.io/badge/TypeScript-5-blue)
![Tailwind](https://img.shields.io/badge/Tailwind-3-38bdf8)
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
## ✨ Features
### 📝 Task Management
- Create, edit, and manage tasks and habits
- Organize tasks with custom color-coded categories
- Mark tasks as complete with smooth animations
- Hold-to-complete gesture for mobile-friendly interaction
- Separate sections for regular tasks and habits
### ⏱️ Pomodoro Timer
- Customizable timer presets (5, 10, 25, 50 minutes)
- Real-time countdown with animated display
- Overtime tracking for extended work sessions
- Break timer with automatic transitions
- Focus time tracking for productivity insights
- Music player-style controls (play/pause/stop)
### 🔄 Habit Tracking
- GitHub-style contribution graph for 90-day habit history
- Monthly view with visual intensity indicators
- Individual habit tracking with daily completion
- Interactive grid for marking habit completion
- Comprehensive habit analytics and progress visualization
### 🎨 Beautiful UI/UX
- Modern bottom-sheet modal design
- Smooth spring animations with Framer Motion
- Responsive design optimized for mobile and desktop
- Dark/Light mode support with seamless transitions
- Primary color theming throughout the app
- Intuitive touch gestures and micro-interactions
### 📊 Data Management
- Export/Import functionality for data backup
- Local storage persistence
- Settings panel with theme controls
- Motivational intro screen with random quotes
## 🚀 Getting Started
### Prerequisites
- Node.js 18+
- npm or yarn package manager
### Installation
1. **Clone the repository**
```bash
git clone https://github.com/anoyrc/priospace.git
cd priospace
```
2. **Install dependencies**
```bash
npm install
# or
yarn install
```
3. **Run the development server**
```bash
npm run dev
# or
yarn dev
```
4. **Open your browser**
Navigate to [http://localhost:3000](http://localhost:3000)
## 🏗️ Tech Stack
### Frontend
- **Next.js 15** - React framework with App Router
- **React 19** - UI library with hooks and modern patterns
- **TypeScript** - Type-safe JavaScript development
- **Tailwind CSS** - Utility-first CSS framework
- **Framer Motion** - Animation library for smooth interactions
### UI Components
- **shadcn/ui** - High-quality, accessible UI components
- **Lucide React** - Beautiful, customizable icons
- **Custom Components** - CountdownTimer, CircleProgress, and modal systems
### State Management
- **React Hooks** - useState, useEffect, useRef for state management
- **Local Storage** - Data persistence in browser
- **Context API** - Theme and global state management
## 📁 Project Structure
```
todo-timer/
├── app/ # Next.js App Router
│ ├── globals.css # Global styles and Tailwind imports
│ ├── layout.tsx # Root layout with providers
│ └── page.tsx # Main application page
├── components/ # Reusable components
├── lib/ # Utility functions
│ └── utils.ts # Helper functions and utilities
├── public/ # Static assets
└── package.json # Dependencies and scripts
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## 🎮 Usage
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
### Creating Tasks
1. Click the **"Add Task"** button
2. Enter your task title in the borderless input
3. Optionally select or create a category with custom colors
4. Click **"Add Task"** to save
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
### Using the Timer
1. Select a task from your list
2. Choose a timer preset (5, 10, 25, or 50 minutes)
3. Use the music player-style controls:
- **Play/Pause** - Start or pause the timer
- **Stop** - Reset the timer to original time
- **Break** - Take a 5-minute break
4. Complete your task when finished
## Learn More
### Tracking Habits
1. Open the **Habit Tracker** from the bottom navigation
2. Add new habits with categories
3. Click on grid squares to mark daily completions
4. View your 90-day progress with GitHub-style visualization
5. Navigate between habit overview and individual habit views
To learn more about Next.js, take a look at the following resources:
### Managing Data
1. Access **Settings** to:
- Toggle between light and dark themes
- Export your data as JSON backup
- Import previously exported data
- Support the developer with "Buy Me a Coffee"
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## 🔧 Configuration
## Deploy on Vercel
### Customizing Colors
The app uses CSS custom properties for theming. Update your `globals.css`:
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
```css
:root {
--primary: your-color-hue your-color-saturation your-color-lightness;
}
```
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
### Timer Presets
Modify the timer presets in `TimerModal.tsx`:
```javascript
const presets = [
{ value: "5", label: "5 min", seconds: 5 * 60 },
{ value: "15", label: "15 min", seconds: 15 * 60 },
// Add your custom presets
];
```
### Motivational Quotes
Add custom quotes in `IntroScreen.tsx`:
```javascript
const motivationalQuotes = [
"Your custom quote here.",
// Add more quotes
];
```
## 📱 Progressive Web App Features
- Responsive design for mobile and desktop
- Touch gestures and mobile-optimized interactions
- Offline-ready with local storage persistence
- Fast loading with Next.js optimization
## 🤝 Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
### Development Guidelines
- Follow the existing code style and component patterns
- Use TypeScript for type safety
- Add appropriate animations with Framer Motion
- Ensure responsive design works on all screen sizes
- Test both light and dark themes
- Maintain accessibility standards
## 🐛 Bug Reports & Feature Requests
Please use the [GitHub Issues](https://github.com/yourusername/todo-timer/issues) page to:
- Report bugs with detailed reproduction steps
- Request new features with clear use cases
- Discuss improvements and suggestions
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🙏 Acknowledgments
- **shadcn/ui** for the beautiful component library
- **Framer Motion** for smooth animations
- **Lucide** for the icon set
- **Tailwind CSS** for the utility-first styling approach
- **Next.js** team for the amazing React framework
## 💖 Support
If you find this project helpful, consider:
- ⭐ Starring the repository
- 🐛 Reporting bugs or requesting features
- ☕ [Buying me a coffee](https://coff.ee/anoy)
- 🐦 Following [@Anoyroyc](https://x.com/Anoyroyc) on Twitter
---
<div align="center">
### 🎯 vibecoded
**Coded with ❤️ by [Anoy Roy Chowdhury](https://x.com/Anoyroyc)**
*Focus • Track • Achieve*
</div>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 170 KiB

+2 -2
View File
@@ -9,8 +9,8 @@ const nunito = Nunito({
});
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "PrioSpace",
description: "Focus on what matters.",
};
export default function RootLayout({ children }) {
+222 -144
View File
@@ -13,6 +13,7 @@ import { TaskOptionsModal } from "@/components/task-options-modal";
import { HabitTracker } from "@/components/habit-tracker";
import { TimerModal } from "@/components/timer-modal";
import { SettingsModal } from "@/components/settings-modal";
import { IntroScreen } from "@/components/intro-screen";
export default function Home() {
const [darkMode, setDarkMode] = useState(false);
@@ -26,6 +27,7 @@ export default function Home() {
const [showSettings, setShowSettings] = useState(false);
const [selectedTask, setSelectedTask] = useState(null);
const [selectedDate, setSelectedDate] = useState(new Date());
const [showIntroScreen, setShowIntroScreen] = useState(true);
// Load data from localStorage on mount
useEffect(() => {
@@ -60,6 +62,72 @@ export default function Home() {
}
}, []);
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (event) => {
if (showIntroScreen) return;
const isEscapePressed = event.key === "Escape";
if (isEscapePressed) {
setShowAddTask(false);
setShowHabits(false);
setShowTimer(false);
setShowSettings(false);
setShowTaskOptions(false);
return;
}
if (
showAddTask ||
showHabits ||
showTimer ||
showSettings ||
showTaskOptions
) {
return;
}
const isModifierPressed = event.ctrlKey || event.metaKey; // Ctrl for Windows/Linux, Cmd for Mac
if (isModifierPressed) {
switch (event.key.toLowerCase()) {
case "a": // Ctrl/Cmd + T for Add Task
event.preventDefault();
setShowAddTask(true);
break;
case "h": // Ctrl/Cmd + H for Habits
event.preventDefault();
setShowHabits(true);
break;
case "c": // Ctrl/Cmd + C for Timer
event.preventDefault();
setShowTimer(true);
break;
case "x": // Ctrl/Cmd + X for Settings
event.preventDefault();
setShowSettings(true);
break;
default:
break;
}
}
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [
showAddTask,
showHabits,
showTimer,
showSettings,
showTaskOptions,
showIntroScreen,
]);
// Save to localStorage whenever data changes
useEffect(() => {
localStorage.setItem("darkMode", JSON.stringify(darkMode));
@@ -355,160 +423,170 @@ export default function Home() {
const allTasks = [...regularTasks, ...dailyHabitTasks];
return (
<div className="min-h-screen transition-colors duration-300 bg-background">
<div className="max-w-lg mx-auto min-h-screen px-4 relative overflow-hidden">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="flex flex-col h-screen relative"
>
<button
onClick={() => setShowSettings(true)}
className="absolute left-1/2 -translate-x-1/2 z-10 bg-primary text-background rounded-b-lg py-2 px-2 pt-1"
>
<Settings className="h-3 w-3" />
</button>
<>
<AnimatePresence>
{showIntroScreen && (
<IntroScreen onAnimationComplete={() => setShowIntroScreen(false)} />
)}
</AnimatePresence>
{/* Header Section */}
<div className="p-4 px-0 border-b border-dashed">
<div className="flex items-center justify-between">
<DayNightCycle selectedDate={selectedDate} />
<div className="flex items-center gap-2">
<div className="text-right flex flex-col">
<div className="text-xl font-extrabold flex items-center gap-2">
<AnimatedNumber
value={selectedDate.getDate()}
fontSize={20}
/>
{selectedDate.toLocaleDateString("en-US", {
month: "long",
})}
</div>
<div className="text-xl opacity-90 -mt-1 flex justify-end">
<AnimatedYear
year={selectedDate.getFullYear()}
fontSize={30}
/>
{!showIntroScreen && (
<div className="min-h-screen transition-colors duration-300 bg-background">
<div className="max-w-lg mx-auto min-h-screen px-4 relative overflow-hidden">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="flex flex-col h-screen relative"
>
<button
onClick={() => setShowSettings(true)}
className="absolute left-1/2 -translate-x-1/2 z-10 bg-primary text-background rounded-b-lg py-2 px-2 pt-1"
>
<Settings className="h-3 w-3" />
</button>
{/* Header Section */}
<div className="p-4 px-0 border-b border-dashed">
<div className="flex items-center justify-between">
<DayNightCycle selectedDate={selectedDate} />
<div className="flex items-center gap-2">
<div className="text-right flex flex-col">
<div className="text-xl font-extrabold flex items-center gap-2">
<AnimatedNumber
value={selectedDate.getDate()}
fontSize={20}
/>
{selectedDate.toLocaleDateString("en-US", {
month: "long",
})}
</div>
<div className="text-xl opacity-90 -mt-1 flex justify-end">
<AnimatedYear
year={selectedDate.getFullYear()}
fontSize={30}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="py-3 border-b border-dashed">
<WeeklyCalendar
selectedDate={selectedDate}
onDateSelect={setSelectedDate}
/>
</div>
<div className="py-3 border-b border-dashed">
<WeeklyCalendar
selectedDate={selectedDate}
onDateSelect={setSelectedDate}
/>
</div>
<div className="flex-1 overflow-hidden">
<TaskList
tasks={allTasks}
customTags={customTags}
onToggleTask={toggleTask}
onDeleteTask={deleteTask}
onTaskClick={handleTaskClick}
/>
</div>
<div className="flex-1 overflow-hidden">
<TaskList
tasks={allTasks}
customTags={customTags}
onToggleTask={toggleTask}
onDeleteTask={deleteTask}
onTaskClick={handleTaskClick}
/>
</div>
<div className="p-4 border-t border-dashed dark:border-gray-700">
<div className="flex items-center justify-between">
<Button
onClick={() => setShowTimer(true)}
variant="ghost"
size="lg"
className="flex-1 flex items-center justify-center px-4 sm:px-8 gap-2 font-extrabold hover:bg-primary/5 group dark:text-white"
>
<div className="group-hover:scale-110 transition-transform flex items-center gap-2">
<Timer className="h-5 w-5" />
<span>Timer</span>
<div className="p-4 border-t border-dashed dark:border-gray-700">
<div className="flex items-center justify-between">
<Button
onClick={() => setShowTimer(true)}
variant="ghost"
size="lg"
className="flex-1 flex items-center justify-center px-4 sm:px-8 gap-2 font-extrabold hover:bg-primary/5 group dark:text-white"
>
<div className="group-hover:scale-110 transition-transform flex items-center gap-2">
<Timer className="h-5 w-5" />
<span>Timer</span>
</div>
</Button>
<Button
onClick={() => setShowAddTask(true)}
size="lg"
className="mx-4 rounded-full w-12 h-12 px-4 sm:px-8 bg-primary hover:bg-primary/90 group hover:scale-110 transition-transform [&_svg]:size-5"
>
<Plus className="h-5 w-5 group-hover:scale-110 transition-transform" />
</Button>
<Button
onClick={() => setShowHabits(true)}
variant="ghost"
size="lg"
className="flex-1 flex items-center justify-center px-4 sm:px-8 gap-2 font-extrabold hover:bg-primary/5 group dark:text-white"
>
<div className="group-hover:scale-110 transition-transform flex items-center gap-2">
<BarChart3 className="h-5 w-5" />
<span>Habits</span>
</div>
</Button>
</div>
</Button>
</div>
</motion.div>
<Button
onClick={() => setShowAddTask(true)}
size="lg"
className="mx-4 rounded-full w-12 h-12 px-4 sm:px-8 bg-primary hover:bg-primary/90 group hover:scale-110 transition-transform [&_svg]:size-5"
>
<Plus className="h-5 w-5 group-hover:scale-110 transition-transform" />
</Button>
<AnimatePresence>
{showSettings && (
<SettingsModal
onClose={() => setShowSettings(false)}
darkMode={darkMode}
onToggleDarkMode={() => setDarkMode(!darkMode)}
onExportData={exportData}
onImportData={importData}
/>
)}
<Button
onClick={() => setShowHabits(true)}
variant="ghost"
size="lg"
className="flex-1 flex items-center justify-center px-4 sm:px-8 gap-2 font-extrabold hover:bg-primary/5 group dark:text-white"
>
<div className="group-hover:scale-110 transition-transform flex items-center gap-2">
<BarChart3 className="h-5 w-5" />
<span>Habits</span>
</div>
</Button>
</div>
{showAddTask && (
<AddTaskModal
onClose={() => setShowAddTask(false)}
onAddTask={addTask}
customTags={customTags}
onAddCustomTag={addCustomTag}
/>
)}
{showTaskOptions && selectedTask && (
<TaskOptionsModal
task={selectedTask}
customTags={customTags}
onClose={() => {
setShowTaskOptions(false);
setSelectedTask(null);
}}
onUpdateTask={updateTask}
onDeleteTask={deleteTask}
onAddCustomTag={addCustomTag}
onToggleTask={toggleTask}
selectedDate={selectedDate}
onTransferTask={transferTaskToCurrentDay}
currentActualDate={new Date()}
/>
)}
{showHabits && (
<HabitTracker
habits={habits}
customTags={customTags}
onClose={() => setShowHabits(false)}
onUpdateHabits={setHabits}
onAddCustomTag={addCustomTag}
/>
)}
{showTimer && (
<TimerModal
tasks={allTasks}
onClose={() => setShowTimer(false)}
onUpdateTaskTime={updateTaskTime}
onUpdateTaskFocusTime={updateTaskFocusTime}
onToggleTask={toggleTask}
/>
)}
</AnimatePresence>
</div>
</motion.div>
<AnimatePresence>
{showSettings && (
<SettingsModal
onClose={() => setShowSettings(false)}
darkMode={darkMode}
onToggleDarkMode={() => setDarkMode(!darkMode)}
onExportData={exportData}
onImportData={importData}
/>
)}
{showAddTask && (
<AddTaskModal
onClose={() => setShowAddTask(false)}
onAddTask={addTask}
customTags={customTags}
onAddCustomTag={addCustomTag}
/>
)}
{showTaskOptions && selectedTask && (
<TaskOptionsModal
task={selectedTask}
customTags={customTags}
onClose={() => {
setShowTaskOptions(false);
setSelectedTask(null);
}}
onUpdateTask={updateTask}
onDeleteTask={deleteTask}
onAddCustomTag={addCustomTag}
onToggleTask={toggleTask}
selectedDate={selectedDate}
onTransferTask={transferTaskToCurrentDay}
currentActualDate={new Date()}
/>
)}
{showHabits && (
<HabitTracker
habits={habits}
customTags={customTags}
onClose={() => setShowHabits(false)}
onUpdateHabits={setHabits}
onAddCustomTag={addCustomTag}
/>
)}
{showTimer && (
<TimerModal
tasks={allTasks}
onClose={() => setShowTimer(false)}
onUpdateTaskTime={updateTaskTime}
onUpdateTaskFocusTime={updateTaskFocusTime}
onToggleTask={toggleTask}
/>
)}
</AnimatePresence>
</div>
</div>
</div>
)}
</>
);
}
+212
View File
@@ -0,0 +1,212 @@
"use client";
import { motion } from "framer-motion";
import { useState, useEffect } from "react";
import { CheckCircle, Timer, RotateCcw } from "lucide-react";
const motivationalQuotes = [
"Focus on what matters.",
"Every day is a new beginning.",
"Small steps, big results.",
"Your time is your most valuable asset.",
"Achieve your goals, one task at a time.",
"Make today count.",
"Progress, not perfection.",
"Start where you are. Use what you have. Do what you can.",
"The best way to predict the future is to create it.",
"Discipline is choosing between what you want now and what you want most.",
"The journey of a thousand miles begins with a single step.",
"Believe you can and you're halfway there.",
"The future belongs to those who believe in the beauty of their dreams.",
"Success is not final, failure is not fatal: it is the courage to continue that counts.",
"The only way to do great work is to love what you do.",
"Wake UP!",
"Make it COUNT!",
"You have no time to waste.",
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.3,
},
},
exit: {
opacity: 0,
scale: 0.95,
transition: {
duration: 0.8,
ease: "easeInOut",
},
},
};
const logoVariants = {
hidden: { opacity: 0, scale: 0.8, y: -20 },
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: {
type: "spring",
stiffness: 300,
damping: 25,
duration: 0.8,
},
},
};
const iconVariants = {
hidden: { opacity: 0, scale: 0 },
visible: {
opacity: 1,
scale: 1,
transition: {
type: "spring",
stiffness: 500,
damping: 30,
delay: 0.1,
},
},
};
const textContainerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.08,
delayChildren: 1.2, // Increased delay to let other animations complete first
},
},
};
const wordVariants = {
hidden: {
opacity: 0,
y: 20,
filter: "blur(4px)",
},
visible: {
opacity: 1,
y: 0,
filter: "blur(0px)",
transition: {
duration: 0.6,
ease: [0.25, 0.46, 0.45, 0.94], // Custom easing
},
},
};
const brandVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: {
delay: 0.8,
duration: 0.6,
ease: "easeOut",
},
},
};
export function IntroScreen({ onAnimationComplete }) {
const [currentQuote, setCurrentQuote] = useState("");
const [words, setWords] = useState([]);
useEffect(() => {
// Select a random quote on mount
const randomIndex = Math.floor(Math.random() * motivationalQuotes.length);
const selectedQuote = motivationalQuotes[randomIndex];
setCurrentQuote(selectedQuote);
setWords(selectedQuote.split(" "));
// Set timeout for the full screen to fade out
const timer = setTimeout(() => {
onAnimationComplete();
}, 1500); // Total duration: 1.5 seconds
return () => clearTimeout(timer);
}, [onAnimationComplete]);
return (
<motion.div
className="fixed inset-0 bg-gradient-to-br from-white via-gray-50 to-gray-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-700 flex flex-col items-center justify-center z-[100] overflow-hidden"
variants={containerVariants}
initial="hidden"
animate="visible"
exit="exit"
>
{/* Main Content */}
<div className="relative z-10 flex flex-col items-center space-y-8 px-8 max-w-4xl mx-auto">
{/* App Logo/Brand */}
<motion.div
variants={logoVariants}
className="flex flex-col items-center space-y-4 max-w-lg"
>
<div className="flex items-center gap-4">
<motion.div
variants={iconVariants}
className="p-4 bg-primary/10 rounded-2xl"
>
<Timer className="h-12 w-12 text-primary" />
</motion.div>
<motion.div
variants={iconVariants}
className="p-4 bg-primary/10 rounded-2xl"
style={{ animationDelay: "0.1s" }}
>
<CheckCircle className="h-12 w-12 text-primary" />
</motion.div>
<motion.div
variants={iconVariants}
className="p-4 bg-primary/10 rounded-2xl"
style={{ animationDelay: "0.2s" }}
>
<RotateCcw className="h-12 w-12 text-primary" />
</motion.div>
</div>
<motion.h1
variants={logoVariants}
className="text-5xl md:text-6xl font-extrabold text-gray-900 dark:text-gray-100 text-center tracking-tight"
>
Prio Space
</motion.h1>
<motion.div
variants={logoVariants}
className="text-lg font-bold text-primary uppercase tracking-wider"
>
Focus Track Achieve
</motion.div>
</motion.div>
{/* Motivational Quote */}
<motion.div
variants={textContainerVariants}
className="text-center"
initial="hidden"
animate="visible"
>
<div className="text-gray-700 text-center dark:text-gray-300 text-xl md:text-2xl font-medium leading-relaxed max-w-lg mx-auto">
{words.map((word, i) => (
<motion.span
key={`${word}-${i}`} // Better key to handle duplicate words
className="inline-block mr-2"
variants={wordVariants}
custom={i} // Pass index as custom prop if needed for more control
>
{word}
</motion.span>
))}
</div>
</motion.div>
</div>
</motion.div>
);
}
+1 -1
View File
@@ -143,7 +143,7 @@ export function SettingsModal({
variant="ghost"
size="sm"
onClick={onClose}
className="hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl p-2"
className="hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl p-2 dark:text-white"
>
<X className="h-5 w-5" />
</Button>
+47 -1
View File
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Calendar, Check, Clock, RotateCcw } from "lucide-react";
import { formatFocusTime } from "@/utils/time"; // Assuming this utility exists
@@ -15,12 +15,55 @@ export function TaskList({
const [holdingTask, setHoldingTask] = useState(null);
const [holdProgress, setHoldProgress] = useState(0);
// Audio refs for sound effects
const holdAudioRef = useRef(null);
const completeAudioRef = useRef(null);
// Initialize audio elements
if (typeof window !== "undefined") {
if (!holdAudioRef.current) {
holdAudioRef.current = new Audio("/music/hold.mp3");
holdAudioRef.current.volume = 0.5; // Adjust volume as needed
}
if (!completeAudioRef.current) {
completeAudioRef.current = new Audio("/music/complete.mp3");
completeAudioRef.current.volume = 0.3; // Adjust volume as needed
}
}
const playHoldSound = () => {
if (holdAudioRef.current) {
holdAudioRef.current.currentTime = 0; // Reset to beginning
holdAudioRef.current
.play()
.catch((e) => console.log("Hold sound play failed:", e));
}
};
const stopHoldSound = () => {
if (holdAudioRef.current) {
holdAudioRef.current.pause();
holdAudioRef.current.currentTime = 0;
}
};
const playCompleteSound = () => {
if (completeAudioRef.current) {
completeAudioRef.current.currentTime = 0; // Reset to beginning
completeAudioRef.current
.play()
.catch((e) => console.log("Complete sound play failed:", e));
}
};
const handleMouseDown = (taskId, event) => {
event.preventDefault();
const task = tasks.find((t) => t.id === taskId);
if (task?.completed) return;
setHoldingTask(taskId);
playHoldSound(); // Play hold sound when starting to hold
let progress = 0;
let holdCompleted = false;
let animationFrameId;
@@ -31,6 +74,8 @@ export function TaskList({
if (progress >= 100) {
cancelAnimationFrame(animationFrameId);
holdCompleted = true;
stopHoldSound(); // Stop hold sound
playCompleteSound(); // Play complete sound
onToggleTask(taskId);
setHoldingTask(null);
setHoldProgress(0);
@@ -43,6 +88,7 @@ export function TaskList({
const cleanup = () => {
cancelAnimationFrame(animationFrameId);
stopHoldSound(); // Stop hold sound if releasing early
setHoldingTask(null);
setHoldProgress(0);
event.target.holdCompleted = holdCompleted;
-1
View File
@@ -97,7 +97,6 @@ export function TaskOptionsModal({
onClose();
};
const currentTag = customTags.find((tag) => tag.id === task.tag);
const isDifferentDay =
task.createdAt.toDateString() !== currentActualDate.toDateString();
-1
View File
@@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

-1
View File
@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

-1
View File
@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

-1
View File
@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B