From 26559d4cf8c8b94f112f28e2a3d17ea85e98fcd9 Mon Sep 17 00:00:00 2001 From: muken Date: Sat, 27 Jun 2026 16:33:20 -0500 Subject: [PATCH] Add login, role-based portals, and browser-local admin image upload --- README.md | 17 +++++ src/app/admin/page.tsx | 128 ++++++++++++++++++++++++++++++++++++ src/app/login/page.tsx | 79 +++++++++++++++++++++++ src/app/user/page.tsx | 132 ++++++++++++++++++++++++++++++++++++++ src/components/Navbar.tsx | 82 +++++++++++++++++++++++ src/hooks/useAuth.ts | 47 ++++++++++++++ 6 files changed, 485 insertions(+) create mode 100644 src/app/admin/page.tsx create mode 100644 src/app/login/page.tsx create mode 100644 src/app/user/page.tsx create mode 100644 src/components/Navbar.tsx create mode 100644 src/hooks/useAuth.ts diff --git a/README.md b/README.md index e4ae9d9..e76da64 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,20 @@ src/app/page.tsx src/components/MeasuredText.tsx README.md ``` + +## Authentication + +The app now includes simple browser-local authentication with two hardcoded accounts: + +- `admin` / `admin` — Admin portal (`/admin`) +- `user` / `user` — User portal (`/user`) + +The logged-in role is stored in `localStorage`. The **Login / Logout** control lives in the Navbar. + +## Admin portal + +Visit `/admin` after logging in as `admin` to upload images. Images are previewed in a gallery and can be deleted individually. For now, uploaded images are stored as base64 strings in the browser's `localStorage` under the key `la-huasca-admin-images` — they are not persisted on a server. + +## User portal + +Visit `/user` after logging in as `user` to see a member portal with mock reservations, discount coupons, offers, the WiFi password, and a list of benefits. diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..ec3c0b9 --- /dev/null +++ b/src/app/admin/page.tsx @@ -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([]); + + 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) => { + 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 ( +
+

Please log in as admin to access this page.

+
+ ); + } + + return ( +
+

Admin Portal

+

Total uploaded images: {images.length}

+ + {images.length === 0 ? ( +

No images uploaded yet.

+ ) : ( +
+ {images.map((src, i) => ( +
+ {`Uploaded + +
+ ))} +
+ )} +
+ ); +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..912ee44 --- /dev/null +++ b/src/app/login/page.tsx @@ -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(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 ( +
+

Login

+
+ + + {error &&

{error}

} + +
+

+ Demo accounts: admin / admin and user / user. +

+
+ ); +} diff --git a/src/app/user/page.tsx b/src/app/user/page.tsx new file mode 100644 index 0000000..2df1dcb --- /dev/null +++ b/src/app/user/page.tsx @@ -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 ( +
+

Please log in as user to access this page.

+
+ ); + } + + return ( +
+

User Portal

+ +
+

Future Reservations

+ {reservations.length === 0 ? ( +

No upcoming reservations.

+ ) : ( +
    + {reservations.map((r) => ( +
  • + {r.date} at {r.time} · {r.guests} guests · {r.table} +
  • + ))} +
+ )} +
+ +
+

Discount Coupons

+
+ {coupons.map((c) => ( +
+ {c.code} +

{c.discount}

+
+ ))} +
+
+ +
+

Offers

+
    + {offers.map((o) => ( +
  • + {o.title} — {o.desc} +
  • + ))} +
+
+ +
+

WiFi Password

+

+ LaHuascaGuest2026 +

+
+ +
+

Member Benefits

+
    + {benefits.map((b) => ( +
  • {b}
  • + ))} +
+
+
+ ); +} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 0000000..2d50ecd --- /dev/null +++ b/src/components/Navbar.tsx @@ -0,0 +1,82 @@ +'use client'; + +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { useAuth } from '@/hooks/useAuth'; + +const links = [ + { href: '/', label: 'Home' }, + { href: '/rooms', label: 'Rooms' }, + { href: '/coffee', label: 'Coffee' }, + { href: '/landscape', label: 'Landscape' }, + { href: '/comments', label: 'Comments' }, +]; + +export default function Navbar() { + const { isLoggedIn, logout } = useAuth(); + const router = useRouter(); + + const handleLogout = () => { + logout(); + router.push('/login'); + }; + + return ( + + ); +} diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts new file mode 100644 index 0000000..8ee0b4a --- /dev/null +++ b/src/hooks/useAuth.ts @@ -0,0 +1,47 @@ +'use client'; + +import { useCallback, useEffect, useState } from 'react'; + +const ROLE_KEY = 'la-huasca-role'; +const IMAGES_KEY = 'la-huasca-admin-images'; + +type Role = 'admin' | 'user' | null; + +export function useAuth() { + const [role, setRole] = useState(null); + const [hydrated, setHydrated] = useState(false); + + useEffect(() => { + const stored = typeof window !== 'undefined' ? localStorage.getItem(ROLE_KEY) : null; + setRole((stored as Role) ?? null); + setHydrated(true); + }, []); + + const login = useCallback((username: string, password: string): boolean => { + if (typeof window === 'undefined') return false; + if (username === 'admin' && password === 'admin') { + localStorage.setItem(ROLE_KEY, 'admin'); + setRole('admin'); + return true; + } + if (username === 'user' && password === 'user') { + localStorage.setItem(ROLE_KEY, 'user'); + setRole('user'); + return true; + } + return false; + }, []); + + const logout = useCallback(() => { + if (typeof window === 'undefined') return; + localStorage.removeItem(ROLE_KEY); + localStorage.removeItem(IMAGES_KEY); + setRole(null); + }, []); + + const isAdmin = role === 'admin'; + const isUser = role === 'user'; + const isLoggedIn = role !== null; + + return { role, hydrated, login, logout, isAdmin, isUser, isLoggedIn }; +}