Finalizing Version 1.0.0
@@ -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:
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
```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>
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 170 KiB |
@@ -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 }) {
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 +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 +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 |
|
After Width: | Height: | Size: 20 KiB |
@@ -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 +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 +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 |