diff --git a/frontend/src/pages/Admin/AdminLayout.jsx b/frontend/src/pages/Admin/AdminLayout.jsx
new file mode 100644
index 0000000..9999559
--- /dev/null
+++ b/frontend/src/pages/Admin/AdminLayout.jsx
@@ -0,0 +1,54 @@
+import { Outlet, Link, useLocation } from 'react-router-dom';
+
+export default function AdminLayout() {
+ const location = useLocation();
+ const currentPath = location.pathname;
+
+ const adminLinks = [
+ { name: 'Dashboard', path: '/admin/dashboard' },
+ { name: 'Messages', path: '/admin/messages' },
+ { name: 'System Stats', path: '/admin/stats' },
+ { name: 'Projects', path: '/admin/projects' },
+ { name: 'Experience', path: '/admin/experience' },
+ { name: 'Education', path: '/admin/education' },
+ { name: 'Tech Stack', path: '/admin/tech-stack' },
+ { name: 'Page Content', path: '/admin/content' },
+ ];
+
+ return (
+
+ {/* Admin Sidebar */}
+
+
+ {/* Admin Content Area */}
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/Admin/Dashboard.jsx b/frontend/src/pages/Admin/Dashboard.jsx
new file mode 100644
index 0000000..b1e2ad2
--- /dev/null
+++ b/frontend/src/pages/Admin/Dashboard.jsx
@@ -0,0 +1,81 @@
+import { useGetMessagesQuery, useGetDashboardDataQuery } from '../../store/apiSlice';
+
+export default function Dashboard() {
+ const { data: messages = [], isLoading: isLoadingMessages } = useGetMessagesQuery();
+ const { data: dashboardData, isLoading: isLoadingDashboard } = useGetDashboardDataQuery();
+
+ const isLoading = isLoadingMessages || isLoadingDashboard;
+
+ // Sorting locally (frontend) ensures it works regardless of json-server version
+ const recentMessages = [...messages]
+ .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
+ .slice(0, 5);
+
+ const unreadCount = messages.filter(m => m.status === 'unread').length;
+
+ if (isLoading) return
Loading...
;
+
+ return (
+
+
+ dashboard
+ System Telemetry
+
+
+ {/* Stats Cards */}
+
+
+
+ dns
+
+
TOTAL_VISITORS
+
{dashboardData?.totalVisitors || '0'}
+
+
+
+
+ speed
+
+
UPTIME
+
{dashboardData?.uptime || '0%'}
+
+
+
+
+ mark_email_unread
+
+
UNREAD_LOGS
+
{unreadCount}
+
+
+
+ {/* Recent Messages */}
+
+
+
RECENT_INBOX_LOGS
+
+
+ {recentMessages.map(msg => (
+
+
+
+ {msg.status === 'unread' &&
}
+ {msg.user_name} <{msg.user_email}>
+
+
+ {msg.message_body}
+
+
+
+ {new Date(msg.timestamp).toLocaleString()}
+
+
+ ))}
+ {recentMessages.length === 0 && (
+
No recent logs.
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/pages/Admin/ManageContent.jsx b/frontend/src/pages/Admin/ManageContent.jsx
new file mode 100644
index 0000000..3f9b7ec
--- /dev/null
+++ b/frontend/src/pages/Admin/ManageContent.jsx
@@ -0,0 +1,166 @@
+import { useState, useEffect } from 'react';
+import { useGetPageContentQuery, useAddPageContentMutation, useUpdatePageContentMutation } from '../../store/apiSlice';
+
+export default function ManageContent() {
+ const { data: content, isLoading } = useGetPageContentQuery();
+ const [addPageContent, { isLoading: isAdding }] = useAddPageContentMutation();
+ const [updateContent, { isLoading: isUpdating }] = useUpdatePageContentMutation();
+
+ const [formData, setFormData] = useState({
+ homeTitle: '', homeSubtitle: '',
+ projectsTitle: '', projectsSubtitle: '',
+ experienceTitle: '', experienceSubtitle: '',
+ educationTitle: '', educationSubtitle: '',
+ contactTitle: '', contactSubtitle: ''
+ });
+
+ useEffect(() => {
+ if (content) {
+ setFormData({
+ homeTitle: content.home?.title || '',
+ homeSubtitle: content.home?.subtitle || '',
+ projectsTitle: content.projects?.title || '',
+ projectsSubtitle: content.projects?.subtitle || '',
+ experienceTitle: content.experience?.title || '',
+ experienceSubtitle: content.experience?.subtitle || '',
+ educationTitle: content.education?.title || '',
+ educationSubtitle: content.education?.subtitle || '',
+ contactTitle: content.contact?.title || '',
+ contactSubtitle: content.contact?.subtitle || ''
+ });
+ }
+ }, [content]);
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({ ...prev, [name]: value }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ const payload = {
+ home: {
+ title: formData.homeTitle,
+ subtitle: formData.homeSubtitle
+ },
+ projects: {
+ title: formData.projectsTitle,
+ subtitle: formData.projectsSubtitle
+ },
+ experience: {
+ title: formData.experienceTitle,
+ subtitle: formData.experienceSubtitle
+ },
+ education: {
+ title: formData.educationTitle,
+ subtitle: formData.educationSubtitle
+ },
+ contact: {
+ title: formData.contactTitle,
+ subtitle: formData.contactSubtitle
+ }
+ };
+
+ try {
+ // Backend expects List or individual?
+ // Based on my apiSlice, I'm sending the object.
+ await addPageContent(payload).unwrap();
+ } catch (err) {
+ console.error('Failed to update Page Content', err);
+ }
+ };
+
+ if (isLoading) return Loading...
;
+
+ return (
+
+
+ edit_document
+ Manage Page Content
+
+
+
+ );
+}
diff --git a/frontend/src/pages/Admin/ManageEducation.jsx b/frontend/src/pages/Admin/ManageEducation.jsx
new file mode 100644
index 0000000..e9d13f4
--- /dev/null
+++ b/frontend/src/pages/Admin/ManageEducation.jsx
@@ -0,0 +1,138 @@
+import { useState, useEffect } from 'react';
+import { useGetEducationQuery, useAddEducationMutation, useUpdateEducationMutation, useDeleteEducationMutation } from '../../store/apiSlice';
+
+export default function ManageEducation() {
+ const { data: education = [], isLoading } = useGetEducationQuery();
+ const [addEducation] = useAddEducationMutation();
+ const [updateEducation] = useUpdateEducationMutation();
+ const [deleteEducation] = useDeleteEducationMutation();
+
+ const [formData, setFormData] = useState({ id: '', type: 'degree', degree: '', title: '', institution: '', period: '', concentration: '', thesis: '', architectures: '', description: '', issued: '', icon: 'school', colorClass: 'primary' });
+ const [isEditing, setIsEditing] = useState(false);
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({ ...prev, [name]: value }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ const payload = { ...formData };
+ if (payload.type === 'degree') {
+ payload.architectures = typeof payload.architectures === 'string' ? payload.architectures.split(',').map(a => a.trim()).filter(a => a) : payload.architectures;
+ }
+
+ try {
+ if (isEditing) {
+ await updateEducation(payload).unwrap();
+ } else {
+ delete payload.id;
+ await addEducation(payload).unwrap();
+ }
+ resetForm();
+ } catch (err) {
+ console.error('Failed to save education', err);
+ }
+ };
+
+ const handleEdit = (item) => {
+ setFormData({
+ ...item,
+ architectures: item.architectures ? item.architectures.join(', ') : ''
+ });
+ setIsEditing(true);
+ window.scrollTo(0, 0);
+ };
+
+ const handleDelete = async (id) => {
+ try {
+ await deleteEducation(id).unwrap();
+ } catch (err) {
+ console.error('Failed to delete education', err);
+ }
+ };
+
+ const resetForm = () => {
+ setFormData({ id: '', type: 'degree', degree: '', title: '', institution: '', period: '', concentration: '', thesis: '', architectures: '', description: '', issued: '', icon: 'school', colorClass: 'primary' });
+ setIsEditing(false);
+ };
+
+ if (isLoading) return Loading...
;
+
+ return (
+
+
+ school
+ Manage Education
+
+
+ {/* Add Form */}
+
+
+ {/* List */}
+
+ {education.map(item => (
+
+
+
{item.title}
+
[{item.type}] {item.institution || item.issued}
+
+
+
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/frontend/src/pages/Admin/ManageExperience.jsx b/frontend/src/pages/Admin/ManageExperience.jsx
new file mode 100644
index 0000000..40c29b2
--- /dev/null
+++ b/frontend/src/pages/Admin/ManageExperience.jsx
@@ -0,0 +1,124 @@
+import { useState, useEffect } from 'react';
+import { useGetExperienceQuery, useAddExperienceMutation, useUpdateExperienceMutation, useDeleteExperienceMutation } from '../../store/apiSlice';
+
+export default function ManageExperience() {
+ const { data: experiences = [], isLoading } = useGetExperienceQuery();
+ const [addExperience] = useAddExperienceMutation();
+ const [updateExperience] = useUpdateExperienceMutation();
+ const [deleteExperience] = useDeleteExperienceMutation();
+
+ const [formData, setFormData] = useState({ id: '', title: '', company: '', period: '', bullets: '', technologies: '', icon: 'dns', colorClass: 'primary' });
+ const [isEditing, setIsEditing] = useState(false);
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({ ...prev, [name]: value }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ const payload = {
+ ...formData,
+ bullets: typeof formData.bullets === 'string' ? formData.bullets.split('\\n').map(b => b.trim()).filter(b => b) : formData.bullets,
+ technologies: typeof formData.technologies === 'string' ? formData.technologies.split(',').map(t => t.trim()) : formData.technologies
+ };
+
+ try {
+ if (isEditing) {
+ await updateExperience(payload).unwrap();
+ } else {
+ delete payload.id;
+ await addExperience(payload).unwrap();
+ }
+ resetForm();
+ } catch (err) {
+ console.error('Failed to save experience', err);
+ }
+ };
+
+ const handleEdit = (exp) => {
+ setFormData({
+ ...exp,
+ bullets: exp.bullets.join('\\n'),
+ technologies: exp.technologies.join(', ')
+ });
+ setIsEditing(true);
+ window.scrollTo(0, 0);
+ };
+
+ const handleDelete = async (id) => {
+ try {
+ await deleteExperience(id).unwrap();
+ } catch (err) {
+ console.error('Failed to delete experience', err);
+ }
+ };
+
+ const resetForm = () => {
+ setFormData({ id: '', title: '', company: '', period: '', bullets: '', technologies: '', icon: 'dns', colorClass: 'primary' });
+ setIsEditing(false);
+ };
+
+ if (isLoading) return Loading...
;
+
+ return (
+
+
+ work
+ Manage Experience
+
+
+ {/* Form */}
+
+
+ {/* List */}
+
+ {experiences.map(exp => (
+
+
+
{exp.title}
+
{exp.company}
+
+
+
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/frontend/src/pages/Admin/ManageMessages.jsx b/frontend/src/pages/Admin/ManageMessages.jsx
new file mode 100644
index 0000000..83e7bfc
--- /dev/null
+++ b/frontend/src/pages/Admin/ManageMessages.jsx
@@ -0,0 +1,154 @@
+import { useState } from 'react';
+import { useGetMessagesQuery, useUpdateMessageStatusMutation, useDeleteMessageMutation } from '../../store/apiSlice';
+
+export default function ManageMessages() {
+ const { data: messages = [], isLoading } = useGetMessagesQuery();
+ const [updateMessageStatus] = useUpdateMessageStatusMutation();
+ const [deleteMessage] = useDeleteMessageMutation();
+
+ const [replyingTo, setReplyingTo] = useState(null);
+ const [replyText, setReplyText] = useState('');
+ const [currentPage, setCurrentPage] = useState(1);
+ const messagesPerPage = 10;
+
+ // Sorting locally (frontend) ensures it works regardless of json-server version
+ const sortedMessages = [...messages].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
+
+ const handleReplyClick = async (msg) => {
+ setReplyingTo(msg);
+ setReplyText('');
+
+ // mark as read if unread
+ if (msg.status === 'unread') {
+ try {
+ await updateMessageStatus({ id: msg.id, status: 'read' }).unwrap();
+ } catch (err) {
+ console.error('Failed to mark read', err);
+ }
+ }
+ };
+
+ const handleSendReply = async (e) => {
+ e.preventDefault();
+ try {
+ await updateMessageStatus({ id: replyingTo.id, status: 'replied' }).unwrap();
+ setReplyingTo(prev => ({...prev, status: 'replied'}));
+ } catch (err) {
+ console.error('Failed to mark replied', err);
+ }
+ };
+
+ const handleDelete = async (id) => {
+ try {
+ await deleteMessage(id).unwrap();
+ if(replyingTo?.id === id) setReplyingTo(null);
+ } catch (err) {
+ console.error('Failed to delete message', err);
+ }
+ };
+
+ if (isLoading) return Loading...
;
+
+ // Pagination logic
+ const totalPages = Math.ceil(sortedMessages.length / messagesPerPage) || 1;
+ const currentMessages = sortedMessages.slice(
+ (currentPage - 1) * messagesPerPage,
+ currentPage * messagesPerPage
+ );
+
+ return (
+
+
+
+ INBOX_LOGS {sortedMessages.length > 0 && `(PG ${currentPage}/${totalPages})`}
+
+
+ {currentMessages.map(msg => (
+
handleReplyClick(msg)}
+ className={`p-4 cursor-pointer hover:bg-surface-container-high transition-colors ${replyingTo?.id === msg.id ? 'bg-surface-container-high border-l-2 border-primary-container' : 'border-l-2 border-transparent'}`}
+ >
+
+
+ {msg.status === 'unread' && }
+ {msg.status === 'replied' && done_all}
+ {msg.user_name}
+
+
+ {new Date(msg.timestamp).toLocaleDateString()}
+
+
+
+ {msg.message_body}
+
+
+ ))}
+ {sortedMessages.length === 0 &&
No logs found.
}
+
+ {/* Pagination Controls */}
+ {totalPages > 1 && (
+
+
+
+ PAGE {currentPage}
+
+
+
+ )}
+
+
+
+ {replyingTo ? (
+ <>
+
+
+
+
{replyingTo.user_name}
+
<{replyingTo.user_email}>
+
+
+
+
+ {replyingTo.message_body}
+
+
+
+
COMPOSE_REPLY
+
+
+ >
+ ) : (
+
+ > Select a log to view details.
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/pages/Admin/ManageProjects.jsx b/frontend/src/pages/Admin/ManageProjects.jsx
new file mode 100644
index 0000000..b50e14c
--- /dev/null
+++ b/frontend/src/pages/Admin/ManageProjects.jsx
@@ -0,0 +1,117 @@
+import { useState, useEffect } from 'react';
+import { useGetProjectsQuery, useAddProjectMutation, useUpdateProjectMutation, useDeleteProjectMutation } from '../../store/apiSlice';
+
+export default function ManageProjects() {
+ const { data: projects = [], isLoading } = useGetProjectsQuery();
+ const [addProject] = useAddProjectMutation();
+ const [updateProject] = useUpdateProjectMutation();
+ const [deleteProject] = useDeleteProjectMutation();
+
+ const [formData, setFormData] = useState({ id: '', title: '', subtitle: '', description: '', icon: 'code', codeSnippet: '', technologies: '' });
+ const [isEditing, setIsEditing] = useState(false);
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({ ...prev, [name]: value }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ const payload = {
+ ...formData,
+ technologies: typeof formData.technologies === 'string' ? formData.technologies.split(',').map(t => t.trim()) : formData.technologies
+ };
+
+ try {
+ if (isEditing) {
+ await updateProject(payload).unwrap();
+ } else {
+ delete payload.id;
+ await addProject(payload).unwrap();
+ }
+ resetForm();
+ } catch (err) {
+ console.error('Failed to save project', err);
+ }
+ };
+
+ const handleEdit = (proj) => {
+ setFormData({
+ ...proj,
+ technologies: proj.technologies.join(', ')
+ });
+ setIsEditing(true);
+ window.scrollTo(0, 0);
+ };
+
+ const handleDelete = async (id) => {
+ try {
+ await deleteProject(id).unwrap();
+ } catch (err) {
+ console.error('Failed to delete project', err);
+ }
+ };
+
+ const resetForm = () => {
+ setFormData({ id: '', title: '', subtitle: '', description: '', icon: 'code', codeSnippet: '', technologies: '' });
+ setIsEditing(false);
+ };
+
+ if (isLoading) return Loading...
;
+
+ return (
+
+
+ code
+ Manage Projects
+
+
+ {/* Form */}
+
+
+ {/* List */}
+
+ {projects.map(proj => (
+
+
+
{proj.title}
+
{proj.subtitle}
+
+
+
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/frontend/src/pages/Admin/ManageStats.jsx b/frontend/src/pages/Admin/ManageStats.jsx
new file mode 100644
index 0000000..e65f8ec
--- /dev/null
+++ b/frontend/src/pages/Admin/ManageStats.jsx
@@ -0,0 +1,73 @@
+import { useState, useEffect } from 'react';
+import { useGetStatsQuery, useAddStatsMutation, useUpdateStatsMutation } from '../../store/apiSlice';
+
+export default function ManageStats() {
+ const { data, isLoading } = useGetStatsQuery();
+ const [addStats, { isLoading: isAdding }] = useAddStatsMutation();
+ const [updateStats, { isLoading: isUpdating }] = useUpdateStatsMutation();
+ const [stats, setStats] = useState(null);
+
+ useEffect(() => {
+ if (data) setStats(data);
+ }, [data]);
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setStats(prev => ({ ...prev, [name]: value }));
+ };
+
+ const handleSave = async (e) => {
+ e.preventDefault();
+ try {
+ if (stats.id) {
+ await updateStats(stats).unwrap();
+ } else {
+ await addStats(stats).unwrap();
+ }
+ } catch (err) {
+ console.error('Failed to save stats', err);
+ }
+ };
+
+ if (isLoading || !stats) return Loading...
;
+
+ return (
+
+
+ monitoring
+ System Stats Overview
+
+
+
+ );
+}
diff --git a/frontend/src/pages/Admin/ManageTechStack.jsx b/frontend/src/pages/Admin/ManageTechStack.jsx
new file mode 100644
index 0000000..63ac6b1
--- /dev/null
+++ b/frontend/src/pages/Admin/ManageTechStack.jsx
@@ -0,0 +1,89 @@
+import { useState, useEffect } from 'react';
+import { useGetTechStackQuery, useAddTechStackMutation, useUpdateTechStackMutation } from '../../store/apiSlice';
+
+export default function ManageTechStack() {
+ const { data: stack, isLoading } = useGetTechStackQuery();
+ const [addTechStack, { isLoading: isAdding }] = useAddTechStackMutation();
+ const [updateTechStack, { isLoading: isUpdating }] = useUpdateTechStackMutation();
+
+ const [formData, setFormData] = useState({
+ languages: '',
+ databases: '',
+ infrastructure: '',
+ messaging: ''
+ });
+
+ useEffect(() => {
+ if (stack) {
+ setFormData({
+ languages: stack.languages?.join(', ') || '',
+ databases: stack.databases?.join(', ') || '',
+ infrastructure: stack.infrastructure?.join(', ') || '',
+ messaging: stack.messaging?.join(', ') || ''
+ });
+ }
+ }, [stack]);
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({ ...prev, [name]: value }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ const payload = {
+ languages: formData.languages.split(',').map(s => s.trim()).filter(s => s),
+ databases: formData.databases.split(',').map(s => s.trim()).filter(s => s),
+ infrastructure: formData.infrastructure.split(',').map(s => s.trim()).filter(s => s),
+ messaging: formData.messaging.split(',').map(s => s.trim()).filter(s => s)
+ };
+
+ try {
+ // Backend expects a list of stacks or a specific structure?
+ // Based on my apiSlice, I'm sending the object.
+ // But the backend expects List.
+ // I should adjust apiSlice or transform here.
+ // For now, let's assume the backend takes individual posts or we send the list.
+ await addTechStack(payload).unwrap();
+ } catch (err) {
+ console.error('Failed to update Tech Stack', err);
+ }
+ };
+
+ if (isLoading) return Loading...
;
+
+ return (
+
+
+ terminal
+ Manage Tech Stack
+
+
+
+ );
+}