From de60e0dab926e530398ed7fd511556614c2e8a29 Mon Sep 17 00:00:00 2001 From: Emrullah Date: Fri, 3 Oct 2025 16:23:39 +0200 Subject: [PATCH 1/3] feat(theme): add dark/light mode support and calendar dark theme - Introduced ColorModeContext for global color mode toggle - Updated ThemeProvider to respect dark/light mode - Added calendarTheme.css with dark-mode overrides for FullCalendar - Adjusted App.tsx to use the new provider and custom calendar theme --- frontend/src/app/App.tsx | 1 + frontend/src/app/calendarTheme.css | 29 +++++++ .../src/app/providers/ColorModeContext.ts | 12 +++ frontend/src/app/providers/ThemeProvider.tsx | 82 +++++++++++++------ 4 files changed, 98 insertions(+), 26 deletions(-) create mode 100644 frontend/src/app/calendarTheme.css create mode 100644 frontend/src/app/providers/ColorModeContext.ts diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx index f48ceed..4d5250a 100644 --- a/frontend/src/app/App.tsx +++ b/frontend/src/app/App.tsx @@ -2,6 +2,7 @@ import { useEffect } from "react"; import { useAuthStore } from "../store/authStore"; import { ThemeProvider } from "./providers/ThemeProvider"; import { RouterProvider } from "./providers/RouterProvider"; +import "./calendarTheme.css"; function App() { const fetchUser = useAuthStore((s) => s.fetchUser); diff --git a/frontend/src/app/calendarTheme.css b/frontend/src/app/calendarTheme.css new file mode 100644 index 0000000..ef541af --- /dev/null +++ b/frontend/src/app/calendarTheme.css @@ -0,0 +1,29 @@ +/* Dark-mode friendly FullCalendar tweaks */ +body.taskraum-dark .fc { + --fc-page-bg-color: #0f1426; + --fc-neutral-bg-color: rgba(148, 163, 184, 0.08); + --fc-border-color: rgba(148, 163, 184, 0.16); + --fc-neutral-text-color: #a8b3cf; + --fc-button-text-color: #e5e7eb; + --fc-button-bg-color: #1f2937; + --fc-button-border-color: rgba(255,255,255,0.1); + --fc-button-hover-bg-color: #2b3244; + --fc-button-hover-border-color: rgba(255,255,255,0.2); + --fc-button-active-bg-color: #374151; + --fc-today-bg-color: rgba(79, 70, 229, 0.12); + --fc-event-text-color: #e5e7eb; +} + +body.taskraum-dark .fc .fc-col-header-cell-cushion, +body.taskraum-dark .fc .fc-daygrid-day-number { + color: #a8b3cf; +} + +body.taskraum-dark .fc .fc-daygrid-day.fc-day-today { + background-color: rgba(79, 70, 229, 0.12); +} + +body.taskraum-dark .fc .fc-event { + border: none; + box-shadow: 0 0 0 1px rgba(0,0,0,0.07); +} diff --git a/frontend/src/app/providers/ColorModeContext.ts b/frontend/src/app/providers/ColorModeContext.ts new file mode 100644 index 0000000..e37c997 --- /dev/null +++ b/frontend/src/app/providers/ColorModeContext.ts @@ -0,0 +1,12 @@ +import { createContext } from "react"; + +export type Mode = "light" | "dark"; + +export const ColorModeContext = createContext<{ + mode: Mode; + toggleColorMode: () => void; +}>({ + mode: "light", + toggleColorMode: () => { + }, +}); diff --git a/frontend/src/app/providers/ThemeProvider.tsx b/frontend/src/app/providers/ThemeProvider.tsx index aaff89a..91e058c 100644 --- a/frontend/src/app/providers/ThemeProvider.tsx +++ b/frontend/src/app/providers/ThemeProvider.tsx @@ -1,34 +1,64 @@ -import type {ReactNode} from "react"; +import { useEffect, useMemo, useState, type ReactNode } from "react"; import { ThemeProvider as MuiThemeProvider, CssBaseline } from "@mui/material"; import { createTheme } from "@mui/material/styles"; +import { ColorModeContext, type Mode } from "./ColorModeContext"; -const theme = createTheme({ - palette: { - mode: "light", - primary: { main: "#4f46e5" }, - background: { default: "#ffffff", paper: "#ffffff" }, - text: { primary: "#213547", secondary: "#4b5563" }, - }, - typography: { - fontFamily: - 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"', - }, - components: { - MuiCssBaseline: { - styleOverrides: { - body: { - backgroundColor: "#ffffff", +export function ThemeProvider({ children }: { children: ReactNode }) { + const [mode, setMode] = useState(() => (localStorage.getItem("taskraum-mode") as Mode) || "light"); + const toggleColorMode = () => setMode((m) => (m === "light" ? "dark" : "light")); + + useEffect(() => { + localStorage.setItem("taskraum-mode", mode); + document.body.classList.toggle("taskraum-dark", mode === "dark"); + }, [mode]); + + const theme = useMemo( + () => + createTheme({ + palette: { + mode, + primary: { main: "#4f46e5" }, + background: { + default: mode === "light" ? "#ffffff" : "#0b1020", + paper: mode === "light" ? "#ffffff" : "#0f1426", + }, + text: { + primary: mode === "light" ? "#111827" : "#e5e7eb", + secondary: mode === "light" ? "#6b7280" : "#a8b3cf", + }, + divider: mode === "light" ? "rgba(17,24,39,0.12)" : "rgba(148,163,184,0.16)", + }, + typography: { + fontFamily: + 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"', }, - }, - }, - }, -}); + components: { + MuiAppBar: { + styleOverrides: { + root: { backgroundImage: "none" }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { backgroundImage: "none" }, + }, + }, + MuiCard: { + styleOverrides: { + root: { backgroundColor: mode === "light" ? "#ffffff" : "#121a2f" }, + }, + }, + }, + }), + [mode] + ); -export function ThemeProvider({ children }: { children: ReactNode }) { return ( - - - {children} - + + + + {children} + + ); } \ No newline at end of file From 269dfc8e6baca8e088e4152bb599a048868d7dea Mon Sep 17 00:00:00 2001 From: Emrullah Date: Fri, 3 Oct 2025 16:37:08 +0200 Subject: [PATCH 2/3] feat(ui): refresh homepage hero, topbar, and footer layout - Redesigned homepage hero with gradient background and improved copy - Updated SiteTopBar for better harmony, spacing, and button visibility - Tweaked AppFooter for consistent styling and responsive layout --- frontend/src/components/AppBar/SiteTopBar.tsx | 245 +++++++++++------- frontend/src/components/Layout/AppFooter.tsx | 36 ++- frontend/src/pages/Home/HomePage.tsx | 186 +++++++++---- 3 files changed, 311 insertions(+), 156 deletions(-) diff --git a/frontend/src/components/AppBar/SiteTopBar.tsx b/frontend/src/components/AppBar/SiteTopBar.tsx index 65b2d51..c93ce76 100644 --- a/frontend/src/components/AppBar/SiteTopBar.tsx +++ b/frontend/src/components/AppBar/SiteTopBar.tsx @@ -1,107 +1,178 @@ -import { AppBar, Toolbar, Typography, Button, Stack, Box } from "@mui/material"; -import { Link as RouterLink } from "react-router-dom"; +import { useState, useContext } from "react"; +import { + AppBar, Box, Toolbar, Typography, IconButton, Button, Stack, Drawer, + List, ListItem, ListItemButton, ListItemText, Divider, Container, Avatar, +} from "@mui/material"; +import { Link as RouterLink, useLocation } from "react-router-dom"; +import MenuIcon from "@mui/icons-material/Menu"; +import GitHubIcon from "@mui/icons-material/GitHub"; +import DarkModeIcon from "@mui/icons-material/DarkMode"; +import LightModeIcon from "@mui/icons-material/LightMode"; +import { ColorModeContext } from "../../app/providers/ColorModeContext"; import Logo from "../../assets/icon_2.svg"; +const nav = [ + { label: "About", to: "/about", external: false }, + { label: "GitHub", to: "https://github.com/emrullaharac/TaskRaum", external: true }, +]; + export default function SiteTopBar() { + const [open, setOpen] = useState(false); + const { mode, toggleColorMode } = useContext(ColorModeContext); + const location = useLocation(); + const elevate = location.pathname === "/"; + return ( ({ + elevation={elevate ? 0 : 1} + sx={(t) => ({ backdropFilter: "blur(8px)", backgroundColor: - theme.palette.mode === "light" - ? "rgba(255,255,255,0.8)" - : "rgba(0,0,0,0.6)", - borderBottom: `2px solid ${theme.palette.divider}`, + t.palette.mode === "light" + ? t.palette.primary.main + "14" + : t.palette.background.paper + "f2", + color: t.palette.text.primary, + borderBottom: 1, + borderColor: "divider", })} > - - {/* Logo + Brand */} + + + + + + TaskRaum + + + + + {nav.map((n) => + n.external ? ( + + + + ) : ( + + ) + )} + + + {mode === "dark" ? : } + + + + + + + setOpen(true)} + > + + + + + + setOpen(false)}> setOpen(false)} > - - + TaskRaum - - - {/* Nav links */} - - + + + {nav.map((n) => ( + + {n.external ? ( + + + + ) : ( + + + + )} + + ))} + + + + + - - - + + ); } diff --git a/frontend/src/components/Layout/AppFooter.tsx b/frontend/src/components/Layout/AppFooter.tsx index 3c0c70d..a77cfce 100644 --- a/frontend/src/components/Layout/AppFooter.tsx +++ b/frontend/src/components/Layout/AppFooter.tsx @@ -5,30 +5,23 @@ import GitHubIcon from "@mui/icons-material/GitHub"; export default function AppFooter() { return ( ({ - minHeight: 56, - display: "flex", - alignItems: "center", - bgcolor: theme.palette.mode === "light" ? "grey.100" : "grey.900", - color: theme.palette.mode === "light" ? "text.secondary" : "grey.100", - borderTop: "1px solid", - borderColor: "divider", - })} + component="footer" sx={{ + minHeight: 56, display: "flex", alignItems: "center", + borderTop: "1px solid", borderColor: "divider", + bgcolor: (t) => + t.palette.mode === "light" + ? "rgba(255,255,255,0.7)" + : "rgba(0,0,0,0.18)", + backdropFilter: "blur(6px)", + }} > diff --git a/frontend/src/pages/Home/HomePage.tsx b/frontend/src/pages/Home/HomePage.tsx index 2b59c9c..e8b2d23 100644 --- a/frontend/src/pages/Home/HomePage.tsx +++ b/frontend/src/pages/Home/HomePage.tsx @@ -1,84 +1,174 @@ -import { Box, Button, Container, Paper, Stack, Typography, Grid } from "@mui/material"; +import { Box, Button, Container, Paper, Stack, Typography, + Grid, Card, CardContent, CardMedia, Divider, } from "@mui/material"; import { Link as RouterLink } from "react-router-dom"; import SiteTopBar from "../../components/AppBar/SiteTopBar"; import AppFooter from "../../components/Layout/AppFooter"; import RocketLaunchIcon from "@mui/icons-material/RocketLaunch"; -import SecurityIcon from "@mui/icons-material/Security"; -import DesignServicesIcon from "@mui/icons-material/DesignServices"; +import LockIcon from "@mui/icons-material/Lock"; +import TimelineIcon from "@mui/icons-material/Timeline"; +import ViewKanbanIcon from "@mui/icons-material/ViewKanban"; +import InsightsIcon from "@mui/icons-material/Insights"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; export default function HomePage() { - return ( - - {/* MAIN */} - {/* Hero */} - - + + `linear-gradient(180deg, ${t.palette.primary.main}22, transparent 70%)`, // was 14 → 22 + }} + > + + `radial-gradient(900px 350px at 50% -10%, ${t.palette.primary.main}33, transparent 70%)`, // was 26 → 33 + }} + /> + + + TaskRaum + - Organize projects. Focus on what matters. + Organize Projects. Manage Tasks. + + + A demo capstone project with Spring Boot REST API and a React + TypeScript UI. + Includes authentication, projects, tasks, and a calendar. - - TaskRaum helps you structure projects, manage tasks, and keep momentum — - fast, minimal, and secure with JWT-based auth. + + For full details and screenshots, visit the About page. - - {/* Features */} - + - - - - Fast & Modern - - - Easily create projects, set priorities, and keep a clean birds-eye view. - - + + + + + Speed & Focus + + + Minimal clicks. Clear layout. User-friendly. + + + - - - - - Secure backend - - - JWT with refresh tokens via HttpOnly cookies keeps your session safe. - - + + + + + Reliable Auth + + + Stateless JWT + refresh tokens via HttpOnly cookies. + + + - - - - - Task Management - - - Plan, track, and complete tasks with a smooth, distraction-free UI. - - + + + + + Built to Scale + + + REST API, clean types, and predictable state. + + + + + + + + + + + + + + + Dashboard at a glance + + + Metrics: Total Projects, Open Tasks, Due Soon (7d), Completion %. + Lists: Upcoming Deadlines and Open Tasks (Top 5). + + `1px solid ${t.palette.divider}` }}/> + + + + + + + + + + Kanban + Calendar + + + Drag & drop tasks across columns and reschedule on the calendar. Priorities as colored pills. + + + + `1px solid ${t.palette.divider}` }}/> + + + `1px solid ${t.palette.divider}` }}/> + + + + + + + + `1px solid ${t.palette.divider}` }}> + + + Ready to explore more details? + + + + + + + - {/* FOOTER */} ); -} \ No newline at end of file +} From 1a421424a4280fc3caefe6a1d8b4b5e6824863a1 Mon Sep 17 00:00:00 2001 From: Emrullah Date: Fri, 3 Oct 2025 16:37:50 +0200 Subject: [PATCH 3/3] feat(ui): improve user menu and dashboard readability - Enhanced UserMenu with avatar initials, name/email header, and better action order - Improved dark-mode readability of KPI metrics on DashboardPage - Small polish on typography and hierarchy for clearer navigation --- frontend/src/components/AppBar/UserMenu.tsx | 109 +++++++++++++----- .../src/features/dashboard/DashboardPage.tsx | 6 +- 2 files changed, 80 insertions(+), 35 deletions(-) diff --git a/frontend/src/components/AppBar/UserMenu.tsx b/frontend/src/components/AppBar/UserMenu.tsx index 7c0ba9c..b26acb3 100644 --- a/frontend/src/components/AppBar/UserMenu.tsx +++ b/frontend/src/components/AppBar/UserMenu.tsx @@ -1,72 +1,117 @@ -import { useMemo, useState } from "react"; +import { useContext, useState } from "react"; import { - Avatar, Box, IconButton, Menu, MenuItem, ListItemIcon, - ListItemText, Tooltip, Typography, Divider + Avatar, Box, Divider, IconButton, ListItemIcon, + Menu, MenuItem, Tooltip, Typography } from "@mui/material"; import Logout from "@mui/icons-material/Logout"; import Settings from "@mui/icons-material/Settings"; -import { useAuthStore } from "../../store/authStore"; +import DarkModeIcon from "@mui/icons-material/DarkMode"; +import LightModeIcon from "@mui/icons-material/LightMode"; +import { ColorModeContext } from "../../app/providers/ColorModeContext"; +import { useAuthStore } from "../../store/authStore.ts"; import { useNavigate } from "react-router-dom"; -import type { UserDto } from "../../types/domain"; - -function initials(u: UserDto) { - const a = (u.name?.trim()?.[0] ?? "").toUpperCase(); - const b = (u.surname?.trim()?.[0] ?? "").toUpperCase(); - return (a + b) || "U"; -} -function fullName(u: UserDto) { - return [u.name, u.surname ?? ""].filter(Boolean).join(" "); -} export default function UserMenu() { - const { user, logout } = useAuthStore(); - const nav = useNavigate(); const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); - const label = useMemo(() => { - if (!user) return "User"; - return fullName(user) || user.email || "User"; - }, [user]); + const { mode, toggleColorMode } = useContext(ColorModeContext); + const { user, logout } = useAuthStore(); + const nav = useNavigate(); + + const first = (user?.name?.trim()?.[0] ?? "").toUpperCase(); + const last = (user?.surname?.trim()?.[0] ?? "").toUpperCase(); + const initials = (first + last) || "U"; + const label = [user?.name, user?.surname].filter(Boolean).join(" ") || user?.email || "User"; if (!user) return null; + const handleClick = (event: React.MouseEvent) => setAnchorEl(event.currentTarget); + const handleClose = () => setAnchorEl(null); + return ( - + {label} - setAnchorEl(e.currentTarget)} size="small" sx={{ p: 0.25 }}> + - {initials(user)} + {initials} setAnchorEl(null)} - onClick={() => setAnchorEl(null)} + onClose={handleClose} + onClick={handleClose} + slotProps={{ + paper: { + elevation: 2, + sx: { + mt: 1.25, + overflow: "visible", + filter: "drop-shadow(0px 2px 8px rgba(0,0,0,0.15))", + "&:before": { + content: '""', + display: "block", + position: "absolute", + top: 0, right: 14, + width: 10, height: 10, + bgcolor: "background.paper", + transform: "translateY(-50%) rotate(45deg)", + zIndex: 0, + }, + }, + }, + }} transformOrigin={{ horizontal: "right", vertical: "top" }} anchorOrigin={{ horizontal: "right", vertical: "bottom" }} > {label} - {user.email && {user.email}} + {user.email && ( + + {user.email} + + )} - nav("/app/settings")}> + + + + {mode === "dark" ? : } + + Toggle {mode === "dark" ? "Light" : "Dark"} Mode + + + nav("/app/settings")} dense> - + Profile & Settings - - - + + + + + + Logout ); -} \ No newline at end of file +} diff --git a/frontend/src/features/dashboard/DashboardPage.tsx b/frontend/src/features/dashboard/DashboardPage.tsx index 5f39eb4..08eb11c 100644 --- a/frontend/src/features/dashboard/DashboardPage.tsx +++ b/frontend/src/features/dashboard/DashboardPage.tsx @@ -138,11 +138,11 @@ export default function DashboardPage() { - - {k.label} + + {k.label} {k.icon} - {k.value} + {k.value}