+
@@ -688,6 +791,44 @@ export default function AdminDashboard({
)}
+
+ {/* ================= LIVE PREVIEW MODAL ================= */}
+ {isPreviewModalOpen && (
+
+
+
+
+
+
Live Site Preview
+
+
+
+
+ {Math.round(previewZoom * 100)}%
+
+
+
+
+
+
+
+
+
+ )}
>
);
}
diff --git a/src/app/admin/actions.ts b/src/app/admin/actions.ts
index a4c0acf..81d7d39 100644
--- a/src/app/admin/actions.ts
+++ b/src/app/admin/actions.ts
@@ -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) {
diff --git a/src/app/admin/login/actions.ts b/src/app/admin/login/actions.ts
index a01b206..1395eca 100644
--- a/src/app/admin/login/actions.ts
+++ b/src/app/admin/login/actions.ts
@@ -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" };
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index c56784f..8760316 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -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';
diff --git a/src/app/css/modules/_responsive.css b/src/app/css/modules/_responsive.css
index 1b7ad19..6d96c0b 100644
--- a/src/app/css/modules/_responsive.css
+++ b/src/app/css/modules/_responsive.css
@@ -179,7 +179,7 @@
/* Back to top smaller — centered */
#back-to-top {
- bottom: 16px;
+ bottom: 36px;
left: 50%;
right: auto;
width: 42px;
diff --git a/src/app/css/modules/_utilities.css b/src/app/css/modules/_utilities.css
index 5881f85..b78fc23 100644
--- a/src/app/css/modules/_utilities.css
+++ b/src/app/css/modules/_utilities.css
@@ -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 */
+}
\ No newline at end of file
diff --git a/src/app/globals.css b/src/app/globals.css
index d949e7f..214ec47 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -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;
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index bc9129f..6ae374d 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -15,6 +15,7 @@ export default function RootLayout({
+
diff --git a/src/components/ClientCardModals.tsx b/src/components/ClientCardModals.tsx
new file mode 100644
index 0000000..1fe3864
--- /dev/null
+++ b/src/components/ClientCardModals.tsx
@@ -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 (
+ <>
+
setIsOpen(true)} style={{ cursor: 'pointer' }}>
+
+

+
+
+
+
+
{project.title}
+ ↗
+
+
{project.des}
+
+ {project.techstack ? project.techstack.split(',').map((tag: string) => tag.trim()).map((tag: string, i: number) => (
+ {tag}
+ )) : null}
+
+
+
+
+ {isOpen && (
+
+ setIsOpen(false)} style={{ zIndex: 9999 }}>
+
e.stopPropagation()}>
+
+
{project.title}
+
+
+
+

+
{project.des}
+
+
+
Tech Stack
+
+ {project.techstack ? project.techstack.split(',').map((tag: string) => tag.trim()).map((tag: string, i: number) => (
+ {tag}
+ )) : null}
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+}
+
+// --- Article Card & Modal ---
+export function ArticleCard({ article }: { article: any }) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+ <>
+
setIsOpen(true)} style={{ cursor: 'pointer' }}>
+
+ {article.date_published}
+ {article.tag && {article.tag}}
+
+ {article.title}
+ {article.excerpt}
+ Read Article →
+
+
+ {isOpen && (
+
+ setIsOpen(false)} style={{ zIndex: 9999 }}>
+
e.stopPropagation()}>
+
+
{article.title}
+
+
+
+
+ {article.date_published}
+ {article.tag && {article.tag}}
+
+
{article.excerpt}
+
+
+
+
+
+
+ )}
+ >
+ );
+}
+
+// --- 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 (
+ <>
+
setIsOpen(true)} style={{ cursor: 'pointer' }}>
+ {cert.img_url ? (
+
+

+
+ ) : (
+
{cert.icon?.includes('fa-') ? : (cert.icon || "🏆")}
+ )}
+
+
{cert.title}
+
{cert.issuer}
+
Issued {cert.date_issued}
+
+
+
+ {isOpen && (
+
+ setIsOpen(false)} style={{ zIndex: 9999 }}>
+
e.stopPropagation()} style={{ maxWidth: '800px', display: 'flex', flexDirection: 'column' }}>
+
+
{cert.title}
+
+ {cert.img_url && (
+
+
+
+
+
+ )}
+
+
+
+
+
Issuer: {cert.issuer}
+
Date: {cert.date_issued}
+
+ {cert.img_url ? (
+
+
+

+
+
+ ) : (
+
{cert.icon?.includes('fa-') ? : (cert.icon || "🏆")}
+ )}
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/src/components/FloatingBackButton.tsx b/src/components/FloatingBackButton.tsx
new file mode 100644
index 0000000..c5c113b
--- /dev/null
+++ b/src/components/FloatingBackButton.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+
+export default function FloatingBackButton() {
+ const router = useRouter();
+
+ return (
+
+ );
+}
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 5007410..3f08aa3 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -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
) => {
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 (
<>
-