diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..06b8e2c --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,54 @@ +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import Layout from './components/Layout'; +import Home from './pages/Home'; +import Projects from './pages/Projects'; +import Experience from './pages/Experience'; +import Education from './pages/Education'; +import Contact from './pages/Contact'; +import Login from './pages/Login'; +import ProtectedRoute from './components/ProtectedRoute'; + +// Admin +import AdminLayout from './pages/Admin/AdminLayout'; +import Dashboard from './pages/Admin/Dashboard'; +import ManageMessages from './pages/Admin/ManageMessages'; +import ManageStats from './pages/Admin/ManageStats'; +import ManageProjects from './pages/Admin/ManageProjects'; +import ManageExperience from './pages/Admin/ManageExperience'; +import ManageEducation from './pages/Admin/ManageEducation'; +import ManageTechStack from './pages/Admin/ManageTechStack'; +import ManageContent from './pages/Admin/ManageContent'; + +function App() { + return ( + + + + } /> + } /> + } /> + } /> + } /> + + } /> + + }> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + + ); +} + +export default App; diff --git a/frontend/src/components/Footer.jsx b/frontend/src/components/Footer.jsx new file mode 100644 index 0000000..c47f7fa --- /dev/null +++ b/frontend/src/components/Footer.jsx @@ -0,0 +1,29 @@ +export default function Footer() { + return ( + + ); +} diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx new file mode 100644 index 0000000..98d0ecb --- /dev/null +++ b/frontend/src/components/Layout.jsx @@ -0,0 +1,14 @@ +import Navbar from './Navbar'; +import Footer from './Footer'; + +export default function Layout({ children }) { + return ( +
+ +
+ {children} +
+
+ ); +} diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx new file mode 100644 index 0000000..6ebad4a --- /dev/null +++ b/frontend/src/components/Navbar.jsx @@ -0,0 +1,48 @@ +import { Link, useLocation } from 'react-router-dom'; + +export default function Navbar() { + const location = useLocation(); + const currentPath = location.pathname; + + const links = [ + { name: 'Home', path: '/' }, + { name: 'Projects', path: '/projects' }, + { name: 'Experience', path: '/experience' }, + { name: 'Education', path: '/education' }, + { name: 'Contact', path: '/contact' }, + ]; + + return ( + + ); +} diff --git a/frontend/src/components/ProtectedRoute.jsx b/frontend/src/components/ProtectedRoute.jsx new file mode 100644 index 0000000..7e8b071 --- /dev/null +++ b/frontend/src/components/ProtectedRoute.jsx @@ -0,0 +1,31 @@ +import { Navigate, Outlet } from 'react-router-dom'; +import { jwtDecode } from 'jwt-decode'; + +export default function ProtectedRoute() { + const token = localStorage.getItem('token'); + let isValid = false; + + if (token) { + try { + const decoded = jwtDecode(token); + const currentTime = Date.now() / 1000; + + // Quarkus/SmallRye JWT usually puts roles in 'groups' claim + const roles = decoded.groups || []; + if (decoded.exp > currentTime && (roles.includes('OWNER') || decoded.role === 'OWNER')) { + isValid = true; + } + } catch (err) { + console.error('Invalid token format'); + } + } + + if (!isValid) { + // Clear any invalid state + localStorage.removeItem('isAuthenticated'); + localStorage.removeItem('token'); + return ; + } + + return ; +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..c0adf52 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,48 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400;500;800&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + body { + @apply bg-background text-on-background min-h-screen flex flex-col font-body-base text-body-base selection:bg-primary-container selection:text-on-primary-container grid-bg; + } +} + +@layer utilities { + .grid-bg { + background-image: + linear-gradient(to right, rgba(132, 150, 126, 0.05) 1px, transparent 1px), + linear-gradient(to bottom, rgba(132, 150, 126, 0.05) 1px, transparent 1px); + background-size: 64px 64px; + } + + .glow-border { + box-shadow: 0 0 8px 0 theme('colors.primary-container'); + } + + .glow-border-sm { + box-shadow: 0 0 4px 0 theme('colors.primary-container'); + } + + .glow-border-active { + box-shadow: 0 0 8px 0 theme('colors.primary-container'); + border-color: theme('colors.primary-container'); + } + + .glow-border-hover:hover { + box-shadow: 0 0 8px 0 theme('colors.primary-container'); + border-color: theme('colors.primary-container'); + } + + .cursor-blink { + animation: blink 1s step-end infinite; + } +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..5eaaa2f --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,15 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +import { Provider } from 'react-redux'; +import { store } from './store'; + +createRoot(document.getElementById('root')).render( + + + + + , +)