From 3089aa927b1990e28745a2bee8a5d38533f0e521 Mon Sep 17 00:00:00 2001 From: Mohammed Sanabani Date: Wed, 10 Dec 2025 16:03:51 +0300 Subject: [PATCH 01/41] feat: add authentication, api service and secure admin panel with login --- backend/auth.py | 65 ++++++++ backend/main.py | 68 ++++++++- backend/models.py | 6 + backend/requirements.txt | 3 + backend/routers/about.py | 17 ++- backend/routers/auth.py | 37 +++++ backend/routers/calendar.py | 25 ++- backend/routers/projects.py | 23 ++- backend/routers/resume.py | 11 +- backend/routers/settings.py | 11 +- backend/routers/technologies.py | 14 +- create_admin.py | 36 +++++ frontend/src/App.jsx | 32 ++-- frontend/src/api/api.js | 143 ++++++++++++++++++ frontend/src/api/client.js | 52 +++++++ frontend/src/components/Navbar.jsx | 26 +++- frontend/src/components/ProtectedRoute.jsx | 22 +++ .../src/features/admin/hooks/useSettings.js | 10 +- .../src/features/auth/components/Login.jsx | 98 ++++++++++++ .../src/features/auth/context/AuthContext.jsx | 61 ++++++++ .../features/calendar/hooks/useCalendar.js | 27 ++-- .../calendar/hooks/useCalendarData.js | 14 +- .../projects/components/AdminProjects.jsx | 12 +- .../components/ProjectShowcaseInfo.jsx | 2 +- .../features/projects/hooks/useProjects.js | 23 ++- .../src/features/resume/hooks/useResume.js | 12 +- .../src/features/showcasing/hooks/useAbout.js | 28 ++-- frontend/src/pages/Home.jsx | 10 +- frontend/src/pages/Resume.jsx | 14 +- 29 files changed, 777 insertions(+), 125 deletions(-) create mode 100644 backend/auth.py create mode 100644 backend/routers/auth.py create mode 100644 create_admin.py create mode 100644 frontend/src/api/api.js create mode 100644 frontend/src/api/client.js create mode 100644 frontend/src/components/ProtectedRoute.jsx create mode 100644 frontend/src/features/auth/components/Login.jsx create mode 100644 frontend/src/features/auth/context/AuthContext.jsx diff --git a/backend/auth.py b/backend/auth.py new file mode 100644 index 0000000..b38ca3d --- /dev/null +++ b/backend/auth.py @@ -0,0 +1,65 @@ +from datetime import datetime, timedelta +from typing import Optional +from jose import JWTError, jwt +import bcrypt +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from sqlmodel import Session, select +from .database import engine +from .models import Admin + +def get_session(): + with Session(engine) as session: + yield session + +# Configuration +SECRET_KEY = "your-secret-key-change-this-in-production" # TODO: Move to environment variable +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token") + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """Verify a password against its hash.""" + return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8')) + + +def get_password_hash(password: str) -> str: + """Hash a password.""" + salt = bcrypt.gensalt() + hashed = bcrypt.hashpw(password.encode('utf-8'), salt) + return hashed.decode('utf-8') + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + """Create a JWT access token.""" + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +def get_current_admin(token: str = Depends(oauth2_scheme), session: Session = Depends(get_session)) -> Admin: + """Dependency to get the current authenticated admin from the token.""" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + except JWTError: + raise credentials_exception + + admin = session.exec(select(Admin).where(Admin.username == username)).first() + if admin is None: + raise credentials_exception + return admin diff --git a/backend/main.py b/backend/main.py index c607daf..130357d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,9 +1,19 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Request, status from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse from .database import create_db_and_tables -from .routers import about, projects, calendar, resume, settings, technologies +from .routers import about, projects, calendar, resume, settings, technologies, auth +import time +import logging -app = FastAPI(title="Portfolio API") +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +app = FastAPI(title="Portfolio API", version="1.0.0") origins = [ "http://localhost:5173", @@ -20,10 +30,55 @@ allow_headers=["*"], ) +# Request Logging Middleware +@app.middleware("http") +async def log_requests(request: Request, call_next): + start_time = time.time() + + # Log the request + logger.info(f"Request: {request.method} {request.url.path}") + + try: + response = await call_next(request) + + # Log the response + process_time = time.time() - start_time + logger.info( + f"Response: {request.method} {request.url.path} - " + f"Status: {response.status_code} - " + f"Duration: {process_time:.3f}s" + ) + + response.headers["X-Process-Time"] = str(process_time) + return response + except Exception as e: + process_time = time.time() - start_time + logger.error( + f"Error: {request.method} {request.url.path} - " + f"Exception: {str(e)} - " + f"Duration: {process_time:.3f}s" + ) + raise + +# Global Exception Handler +@app.exception_handler(Exception) +async def global_exception_handler(request: Request, exc: Exception): + logger.error(f"Unhandled exception: {exc}", exc_info=True) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "detail": "An internal server error occurred", + "error": str(exc) if app.debug else "Internal Server Error" + } + ) + @app.on_event("startup") def on_startup(): create_db_and_tables() + logger.info("Database initialized") +# API v1 routes +app.include_router(auth.router) app.include_router(about.router) app.include_router(projects.router) app.include_router(calendar.router) @@ -33,6 +88,9 @@ def on_startup(): @app.get("/") def read_root(): - return {"message": "Welcome to the Portfolio API"} + return {"message": "Welcome to the Portfolio API", "version": "1.0.0"} + +@app.get("/api/v1/health") +def health_check(): + return {"status": "healthy", "version": "1.0.0"} -# Trigger reload diff --git a/backend/models.py b/backend/models.py index a1d8b97..8f037a3 100644 --- a/backend/models.py +++ b/backend/models.py @@ -67,3 +67,9 @@ class Settings(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) calendar_start_year: int = Field(default=2020) calendar_end_year: int = Field(default=2030) + +class Admin(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + username: str = Field(unique=True, index=True) + hashed_password: str + diff --git a/backend/requirements.txt b/backend/requirements.txt index 0ce213c..f85d595 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,3 +3,6 @@ uvicorn sqlmodel python-multipart pymysql +python-jose[cryptography] +passlib[bcrypt] +python-dotenv diff --git a/backend/routers/about.py b/backend/routers/about.py index 73ba173..ea7362b 100644 --- a/backend/routers/about.py +++ b/backend/routers/about.py @@ -1,11 +1,12 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form from sqlmodel import Session, select from ..database import engine -from ..models import About, Technology +from ..models import About, Technology, Admin +from ..auth import get_current_admin import base64 import json -router = APIRouter(prefix="/about", tags=["about"]) +router = APIRouter(prefix="/api/v1/about", tags=["about"]) def get_session(): with Session(engine) as session: @@ -39,7 +40,8 @@ def update_about( description: str = Form(...), social_links: str = Form(...), avatar: UploadFile = File(None), - session: Session = Depends(get_session) + session: Session = Depends(get_session), + current_admin: Admin = Depends(get_current_admin) ): existing_about = session.exec(select(About)).first() if not existing_about: @@ -83,7 +85,8 @@ def get_technologies(session: Session = Depends(get_session)): def add_technology( title: str = Form(...), image: UploadFile = File(...), - session: Session = Depends(get_session) + session: Session = Depends(get_session), + current_admin: Admin = Depends(get_current_admin) ): tech = Technology(title=title, image=image.file.read()) session.add(tech) @@ -91,7 +94,11 @@ def add_technology( return {"status": "success"} @router.delete("/technologies/{tech_id}") -def delete_technology(tech_id: int, session: Session = Depends(get_session)): +def delete_technology( + tech_id: int, + session: Session = Depends(get_session), + current_admin: Admin = Depends(get_current_admin) +): tech = session.get(Technology, tech_id) if not tech: raise HTTPException(status_code=404, detail="Technology not found") diff --git a/backend/routers/auth.py b/backend/routers/auth.py new file mode 100644 index 0000000..e017f6d --- /dev/null +++ b/backend/routers/auth.py @@ -0,0 +1,37 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm +from sqlmodel import Session, select +from datetime import timedelta +from ..auth import get_session +from ..models import Admin +from ..auth import ( + verify_password, + create_access_token, + get_password_hash, + ACCESS_TOKEN_EXPIRE_MINUTES +) + +router = APIRouter(prefix="/api/v1/auth", tags=["Authentication"]) + + +@router.post("/token") +async def login( + form_data: OAuth2PasswordRequestForm = Depends(), + session: Session = Depends(get_session) +): + """Login endpoint - returns JWT access token.""" + admin = session.exec(select(Admin).where(Admin.username == form_data.username)).first() + + if not admin or not verify_password(form_data.password, admin.hashed_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": admin.username}, expires_delta=access_token_expires + ) + + return {"access_token": access_token, "token_type": "bearer"} diff --git a/backend/routers/calendar.py b/backend/routers/calendar.py index f50ba6c..d7961df 100644 --- a/backend/routers/calendar.py +++ b/backend/routers/calendar.py @@ -2,9 +2,11 @@ from fastapi import APIRouter, Depends, HTTPException from sqlmodel import Session, select from ..database import engine -from ..models import CalendarEvent +from ..models import CalendarEvent, Admin +from ..auth import get_current_admin +from datetime import date -router = APIRouter(prefix="/calendar", tags=["calendar"]) +router = APIRouter(prefix="/api/v1/calendar", tags=["calendar"]) def get_session(): with Session(engine) as session: @@ -17,7 +19,11 @@ def get_events(session: Session = Depends(get_session)): from datetime import date @router.post("/", response_model=CalendarEvent) -def create_event(event: CalendarEvent, session: Session = Depends(get_session)): +def create_event( + event: CalendarEvent, + session: Session = Depends(get_session), + current_admin: Admin = Depends(get_current_admin) +): if isinstance(event.start_date, str): event.start_date = date.fromisoformat(event.start_date) if event.end_date and isinstance(event.end_date, str): @@ -29,7 +35,12 @@ def create_event(event: CalendarEvent, session: Session = Depends(get_session)): return event @router.put("/{event_id}", response_model=CalendarEvent) -def update_event(event_id: int, event: CalendarEvent, session: Session = Depends(get_session)): +def update_event( + event_id: int, + event: CalendarEvent, + session: Session = Depends(get_session), + current_admin: Admin = Depends(get_current_admin) +): db_event = session.get(CalendarEvent, event_id) if not db_event: raise HTTPException(status_code=404, detail="Event not found") @@ -46,7 +57,11 @@ def update_event(event_id: int, event: CalendarEvent, session: Session = Depends return db_event @router.delete("/{event_id}") -def delete_event(event_id: int, session: Session = Depends(get_session)): +def delete_event( + event_id: int, + session: Session = Depends(get_session), + current_admin: Admin = Depends(get_current_admin) +): event = session.get(CalendarEvent, event_id) if not event: raise HTTPException(status_code=404, detail="Event not found") diff --git a/backend/routers/projects.py b/backend/routers/projects.py index 323a1ae..236c28e 100644 --- a/backend/routers/projects.py +++ b/backend/routers/projects.py @@ -2,11 +2,12 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, Form, Response from sqlmodel import Session, select from ..database import engine -from ..models import Project, Technology, ProjectTechnologyLink, ProjectRead +from ..models import Project, Technology, ProjectTechnologyLink, ProjectRead, Admin +from ..auth import get_current_admin from datetime import date import json -router = APIRouter(prefix="/projects", tags=["projects"]) +router = APIRouter(prefix="/api/v1/projects", tags=["projects"]) def get_session(): with Session(engine) as session: @@ -19,7 +20,7 @@ def get_projects(session: Session = Depends(get_session)): for p in projects: p_dict = p.dict(exclude={"background_image"}) # Add simulated url field - p_dict["background_image_url"] = f"http://localhost:8000/projects/{p.id}/background" if p.background_image else None + p_dict["background_image_url"] = f"http://localhost:8000/api/v1/projects/{p.id}/background" if p.background_image else None p_dict["technologies"] = p.technologies result.append(p_dict) return result @@ -42,7 +43,8 @@ def create_project( live_demo_link: Optional[str] = Form(None), background_image: Optional[UploadFile] = None, technology_ids: Optional[str] = Form(None), # Comma separated IDs or JSON string - session: Session = Depends(get_session) + session: Session = Depends(get_session), + current_admin: Admin = Depends(get_current_admin) ): # Parse dates s_date = date.fromisoformat(start_date) @@ -87,7 +89,7 @@ def create_project( # Return matched structure p_dict = new_project.dict(exclude={"background_image"}) - p_dict["background_image_url"] = f"http://localhost:8000/projects/{new_project.id}/background" if new_project.background_image else None + p_dict["background_image_url"] = f"http://localhost:8000/api/v1/projects/{new_project.id}/background" if new_project.background_image else None return p_dict @router.put("/{project_id}", response_model=ProjectRead) @@ -102,7 +104,8 @@ def update_project( live_demo_link: Optional[str] = Form(None), background_image: Optional[UploadFile] = None, technology_ids: Optional[str] = Form(None), - session: Session = Depends(get_session) + session: Session = Depends(get_session), + current_admin: Admin = Depends(get_current_admin) ): project = session.get(Project, project_id) if not project: @@ -141,11 +144,15 @@ def update_project( session.refresh(project) p_dict = project.dict(exclude={"background_image"}) - p_dict["background_image_url"] = f"http://localhost:8000/projects/{project.id}/background" if project.background_image else None + p_dict["background_image_url"] = f"http://localhost:8000/api/v1/projects/{project.id}/background" if project.background_image else None return p_dict @router.delete("/{project_id}") -def delete_project(project_id: int, session: Session = Depends(get_session)): +def delete_project( + project_id: int, + session: Session = Depends(get_session), + current_admin: Admin = Depends(get_current_admin) +): project = session.get(Project, project_id) if not project: raise HTTPException(status_code=404, detail="Project not found") diff --git a/backend/routers/resume.py b/backend/routers/resume.py index 4d10823..fdf8eb0 100644 --- a/backend/routers/resume.py +++ b/backend/routers/resume.py @@ -1,10 +1,11 @@ from fastapi import APIRouter, Depends, HTTPException from sqlmodel import Session, select from ..database import engine -from ..models import Resume +from ..models import Resume, Admin +from ..auth import get_current_admin from pydantic import BaseModel -router = APIRouter(prefix="/resume", tags=["resume"]) +router = APIRouter(prefix="/api/v1/resume", tags=["resume"]) def get_session(): with Session(engine) as session: @@ -21,7 +22,11 @@ def get_resume(session: Session = Depends(get_session)): return {"content": resume.content} @router.post("/") -def create_or_update_resume(resume_data: ResumeUpdate, session: Session = Depends(get_session)): +def create_or_update_resume( + resume_data: ResumeUpdate, + session: Session = Depends(get_session), + current_admin: Admin = Depends(get_current_admin) +): existing_resume = session.exec(select(Resume)).first() if existing_resume: existing_resume.content = resume_data.content diff --git a/backend/routers/settings.py b/backend/routers/settings.py index 8b99d54..e907f90 100644 --- a/backend/routers/settings.py +++ b/backend/routers/settings.py @@ -1,14 +1,15 @@ from fastapi import APIRouter, Depends, HTTPException from sqlmodel import Session, select from ..database import engine -from ..models import Settings +from ..models import Settings, Admin +from ..auth import get_current_admin def get_session(): with Session(engine) as session: yield session router = APIRouter( - prefix="/settings", + prefix="/api/v1/settings", tags=["settings"] ) @@ -24,7 +25,11 @@ def get_settings(session: Session = Depends(get_session)): return settings @router.put("/", response_model=Settings) -def update_settings(settings_update: Settings, session: Session = Depends(get_session)): +def update_settings( + settings_update: Settings, + session: Session = Depends(get_session), + current_admin: Admin = Depends(get_current_admin) +): db_settings = session.exec(select(Settings)).first() if not db_settings: db_settings = Settings() diff --git a/backend/routers/technologies.py b/backend/routers/technologies.py index 9bc3133..64556b1 100644 --- a/backend/routers/technologies.py +++ b/backend/routers/technologies.py @@ -2,9 +2,10 @@ from fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException, Response from sqlmodel import Session, select from ..database import engine -from ..models import Technology, TechnologyRead +from ..models import Technology, TechnologyRead, Admin +from ..auth import get_current_admin -router = APIRouter(prefix="/technologies", tags=["technologies"]) +router = APIRouter(prefix="/api/v1/technologies", tags=["technologies"]) def get_session(): with Session(engine) as session: @@ -18,7 +19,8 @@ def get_technologies(session: Session = Depends(get_session)): def create_technology( title: str = Form(...), image: UploadFile = File(...), - session: Session = Depends(get_session) + session: Session = Depends(get_session), + current_admin: Admin = Depends(get_current_admin) ): image_bytes = image.file.read() tech = Technology(title=title, image=image_bytes) @@ -35,7 +37,11 @@ def get_technology_image(tech_id: int, session: Session = Depends(get_session)): return Response(content=tech.image, media_type="image/png") @router.delete("/{tech_id}") -def delete_technology(tech_id: int, session: Session = Depends(get_session)): +def delete_technology( + tech_id: int, + session: Session = Depends(get_session), + current_admin: Admin = Depends(get_current_admin) +): tech = session.get(Technology, tech_id) if not tech: raise HTTPException(status_code=404) diff --git a/create_admin.py b/create_admin.py new file mode 100644 index 0000000..ec540f8 --- /dev/null +++ b/create_admin.py @@ -0,0 +1,36 @@ +""" +Script to create an admin user for the portfolio application. +Run this script once to create your initial admin user. +""" +from sqlmodel import Session, select +from backend.database import engine +from backend.models import Admin +from backend.auth import get_password_hash + +def create_admin(username: str, password: str): + """Create a new admin user.""" + with Session(engine) as session: + # Check if admin already exists + existing_admin = session.exec(select(Admin).where(Admin.username == username)).first() + if existing_admin: + print(f"Admin user '{username}' already exists!") + return + + # Create new admin + hashed_password = get_password_hash(password) + new_admin = Admin(username=username, hashed_password=hashed_password) + session.add(new_admin) + session.commit() + print(f"[SUCCESS] Admin user '{username}' created successfully!") + +if __name__ == "__main__": + # Default admin credentials - CHANGE THESE! + USERNAME = "admin" + PASSWORD = "admin123" # TODO: Change this password! + + print("Creating admin user...") + create_admin(USERNAME, PASSWORD) + print(f"\nYou can now login with:") + print(f" Username: {USERNAME}") + print(f" Password: {PASSWORD}") + print("\n⚠️ IMPORTANT: Change these credentials immediately!") diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 13ed3f0..ebb3ac7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,28 +1,38 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { ThemeProvider } from './context/ThemeContext'; import { ToastProvider } from './context/ToastContext'; +import { AuthProvider } from './features/auth/context/AuthContext'; +import ProtectedRoute from './components/ProtectedRoute'; import Layout from './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 Login from './features/auth/components/Login'; function App() { return ( - - - }> - } /> - } /> - } /> - } /> - } /> - - - + + + + } /> + }> + } /> + } /> + } /> + } /> + + + + } /> + + + + ); diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js new file mode 100644 index 0000000..4cf66d8 --- /dev/null +++ b/frontend/src/api/api.js @@ -0,0 +1,143 @@ +import client from './client'; + +// ============================================ +// AUTH API +// ============================================ +export const login = async (credentials) => { + const formData = new FormData(); + formData.append('username', credentials.username); + formData.append('password', credentials.password); + + const response = await client.post('/auth/token', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + return response.data; +}; + +// ============================================ +// PROJECTS API +// ============================================ +export const getProjects = async () => { + const response = await client.get('/projects/'); + return response.data; +}; + +export const createProject = async (formData) => { + const response = await client.post('/projects/', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + return response.data; +}; + +export const updateProject = async (id, formData) => { + const response = await client.put(`/projects/${id}`, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + return response.data; +}; + +export const deleteProject = async (id) => { + const response = await client.delete(`/projects/${id}`); + return response.data; +}; + +// ============================================ +// TECHNOLOGIES API +// ============================================ +export const getTechnologies = async () => { + const response = await client.get('/technologies/'); + return response.data; +}; + +export const createTechnology = async (formData) => { + const response = await client.post('/technologies/', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + return response.data; +}; + +export const deleteTechnology = async (id) => { + const response = await client.delete(`/technologies/${id}`); + return response.data; +}; + +// ============================================ +// CALENDAR API +// ============================================ +export const getEvents = async () => { + const response = await client.get('/calendar/'); + return response.data; +}; + +export const createEvent = async (eventData) => { + const response = await client.post('/calendar/', eventData); + return response.data; +}; + +export const updateEvent = async (id, eventData) => { + const response = await client.put(`/calendar/${id}`, eventData); + return response.data; +}; + +export const deleteEvent = async (id) => { + const response = await client.delete(`/calendar/${id}`); + return response.data; +}; + +// ============================================ +// ABOUT API +// ============================================ +export const getAbout = async () => { + const response = await client.get('/about/'); + return response.data; +}; + +export const updateAbout = async (formData) => { + const response = await client.post('/about/', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + return response.data; +}; + +export const getAboutTechnologies = async () => { + const response = await client.get('/about/technologies'); + return response.data; +}; + +export const addAboutTechnology = async (formData) => { + const response = await client.post('/about/technologies', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + return response.data; +}; + +export const deleteAboutTechnology = async (id) => { + const response = await client.delete(`/about/technologies/${id}`); + return response.data; +}; + +// ============================================ +// RESUME API +// ============================================ +export const getResume = async () => { + const response = await client.get('/resume/'); + return response.data; +}; + +export const uploadResume = async (resumeData) => { + const response = await client.post('/resume/', resumeData); + return response.data; +}; + +// ============================================ +// SETTINGS API +// ============================================ +export const getSettings = async () => { + const response = await client.get('/settings/'); + return response.data; +}; + +export const updateSettings = async (settingsData) => { + const response = await client.put('/settings/', settingsData); + return response.data; +}; diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js new file mode 100644 index 0000000..8fb129a --- /dev/null +++ b/frontend/src/api/client.js @@ -0,0 +1,52 @@ +import axios from 'axios'; + +// Create axios instance with base configuration +const client = axios.create({ + baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8000/api/v1', + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Request interceptor - attach auth token if available +client.interceptors.request.use( + (config) => { + const token = localStorage.getItem('access_token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// Response interceptor - handle errors globally +client.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + if (error.response.status === 401) { + // Unauthorized - clear token and redirect to login + localStorage.removeItem('access_token'); + localStorage.removeItem('user'); + window.location.href = '/login'; + } + } else if (error.request) { + // The request was made but no response was received + console.error('No response received:', error.request); + } else { + // Something happened in setting up the request that triggered an Error + console.error('Error:', error.message); + } + return Promise.reject(error); + } +); + +export default client; diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 58b21cc..11d5f45 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -1,11 +1,13 @@ import { useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; -import { Calendar, FileText, LayoutGrid, Settings, Menu, X, Home } from 'lucide-react'; +import { Calendar, FileText, LayoutGrid, Settings, Menu, X, Home, LogOut } from 'lucide-react'; import ThemeToggle from './ThemeToggle'; +import { useAuth } from '../features/auth/context/AuthContext'; const Navbar = () => { const [isOpen, setIsOpen] = useState(false); const location = useLocation(); + const { isAuthenticated, logout } = useAuth(); const links = [ { name: 'Home', path: '/', icon: }, @@ -46,6 +48,16 @@ const Navbar = () => {
+ {isAuthenticated() && ( + + )} {/* Mobile Toggle */} + )}
)} diff --git a/frontend/src/components/ProtectedRoute.jsx b/frontend/src/components/ProtectedRoute.jsx new file mode 100644 index 0000000..09ee6bc --- /dev/null +++ b/frontend/src/components/ProtectedRoute.jsx @@ -0,0 +1,22 @@ +import { useAuth } from '../features/auth/context/AuthContext'; +import { Navigate } from 'react-router-dom'; + +const ProtectedRoute = ({ children }) => { + const { isAuthenticated, loading } = useAuth(); + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + if (!isAuthenticated()) { + return ; + } + + return children; +}; + +export default ProtectedRoute; diff --git a/frontend/src/features/admin/hooks/useSettings.js b/frontend/src/features/admin/hooks/useSettings.js index eab7f8b..2955054 100644 --- a/frontend/src/features/admin/hooks/useSettings.js +++ b/frontend/src/features/admin/hooks/useSettings.js @@ -1,9 +1,7 @@ import { useState, useCallback } from 'react'; -import axios from 'axios'; +import { getSettings, updateSettings as apiUpdateSettings } from '../../../api/api'; import { useToast } from '../../../context/ToastContext'; -const SETTINGS_URL = 'http://localhost:8000/settings/'; - export const useSettings = () => { const [settings, setSettings] = useState({ calendar_start_year: 2020, calendar_end_year: 2030 }); const [loading, setLoading] = useState(false); @@ -12,8 +10,8 @@ export const useSettings = () => { const fetchSettings = useCallback(async () => { setLoading(true); try { - const response = await axios.get(SETTINGS_URL); - if (response.data) setSettings(response.data); + const data = await getSettings(); + if (data) setSettings(data); } catch (err) { console.error(err); } finally { @@ -24,7 +22,7 @@ export const useSettings = () => { const updateSettings = async (settingsData) => { setLoading(true); try { - await axios.put(SETTINGS_URL, settingsData); + await apiUpdateSettings(settingsData); addToast('Settings updated!', 'success'); fetchSettings(); return true; diff --git a/frontend/src/features/auth/components/Login.jsx b/frontend/src/features/auth/components/Login.jsx new file mode 100644 index 0000000..5a83adf --- /dev/null +++ b/frontend/src/features/auth/components/Login.jsx @@ -0,0 +1,98 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { LogIn, User, Lock, AlertCircle } from 'lucide-react'; + +const Login = () => { + const [credentials, setCredentials] = useState({ username: '', password: '' }); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + const { login } = useAuth(); + const navigate = useNavigate(); + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + setLoading(true); + + const result = await login(credentials); + setLoading(false); + + if (result.success) { + navigate('/admin'); + } else { + setError(result.error); + } + }; + + const inputClass = "w-full bg-surface border border-accent rounded-xl p-3 pl-10 text-muted focus:outline-none focus:ring-2 focus:ring-accent transition-all"; + const labelClass = "block text-sm font-medium mb-2 text-accent"; + + return ( +
+
+
+
+
+ +
+

Admin Login

+

Sign in to access the dashboard

+
+ + {error && ( +
+ +

{error}

+
+ )} + +
+
+ +
+
+ +
+ setCredentials({ ...credentials, username: e.target.value })} + required + autoFocus + /> +
+
+ +
+ +
+
+ +
+ setCredentials({ ...credentials, password: e.target.value })} + required + /> +
+
+ + +
+
+
+
+ ); +}; + +export default Login; diff --git a/frontend/src/features/auth/context/AuthContext.jsx b/frontend/src/features/auth/context/AuthContext.jsx new file mode 100644 index 0000000..d927766 --- /dev/null +++ b/frontend/src/features/auth/context/AuthContext.jsx @@ -0,0 +1,61 @@ +import { createContext, useContext, useState, useEffect } from 'react'; +import { login as apiLogin } from '../../../api/api'; + +const AuthContext = createContext(null); + +export const AuthProvider = ({ children }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Check if user is already logged in + const token = localStorage.getItem('access_token'); + const storedUser = localStorage.getItem('user'); + if (token && storedUser) { + setUser(JSON.parse(storedUser)); + } + setLoading(false); + }, []); + + const login = async (credentials) => { + try { + const data = await apiLogin(credentials); + localStorage.setItem('access_token', data.access_token); + const userData = { username: credentials.username }; + localStorage.setItem('user', JSON.stringify(userData)); + setUser(userData); + return { success: true }; + } catch (error) { + console.error('Login failed:', error); + return { + success: false, + error: error.response?.data?.detail || 'Login failed' + }; + } + }; + + const logout = () => { + localStorage.removeItem('access_token'); + localStorage.removeItem('user'); + setUser(null); + window.location.href = '/login'; + }; + + const isAuthenticated = () => { + return !!user && !!localStorage.getItem('access_token'); + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; diff --git a/frontend/src/features/calendar/hooks/useCalendar.js b/frontend/src/features/calendar/hooks/useCalendar.js index c59d44d..6158bcc 100644 --- a/frontend/src/features/calendar/hooks/useCalendar.js +++ b/frontend/src/features/calendar/hooks/useCalendar.js @@ -1,36 +1,38 @@ import { useState, useCallback } from 'react'; -import axios from 'axios'; +import { getEvents, createEvent as apiCreateEvent, updateEvent as apiUpdateEvent, deleteEvent as apiDeleteEvent } from '../../../api/api'; import { useToast } from '../../../context/ToastContext'; -const API_URL = 'http://localhost:8000/calendar/'; - export const useCalendar = () => { const [events, setEvents] = useState([]); const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); const { addToast } = useToast(); const fetchEvents = useCallback(async () => { setLoading(true); try { - const response = await axios.get(API_URL); - setEvents(response.data); + const data = await getEvents(); + setEvents(data); + setError(null); } catch (err) { console.error(err); - // addToast('Failed to fetch events', 'error'); + setError('Failed to fetch events'); } finally { setLoading(false); } - }, [addToast]); + }, []); const createEvent = async (eventData) => { setLoading(true); try { - await axios.post(API_URL, eventData); + const response = await apiCreateEvent(eventData); + setEvents(prev => [...prev, response]); addToast('Event created successfully!', 'success'); fetchEvents(); return true; } catch (err) { console.error(err); + setError('Failed to create event'); addToast('Failed to create event', 'error'); return false; } finally { @@ -41,12 +43,13 @@ export const useCalendar = () => { const updateEvent = async (id, eventData) => { setLoading(true); try { - await axios.put(`${API_URL}${id}`, eventData); + await apiUpdateEvent(id, eventData); addToast('Event updated successfully!', 'success'); fetchEvents(); return true; } catch (err) { console.error(err); + setError('Failed to update event'); addToast('Failed to update event', 'error'); return false; } finally { @@ -55,14 +58,17 @@ export const useCalendar = () => { }; const deleteEvent = async (id) => { + if (!window.confirm("Are you sure you want to delete this event?")) return false; + setLoading(true); try { - await axios.delete(`${API_URL}${id}`); + await apiDeleteEvent(id); setEvents(prev => prev.filter(e => e.id !== id)); addToast('Event deleted successfully', 'success'); return true; } catch (err) { console.error(err); + setError('Failed to delete event'); addToast('Failed to delete event', 'error'); return false; } finally { @@ -73,6 +79,7 @@ export const useCalendar = () => { return { events, loading, + error, fetchEvents, createEvent, updateEvent, diff --git a/frontend/src/features/calendar/hooks/useCalendarData.js b/frontend/src/features/calendar/hooks/useCalendarData.js index d489447..5433f8e 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 axios from 'axios'; +import { getEvents, getSettings } from '../../../api/api'; export const useCalendarData = () => { const [events, setEvents] = useState([]); @@ -10,14 +10,14 @@ export const useCalendarData = () => { useEffect(() => { const fetchData = async () => { try { - const [eventsRes, settingsRes] = await Promise.all([ - axios.get('http://localhost:8000/calendar/'), - axios.get('http://localhost:8000/settings/') + const [eventsData, settingsData] = await Promise.all([ + getEvents(), + getSettings() ]); - setEvents(eventsRes.data); - if (settingsRes.data) { - setSettings(settingsRes.data); + setEvents(eventsData); + if (settingsData) { + setSettings(settingsData); } } catch (err) { console.error("Failed to fetch calendar data", err); diff --git a/frontend/src/features/projects/components/AdminProjects.jsx b/frontend/src/features/projects/components/AdminProjects.jsx index 81623a2..4f67d77 100644 --- a/frontend/src/features/projects/components/AdminProjects.jsx +++ b/frontend/src/features/projects/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 axios from 'axios'; +import { getTechnologies } from '../../../api/api'; const AdminProjects = () => { const { projects, fetchProjects, createProject, updateProject, deleteProject } = useProjects(); @@ -27,13 +27,13 @@ const AdminProjects = () => { useEffect(() => { fetchProjects(); - fetchTechnologies(); + fetchTechnologiesData(); }, [fetchProjects]); - const fetchTechnologies = async () => { + const fetchTechnologiesData = async () => { try { - const res = await axios.get('http://localhost:8000/technologies/'); - setTechnologies(res.data); + const data = await getTechnologies(); + setTechnologies(data); } catch (e) { console.error("Failed to fetch technologies", e); } @@ -148,7 +148,7 @@ const AdminProjects = () => { }`} >
- {tech.title} e.target.style.display = 'none'} /> + {tech.title} e.target.style.display = 'none'} />
{tech.title} diff --git a/frontend/src/features/projects/components/ProjectShowcaseInfo.jsx b/frontend/src/features/projects/components/ProjectShowcaseInfo.jsx index 18631af..0cf5bc8 100644 --- a/frontend/src/features/projects/components/ProjectShowcaseInfo.jsx +++ b/frontend/src/features/projects/components/ProjectShowcaseInfo.jsx @@ -29,7 +29,7 @@ const ProjectShowcaseInfo = ({ project, titleRef, detailsRef }) => {
{tech.title} e.target.style.display = 'none'} diff --git a/frontend/src/features/projects/hooks/useProjects.js b/frontend/src/features/projects/hooks/useProjects.js index 1caf517..0b99c68 100644 --- a/frontend/src/features/projects/hooks/useProjects.js +++ b/frontend/src/features/projects/hooks/useProjects.js @@ -1,9 +1,7 @@ import { useState, useCallback } from 'react'; -import axios from 'axios'; +import { getProjects, createProject as apiCreateProject, updateProject as apiUpdateProject, deleteProject as apiDeleteProject } from '../../../api/api'; import { useToast } from '../../../context/ToastContext'; -const API_URL = 'http://localhost:8000/projects/'; - export const useProjects = () => { const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(false); @@ -13,25 +11,24 @@ export const useProjects = () => { const fetchProjects = useCallback(async () => { setLoading(true); try { - const response = await axios.get(API_URL); - setProjects(response.data); + const data = await getProjects(); + setProjects(data); setError(null); } catch (err) { console.error(err); setError('Failed to fetch projects'); - // addToast('Failed to fetch projects', 'error'); // Optional: don't spam toasts on load } finally { setLoading(false); } - }, [addToast]); + }, []); const createProject = async (projectData) => { setLoading(true); try { - const response = await axios.post(API_URL, projectData); - setProjects(prev => [...prev, response.data]); // Optimistic or append + const response = await apiCreateProject(projectData); + setProjects(prev => [...prev, response]); addToast('Project created successfully!', 'success'); - fetchProjects(); // Refresh to be sure + fetchProjects(); return true; } catch (err) { console.error(err); @@ -46,7 +43,7 @@ export const useProjects = () => { const updateProject = async (id, projectData) => { setLoading(true); try { - await axios.put(`${API_URL}${id}`, projectData); + await apiUpdateProject(id, projectData); addToast('Project updated successfully!', 'success'); fetchProjects(); return true; @@ -61,11 +58,11 @@ export const useProjects = () => { }; const deleteProject = async (id) => { - if (!window.confirm("Are you sure you want to delete this project?")) return false; // Keep confirmation here or in UI? usually UI, but logic can be here. I'll leave confirmation to UI usually, but for quick port: + if (!window.confirm("Are you sure you want to delete this project?")) return false; setLoading(true); try { - await axios.delete(`${API_URL}${id}`); + await apiDeleteProject(id); setProjects(prev => prev.filter(p => p.id !== id)); addToast('Project deleted successfully', 'success'); return true; diff --git a/frontend/src/features/resume/hooks/useResume.js b/frontend/src/features/resume/hooks/useResume.js index c157771..50b10c9 100644 --- a/frontend/src/features/resume/hooks/useResume.js +++ b/frontend/src/features/resume/hooks/useResume.js @@ -1,9 +1,7 @@ import { useState, useCallback } from 'react'; -import axios from 'axios'; +import { getResume, uploadResume } from '../../../api/api'; import { useToast } from '../../../context/ToastContext'; -const RESUME_URL = 'http://localhost:8000/resume/'; - export const useResume = () => { const [resumeData, setResumeData] = useState(null); const [loading, setLoading] = useState(false); @@ -12,10 +10,10 @@ export const useResume = () => { const fetchResume = useCallback(async () => { setLoading(true); try { - const response = await axios.get(RESUME_URL); - if (response.data && response.data.content) { + const data = await getResume(); + if (data && data.content) { try { - setResumeData(JSON.parse(response.data.content)); + setResumeData(JSON.parse(data.content)); } catch (e) { setResumeData(null); } @@ -30,7 +28,7 @@ export const useResume = () => { const updateResume = async (formData) => { setLoading(true); try { - await axios.post(RESUME_URL, { content: JSON.stringify(formData) }); + await uploadResume({ content: JSON.stringify(formData) }); addToast('Resume updated successfully!', 'success'); fetchResume(); return true; diff --git a/frontend/src/features/showcasing/hooks/useAbout.js b/frontend/src/features/showcasing/hooks/useAbout.js index a25edcc..400bfc9 100644 --- a/frontend/src/features/showcasing/hooks/useAbout.js +++ b/frontend/src/features/showcasing/hooks/useAbout.js @@ -1,10 +1,7 @@ import { useState, useCallback } from 'react'; -import axios from 'axios'; +import { getAbout, updateAbout as apiUpdateAbout, getAboutTechnologies, addAboutTechnology, deleteAboutTechnology } from '../../../api/api'; import { useToast } from '../../../context/ToastContext'; -const ABOUT_URL = 'http://localhost:8000/about/'; -const TECH_URL = 'http://localhost:8000/about/technologies'; - export const useAbout = () => { const [aboutData, setAboutData] = useState({ name: '', occupation: '', title: '', description: '', social_links: '[]', avatar_image: null }); const [technologies, setTechnologies] = useState([]); @@ -14,26 +11,23 @@ export const useAbout = () => { const fetchAboutData = useCallback(async () => { setLoading(true); try { - const [aboutRes, techRes] = await Promise.all([ - axios.get(ABOUT_URL), - axios.get(TECH_URL) + const [aboutData, techData] = await Promise.all([ + getAbout(), + getAboutTechnologies() ]); - if (aboutRes.data) setAboutData(aboutRes.data); - setTechnologies(techRes.data); + if (aboutData) setAboutData(aboutData); + setTechnologies(techData); } catch (err) { console.error(err); - // addToast('Failed to fetch about data', 'error'); } finally { setLoading(false); } - }, [addToast]); + }, []); const updateAbout = async (formData) => { setLoading(true); try { - await axios.post(ABOUT_URL, formData, { - headers: { 'Content-Type': 'multipart/form-data' } - }); + await apiUpdateAbout(formData); addToast('About section updated!', 'success'); fetchAboutData(); return true; @@ -49,9 +43,7 @@ export const useAbout = () => { const addTechnology = async (formData) => { setLoading(true); try { - await axios.post(TECH_URL, formData, { - headers: { 'Content-Type': 'multipart/form-data' } - }); + await addAboutTechnology(formData); addToast('Technology added!', 'success'); fetchAboutData(); return true; @@ -67,7 +59,7 @@ export const useAbout = () => { const deleteTechnology = async (id) => { setLoading(true); try { - await axios.delete(`${TECH_URL}/${id}`); + await deleteAboutTechnology(id); setTechnologies(prev => prev.filter(t => t.id !== id)); addToast('Technology deleted', 'success'); return true; diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 88bfb04..94dfce9 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react'; import gsap from 'gsap'; -import axios from 'axios'; +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'; @@ -22,11 +22,11 @@ const Home = () => { useEffect(() => { const fetchAboutData = async () => { try { - const aboutRes = await axios.get('http://localhost:8000/about/'); - setAboutData(aboutRes.data); + const aboutRes = await getAbout(); + setAboutData(aboutRes); - const techRes = await axios.get('http://localhost:8000/about/technologies'); - setTechnologies(techRes.data); + const techRes = await getAboutTechnologies(); + setTechnologies(techRes); } catch (error) { console.error('Error fetching about data:', error); } diff --git a/frontend/src/pages/Resume.jsx b/frontend/src/pages/Resume.jsx index d4ab345..152ca96 100644 --- a/frontend/src/pages/Resume.jsx +++ b/frontend/src/pages/Resume.jsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { Mail, MapPin, Phone, Download } from 'lucide-react'; -import axios from 'axios'; +import { getResume } from '../api/api'; const Resume = () => { const [resumeData, setResumeData] = useState(null); @@ -9,9 +9,9 @@ const Resume = () => { useEffect(() => { const fetchResume = async () => { try { - const response = await axios.get('http://localhost:8000/resume/'); - if (response.data && response.data.content) { - setResumeData(JSON.parse(response.data.content)); + const response = await getResume(); + if (response && response.content) { + setResumeData(JSON.parse(response.content)); } } catch (error) { console.error("Failed to fetch resume", error); @@ -55,19 +55,19 @@ const Resume = () => {
{personalInfo.email && (

- {personalInfo.email} + {personalInfo.email}

)} {personalInfo.phone && (

- {personalInfo.phone} + {personalInfo.phone}

)} {personalInfo.location && (

- {personalInfo.location} + {personalInfo.location}

)}
From 8be936bf0a9dede03bba743795590cc175814943 Mon Sep 17 00:00:00 2001 From: Mohammed Sanabani Date: Wed, 10 Dec 2025 16:45:30 +0300 Subject: [PATCH 02/41] feat: implement comprehensive error handling with dedicated 404, 500, and React Error Boundary pages, along with supporting documentation. --- ERROR_PAGES.md | 107 ++++++++++++++++++++++ frontend/src/App.jsx | 52 ++++++----- frontend/src/api/client.js | 10 +- frontend/src/components/ErrorBoundary.jsx | 72 +++++++++++++++ frontend/src/pages/NotFound.jsx | 58 ++++++++++++ frontend/src/pages/ServerError.jsx | 68 ++++++++++++++ 6 files changed, 343 insertions(+), 24 deletions(-) create mode 100644 ERROR_PAGES.md create mode 100644 frontend/src/components/ErrorBoundary.jsx create mode 100644 frontend/src/pages/NotFound.jsx create mode 100644 frontend/src/pages/ServerError.jsx diff --git a/ERROR_PAGES.md b/ERROR_PAGES.md new file mode 100644 index 0000000..17f480e --- /dev/null +++ b/ERROR_PAGES.md @@ -0,0 +1,107 @@ +# 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 ebb3ac7..75e8897 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,6 +2,7 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { ThemeProvider } from './context/ThemeContext'; import { ToastProvider } from './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 Home from './pages/Home'; @@ -10,31 +11,38 @@ import Calendar from './pages/Calendar'; import Resume from './pages/Resume'; import Admin from './pages/Admin'; import Login from './features/auth/components/Login'; +import NotFound from './pages/NotFound'; +import ServerError from './pages/ServerError'; function App() { return ( - - - - - - } /> - }> - } /> - } /> - } /> - } /> - - - - } /> - - - - - - + + + + + + + } /> + } /> + }> + } /> + } /> + } /> + } /> + + + + } /> + {/* 404 Catch-all - must be last */} + } /> + + + + + + + ); } diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index 8fb129a..e21c4de 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -37,10 +37,16 @@ client.interceptors.response.use( localStorage.removeItem('access_token'); localStorage.removeItem('user'); window.location.href = '/login'; + } else if (error.response.status >= 500) { + // Server error - optionally redirect to error page + // Comment this out if you want to handle 500 errors in components + console.error('Server error:', error.response.status); + // window.location.href = '/error'; } } else if (error.request) { - // The request was made but no response was received - console.error('No response received:', error.request); + // The request was made but no response was received (network error) + console.error('No response received (network error):', error.request); + // Could show a toast or redirect to error page } else { // Something happened in setting up the request that triggered an Error console.error('Error:', error.message); diff --git a/frontend/src/components/ErrorBoundary.jsx b/frontend/src/components/ErrorBoundary.jsx new file mode 100644 index 0000000..150e218 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.jsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { AlertTriangle, Home } from 'lucide-react'; + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error) { + return { hasError: true, error }; + } + + componentDidCatch(error, errorInfo) { + console.error('Error caught by boundary:', error, errorInfo); + } + + render() { + if (this.state.hasError) { + return ; + } + + return this.props.children; + } +} + +const ErrorFallback = ({ error }) => { + const navigate = useNavigate(); + + return ( +
+
+
+
+
+
+ +
+
+
+ +

+ Oops! Something Went Wrong +

+

+ An unexpected error occurred while rendering this page. +

+ + {error && ( +
+

+ {error.toString()} +

+
+ )} + + +
+
+ ); +}; + +export default ErrorBoundary; diff --git a/frontend/src/pages/NotFound.jsx b/frontend/src/pages/NotFound.jsx new file mode 100644 index 0000000..4638f75 --- /dev/null +++ b/frontend/src/pages/NotFound.jsx @@ -0,0 +1,58 @@ +import { useNavigate } from 'react-router-dom'; +import { Home, ArrowLeft } from 'lucide-react'; + +const NotFound = () => { + const navigate = useNavigate(); + + return ( +
+
+ {/* Animated 404 */} +
+

+ 404 +

+
+

+ Page Not Found +

+

+ Oops! The page you're looking for seems to have wandered off into the digital void. +

+
+
+ + {/* Action Buttons */} +
+ + +
+ + {/* Decorative Elements */} +
+ {[...Array(5)].map((_, i) => ( +
+ ))} +
+
+
+ ); +}; + +export default NotFound; diff --git a/frontend/src/pages/ServerError.jsx b/frontend/src/pages/ServerError.jsx new file mode 100644 index 0000000..ddc3f8a --- /dev/null +++ b/frontend/src/pages/ServerError.jsx @@ -0,0 +1,68 @@ +import { useNavigate } from 'react-router-dom'; +import { RefreshCw, Home, AlertTriangle } from 'lucide-react'; + +const ServerError = () => { + const navigate = useNavigate(); + + const handleRefresh = () => { + window.location.reload(); + }; + + return ( +
+
+ {/* Animated Error Icon */} +
+
+
+
+ +
+
+
+ + {/* Error Message */} +
+

+ 500 +

+

+ Server Error +

+

+ Something went wrong on our end. +

+

+ We're working to fix the issue. Please try again in a moment. +

+
+ + {/* Action Buttons */} +
+ + +
+ + {/* Status Info */} +
+

Error Code: 500 Internal Server Error

+

If this persists, please contact support.

+
+
+
+ ); +}; + +export default ServerError; From b2c451ee29fcb35b829d245dbe6c12eb8a186bb0 Mon Sep 17 00:00:00 2001 From: Mohammed Sanabani Date: Sat, 13 Dec 2025 17:10:03 +0300 Subject: [PATCH 03/41] fix: fix logout button appearnce , fix admin about form styles, implement better toast & replace alerts. --- frontend/src/components/Navbar.jsx | 4 +- frontend/src/components/ToastDemo.jsx | 84 ++++++++++++++++++ frontend/src/context/ToastContext.jsx | 87 +++++++++++++------ .../showcasing/components/AdminAbout.jsx | 4 +- 4 files changed, 151 insertions(+), 28 deletions(-) create mode 100644 frontend/src/components/ToastDemo.jsx diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 11d5f45..ce827bb 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -51,7 +51,7 @@ const Navbar = () => { {isAuthenticated() && ( + ); + })} +
+ +
+

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/context/ToastContext.jsx b/frontend/src/context/ToastContext.jsx index 353fd95..c3145e8 100644 --- a/frontend/src/context/ToastContext.jsx +++ b/frontend/src/context/ToastContext.jsx @@ -1,8 +1,32 @@ import { createContext, useContext, useState, useCallback } from 'react'; -import { X, CheckCircle, AlertCircle, Info } from 'lucide-react'; +import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react'; const ToastContext = createContext(null); +// Toast type configurations with icon colors +const toastConfig = { + success: { + icon: CheckCircle, + iconColor: 'text-green-500 dark:text-green-400', + bgGlow: 'shadow-green-500/10' + }, + error: { + icon: AlertCircle, + iconColor: 'text-red-500 dark:text-red-400', + bgGlow: 'shadow-red-500/10' + }, + warning: { + icon: AlertTriangle, + iconColor: 'text-yellow-500 dark:text-yellow-400', + bgGlow: 'shadow-yellow-500/10' + }, + info: { + icon: Info, + iconColor: 'text-blue-500 dark:text-blue-400', + bgGlow: 'shadow-blue-500/10' + } +}; + export const ToastProvider = ({ children }) => { const [toasts, setToasts] = useState([]); @@ -24,32 +48,45 @@ export const ToastProvider = ({ children }) => { return ( {children} -
- {toasts.map(toast => ( -
- {toast.type === 'success' && } - {toast.type === 'error' && } - {toast.type === 'info' && } +
+ {toasts.map(toast => { + const config = toastConfig[toast.type] || toastConfig.info; + const Icon = config.icon; -

{toast.message}

- - -
- ))} + {/* Icon */} +
+ +
+ + {/* Message */} +

+ {toast.message} +

+ + {/* Close Button */} + +
+ ); + })}
); diff --git a/frontend/src/features/showcasing/components/AdminAbout.jsx b/frontend/src/features/showcasing/components/AdminAbout.jsx index 4cd8305..816fd80 100644 --- a/frontend/src/features/showcasing/components/AdminAbout.jsx +++ b/frontend/src/features/showcasing/components/AdminAbout.jsx @@ -1,9 +1,11 @@ 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(); // Local state for forms const [localAbout, setLocalAbout] = useState({ name: '', occupation: '', title: '', description: '', social_links: '[]', avatar_image: null }); @@ -38,7 +40,7 @@ const AdminAbout = () => { const handleAddTechnology = async (e) => { e.preventDefault(); if (!newTech.title || !newTech.image) { - alert("Please provide both title and image."); + addToast('Please provide both title and image.', 'warning'); return; } const formData = new FormData(); From 74144b50e6cd2a898c8a3b55e6a9eef10df8147a Mon Sep 17 00:00:00 2001 From: Mohammed Sanabani Date: Sat, 13 Dec 2025 22:45:12 +0300 Subject: [PATCH 04/41] 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 05/41] 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 06/41] 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 (