diff --git a/frontend/src/pages/Contact.jsx b/frontend/src/pages/Contact.jsx new file mode 100644 index 0000000..80c703c --- /dev/null +++ b/frontend/src/pages/Contact.jsx @@ -0,0 +1,195 @@ +import { useState, useEffect } from 'react'; +import { useAddMessageMutation, useGetPageContentQuery } from '../store/apiSlice'; + +export default function Contact() { + const { data: pageContent, isLoading: isLoadingContent } = useGetPageContentQuery(); + const [formData, setFormData] = useState({ user_name: '', user_email: '', title_header: '', message_body: '' }); + const [status, setStatus] = useState('idle'); // idle, success, error + const [cooldown, setCooldown] = useState(0); + + const [addMessage, { isLoading }] = useAddMessageMutation(); + + useEffect(() => { + let timer; + if (cooldown > 0) { + timer = setInterval(() => setCooldown(c => c - 1), 1000); + } + return () => clearInterval(timer); + }, [cooldown]); + + const handleChange = (e) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (cooldown > 0) return; + + try { + const payload = { + ...formData, + timestamp: new Date().toISOString(), + status: 'unread' + }; + + await addMessage(payload).unwrap(); + + setStatus('success'); + setFormData({ user_name: '', user_email: '', title_header: '', message_body: '' }); + setCooldown(60); // 60 seconds cooldown to prevent spam + + setTimeout(() => setStatus('idle'), 5000); + } catch (err) { + setStatus('error'); + } + }; + + if (isLoadingContent) { + return ( +
+
Loading...
+
+ ); + } + + return ( +
+
+ {/* Terminal Header */} +
+
+
+
+
+
+
+ user@backend-architect: ~/contact +
+
+ + {/* Terminal Body */} +
+
+

+ {pageContent?.contact?.title || 'INITIATE_CONTACT'} +

+

+ {pageContent?.contact?.subtitle || 'Awaiting input. Provide valid parameters to establish a secure connection or transmit a payload directly via the form interface below.'} +

+
+ +
+ {/* Form Section */} +
+
+ +
+ > + +
+
+
+ +
+ > + +
+
+
+ +
+ > + +
+
+
+ +
+ > + +
+
+ + {status === 'success' && ( +
+ > Payload transmitted successfully. Connection closed. +
+ )} + {status === 'error' && ( +
+ > Transmission failed. Check connection logs. +
+ )} + +
+ +
+
+ + {/* Direct Links Section */} +
+
+ // DIRECT_LINKS +
+ +
+ code +
+
GitHub
+
github.com/backend_arch
+
+
+ arrow_outward +
+ +
+ work +
+
LinkedIn
+
linkedin.com/in/backend_arch
+
+
+ arrow_outward +
+ +
+ mail +
+
Email
+
sysadmin@backend.local
+
+
+ arrow_outward +
+ + {/* Terminal Output Mockup */} +
+
+ sys@admin:~$ status check
+ > System online.
+ > Listening on port 443.
+ > Ready for input +
+
+
+
+
+
+
+ ); +} diff --git a/frontend/src/pages/Education.jsx b/frontend/src/pages/Education.jsx new file mode 100644 index 0000000..487a058 --- /dev/null +++ b/frontend/src/pages/Education.jsx @@ -0,0 +1,88 @@ +import { useGetEducationQuery, useGetPageContentQuery } from '../store/apiSlice'; + +export default function Education() { + const { data: education, isLoading: isLoadingEdu } = useGetEducationQuery(); + const { data: pageContent, isLoading: isLoadingContent } = useGetPageContentQuery(); + + if (isLoadingEdu || isLoadingContent) { + return ( +
+
Loading...
+
+ ); + } + + if (!education) return null; + + return ( +
+

+ {pageContent?.education?.title || 'ACADEMIC_RECORD'} +

+

+ {pageContent?.education?.subtitle || 'Degrees, certifications, and formal training.'} +

+ +
+ {/* Degrees Section */} +
+
+ school + ACADEMIC_DEGREES +
+
+ {education.filter(e => e.typeEducation === 'DEGREE').map(deg => ( +
+
+
+
{deg.period}
+

{deg.degree}

+
{deg.title}
+
+
+

{deg.institution}

+
+
+
SPECIALIZATION:
+
{deg.specialization}
+
+
+
+
CORE_ARCHITECTURES:
+
+ {deg.architectures.map(arch => ( + + {arch} + + ))} +
+
+
+
+ ))} +
+
+ + {/* Certifications Section */} +
+
+ verified + CERTIFICATIONS_&_MODULES +
+
+ {education.filter(e => e.typeEducation === 'CERTIFICATION').map(cert => ( +
+
+ {cert.icon} +
{cert.issued}
+
+

{cert.title}

+

{cert.description}

+
+ ))} +
+
+
+
+ ); +} diff --git a/frontend/src/pages/Experience.jsx b/frontend/src/pages/Experience.jsx new file mode 100644 index 0000000..7d32e39 --- /dev/null +++ b/frontend/src/pages/Experience.jsx @@ -0,0 +1,74 @@ +import { useGetExperienceQuery, useGetPageContentQuery } from '../store/apiSlice'; + +export default function Experience() { + const { data: experiences, isLoading: isLoadingExp } = useGetExperienceQuery(); + const { data: pageContent, isLoading: isLoadingContent } = useGetPageContentQuery(); + + if (isLoadingExp || isLoadingContent) { + return ( +
+
Loading...
+
+ ); + } + + if (!experiences) return null; + + return ( +
+

+ {pageContent?.experience?.title || 'RUNTIME_HISTORY'} +

+

+ {pageContent?.experience?.subtitle || 'A chronological log of my professional execution states and deployed responsibilities.'} +

+ +
+ {experiences.map((exp, index) => ( +
+ {/* Timeline Node */} +
+
+
+ +
+
+
+

+ {exp.icon} + {exp.title} +

+
+ @{exp.company} +
+
+
+ {exp.period} +
+
+ +
    + {exp.bullets.map((bullet, i) => ( +
  • + > + {bullet} +
  • + ))} +
+ + +
+
+ ))} +
+
+ ); +} diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx new file mode 100644 index 0000000..f8d9d87 --- /dev/null +++ b/frontend/src/pages/Home.jsx @@ -0,0 +1,128 @@ +import { useGetStatsQuery, useGetPageContentQuery, useGetTechStackQuery } from '../store/apiSlice'; + +export default function Home() { + const { data: stats, isLoading: isLoadingStats } = useGetStatsQuery(); + const { data: pageContent, isLoading: isLoadingContent } = useGetPageContentQuery(); + const { data: techStack, isLoading: isLoadingTechStack } = useGetTechStackQuery(); + + const getSplitTitle = (title) => { + if (!title) return { before: 'ARCHITECTING', highlighted: 'DISTRIBUTED', after: 'SYSTEMS.' }; + const words = title.split(' ').filter(Boolean); + const len = words.length; + if (len < 3) return { before: words[0] || '', highlighted: words[1] || '', after: '' }; + + const midIndex = Math.floor(len / 2); + const startHighlight = len % 2 === 0 ? midIndex - 1 : midIndex; + const highlightCount = len % 2 === 0 ? 2 : 1; + + return { + before: words.slice(0, startHighlight).join(' '), + highlighted: words.slice(startHighlight, startHighlight + highlightCount).join(' '), + after: words.slice(startHighlight + highlightCount).join(' ') + }; + }; + + if (isLoadingStats || isLoadingContent || isLoadingTechStack) { + return ( +
+
Loading...
+
+ ); + } + + if (!stats) return null; + + return ( +
+ {/* Hero Section */} +
+
+ + STATUS: ONLINE +
+

+ {getSplitTitle(pageContent?.home?.title).before}
+ + {getSplitTitle(pageContent?.home?.title).highlighted} +
+ {getSplitTitle(pageContent?.home?.title).after} +

+

+ {pageContent?.home?.subtitle || 'Specializing in high-performance backends, microservices, and cloud-native infrastructure. Building robust, scalable solutions for complex data pipelines.'} +

+ +
+ + +
+
+ + {/* Telemetry/Stats Section */} +
+
+
+
+ dns +
{stats.yearsExperience}
+
YEARS_EXPERIENCE
+
+
+ +
+
+
+ memory +
{stats.systemDeployed}
+
SYS_DEPLOYED
+
+
+ +
+
+
+ public +
{stats.uptimeSLA}
+
UPTIME_SLA
+
+
+ +
+
+
+ code_blocks +
{stats.commitsLogged}
+
COMMITS_LOGGED
+
+
+
+ + {/* Core Tech Stack Terminal Mockup */} +
+
+
+
+
+
+
+
+ tech_stack.sh +
+
+
+ sysadmin@backend:~$ cat core_stack.json
+ {'{'}
+   "languages": {JSON.stringify(techStack?.languages || ["Gol"])},
+   "databases": {JSON.stringify(techStack?.databases || ["Pos"])},
+   "infrastructure": {JSON.stringify(techStack?.infrastructure || ["Kub"])},
+   "messaging": {JSON.stringify(techStack?.messaging || ["Kaf"])}
+ {'}'}
+ sysadmin@backend:~$ +
+
+
+ ); +} diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx new file mode 100644 index 0000000..58f4181 --- /dev/null +++ b/frontend/src/pages/Login.jsx @@ -0,0 +1,87 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useLoginMutation } from '../store/apiSlice'; + +export default function Login() { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const navigate = useNavigate(); + const [login, { isLoading }] = useLoginMutation(); + + const handleLogin = async (e) => { + e.preventDefault(); + try { + const response = await login({ username, password }).unwrap(); + localStorage.setItem('isAuthenticated', 'true'); + localStorage.setItem('token', response.token); + localStorage.setItem('username', response.username); + localStorage.setItem('role', response.role); + navigate('/admin/dashboard'); + } catch (err) { + console.error('Login error:', err); + setError(err.data?.message || 'ACCESS_DENIED: Invalid credentials.'); + } + }; + + return ( +
+
+
+
+
+
+
+
+
+ root@auth:~ +
+
+ +
+

+ lock + ADMIN_AUTH_REQUIRED +

+ + {error && ( +
+ {error} +
+ )} + +
+
+ +
+ > + setUsername(e.target.value)} className="w-full bg-surface-container-low border border-surface-container-highest focus:border-primary-container text-on-background font-code-snippet pl-8 py-3 outline-none" /> +
+
+
+ +
+ > + setPassword(e.target.value)} className="w-full bg-surface-container-low border border-surface-container-highest focus:border-primary-container text-on-background font-code-snippet pl-8 py-3 outline-none" /> +
+
+ + +
+
+
+
+ ); +} diff --git a/frontend/src/pages/Projects.jsx b/frontend/src/pages/Projects.jsx new file mode 100644 index 0000000..12c3455 --- /dev/null +++ b/frontend/src/pages/Projects.jsx @@ -0,0 +1,66 @@ +import { useGetProjectsQuery, useGetPageContentQuery } from '../store/apiSlice'; + +export default function Projects() { + const { data: projects, isLoading: isLoadingProjects } = useGetProjectsQuery(); + const { data: pageContent, isLoading: isLoadingContent } = useGetPageContentQuery(); + + if (isLoadingProjects || isLoadingContent) { + return ( +
+
Loading...
+
+ ); + } + + if (!projects) return null; + + return ( +
+

+ {pageContent?.projects?.title || 'ARCHITECTURE_REPOSITORY'} +

+

+ {pageContent?.projects?.subtitle || 'A selection of distributed systems, microservices, and high-performance APIs I have designed and implemented.'} +

+ +
+ {projects.map((proj) => ( +
+
+ +
+
+
+ {proj.icon} +
+ v{proj.id}.0 +
+
+

{proj.title}

+
{proj.subtitle}
+
+ +

+ {proj.description} +

+ +
+
{proj.codeSnippet}
+
+ + +
+
+ ))} +
+
+ ); +}