537 lines
17 KiB
JavaScript
537 lines
17 KiB
JavaScript
import { useState, useEffect } from 'react'
|
||
import Auth from './Auth'
|
||
import './App.css'
|
||
|
||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
|
||
|
||
const AVAILABLE_ICONS = [
|
||
'📝', '💼', '🏠', '🛒', '🎯', '💪', '📚', '✈️',
|
||
'🎨', '🎮', '🎵', '🎬', '📱', '💻', '⚽', '🏃',
|
||
'🍕', '☕', '🌟', '❤️', '🔥', '✨', '🌈', '🎉',
|
||
'📖', '✍️', '🎓', '💡', '🔧', '🏆', '🎪', '🎭'
|
||
]
|
||
|
||
function App() {
|
||
const [user, setUser] = useState(null)
|
||
const [token, setToken] = useState(null)
|
||
const [lists, setLists] = useState([])
|
||
const [selectedList, setSelectedList] = useState(null)
|
||
const [tasks, setTasks] = useState([])
|
||
const [newTask, setNewTask] = useState('')
|
||
const [newListName, setNewListName] = useState('')
|
||
const [selectedIcon, setSelectedIcon] = useState('📝')
|
||
const [showNewListForm, setShowNewListForm] = useState(false)
|
||
const [filter, setFilter] = useState('all')
|
||
const [loading, setLoading] = useState(false)
|
||
const [editingTaskId, setEditingTaskId] = useState(null)
|
||
const [editingTaskTitle, setEditingTaskTitle] = useState('')
|
||
const [confirmModal, setConfirmModal] = useState({ show: false, message: '', onConfirm: null })
|
||
const [darkMode, setDarkMode] = useState(false)
|
||
|
||
useEffect(() => {
|
||
// Check for stored token on mount
|
||
const storedToken = localStorage.getItem('token')
|
||
const storedUser = localStorage.getItem('user')
|
||
|
||
if (storedToken && storedUser) {
|
||
setToken(storedToken)
|
||
const userData = JSON.parse(storedUser)
|
||
setUser(userData)
|
||
// Load user's dark mode preference
|
||
const userDarkMode = localStorage.getItem(`darkMode_${userData.id}`)
|
||
setDarkMode(userDarkMode ? JSON.parse(userDarkMode) : false)
|
||
}
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
if (token) {
|
||
fetchLists()
|
||
}
|
||
}, [token])
|
||
|
||
useEffect(() => {
|
||
if (selectedList && token) {
|
||
fetchTasks(selectedList.id)
|
||
}
|
||
}, [selectedList, token])
|
||
|
||
useEffect(() => {
|
||
if (user) {
|
||
localStorage.setItem(`darkMode_${user.id}`, JSON.stringify(darkMode))
|
||
}
|
||
if (darkMode) {
|
||
document.body.classList.add('dark-mode')
|
||
} else {
|
||
document.body.classList.remove('dark-mode')
|
||
}
|
||
}, [darkMode, user])
|
||
|
||
const toggleDarkMode = () => {
|
||
setDarkMode(!darkMode)
|
||
}
|
||
|
||
const getAuthHeaders = () => ({
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`
|
||
})
|
||
|
||
const handleLogin = (userData, userToken) => {
|
||
setUser(userData)
|
||
setToken(userToken)
|
||
// Load user's dark mode preference
|
||
const userDarkMode = localStorage.getItem(`darkMode_${userData.id}`)
|
||
setDarkMode(userDarkMode ? JSON.parse(userDarkMode) : false)
|
||
}
|
||
|
||
const handleLogout = () => {
|
||
setConfirmModal({
|
||
show: true,
|
||
message: 'Are you sure you want to sign out?',
|
||
onConfirm: async () => {
|
||
try {
|
||
await fetch(`${API_URL}/logout`, {
|
||
method: 'POST',
|
||
headers: getAuthHeaders()
|
||
})
|
||
} catch (error) {
|
||
console.error('Logout error:', error)
|
||
}
|
||
|
||
localStorage.removeItem('token')
|
||
localStorage.removeItem('user')
|
||
setUser(null)
|
||
setToken(null)
|
||
setLists([])
|
||
setTasks([])
|
||
setSelectedList(null)
|
||
setConfirmModal({ show: false, message: '', onConfirm: null })
|
||
}
|
||
})
|
||
}
|
||
|
||
const fetchLists = async () => {
|
||
try {
|
||
const response = await fetch(`${API_URL}/lists`, {
|
||
headers: getAuthHeaders()
|
||
})
|
||
const data = await response.json()
|
||
setLists(data)
|
||
if (data.length > 0 && !selectedList) {
|
||
setSelectedList(data[0])
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching lists:', error)
|
||
}
|
||
}
|
||
|
||
const fetchTasks = async (listId) => {
|
||
try {
|
||
const response = await fetch(`${API_URL}/tasks?list_id=${listId}`, {
|
||
headers: getAuthHeaders()
|
||
})
|
||
const data = await response.json()
|
||
setTasks(data)
|
||
} catch (error) {
|
||
console.error('Error fetching tasks:', error)
|
||
}
|
||
}
|
||
|
||
const addList = async (e) => {
|
||
e.preventDefault()
|
||
if (!newListName.trim()) return
|
||
|
||
const colors = ['#667eea', '#f093fb', '#4facfe', '#43e97b', '#fa709a', '#feca57', '#ff6b6b', '#48dbfb']
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/lists`, {
|
||
method: 'POST',
|
||
headers: getAuthHeaders(),
|
||
body: JSON.stringify({
|
||
name: newListName,
|
||
icon: selectedIcon,
|
||
color: colors[Math.floor(Math.random() * colors.length)]
|
||
})
|
||
})
|
||
const data = await response.json()
|
||
setLists([...lists, data])
|
||
setNewListName('')
|
||
setSelectedIcon('📝')
|
||
setShowNewListForm(false)
|
||
setSelectedList(data)
|
||
} catch (error) {
|
||
console.error('Error adding list:', error)
|
||
}
|
||
}
|
||
|
||
const deleteList = async (listId) => {
|
||
setConfirmModal({
|
||
show: true,
|
||
message: 'Delete this list and all its tasks?',
|
||
onConfirm: async () => {
|
||
try {
|
||
await fetch(`${API_URL}/lists/${listId}`, {
|
||
method: 'DELETE',
|
||
headers: getAuthHeaders()
|
||
})
|
||
const updatedLists = lists.filter(list => list.id !== listId)
|
||
setLists(updatedLists)
|
||
if (selectedList?.id === listId) {
|
||
setSelectedList(updatedLists[0] || null)
|
||
setTasks([])
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting list:', error)
|
||
}
|
||
setConfirmModal({ show: false, message: '', onConfirm: null })
|
||
}
|
||
})
|
||
}
|
||
|
||
const addTask = async (e) => {
|
||
e.preventDefault()
|
||
if (!newTask.trim() || !selectedList) return
|
||
|
||
setLoading(true)
|
||
try {
|
||
const response = await fetch(`${API_URL}/tasks`, {
|
||
method: 'POST',
|
||
headers: getAuthHeaders(),
|
||
body: JSON.stringify({ title: newTask, list_id: selectedList.id })
|
||
})
|
||
const data = await response.json()
|
||
setTasks([...tasks, data])
|
||
setNewTask('')
|
||
} catch (error) {
|
||
console.error('Error adding task:', error)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const toggleTask = async (id, completed) => {
|
||
try {
|
||
const response = await fetch(`${API_URL}/tasks/${id}`, {
|
||
method: 'PUT',
|
||
headers: getAuthHeaders(),
|
||
body: JSON.stringify({ completed: !completed })
|
||
})
|
||
const data = await response.json()
|
||
setTasks(tasks.map(task => task.id === id ? data : task))
|
||
} catch (error) {
|
||
console.error('Error updating task:', error)
|
||
}
|
||
}
|
||
|
||
const deleteTask = async (id) => {
|
||
try {
|
||
await fetch(`${API_URL}/tasks/${id}`, {
|
||
method: 'DELETE',
|
||
headers: getAuthHeaders()
|
||
})
|
||
setTasks(tasks.filter(task => task.id !== id))
|
||
} catch (error) {
|
||
console.error('Error deleting task:', error)
|
||
}
|
||
}
|
||
|
||
const startEditTask = (task) => {
|
||
setEditingTaskId(task.id)
|
||
setEditingTaskTitle(task.title)
|
||
}
|
||
|
||
const saveEditTask = async (id) => {
|
||
if (!editingTaskTitle.trim()) return
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/tasks/${id}`, {
|
||
method: 'PUT',
|
||
headers: getAuthHeaders(),
|
||
body: JSON.stringify({ title: editingTaskTitle })
|
||
})
|
||
const data = await response.json()
|
||
setTasks(tasks.map(task => task.id === id ? data : task))
|
||
setEditingTaskId(null)
|
||
setEditingTaskTitle('')
|
||
} catch (error) {
|
||
console.error('Error updating task:', error)
|
||
}
|
||
}
|
||
|
||
const cancelEditTask = () => {
|
||
setEditingTaskId(null)
|
||
setEditingTaskTitle('')
|
||
}
|
||
|
||
const filteredTasks = tasks.filter(task => {
|
||
if (filter === 'active') return !task.completed
|
||
if (filter === 'completed') return task.completed
|
||
return true
|
||
})
|
||
|
||
const activeTasks = tasks.filter(t => !t.completed).length
|
||
|
||
if (!user || !token) {
|
||
return <Auth onLogin={handleLogin} />
|
||
}
|
||
|
||
return (
|
||
<div className={`app ${darkMode ? 'dark-mode' : ''}`}>
|
||
<div className="sidebar">
|
||
<div className="sidebar-header">
|
||
<div className="header-top">
|
||
<h2 className="sidebar-title">✓ Tasko</h2>
|
||
<button onClick={toggleDarkMode} className="theme-toggle" title={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}>
|
||
{darkMode ? '☀️' : '🌙'}
|
||
</button>
|
||
</div>
|
||
<div className="user-info">
|
||
<span className="username">Hello, {user.username}!</span>
|
||
<button onClick={handleLogout} className="logout-btn" title="Sign out">
|
||
<span className="logout-icon">→</span>
|
||
<span className="logout-text">Sign Out</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="lists-container">
|
||
{lists.map(list => (
|
||
<div
|
||
key={list.id}
|
||
className={`list-item ${selectedList?.id === list.id ? 'active' : ''}`}
|
||
onClick={() => setSelectedList(list)}
|
||
>
|
||
<div className="list-info">
|
||
<span className="list-icon" style={{ color: list.color }}>{list.icon}</span>
|
||
<span className="list-name">{list.name}</span>
|
||
</div>
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
deleteList(list.id)
|
||
}}
|
||
className="list-delete-btn"
|
||
title="Delete list"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{showNewListForm ? (
|
||
<form onSubmit={addList} className="new-list-form">
|
||
<input
|
||
type="text"
|
||
value={newListName}
|
||
onChange={(e) => setNewListName(e.target.value)}
|
||
placeholder="List name..."
|
||
className="new-list-input"
|
||
autoFocus
|
||
/>
|
||
|
||
<div className="icon-picker-section">
|
||
<label className="picker-label">Choose Icon:</label>
|
||
<div className="icon-grid">
|
||
{AVAILABLE_ICONS.map(icon => (
|
||
<button
|
||
key={icon}
|
||
type="button"
|
||
className={`icon-option ${selectedIcon === icon ? 'selected' : ''}`}
|
||
onClick={() => setSelectedIcon(icon)}
|
||
>
|
||
{icon}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="new-list-actions">
|
||
<button type="submit" className="new-list-save">
|
||
{selectedIcon} Save
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setShowNewListForm(false)
|
||
setNewListName('')
|
||
setSelectedIcon('📝')
|
||
}}
|
||
className="new-list-cancel"
|
||
>
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</form>
|
||
) : (
|
||
<button
|
||
onClick={() => setShowNewListForm(true)}
|
||
className="add-list-btn"
|
||
>
|
||
+ New List
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<div className="main-content">
|
||
{selectedList ? (
|
||
<>
|
||
<div className="content-header">
|
||
<h1 className="content-title">
|
||
<span style={{ color: selectedList.color }}>{selectedList.icon}</span>
|
||
{selectedList.name}
|
||
</h1>
|
||
</div>
|
||
|
||
<form onSubmit={addTask} className="task-form">
|
||
<input
|
||
type="text"
|
||
value={newTask}
|
||
onChange={(e) => setNewTask(e.target.value)}
|
||
placeholder="What needs to be done?"
|
||
className="task-input"
|
||
disabled={loading}
|
||
/>
|
||
<button type="submit" className="add-button" disabled={loading}>
|
||
{loading ? '...' : 'Add Task'}
|
||
</button>
|
||
</form>
|
||
|
||
<div className="filter-tabs">
|
||
<button
|
||
className={filter === 'all' ? 'active' : ''}
|
||
onClick={() => setFilter('all')}
|
||
>
|
||
All ({tasks.length})
|
||
</button>
|
||
<button
|
||
className={filter === 'active' ? 'active' : ''}
|
||
onClick={() => setFilter('active')}
|
||
>
|
||
Active ({activeTasks})
|
||
</button>
|
||
<button
|
||
className={filter === 'completed' ? 'active' : ''}
|
||
onClick={() => setFilter('completed')}
|
||
>
|
||
Completed ({tasks.length - activeTasks})
|
||
</button>
|
||
</div>
|
||
|
||
<div className="task-list">
|
||
{filteredTasks.length === 0 ? (
|
||
<p className="empty-state">
|
||
{filter === 'completed' ? 'No completed tasks yet' :
|
||
filter === 'active' ? 'No active tasks' :
|
||
'No tasks yet. Add one above!'}
|
||
</p>
|
||
) : (
|
||
filteredTasks.map(task => (
|
||
<div key={task.id} className={`task-item ${task.completed ? 'completed' : ''}`}>
|
||
<div className="task-content">
|
||
<input
|
||
type="checkbox"
|
||
checked={task.completed}
|
||
onChange={() => toggleTask(task.id, task.completed)}
|
||
className="task-checkbox"
|
||
/>
|
||
{editingTaskId === task.id ? (
|
||
<input
|
||
type="text"
|
||
value={editingTaskTitle}
|
||
onChange={(e) => setEditingTaskTitle(e.target.value)}
|
||
className="task-edit-input"
|
||
autoFocus
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter') saveEditTask(task.id)
|
||
if (e.key === 'Escape') cancelEditTask()
|
||
}}
|
||
/>
|
||
) : (
|
||
<span
|
||
className="task-title"
|
||
onDoubleClick={() => startEditTask(task)}
|
||
title="Double-click to edit"
|
||
>
|
||
{task.title}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className="task-actions">
|
||
{editingTaskId === task.id ? (
|
||
<>
|
||
<button
|
||
onClick={() => saveEditTask(task.id)}
|
||
className="save-button"
|
||
title="Save"
|
||
>
|
||
✓
|
||
</button>
|
||
<button
|
||
onClick={cancelEditTask}
|
||
className="cancel-button"
|
||
title="Cancel"
|
||
>
|
||
✕
|
||
</button>
|
||
</>
|
||
) : (
|
||
<>
|
||
<button
|
||
onClick={() => startEditTask(task)}
|
||
className="edit-button"
|
||
title="Edit task"
|
||
>
|
||
✎
|
||
</button>
|
||
<button
|
||
onClick={() => deleteTask(task.id)}
|
||
className="delete-button"
|
||
title="Delete task"
|
||
>
|
||
×
|
||
</button>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className="empty-state-main">
|
||
<h2>No lists yet</h2>
|
||
<p>Create a new list to get started!</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Confirmation Modal */}
|
||
{confirmModal.show && (
|
||
<div className="modal-overlay" onClick={() => setConfirmModal({ show: false, message: '', onConfirm: null })}>
|
||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||
<h3 className="modal-title">Confirm Action</h3>
|
||
<p className="modal-message">{confirmModal.message}</p>
|
||
<div className="modal-actions">
|
||
<button
|
||
className="modal-confirm-btn"
|
||
onClick={confirmModal.onConfirm}
|
||
>
|
||
Confirm
|
||
</button>
|
||
<button
|
||
className="modal-cancel-btn"
|
||
onClick={() => setConfirmModal({ show: false, message: '', onConfirm: null })}
|
||
>
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default App
|