implement customer login portal, product catalog with pagination, and shared UI components
This commit is contained in:
parent
86f72c7b0b
commit
c206be1c9d
@ -42,6 +42,9 @@ export default function LoginPage() {
|
|||||||
sessionStorage.setItem('USERID', data.userid);
|
sessionStorage.setItem('USERID', data.userid);
|
||||||
localStorage.setItem('vgproducts_uid', data.userid);
|
localStorage.setItem('vgproducts_uid', data.userid);
|
||||||
localStorage.setItem('d4a_email', data.email);
|
localStorage.setItem('d4a_email', data.email);
|
||||||
|
|
||||||
|
// Dispatch event to update navbar state immediately
|
||||||
|
window.dispatchEvent(new Event('storage'));
|
||||||
} catch {
|
} catch {
|
||||||
console.log('Error setting storage');
|
console.log('Error setting storage');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useState } from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { products } from '@/data/products';
|
import { products } from '@/data/products';
|
||||||
|
import PriceDisplay from '@/components/PriceDisplay';
|
||||||
|
|
||||||
export default function ProductsPage() {
|
export default function ProductsPage() {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
@ -57,9 +58,7 @@ export default function ProductsPage() {
|
|||||||
{product.description}
|
{product.description}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 'auto' }}>
|
<div style={{ marginTop: 'auto' }}>
|
||||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: '18px', fontWeight: 700, color: 'var(--orange)', marginBottom: '16px' }}>
|
<PriceDisplay price={product.price} />
|
||||||
{product.price}
|
|
||||||
</div>
|
|
||||||
<Link href={`/products/${product.slug}`} className="btn-primary" style={{ width: '100%', textAlign: 'center', appearance: 'none', display: 'block' }}>
|
<Link href={`/products/${product.slug}`} className="btn-primary" style={{ width: '100%', textAlign: 'center', appearance: 'none', display: 'block' }}>
|
||||||
View Product →
|
View Product →
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import Image from 'next/image';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { products } from '@/data/products';
|
import { products } from '@/data/products';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
import PriceDisplay from '@/components/PriceDisplay';
|
||||||
|
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
@ -83,9 +84,7 @@ export default async function ProductDetailPage({
|
|||||||
<div className="section-eyebrow" style={{ marginBottom: '16px' }}>{product!.category}</div>
|
<div className="section-eyebrow" style={{ marginBottom: '16px' }}>{product!.category}</div>
|
||||||
<h1 className="section-h2" style={{ fontSize: '48px', marginBottom: '24px', lineHeight: 1.1 }}>{product!.name}</h1>
|
<h1 className="section-h2" style={{ fontSize: '48px', marginBottom: '24px', lineHeight: 1.1 }}>{product!.name}</h1>
|
||||||
|
|
||||||
<div style={{ fontSize: '24px', fontWeight: 700, color: 'var(--orange)', fontFamily: 'var(--font-display)', marginBottom: '32px' }}>
|
<PriceDisplay price={product!.price} isDetail />
|
||||||
{product!.price}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ borderTop: '1px solid var(--gray-200)', borderBottom: '1px solid var(--gray-200)', padding: '32px 0', marginBottom: '40px' }}>
|
<div style={{ borderTop: '1px solid var(--gray-200)', borderBottom: '1px solid var(--gray-200)', padding: '32px 0', marginBottom: '40px' }}>
|
||||||
<h3 style={{ fontFamily: 'var(--font-display)', fontSize: '14px', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em', color: 'var(--navy)', marginBottom: '16px' }}>Description</h3>
|
<h3 style={{ fontFamily: 'var(--font-display)', fontSize: '14px', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em', color: 'var(--navy)', marginBottom: '16px' }}>Description</h3>
|
||||||
|
|||||||
@ -1,10 +1,41 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isLogged, setIsLogged] = useState(false);
|
||||||
|
const [showLogoutConfirm, setShowLogoutConfirm] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAuth = () => {
|
||||||
|
const uid = localStorage.getItem('vgproducts_uid') || sessionStorage.getItem('USERID');
|
||||||
|
setIsLogged(!!uid);
|
||||||
|
};
|
||||||
|
checkAuth();
|
||||||
|
// Check periodically or handle logout event
|
||||||
|
window.addEventListener('storage', checkAuth);
|
||||||
|
return () => window.removeEventListener('storage', checkAuth);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLogoutClick = () => {
|
||||||
|
setShowLogoutConfirm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const performLogout = () => {
|
||||||
|
localStorage.removeItem('vgproducts_uid');
|
||||||
|
localStorage.removeItem('d4a_email');
|
||||||
|
sessionStorage.removeItem('USERID');
|
||||||
|
setIsLogged(false);
|
||||||
|
setIsOpen(false);
|
||||||
|
setShowLogoutConfirm(false);
|
||||||
|
router.push('/');
|
||||||
|
// trigger state update for other components
|
||||||
|
window.dispatchEvent(new Event('storage'));
|
||||||
|
};
|
||||||
|
|
||||||
const toggleMenu = () => setIsOpen(!isOpen);
|
const toggleMenu = () => setIsOpen(!isOpen);
|
||||||
|
|
||||||
@ -32,16 +63,74 @@ export default function Navbar() {
|
|||||||
<li><Link href="/blog" onClick={() => setIsOpen(false)}>Blog</Link></li>
|
<li><Link href="/blog" onClick={() => setIsOpen(false)}>Blog</Link></li>
|
||||||
<li><Link href="/contact" onClick={() => setIsOpen(false)}>Contact</Link></li>
|
<li><Link href="/contact" onClick={() => setIsOpen(false)}>Contact</Link></li>
|
||||||
<li className="mobile-cta-li">
|
<li className="mobile-cta-li">
|
||||||
|
{isLogged ? (
|
||||||
|
<button onClick={handleLogoutClick} className="nav-cta" style={{ width: '100%', justifyContent: 'center', appearance: 'none', border: 'none', cursor: 'pointer' }}>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
<Link href="/login" className="nav-cta" onClick={() => setIsOpen(false)} style={{ width: '100%', justifyContent: 'center' }}>
|
<Link href="/login" className="nav-cta" onClick={() => setIsOpen(false)} style={{ width: '100%', justifyContent: 'center' }}>
|
||||||
Login
|
Login
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
{isLogged ? (
|
||||||
|
<button onClick={handleLogoutClick} className="nav-cta desktop-cta" style={{ appearance: 'none', border: 'none', cursor: 'pointer' }}>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
<Link href="/login" className="nav-cta desktop-cta">
|
<Link href="/login" className="nav-cta desktop-cta">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||||
Login
|
Login
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Logout Confirmation Modal */}
|
||||||
|
{showLogoutConfirm && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0, left: 0, right: 0, bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0,10,30,0.7)',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
zIndex: 99999,
|
||||||
|
backdropFilter: 'blur(4px)'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: '#ffffff',
|
||||||
|
padding: '32px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
width: '90%',
|
||||||
|
maxWidth: '400px',
|
||||||
|
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}>
|
||||||
|
<h3 style={{ fontSize: '24px', fontWeight: 800, color: 'var(--navy)', marginBottom: '12px', fontFamily: 'var(--font-display)' }}>Confirm Logout</h3>
|
||||||
|
<p style={{ color: 'var(--gray-600)', marginBottom: '32px', fontSize: '15px', lineHeight: 1.5 }}>
|
||||||
|
Are you sure you want to log out of your account?
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', gap: '16px', justifyContent: 'center' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowLogoutConfirm(false)}
|
||||||
|
className="btn-secondary"
|
||||||
|
style={{ flex: 1, padding: '12px', border: '1px solid var(--gray-300)', color: 'var(--gray-700)', borderRadius: '6px', fontWeight: 600, cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={performLogout}
|
||||||
|
className="btn-primary"
|
||||||
|
style={{ flex: 1, padding: '12px', background: 'var(--orange)', color: '#fff', border: 'none', borderRadius: '6px', fontWeight: 600, cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
38
components/PriceDisplay.tsx
Normal file
38
components/PriceDisplay.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function PriceDisplay({ price, isDetail = false }: { price: string, isDetail?: boolean }) {
|
||||||
|
const [isLogged, setIsLogged] = useState(false);
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
|
setMounted(true);
|
||||||
|
const uid = localStorage.getItem('vgproducts_uid') || sessionStorage.getItem('USERID');
|
||||||
|
if (uid) {
|
||||||
|
setIsLogged(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return <div style={{ minHeight: isDetail ? '36px' : '27px', marginBottom: isDetail ? '32px' : '16px' }}></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLogged) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDetail) {
|
||||||
|
return (
|
||||||
|
<div style={{ fontSize: '24px', fontWeight: 700, color: 'var(--orange)', fontFamily: 'var(--font-display)', marginBottom: '32px' }}>
|
||||||
|
{price}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ fontFamily: 'var(--font-display)', fontSize: '18px', fontWeight: 700, color: 'var(--orange)', marginBottom: '16px' }}>
|
||||||
|
{price}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
package-lock.json
generated
8
package-lock.json
generated
@ -8,7 +8,7 @@
|
|||||||
"name": "vgfence-website",
|
"name": "vgfence-website",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.15.0",
|
"axios": "^1.15.1",
|
||||||
"next": "16.2.4",
|
"next": "16.2.4",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
@ -2151,9 +2151,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.15.0",
|
"version": "1.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.1.tgz",
|
||||||
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
|
"integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.11",
|
"follow-redirects": "^1.15.11",
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.15.0",
|
"axios": "^1.15.1",
|
||||||
"next": "16.2.4",
|
"next": "16.2.4",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user