Spaces:
Running
Running
| <html lang="en" class="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>FocusFlow | Modern Todo App</title> | |
| <!-- Vue 3 CDN --> | |
| <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> | |
| <!-- Tailwind CSS CDN --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- FontAwesome Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <!-- Google Fonts: Inter --> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <!-- Tailwind Config for Custom Colors --> | |
| <script> | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| fontFamily: { | |
| sans: ['Inter', 'sans-serif'], | |
| }, | |
| colors: { | |
| gray: { | |
| 850: '#1f2937', | |
| 900: '#111827', | |
| 950: '#0b0f19', // Very dark background | |
| }, | |
| primary: { | |
| 500: '#6366f1', // Indigo | |
| 600: '#4f46e5', | |
| } | |
| }, | |
| animation: { | |
| 'fade-in': 'fadeIn 0.3s ease-out', | |
| 'slide-up': 'slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1)', | |
| }, | |
| keyframes: { | |
| fadeIn: { | |
| '0%': { opacity: '0' }, | |
| '100%': { opacity: '1' }, | |
| }, | |
| slideUp: { | |
| '0%': { transform: 'translateY(20px)', opacity: '0' }, | |
| '100%': { transform: 'translateY(0)', opacity: '1' }, | |
| } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| height: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #374151; | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #4b5563; | |
| } | |
| /* Checkbox Customization */ | |
| .custom-checkbox input:checked + div { | |
| background-color: #6366f1; | |
| border-color: #6366f1; | |
| } | |
| .custom-checkbox input:checked + div svg { | |
| display: block; | |
| } | |
| /* Glassmorphism utilities */ | |
| .glass { | |
| background: rgba(31, 41, 55, 0.7); | |
| backdrop-filter: blur(10px); | |
| -webkit-backdrop-filter: blur(10px); | |
| border-right: 1px solid rgba(255, 255, 255, 0.05); | |
| } | |
| /* Dragging State */ | |
| .dragging { | |
| opacity: 0.5; | |
| border: 2px dashed #6366f1; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 text-gray-800 dark:bg-gray-950 dark:text-gray-100 transition-colors duration-300 font-sans h-screen overflow-hidden"> | |
| <div id="app" class="flex h-full"> | |
| <!-- Sidebar --> | |
| <aside | |
| class="w-64 flex-shrink-0 flex flex-col justify-between glass transition-all duration-300 transform z-20" | |
| :class="isSidebarOpen ? 'translate-x-0' : '-translate-x-full absolute h-full'" | |
| > | |
| <!-- Logo Area --> | |
| <div class="p-6 flex items-center gap-3"> | |
| <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-500 to-purple-600 flex items-center justify-center shadow-lg shadow-primary-500/20"> | |
| <i class="fa-solid fa-check text-white text-xs"></i> | |
| </div> | |
| <h1 class="text-xl font-bold tracking-tight">FocusFlow</h1> | |
| </div> | |
| <!-- Navigation --> | |
| <nav class="flex-1 px-4 space-y-2 mt-4"> | |
| <p class="px-4 text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Main</p> | |
| <button | |
| @click="currentFilter = 'all'" | |
| :class="['w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200 group', currentFilter === 'all' ? 'bg-primary-600 text-white shadow-lg shadow-primary-500/20' : 'text-gray-500 hover:bg-gray-800 hover:text-gray-200']" | |
| > | |
| <i class="fa-solid fa-layer-group w-5 text-center transition-transform group-hover:scale-110"></i> | |
| <span class="font-medium">All Tasks</span> | |
| <span class="ml-auto bg-gray-700 text-xs py-0.5 px-2 rounded-full font-mono" v-if="todos.length > 0">{{ todos.length }}</span> | |
| </button> | |
| <button | |
| @click="currentFilter = 'active'" | |
| :class="['w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200 group', currentFilter === 'active' ? 'bg-primary-600 text-white shadow-lg shadow-primary-500/20' : 'text-gray-500 hover:bg-gray-800 hover:text-gray-200']" | |
| > | |
| <i class="fa-regular fa-circle w-5 text-center transition-transform group-hover:scale-110"></i> | |
| <span class="font-medium">Active</span> | |
| <span class="ml-auto bg-gray-700 text-xs py-0.5 px-2 rounded-full font-mono" v-if="activeCount > 0">{{ activeCount }}</span> | |
| </button> | |
| <button | |
| @click="currentFilter = 'completed'" | |
| :class="['w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200 group', currentFilter === 'completed' ? 'bg-primary-600 text-white shadow-lg shadow-primary-500/20' : 'text-gray-500 hover:bg-gray-800 hover:text-gray-200']" | |
| > | |
| <i class="fa-regular fa-circle-check w-5 text-center transition-transform group-hover:scale-110"></i> | |
| <span class="font-medium">Completed</span> | |
| <span class="ml-auto bg-gray-700 text-xs py-0.5 px-2 rounded-full font-mono" v-if="completedCount > 0">{{ completedCount }}</span> | |
| </button> | |
| </nav> | |
| <!-- Bottom Actions --> | |
| <div class="p-4 border-t border-gray-800"> | |
| <button | |
| @click="toggleDarkMode" | |
| class="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-gray-500 hover:text-white hover:bg-gray-800 transition-all duration-200" | |
| > | |
| <i :class="['fa-solid w-5 text-center', isDarkMode ? 'fa-sun' : 'fa-moon']"></i> | |
| <span class="font-medium">{{ isDarkMode ? 'Light Mode' : 'Dark Mode' }}</span> | |
| </button> | |
| <button | |
| @click="clearCompleted" | |
| class="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-gray-500 hover:text-red-400 hover:bg-gray-800 transition-all duration-200 mt-1" | |
| > | |
| <i class="fa-solid fa-trash-can w-5 text-center"></i> | |
| <span class="font-medium">Clear Completed</span> | |
| </button> | |
| </div> | |
| </aside> | |
| <!-- Main Content --> | |
| <main class="flex-1 flex flex-col relative h-full overflow-hidden"> | |
| <!-- Mobile Header --> | |
| <header class="h-16 flex items-center justify-between px-6 border-b border-gray-200 dark:border-gray-800 bg-white/50 dark:bg-gray-950/50 backdrop-blur-sm z-10"> | |
| <div class="flex items-center gap-4"> | |
| <button @click="isSidebarOpen = !isSidebarOpen" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"> | |
| <i class="fa-solid fa-bars text-lg"></i> | |
| </button> | |
| <div> | |
| <h2 class="font-bold text-lg">{{ pageTitle }}</h2> | |
| <p class="text-xs text-gray-500 dark:text-gray-400">{{ dateDisplay }}</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-3"> | |
| <div class="hidden sm:flex items-center gap-2 px-3 py-1.5 bg-gray-100 dark:bg-gray-900 rounded-full text-sm text-gray-500 border border-gray-200 dark:border-gray-800"> | |
| <i class="fa-solid fa-filter text-xs"></i> | |
| <span>Sorted by Date</span> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Scrollable Task Area --> | |
| <div class="flex-1 overflow-y-auto p-6 relative"> | |
| <!-- Empty State --> | |
| <div v-if="filteredTodos.length === 0" class="flex flex-col items-center justify-center h-full text-center opacity-60"> | |
| <div class="w-24 h-24 bg-gray-200 dark:bg-gray-800 rounded-full flex items-center justify-center mb-4 animate-fade-in"> | |
| <i class="fa-solid fa-clipboard-check text-4xl text-gray-400 dark:text-gray-600"></i> | |
| </div> | |
| <h3 class="text-xl font-medium text-gray-900 dark:text-white mb-2">No tasks found</h3> | |
| <p class="text-gray-500 dark:text-gray-400 max-w-xs">Looks like you're all caught up or have filtered out all tasks. Add a new one to get started!</p> | |
| </div> | |
| <!-- Task List --> | |
| <div | |
| v-for="(todo, index) in filteredTodos" | |
| :key="todo.id" | |
| draggable="true" | |
| @dragstart="dragStart(index)" | |
| @dragover.prevent | |
| @drop="dragDrop(index)" | |
| @dragenter="dragEnter(index)" | |
| @dragleave="dragLeave" | |
| class="group flex items-center p-4 mb-3 bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 shadow-sm hover:shadow-md hover:border-primary-500/30 dark:hover:border-primary-500/30 transition-all duration-300 animate-slide-up" | |
| :class="{'opacity-50 grayscale': todo.completed}" | |
| > | |
| <!-- Drag Handle (Desktop) --> | |
| <div class="cursor-move text-gray-300 dark:text-gray-700 mr-4 hidden sm:block"> | |
| <i class="fa-solid fa-grip-vertical"></i> | |
| </div> | |
| <!-- Custom Checkbox --> | |
| <label class="custom-checkbox relative flex items-center cursor-pointer"> | |
| <input type="checkbox" class="sr-only" v-model="todo.completed" @change="saveTodos"> | |
| <div class="w-6 h-6 border-2 border-gray-300 dark:border-gray-600 rounded-lg flex items-center justify-center transition-all duration-200"> | |
| <svg class="w-3 h-3 text-white hidden pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg> | |
| </div> | |
| </label> | |
| <!-- Text Content --> | |
| <div class="flex-1 min-w-0 ml-2"> | |
| <p | |
| class="text-base font-medium truncate transition-all duration-300" | |
| :class="todo.completed ? 'text-gray-400 line-through decoration-gray-400/50' : 'text-gray-800 dark:text-gray-100'" | |
| @dblclick="editTodo(todo)" | |
| > | |
| {{ todo.text }} | |
| </p> | |
| <div class="flex items-center gap-2 mt-1"> | |
| <span class="text-xs px-2 py-0.5 rounded-md bg-gray-100 dark:bg-gray-800 text-gray-500 font-medium"> | |
| {{ todo.tag }} | |
| </span> | |
| <span class="text-xs text-gray-400">{{ formatDate(todo.createdAt) }}</span> | |
| </div> | |
| </div> | |
| <!-- Actions --> | |
| <div class="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200"> | |
| <button | |
| @click="editTodo(todo)" | |
| class="w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:bg-blue-50 hover:text-blue-500 dark:hover:bg-blue-900/30 dark:hover:text-blue-400 transition-colors" | |
| title="Edit" | |
| > | |
| <i class="fa-solid fa-pen text-sm"></i> | |
| </button> | |
| <button | |
| @click="deleteTodo(todo.id)" | |
| class="w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-900/30 dark:hover:text-red-400 transition-colors" | |
| title="Delete" | |
| > | |
| <i class="fa-solid fa-trash text-sm"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Floating Input Area --> | |
| <div class="p-6 bg-white dark:bg-gray-950 border-t border-gray-200 dark:border-gray-800"> | |
| <form @submit.prevent="addTodo" class="relative flex items-center gap-3 max-w-4xl mx-auto"> | |
| <div class="relative flex-1 group"> | |
| <input | |
| v-model="newTodoText" | |
| type="text" | |
| placeholder="What needs to be done?" | |
| class="w-full pl-4 pr-12 py-4 bg-gray-100 dark:bg-gray-900 border-0 rounded-xl focus:ring-2 focus:ring-primary-500 focus:bg-white dark:focus:bg-gray-900 transition-all duration-200 text-gray-800 dark:text-white placeholder-gray-400" | |
| autocomplete="off" | |
| > | |
| <div class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 group-focus-within:text-primary-500 transition-colors"> | |
| <i class="fa-solid fa-arrow-up"></i> | |
| </div> | |
| </div> | |
| <button | |
| type="submit" | |
| class="px-6 py-4 bg-primary-600 hover:bg-primary-500 text-white rounded-xl shadow-lg shadow-primary-600/20 font-medium transition-all duration-200 active:scale-95 flex items-center gap-2" | |
| > | |
| <span>Add</span> | |
| <i class="fa-solid fa-plus"></i> | |
| </button> | |
| </form> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- Vue Application Logic --> | |
| <script> | |
| const { createApp, ref, computed, onMounted, onUnmounted } = Vue; | |
| createApp({ | |
| setup() { | |
| // --- State --- | |
| const todos = ref([]); | |
| const newTodoText = ref(''); | |
| const currentFilter = ref('all'); | |
| const isSidebarOpen = ref(true); // Default open on desktop | |
| const isDarkMode = ref(true); | |
| const dragIndex = ref(null); | |
| // --- Date & Time --- | |
| const dateDisplay = ref(''); | |
| const updateDate = () => { | |
| const now = new Date(); | |
| const options = { weekday: 'long', month: 'long', day: 'numeric' }; | |
| dateDisplay.value = now.toLocaleDateString('en-US', options); | |
| }; | |
| // --- Computed Properties --- | |
| const activeCount = computed(() => todos.value.filter(t => !t.completed).length); | |
| const completedCount = computed(() => todos.value.filter(t => t.completed).length); | |
| const filteredTodos = computed(() => { | |
| if (currentFilter.value === 'active') return todos.value.filter(t => !t.completed); | |
| if (currentFilter.value === 'completed') return todos.value.filter(t => t.completed); | |
| return todos.value; | |
| }); | |
| const pageTitle = computed(() => { | |
| if (currentFilter.value === 'all') return 'All Tasks'; | |
| if (currentFilter.value === 'active') return 'Active Tasks'; | |
| if (currentFilter.value === 'completed') return 'Completed Tasks'; | |
| return 'Tasks'; | |
| }); | |
| // --- Methods --- | |
| // Load from LocalStorage | |
| const loadTodos = () => { | |
| const saved = localStorage.getItem('focusflow_todos'); | |
| const savedTheme = localStorage.getItem('focusflow_theme'); | |
| if (saved) todos.value = JSON.parse(saved); | |
| else { | |
| // Initial Demo Data | |
| todos.value = [ | |
| { id: 1, text: 'Welcome to FocusFlow!', completed: false, tag: 'Welcome', createdAt: Date.now() - 100000 }, | |
| { id: 2, text: 'Try dragging the tasks to reorder them', completed: false, tag: 'Tip', createdAt: Date.now() - 50000 }, | |
| { id: 3, text: 'Double click text to edit', completed: true, tag: 'Tip', createdAt: Date.now() } | |
| ]; | |
| } | |
| if (savedTheme) { | |
| isDarkMode.value = savedTheme === 'dark'; | |
| } | |
| }; | |
| const saveTodos = () => { | |
| localStorage.setItem('focusflow_todos', JSON.stringify(todos.value)); | |
| }; | |
| const toggleDarkMode = () => { | |
| isDarkMode.value = !isDarkMode.value; | |
| const html = document.documentElement; | |
| if (isDarkMode.value) { | |
| html.classList.add('dark'); | |
| localStorage.setItem('focusflow_theme', 'dark'); | |
| } else { | |
| html.classList.remove('dark'); | |
| localStorage.setItem('focusflow_theme', 'light'); | |
| } | |
| }; | |
| const addTodo = () => { | |
| if (newTodoText.value.trim() === '') return; | |
| const tags = ['Personal', 'Work', 'Shopping', 'Health', 'Design']; | |
| const randomTag = tags[Math.floor(Math.random() * tags.length)]; | |
| todos.value.unshift({ | |
| id: Date.now(), | |
| text: newTodoText.value, | |
| completed: false, | |
| tag: randomTag, | |
| createdAt: Date.now() | |
| }); | |
| newTodoText.value = ''; | |
| saveTodos(); | |
| }; | |
| const deleteTodo = (id) => { | |
| todos.value = todos.value.filter(t => t.id !== id); | |
| saveTodos(); | |
| }; | |
| const clearCompleted = () => { | |
| todos.value = todos.value.filter(t => !t.completed); | |
| saveTodos(); | |
| }; | |
| // Edit Logic | |
| const editTodo = (todo) => { | |
| const newText = prompt('Edit task:', todo.text); | |
| if (newText !== null && newText.trim() !== '') { | |
| todo.text = newText.trim(); | |
| saveTodos(); | |
| } | |
| }; | |
| // Date Formatting | |
| const formatDate = (timestamp) => { | |
| const date = new Date(timestamp); | |
| const now = new Date(); | |
| const diff = now - date; | |
| const oneDay = 24 * 60 * 60 * 1000; | |
| if (diff < oneDay && now.getDate() === date.getDate()) { | |
| return 'Today'; | |
| } else if (diff < 2 * oneDay) { | |
| return 'Yesterday'; | |
| } else { | |
| return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); | |
| } | |
| }; | |
| // Drag and Drop Logic | |
| const dragStart = (index) => { | |
| dragIndex.value = index; | |
| }; | |
| const dragEnter = (index) => { | |
| if (dragIndex.value !== index) { | |
| const item = todos.value.splice(dragIndex.value, 1)[0]; | |
| todos.value.splice(index, 0, item); | |
| dragIndex.value = index; | |
| } | |
| }; | |
| const dragDrop = () => { | |
| saveTodos(); | |
| }; | |
| const dragLeave = () => { | |
| // Optional: handle drag leave visual state | |
| }; | |
| // --- Lifecycle --- | |
| onMounted(() => { | |
| loadTodos(); | |
| updateDate(); | |
| // Check system preference | |
| if (!localStorage.getItem('focusflow_theme')) { | |
| if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { | |
| isDarkMode.value = true; | |
| } | |
| } | |
| }); | |
| onUnmounted(() => { | |
| window.removeEventListener('resize', updateDate); // Cleanup if needed | |
| }); | |
| return { | |
| todos, | |
| newTodoText, | |
| currentFilter, | |
| isSidebarOpen, | |
| isDarkMode, | |
| dateDisplay, | |
| activeCount, | |
| completedCount, | |
| filteredTodos, | |
| pageTitle, | |
| addTodo, | |
| deleteTodo, | |
| clearCompleted, | |
| editTodo, | |
| formatDate, | |
| toggleDarkMode, | |
| dragStart, | |
| dragEnter, | |
| dragDrop, | |
| dragLeave | |
| }; | |
| } | |
| }).mount('#app'); | |
| </script> | |
| </body> | |
| </html> |