Add login, role-based portals, and browser-local admin image upload

This commit is contained in:
2026-06-27 16:33:20 -05:00
parent e4b30d6dfd
commit 26559d4cf8
6 changed files with 485 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
const IMAGES_KEY = 'la-huasca-admin-images';
export default function AdminPage() {
const { isAdmin, hydrated } = useAuth();
const router = useRouter();
const [images, setImages] = useState<string[]>([]);
useEffect(() => {
if (!hydrated) return;
if (!isAdmin) {
router.push('/login');
return;
}
const stored = localStorage.getItem(IMAGES_KEY);
if (stored) {
try {
setImages(JSON.parse(stored));
} catch {
setImages([]);
}
}
}, [isAdmin, hydrated, router]);
useEffect(() => {
localStorage.setItem(IMAGES_KEY, JSON.stringify(images));
}, [images]);
const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files) return;
Array.from(files).forEach((file) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
setImages((prev) => [...prev, result]);
};
reader.readAsDataURL(file);
});
e.target.value = '';
};
const handleDelete = (index: number) => {
setImages((prev) => prev.filter((_, i) => i !== index));
};
if (!hydrated || !isAdmin) {
return (
<main style={{ padding: '2rem' }}>
<p>Please log in as admin to access this page.</p>
</main>
);
}
return (
<main style={{ padding: '2rem' }}>
<h1>Admin Portal</h1>
<p>Total uploaded images: <strong>{images.length}</strong></p>
<label
style={{
display: 'inline-block',
padding: '0.75rem 1rem',
background: '#1a1a1a',
color: '#fff',
borderRadius: '4px',
cursor: 'pointer',
marginBottom: '1rem',
}}
>
Upload images
<input
type="file"
accept="image/*"
multiple
onChange={handleUpload}
style={{ display: 'none' }}
/>
</label>
{images.length === 0 ? (
<p style={{ color: 'dimgray' }}>No images uploaded yet.</p>
) : (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
gap: '1rem',
}}
>
{images.map((src, i) => (
<div
key={i}
style={{
border: '1px solid #ddd',
borderRadius: '8px',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
}}
>
<img
src={src}
alt={`Uploaded preview ${i + 1}`}
style={{ width: '100%', height: '160px', objectFit: 'cover' }}
/>
<button
onClick={() => handleDelete(i)}
style={{
padding: '0.5rem',
background: 'crimson',
color: '#fff',
border: 'none',
cursor: 'pointer',
}}
>
Delete
</button>
</div>
))}
</div>
)}
</main>
);
}
+79
View File
@@ -0,0 +1,79 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
export default function LoginPage() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const { login } = useAuth();
const router = useRouter();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setError(null);
const ok = login(username, password);
if (ok) {
router.push(username === 'admin' ? '/admin' : '/user');
} else {
setError('Invalid username or password.');
}
};
return (
<main style={{ padding: '2rem', maxWidth: '28rem', margin: '0 auto' }}>
<h1 style={{ marginBottom: '1rem' }}>Login</h1>
<form
onSubmit={handleSubmit}
style={{
display: 'flex',
flexDirection: 'column',
gap: '1rem',
padding: '1.5rem',
border: '1px solid #ddd',
borderRadius: '8px',
}}
>
<label style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
Username
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
style={{ padding: '0.5rem', borderRadius: '4px', border: '1px solid #ccc' }}
required
/>
</label>
<label style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
Password
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={{ padding: '0.5rem', borderRadius: '4px', border: '1px solid #ccc' }}
required
/>
</label>
{error && <p style={{ color: 'crimson', margin: 0 }}>{error}</p>}
<button
type="submit"
style={{
padding: '0.75rem',
background: '#1a1a1a',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Log in
</button>
</form>
<p style={{ marginTop: '1rem', color: 'dimgray' }}>
Demo accounts: <strong>admin</strong> / admin and <strong>user</strong> / user.
</p>
</main>
);
}
+132
View File
@@ -0,0 +1,132 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
const reservations = [
{ id: 1, date: '2026-07-12', time: '19:00', guests: 4, table: 'Terrace 3' },
{ id: 2, date: '2026-07-25', time: '20:30', guests: 2, table: 'Barrel 7' },
];
const coupons = [
{ code: 'WELCOME20', discount: '20% off your next stay' },
{ code: 'COFFEE5', discount: '$5 off any coffee tasting' },
];
const offers = [
{ title: 'Summer Wine Weekend', desc: 'Complimentary wine tasting with every two-night stay.' },
{ title: 'Sunset Dinner', desc: 'Three-course menu on the terrace every Friday.' },
];
const benefits = [
'Priority booking for peak weekends',
'Late checkout on availability',
'Free welcome cocktail',
'Member-only events and tastings',
];
export default function UserPage() {
const { isUser, hydrated } = useAuth();
const router = useRouter();
useEffect(() => {
if (!hydrated) return;
if (!isUser) {
router.push('/login');
}
}, [isUser, hydrated, router]);
if (!hydrated || !isUser) {
return (
<main style={{ padding: '2rem' }}>
<p>Please log in as user to access this page.</p>
</main>
);
}
return (
<main style={{ padding: '2rem', maxWidth: '48rem', margin: '0 auto' }}>
<h1>User Portal</h1>
<section style={{ marginBottom: '2rem' }}>
<h2>Future Reservations</h2>
{reservations.length === 0 ? (
<p style={{ color: 'dimgray' }}>No upcoming reservations.</p>
) : (
<ul style={{ padding: 0, listStyle: 'none' }}>
{reservations.map((r) => (
<li
key={r.id}
style={{
padding: '1rem',
marginBottom: '0.5rem',
border: '1px solid #ddd',
borderRadius: '8px',
}}
>
{r.date} at {r.time} &middot; {r.guests} guests &middot; {r.table}
</li>
))}
</ul>
)}
</section>
<section style={{ marginBottom: '2rem' }}>
<h2>Discount Coupons</h2>
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
{coupons.map((c) => (
<div
key={c.code}
style={{
padding: '1rem',
border: '2px dashed #1a1a1a',
borderRadius: '8px',
minWidth: '12rem',
}}
>
<strong>{c.code}</strong>
<p style={{ margin: '0.5rem 0 0', color: 'dimgray' }}>{c.discount}</p>
</div>
))}
</div>
</section>
<section style={{ marginBottom: '2rem' }}>
<h2>Offers</h2>
<ul>
{offers.map((o) => (
<li key={o.title}>
<strong>{o.title}</strong> {o.desc}
</li>
))}
</ul>
</section>
<section style={{ marginBottom: '2rem' }}>
<h2>WiFi Password</h2>
<p
style={{
display: 'inline-block',
padding: '0.75rem 1.5rem',
background: '#f4f4f4',
borderRadius: '8px',
fontFamily: 'monospace',
fontSize: '1.25rem',
}}
>
LaHuascaGuest2026
</p>
</section>
<section>
<h2>Member Benefits</h2>
<ul>
{benefits.map((b) => (
<li key={b}>{b}</li>
))}
</ul>
</section>
</main>
);
}