tasko/frontend/src/App.jsx
2025-12-10 19:45:05 +02:00

537 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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