Add: Floating Back button for sub-pages, Cards Modal Update: Mini Preview Certificates, Website Preview in Admin Side, ReCaptcha badge to icon, emoji to icon

This commit is contained in:
ssbg04 committed 2026-06-11 14:50:18 +08:00
1 parent 17a34659b1
commit df552df131
17 files changed
+814 -226

No files matched your search

+90 -47
View File
@@ -11,7 +11,8 @@
"@supabase/supabase-js": "^2.107.0",
"bcryptjs": "^3.0.3",
"jose": "^6.2.3",
"next": "16.2.7",
"lucide-react": "^1.17.0",
"next": "^16.2.9",
"react": "19.2.4",
"react-dom": "19.2.4"
},
@@ -267,9 +268,9 @@
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -1023,9 +1024,9 @@
}
},
"node_modules/@next/env": {
"version": "16.2.7",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.7.tgz",
"integrity": "sha512-tMJizPlj6ZYpBMMdK8S0LJufrP4QTdR6pcv9KQ/bVETPAmg0j1mlHE9G2c38UyGHxoBapgwuj7XjbGJ2RcDFOg==",
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.9.tgz",
"integrity": "sha512-ki5VxxXfzD/9TDe13wyeTKIjQTAwBVpnr8KhRDUr8ltMUq1/NBpWNT5tiPoxiGl+PHM4X2ahSOiPk6iAimIzPg==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -1039,9 +1040,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.2.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.7.tgz",
"integrity": "sha512-vm1EDI/pVaBNNiychmxk3fft+OhQPVD9cIM/tReLZIQ3TfQ4kqI9DwKk00dzuS1ulC7icbrzCFrmRRlk9PfNdw==",
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.9.tgz",
"integrity": "sha512-HkfxNYUCmcct0Xsqib5KxqMSHV4AHJq857BNRchyBDs4YS19aHzVfn1kDuBYKqLLQBjXgnkIsjV2Kd4d2wzYhw==",
"cpu": [
"arm64"
],
@@ -1055,9 +1056,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "16.2.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.7.tgz",
"integrity": "sha512-O3IRSv1ZBL1zs0WrIgefTEcTKFVn+ryxBNe54erJ6KsD+2f/Mmt7g2jOYh8PSBdUwPtKQJuCsTMlZ7tIu2AcsQ==",
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.9.tgz",
"integrity": "sha512-7IAtK4MeybpqRV9GRABWEhJ62mOS+rzWOzOTFie4cSEtm12xsoOMJRcECoZx3FHPzFAqN/IJtHqWAFOLfl152w==",
"cpu": [
"x64"
],
@@ -1071,9 +1072,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.2.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.7.tgz",
"integrity": "sha512-Re6PZtjBDd0aMU+VcZcC/PrIvj4WhrjDYtMhhCVQamWN4L90EVP0pcEOBQD25prSlw7OzNw5QpHLWMilRLsRNw==",
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.9.tgz",
"integrity": "sha512-hBD75iWpUtkL9SmQmcRhmLomn9jgkPzCEkbOcLgHymPEKzv+6ONy13RRiIEz/iEObjkS2Jlb5gYS2XGoS3X4rw==",
"cpu": [
"arm64"
],
@@ -1087,9 +1088,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "16.2.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.7.tgz",
"integrity": "sha512-qyogG9QtBzWxgJfeGBvOEHI3851gTfCF3wLZ5RDLTBJGAmE9p1qDwKCOdrBrvBzRvYDT+gUDp72pzlSEfAXgNA==",
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.9.tgz",
"integrity": "sha512-qZTI3pf9SGc/obr8NkQAekBxmp1QK+kVm+VAf3BALLfFAj+1kUhkTxmrWpVos9R/UYIA8AWX2p6cGI5WdwzVUA==",
"cpu": [
"arm64"
],
@@ -1103,9 +1104,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "16.2.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.7.tgz",
"integrity": "sha512-Vhe4ZDuBpmMogrGi5D4R2Kq4JAQlj6+wvgaFYy31zfES0zPmt6TLA+cuYpM/OLrPZjo2MYQTHVqNUSCR6+fDZQ==",
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.9.tgz",
"integrity": "sha512-xm0HfRNX+UkH4R3c18ynswjj5o5uEj/7iI9p9omdtTSIsRCzQqkGMA+10nzJ4EHnYC3as65IMhbbl5fWRUWHYg==",
"cpu": [
"x64"
],
@@ -1119,9 +1120,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "16.2.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.7.tgz",
"integrity": "sha512-srvian89JahFLw1YLBEuhvPJ0DO5lpUeJQMXy4xYo7g628ZlNgXdNkqoxSAv9OYrBfByh6vxISMwW/mRbzCY+g==",
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.9.tgz",
"integrity": "sha512-QumimHkGEG6vM3PfEDWKyKen03NcqLOkeKB1EfcPe7VxzmEiCa4jNnMyBn/US5zcd/VE1CI+O8Ovb3lfjVHfGw==",
"cpu": [
"x64"
],
@@ -1135,9 +1136,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.2.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.7.tgz",
"integrity": "sha512-GX3wvLpULFuRFJzwHaKfm7QZJ18F4ZSuxlPJ96BoBglCzBmdSjyeBKF+ZhWhvL/ckxNfLnNa7bsObO2ipYpszw==",
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.9.tgz",
"integrity": "sha512-hzQpKZvw8rAwI6A2uQh6SacCSvNAXaIkPNsWwzqqfRiIMiXMfH936skDhz1OO6KpvdKkJrgHHtqQOq5PIXOvdQ==",
"cpu": [
"arm64"
],
@@ -1151,9 +1152,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.2.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.7.tgz",
"integrity": "sha512-J4WlM72NMk076Qsg0jTdK3SNXatlSdnjW7L7oNGLst1tAGjHrJh/FYi+pw9wyIjEtGRKDNzD0zuiY16oWYWVaw==",
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.9.tgz",
"integrity": "sha512-qr2VL3Ce5QrwgO2yh1ujSBawrimjVKX8FGF/cOynmdYKJY0BdHpGVNIRK1tqONB10Vkm25Ub1BD2bkjWs4+96w==",
"cpu": [
"x64"
],
@@ -1359,7 +1360,6 @@
"integrity": "sha512-5L7SUaFC1RyDraj2yRhyBzHTobyXHmohD100CChNtyPyleoq37Mqab5Gn8XEKI04dfN/oqPdpHk38MgcQWHbZg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -1952,6 +1952,40 @@
"node": ">=14.0.0"
}
},
"node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz",
@@ -4448,6 +4482,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.17.0.tgz",
"integrity": "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -4554,12 +4597,12 @@
"license": "MIT"
},
"node_modules/next": {
"version": "16.2.7",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.7.tgz",
"integrity": "sha512-eMJxgjRzBaj3olkP4cBamHDXL79A8FC6u1GcsO1D1Tsx8bw/LLXUJCaoajVxtnhD3A1IJqIT8IcRJjgBIPJq4w==",
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.9.tgz",
"integrity": "sha512-MEOJiq/UvuezAdqVSceHbqDgZt1kDw2tpGVOlsdIoJsQdbN2JY2hpVG4xnXGkbdJUOEWhnRfiu/O4Hpc9Juwww==",
"license": "MIT",
"dependencies": {
"@next/env": "16.2.7",
"@next/env": "16.2.9",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
@@ -4573,14 +4616,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.2.7",
"@next/swc-darwin-x64": "16.2.7",
"@next/swc-linux-arm64-gnu": "16.2.7",
"@next/swc-linux-arm64-musl": "16.2.7",
"@next/swc-linux-x64-gnu": "16.2.7",
"@next/swc-linux-x64-musl": "16.2.7",
"@next/swc-win32-arm64-msvc": "16.2.7",
"@next/swc-win32-x64-msvc": "16.2.7",
"@next/swc-darwin-arm64": "16.2.9",
"@next/swc-darwin-x64": "16.2.9",
"@next/swc-linux-arm64-gnu": "16.2.9",
"@next/swc-linux-arm64-musl": "16.2.9",
"@next/swc-linux-x64-gnu": "16.2.9",
"@next/swc-linux-x64-musl": "16.2.9",
"@next/swc-win32-arm64-msvc": "16.2.9",
"@next/swc-win32-x64-msvc": "16.2.9",
"sharp": "^0.34.5"
},
"peerDependencies": {
@@ -5295,9 +5338,9 @@
}
},
"node_modules/sharp/node_modules/semver": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
"integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
"version": "7.8.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
"license": "ISC",
"optional": true,
"bin": {
+2 -1
View File
@@ -12,7 +12,8 @@
"@supabase/supabase-js": "^2.107.0",
"bcryptjs": "^3.0.3",
"jose": "^6.2.3",
"next": "16.2.7",
"lucide-react": "^1.17.0",
"next": "^16.2.9",
"react": "19.2.4",
"react-dom": "19.2.4"
},
+4
View File
@@ -1,16 +1,20 @@
import { supabase } from "@/lib/supabase";
import ScrollReveal from "@/components/ScrollReveal";
import FloatingBackButton from "@/components/FloatingBackButton";
export const metadata = {
title: "Blog | Cris Charles Garcia",
description: "Blog — Thoughts, tutorials, and notes on software development.",
};
export const dynamic = 'force-dynamic';
export default async function BlogPage() {
const { data: articleRows } = await supabase.from('articles').select('*').order('id', { ascending: false });
return (
<>
<FloatingBackButton />
<ScrollReveal>
<div className="page-hero">
<span className="section-label">Blog</span>
+4
View File
@@ -1,16 +1,20 @@
import { supabase } from "@/lib/supabase";
import ScrollReveal from "@/components/ScrollReveal";
import FloatingBackButton from "@/components/FloatingBackButton";
export const metadata = {
title: "All Certifications | Cris Charles Garcia",
description: "Certifications — A collection of my professional certificates and courses.",
};
export const dynamic = 'force-dynamic';
export default async function CertificationsPage() {
const { data: certRows } = await supabase.from('certifications').select('*').order('id', { ascending: false });
return (
<>
<FloatingBackButton />
<ScrollReveal>
<div className="page-hero">
<span className="section-label">Certifications</span>
+9 -36
View File
@@ -1,6 +1,9 @@
import { supabase } from "@/lib/supabase";
import ScrollReveal from "@/components/ScrollReveal";
import Link from "next/link";
import { ProjectCard, ArticleCard, CertCard } from "@/components/ClientCardModals";
export const dynamic = 'force-dynamic';
export default async function Home() {
const recaptchaSiteKey = process.env.RECAPTCHA_SITE_KEY || "";
@@ -140,24 +143,7 @@ export default async function Home() {
</div>
<div className="project-grid">
{projectRows && projectRows.length > 0 ? projectRows.map((project, index) => (
<a href={project.url} target="_blank" rel="noopener noreferrer" className="project-card" key={index}>
<div className="card-img">
<img src={project.img_url || undefined} alt={project.title} loading="lazy" />
<div className="card-img-overlay"></div>
</div>
<div className="card-content">
<div className="card-header">
<h3>{project.title}</h3>
<span className="arrow-icon">↗</span>
</div>
<p>{project.des}</p>
<div className="stack-badges">
{project.techstack ? project.techstack.split(',').map((tag: string) => tag.trim()).map((tag: string, i: number) => (
<span className="stack-badge" key={i}>{tag}</span>
)) : null}
</div>
</div>
</a>
<ProjectCard project={project} key={index} />
)) : <p style={{ color: "var(--text-muted)", gridColumn: "1/-1", textAlign: "center", padding: "40px 0" }}>No projects available.</p>}
</div>
<div className="view-all-container">
@@ -175,17 +161,12 @@ export default async function Home() {
<span className="section-label">Certifications</span>
<h2 className="section-title">My Certifications</h2>
</div>
<div className="cert-grid">
<div className="cert-carousel-container">
{certRows && certRows.length > 0 ? certRows.map((cert, index) => (
<div className="cert-card" key={index}>
<div className="cert-icon">{cert.icon || "🏆"}</div>
<div className="cert-info">
<h3>{cert.title}</h3>
<p className="issuer">{cert.issuer}</p>
<span className="date">Issued {cert.date_issued}</span>
</div>
<div className="cert-carousel-item" key={index}>
<CertCard cert={cert} />
</div>
)) : <p style={{ color: "var(--text-muted)", gridColumn: "1/-1", textAlign: "center", padding: "40px 0" }}>No certifications available.</p>}
)) : <p style={{ color: "var(--text-muted)", width: "100%", textAlign: "center", padding: "40px 0" }}>No certifications available.</p>}
</div>
<div className="view-all-container">
<Link href="/certifications" className="btn secondary" id="btn-all-certs">All Certifications →</Link>
@@ -204,15 +185,7 @@ export default async function Home() {
</div>
<div className="blog-grid">
{articleRows && articleRows.length > 0 ? articleRows.map((article, index) => (
<article className="blog-card" key={index}>
<div className="blog-meta">
<span className="blog-date">{article.date_published}</span>
{article.tag && <span className="blog-tag">{article.tag}</span>}
</div>
<h3 className="blog-title">{article.title}</h3>
<p className="blog-excerpt">{article.excerpt}</p>
<a href={article.url} target="_blank" rel="noopener noreferrer" className="read-more">Read Article →</a>
</article>
<ArticleCard article={article} key={index} />
)) : <p style={{ color: "var(--text-muted)", gridColumn: "1/-1", textAlign: "center", padding: "40px 0" }}>No articles available.</p>}
</div>
<div className="view-all-container">
+4
View File
@@ -1,16 +1,20 @@
import { supabase } from "@/lib/supabase";
import ScrollReveal from "@/components/ScrollReveal";
import FloatingBackButton from "@/components/FloatingBackButton";
export const metadata = {
title: "All Projects | Cris Charles Garcia",
description: "All Projects — A complete archive of apps, websites, and experiments built by Cris Charles Garcia.",
};
export const dynamic = 'force-dynamic';
export default async function ProjectsPage() {
const { data: projectRows } = await supabase.from('projects').select('*').order('id', { ascending: false });
return (
<>
<FloatingBackButton />
<ScrollReveal>
<div className="page-hero">
<span className="section-label">Projects</span>
+250 -109
View File
@@ -1,7 +1,30 @@
"use client";
import { useState, useRef } from "react";
import { useState, useRef, useEffect } from "react";
import { useRouter } from "next/navigation";
import {
LayoutDashboard,
FolderKanban,
Award,
BookOpen,
Layers,
ShieldCheck,
LogOut,
Moon,
Sun,
Plus,
Edit2,
Trash2,
MonitorPlay,
ZoomIn,
ZoomOut,
Maximize2,
X,
Menu,
CheckCircle2,
AlertCircle
} from "lucide-react";
import {
updateSettings,
upsertProject,
@@ -31,17 +54,18 @@ export default function AdminDashboard({
initialTechStack: any[];
username: string;
}) {
const [activeTab, setActiveTab] = useState("profile");
const [activeTab, setActiveTab] = useState("dashboard");
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const router = useRouter();
// Dark mode toggle state
const [isDark, setIsDark] = useState(() => {
const [isDark, setIsDark] = useState(true);
useEffect(() => {
if (typeof window !== "undefined") {
return localStorage.getItem("theme") !== "light";
setIsDark(localStorage.getItem("theme") !== "light");
}
return true;
});
}, []);
const handleToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
const checked = e.target.checked;
@@ -58,6 +82,10 @@ export default function AdminDashboard({
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
const [isCertModalOpen, setIsCertModalOpen] = useState(false);
const [isArticleModalOpen, setIsArticleModalOpen] = useState(false);
const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false);
// Preview Zoom state
const [previewZoom, setPreviewZoom] = useState(1);
// Form refs for unsaved changes confirmation
const projectFormRef = useRef<HTMLFormElement>(null);
@@ -148,6 +176,32 @@ export default function AdminDashboard({
onChange={handleToggle}
/>
{/* Floating Toast Notification */}
{message.text && (
<div style={{
position: 'fixed',
bottom: '24px',
right: '24px',
background: message.type === 'success' ? '#065f46' : '#7f1d1d',
color: message.type === 'success' ? '#a7f3d0' : '#fecaca',
border: `1px solid ${message.type === 'success' ? '#059669' : '#dc2626'}`,
padding: '16px 20px',
borderRadius: '12px',
display: 'flex',
alignItems: 'center',
gap: '12px',
zIndex: 9999,
boxShadow: '0 10px 25px rgba(0,0,0,0.3)',
animation: 'modalSlideUp 0.3s ease-out'
}}>
{message.type === 'success' ? <CheckCircle2 size={24} /> : <AlertCircle size={24} />}
<div style={{ fontWeight: 600 }}>{message.text}</div>
<button onClick={() => setMessage({ type: "", text: "" })} style={{ background: 'none', border: 'none', color: 'inherit', cursor: 'pointer', marginLeft: 'auto' }}>
<X size={18} />
</button>
</div>
)}
{/* Mobile Header */}
<header className="mobile-top-bar">
<div className="logo-text" style={{ fontSize: "1.25rem" }}>
@@ -157,30 +211,16 @@ export default function AdminDashboard({
<div
className={`hamburger ${isSidebarOpen ? "active" : ""}`}
id="hamburger"
aria-label="Toggle menu"
role="button"
tabIndex={0}
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
>
<span className="bar"></span>
<span className="bar"></span>
<span className="bar"></span>
{isSidebarOpen ? <X /> : <Menu />}
</div>
</header>
{/* Sidebar Navigation */}
<div className={`sidebar ${isSidebarOpen ? "open" : ""}`} id="sidebar">
<div
className={`hamburger sidebar-close-btn ${isSidebarOpen ? "active" : ""}`}
aria-label="Close menu"
role="button"
tabIndex={0}
onClick={() => setIsSidebarOpen(false)}
>
<span className="bar"></span>
<span className="bar"></span>
<span className="bar"></span>
</div>
<div className="logo-container">
<div className="logo-text">
<span>Admin Panel</span>
@@ -188,23 +228,37 @@ export default function AdminDashboard({
</div>
</div>
<div className="sidebar-nav">
<button className={`nav-link ${activeTab === "profile" ? "active" : ""}`} onClick={() => switchTab("profile")}>👤 Profile & Stats</button>
<button className={`nav-link ${activeTab === "projects" ? "active" : ""}`} onClick={() => switchTab("projects")}>💻 Projects</button>
<button className={`nav-link ${activeTab === "certifications" ? "active" : ""}`} onClick={() => switchTab("certifications")}>🏆 Certifications</button>
<button className={`nav-link ${activeTab === "articles" ? "active" : ""}`} onClick={() => switchTab("articles")}>📰 Blog Articles</button>
<button className={`nav-link ${activeTab === "techstack" ? "active" : ""}`} onClick={() => switchTab("techstack")}>⚡ Tech Stack</button>
<button className={`nav-link ${activeTab === "account" ? "active" : ""}`} onClick={() => switchTab("account")}>🔒 Account Security</button>
<button className={`nav-link ${activeTab === "dashboard" ? "active" : ""}`} onClick={() => switchTab("dashboard")} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<LayoutDashboard size={18} /> Dashboard Overview
</button>
<button className={`nav-link ${activeTab === "projects" ? "active" : ""}`} onClick={() => switchTab("projects")} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<FolderKanban size={18} /> Projects ({initialProjects.length})
</button>
<button className={`nav-link ${activeTab === "certifications" ? "active" : ""}`} onClick={() => switchTab("certifications")} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Award size={18} /> Certifications ({initialCerts.length})
</button>
<button className={`nav-link ${activeTab === "articles" ? "active" : ""}`} onClick={() => switchTab("articles")} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<BookOpen size={18} /> Blog Articles ({initialArticles.length})
</button>
<button className={`nav-link ${activeTab === "techstack" ? "active" : ""}`} onClick={() => switchTab("techstack")} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Layers size={18} /> Tech Stack ({initialTechStack.length})
</button>
<button className={`nav-link ${activeTab === "account" ? "active" : ""}`} onClick={() => switchTab("account")} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<ShieldCheck size={18} /> Account Security
</button>
</div>
<div className="sidebar-footer" style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<span style={{ fontSize: '0.85rem', color: 'var(--text-muted)', fontWeight: '600' }}>THEME</span>
<label htmlFor="dark-mode-toggle" className="theme-switch" title="Toggle Theme" aria-label="Toggle dark/light mode" style={{ margin: 0, transform: 'scale(0.85)' }}>
<span className="theme-switch-thumb"></span>
<span className="theme-switch-icon icon-moon">🌙</span>
<span className="theme-switch-icon icon-sun">☀️</span>
<span className="theme-switch-icon icon-moon"><Moon size={14} /></span>
<span className="theme-switch-icon icon-sun"><Sun size={14} /></span>
</label>
</div>
<button onClick={handleLogout} className="logout-btn" style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)' }}>Logout ➔</button>
<button onClick={handleLogout} className="logout-btn" style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: '6px' }}>
<LogOut size={16} /> Logout
</button>
</div>
</div>
@@ -219,23 +273,44 @@ export default function AdminDashboard({
<h1 id="tab-title-text">{activeTab.toUpperCase()} MANAGEMENT</h1>
<p>Welcome back, {username}.</p>
</div>
<div className="user-badge">
<span className="status-dot"></span>
<span>Active Session</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<button
onClick={() => setIsPreviewModalOpen(true)}
className="btn primary"
style={{ display: 'flex', alignItems: 'center', gap: '8px', background: 'var(--accent)' }}
>
<MonitorPlay size={18} /> Live Preview
</button>
<div className="user-badge">
<span className="status-dot"></span>
<span>Active Session</span>
</div>
</div>
</div>
{/* System Alerts */}
{message.text && (
<div className={`alert alert-${message.type === "success" ? "success" : "danger"}`}>
<span>{message.type === "success" ? "✓" : "⚠️"}</span>
<div>{message.text}</div>
</div>
)}
{/* ================= TAB: PROFILE ================= */}
{activeTab === "profile" && (
{/* ================= TAB: DASHBOARD / PROFILE ================= */}
{activeTab === "dashboard" && (
<div className="tab-pane active">
{/* Stats Overview Cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '20px', marginBottom: '24px' }}>
{[
{ title: "Total Projects", count: initialProjects.length, icon: <FolderKanban size={24} /> },
{ title: "Certifications", count: initialCerts.length, icon: <Award size={24} /> },
{ title: "Blog Articles", count: initialArticles.length, icon: <BookOpen size={24} /> },
{ title: "Tech Stack", count: initialTechStack.length, icon: <Layers size={24} /> },
].map((stat, i) => (
<div key={i} className="card" style={{ marginBottom: 0, padding: '24px', display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ background: 'rgba(37, 99, 235, 0.1)', padding: '12px', borderRadius: '12px', color: 'var(--accent)' }}>
{stat.icon}
</div>
<div>
<div style={{ fontSize: '2rem', fontWeight: '700', fontFamily: 'Outfit' }}>{stat.count}</div>
<div style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>{stat.title}</div>
</div>
</div>
))}
</div>
<form onSubmit={(e) => { e.preventDefault(); handleAction(updateSettings, new FormData(e.currentTarget), "Settings updated!"); }}>
<div className="card">
<div className="card-title">📝 About Me Bio Copy</div>
@@ -249,24 +324,6 @@ export default function AdminDashboard({
</div>
</div>
<div className="card">
<div className="card-title">📈 Statistics Counters</div>
<div className="form-grid">
<div className="form-group">
<label htmlFor="years_coding">Years Coding Count</label>
<input type="text" name="years_coding" id="years_coding" required defaultValue={initialSettings?.years_coding} />
</div>
<div className="form-group">
<label htmlFor="projects_count">Projects Built Count</label>
<input type="text" name="projects_count" id="projects_count" required defaultValue={initialSettings?.projects_count} />
</div>
<div className="form-group">
<label htmlFor="certifications_count">Certifications Count</label>
<input type="text" name="certifications_count" id="certifications_count" required defaultValue={initialSettings?.certifications_count} />
</div>
</div>
</div>
<div className="card">
<div className="card-title">🔗 Social Links</div>
<div className="form-grid">
@@ -293,7 +350,7 @@ export default function AdminDashboard({
</div>
</div>
<button type="submit" className="btn primary" disabled={isSubmitting}>Save Changes</button>
<button type="submit" className="btn primary" disabled={isSubmitting}>Save Profile Changes</button>
</form>
</div>
)}
@@ -305,13 +362,16 @@ export default function AdminDashboard({
<button
className="btn primary"
onClick={() => { setEditProject(null); setIsProjectModalOpen(true); }}
style={{ display: 'flex', alignItems: 'center', gap: '6px' }}
>
➕ Add New Project
<Plus size={18} /> Add New Project
</button>
</div>
<div className="card">
<div className="card-title">📁 Current Projects</div>
<div className="card-title" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<FolderKanban size={24} /> Current Projects
</div>
<div className="table-container">
<table>
<thead>
@@ -328,7 +388,9 @@ export default function AdminDashboard({
<td>{proj.techstack}</td>
<td>
<div className="btn-actions">
<button className="btn secondary btn-sm" onClick={() => { setEditProject(proj); setIsProjectModalOpen(true); }}>Edit</button>
<button className="btn secondary btn-sm" onClick={() => { setEditProject(proj); setIsProjectModalOpen(true); }} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<Edit2 size={14} /> Edit
</button>
<button className="btn danger btn-sm" onClick={async () => {
if (confirm("Are you sure?")) {
setIsSubmitting(true);
@@ -336,7 +398,9 @@ export default function AdminDashboard({
setIsSubmitting(false);
router.refresh();
}
}}>Delete</button>
}} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<Trash2 size={14} /> Delete
</button>
</div>
</td>
</tr>
@@ -348,13 +412,13 @@ export default function AdminDashboard({
{/* Project Modal */}
{isProjectModalOpen && (
<div className="modal-overlay" onClick={handleCloseProject}>
<div className="modal-container" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>{editProject ? "✏️ Edit Project" : "➕ Add New Project"}</h2>
<button className="modal-close" onClick={handleCloseProject}>×</button>
<div className="custom-modal-overlay" onClick={handleCloseProject}>
<div className="custom-modal-container" onClick={(e) => e.stopPropagation()}>
<div className="custom-modal-header">
<h2>{editProject ? "Edit Project" : "Add New Project"}</h2>
<button className="custom-modal-close" onClick={handleCloseProject}><X size={24} /></button>
</div>
<div className="modal-body">
<div className="custom-modal-body">
<form ref={projectFormRef} onSubmit={(e) => {
e.preventDefault();
const form = e.currentTarget;
@@ -374,20 +438,28 @@ export default function AdminDashboard({
<label>Tech Stack</label>
<input type="text" name="techstack" defaultValue={editProject?.techstack} />
</div>
<div className="form-group">
<label>Project URL</label>
<input type="text" name="url" defaultValue={editProject?.url} />
</div>
<div className="form-group">
<label>Image URL</label>
<input type="text" name="img_url" defaultValue={editProject?.img_url} />
</div>
<div className="form-group">
<label>Old URL / Extra Link</label>
<input type="text" name="url" defaultValue={editProject?.url} />
</div>
<div className="form-group">
<label>Live Demo URL</label>
<input type="url" name="live_demo_url" defaultValue={editProject?.live_demo_url} />
</div>
<div className="form-group">
<label>Repository URL</label>
<input type="url" name="repo_url" defaultValue={editProject?.repo_url} />
</div>
<div className="form-group form-group-full">
<label>Description</label>
<textarea name="des" required defaultValue={editProject?.des}></textarea>
<textarea name="des" required defaultValue={editProject?.des} rows={4}></textarea>
</div>
</div>
<div className="modal-footer" style={{ display: "flex", gap: "12px", marginTop: "20px", padding: "16px 0 0 0", borderTop: "1px solid var(--border-color)" }}>
<div style={{ display: "flex", gap: "12px", marginTop: "20px" }}>
<button type="button" className="btn secondary" onClick={handleCloseProject}>Cancel</button>
<button type="submit" className="btn primary" disabled={isSubmitting}>{editProject ? "Update Project" : "Add Project"}</button>
</div>
@@ -406,13 +478,16 @@ export default function AdminDashboard({
<button
className="btn primary"
onClick={() => { setEditCert(null); setIsCertModalOpen(true); }}
style={{ display: 'flex', alignItems: 'center', gap: '6px' }}
>
➕ Add Certification
<Plus size={18} /> Add Certification
</button>
</div>
<div className="card">
<div className="card-title">🏆 Current Certifications</div>
<div className="card-title" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Award size={24} /> Current Certifications
</div>
<div className="table-container">
<table>
<thead>
@@ -426,12 +501,12 @@ export default function AdminDashboard({
<tbody>
{initialCerts.map(cert => (
<tr key={cert.id}>
<td>{cert.icon}</td>
<td>{cert.icon?.includes('fa-') ? <i className={cert.icon}></i> : cert.icon}</td>
<td>{cert.title}</td>
<td>{cert.issuer}</td>
<td>
<div className="btn-actions">
<button className="btn secondary btn-sm" onClick={() => { setEditCert(cert); setIsCertModalOpen(true); }}>Edit</button>
<button className="btn secondary btn-sm" onClick={() => { setEditCert(cert); setIsCertModalOpen(true); }} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}><Edit2 size={14} /> Edit</button>
<button className="btn danger btn-sm" onClick={async () => {
if (confirm("Are you sure?")) {
setIsSubmitting(true);
@@ -439,7 +514,7 @@ export default function AdminDashboard({
setIsSubmitting(false);
router.refresh();
}
}}>Delete</button>
}} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}><Trash2 size={14} /> Delete</button>
</div>
</td>
</tr>
@@ -451,13 +526,13 @@ export default function AdminDashboard({
{/* Certification Modal */}
{isCertModalOpen && (
<div className="modal-overlay" onClick={handleCloseCert}>
<div className="modal-container" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>{editCert ? "✏️ Edit Certification" : "➕ Add Certification"}</h2>
<button className="modal-close" onClick={handleCloseCert}>×</button>
<div className="custom-modal-overlay" onClick={handleCloseCert}>
<div className="custom-modal-container" onClick={(e) => e.stopPropagation()}>
<div className="custom-modal-header">
<h2>{editCert ? "Edit Certification" : "Add Certification"}</h2>
<button className="custom-modal-close" onClick={handleCloseCert}><X size={24} /></button>
</div>
<div className="modal-body">
<div className="custom-modal-body">
<form ref={certFormRef} onSubmit={(e) => {
e.preventDefault();
const form = e.currentTarget;
@@ -483,10 +558,14 @@ export default function AdminDashboard({
</div>
<div className="form-group">
<label>Icon</label>
<input type="text" name="icon" defaultValue={editCert?.icon || "🏆"} />
<input type="text" name="icon" defaultValue={editCert?.icon || "🏆"} placeholder="Emoji or 'fa-solid fa-award'" />
</div>
<div className="form-group">
<label>Image URL</label>
<input type="text" name="img_url" defaultValue={editCert?.img_url} placeholder="https://example.com/cert.png" />
</div>
</div>
<div className="modal-footer" style={{ display: "flex", gap: "12px", marginTop: "20px", padding: "16px 0 0 0", borderTop: "1px solid var(--border-color)" }}>
<div style={{ display: "flex", gap: "12px", marginTop: "20px" }}>
<button type="button" className="btn secondary" onClick={handleCloseCert}>Cancel</button>
<button type="submit" className="btn primary" disabled={isSubmitting}>{editCert ? "Update" : "Add"}</button>
</div>
@@ -505,13 +584,16 @@ export default function AdminDashboard({
<button
className="btn primary"
onClick={() => { setEditArticle(null); setIsArticleModalOpen(true); }}
style={{ display: 'flex', alignItems: 'center', gap: '6px' }}
>
➕ Add Article
<Plus size={18} /> Add Article
</button>
</div>
<div className="card">
<div className="card-title">📰 Current Articles</div>
<div className="card-title" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<BookOpen size={24} /> Current Articles
</div>
<div className="table-container">
<table>
<thead>
@@ -530,7 +612,7 @@ export default function AdminDashboard({
<td>{art.date_published}</td>
<td>
<div className="btn-actions">
<button className="btn secondary btn-sm" onClick={() => { setEditArticle(art); setIsArticleModalOpen(true); }}>Edit</button>
<button className="btn secondary btn-sm" onClick={() => { setEditArticle(art); setIsArticleModalOpen(true); }} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}><Edit2 size={14} /> Edit</button>
<button className="btn danger btn-sm" onClick={async () => {
if (confirm("Are you sure?")) {
setIsSubmitting(true);
@@ -538,7 +620,7 @@ export default function AdminDashboard({
setIsSubmitting(false);
router.refresh();
}
}}>Delete</button>
}} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}><Trash2 size={14} /> Delete</button>
</div>
</td>
</tr>
@@ -550,13 +632,13 @@ export default function AdminDashboard({
{/* Article Modal */}
{isArticleModalOpen && (
<div className="modal-overlay" onClick={handleCloseArticle}>
<div className="modal-container" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>{editArticle ? "✏️ Edit Article" : "➕ Add Article"}</h2>
<button className="modal-close" onClick={handleCloseArticle}>×</button>
<div className="custom-modal-overlay" onClick={handleCloseArticle}>
<div className="custom-modal-container" onClick={(e) => e.stopPropagation()}>
<div className="custom-modal-header">
<h2>{editArticle ? "Edit Article" : "Add Article"}</h2>
<button className="custom-modal-close" onClick={handleCloseArticle}><X size={24} /></button>
</div>
<div className="modal-body">
<div className="custom-modal-body">
<form ref={articleFormRef} onSubmit={(e) => {
e.preventDefault();
const form = e.currentTarget;
@@ -586,10 +668,10 @@ export default function AdminDashboard({
</div>
<div className="form-group form-group-full">
<label>Excerpt</label>
<textarea name="excerpt" required defaultValue={editArticle?.excerpt}></textarea>
<textarea name="excerpt" required defaultValue={editArticle?.excerpt} rows={4}></textarea>
</div>
</div>
<div className="modal-footer" style={{ display: "flex", gap: "12px", marginTop: "20px", padding: "16px 0 0 0", borderTop: "1px solid var(--border-color)" }}>
<div style={{ display: "flex", gap: "12px", marginTop: "20px" }}>
<button type="button" className="btn secondary" onClick={handleCloseArticle}>Cancel</button>
<button type="submit" className="btn primary" disabled={isSubmitting}>{editArticle ? "Update" : "Add"}</button>
</div>
@@ -605,7 +687,9 @@ export default function AdminDashboard({
{activeTab === "techstack" && (
<div className="tab-pane active">
<div className="card">
<div className="card-title">➕ Add Technology</div>
<div className="card-title" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Layers size={24} /> Add Technology
</div>
<form onSubmit={(e) => {
e.preventDefault();
const form = e.currentTarget;
@@ -615,12 +699,16 @@ export default function AdminDashboard({
<label>Technology Name</label>
<input type="text" name="tech_name" required placeholder="e.g. Next.js, GraphQL, PostgreSQL" />
</div>
<button type="submit" className="btn primary" disabled={isSubmitting}>Add Tech</button>
<button type="submit" className="btn primary" disabled={isSubmitting} style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<Plus size={18} /> Add Tech
</button>
</form>
</div>
<div className="card">
<div className="card-title">⚡ Current Technologies</div>
<div className="card-title" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Layers size={24} /> Current Technologies
</div>
{initialTechStack.length === 0 ? (
<p style={{ color: "var(--text-muted)" }}>No technologies configured.</p>
) : (
@@ -648,7 +736,14 @@ export default function AdminDashboard({
{activeTab === "account" && (
<div className="tab-pane active">
<div className="card">
<div className="card-title">🔒 Change Admin Credentials</div>
<div className="card-title" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<ShieldCheck size={24} /> Change Admin Credentials
</div>
<div style={{ marginBottom: '24px', paddingBottom: '24px', borderBottom: '1px solid var(--border-color)' }}>
<p style={{ color: 'var(--text-muted)' }}>Update your username or set a new password. You will be logged out upon success.</p>
</div>
<form onSubmit={async (e) => {
e.preventDefault();
setIsSubmitting(true);
@@ -665,10 +760,18 @@ export default function AdminDashboard({
}
}}>
<div className="form-grid">
<div className="form-group">
<div className="form-group form-group-full">
<label>New Username</label>
<input type="text" name="new_username" required defaultValue={username} placeholder="Username" />
</div>
{/* Visual Divider */}
<div className="form-group-full" style={{ display: 'flex', alignItems: 'center', gap: '16px', margin: '10px 0 20px' }}>
<div style={{ height: '1px', flex: 1, background: 'var(--border-color)' }}></div>
<span style={{ color: 'var(--text-muted)', fontSize: '0.85rem', fontWeight: 600 }}>PASSWORD</span>
<div style={{ height: '1px', flex: 1, background: 'var(--border-color)' }}></div>
</div>
<div className="form-group">
<label>New Password (leave blank to keep current)</label>
<input type="password" name="new_password" placeholder="••••••••" />
@@ -678,7 +781,7 @@ export default function AdminDashboard({
<input type="password" name="confirm_password" placeholder="••••••••" />
</div>
</div>
<div style={{ marginTop: "20px" }}>
<div style={{ marginTop: "30px" }}>
<button type="submit" className="btn primary" disabled={isSubmitting}>
Update Credentials
</button>
@@ -688,6 +791,44 @@ export default function AdminDashboard({
</div>
)}
</main>
{/* ================= LIVE PREVIEW MODAL ================= */}
{isPreviewModalOpen && (
<div className="custom-modal-overlay" style={{ zIndex: 3000, padding: 0 }}>
<div className="custom-modal-container" style={{ width: '95vw', maxWidth: '1600px', height: '90vh', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<div className="custom-modal-header" style={{ padding: '12px 24px', background: '#090d16', borderBottom: '1px solid #1e293b' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<MonitorPlay size={20} color="#3b82f6" />
<h2 style={{ fontSize: '1.2rem' }}>Live Site Preview</h2>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', background: '#1e293b', borderRadius: '8px', padding: '4px' }}>
<button onClick={() => setPreviewZoom(z => Math.max(0.5, z - 0.1))} style={{ background: 'none', border: 'none', color: '#94a3b8', cursor: 'pointer', padding: '4px 8px' }} title="Zoom Out"><ZoomOut size={16} /></button>
<span style={{ color: '#f8fafc', fontSize: '0.85rem', width: '45px', textAlign: 'center' }}>{Math.round(previewZoom * 100)}%</span>
<button onClick={() => setPreviewZoom(z => Math.min(1.5, z + 0.1))} style={{ background: 'none', border: 'none', color: '#94a3b8', cursor: 'pointer', padding: '4px 8px' }} title="Zoom In"><ZoomIn size={16} /></button>
<button onClick={() => setPreviewZoom(1)} style={{ background: 'none', border: 'none', color: '#94a3b8', cursor: 'pointer', padding: '4px 8px', borderLeft: '1px solid #334155' }} title="Reset Zoom"><Maximize2 size={16} /></button>
</div>
<button className="custom-modal-close" onClick={() => setIsPreviewModalOpen(false)}><X size={24} /></button>
</div>
</div>
<div style={{ flex: 1, position: 'relative', background: '#0f172a', overflow: 'auto', display: 'flex', alignItems: 'flex-start', justifyContent: 'center' }}>
<div style={{
width: '100%',
height: '100%',
transform: `scale(${previewZoom})`,
transformOrigin: 'top center',
transition: 'transform 0.2s ease'
}}>
<iframe
src="/"
style={{ width: '100%', height: '100%', border: 'none', background: '#fff' }}
title="Portfolio Live Preview"
/>
</div>
</div>
</div>
</div>
)}
</>
);
}
+5 -2
View File
@@ -40,8 +40,10 @@ export async function upsertProject(formData: FormData) {
const techstack = formData.get('techstack') as string;
const url = formData.get('url') as string;
const img_url = formData.get('img_url') as string;
const live_demo_url = formData.get('live_demo_url') as string;
const repo_url = formData.get('repo_url') as string;
const payload = { title, des, techstack, url, img_url };
const payload = { title, des, techstack, url, img_url, live_demo_url, repo_url };
let result;
if (id) {
@@ -69,8 +71,9 @@ export async function upsertCertification(formData: FormData) {
const issuer = formData.get('issuer') as string;
const date_issued = formData.get('date_issued') as string;
const icon = formData.get('icon') as string;
const img_url = formData.get('img_url') as string;
const payload = { title, issuer, date_issued, icon };
const payload = { title, issuer, date_issued, icon, img_url };
let result;
if (id) {
+2 -2
View File
@@ -8,8 +8,8 @@ import { cookies } from "next/headers";
const SECRET_KEY = process.env.JWT_SECRET || "fallback_secret_key_for_development";
export async function handleLogin(formData: FormData) {
const username = formData.get("username")?.toString();
const password = formData.get("password")?.toString();
const username = formData.get("username")?.toString().trim();
const password = formData.get("password")?.toString().trim();
if (!username || !password) {
return { success: false, error: "Username and password are required" };
+2
View File
@@ -5,6 +5,8 @@ import { jwtVerify } from "jose";
const SECRET_KEY = process.env.JWT_SECRET || "fallback_secret_key_for_development";
export const dynamic = 'force-dynamic';
export default async function AdminPage() {
// Check if Supabase is configured
const isDummy = (process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://dummy.supabase.co') === 'https://dummy.supabase.co';
+1 -1
View File
@@ -179,7 +179,7 @@
/* Back to top smaller — centered */
#back-to-top {
bottom: 16px;
bottom: 36px;
left: 50%;
right: auto;
width: 42px;
+27 -10
View File
@@ -5,7 +5,7 @@
/* --- Back to Top Button --- */
#back-to-top {
position: fixed;
bottom: 30px;
bottom: 50px;
left: 50%;
transform: translateX(-50%) translateY(20px);
width: 48px;
@@ -47,10 +47,21 @@
transform: translateY(0);
}
.reveal-delay-1 { transition-delay: 0.1s; }
.reveal-delay-2 { transition-delay: 0.2s; }
.reveal-delay-3 { transition-delay: 0.3s; }
.reveal-delay-4 { transition-delay: 0.4s; }
.reveal-delay-1 {
transition-delay: 0.1s;
}
.reveal-delay-2 {
transition-delay: 0.2s;
}
.reveal-delay-3 {
transition-delay: 0.3s;
}
.reveal-delay-4 {
transition-delay: 0.4s;
}
/* --- Buttons --- */
.btn {
@@ -98,10 +109,16 @@
justify-content: center;
}
/* --- Custom reCAPTCHA Badge Styling --- */
/* --- Custom reCAPTCHA Badge Styling (Icon Only) --- */
.grecaptcha-badge {
left: 24px !important;
right: auto !important;
bottom: 24px !important;
z-index: 2 !important;
left: 24px !important;
right: auto !important;
bottom: 24px !important;
z-index: 2 !important;
width: 74px !important;
overflow: hidden !important;
transition: none !important;
}
.grecaptcha-badge:hover {
width: 74px !important; /* prevent expansion */
}
+85
View File
@@ -2120,3 +2120,88 @@ th {
z-index: 1002;
}
/* Modal CSS */
.custom-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(9, 13, 22, 0.85);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
animation: modalFadeIn 0.2s ease-out;
padding: 20px;
}
.custom-modal-container {
background: var(--surface);
border: 1px solid var(--border-color);
border-radius: 16px;
width: 100%;
max-width: 700px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
animation: modalSlideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1);
display: flex;
flex-direction: column;
}
.custom-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid var(--border-color);
}
.custom-modal-header h2 {
margin: 0;
font-family: 'Outfit', sans-serif;
font-size: 1.5rem;
color: var(--text-light);
}
.custom-modal-close {
background: none;
border: none;
color: var(--text-muted);
font-size: 2rem;
cursor: pointer;
line-height: 1;
transition: color 0.2s;
}
.custom-modal-close:hover {
color: var(--danger);
}
.custom-modal-body {
padding: 24px;
}
.custom-modal-body p {
color: var(--text-muted);
line-height: 1.6;
margin-bottom: 24px;
}
/* Certificate Carousel */
.cert-carousel-container {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
gap: 20px;
padding-bottom: 20px;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
}
.cert-carousel-container::-webkit-scrollbar {
height: 8px;
}
.cert-carousel-container::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
.cert-carousel-item {
scroll-snap-align: start;
min-width: 300px;
flex-shrink: 0;
}
+1
View File
@@ -15,6 +15,7 @@ export default function RootLayout({
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossOrigin="anonymous" referrerPolicy="no-referrer" />
</head>
<body>
<input type="checkbox" id="dark-mode-toggle" className="toggle-checkbox" defaultChecked />
+252
View File
@@ -0,0 +1,252 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
// Helper component for Portal
function ModalPortal({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
return () => setMounted(false);
}, []);
if (!mounted) return null;
return createPortal(children, document.body);
}
// --- Project Card & Modal ---
export function ProjectCard({ project }: { project: any }) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<div className="project-card" onClick={() => setIsOpen(true)} style={{ cursor: 'pointer' }}>
<div className="card-img">
<img src={project.img_url || undefined} alt={project.title} loading="lazy" />
<div className="card-img-overlay"></div>
</div>
<div className="card-content">
<div className="card-header">
<h3>{project.title}</h3>
<span className="arrow-icon">↗</span>
</div>
<p>{project.des}</p>
<div className="stack-badges">
{project.techstack ? project.techstack.split(',').map((tag: string) => tag.trim()).map((tag: string, i: number) => (
<span className="stack-badge" key={i}>{tag}</span>
)) : null}
</div>
</div>
</div>
{isOpen && (
<ModalPortal>
<div className="custom-modal-overlay" onClick={() => setIsOpen(false)} style={{ zIndex: 9999 }}>
<div className="custom-modal-container" onClick={(e) => e.stopPropagation()}>
<div className="custom-modal-header">
<h2>{project.title}</h2>
<button className="custom-modal-close" onClick={() => setIsOpen(false)}>×</button>
</div>
<div className="custom-modal-body">
<img src={project.img_url || undefined} alt={project.title} style={{ width: '100%', borderRadius: '8px', marginBottom: '20px' }} />
<p>{project.des}</p>
<div style={{ marginBottom: '24px' }}>
<h4 style={{ color: 'var(--text-light)', marginBottom: '10px' }}>Tech Stack</h4>
<div className="stack-badges">
{project.techstack ? project.techstack.split(',').map((tag: string) => tag.trim()).map((tag: string, i: number) => (
<span className="stack-badge" key={i}>{tag}</span>
)) : null}
</div>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
{project.live_demo_url ? (
<a href={project.live_demo_url} target="_blank" rel="noopener noreferrer" className="btn primary" style={{ textDecoration: 'none', display: 'inline-block' }}>
Live Demo ↗
</a>
) : project.url ? (
<a href={project.url} target="_blank" rel="noopener noreferrer" className="btn primary" style={{ textDecoration: 'none', display: 'inline-block' }}>
Live Demo ↗
</a>
) : (
<button className="btn secondary" disabled>No Live Demo</button>
)}
{project.repo_url && (
<a href={project.repo_url} target="_blank" rel="noopener noreferrer" className="btn secondary" style={{ textDecoration: 'none', display: 'inline-block' }}>
View Repository ↗
</a>
)}
</div>
</div>
</div>
</div>
</ModalPortal>
)}
</>
);
}
// --- Article Card & Modal ---
export function ArticleCard({ article }: { article: any }) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<article className="blog-card" onClick={() => setIsOpen(true)} style={{ cursor: 'pointer' }}>
<div className="blog-meta">
<span className="blog-date">{article.date_published}</span>
{article.tag && <span className="blog-tag">{article.tag}</span>}
</div>
<h3 className="blog-title">{article.title}</h3>
<p className="blog-excerpt">{article.excerpt}</p>
<span className="read-more">Read Article →</span>
</article>
{isOpen && (
<ModalPortal>
<div className="custom-modal-overlay" onClick={() => setIsOpen(false)} style={{ zIndex: 9999 }}>
<div className="custom-modal-container" onClick={(e) => e.stopPropagation()}>
<div className="custom-modal-header">
<h2>{article.title}</h2>
<button className="custom-modal-close" onClick={() => setIsOpen(false)}>×</button>
</div>
<div className="custom-modal-body">
<div className="blog-meta" style={{ marginBottom: '16px' }}>
<span className="blog-date">{article.date_published}</span>
{article.tag && <span className="blog-tag">{article.tag}</span>}
</div>
<p style={{ fontSize: '1.1rem', color: 'var(--text-light)' }}>{article.excerpt}</p>
<div style={{ marginTop: '24px' }}>
{article.url && article.url !== "#" ? (
<a href={article.url} target="_blank" rel="noopener noreferrer" className="btn primary" style={{ textDecoration: 'none', display: 'inline-block' }}>
Read Full Article ↗
</a>
) : (
<button className="btn secondary" disabled>Article link unavailable</button>
)}
</div>
</div>
</div>
</div>
</ModalPortal>
)}
</>
);
}
// --- Certification Card & Modal ---
export function CertCard({ cert }: { cert: any }) {
const [isOpen, setIsOpen] = useState(false);
const [zoom, setZoom] = useState(1);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const dragStart = useRef({ x: 0, y: 0 });
const handlePointerDown = (e: React.PointerEvent) => {
setIsDragging(true);
dragStart.current = { x: e.clientX - position.x, y: e.clientY - position.y };
e.currentTarget.setPointerCapture(e.pointerId);
};
const handlePointerMove = (e: React.PointerEvent) => {
if (!isDragging) return;
setPosition({
x: e.clientX - dragStart.current.x,
y: e.clientY - dragStart.current.y
});
};
const handlePointerUp = (e: React.PointerEvent) => {
setIsDragging(false);
e.currentTarget.releasePointerCapture(e.pointerId);
};
const resetZoom = () => {
setZoom(1);
setPosition({ x: 0, y: 0 });
};
return (
<>
<div className="cert-card" onClick={() => setIsOpen(true)} style={{ cursor: 'pointer' }}>
{cert.img_url ? (
<div style={{ width: '60px', height: '60px', overflow: 'hidden', borderRadius: '6px', border: '1px solid var(--border-color)', display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: 'var(--card-bg)', flexShrink: 0 }}>
<img src={cert.img_url} alt={cert.title} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
</div>
) : (
<div className="cert-icon">{cert.icon?.includes('fa-') ? <i className={cert.icon}></i> : (cert.icon || "🏆")}</div>
)}
<div className="cert-info">
<h3>{cert.title}</h3>
<p className="issuer">{cert.issuer}</p>
<span className="date">Issued {cert.date_issued}</span>
</div>
</div>
{isOpen && (
<ModalPortal>
<div className="custom-modal-overlay" onClick={() => setIsOpen(false)} style={{ zIndex: 9999 }}>
<div className="custom-modal-container" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '800px', display: 'flex', flexDirection: 'column' }}>
<div className="custom-modal-header">
<h2>{cert.title}</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{cert.img_url && (
<div style={{ display: 'flex', background: 'var(--card-bg)', borderRadius: '6px', border: '1px solid var(--border-color)', marginRight: '16px' }}>
<button onClick={() => setZoom(z => Math.max(0.5, z - 0.2))} style={{ background: 'none', border: 'none', padding: '4px 8px', color: 'var(--text-main)', cursor: 'pointer' }}>-</button>
<button onClick={resetZoom} style={{ background: 'none', border: 'none', padding: '4px 8px', color: 'var(--text-main)', cursor: 'pointer', fontSize: '0.8rem', minWidth: '40px' }}>{Math.round(zoom * 100)}%</button>
<button onClick={() => setZoom(z => Math.min(3, z + 0.2))} style={{ background: 'none', border: 'none', padding: '4px 8px', color: 'var(--text-main)', cursor: 'pointer' }}>+</button>
</div>
)}
<button className="custom-modal-close" onClick={() => setIsOpen(false)}>×</button>
</div>
</div>
<div className="custom-modal-body" style={{ textAlign: 'center', flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<p style={{ marginBottom: '8px' }}><strong>Issuer:</strong> {cert.issuer}</p>
<p style={{ marginBottom: '24px' }}><strong>Date:</strong> {cert.date_issued}</p>
{cert.img_url ? (
<div style={{ flex: 1, position: 'relative', overflow: 'hidden', background: '#0a0a0a', borderRadius: '8px', minHeight: '400px', cursor: isDragging ? 'grabbing' : 'grab' }}>
<div
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={handlePointerUp}
style={{
position: 'absolute',
top: 0, left: 0, right: 0, bottom: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
touchAction: 'none'
}}
>
<img
src={cert.img_url}
alt={cert.title}
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
transform: `translate(${position.x}px, ${position.y}px) scale(${zoom})`,
transition: isDragging ? 'none' : 'transform 0.2s ease',
pointerEvents: 'none'
}}
/>
</div>
</div>
) : (
<div style={{ fontSize: '8rem', margin: '40px 0' }}>{cert.icon?.includes('fa-') ? <i className={cert.icon}></i> : (cert.icon || "🏆")}</div>
)}
</div>
</div>
</div>
</ModalPortal>
)}
</>
);
}
+57
View File
@@ -0,0 +1,57 @@
"use client";
import { useRouter } from "next/navigation";
export default function FloatingBackButton() {
const router = useRouter();
return (
<button
onClick={() => router.back()}
className="floating-back-btn"
aria-label="Go back"
title="Go back"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M19 12H5M12 19l-7-7 7-7"/>
</svg>
<style jsx>{`
.floating-back-btn {
position: fixed;
top: 80px; /* Below navbar */
left: 20px;
width: 45px;
height: 45px;
border-radius: 50%;
background: var(--surface);
border: 1px solid var(--border-color);
color: var(--text-light);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 90;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
}
.floating-back-btn:hover {
background: var(--surface-hover);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}
.floating-back-btn svg {
width: 20px;
height: 20px;
}
@media (max-width: 768px) {
.floating-back-btn {
top: 70px;
left: 15px;
width: 40px;
height: 40px;
}
}
`}</style>
</button>
);
}
+19 -18
View File
@@ -1,15 +1,16 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import Link from "next/link";
export default function Navbar() {
const [isDark, setIsDark] = useState(() => {
const [isDark, setIsDark] = useState(true);
useEffect(() => {
if (typeof window !== "undefined") {
return localStorage.getItem("theme") !== "light";
setIsDark(localStorage.getItem("theme") !== "light");
}
return true;
});
}, []);
const handleToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
const checked = e.target.checked;
@@ -21,7 +22,7 @@ export default function Navbar() {
const navLinks = document.getElementById("nav-links");
const hamburger = document.getElementById("hamburger");
const navOverlay = document.getElementById("nav-overlay");
if (navLinks?.classList.contains("active")) {
closeMenu();
} else {
@@ -52,17 +53,17 @@ export default function Navbar() {
return (
<>
<input
type="checkbox"
id="dark-mode-toggle"
className="toggle-checkbox"
checked={isDark}
onChange={handleToggle}
<input
type="checkbox"
id="dark-mode-toggle"
className="toggle-checkbox"
checked={isDark}
onChange={handleToggle}
/>
<nav id="main-nav">
<div className="logo">
<a href="/#" className="logo-link" onClick={(e) => { e.preventDefault(); window.scrollTo({ top: 0, behavior: 'smooth' }); }}>CG</a>
<Link href="/" className="logo-link">CG</Link>
<Link href="/admin/login" className="logo-dot-link">.</Link>
</div>
@@ -76,18 +77,18 @@ export default function Navbar() {
<Link href="/blog" className="nav-item" onClick={handleLinkClick}>Blog</Link>
{/* eslint-disable-next-line @next/next/no-html-link-for-pages */}
<a href="/#contact" className="nav-item" onClick={handleLinkClick}>Contact</a>
<label htmlFor="dark-mode-toggle" className="theme-switch theme-switch-panel" title="Toggle Theme" aria-label="Toggle dark/light mode">
<span className="theme-switch-thumb"></span>
<span className="theme-switch-icon icon-moon">🌙</span>
<span className="theme-switch-icon icon-sun">☀️</span>
<span className="theme-switch-icon icon-moon"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg></span>
<span className="theme-switch-icon icon-sun"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="5" /><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" /></svg></span>
</label>
</div>
<label htmlFor="dark-mode-toggle" className="theme-switch theme-switch-nav" title="Toggle Theme" aria-label="Toggle dark/light mode">
<span className="theme-switch-thumb"></span>
<span className="theme-switch-icon icon-moon">🌙</span>
<span className="theme-switch-icon icon-sun">☀️</span>
<span className="theme-switch-icon icon-moon"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg></span>
<span className="theme-switch-icon icon-sun"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="5" /><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" /></svg></span>
</label>
<div className="hamburger" id="hamburger" aria-label="Toggle menu" role="button" tabIndex={0} onClick={toggleMenu}>