Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions frontend/src/pages/Admin/AdminLayout.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<main className="flex-grow pt-32 pb-24 px-6 md:px-12 w-full max-w-[1400px] mx-auto flex flex-col md:flex-row gap-8">
{/* Admin Sidebar */}
<aside className="w-full md:w-64 flex-shrink-0">
<div className="bg-surface-container border border-surface-container-highest p-6 sticky top-32">
<div className="font-label-mono text-primary-container border-b border-surface-container-highest pb-4 mb-6 flex items-center gap-2">
<span className="material-symbols-outlined text-[18px]">admin_panel_settings</span>
ADMIN_CONSOLE
</div>
<nav className="flex flex-col gap-2">
{adminLinks.map((link) => {
const isActive = currentPath.includes(link.path);
return (
<Link
key={link.name}
to={link.path}
className={`font-code-snippet text-code-snippet p-3 border-l-2 transition-all duration-200 ${
isActive
? 'border-primary-container bg-surface-container-high text-on-surface'
: 'border-transparent text-on-surface-variant hover:border-surface-variant hover:bg-surface-container-lowest'
}`}
>
{link.name}
</Link>
);
})}
</nav>
</div>
</aside>

{/* Admin Content Area */}
<div className="flex-grow bg-surface-container/50 border border-surface-container-highest p-8 backdrop-blur-sm">
<Outlet />
</div>
</main>
);
}
81 changes: 81 additions & 0 deletions frontend/src/pages/Admin/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -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 <div className="font-label-mono text-primary-container"><span className="cursor-blink">Loading...</span></div>;

return (
<div className="space-y-8">
<h2 className="font-headline-md text-headline-md text-on-surface mb-6 border-b border-surface-container-highest pb-2 flex items-center gap-2">
<span className="material-symbols-outlined text-primary-container">dashboard</span>
System Telemetry
</h2>

{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-surface-container border border-surface-container-highest p-6 relative overflow-hidden group hover:border-primary-container transition-colors">
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
<span className="material-symbols-outlined text-6xl text-primary-container">dns</span>
</div>
<div className="font-label-mono text-label-mono text-surface-variant mb-2">TOTAL_VISITORS</div>
<div className="font-headline-xl text-headline-xl text-on-surface">{dashboardData?.totalVisitors || '0'}</div>
</div>

<div className="bg-surface-container border border-surface-container-highest p-6 relative overflow-hidden group hover:border-secondary-container transition-colors">
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
<span className="material-symbols-outlined text-6xl text-secondary-container">speed</span>
</div>
<div className="font-label-mono text-label-mono text-surface-variant mb-2">UPTIME</div>
<div className="font-headline-xl text-headline-xl text-on-surface">{dashboardData?.uptime || '0%'}</div>
</div>

<div className="bg-surface-container border border-surface-container-highest p-6 relative overflow-hidden group hover:border-error transition-colors">
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
<span className="material-symbols-outlined text-6xl text-error">mark_email_unread</span>
</div>
<div className="font-label-mono text-label-mono text-surface-variant mb-2">UNREAD_LOGS</div>
<div className="font-headline-xl text-headline-xl text-on-surface">{unreadCount}</div>
</div>
</div>

{/* Recent Messages */}
<div className="bg-surface-container border border-surface-container-highest">
<div className="p-4 border-b border-surface-container-highest flex justify-between items-center bg-surface-container-low">
<h3 className="font-label-mono text-label-mono text-on-surface-variant">RECENT_INBOX_LOGS</h3>
</div>
<div className="divide-y divide-surface-container-highest">
{recentMessages.map(msg => (
<div key={msg.id} className="p-4 flex flex-col md:flex-row md:items-center justify-between hover:bg-surface-container-high transition-colors">
<div className="mb-2 md:mb-0">
<div className="font-code-snippet text-sm text-on-surface flex items-center gap-2">
{msg.status === 'unread' && <div className="w-2 h-2 rounded-full bg-error"></div>}
{msg.user_name} &lt;{msg.user_email}&gt;
</div>
<div className="font-body-base text-on-surface-variant truncate max-w-lg mt-1">
{msg.message_body}
</div>
</div>
<div className="font-code-snippet text-xs text-surface-variant">
{new Date(msg.timestamp).toLocaleString()}
</div>
</div>
))}
{recentMessages.length === 0 && (
<div className="p-8 text-center font-code-snippet text-surface-variant">No recent logs.</div>
)}
</div>
</div>
</div>
);
}
166 changes: 166 additions & 0 deletions frontend/src/pages/Admin/ManageContent.jsx
Original file line number Diff line number Diff line change
@@ -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<ContentDTORequest> 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 <div className="font-label-mono text-primary-container"><span className="cursor-blink">Loading...</span></div>;

return (
<div>
<h2 className="font-headline-md text-headline-md text-on-surface mb-6 border-b border-surface-container-highest pb-2 flex items-center gap-2">
<span className="material-symbols-outlined text-primary-container">edit_document</span>
Manage Page Content
</h2>
<div className="bg-surface-container border border-surface-container-highest p-6 max-w-3xl">
<form onSubmit={handleSubmit} className="space-y-8">

{/* Home Page Content */}
<div>
<h3 className="font-label-mono text-primary-container mb-4">HOME_PAGE</h3>
<div className="space-y-4">
<div className="space-y-2">
<label className="font-label-mono text-label-mono text-surface-variant">Title</label>
<input type="text" name="homeTitle" value={formData.homeTitle} onChange={handleChange} className="w-full bg-surface-container-lowest border border-surface-container-highest focus:border-primary-container text-on-background font-code-snippet p-2 outline-none" />
</div>
<div className="space-y-2">
<label className="font-label-mono text-label-mono text-surface-variant">Subtitle</label>
<textarea name="homeSubtitle" value={formData.homeSubtitle} onChange={handleChange} rows="3" className="w-full bg-surface-container-lowest border border-surface-container-highest focus:border-primary-container text-on-background font-code-snippet p-2 outline-none resize-none"></textarea>
</div>
</div>
</div>

{/* Projects Page Content */}
<div className="border-t border-surface-container-highest pt-6">
<h3 className="font-label-mono text-primary-container mb-4">PROJECTS_PAGE</h3>
<div className="space-y-4">
<div className="space-y-2">
<label className="font-label-mono text-label-mono text-surface-variant">Title</label>
<input type="text" name="projectsTitle" value={formData.projectsTitle} onChange={handleChange} className="w-full bg-surface-container-lowest border border-surface-container-highest focus:border-primary-container text-on-background font-code-snippet p-2 outline-none" />
</div>
<div className="space-y-2">
<label className="font-label-mono text-label-mono text-surface-variant">Subtitle</label>
<textarea name="projectsSubtitle" value={formData.projectsSubtitle} onChange={handleChange} rows="2" className="w-full bg-surface-container-lowest border border-surface-container-highest focus:border-primary-container text-on-background font-code-snippet p-2 outline-none resize-none"></textarea>
</div>
</div>
</div>

{/* Experience Page Content */}
<div className="border-t border-surface-container-highest pt-6">
<h3 className="font-label-mono text-primary-container mb-4">EXPERIENCE_PAGE</h3>
<div className="space-y-4">
<div className="space-y-2">
<label className="font-label-mono text-label-mono text-surface-variant">Title</label>
<input type="text" name="experienceTitle" value={formData.experienceTitle} onChange={handleChange} className="w-full bg-surface-container-lowest border border-surface-container-highest focus:border-primary-container text-on-background font-code-snippet p-2 outline-none" />
</div>
<div className="space-y-2">
<label className="font-label-mono text-label-mono text-surface-variant">Subtitle</label>
<textarea name="experienceSubtitle" value={formData.experienceSubtitle} onChange={handleChange} rows="2" className="w-full bg-surface-container-lowest border border-surface-container-highest focus:border-primary-container text-on-background font-code-snippet p-2 outline-none resize-none"></textarea>
</div>
</div>
</div>

{/* Education Page Content */}
<div className="border-t border-surface-container-highest pt-6">
<h3 className="font-label-mono text-primary-container mb-4">EDUCATION_PAGE</h3>
<div className="space-y-4">
<div className="space-y-2">
<label className="font-label-mono text-label-mono text-surface-variant">Title</label>
<input type="text" name="educationTitle" value={formData.educationTitle} onChange={handleChange} className="w-full bg-surface-container-lowest border border-surface-container-highest focus:border-primary-container text-on-background font-code-snippet p-2 outline-none" />
</div>
<div className="space-y-2">
<label className="font-label-mono text-label-mono text-surface-variant">Subtitle</label>
<textarea name="educationSubtitle" value={formData.educationSubtitle} onChange={handleChange} rows="2" className="w-full bg-surface-container-lowest border border-surface-container-highest focus:border-primary-container text-on-background font-code-snippet p-2 outline-none resize-none"></textarea>
</div>
</div>
</div>

{/* Contact Page Content */}
<div className="border-t border-surface-container-highest pt-6">
<h3 className="font-label-mono text-primary-container mb-4">CONTACT_PAGE</h3>
<div className="space-y-4">
<div className="space-y-2">
<label className="font-label-mono text-label-mono text-surface-variant">Title</label>
<input type="text" name="contactTitle" value={formData.contactTitle} onChange={handleChange} className="w-full bg-surface-container-lowest border border-surface-container-highest focus:border-primary-container text-on-background font-code-snippet p-2 outline-none" />
</div>
<div className="space-y-2">
<label className="font-label-mono text-label-mono text-surface-variant">Subtitle</label>
<textarea name="contactSubtitle" value={formData.contactSubtitle} onChange={handleChange} rows="2" className="w-full bg-surface-container-lowest border border-surface-container-highest focus:border-primary-container text-on-background font-code-snippet p-2 outline-none resize-none"></textarea>
</div>
</div>
</div>

<button type="submit" disabled={isUpdating || isAdding} className="font-label-mono text-label-mono border border-surface-container-highest text-on-surface hover:text-surface-container-lowest hover:bg-primary-container hover:border-primary-container px-6 py-3 mt-4 transition-all duration-200">
{isUpdating || isAdding ? '[ Saving... ]' : '[ Save_Content ]'}
</button>
</form>
</div>
</div>
);
}
Loading