From 74144b50e6cd2a898c8a3b55e6a9eef10df8147a Mon Sep 17 00:00:00 2001 From: Mohammed Sanabani Date: Sat, 13 Dec 2025 22:45:12 +0300 Subject: [PATCH 01/38] refactor:reorganize in feature-first arch style --- .vscode/settings.json | 1 + frontend/src/App.css | 42 ---------- frontend/src/App.jsx | 22 ++--- frontend/src/components/ToastDemo.jsx | 84 ------------------- frontend/src/{ => core}/api/api.js | 0 frontend/src/{ => core}/api/client.js | 0 .../{ => core}/components/ErrorBoundary.jsx | 0 frontend/src/{ => core}/components/Layout.jsx | 0 frontend/src/{ => core}/components/Navbar.jsx | 2 +- .../{ => core}/components/ProtectedRoute.jsx | 2 +- .../src/{ => core}/components/ResumeForm.jsx | 0 .../src/{ => core}/components/ThemeToggle.jsx | 0 .../components/ui/HomeAboutCard.jsx | 0 .../components/ui}/HomeCalendarCard.jsx | 0 .../components/ui}/HomeResumeCard.jsx | 0 .../src/{ => core}/context/ThemeContext.jsx | 0 .../src/{ => core}/context/ToastContext.jsx | 0 .../components/AdminAbout.jsx | 23 ++--- .../components/AdminCalendar.jsx | 0 .../components/AdminProjects.jsx | 2 +- .../components/AdminResume.jsx | 2 +- .../{showcasing => admin}/hooks/useAbout.js | 32 ++++--- .../{calendar => admin}/hooks/useCalendar.js | 9 +- .../{projects => admin}/hooks/useProjects.js | 4 +- .../{resume => admin}/hooks/useResume.js | 4 +- .../src/features/admin/hooks/useSettings.js | 4 +- .../src/{ => features/admin}/pages/Admin.jsx | 10 +-- .../features/{projects => admin}/schemas.js | 0 .../src/features/auth/context/AuthContext.jsx | 2 +- .../calendar/hooks/useCalendarData.js | 2 +- .../calendar}/pages/Calendar.jsx | 12 +-- .../projects}/components/FabricBackground.jsx | 0 .../projects/pages/projects.jsx} | 6 +- .../{ => features/resume}/pages/Resume.jsx | 2 +- frontend/src/pages/Home.jsx | 10 +-- frontend/vite.config.js | 7 ++ 36 files changed, 90 insertions(+), 194 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 frontend/src/App.css delete mode 100644 frontend/src/components/ToastDemo.jsx rename frontend/src/{ => core}/api/api.js (100%) rename frontend/src/{ => core}/api/client.js (100%) rename frontend/src/{ => core}/components/ErrorBoundary.jsx (100%) rename frontend/src/{ => core}/components/Layout.jsx (100%) rename frontend/src/{ => core}/components/Navbar.jsx (98%) rename frontend/src/{ => core}/components/ProtectedRoute.jsx (88%) rename frontend/src/{ => core}/components/ResumeForm.jsx (100%) rename frontend/src/{ => core}/components/ThemeToggle.jsx (100%) rename frontend/src/{ => core}/components/ui/HomeAboutCard.jsx (100%) rename frontend/src/{features/calendar/components => core/components/ui}/HomeCalendarCard.jsx (100%) rename frontend/src/{features/resume/components => core/components/ui}/HomeResumeCard.jsx (100%) rename frontend/src/{ => core}/context/ThemeContext.jsx (100%) rename frontend/src/{ => core}/context/ToastContext.jsx (100%) rename frontend/src/features/{showcasing => admin}/components/AdminAbout.jsx (94%) rename frontend/src/features/{calendar => admin}/components/AdminCalendar.jsx (100%) rename frontend/src/features/{projects => admin}/components/AdminProjects.jsx (99%) rename frontend/src/features/{resume => admin}/components/AdminResume.jsx (92%) rename frontend/src/features/{showcasing => admin}/hooks/useAbout.js (74%) rename frontend/src/features/{calendar => admin}/hooks/useCalendar.js (91%) rename frontend/src/features/{projects => admin}/hooks/useProjects.js (96%) rename frontend/src/features/{resume => admin}/hooks/useResume.js (91%) rename frontend/src/{ => features/admin}/pages/Admin.jsx (87%) rename frontend/src/features/{projects => admin}/schemas.js (100%) rename frontend/src/{ => features/calendar}/pages/Calendar.jsx (71%) rename frontend/src/{ => features/projects}/components/FabricBackground.jsx (100%) rename frontend/src/{pages/Showcasing.jsx => features/projects/pages/projects.jsx} (95%) rename frontend/src/{ => features/resume}/pages/Resume.jsx (99%) diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 75e8897..b78842c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,15 +1,15 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; -import { ThemeProvider } from './context/ThemeContext'; -import { ToastProvider } from './context/ToastContext'; +import { ThemeProvider } from './core/context/ThemeContext'; +import { ToastProvider } from './core/context/ToastContext'; import { AuthProvider } from './features/auth/context/AuthContext'; -import ErrorBoundary from './components/ErrorBoundary'; -import ProtectedRoute from './components/ProtectedRoute'; -import Layout from './components/Layout'; +import ErrorBoundary from './core/components/ErrorBoundary'; +import ProtectedRoute from './core/components/ProtectedRoute'; +import Layout from './core/components/Layout'; import Home from './pages/Home'; -import Showcasing from './pages/Showcasing'; -import Calendar from './pages/Calendar'; -import Resume from './pages/Resume'; -import Admin from './pages/Admin'; +import Showcasing from './features/projects/pages/projects'; +import Calendar from './features/calendar/pages/Calendar'; +import Resume from './features/resume/pages/Resume'; +import Admin from './features/admin/pages/Admin'; import Login from './features/auth/components/Login'; import NotFound from './pages/NotFound'; import ServerError from './pages/ServerError'; @@ -34,9 +34,9 @@ function App() { } /> - {/* 404 Catch-all - must be last */} - } /> + {/* 404 Catch-all - must be last */} + } /> diff --git a/frontend/src/components/ToastDemo.jsx b/frontend/src/components/ToastDemo.jsx deleted file mode 100644 index f23ed96..0000000 --- a/frontend/src/components/ToastDemo.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useToast } from '../context/ToastContext'; -import { CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react'; - -const ToastDemo = () => { - const { addToast } = useToast(); - - const demoToasts = [ - { - type: 'success', - icon: CheckCircle, - label: 'Success Toast', - message: 'Operation completed successfully!', - color: 'bg-green-500 hover:bg-green-600' - }, - { - type: 'error', - icon: AlertCircle, - label: 'Error Toast', - message: 'Something went wrong. Please try again.', - color: 'bg-red-500 hover:bg-red-600' - }, - { - type: 'warning', - icon: AlertTriangle, - label: 'Warning Toast', - message: 'Please review your changes before saving.', - color: 'bg-yellow-500 hover:bg-yellow-600' - }, - { - type: 'info', - icon: Info, - label: 'Info Toast', - message: 'Here is some helpful information for you.', - color: 'bg-blue-500 hover:bg-blue-600' - } - ]; - - return ( -
-

- Toast Notifications Demo -

-

- Click any button below to see different toast notification styles -

- -
- {demoToasts.map((toast) => { - const Icon = toast.icon; - return ( - - ); - })} -
- -
-

Toast Features:

-
    -
  • Left thick border in secondary color
  • -
  • Background uses surface color from theme
  • -
  • Text in primary color
  • -
  • Icons colored based on toast type (success/error/warning/info)
  • -
  • Auto-dismiss after 3 seconds
  • -
  • Smooth slide-in animation
  • -
  • Manual close button
  • -
-
-
- ); -}; - -export default ToastDemo; diff --git a/frontend/src/api/api.js b/frontend/src/core/api/api.js similarity index 100% rename from frontend/src/api/api.js rename to frontend/src/core/api/api.js diff --git a/frontend/src/api/client.js b/frontend/src/core/api/client.js similarity index 100% rename from frontend/src/api/client.js rename to frontend/src/core/api/client.js diff --git a/frontend/src/components/ErrorBoundary.jsx b/frontend/src/core/components/ErrorBoundary.jsx similarity index 100% rename from frontend/src/components/ErrorBoundary.jsx rename to frontend/src/core/components/ErrorBoundary.jsx diff --git a/frontend/src/components/Layout.jsx b/frontend/src/core/components/Layout.jsx similarity index 100% rename from frontend/src/components/Layout.jsx rename to frontend/src/core/components/Layout.jsx diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/core/components/Navbar.jsx similarity index 98% rename from frontend/src/components/Navbar.jsx rename to frontend/src/core/components/Navbar.jsx index ce827bb..52a63c0 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/core/components/Navbar.jsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { Calendar, FileText, LayoutGrid, Settings, Menu, X, Home, LogOut } from 'lucide-react'; import ThemeToggle from './ThemeToggle'; -import { useAuth } from '../features/auth/context/AuthContext'; +import { useAuth } from '../../features/auth/context/AuthContext'; const Navbar = () => { const [isOpen, setIsOpen] = useState(false); diff --git a/frontend/src/components/ProtectedRoute.jsx b/frontend/src/core/components/ProtectedRoute.jsx similarity index 88% rename from frontend/src/components/ProtectedRoute.jsx rename to frontend/src/core/components/ProtectedRoute.jsx index 09ee6bc..60bc67d 100644 --- a/frontend/src/components/ProtectedRoute.jsx +++ b/frontend/src/core/components/ProtectedRoute.jsx @@ -1,4 +1,4 @@ -import { useAuth } from '../features/auth/context/AuthContext'; +import { useAuth } from '../../features/auth/context/AuthContext'; import { Navigate } from 'react-router-dom'; const ProtectedRoute = ({ children }) => { diff --git a/frontend/src/components/ResumeForm.jsx b/frontend/src/core/components/ResumeForm.jsx similarity index 100% rename from frontend/src/components/ResumeForm.jsx rename to frontend/src/core/components/ResumeForm.jsx diff --git a/frontend/src/components/ThemeToggle.jsx b/frontend/src/core/components/ThemeToggle.jsx similarity index 100% rename from frontend/src/components/ThemeToggle.jsx rename to frontend/src/core/components/ThemeToggle.jsx diff --git a/frontend/src/components/ui/HomeAboutCard.jsx b/frontend/src/core/components/ui/HomeAboutCard.jsx similarity index 100% rename from frontend/src/components/ui/HomeAboutCard.jsx rename to frontend/src/core/components/ui/HomeAboutCard.jsx diff --git a/frontend/src/features/calendar/components/HomeCalendarCard.jsx b/frontend/src/core/components/ui/HomeCalendarCard.jsx similarity index 100% rename from frontend/src/features/calendar/components/HomeCalendarCard.jsx rename to frontend/src/core/components/ui/HomeCalendarCard.jsx diff --git a/frontend/src/features/resume/components/HomeResumeCard.jsx b/frontend/src/core/components/ui/HomeResumeCard.jsx similarity index 100% rename from frontend/src/features/resume/components/HomeResumeCard.jsx rename to frontend/src/core/components/ui/HomeResumeCard.jsx diff --git a/frontend/src/context/ThemeContext.jsx b/frontend/src/core/context/ThemeContext.jsx similarity index 100% rename from frontend/src/context/ThemeContext.jsx rename to frontend/src/core/context/ThemeContext.jsx diff --git a/frontend/src/context/ToastContext.jsx b/frontend/src/core/context/ToastContext.jsx similarity index 100% rename from frontend/src/context/ToastContext.jsx rename to frontend/src/core/context/ToastContext.jsx diff --git a/frontend/src/features/showcasing/components/AdminAbout.jsx b/frontend/src/features/admin/components/AdminAbout.jsx similarity index 94% rename from frontend/src/features/showcasing/components/AdminAbout.jsx rename to frontend/src/features/admin/components/AdminAbout.jsx index 816fd80..1352193 100644 --- a/frontend/src/features/showcasing/components/AdminAbout.jsx +++ b/frontend/src/features/admin/components/AdminAbout.jsx @@ -1,11 +1,17 @@ import { useState, useEffect } from 'react'; import { User, Trash2 } from 'lucide-react'; import { useAbout } from '../hooks/useAbout'; -import { useToast } from '../../../context/ToastContext'; const AdminAbout = () => { - const { aboutData, technologies, fetchAboutData, updateAbout, addTechnology, deleteTechnology } = useAbout(); - const { addToast } = useToast(); + const { + aboutData, + technologies, + fetchAboutData, + updateAbout, + addTechnology, + deleteTechnology, + notify + } = useAbout(); // Local state for forms const [localAbout, setLocalAbout] = useState({ name: '', occupation: '', title: '', description: '', social_links: '[]', avatar_image: null }); @@ -17,9 +23,7 @@ const AdminAbout = () => { }, [fetchAboutData]); useEffect(() => { - if (aboutData) { - setLocalAbout({ ...aboutData, avatar_image: null }); - } + if (aboutData) setLocalAbout({ ...aboutData, avatar_image: null }); }, [aboutData]); const handleSaveAbout = async (e) => { @@ -30,9 +34,7 @@ const AdminAbout = () => { formData.append('title', localAbout.title); formData.append('description', localAbout.description); formData.append('social_links', localAbout.social_links); - if (avatarFile) { - formData.append('avatar', avatarFile); - } + if (avatarFile) formData.append('avatar', avatarFile); await updateAbout(formData); }; @@ -40,7 +42,7 @@ const AdminAbout = () => { const handleAddTechnology = async (e) => { e.preventDefault(); if (!newTech.title || !newTech.image) { - addToast('Please provide both title and image.', 'warning'); + notify('Please provide both title and image.', 'warning'); return; } const formData = new FormData(); @@ -50,7 +52,6 @@ const AdminAbout = () => { const success = await addTechnology(formData); if (success) { setNewTech({ title: '', image: null }); - // Reset file input const fileInput = document.getElementById('tech-image-input'); if (fileInput) fileInput.value = ""; } diff --git a/frontend/src/features/calendar/components/AdminCalendar.jsx b/frontend/src/features/admin/components/AdminCalendar.jsx similarity index 100% rename from frontend/src/features/calendar/components/AdminCalendar.jsx rename to frontend/src/features/admin/components/AdminCalendar.jsx diff --git a/frontend/src/features/projects/components/AdminProjects.jsx b/frontend/src/features/admin/components/AdminProjects.jsx similarity index 99% rename from frontend/src/features/projects/components/AdminProjects.jsx rename to frontend/src/features/admin/components/AdminProjects.jsx index 4f67d77..95996e2 100644 --- a/frontend/src/features/projects/components/AdminProjects.jsx +++ b/frontend/src/features/admin/components/AdminProjects.jsx @@ -4,7 +4,7 @@ import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useProjects } from '../hooks/useProjects'; import { projectSchema } from '../schemas'; -import { getTechnologies } from '../../../api/api'; +import { getTechnologies } from '../../../core/api/api'; const AdminProjects = () => { const { projects, fetchProjects, createProject, updateProject, deleteProject } = useProjects(); diff --git a/frontend/src/features/resume/components/AdminResume.jsx b/frontend/src/features/admin/components/AdminResume.jsx similarity index 92% rename from frontend/src/features/resume/components/AdminResume.jsx rename to frontend/src/features/admin/components/AdminResume.jsx index 44386f8..2c23c9c 100644 --- a/frontend/src/features/resume/components/AdminResume.jsx +++ b/frontend/src/features/admin/components/AdminResume.jsx @@ -4,7 +4,7 @@ import { useResume } from '../hooks/useResume'; // Adjusted import path based on where this file is. // This file: frontend/src/features/resume/components/AdminResume.jsx // ResumeForm: frontend/src/components/ResumeForm.jsx -import ResumeForm from '../../../components/ResumeForm'; +import ResumeForm from '../../../core/components/ResumeForm'; const AdminResume = () => { const { resumeData, fetchResume, updateResume } = useResume(); diff --git a/frontend/src/features/showcasing/hooks/useAbout.js b/frontend/src/features/admin/hooks/useAbout.js similarity index 74% rename from frontend/src/features/showcasing/hooks/useAbout.js rename to frontend/src/features/admin/hooks/useAbout.js index 400bfc9..5fafc93 100644 --- a/frontend/src/features/showcasing/hooks/useAbout.js +++ b/frontend/src/features/admin/hooks/useAbout.js @@ -1,6 +1,12 @@ import { useState, useCallback } from 'react'; -import { getAbout, updateAbout as apiUpdateAbout, getAboutTechnologies, addAboutTechnology, deleteAboutTechnology } from '../../../api/api'; -import { useToast } from '../../../context/ToastContext'; +import { + getAbout, + updateAbout as apiUpdateAbout, + getAboutTechnologies, + addAboutTechnology, + deleteAboutTechnology +} from '@/core/api/api'; +import { useToast } from '@/core/context/ToastContext'; export const useAbout = () => { const [aboutData, setAboutData] = useState({ name: '', occupation: '', title: '', description: '', social_links: '[]', avatar_image: null }); @@ -11,25 +17,23 @@ export const useAbout = () => { const fetchAboutData = useCallback(async () => { setLoading(true); try { - const [aboutData, techData] = await Promise.all([ - getAbout(), - getAboutTechnologies() - ]); - if (aboutData) setAboutData(aboutData); - setTechnologies(techData); + const [aboutRes, techRes] = await Promise.all([getAbout(), getAboutTechnologies()]); + if (aboutRes) setAboutData(aboutRes); + setTechnologies(techRes); } catch (err) { console.error(err); + addToast('Failed to fetch About data', 'error'); } finally { setLoading(false); } - }, []); + }, [addToast]); const updateAbout = async (formData) => { setLoading(true); try { await apiUpdateAbout(formData); addToast('About section updated!', 'success'); - fetchAboutData(); + await fetchAboutData(); return true; } catch (err) { console.error(err); @@ -45,7 +49,7 @@ export const useAbout = () => { try { await addAboutTechnology(formData); addToast('Technology added!', 'success'); - fetchAboutData(); + await fetchAboutData(); return true; } catch (err) { console.error(err); @@ -72,6 +76,9 @@ export const useAbout = () => { } }; + // Unified toast helper for validations in component + const notify = (message, type = 'info') => addToast(message, type); + return { aboutData, technologies, @@ -79,6 +86,7 @@ export const useAbout = () => { fetchAboutData, updateAbout, addTechnology, - deleteTechnology + deleteTechnology, + notify }; }; diff --git a/frontend/src/features/calendar/hooks/useCalendar.js b/frontend/src/features/admin/hooks/useCalendar.js similarity index 91% rename from frontend/src/features/calendar/hooks/useCalendar.js rename to frontend/src/features/admin/hooks/useCalendar.js index 6158bcc..2fbb8e0 100644 --- a/frontend/src/features/calendar/hooks/useCalendar.js +++ b/frontend/src/features/admin/hooks/useCalendar.js @@ -1,6 +1,11 @@ import { useState, useCallback } from 'react'; -import { getEvents, createEvent as apiCreateEvent, updateEvent as apiUpdateEvent, deleteEvent as apiDeleteEvent } from '../../../api/api'; -import { useToast } from '../../../context/ToastContext'; +import { + getEvents, + createEvent as apiCreateEvent, + updateEvent as apiUpdateEvent, + deleteEvent as apiDeleteEvent + } from '@/core/api/api'; +import { useToast } from '@/core/context/ToastContext'; export const useCalendar = () => { const [events, setEvents] = useState([]); diff --git a/frontend/src/features/projects/hooks/useProjects.js b/frontend/src/features/admin/hooks/useProjects.js similarity index 96% rename from frontend/src/features/projects/hooks/useProjects.js rename to frontend/src/features/admin/hooks/useProjects.js index 0b99c68..1b519c3 100644 --- a/frontend/src/features/projects/hooks/useProjects.js +++ b/frontend/src/features/admin/hooks/useProjects.js @@ -1,6 +1,6 @@ import { useState, useCallback } from 'react'; -import { getProjects, createProject as apiCreateProject, updateProject as apiUpdateProject, deleteProject as apiDeleteProject } from '../../../api/api'; -import { useToast } from '../../../context/ToastContext'; +import { getProjects, createProject as apiCreateProject, updateProject as apiUpdateProject, deleteProject as apiDeleteProject } from '../../../core/api/api'; +import { useToast } from '../../../core/context/ToastContext'; export const useProjects = () => { const [projects, setProjects] = useState([]); diff --git a/frontend/src/features/resume/hooks/useResume.js b/frontend/src/features/admin/hooks/useResume.js similarity index 91% rename from frontend/src/features/resume/hooks/useResume.js rename to frontend/src/features/admin/hooks/useResume.js index 50b10c9..667c5ca 100644 --- a/frontend/src/features/resume/hooks/useResume.js +++ b/frontend/src/features/admin/hooks/useResume.js @@ -1,6 +1,6 @@ import { useState, useCallback } from 'react'; -import { getResume, uploadResume } from '../../../api/api'; -import { useToast } from '../../../context/ToastContext'; +import { getResume, uploadResume } from '../../../core/api/api'; +import { useToast } from '../../../core/context/ToastContext'; export const useResume = () => { const [resumeData, setResumeData] = useState(null); diff --git a/frontend/src/features/admin/hooks/useSettings.js b/frontend/src/features/admin/hooks/useSettings.js index 2955054..7ffce72 100644 --- a/frontend/src/features/admin/hooks/useSettings.js +++ b/frontend/src/features/admin/hooks/useSettings.js @@ -1,6 +1,6 @@ import { useState, useCallback } from 'react'; -import { getSettings, updateSettings as apiUpdateSettings } from '../../../api/api'; -import { useToast } from '../../../context/ToastContext'; +import { getSettings, updateSettings as apiUpdateSettings } from '../../../core/api/api'; +import { useToast } from '../../../core/context/ToastContext'; export const useSettings = () => { const [settings, setSettings] = useState({ calendar_start_year: 2020, calendar_end_year: 2030 }); diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/features/admin/pages/Admin.jsx similarity index 87% rename from frontend/src/pages/Admin.jsx rename to frontend/src/features/admin/pages/Admin.jsx index 948d327..6bbb0f7 100644 --- a/frontend/src/pages/Admin.jsx +++ b/frontend/src/features/admin/pages/Admin.jsx @@ -1,11 +1,11 @@ import { useState } from 'react'; import { User, Folder, Calendar, FileText, Settings } from 'lucide-react'; -import AdminAbout from '../features/showcasing/components/AdminAbout'; -import AdminProjects from '../features/projects/components/AdminProjects'; -import AdminCalendar from '../features/calendar/components/AdminCalendar'; -import AdminResume from '../features/resume/components/AdminResume'; -import AdminSettings from '../features/admin/components/AdminSettings'; +import AdminAbout from '../components/AdminAbout'; +import AdminProjects from '../components/AdminProjects'; +import AdminCalendar from '../components/AdminCalendar'; +import AdminResume from '../components/AdminResume'; +import AdminSettings from '../components/AdminSettings'; const Admin = () => { const [activeTab, setActiveTab] = useState('about'); diff --git a/frontend/src/features/projects/schemas.js b/frontend/src/features/admin/schemas.js similarity index 100% rename from frontend/src/features/projects/schemas.js rename to frontend/src/features/admin/schemas.js diff --git a/frontend/src/features/auth/context/AuthContext.jsx b/frontend/src/features/auth/context/AuthContext.jsx index d927766..4201dfe 100644 --- a/frontend/src/features/auth/context/AuthContext.jsx +++ b/frontend/src/features/auth/context/AuthContext.jsx @@ -1,5 +1,5 @@ import { createContext, useContext, useState, useEffect } from 'react'; -import { login as apiLogin } from '../../../api/api'; +import { login as apiLogin } from '../../../core/api/api'; const AuthContext = createContext(null); diff --git a/frontend/src/features/calendar/hooks/useCalendarData.js b/frontend/src/features/calendar/hooks/useCalendarData.js index 5433f8e..9571485 100644 --- a/frontend/src/features/calendar/hooks/useCalendarData.js +++ b/frontend/src/features/calendar/hooks/useCalendarData.js @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { getEvents, getSettings } from '../../../api/api'; +import { getEvents, getSettings } from '../../../core/api/api'; export const useCalendarData = () => { const [events, setEvents] = useState([]); diff --git a/frontend/src/pages/Calendar.jsx b/frontend/src/features/calendar/pages/Calendar.jsx similarity index 71% rename from frontend/src/pages/Calendar.jsx rename to frontend/src/features/calendar/pages/Calendar.jsx index cf67942..080d628 100644 --- a/frontend/src/pages/Calendar.jsx +++ b/frontend/src/features/calendar/pages/Calendar.jsx @@ -1,10 +1,10 @@ import { useRef } from 'react'; -import { useCalendarData } from '../features/calendar/hooks/useCalendarData'; -import { useTimelineCalculations } from '../features/calendar/hooks/useTimelineCalculations'; -import { useCalendarInteraction } from '../features/calendar/hooks/useCalendarInteraction'; -import CalendarTimeline from '../features/calendar/components/CalendarTimeline'; -import CalendarScrollbar from '../features/calendar/components/CalendarScrollbar'; -import CalendarYearCounter from '../features/calendar/components/CalendarYearCounter'; +import { useCalendarData } from '../hooks/useCalendarData'; +import { useTimelineCalculations } from '../hooks/useTimelineCalculations'; +import { useCalendarInteraction } from '../hooks/useCalendarInteraction'; +import CalendarTimeline from '../components/CalendarTimeline'; +import CalendarScrollbar from '../components/CalendarScrollbar'; +import CalendarYearCounter from '../components/CalendarYearCounter'; const Calendar = () => { const { events, settings } = useCalendarData(); diff --git a/frontend/src/components/FabricBackground.jsx b/frontend/src/features/projects/components/FabricBackground.jsx similarity index 100% rename from frontend/src/components/FabricBackground.jsx rename to frontend/src/features/projects/components/FabricBackground.jsx diff --git a/frontend/src/pages/Showcasing.jsx b/frontend/src/features/projects/pages/projects.jsx similarity index 95% rename from frontend/src/pages/Showcasing.jsx rename to frontend/src/features/projects/pages/projects.jsx index a6fbdc5..a200f88 100644 --- a/frontend/src/pages/Showcasing.jsx +++ b/frontend/src/features/projects/pages/projects.jsx @@ -1,9 +1,9 @@ import { useState, useEffect, useRef } from 'react'; import gsap from 'gsap'; import FabricBackground from '../components/FabricBackground'; -import { useProjects } from '../features/projects/hooks/useProjects'; -import ProjectShowcaseInfo from '../features/projects/components/ProjectShowcaseInfo'; -import ProjectShowcaseNavigation from '../features/projects/components/ProjectShowcaseNavigation'; +import { useProjects } from '../../admin/hooks/useProjects'; +import ProjectShowcaseInfo from '../components/ProjectShowcaseInfo'; +import ProjectShowcaseNavigation from '../components/ProjectShowcaseNavigation'; const Showcasing = () => { const { projects, loading, fetchProjects } = useProjects(); diff --git a/frontend/src/pages/Resume.jsx b/frontend/src/features/resume/pages/Resume.jsx similarity index 99% rename from frontend/src/pages/Resume.jsx rename to frontend/src/features/resume/pages/Resume.jsx index 152ca96..b24e3d6 100644 --- a/frontend/src/pages/Resume.jsx +++ b/frontend/src/features/resume/pages/Resume.jsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { Mail, MapPin, Phone, Download } from 'lucide-react'; -import { getResume } from '../api/api'; +import { getResume } from '../../../core/api/api'; const Resume = () => { const [resumeData, setResumeData] = useState(null); diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 94dfce9..c27dfb0 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,11 +1,11 @@ import { useEffect, useRef, useState } from 'react'; import gsap from 'gsap'; -import { getAbout, getAboutTechnologies } from '../api/api'; -import { useTheme } from '../context/ThemeContext'; -import HomeResumeCard from '../features/resume/components/HomeResumeCard'; -import HomeCalendarCard from '../features/calendar/components/HomeCalendarCard'; -import HomeAboutCard from '../components/ui/HomeAboutCard'; +import { getAbout, getAboutTechnologies } from '../core/api/api'; +import { useTheme } from '../core/context/ThemeContext'; +import HomeResumeCard from '../core/components/ui/HomeResumeCard'; +import HomeCalendarCard from '../core/components/ui/HomeCalendarCard'; +import HomeAboutCard from '../core/components/ui/HomeAboutCard'; const Home = () => { const containerRef = useRef(null); diff --git a/frontend/vite.config.js b/frontend/vite.config.js index c4069b7..72de58c 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,8 +1,15 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' +import path from 'path' + // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + }, + }, }) From 1ce411e379cc5c95beb2de797092e07f68971ea7 Mon Sep 17 00:00:00 2001 From: Mohammed Sanabani Date: Sun, 14 Dec 2025 00:11:12 +0300 Subject: [PATCH 02/38] refactor: clean up clutter , dry , solid --- ERROR_PAGES.md | 107 -------- frontend/src/App.jsx | 28 +-- frontend/src/core/components/Navbar.jsx | 2 +- .../src/core/components/ProtectedRoute.jsx | 2 +- frontend/src/core/components/ThemeToggle.jsx | 2 +- .../auth => core}/context/AuthContext.jsx | 4 +- .../admin/components/AdminProjects.jsx | 2 +- .../features/admin/components/AdminResume.jsx | 5 +- .../admin}/components/ResumeForm.jsx | 0 .../src/features/admin/hooks/useProjects.js | 9 +- .../src/features/admin/hooks/useResume.js | 4 +- .../src/features/admin/hooks/useSettings.js | 4 +- .../calendar/components/CalendarEvent.jsx | 2 +- .../calendar/hooks/useCalendarData.js | 2 +- .../components/ProjectShowcaseInfo.jsx | 4 +- .../projects/hooks/useProjectsData.js | 29 +++ .../src/features/projects/pages/projects.jsx | 92 ++++--- .../components/CertificationsSection.jsx | 34 +++ .../resume/components/EducationSection.jsx | 40 +++ .../resume/components/ExperienceSection.jsx | 44 ++++ .../resume/components/LanguagesSection.jsx | 24 ++ .../resume/components/PrintButton.jsx | 13 + .../resume/components/ResumeHeader.jsx | 37 +++ .../resume/components/ResumeSection.jsx | 14 ++ .../resume/components/SkillsSection.jsx | 24 ++ .../features/resume/hooks/useResumeData.js | 34 +++ frontend/src/features/resume/pages/Resume.jsx | 229 +++--------------- frontend/src/index.css | 2 +- .../auth/components => pages}/Login.jsx | 2 +- 29 files changed, 425 insertions(+), 370 deletions(-) delete mode 100644 ERROR_PAGES.md rename frontend/src/{features/auth => core}/context/AuthContext.jsx (94%) rename frontend/src/{core => features/admin}/components/ResumeForm.jsx (100%) create mode 100644 frontend/src/features/projects/hooks/useProjectsData.js create mode 100644 frontend/src/features/resume/components/CertificationsSection.jsx create mode 100644 frontend/src/features/resume/components/EducationSection.jsx create mode 100644 frontend/src/features/resume/components/ExperienceSection.jsx create mode 100644 frontend/src/features/resume/components/LanguagesSection.jsx create mode 100644 frontend/src/features/resume/components/PrintButton.jsx create mode 100644 frontend/src/features/resume/components/ResumeHeader.jsx create mode 100644 frontend/src/features/resume/components/ResumeSection.jsx create mode 100644 frontend/src/features/resume/components/SkillsSection.jsx create mode 100644 frontend/src/features/resume/hooks/useResumeData.js rename frontend/src/{features/auth/components => pages}/Login.jsx (98%) diff --git a/ERROR_PAGES.md b/ERROR_PAGES.md deleted file mode 100644 index 17f480e..0000000 --- a/ERROR_PAGES.md +++ /dev/null @@ -1,107 +0,0 @@ -# Error Pages Documentation - -This application has comprehensive error handling with dedicated error pages. - -## Error Pages - -### 1. **404 Not Found** (`/pages/NotFound.jsx`) -**When it triggers:** -- User navigates to a route that doesn't exist (e.g., `/random-page`) -- Clicking on a broken link -- Manual URL entry with invalid path - -**Features:** -- Beautiful gradient background -- Animated 404 text -- "Go Back" button (uses browser history) -- "Go Home" button (redirects to `/`) -- Decorative bouncing dots animation - -**Testing:** -- Navigate to `http://localhost:5173/does-not-exist` -- Try any invalid URL path - ---- - -### 2. **500 Server Error** (`/pages/ServerError.jsx`) -**When it triggers:** -- Available at `/error` route for manual navigation -- Can be triggered by uncommenting redirect in `api/client.js` (line 44) for automatic 500 error handling -- Backend returns 500+ status codes (currently logged, not redirected automatically) - -**Features:** -- Red/orange gradient background (error theme) -- Warning icon with glow effect -- "Refresh Page" button -- "Go Home" button -- Error code display - -**Testing:** -- Navigate to `http://localhost:5173/error` -- Uncomment line 44 in `api/client.js` to enable automatic redirect on 500 errors - ---- - -### 3. **Error Boundary** (`/components/ErrorBoundary.jsx`) -**When it triggers:** -- React component throws an uncaught error during rendering -- JavaScript error in component lifecycle -- Errors in event handlers (if propagated up) - -**Features:** -- Catches all React errors in component tree -- Shows error message for debugging -- "Go Home" button to recover -- Logs errors to console - -**Testing:** -- Create a component that throws an error -- Example: Add `throw new Error('Test error')` to any component - ---- - -## Error Handling Flow - -``` -User Action - ↓ -┌─────────────────────────────────────┐ -│ 1. Invalid Route? │ → NotFound page (404) -│ 2. React Component Error? │ → ErrorBoundary fallback -│ 3. API Error (401)? │ → Redirect to /login -│ 4. API Error (500+)? │ → Log (optional: redirect to /error) -│ 5. Network Error? │ → Log + Toast (handled in components) -└─────────────────────────────────────┘ -``` - -## Configuration - -### Automatic 500 Error Redirect -To enable automatic redirection to `/error` page on server errors: - -**File:** `frontend/src/api/client.js` -```javascript -// Line 44 - Uncomment this line: -window.location.href = '/error'; -``` - -**Note:** By default, 500 errors are logged but not redirected, allowing components to handle them gracefully with toast messages. - -### Custom Error Messages -Each error page can be customized in its respective component file: -- `NotFound.jsx` - Change messaging for 404 -- `ServerError.jsx` - Customize 500 error display -- `ErrorBoundary.jsx` - Modify fallback UI - -## Best Practices - -1. **404 Errors**: Let the catch-all route handle them (already configured) -2. **500 Errors**: Use toast notifications in components for better UX -3. **React Errors**: ErrorBoundary will catch them automatically -4. **Network Errors**: Handle in individual components with try/catch and show inline errors - -## Routes Summary -- `/login` - Login page -- `/error` - Server error page (500) -- `/*` (any invalid route) - Not found page (404) -- All other routes wrapped in ErrorBoundary for React errors diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b78842c..e7f75c4 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,18 +1,18 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; -import { ThemeProvider } from './core/context/ThemeContext'; -import { ToastProvider } from './core/context/ToastContext'; -import { AuthProvider } from './features/auth/context/AuthContext'; -import ErrorBoundary from './core/components/ErrorBoundary'; -import ProtectedRoute from './core/components/ProtectedRoute'; -import Layout from './core/components/Layout'; -import Home from './pages/Home'; -import Showcasing from './features/projects/pages/projects'; -import Calendar from './features/calendar/pages/Calendar'; -import Resume from './features/resume/pages/Resume'; -import Admin from './features/admin/pages/Admin'; -import Login from './features/auth/components/Login'; -import NotFound from './pages/NotFound'; -import ServerError from './pages/ServerError'; +import { ThemeProvider } from '@/core/context/ThemeContext'; +import { ToastProvider } from '@/core/context/ToastContext'; +import { AuthProvider } from '@/core/context/AuthContext'; +import ErrorBoundary from '@/core/components/ErrorBoundary'; +import ProtectedRoute from '@/core/components/ProtectedRoute'; +import Layout from '@/core/components/Layout'; +import Home from '@/pages/Home'; +import Showcasing from '@/features/projects/pages/projects'; +import Calendar from '@/features/calendar/pages/Calendar'; +import Resume from '@/features/resume/pages/Resume'; +import Admin from '@/features/admin/pages/Admin'; +import Login from '@/pages/Login'; +import NotFound from '@/pages/NotFound'; +import ServerError from '@/pages/ServerError'; function App() { return ( diff --git a/frontend/src/core/components/Navbar.jsx b/frontend/src/core/components/Navbar.jsx index 52a63c0..17dc422 100644 --- a/frontend/src/core/components/Navbar.jsx +++ b/frontend/src/core/components/Navbar.jsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { Calendar, FileText, LayoutGrid, Settings, Menu, X, Home, LogOut } from 'lucide-react'; import ThemeToggle from './ThemeToggle'; -import { useAuth } from '../../features/auth/context/AuthContext'; +import { useAuth } from '@/core/context/AuthContext'; const Navbar = () => { const [isOpen, setIsOpen] = useState(false); diff --git a/frontend/src/core/components/ProtectedRoute.jsx b/frontend/src/core/components/ProtectedRoute.jsx index 60bc67d..d438156 100644 --- a/frontend/src/core/components/ProtectedRoute.jsx +++ b/frontend/src/core/components/ProtectedRoute.jsx @@ -1,4 +1,4 @@ -import { useAuth } from '../../features/auth/context/AuthContext'; +import { useAuth } from '@/core/context/AuthContext'; import { Navigate } from 'react-router-dom'; const ProtectedRoute = ({ children }) => { diff --git a/frontend/src/core/components/ThemeToggle.jsx b/frontend/src/core/components/ThemeToggle.jsx index f926e4e..660d691 100644 --- a/frontend/src/core/components/ThemeToggle.jsx +++ b/frontend/src/core/components/ThemeToggle.jsx @@ -1,4 +1,4 @@ -import { useTheme } from '../context/ThemeContext'; +import { useTheme } from '@/core/context/ThemeContext'; import { Sun, Moon } from 'lucide-react'; import { useRef, useEffect } from 'react'; import gsap from 'gsap'; diff --git a/frontend/src/features/auth/context/AuthContext.jsx b/frontend/src/core/context/AuthContext.jsx similarity index 94% rename from frontend/src/features/auth/context/AuthContext.jsx rename to frontend/src/core/context/AuthContext.jsx index 4201dfe..511551b 100644 --- a/frontend/src/features/auth/context/AuthContext.jsx +++ b/frontend/src/core/context/AuthContext.jsx @@ -1,5 +1,5 @@ import { createContext, useContext, useState, useEffect } from 'react'; -import { login as apiLogin } from '../../../core/api/api'; +import { login as apiLogin } from '@/core/api/api'; const AuthContext = createContext(null); @@ -38,7 +38,7 @@ export const AuthProvider = ({ children }) => { localStorage.removeItem('access_token'); localStorage.removeItem('user'); setUser(null); - window.location.href = '/login'; + window.location.href = '/'; }; const isAuthenticated = () => { diff --git a/frontend/src/features/admin/components/AdminProjects.jsx b/frontend/src/features/admin/components/AdminProjects.jsx index 95996e2..def0bcf 100644 --- a/frontend/src/features/admin/components/AdminProjects.jsx +++ b/frontend/src/features/admin/components/AdminProjects.jsx @@ -4,7 +4,7 @@ import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useProjects } from '../hooks/useProjects'; import { projectSchema } from '../schemas'; -import { getTechnologies } from '../../../core/api/api'; +import { getTechnologies } from '@/core/api/api'; const AdminProjects = () => { const { projects, fetchProjects, createProject, updateProject, deleteProject } = useProjects(); diff --git a/frontend/src/features/admin/components/AdminResume.jsx b/frontend/src/features/admin/components/AdminResume.jsx index 2c23c9c..a2561f5 100644 --- a/frontend/src/features/admin/components/AdminResume.jsx +++ b/frontend/src/features/admin/components/AdminResume.jsx @@ -1,10 +1,7 @@ import { useEffect } from 'react'; import { FileText } from 'lucide-react'; import { useResume } from '../hooks/useResume'; -// Adjusted import path based on where this file is. -// This file: frontend/src/features/resume/components/AdminResume.jsx -// ResumeForm: frontend/src/components/ResumeForm.jsx -import ResumeForm from '../../../core/components/ResumeForm'; +import ResumeForm from './ResumeForm'; const AdminResume = () => { const { resumeData, fetchResume, updateResume } = useResume(); diff --git a/frontend/src/core/components/ResumeForm.jsx b/frontend/src/features/admin/components/ResumeForm.jsx similarity index 100% rename from frontend/src/core/components/ResumeForm.jsx rename to frontend/src/features/admin/components/ResumeForm.jsx diff --git a/frontend/src/features/admin/hooks/useProjects.js b/frontend/src/features/admin/hooks/useProjects.js index 1b519c3..dd2b6b6 100644 --- a/frontend/src/features/admin/hooks/useProjects.js +++ b/frontend/src/features/admin/hooks/useProjects.js @@ -1,6 +1,11 @@ import { useState, useCallback } from 'react'; -import { getProjects, createProject as apiCreateProject, updateProject as apiUpdateProject, deleteProject as apiDeleteProject } from '../../../core/api/api'; -import { useToast } from '../../../core/context/ToastContext'; +import { + getProjects, + createProject as apiCreateProject, + updateProject as apiUpdateProject, + deleteProject as apiDeleteProject + } from '@/core/api/api'; +import { useToast } from '@/core/context/ToastContext'; export const useProjects = () => { const [projects, setProjects] = useState([]); diff --git a/frontend/src/features/admin/hooks/useResume.js b/frontend/src/features/admin/hooks/useResume.js index 667c5ca..fba1596 100644 --- a/frontend/src/features/admin/hooks/useResume.js +++ b/frontend/src/features/admin/hooks/useResume.js @@ -1,6 +1,6 @@ import { useState, useCallback } from 'react'; -import { getResume, uploadResume } from '../../../core/api/api'; -import { useToast } from '../../../core/context/ToastContext'; +import { getResume, uploadResume } from '@/core/api/api'; +import { useToast } from '@/core/context/ToastContext'; export const useResume = () => { const [resumeData, setResumeData] = useState(null); diff --git a/frontend/src/features/admin/hooks/useSettings.js b/frontend/src/features/admin/hooks/useSettings.js index 7ffce72..1bbfe05 100644 --- a/frontend/src/features/admin/hooks/useSettings.js +++ b/frontend/src/features/admin/hooks/useSettings.js @@ -1,6 +1,6 @@ import { useState, useCallback } from 'react'; -import { getSettings, updateSettings as apiUpdateSettings } from '../../../core/api/api'; -import { useToast } from '../../../core/context/ToastContext'; +import { getSettings, updateSettings as apiUpdateSettings } from '@/core/api/api'; +import { useToast } from '@/core/context/ToastContext'; export const useSettings = () => { const [settings, setSettings] = useState({ calendar_start_year: 2020, calendar_end_year: 2030 }); diff --git a/frontend/src/features/calendar/components/CalendarEvent.jsx b/frontend/src/features/calendar/components/CalendarEvent.jsx index 87f57e9..9f94ea5 100644 --- a/frontend/src/features/calendar/components/CalendarEvent.jsx +++ b/frontend/src/features/calendar/components/CalendarEvent.jsx @@ -1,6 +1,6 @@ import { useRef, useLayoutEffect } from 'react'; import gsap from 'gsap'; -import { getEventColor } from '../../../utils/colors'; +import { getEventColor } from '@/utils/colors'; const CalendarEvent = ({ event, index, style, className = "" }) => { const elRef = useRef(null); diff --git a/frontend/src/features/calendar/hooks/useCalendarData.js b/frontend/src/features/calendar/hooks/useCalendarData.js index 9571485..315d198 100644 --- a/frontend/src/features/calendar/hooks/useCalendarData.js +++ b/frontend/src/features/calendar/hooks/useCalendarData.js @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { getEvents, getSettings } from '../../../core/api/api'; +import { getEvents, getSettings } from '@/core/api/api'; export const useCalendarData = () => { const [events, setEvents] = useState([]); diff --git a/frontend/src/features/projects/components/ProjectShowcaseInfo.jsx b/frontend/src/features/projects/components/ProjectShowcaseInfo.jsx index 0cf5bc8..a68a623 100644 --- a/frontend/src/features/projects/components/ProjectShowcaseInfo.jsx +++ b/frontend/src/features/projects/components/ProjectShowcaseInfo.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { Github, ExternalLink } from 'lucide-react'; -import { getEventColor } from '../../../utils/colors'; -import { formatDateRange } from '../../../utils/dateUtils'; +import { getEventColor } from '@/utils/colors'; +import { formatDateRange } from '@/utils/dateUtils'; const ProjectShowcaseInfo = ({ project, titleRef, detailsRef }) => { if (!project) return null; diff --git a/frontend/src/features/projects/hooks/useProjectsData.js b/frontend/src/features/projects/hooks/useProjectsData.js new file mode 100644 index 0000000..baa9f14 --- /dev/null +++ b/frontend/src/features/projects/hooks/useProjectsData.js @@ -0,0 +1,29 @@ +import { useState, useCallback } from 'react'; +import { getProjects } from '@/core/api/api'; + +export const useProjectsData = () => { + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchProjectsData = useCallback(async () => { + setLoading(true); + try { + const data = await getProjects(); + setProjects(data); + setError(null); + } catch (err) { + console.error(err); + setError('Failed to load projects'); + } finally { + setLoading(false); + } + }, []); + + return { + projects, + loading, + error, + fetchProjectsData + }; +}; diff --git a/frontend/src/features/projects/pages/projects.jsx b/frontend/src/features/projects/pages/projects.jsx index a200f88..2738674 100644 --- a/frontend/src/features/projects/pages/projects.jsx +++ b/frontend/src/features/projects/pages/projects.jsx @@ -1,12 +1,13 @@ import { useState, useEffect, useRef } from 'react'; import gsap from 'gsap'; import FabricBackground from '../components/FabricBackground'; -import { useProjects } from '../../admin/hooks/useProjects'; +import { useProjectsData } from '../hooks/useProjectsData'; import ProjectShowcaseInfo from '../components/ProjectShowcaseInfo'; import ProjectShowcaseNavigation from '../components/ProjectShowcaseNavigation'; -const Showcasing = () => { - const { projects, loading, fetchProjects } = useProjects(); +const Projects = () => { + const { projects, loading, fetchProjectsData } = useProjectsData(); + const [currentIndex, setCurrentIndex] = useState(0); const [isAnimating, setIsAnimating] = useState(false); @@ -15,50 +16,55 @@ const Showcasing = () => { const titleRef = useRef(null); const detailsRef = useRef(null); - // Initial load + /* --------------------------- Initial load --------------------------- */ + useEffect(() => { - fetchProjects(); - }, [fetchProjects]); + fetchProjectsData(); + }, [fetchProjectsData]); + /* ---------------------- Animation on index change ------------------- */ - // Animation when index changes useEffect(() => { if (!projects || projects.length === 0) return; const ctx = gsap.context(() => { - // Animate Content (Slide Up) const tl = gsap.timeline(); if (titleRef.current) { - tl.fromTo(titleRef.current, + tl.fromTo( + titleRef.current, { y: 100, opacity: 0 }, - { y: 0, opacity: 1, duration: 1, ease: "power3.out" } + { y: 0, opacity: 1, duration: 1, ease: 'power3.out' } ); } if (detailsRef.current) { - tl.fromTo(detailsRef.current, + tl.fromTo( + detailsRef.current, { y: 50, opacity: 0 }, - { y: 0, opacity: 1, duration: 0.8, ease: "power3.out" }, - "-=0.6" + { y: 0, opacity: 1, duration: 0.8, ease: 'power3.out' }, + '-=0.6' ); } - }, containerRef); return () => ctx.revert(); }, [currentIndex, projects]); - // Mouse wheel scroll navigation logic + /* --------------------- Mouse wheel navigation ----------------------- */ + useEffect(() => { if (!projects || projects.length === 0) return; let scrollTimeout; + const handleWheel = (event) => { event.preventDefault(); clearTimeout(scrollTimeout); + scrollTimeout = setTimeout(() => { if (isAnimating) return; + if (event.deltaY > 0) handleNext(); else if (event.deltaY < 0) handlePrev(); }, 50); @@ -75,12 +81,14 @@ const Showcasing = () => { } clearTimeout(scrollTimeout); }; - }, [projects, isAnimating, currentIndex]); // Added currentIndex to dependencies to ensure state freshness is handled if needed (though handleNext accesses state via setter callback usually) + }, [projects, isAnimating, currentIndex]); + + /* --------------------------- Navigation ----------------------------- */ const handleNext = () => { if (isAnimating || !projects.length) return; - setIsAnimating(true); + setIsAnimating(true); const targets = [titleRef.current, detailsRef.current].filter(Boolean); gsap.to(targets, { @@ -96,8 +104,8 @@ const Showcasing = () => { const handlePrev = () => { if (isAnimating || !projects.length) return; - setIsAnimating(true); + setIsAnimating(true); const targets = [titleRef.current, detailsRef.current].filter(Boolean); gsap.to(targets, { @@ -111,46 +119,53 @@ const Showcasing = () => { }); }; + /* ------------------------------ States ------------------------------ */ + if (loading && projects.length === 0) { - return
Loading Projects...
; + return ( +
+ Loading Projects... +
+ ); } if (!projects || projects.length === 0) { - return
No projects found.
; + return ( +
+ No projects found. +
+ ); } const currentProject = projects[currentIndex]; - - // Safety check if (!currentProject) return null; + /* ------------------------------- UI -------------------------------- */ + return (
{/* Dynamic Background */} - + +
- {/* Content Container */}
-
- - {/* Left: Info */} +
- {/* Right: Visual Placeholder (for balance) */} -
- {/* - Potential for a 3D model or detailed image later. - Currently keeping empty to maintain the "vast" feel - */} -
+
- {/* Navigation */} { isAnimating={isAnimating} /> - {/* Footer / Counter */}
- {String(currentIndex + 1).padStart(2, '0')} + + {String(currentIndex + 1).padStart(2, '0')} + / {String(projects.length).padStart(2, '0')}
@@ -170,4 +186,4 @@ const Showcasing = () => { ); }; -export default Showcasing; +export default Projects; diff --git a/frontend/src/features/resume/components/CertificationsSection.jsx b/frontend/src/features/resume/components/CertificationsSection.jsx new file mode 100644 index 0000000..9ce457c --- /dev/null +++ b/frontend/src/features/resume/components/CertificationsSection.jsx @@ -0,0 +1,34 @@ +import ResumeSection from './ResumeSection'; + +const CertificationsSection = ({ certifications }) => { + if (!certifications?.length) return null; + + return ( + + {certifications.map((cert, index) => ( +
+

+ {cert.title} +

+ +

+ {cert.issuer} • {cert.date} +

+ + {cert.url && ( + + Verify Certificate + + )} +
+ ))} +
+ ); +}; + +export default CertificationsSection; diff --git a/frontend/src/features/resume/components/EducationSection.jsx b/frontend/src/features/resume/components/EducationSection.jsx new file mode 100644 index 0000000..8193c40 --- /dev/null +++ b/frontend/src/features/resume/components/EducationSection.jsx @@ -0,0 +1,40 @@ +import ResumeSection from './ResumeSection'; + +const EducationSection = ({ education }) => { + if (!education?.length) return null; + + return ( + + {education.map((edu, index) => ( +
+
+

+ {edu.degree} +

+ + {edu.period} + +
+ +

+ {edu.institution} +

+ + {edu.location && ( +

+ {edu.location} +

+ )} + + {edu.description && ( +

+ {edu.description} +

+ )} +
+ ))} +
+ ); +}; + +export default EducationSection; diff --git a/frontend/src/features/resume/components/ExperienceSection.jsx b/frontend/src/features/resume/components/ExperienceSection.jsx new file mode 100644 index 0000000..050096d --- /dev/null +++ b/frontend/src/features/resume/components/ExperienceSection.jsx @@ -0,0 +1,44 @@ +import ResumeSection from './ResumeSection'; + +const ExperienceSection = ({ experience }) => { + if (!experience?.length) return null; + + return ( + + {experience.map((exp, index) => ( +
+
+

+ {exp.role} +

+ + {exp.period} + +
+ +

+ {exp.company} +

+ + {exp.location && ( +

+ {exp.location} +

+ )} + + {exp.achievements?.length > 0 && ( +
    + {exp.achievements + .filter(Boolean) + .map((item, i) => ( +
  • {item}
  • + ))} +
+ )} +
+ ))} +
+ ); +}; + +export default ExperienceSection; diff --git a/frontend/src/features/resume/components/LanguagesSection.jsx b/frontend/src/features/resume/components/LanguagesSection.jsx new file mode 100644 index 0000000..84e11c9 --- /dev/null +++ b/frontend/src/features/resume/components/LanguagesSection.jsx @@ -0,0 +1,24 @@ +import ResumeSection from './ResumeSection'; + +const LanguagesSection = ({ languages }) => { + if (!languages?.length) return null; + + return ( + +
+ {languages.map((lang, index) => ( +
+ + {lang.name} + + + {lang.level} + +
+ ))} +
+
+ ); +}; + +export default LanguagesSection; diff --git a/frontend/src/features/resume/components/PrintButton.jsx b/frontend/src/features/resume/components/PrintButton.jsx new file mode 100644 index 0000000..279ca7d --- /dev/null +++ b/frontend/src/features/resume/components/PrintButton.jsx @@ -0,0 +1,13 @@ +import { Download } from 'lucide-react'; + +const PrintButton = () => ( + +); + +export default PrintButton; diff --git a/frontend/src/features/resume/components/ResumeHeader.jsx b/frontend/src/features/resume/components/ResumeHeader.jsx new file mode 100644 index 0000000..c670b63 --- /dev/null +++ b/frontend/src/features/resume/components/ResumeHeader.jsx @@ -0,0 +1,37 @@ +import { Mail, MapPin, Phone } from 'lucide-react'; + +const ResumeHeader = ({ personalInfo }) => { + if (!personalInfo) return null; + + return ( +
+

+ {personalInfo.fullName} +

+ +

+ {personalInfo.professionalTitle} +

+ +
+ {personalInfo.email && ( +

+ {personalInfo.email} +

+ )} + {personalInfo.phone && ( +

+ {personalInfo.phone} +

+ )} + {personalInfo.location && ( +

+ {personalInfo.location} +

+ )} +
+
+ ); +}; + +export default ResumeHeader; diff --git a/frontend/src/features/resume/components/ResumeSection.jsx b/frontend/src/features/resume/components/ResumeSection.jsx new file mode 100644 index 0000000..85551ea --- /dev/null +++ b/frontend/src/features/resume/components/ResumeSection.jsx @@ -0,0 +1,14 @@ +const ResumeSection = ({ title, children }) => { + if (!children) return null; + + return ( +
+

+ {title} +

+ {children} +
+ ); +}; + +export default ResumeSection; diff --git a/frontend/src/features/resume/components/SkillsSection.jsx b/frontend/src/features/resume/components/SkillsSection.jsx new file mode 100644 index 0000000..e75e3cb --- /dev/null +++ b/frontend/src/features/resume/components/SkillsSection.jsx @@ -0,0 +1,24 @@ +import ResumeSection from './ResumeSection'; + +const SkillsSection = ({ skills }) => { + if (!skills?.length) return null; + + return ( + +
+ {skills.map((skill, index) => ( +
+ + {skill.category}: + {' '} + + {skill.items} + +
+ ))} +
+
+ ); +}; + +export default SkillsSection; diff --git a/frontend/src/features/resume/hooks/useResumeData.js b/frontend/src/features/resume/hooks/useResumeData.js new file mode 100644 index 0000000..b8d1edc --- /dev/null +++ b/frontend/src/features/resume/hooks/useResumeData.js @@ -0,0 +1,34 @@ +import { useState, useEffect } from 'react'; +import { getResume } from '@/core/api/api'; + +export const useResumeData = () => { + const [resumeData, setResumeData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchResume = async () => { + try { + const response = await getResume(); + if (response?.content) { + setResumeData(JSON.parse(response.content)); + } else { + setResumeData(null); + } + } catch (err) { + console.error(err); + setError('Failed to fetch resume'); + } finally { + setLoading(false); + } + }; + + fetchResume(); + }, []); + + return { + resumeData, + loading, + error + }; +}; diff --git a/frontend/src/features/resume/pages/Resume.jsx b/frontend/src/features/resume/pages/Resume.jsx index b24e3d6..fe03181 100644 --- a/frontend/src/features/resume/pages/Resume.jsx +++ b/frontend/src/features/resume/pages/Resume.jsx @@ -1,216 +1,67 @@ -import { useState, useEffect } from 'react'; -import { Mail, MapPin, Phone, Download } from 'lucide-react'; +import { useResumeData } from '../hooks/useResumeData'; -import { getResume } from '../../../core/api/api'; +import ResumeHeader from '../components/ResumeHeader'; +import ExperienceSection from '../components/ExperienceSection'; +import EducationSection from '../components/EducationSection'; +import SkillsSection from '../components/SkillsSection'; +import LanguagesSection from '../components/LanguagesSection'; +import CertificationsSection from '../components/CertificationsSection'; +import PrintButton from '../components/PrintButton'; +import ResumeSection from '../components/ResumeSection'; const Resume = () => { - const [resumeData, setResumeData] = useState(null); + const { resumeData, loading } = useResumeData(); - useEffect(() => { - const fetchResume = async () => { - try { - const response = await getResume(); - if (response && response.content) { - setResumeData(JSON.parse(response.content)); - } - } catch (error) { - console.error("Failed to fetch resume", error); - } - }; - fetchResume(); - }, []); - - const handlePrint = () => { - window.print(); - }; + if (loading) { + return ( +
+ Loading resume… +
+ ); + } - // Fallback if no data - if (!resumeData || !resumeData.personalInfo) { + if (!resumeData?.personalInfo) { return ( -
-

No resume data available. Please configure your resume in the Admin panel.

+
+ No resume data available.
); } - const { personalInfo, experience, education, skills, languages, certifications } = resumeData; + const { + personalInfo, + experience, + education, + skills, + languages, + certifications + } = resumeData; return ( -
- {/* A4 Paper Container */} -
- - {/* Europass Blue Header Bar */} -
+
+
+
- {/* Header Section */} -
-

- {personalInfo.fullName || "Your Name"} -

-

- {personalInfo.professionalTitle || "Professional Title"} -

-
- {personalInfo.email && ( -

- {personalInfo.email} -

- )} - - {personalInfo.phone && ( -

- {personalInfo.phone} -

- )} + - {personalInfo.location && ( -

- {personalInfo.location} -

- )} -
-
- - {/* Professional Summary */} {personalInfo.summary && ( -
-

- Professional Summary -

+

{personalInfo.summary}

-
- )} - - {/* Work Experience */} - {experience && experience.length > 0 && ( -
-

- Work Experience -

- {experience.map((exp, index) => ( -
-
-

{exp.role}

- {exp.period} -
-

{exp.company}

- {exp.location &&

{exp.location}

} - {exp.achievements && exp.achievements.length > 0 && ( -
    - {exp.achievements.filter(a => a.trim()).map((achievement, i) => ( -
  • {achievement}
  • - ))} -
- )} -
- ))} -
- )} - - {/* Education */} - {education && education.length > 0 && ( -
-

- Education -

- {education.map((edu, index) => ( -
-
-

{edu.degree}

- {edu.period} -
-

{edu.institution}

- {edu.location &&

{edu.location}

} - {edu.description &&

{edu.description}

} -
- ))} -
- )} - - {/* Skills */} - {skills && skills.length > 0 && ( -
-

- Skills -

-
- {skills.map((skill, index) => ( -
- {skill.category}:{' '} - {skill.items} -
- ))} -
-
+ )} - {/* Languages */} - {languages && languages.length > 0 && ( -
-

- Languages -

-
- {languages.map((lang, index) => ( -
- {lang.name} - {lang.level} -
- ))} -
-
- )} - - {/* Certifications */} - {certifications && certifications.length > 0 && ( -
-

- Certifications -

- {certifications.map((cert, index) => ( -
-

{cert.title}

-

- {cert.issuer} • {cert.date} -

- {cert.url && ( - - Verify Certificate - - )} -
- ))} -
- )} + + + + +
- {/* Floating Download/Print Button */} - - - {/* Print Styles */} - +
); }; diff --git a/frontend/src/index.css b/frontend/src/index.css index 83dc81f..ec887a3 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -53,7 +53,7 @@ body { margin: 0; min-height: 100vh; font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - background-color: var(--color-primary); + background-color: var(--color-surface); color: var(--color-text); transition: background-color 0.3s ease, color 0.3s ease; } diff --git a/frontend/src/features/auth/components/Login.jsx b/frontend/src/pages/Login.jsx similarity index 98% rename from frontend/src/features/auth/components/Login.jsx rename to frontend/src/pages/Login.jsx index 5a83adf..9a1d28c 100644 --- a/frontend/src/features/auth/components/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useAuth } from '../context/AuthContext'; +import { useAuth } from '@/core/context/AuthContext'; import { LogIn, User, Lock, AlertCircle } from 'lucide-react'; const Login = () => { From 6224e3ac51b097ef89878133d84763da80deb106 Mon Sep 17 00:00:00 2001 From: Mohammed Sanabani Date: Sun, 14 Dec 2025 00:56:09 +0300 Subject: [PATCH 03/38] fix: enhance navbar appearence in showcasing page --- frontend/src/core/components/Navbar.jsx | 129 +++++++++++++++++------- 1 file changed, 93 insertions(+), 36 deletions(-) diff --git a/frontend/src/core/components/Navbar.jsx b/frontend/src/core/components/Navbar.jsx index 17dc422..fa6f928 100644 --- a/frontend/src/core/components/Navbar.jsx +++ b/frontend/src/core/components/Navbar.jsx @@ -1,6 +1,16 @@ import { useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; -import { Calendar, FileText, LayoutGrid, Settings, Menu, X, Home, LogOut } from 'lucide-react'; +import { + Calendar, + FileText, + LayoutGrid, + Settings, + Menu, + X, + Home, + LogOut, +} from 'lucide-react'; + import ThemeToggle from './ThemeToggle'; import { useAuth } from '@/core/context/AuthContext'; @@ -9,6 +19,8 @@ const Navbar = () => { const location = useLocation(); const { isAuthenticated, logout } = useAuth(); + const isShowcasing = location.pathname === '/showcasing'; + const links = [ { name: 'Home', path: '/', icon: }, { name: 'Calendar', path: '/calendar', icon: }, @@ -19,77 +31,122 @@ const Navbar = () => { const toggleMenu = () => setIsOpen(!isOpen); + /** Forced color tokens */ + const baseText = isShowcasing ? 'text-white' : 'text-secondary'; + const hoverText = isShowcasing ? 'hover:text-white' : 'hover:text-primary'; + const activeText = isShowcasing ? 'text-white' : 'text-secondary'; + return (