diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..45be71f --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "tabWidth": 4, + "semi": true, + "singleQuote": true, + "trailingComma": "es5" +} diff --git a/eslint.config.mjs b/eslint.config.mjs index c692093..302d0ef 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -10,4 +10,14 @@ export default [ pluginJs.configs.recommended, ...tseslint.configs.recommended, pluginReact.configs.flat.recommended, -]; \ No newline at end of file + { + rules: { + "@typescript-eslint/no-explicit-any": "off" + }, + settings: { + react: { + version: "18" + } + } + } +]; diff --git a/frontend/App.tsx b/frontend/App.tsx index 793c91e..75295c8 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -1,199 +1,245 @@ -import React, { useEffect, useState, Suspense, lazy } from "react"; -import { - Routes, - Route, - Navigate, - Outlet -} from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import Login from "./components/Login"; -import NotFound from "./components/Shared/NotFound"; -import ProjectDetails from "./components/Project/ProjectDetails"; -import Projects from "./components/Projects"; -import AreaDetails from "./components/Area/AreaDetails"; -import Areas from "./components/Areas"; -import TagDetails from "./components/Tag/TagDetails"; -import Tags from "./components/Tags"; -import Notes from "./components/Notes"; -import NoteDetails from "./components/Note/NoteDetails"; -import Calendar from "./components/Calendar"; -import ProfileSettings from "./components/Profile/ProfileSettings"; -import About from "./components/About"; -import Layout from "./Layout"; -import { User } from "./entities/User"; -import TasksToday from "./components/Task/TasksToday"; -import TaskView from "./components/Task/TaskView"; -import LoadingScreen from "./components/Shared/LoadingScreen"; -import InboxItems from "./components/Inbox/InboxItems"; +import React, { useEffect, useState, Suspense, lazy } from 'react'; +import { Routes, Route, Navigate, Outlet } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import Login from './components/Login'; +import NotFound from './components/Shared/NotFound'; +import ProjectDetails from './components/Project/ProjectDetails'; +import Projects from './components/Projects'; +import AreaDetails from './components/Area/AreaDetails'; +import Areas from './components/Areas'; +import TagDetails from './components/Tag/TagDetails'; +import Tags from './components/Tags'; +import Notes from './components/Notes'; +import NoteDetails from './components/Note/NoteDetails'; +import Calendar from './components/Calendar'; +import ProfileSettings from './components/Profile/ProfileSettings'; +import About from './components/About'; +import Layout from './Layout'; +import { User } from './entities/User'; +import TasksToday from './components/Task/TasksToday'; +import TaskView from './components/Task/TaskView'; +import LoadingScreen from './components/Shared/LoadingScreen'; +import InboxItems from './components/Inbox/InboxItems'; // Lazy load Tasks component to prevent issues with tags loading -const Tasks = lazy(() => import("./components/Tasks")); +const Tasks = lazy(() => import('./components/Tasks')); const App: React.FC = () => { - const { t, i18n } = useTranslation(); - - if (!i18n.isInitialized) { - return ; - } - - const [currentUser, setCurrentUser] = useState(null); - const [loading, setLoading] = useState(true); + const { i18n } = useTranslation(); - const fetchCurrentUser = async () => { - try { - const response = await fetch("/api/current_user", { - credentials: "include", - headers: { - Accept: "application/json", - }, - }); - - if (!response.ok) { - if (response.status === 401) { - setCurrentUser(null); - return; - } - throw new Error(`Failed to fetch user: ${response.status}`); - } - - const data = await response.json(); - if (data.user) { - setCurrentUser(data.user); - } else { - setCurrentUser(null); - } - } catch (err) { - setCurrentUser(null); - } finally { - setLoading(false); + if (!i18n.isInitialized) { + return ; } - }; - useEffect(() => { - // Fetch user on mount - fetchCurrentUser(); - }, []); + const [currentUser, setCurrentUser] = useState(null); + const [loading, setLoading] = useState(true); - // Listen for login events to update user state - useEffect(() => { - const handleUserLoggedIn = (event: CustomEvent) => { - const user = event.detail; - setCurrentUser(user); - }; - - window.addEventListener('userLoggedIn', handleUserLoggedIn as EventListener); - return () => window.removeEventListener('userLoggedIn', handleUserLoggedIn as EventListener); - }, []); + const fetchCurrentUser = async () => { + try { + const response = await fetch('/api/current_user', { + credentials: 'include', + headers: { + Accept: 'application/json', + }, + }); - useEffect(() => { - if (i18n.isInitialized) { - fetch(`/locales/${i18n.language}/translation.json`) - .then(response => { - return response.json(); - }) - .then(data => { - i18n.addResourceBundle(i18n.language, 'translation', data, true, true); - }) - .catch(error => { - console.error("Error manually fetching translation file:", error); - }); - } - }, [i18n.isInitialized]); - - const [isDarkMode, setIsDarkMode] = useState(() => { - const storedPreference = localStorage.getItem("isDarkMode"); - return storedPreference !== null - ? storedPreference === "true" - : window.matchMedia("(prefers-color-scheme: dark)").matches; - }); - - const toggleDarkMode = () => { - const newValue = !isDarkMode; - setIsDarkMode(newValue); - localStorage.setItem("isDarkMode", JSON.stringify(newValue)); - }; - - useEffect(() => { - const updateTheme = () => { - document.documentElement.classList.toggle("dark", isDarkMode); - }; - updateTheme(); - - const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); - const mediaListener = (e: MediaQueryListEvent) => { - if (!localStorage.getItem("isDarkMode")) { - setIsDarkMode(e.matches); - } - }; - mediaQuery.addEventListener("change", mediaListener); - return () => mediaQuery.removeEventListener("change", mediaListener); - }, [isDarkMode]); - - - const LoadingComponent = () => ( -
-
- {i18n.t('common.loading', 'Loading application... Please wait.')} -
-
- ); - - if (loading) { - return ; - } - - return ( - }> - - {currentUser ? ( - <> - - - - } - > - } /> - } /> - } /> - {i18n.t('common.loading', 'Loading...')}}> - - + if (!response.ok) { + if (response.status === 401) { + setCurrentUser(null); + return; } - /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - ) : ( - <> - } /> - } /> - } /> - - )} - - - ); + throw new Error(`Failed to fetch user: ${response.status}`); + } + + const data = await response.json(); + if (data.user) { + setCurrentUser(data.user); + } else { + setCurrentUser(null); + } + } catch { + setCurrentUser(null); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + // Fetch user on mount + fetchCurrentUser(); + }, []); + + // Listen for login events to update user state + useEffect(() => { + const handleUserLoggedIn = (event: CustomEvent) => { + const user = event.detail; + setCurrentUser(user); + }; + + window.addEventListener( + 'userLoggedIn', + handleUserLoggedIn as EventListener + ); + return () => + window.removeEventListener( + 'userLoggedIn', + handleUserLoggedIn as EventListener + ); + }, []); + + useEffect(() => { + if (i18n.isInitialized) { + fetch(`/locales/${i18n.language}/translation.json`) + .then((response) => { + return response.json(); + }) + .then((data) => { + i18n.addResourceBundle( + i18n.language, + 'translation', + data, + true, + true + ); + }) + .catch((error) => { + console.error( + 'Error manually fetching translation file:', + error + ); + }); + } + }, [i18n.isInitialized]); + + const [isDarkMode, setIsDarkMode] = useState(() => { + const storedPreference = localStorage.getItem('isDarkMode'); + return storedPreference !== null + ? storedPreference === 'true' + : window.matchMedia('(prefers-color-scheme: dark)').matches; + }); + + const toggleDarkMode = () => { + const newValue = !isDarkMode; + setIsDarkMode(newValue); + localStorage.setItem('isDarkMode', JSON.stringify(newValue)); + }; + + useEffect(() => { + const updateTheme = () => { + document.documentElement.classList.toggle('dark', isDarkMode); + }; + updateTheme(); + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const mediaListener = (e: MediaQueryListEvent) => { + if (!localStorage.getItem('isDarkMode')) { + setIsDarkMode(e.matches); + } + }; + mediaQuery.addEventListener('change', mediaListener); + return () => mediaQuery.removeEventListener('change', mediaListener); + }, [isDarkMode]); + + const LoadingComponent = () => ( +
+
+ {i18n.t( + 'common.loading', + 'Loading application... Please wait.' + )} +
+
+ ); + + if (loading) { + return ; + } + + return ( + }> + + {currentUser ? ( + <> + + + + } + > + } + /> + } /> + } /> + + {i18n.t( + 'common.loading', + 'Loading...' + )} + + } + > + + + } + /> + } /> + } /> + } + /> + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + + } + /> + } /> + } /> + + + ) : ( + <> + } /> + } + /> + } + /> + + )} + + + ); }; export default App; diff --git a/frontend/Layout.tsx b/frontend/Layout.tsx index ea996d7..8252e23 100644 --- a/frontend/Layout.tsx +++ b/frontend/Layout.tsx @@ -1,569 +1,570 @@ -import React, { useState, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { useToast } from "./components/Shared/ToastContext"; -import Navbar from "./components/Navbar"; -import Sidebar from "./components/Sidebar"; -import "./styles/tailwind.css"; -import ProjectModal from "./components/Project/ProjectModal"; -import NoteModal from "./components/Note/NoteModal"; -import AreaModal from "./components/Area/AreaModal"; -import TagModal from "./components/Tag/TagModal"; -import InboxModal from "./components/Inbox/InboxModal"; -import TaskModal from "./components/Task/TaskModal"; -import { Note } from "./entities/Note"; -import { Area } from "./entities/Area"; -import { Tag } from "./entities/Tag"; -import { Project } from "./entities/Project"; -import { Task } from "./entities/Task"; -import { User } from "./entities/User"; -import { useStore } from "./store/useStore"; -import { fetchNotes, createNote, updateNote } from "./utils/notesService"; -import { fetchAreas, createArea, updateArea } from "./utils/areasService"; -import { fetchTags, createTag, updateTag } from "./utils/tagsService"; -import { fetchProjects, createProject, updateProject } from "./utils/projectsService"; -import { createTask, updateTask } from "./utils/tasksService"; -import { isAuthError } from "./utils/authUtils"; +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useToast } from './components/Shared/ToastContext'; +import Navbar from './components/Navbar'; +import Sidebar from './components/Sidebar'; +import './styles/tailwind.css'; +import ProjectModal from './components/Project/ProjectModal'; +import NoteModal from './components/Note/NoteModal'; +import AreaModal from './components/Area/AreaModal'; +import TagModal from './components/Tag/TagModal'; +import InboxModal from './components/Inbox/InboxModal'; +import TaskModal from './components/Task/TaskModal'; +import { Note } from './entities/Note'; +import { Area } from './entities/Area'; +import { Tag } from './entities/Tag'; +import { Project } from './entities/Project'; +import { Task } from './entities/Task'; +import { User } from './entities/User'; +import { useStore } from './store/useStore'; +import { fetchNotes, createNote, updateNote } from './utils/notesService'; +import { fetchAreas, createArea, updateArea } from './utils/areasService'; +import { fetchTags, createTag, updateTag } from './utils/tagsService'; +import { + fetchProjects, + createProject, + updateProject, +} from './utils/projectsService'; +import { createTask, updateTask } from './utils/tasksService'; +import { isAuthError } from './utils/authUtils'; interface LayoutProps { - currentUser: User; - isDarkMode: boolean; - setCurrentUser: React.Dispatch>; - toggleDarkMode: () => void; - children: React.ReactNode; + currentUser: User; + isDarkMode: boolean; + setCurrentUser: React.Dispatch>; + toggleDarkMode: () => void; + children: React.ReactNode; } const Layout: React.FC = ({ - currentUser, - setCurrentUser, - isDarkMode, - toggleDarkMode, - children, + currentUser, + setCurrentUser, + isDarkMode, + toggleDarkMode, + children, }) => { - const { t } = useTranslation(); - const { showSuccessToast } = useToast(); - const [isSidebarOpen, setIsSidebarOpen] = useState(window.innerWidth >= 1024); - const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); - const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); - const [isNoteModalOpen, setIsNoteModalOpen] = useState(false); - const [isAreaModalOpen, setIsAreaModalOpen] = useState(false); - const [isTagModalOpen, setIsTagModalOpen] = useState(false); - const [taskModalType, setTaskModalType] = useState<'simplified' | 'full'>('simplified'); + const { t } = useTranslation(); + const { showSuccessToast } = useToast(); + const [isSidebarOpen, setIsSidebarOpen] = useState( + window.innerWidth >= 1024 + ); + const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); + const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); + const [isNoteModalOpen, setIsNoteModalOpen] = useState(false); + const [isAreaModalOpen, setIsAreaModalOpen] = useState(false); + const [isTagModalOpen, setIsTagModalOpen] = useState(false); + const [taskModalType, setTaskModalType] = useState<'simplified' | 'full'>( + 'simplified' + ); - const [selectedNote, setSelectedNote] = useState(null); - const [selectedArea, setSelectedArea] = useState(null); - const [selectedTag, setSelectedTag] = useState(null); - const [newTask, setNewTask] = useState(null); + const [selectedNote, setSelectedNote] = useState(null); + const [selectedArea, setSelectedArea] = useState(null); + const [selectedTag, setSelectedTag] = useState(null); - const { - notesStore: { - notes, - setNotes, - setLoading: setNotesLoading, - setError: setNotesError, - isLoading: isNotesLoading, - isError: isNotesError, - }, - areasStore: { - areas, - setAreas, - setLoading: setAreasLoading, - setError: setAreasError, - isLoading: isAreasLoading, - isError: isAreasError, - }, - tasksStore: { - setLoading: setTasksLoading, - setError: setTasksError, - isLoading: isTasksLoading, - isError: isTasksError, - }, - projectsStore: { - projects, - setProjects, - setLoading: setProjectsLoading, - setError: setProjectsError, - isLoading: isProjectsLoading, - isError: isProjectsError, - }, - tagsStore: { - tags, - setTags, - setLoading: setTagsLoading, - setError: setTagsError, - isLoading: isTagsLoading, - isError: isTagsError, - }, - } = useStore(); + const { + notesStore: { + notes, + setNotes, + setLoading: setNotesLoading, + setError: setNotesError, + isLoading: isNotesLoading, + isError: isNotesError, + }, + areasStore: { + areas, + setAreas, + setLoading: setAreasLoading, + setError: setAreasError, + isLoading: isAreasLoading, + isError: isAreasError, + }, + tasksStore: { isLoading: isTasksLoading, isError: isTasksError }, + projectsStore: { + projects, + setProjects, + setLoading: setProjectsLoading, + setError: setProjectsError, + isLoading: isProjectsLoading, + isError: isProjectsError, + }, + tagsStore: { + tags, + setTags, + setLoading: setTagsLoading, + setError: setTagsError, + isLoading: isTagsLoading, + isError: isTagsError, + }, + } = useStore(); - const openTaskModal = (type: 'simplified' | 'full' = 'simplified') => { - setIsTaskModalOpen(true); - setTaskModalType(type); - }; - - - - useEffect(() => { - const handleResize = () => { - setIsSidebarOpen(window.innerWidth >= 1024); + const openTaskModal = (type: 'simplified' | 'full' = 'simplified') => { + setIsTaskModalOpen(true); + setTaskModalType(type); }; - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); - }, []); - const loadNotes = async () => { - setNotesLoading(true); - try { - const notesData = await fetchNotes(); - setNotes(notesData); - } catch (error) { - console.error("Error fetching notes:", error); - setNotesError(true); - } finally { - setNotesLoading(false); - } - }; + useEffect(() => { + const handleResize = () => { + setIsSidebarOpen(window.innerWidth >= 1024); + }; + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); - const loadAreas = async () => { - setAreasLoading(true); - try { - const areasData = await fetchAreas(); - setAreas(areasData); - } catch (error) { - console.error("Error fetching areas:", error); - setAreasError(true); - } finally { - setAreasLoading(false); - } - }; + const loadNotes = async () => { + setNotesLoading(true); + try { + const notesData = await fetchNotes(); + setNotes(notesData); + } catch (error) { + console.error('Error fetching notes:', error); + setNotesError(true); + } finally { + setNotesLoading(false); + } + }; - const loadTags = async () => { - setTagsLoading(true); - try { - const tagsData = await fetchTags(); - setTags(tagsData); - } catch (error) { - console.error("Error fetching tags:", error); - setTagsError(true); - } finally { - setTagsLoading(false); - } - }; + const loadAreas = async () => { + setAreasLoading(true); + try { + const areasData = await fetchAreas(); + setAreas(areasData); + } catch (error) { + console.error('Error fetching areas:', error); + setAreasError(true); + } finally { + setAreasLoading(false); + } + }; - const loadProjects = async () => { - setProjectsLoading(true); - try { - const projectsData = await fetchProjects(); - setProjects(projectsData); - } catch (error) { - console.error("Error fetching projects:", error); - setProjectsError(true); - } finally { - setProjectsLoading(false); - } - }; + const loadTags = async () => { + setTagsLoading(true); + try { + const tagsData = await fetchTags(); + setTags(tagsData); + } catch (error) { + console.error('Error fetching tags:', error); + setTagsError(true); + } finally { + setTagsLoading(false); + } + }; - useEffect(() => { - loadNotes(); - loadAreas(); - loadTags(); - loadProjects(); - }, []); + const loadProjects = async () => { + setProjectsLoading(true); + try { + const projectsData = await fetchProjects(); + setProjects(projectsData); + } catch (error) { + console.error('Error fetching projects:', error); + setProjectsError(true); + } finally { + setProjectsLoading(false); + } + }; - const openNoteModal = (note: Note | null = null) => { - setSelectedNote(note); - setIsNoteModalOpen(true); - }; + useEffect(() => { + loadNotes(); + loadAreas(); + loadTags(); + loadProjects(); + }, []); - const closeNoteModal = () => { - setIsNoteModalOpen(false); - setSelectedNote(null); - }; + const openNoteModal = (note: Note | null = null) => { + setSelectedNote(note); + setIsNoteModalOpen(true); + }; - const closeTaskModal = () => { - setIsTaskModalOpen(false); - setNewTask(null); - }; + const closeNoteModal = () => { + setIsNoteModalOpen(false); + setSelectedNote(null); + }; - const openProjectModal = () => { - setIsProjectModalOpen(true); - }; + const closeTaskModal = () => { + setIsTaskModalOpen(false); + }; - const closeProjectModal = () => { - setIsProjectModalOpen(false); - }; + const openProjectModal = () => { + setIsProjectModalOpen(true); + }; - const openAreaModal = (area: Area | null = null) => { - setSelectedArea(area); - setIsAreaModalOpen(true); - }; + const closeProjectModal = () => { + setIsProjectModalOpen(false); + }; - const closeAreaModal = () => { - setIsAreaModalOpen(false); - setSelectedArea(null); - }; + const openAreaModal = (area: Area | null = null) => { + setSelectedArea(area); + setIsAreaModalOpen(true); + }; - const openTagModal = (tag: Tag | null = null) => { - setSelectedTag(tag); - setIsTagModalOpen(true); - }; + const closeAreaModal = () => { + setIsAreaModalOpen(false); + setSelectedArea(null); + }; - const closeTagModal = () => { - setIsTagModalOpen(false); - setSelectedTag(null); - }; + const openTagModal = (tag: Tag | null = null) => { + setSelectedTag(tag); + setIsTagModalOpen(true); + }; - const handleSaveNote = async (noteData: Note) => { - try { - if (noteData.id) { - await updateNote(noteData.id, noteData); - } else { - await createNote(noteData); - } - loadNotes(); - closeNoteModal(); - } catch (error: any) { - console.error("Error saving note:", error); - // Don't close modal if there's an auth error (user will be redirected) - if (isAuthError(error)) { - return; - } - closeNoteModal(); - } - }; + const closeTagModal = () => { + setIsTagModalOpen(false); + setSelectedTag(null); + }; - const handleSaveTask = async (taskData: Task) => { - try { - if (taskData.id) { - await updateTask(taskData.id, taskData); - const taskLink = ( - - {t('task.updated', 'Task')} {taskData.name} {t('task.updatedSuccessfully', 'updated successfully!')} - - ); - showSuccessToast(taskLink); - } else { - const createdTask = await createTask(taskData); - const taskLink = ( - - {t('task.created', 'Task')} {createdTask.name} {t('task.createdSuccessfully', 'created successfully!')} - - ); - showSuccessToast(taskLink); - } - // Don't refetch all tasks here - let individual components handle their own state - // This prevents unnecessary re-renders and race conditions - closeTaskModal(); - } catch (error: any) { - console.error("Error saving task:", error); - // Don't close modal if there's an auth error (user will be redirected) - if (isAuthError(error)) { - return; - } - // For other errors, still close the modal but let the error bubble up - closeTaskModal(); - throw error; - } - }; - - const handleCreateProject = async (name: string): Promise => { - try { - const newProject = await createProject({ - name, - active: true, - }); - return newProject; - } catch (error) { - console.error("Error creating project:", error); - throw error; - } - }; - - const handleSaveProject = async (projectData: Project) => { - try { - if (projectData.id) { - await updateProject(projectData.id, projectData); - } else { - await createProject(projectData); - } - const projectsData = await fetchProjects(); - setProjects(projectsData); - closeProjectModal(); - } catch (error: any) { - console.error("Error saving project:", error); - // Don't close modal if there's an auth error (user will be redirected) - if (isAuthError(error)) { - return; - } - closeProjectModal(); - } - }; - - - const handleSaveArea = async (areaData: Partial) => { - try { - if (areaData.id) { - await updateArea(areaData.id, areaData); - } else { - await createArea(areaData); - } - loadAreas(); - closeAreaModal(); - } catch (error: any) { - console.error("Error saving area:", error); - // Don't close modal if there's an auth error (user will be redirected) - if (isAuthError(error)) { - return; - } - closeAreaModal(); - } - }; - - const handleSaveTag = async (tagData: Tag) => { - try { - if (tagData.id) { - await updateTag(tagData.id, tagData); - } else { - await createTag(tagData); - } - const tagsData = await fetchTags(); - setTags(tagsData); - closeTagModal(); - } catch (error: any) { - console.error("Error saving tag:", error); - // Don't close modal if there's an auth error (user will be redirected) - if (isAuthError(error)) { - return; - } - closeTagModal(); - } - }; - - const handleLogout = async () => { - try { - const response = await fetch('/api/logout', { - method: 'GET', - credentials: 'include', - }); - - if (response.ok) { - setCurrentUser(null); - } else { - console.error('Logout failed:', await response.json()); - } - } catch (error) { - console.error('Error during logout:', error); - } - }; - - const mainContentMarginLeft = isSidebarOpen ? "ml-72" : "ml-0"; - - const isLoading = - isNotesLoading || - isAreasLoading || - isTasksLoading || - isProjectsLoading || - isTagsLoading; - const isError = - isNotesError || - isAreasError || - isTasksError || - isProjectsError || - isTagsError; - - if (isLoading) { - return ( -
- - -
-
- {t('common.loading')} -
-
-
- ); - } - - if (isError) { - return ( -
- - -
-
{t('errors.somethingWentWrong')}
-
-
- ); - } - - return ( -
- - - -
-
-
-
{children}
-
-
-
- - - {isTaskModalOpen && ( - taskModalType === 'simplified' ? ( - - ) : ( - {}} - projects={projects} - onCreateProject={handleCreateProject} - /> - ) - )} - - {isProjectModalOpen && ( - { - try { - const { deleteProject } = await import('./utils/projectsService'); - await deleteProject(projectId); - loadProjects(); - closeProjectModal(); - } catch (error) { - console.error('Error deleting project:', error); + const handleSaveNote = async (noteData: Note) => { + try { + if (noteData.id) { + await updateNote(noteData.id, noteData); + } else { + await createNote(noteData); } - }} - areas={areas} - /> - )} - - {isNoteModalOpen && ( - { - try { - const { deleteNote } = await import('./utils/notesService'); - await deleteNote(noteId); - loadNotes(); - closeNoteModal(); - } catch (error) { - console.error('Error deleting note:', error); + loadNotes(); + closeNoteModal(); + } catch (error: any) { + console.error('Error saving note:', error); + // Don't close modal if there's an auth error (user will be redirected) + if (isAuthError(error)) { + return; } - }} - note={selectedNote} - projects={projects} - onCreateProject={handleCreateProject} - /> - )} + closeNoteModal(); + } + }; - {isAreaModalOpen && ( - - )} + const handleSaveTask = async (taskData: Task) => { + try { + if (taskData.id) { + await updateTask(taskData.id, taskData); + const taskLink = ( + + {t('task.updated', 'Task')}{' '} + + {taskData.name} + {' '} + {t('task.updatedSuccessfully', 'updated successfully!')} + + ); + showSuccessToast(taskLink); + } else { + const createdTask = await createTask(taskData); + const taskLink = ( + + {t('task.created', 'Task')}{' '} + + {createdTask.name} + {' '} + {t('task.createdSuccessfully', 'created successfully!')} + + ); + showSuccessToast(taskLink); + } + // Don't refetch all tasks here - let individual components handle their own state + // This prevents unnecessary re-renders and race conditions + closeTaskModal(); + } catch (error: any) { + console.error('Error saving task:', error); + // Don't close modal if there's an auth error (user will be redirected) + if (isAuthError(error)) { + return; + } + // For other errors, still close the modal but let the error bubble up + closeTaskModal(); + throw error; + } + }; - {isTagModalOpen && ( - - )} -
- ); + const handleCreateProject = async (name: string): Promise => { + try { + const newProject = await createProject({ + name, + active: true, + }); + return newProject; + } catch (error) { + console.error('Error creating project:', error); + throw error; + } + }; + + const handleSaveProject = async (projectData: Project) => { + try { + if (projectData.id) { + await updateProject(projectData.id, projectData); + } else { + await createProject(projectData); + } + const projectsData = await fetchProjects(); + setProjects(projectsData); + closeProjectModal(); + } catch (error: any) { + console.error('Error saving project:', error); + // Don't close modal if there's an auth error (user will be redirected) + if (isAuthError(error)) { + return; + } + closeProjectModal(); + } + }; + + const handleSaveArea = async (areaData: Partial) => { + try { + if (areaData.id) { + await updateArea(areaData.id, areaData); + } else { + await createArea(areaData); + } + loadAreas(); + closeAreaModal(); + } catch (error: any) { + console.error('Error saving area:', error); + // Don't close modal if there's an auth error (user will be redirected) + if (isAuthError(error)) { + return; + } + closeAreaModal(); + } + }; + + const handleSaveTag = async (tagData: Tag) => { + try { + if (tagData.id) { + await updateTag(tagData.id, tagData); + } else { + await createTag(tagData); + } + const tagsData = await fetchTags(); + setTags(tagsData); + closeTagModal(); + } catch (error: any) { + console.error('Error saving tag:', error); + // Don't close modal if there's an auth error (user will be redirected) + if (isAuthError(error)) { + return; + } + closeTagModal(); + } + }; + + const mainContentMarginLeft = isSidebarOpen ? 'ml-72' : 'ml-0'; + + const isLoading = + isNotesLoading || + isAreasLoading || + isTasksLoading || + isProjectsLoading || + isTagsLoading; + const isError = + isNotesError || + isAreasError || + isTasksError || + isProjectsError || + isTagsError; + + if (isLoading) { + return ( +
+ + +
+
+ {t('common.loading')} +
+
+
+ ); + } + + if (isError) { + return ( +
+ + +
+
+ {t('errors.somethingWentWrong')} +
+
+
+ ); + } + + return ( +
+ + + +
+
+
+
+ {children} +
+
+
+
+ + {isTaskModalOpen && + (taskModalType === 'simplified' ? ( + + ) : ( + {}} + projects={projects} + onCreateProject={handleCreateProject} + /> + ))} + + {isProjectModalOpen && ( + { + try { + const { deleteProject } = await import( + './utils/projectsService' + ); + await deleteProject(projectId); + loadProjects(); + closeProjectModal(); + } catch (error) { + console.error('Error deleting project:', error); + } + }} + areas={areas} + /> + )} + + {isNoteModalOpen && ( + { + try { + const { deleteNote } = await import( + './utils/notesService' + ); + await deleteNote(noteId); + loadNotes(); + closeNoteModal(); + } catch (error) { + console.error('Error deleting note:', error); + } + }} + note={selectedNote} + projects={projects} + onCreateProject={handleCreateProject} + /> + )} + + {isAreaModalOpen && ( + + )} + + {isTagModalOpen && ( + + )} +
+ ); }; export default Layout; diff --git a/frontend/components/About.tsx b/frontend/components/About.tsx index 2a1e920..7cda8f0 100644 --- a/frontend/components/About.tsx +++ b/frontend/components/About.tsx @@ -3,188 +3,242 @@ import { useTranslation } from 'react-i18next'; import { HeartIcon, InformationCircleIcon } from '@heroicons/react/24/outline'; const About: React.FC = () => { - const { t } = useTranslation(); - const [version, setVersion] = useState("0.3"); + const { t } = useTranslation(); + const [version, setVersion] = useState('0.3'); - useEffect(() => { - // Fetch version from the deployed app - fetch('/api/version') - .then(response => response.json()) - .then(data => { - if (data.version) { - setVersion(data.version); - } - }) - .catch(error => { - console.error('Error fetching version:', error); - // Keep default version if fetch fails - }); - }, []); + useEffect(() => { + // Fetch version from the deployed app + fetch('/api/version') + .then((response) => response.json()) + .then((data) => { + if (data.version) { + setVersion(data.version); + } + }) + .catch((error) => { + console.error('Error fetching version:', error); + // Keep default version if fetch fails + }); + }, []); - return ( -
-
-
- -

{t('about.title', 'About')}

-
+ return ( +
+
+
+ +

+ {t('about.title', 'About')} +

+
-
- {/* Logo and Version */} -
-

- tududi -

-

- {t('about.version', 'Version')} {version} -

-
+
+ {/* Logo and Version */} +
+

+ tududi +

+

+ {t('about.version', 'Version')} {version} +

+
- {/* Description */} -
-

- {t('about.description', 'Self-hosted task management with hierarchical organization, multi-language support, and Telegram integration. Built with love for productivity enthusiasts.')} -

-
+ {/* Description */} +
+

+ {t( + 'about.description', + 'Self-hosted task management with hierarchical organization, multi-language support, and Telegram integration. Built with love for productivity enthusiasts.' + )} +

+
- {/* Appreciation */} -
-
- - - {t('about.madeWithLove', 'Made with love')} - -
-

- {t('about.appreciation', 'Thank you for using tududi! Your support helps keep this project alive and growing. If you find it useful, consider supporting the development.')} -

-
+ {/* Appreciation */} +
+
+ + + {t('about.madeWithLove', 'Made with love')} + +
+

+ {t( + 'about.appreciation', + 'Thank you for using tududi! Your support helps keep this project alive and growing. If you find it useful, consider supporting the development.' + )} +

+
- {/* Support Links */} -
-

- {t('about.supportDevelopment', 'Support Development')} -

- -
+ {/* Support Links */} +
+

+ {t( + 'about.supportDevelopment', + 'Support Development' + )} +

+ +
- {/* Community Links */} -
-

- {t('about.community', 'Community')} -

- -
+ {/* Community Links */} +
+

+ {t('about.community', 'Community')} +

+ +
- {/* Links */} -
-
- - {t('about.viewOnGitHub', 'View on GitHub')} - - - - - - {t('about.license', 'Licensed for personal use')} - -
-
+ {/* Links */} +
+
+ + {t('about.viewOnGitHub', 'View on GitHub')} + + + + + + {t( + 'about.license', + 'Licensed for personal use' + )} + +
+
- {/* Footer */} -
-

- {t('about.builtBy', 'Built by')} Chris Veleris -

+ {/* Footer */} +
+

+ {t('about.builtBy', 'Built by')}{' '} + + Chris Veleris + +

+
+
-
-
- ); + ); }; -export default About; \ No newline at end of file +export default About; diff --git a/frontend/components/Area/AreaDetails.tsx b/frontend/components/Area/AreaDetails.tsx index e377cc7..9df4b10 100644 --- a/frontend/components/Area/AreaDetails.tsx +++ b/frontend/components/Area/AreaDetails.tsx @@ -1,63 +1,65 @@ import React, { useEffect, useState } from 'react'; import { useParams, Link } from 'react-router-dom'; -import { useStore } from '../../store/useStore'; +import { useStore } from '../../store/useStore'; import { Area } from '../../entities/Area'; import { useTranslation } from 'react-i18next'; const AreaDetails: React.FC = () => { - const { t } = useTranslation(); - const { id } = useParams<{ id: string }>(); - const { areas } = useStore((state) => state.areasStore); - const [area, setArea] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isError, setIsError] = useState(false); + const { t } = useTranslation(); + const { id } = useParams<{ id: string }>(); + const { areas } = useStore((state) => state.areasStore); + const [area, setArea] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isError, setIsError] = useState(false); - useEffect(() => { - if (!areas.length) setIsLoading(true); - const foundArea = areas.find((a: Area) => a.id === Number(id)); - setArea(foundArea || null); - if (!foundArea) { - setIsError(true); + useEffect(() => { + if (!areas.length) setIsLoading(true); + const foundArea = areas.find((a: Area) => a.id === Number(id)); + setArea(foundArea || null); + if (!foundArea) { + setIsError(true); + } + setIsLoading(false); + }, [id, areas]); + + if (isLoading) { + return ( +
+
+ {t('areas.loading')} +
+
+ ); + } + + if (isError || !area) { + return ( +
+
+ {isError ? t('areas.error') : t('areas.notFound')} +
+
+ ); } - setIsLoading(false); - }, [id, areas]); - if (isLoading) { return ( -
-
- {t('areas.loading')} +
+
+

+ {t('areas.details')}: {area?.name} +

+

+ {area?.description} +

+ + {t('areas.viewProjects', { name: area?.name })} + +
-
); - } - - if (isError || !area) { - return ( -
-
- {isError ? t('areas.error') : t('areas.notFound')} -
-
- ); - } - - return ( -
-
-

- {t('areas.details')}: {area?.name} -

-

{area?.description}

- - {t('areas.viewProjects', { name: area?.name })} - -
-
- ); }; -export default AreaDetails; \ No newline at end of file +export default AreaDetails; diff --git a/frontend/components/Area/AreaModal.tsx b/frontend/components/Area/AreaModal.tsx index 8ecafbd..8205271 100644 --- a/frontend/components/Area/AreaModal.tsx +++ b/frontend/components/Area/AreaModal.tsx @@ -5,225 +5,260 @@ import { useTranslation } from 'react-i18next'; import { TrashIcon } from '@heroicons/react/24/outline'; interface AreaModalProps { - isOpen: boolean; - onClose: () => void; - onSave: (areaData: Partial) => Promise; - onDelete?: (areaId: number) => Promise; - area?: Area | null; + isOpen: boolean; + onClose: () => void; + onSave: (areaData: Partial) => Promise; + onDelete?: (areaId: number) => Promise; + area?: Area | null; } -const AreaModal: React.FC = ({ isOpen, onClose, area, onSave, onDelete }) => { - const { t } = useTranslation(); - const [formData, setFormData] = useState({ - id: area?.id || 0, - name: area?.name || '', - description: area?.description || '', - }); - - const [error, setError] = useState(null); - const modalRef = useRef(null); - const [isSubmitting, setIsSubmitting] = useState(false); - const [isClosing, setIsClosing] = useState(false); - - const { showSuccessToast, showErrorToast } = useToast(); - - useEffect(() => { - if (isOpen) { - setFormData({ +const AreaModal: React.FC = ({ + isOpen, + onClose, + area, + onSave, + onDelete, +}) => { + const { t } = useTranslation(); + const [formData, setFormData] = useState({ id: area?.id || 0, name: area?.name || '', description: area?.description || '', - }); - setError(null); - } - }, [isOpen, area]); + }); - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - modalRef.current && - !modalRef.current.contains(event.target as Node) - ) { - handleClose(); - } + const [error, setError] = useState(null); + const modalRef = useRef(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isClosing, setIsClosing] = useState(false); + + const { showSuccessToast, showErrorToast } = useToast(); + + useEffect(() => { + if (isOpen) { + setFormData({ + id: area?.id || 0, + name: area?.name || '', + description: area?.description || '', + }); + setError(null); + } + }, [isOpen, area]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + modalRef.current && + !modalRef.current.contains(event.target as Node) + ) { + handleClose(); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + handleClose(); + } + }; + + if (isOpen) { + document.addEventListener('keydown', handleKeyDown); + } + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [isOpen]); + + const handleChange = ( + e: React.ChangeEvent + ) => { + const { name, value } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: value, + })); }; - if (isOpen) { - document.addEventListener('mousedown', handleClickOutside); - } - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [isOpen]); + const handleSubmit = async () => { + if (!formData.name.trim()) { + setError(t('errors.areaNameRequired')); + return; + } - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - handleClose(); - } + setIsSubmitting(true); + setError(null); + + try { + await onSave(formData); + showSuccessToast( + formData.id + ? t('success.areaUpdated') + : t('success.areaCreated') + ); + handleClose(); + } catch (err) { + setError((err as Error).message); + showErrorToast(t('errors.failedToSaveArea')); + } finally { + setIsSubmitting(false); + } }; - if (isOpen) { - document.addEventListener('keydown', handleKeyDown); - } - return () => { - document.removeEventListener('keydown', handleKeyDown); + const handleClose = () => { + setIsClosing(true); + setTimeout(() => { + onClose(); + setIsClosing(false); + }, 300); }; - }, [isOpen]); - const handleChange = ( - e: React.ChangeEvent - ) => { - const { name, value } = e.target; - setFormData((prev) => ({ - ...prev, - [name]: value, - })); - }; + const handleDeleteArea = async () => { + if (formData.id && formData.id !== 0 && onDelete) { + try { + await onDelete(formData.id); + showSuccessToast( + t('success.areaDeleted', 'Area deleted successfully!') + ); + handleClose(); + } catch (err) { + setError((err as Error).message); + showErrorToast( + t('errors.failedToDeleteArea', 'Failed to delete area.') + ); + } + } + }; - const handleSubmit = async () => { - if (!formData.name.trim()) { - setError(t('errors.areaNameRequired')); - return; - } + if (!isOpen) return null; - setIsSubmitting(true); - setError(null); - - try { - await onSave(formData); - showSuccessToast(formData.id ? t('success.areaUpdated') : t('success.areaCreated')); - handleClose(); - } catch (err) { - setError((err as Error).message); - showErrorToast(t('errors.failedToSaveArea')); - } finally { - setIsSubmitting(false); - } - }; - - const handleClose = () => { - setIsClosing(true); - setTimeout(() => { - onClose(); - setIsClosing(false); - }, 300); - }; - - const handleDeleteArea = async () => { - if (formData.id && formData.id !== 0 && onDelete) { - try { - await onDelete(formData.id); - showSuccessToast(t('success.areaDeleted', 'Area deleted successfully!')); - handleClose(); - } catch (err) { - setError((err as Error).message); - showErrorToast(t('errors.failedToDeleteArea', 'Failed to delete area.')); - } - } - }; - - if (!isOpen) return null; - - return ( -
-
+ return (
-
- {/* Main Form Section */} -
-
-
-
-
- {/* Area Title Section - Always Visible */} -
- -
- - {/* Description Section - Always Visible */} -
-