Skip to content

Commit 596584e

Browse files
committed
feat: Add Privacy and Terms pages and navigation
Introduces dedicated pages for Privacy Policy and Terms of Service, along with their corresponding navigation links in the footer. Updates the `App.tsx` to route to these new pages. Also includes: - Enhancements to Supabase client creation for server-side usage. - Refactoring of state management in `lib/store.tsx` to include a new `Service` type and initial service data. - Integration of the new `Service` type and management into the admin panel (`app/admin.tsx`). - Display of services on the About page (`app/about.tsx`) with corresponding icons.
1 parent 326b4c8 commit 596584e

8 files changed

Lines changed: 293 additions & 19 deletions

File tree

App.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import ProjectDetailPage from './app/project-detail';
77
import BlogPage from './app/blog';
88
import BlogDetailPage from './app/blog-detail';
99
import AboutPage from './app/about';
10+
import PrivacyPage from './app/privacy';
11+
import TermsPage from './app/terms';
1012
import { Toaster } from 'sonner';
1113
import { RouterProvider, useRouter } from './lib/router';
1214
import { StoreProvider, useStore } from './lib/store';
@@ -63,6 +65,8 @@ function AppContent() {
6365
if (path === '/projects') return <ProjectsPage />;
6466
if (path === '/blog') return <BlogPage />;
6567
if (path === '/about') return <AboutPage />;
68+
if (path === '/privacy') return <PrivacyPage />;
69+
if (path === '/terms') return <TermsPage />;
6670

6771
// Dynamic Routes
6872
if (path.startsWith('/projects/')) {
@@ -91,4 +95,4 @@ export default function App() {
9195
<Toaster position="bottom-right" theme="system" />
9296
</div>
9397
);
94-
}
98+
}

app/about.tsx

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
11
import React from 'react';
22
import { useStore } from '../lib/store';
33
import { Navbar } from '../components/Navbar';
4-
import { Download, MapPin, Briefcase, GraduationCap, Mail, Github, Linkedin, Twitter, Instagram, Youtube, Gamepad2, Phone } from 'lucide-react';
4+
import { Download, MapPin, Briefcase, GraduationCap, Mail, Github, Linkedin, Twitter, Instagram, Youtube, Code, Smartphone, Cloud, Terminal, Layout, Database, Zap } from 'lucide-react';
55
import { motion } from 'framer-motion';
66

7+
const IconMap = {
8+
code: Code,
9+
smartphone: Smartphone,
10+
cloud: Cloud,
11+
terminal: Terminal,
12+
layout: Layout,
13+
database: Database
14+
};
15+
716
export default function AboutPage() {
8-
const { profile, experience, education } = useStore();
17+
const { profile, experience, education, services, theme } = useStore();
18+
19+
const getGithubUsername = (url: string) => {
20+
if (!url) return '';
21+
const cleanUrl = url.replace(/\/$/, ''); // Remove trailing slash
22+
return cleanUrl.split('/').pop();
23+
};
24+
25+
const username = getGithubUsername(profile.socials.github);
26+
const statsTheme = theme === 'dark' ? 'dark' : 'default';
927

1028
return (
1129
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-white transition-colors duration-300">
@@ -71,6 +89,38 @@ export default function AboutPage() {
7189
</div>
7290
</div>
7391

92+
{/* Services Section */}
93+
{services && services.length > 0 && (
94+
<div className="mb-20">
95+
<h2 className="text-2xl font-bold mb-8 flex items-center gap-3">
96+
<div className="p-2 bg-yellow-100 dark:bg-yellow-900/30 rounded-lg text-yellow-600 dark:text-yellow-400">
97+
<Zap className="h-6 w-6" />
98+
</div>
99+
What I Do
100+
</h2>
101+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
102+
{services.map((service, idx) => {
103+
const Icon = IconMap[service.icon] || Code;
104+
return (
105+
<motion.div
106+
key={service.id}
107+
initial={{ opacity: 0, y: 20 }}
108+
whileInView={{ opacity: 1, y: 0 }}
109+
transition={{ delay: idx * 0.1 }}
110+
className="bg-white border border-slate-200 dark:bg-slate-900/50 dark:border-white/5 p-6 rounded-xl hover:border-indigo-500/30 transition-all shadow-sm dark:shadow-none group"
111+
>
112+
<div className="h-12 w-12 bg-indigo-50 dark:bg-slate-800 rounded-lg flex items-center justify-center text-indigo-600 dark:text-indigo-400 mb-4 group-hover:bg-indigo-600 group-hover:text-white transition-colors">
113+
<Icon className="h-6 w-6" />
114+
</div>
115+
<h3 className="text-lg font-bold mb-2 text-slate-900 dark:text-white">{service.title}</h3>
116+
<p className="text-slate-600 dark:text-slate-400 text-sm leading-relaxed">{service.description}</p>
117+
</motion.div>
118+
);
119+
})}
120+
</div>
121+
</div>
122+
)}
123+
74124
<div className="grid md:grid-cols-2 gap-12 mb-20">
75125
{/* Experience */}
76126
<div>
@@ -157,18 +207,17 @@ export default function AboutPage() {
157207
</div>
158208

159209
{/* GitHub Stats Section */}
160-
{profile.socials.github && (
210+
{username && (
161211
<div className="border-t border-slate-200 dark:border-white/10 pt-16">
162-
<h2 className="text-2xl font-bold mb-8 text-center">Coding Activity</h2>
212+
<h2 className="text-2xl font-bold mb-8 text-center text-slate-900 dark:text-white">Coding Activity</h2>
163213
<div className="flex flex-col md:flex-row gap-6 justify-center items-center">
164-
{/* Replace 'username' in the URL with the actual GitHub username extracted from the profile URL if possible, or just use a placeholder mechanism */}
165214
<img
166-
src={`https://github-readme-stats.vercel.app/api?username=${profile.socials.github.split('/').pop()}&show_icons=true&theme=${'dark'}&bg_color=0f172a&title_color=fff&text_color=94a3b8&icon_color=6366f1&border_color=1e293b`}
215+
src={`https://github-readme-stats.vercel.app/api?username=${username}&show_icons=true&theme=${statsTheme}&bg_color=${statsTheme === 'dark' ? '0f172a' : 'ffffff'}&title_color=${statsTheme === 'dark' ? 'ffffff' : '0f172a'}&text_color=${statsTheme === 'dark' ? '94a3b8' : '475569'}&icon_color=6366f1&border_color=${statsTheme === 'dark' ? '1e293b' : 'e2e8f0'}`}
167216
alt="GitHub Stats"
168217
className="rounded-xl border border-slate-200 dark:border-white/10 shadow-lg max-w-full"
169218
/>
170219
<img
171-
src={`https://github-readme-stats.vercel.app/api/top-langs/?username=${profile.socials.github.split('/').pop()}&layout=compact&theme=${'dark'}&bg_color=0f172a&title_color=fff&text_color=94a3b8&border_color=1e293b`}
220+
src={`https://github-readme-stats.vercel.app/api/top-langs/?username=${username}&layout=compact&theme=${statsTheme}&bg_color=${statsTheme === 'dark' ? '0f172a' : 'ffffff'}&title_color=${statsTheme === 'dark' ? 'ffffff' : '0f172a'}&text_color=${statsTheme === 'dark' ? '94a3b8' : '475569'}&border_color=${statsTheme === 'dark' ? '1e293b' : 'e2e8f0'}`}
172221
alt="Top Languages"
173222
className="rounded-xl border border-slate-200 dark:border-white/10 shadow-lg max-w-full"
174223
/>

app/admin.tsx

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
'use client';
22

33
import React, { useState, useEffect } from 'react';
4-
import { useStore, Experience, Education } from '../lib/store';
4+
import { useStore, Experience, Education, Service } from '../lib/store';
55
import { useRouter } from '../lib/router';
6-
import { Plus, Trash2, FolderKanban, Mail, LogOut, Settings, FileText, User, Edit, X, Save, CheckCheck, ExternalLink, Briefcase, GraduationCap, ChevronLeft, ChevronRight } from 'lucide-react';
6+
import { Plus, Trash2, FolderKanban, Mail, LogOut, Settings, FileText, User, Edit, X, Save, CheckCheck, ExternalLink, Briefcase, GraduationCap, ChevronLeft, ChevronRight, Zap } from 'lucide-react';
77
import { toast } from 'sonner';
88

99
const ADMIN_ITEMS_PER_PAGE = 5;
1010

1111
export default function AdminPage() {
1212
const {
13-
projects, posts, messages, profile, experience, education,
13+
projects, posts, messages, profile, experience, education, services,
1414
deleteProject, addProject, updateProject,
1515
deletePost, addPost, updatePost,
1616
updateProfile, setExperience, updateExperience, setEducation, updateEducation,
17+
setServices, updateService, deleteService,
1718
markMessageRead, markAllMessagesRead, deleteMessage,
1819
logout
1920
} = useStore();
2021

2122
const { navigate } = useRouter();
22-
const [activeTab, setActiveTab] = useState<'overview' | 'resume' | 'projects' | 'blog' | 'messages'>('overview');
23+
const [activeTab, setActiveTab] = useState<'overview' | 'resume' | 'services' | 'projects' | 'blog' | 'messages'>('overview');
2324

2425
// -- PAGINATION STATES --
2526
const [projectPage, setProjectPage] = useState(1);
@@ -57,6 +58,11 @@ export default function AdminPage() {
5758
const [showExpForm, setShowExpForm] = useState(false);
5859
const [showEduForm, setShowEduForm] = useState(false);
5960

61+
// -- SERVICES STATES --
62+
const [serviceForm, setServiceForm] = useState<{title: string, description: string, icon: Service['icon']}>({ title: '', description: '', icon: 'code' });
63+
const [editingServiceId, setEditingServiceId] = useState<string | null>(null);
64+
const [showServiceForm, setShowServiceForm] = useState(false);
65+
6066
// Unread Messages Count
6167
const unreadCount = messages ? messages.filter(m => !m.is_read).length : 0;
6268

@@ -147,6 +153,33 @@ export default function AdminPage() {
147153
};
148154
const handleDeleteEdu = (id: string) => setEducation(education.filter(e => e.id !== id));
149155

156+
// --- SERVICES HANDLERS ---
157+
const handleEditService = (srv?: Service) => {
158+
if (srv) {
159+
setEditingServiceId(srv.id);
160+
setServiceForm({
161+
title: srv.title,
162+
description: srv.description,
163+
icon: srv.icon
164+
});
165+
} else {
166+
setEditingServiceId(null);
167+
setServiceForm({ title: '', description: '', icon: 'code' });
168+
}
169+
setShowServiceForm(true);
170+
}
171+
172+
const saveService = (e: React.FormEvent) => {
173+
e.preventDefault();
174+
if (editingServiceId) {
175+
updateService(editingServiceId, serviceForm);
176+
toast.success("Service updated!");
177+
} else {
178+
setServices([...services, { ...serviceForm, id: Math.random().toString(36).substr(2, 9) }]);
179+
toast.success("Service added!");
180+
}
181+
setShowServiceForm(false);
182+
}
150183

151184
// --- PROJECT HANDLERS ---
152185
const openProjectForm = (project?: any) => {
@@ -256,6 +289,7 @@ export default function AdminPage() {
256289
{[
257290
{id: 'overview', icon: User, label: 'Overview'},
258291
{id: 'resume', icon: Briefcase, label: 'Resume'},
292+
{id: 'services', icon: Zap, label: 'Services'},
259293
{id: 'projects', icon: FolderKanban, label: 'Projects'},
260294
{id: 'blog', icon: FileText, label: 'Blog'},
261295
{id: 'messages', icon: Mail, label: 'Messages'},
@@ -485,6 +519,69 @@ export default function AdminPage() {
485519
</div>
486520
)}
487521

522+
{activeTab === 'services' && (
523+
<div className="max-w-4xl">
524+
<h1 className="text-2xl font-bold text-slate-900 dark:text-white mb-6">Services Management</h1>
525+
526+
<div className="mb-12">
527+
<div className="flex justify-between items-center mb-4">
528+
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">Offered Services</h2>
529+
<button onClick={() => handleEditService()} className="text-sm bg-indigo-600 text-white px-3 py-1.5 rounded-lg flex items-center gap-1 hover:bg-indigo-700"><Plus className="h-4 w-4" /> Add</button>
530+
</div>
531+
532+
{showServiceForm && (
533+
<form onSubmit={saveService} className="bg-white border border-slate-200 dark:bg-slate-900 p-6 rounded-xl dark:border-white/10 mb-6 space-y-4 shadow-sm">
534+
<h3 className="text-slate-900 dark:text-white font-medium mb-2">{editingServiceId ? 'Edit Service' : 'New Service'}</h3>
535+
<input placeholder="Service Title (e.g., Web Development)" required value={serviceForm.title} onChange={e => setServiceForm({...serviceForm, title: e.target.value})} className="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-white/10 rounded px-3 py-2 text-slate-900 dark:text-white" />
536+
<textarea placeholder="Description" required value={serviceForm.description} onChange={e => setServiceForm({...serviceForm, description: e.target.value})} className="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-white/10 rounded px-3 py-2 text-slate-900 dark:text-white" rows={3} />
537+
538+
<div>
539+
<label className="block text-sm text-slate-500 dark:text-slate-400 mb-2">Icon</label>
540+
<div className="flex flex-wrap gap-2">
541+
{(['code', 'smartphone', 'cloud', 'terminal', 'layout', 'database'] as const).map(icon => (
542+
<button
543+
key={icon}
544+
type="button"
545+
onClick={() => setServiceForm({...serviceForm, icon})}
546+
className={`p-2 rounded border transition-colors capitalize text-xs ${serviceForm.icon === icon ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 border-slate-200 dark:border-white/10'}`}
547+
>
548+
{icon}
549+
</button>
550+
))}
551+
</div>
552+
</div>
553+
554+
<div className="flex justify-end gap-2">
555+
<button type="button" onClick={() => setShowServiceForm(false)} className="text-slate-500 dark:text-slate-400 text-sm">Cancel</button>
556+
<button type="submit" className="bg-indigo-600 text-white px-4 py-1.5 rounded-lg text-sm">{editingServiceId ? 'Update' : 'Save'}</button>
557+
</div>
558+
</form>
559+
)}
560+
561+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
562+
{services && services.map(srv => (
563+
<div key={srv.id} className="bg-white border border-slate-200 dark:bg-slate-900 dark:border-white/5 p-4 rounded-lg flex justify-between items-start group shadow-sm">
564+
<div className="flex items-start gap-3">
565+
<div className="p-2 bg-slate-100 dark:bg-slate-800 rounded text-indigo-600 dark:text-indigo-400">
566+
<Zap className="h-5 w-5" />
567+
</div>
568+
<div>
569+
<h3 className="font-bold text-slate-900 dark:text-white text-sm">{srv.title}</h3>
570+
<p className="text-slate-600 dark:text-slate-400 text-xs mt-1 leading-relaxed">{srv.description}</p>
571+
<div className="mt-2 inline-block px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-xs text-slate-500 dark:text-slate-400 capitalize">{srv.icon}</div>
572+
</div>
573+
</div>
574+
<div className="flex flex-col gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
575+
<button onClick={() => handleEditService(srv)} className="text-blue-500 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-500/10 p-1 rounded"><Edit className="h-4 w-4" /></button>
576+
<button onClick={() => deleteService(srv.id)} className="text-red-500 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10 p-1 rounded"><Trash2 className="h-4 w-4" /></button>
577+
</div>
578+
</div>
579+
))}
580+
</div>
581+
</div>
582+
</div>
583+
)}
584+
488585
{activeTab === 'projects' && (
489586
<div>
490587
<div className="flex justify-between items-center mb-6">

app/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ export default function Page() {
157157
© {new Date().getFullYear()} DevFolio. Built with Next.js & Supabase.
158158
</span>
159159
<div className="flex gap-6">
160-
<button className="text-slate-500 hover:text-slate-900 dark:hover:text-white text-sm">Privacy Policy</button>
161-
<button className="text-slate-500 hover:text-slate-900 dark:hover:text-white text-sm">Terms of Service</button>
160+
<button onClick={() => navigate('/privacy')} className="text-slate-500 hover:text-slate-900 dark:hover:text-white text-sm">Privacy Policy</button>
161+
<button onClick={() => navigate('/terms')} className="text-slate-500 hover:text-slate-900 dark:hover:text-white text-sm">Terms of Service</button>
162162
</div>
163163
</div>
164164
</div>

app/privacy.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { Navbar } from '../components/Navbar';
5+
6+
export default function PrivacyPage() {
7+
return (
8+
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-white transition-colors duration-300">
9+
<Navbar />
10+
<div className="pt-32 pb-20 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
11+
<h1 className="text-4xl font-bold mb-8">Privacy Policy</h1>
12+
<div className="prose prose-slate dark:prose-invert max-w-none">
13+
<p>Last updated: {new Date().toLocaleDateString()}</p>
14+
<p>
15+
Your privacy is important to us. It is our policy to respect your privacy regarding any information we may collect from you across our website.
16+
</p>
17+
18+
<h3>1. Information We Collect</h3>
19+
<p>
20+
We only ask for personal information when we truly need it to provide a service to you. We collect it by fair and lawful means, with your knowledge and consent. We also let you know why we’re collecting it and how it will be used.
21+
</p>
22+
23+
<h3>2. Log Data</h3>
24+
<p>
25+
When you visit our website, our servers may automatically log the standard data provided by your web browser. It includes your computer’s Internet Protocol (IP) address, your browser type and version, the pages you visit, the time and date of your visit, the time spent on each page, and other details.
26+
</p>
27+
28+
<h3>3. Use of Information</h3>
29+
<p>
30+
We may use the information we collect from you to:
31+
</p>
32+
<ul>
33+
<li>Provide and operate our services</li>
34+
<li>Respond to your comments, questions, and requests</li>
35+
<li>Send you technical notices, updates, security alerts, and support messages</li>
36+
</ul>
37+
38+
<h3>4. Third-Party Services</h3>
39+
<p>
40+
We do not share any personally identifying information publicly or with third-parties, except when required to by law.
41+
</p>
42+
43+
<h3>5. Contact Us</h3>
44+
<p>
45+
If you have any questions about this privacy policy, please contact us via the contact form on our website.
46+
</p>
47+
</div>
48+
</div>
49+
</div>
50+
);
51+
}

0 commit comments

Comments
 (0)