Replace entire repo content with code from /root/shahikitchen-google/
This commit is contained in:
+204
-66
@@ -1,101 +1,181 @@
|
||||
'use client';
|
||||
|
||||
import { logoUrl } from "@/lib/assets";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import { Suspense, useEffect, useState } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { logoUrl } from '@/lib/assets';
|
||||
import Navbar from '@/components/Navbar';
|
||||
import Footer from '@/components/Footer';
|
||||
import { useLanguage } from '@/lib/language-context';
|
||||
import { getTranslation } from '@/lib/translations';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { User, Lock, MapPin, LogIn, Phone, MessageCircle } from 'lucide-react';
|
||||
|
||||
import Footer from "@/components/Footer";
|
||||
import { useLanguage } from "@/lib/language-context";
|
||||
import { getTranslation } from "@/lib/translations";
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { User, Lock, MapPin, LogIn, Phone, MessageCircle } from "lucide-react";
|
||||
type LoginTab = 'customer' | 'staff';
|
||||
|
||||
/**
|
||||
* =============================================================================
|
||||
* STAFF LOGIN / AUTHENTICATION PAGE
|
||||
* =============================================================================
|
||||
*
|
||||
* Beautiful, theme-matched placeholder login experience.
|
||||
* - Fully language compatible (sv default + en/hi/ur)
|
||||
* - No real backend yet — always shows friendly "under construction" message
|
||||
* - Mobile-first: large tap targets, excellent spacing, easy form on phones
|
||||
* - Desktop: elegant split layout with dark luxury graphic panel + form
|
||||
* - Uses existing design tokens (gold accents, cream bg, rounded cards, serif titles)
|
||||
*
|
||||
* Future: when real auth is added, this will become the entry to a protected staff area
|
||||
* (per-site menus, orders, inventory etc. for Askim vs Backaplan).
|
||||
*/
|
||||
function GoogleIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
|
||||
/>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="#FBBC05"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="#EA4335"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
function LoginPageContent() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { language } = useLanguage();
|
||||
const t = getTranslation(language);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
username: '',
|
||||
password: '',
|
||||
site: '',
|
||||
});
|
||||
const initialTab = searchParams.get('tab') === 'staff' ? 'staff' : 'customer';
|
||||
const [activeTab, setActiveTab] = useState<LoginTab>(initialTab);
|
||||
const [formData, setFormData] = useState({ username: '', password: '', site: '' });
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const [loginError, setLoginError] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [customerError, setCustomerError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const tab = searchParams.get('tab');
|
||||
if (tab === 'staff' || tab === 'customer') {
|
||||
setActiveTab(tab);
|
||||
}
|
||||
|
||||
const error = searchParams.get('error');
|
||||
if (error) {
|
||||
const errorMessages: Record<string, string> = {
|
||||
google_not_configured: t.auth.customer.notConfigured,
|
||||
google_denied: t.auth.customer.googleDenied,
|
||||
google_failed: t.auth.customer.googleFailed,
|
||||
invalid_state: t.auth.customer.googleFailed,
|
||||
missing_code: t.auth.customer.googleFailed,
|
||||
};
|
||||
setCustomerError(errorMessages[error] ?? t.auth.customer.googleFailed);
|
||||
setActiveTab('customer');
|
||||
}
|
||||
}, [searchParams, t.auth.customer]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkCustomerSession = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/customer/session', { cache: 'no-store' });
|
||||
const data = (await response.json()) as { authenticated: boolean };
|
||||
if (data.authenticated) {
|
||||
router.replace('/account');
|
||||
}
|
||||
} catch {
|
||||
// Stay on login page.
|
||||
}
|
||||
};
|
||||
|
||||
void checkCustomerSession();
|
||||
}, [router]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleStaffSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
// Placeholder behaviour: always show the construction notice.
|
||||
// No validation beyond native required fields — real auth coming later.
|
||||
setIsSubmitted(true);
|
||||
setLoginError('');
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setLoginError(t.auth.accessDenied);
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.username.trim() === 'admin') {
|
||||
setLoginError(t.auth.accessDenied);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitted(true);
|
||||
} catch {
|
||||
setLoginError('Could not sign in right now. Please try again.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
const resetStaffForm = () => {
|
||||
setFormData({ username: '', password: '', site: '' });
|
||||
setIsSubmitted(false);
|
||||
setLoginError('');
|
||||
};
|
||||
|
||||
const handleGoogleSignIn = () => {
|
||||
setCustomerError('');
|
||||
const returnTo = searchParams.get('returnTo');
|
||||
const authUrl =
|
||||
returnTo && returnTo.startsWith('/') && !returnTo.startsWith('//')
|
||||
? `/api/auth/google?returnTo=${encodeURIComponent(returnTo)}`
|
||||
: '/api/auth/google';
|
||||
window.location.href = authUrl;
|
||||
};
|
||||
|
||||
const panelTitle = activeTab === 'customer' ? t.auth.customer.title : t.auth.title;
|
||||
const panelSubtitle = activeTab === 'customer' ? t.auth.customer.subtitle : t.auth.subtitle;
|
||||
const panelBadge = activeTab === 'customer' ? t.auth.customer.badge : 'STAFF PORTAL';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
|
||||
<Navbar />
|
||||
|
||||
<main className="pb-20">
|
||||
{/* Elegant header */}
|
||||
<div className="max-w-5xl mx-auto px-6 text-center">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-[#B38B4D]/10 px-4 py-1 text-[#B38B4D] text-[10px] tracking-[3.5px] font-medium mb-3">
|
||||
STAFF PORTAL
|
||||
{panelBadge}
|
||||
</div>
|
||||
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-2 text-[#101724] break-words">
|
||||
{t.auth.title}
|
||||
{panelTitle}
|
||||
</h1>
|
||||
<p className="mx-auto max-w-md text-sm md:text-[15px] text-[#6B665F] leading-relaxed">
|
||||
{t.auth.subtitle}
|
||||
{panelSubtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content area — beautiful split layout on desktop, clean single column on mobile */}
|
||||
<div className="mx-auto max-w-5xl px-6 mt-10">
|
||||
<div className="mx-auto max-w-5xl px-4 sm:px-6 mt-8 sm:mt-10">
|
||||
<div className="grid items-start gap-8 md:grid-cols-5">
|
||||
{/* LEFT: Premium dark graphic panel (desktop only). Matches Shahi theme with gold details. */}
|
||||
<div className="hidden md:col-span-2 md:block">
|
||||
<div className="sticky top-[116px] flex h-[520px] flex-col justify-between overflow-hidden rounded-3xl border border-[#c99a2e]/20 bg-gradient-to-br from-[#101724] via-[#1a1816] to-[#2C2A26] p-10 text-white shadow-2xl">
|
||||
{/* Subtle gold frame lines */}
|
||||
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-[#c99a2e] to-transparent" />
|
||||
<div className="absolute inset-x-0 bottom-0 h-px bg-gradient-to-r from-transparent via-[#c99a2e]/50 to-transparent" />
|
||||
|
||||
<div>
|
||||
<div className="mb-8 inline-flex h-16 w-16 items-center justify-center rounded-2xl border border-[#c99a2e]/30 bg-white/5 p-3 backdrop-blur">
|
||||
<img
|
||||
src={logoUrl()}
|
||||
alt="Shahi Kitchen"
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
<img src={logoUrl()} alt="Shahi Kitchen" className="h-full w-full object-contain" />
|
||||
</div>
|
||||
|
||||
<div className="font-serif text-[42px] leading-[0.95] tracking-[-1.8px]">
|
||||
Shahi<br />Kitchen
|
||||
</div>
|
||||
<div className="mt-1 text-sm font-medium tracking-[2.5px] text-[#c99a2e]">
|
||||
STAFF ACCESS
|
||||
{activeTab === 'customer' ? 'CUSTOMER ACCESS' : 'STAFF ACCESS'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -111,27 +191,73 @@ export default function LoginPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT / MOBILE: The actual elegant login card */}
|
||||
<div className="md:col-span-3">
|
||||
<div className="rounded-3xl border border-[#EDE6D9] bg-white p-8 shadow-xl md:p-10">
|
||||
{/* Mobile-only logo accent for visual continuity */}
|
||||
<div className="rounded-3xl border border-[#EDE6D9] bg-white p-5 sm:p-8 shadow-xl md:p-10">
|
||||
<div className="mb-6 flex justify-center md:hidden">
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-2xl border border-[#c99a2e]/30 bg-[#F8F5F0] p-2.5">
|
||||
<img src={logoUrl()} alt="Shahi Kitchen" className="h-full w-full object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 sm:mb-8 flex rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] p-1">
|
||||
{(['customer', 'staff'] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveTab(tab);
|
||||
setCustomerError('');
|
||||
setLoginError('');
|
||||
}}
|
||||
className={`flex-1 rounded-xl px-3 py-3.5 text-xs sm:text-sm font-semibold transition min-h-[44px] touch-manipulation ${
|
||||
activeTab === tab
|
||||
? 'bg-[#101724] text-white shadow-sm'
|
||||
: 'text-[#6B665F] hover:text-[#101724]'
|
||||
}`}
|
||||
>
|
||||
{t.auth.tabs[tab]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{!isSubmitted ? (
|
||||
{activeTab === 'customer' ? (
|
||||
<motion.div
|
||||
key="login-form"
|
||||
key="customer-login"
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -8 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="text-center"
|
||||
>
|
||||
{customerError && (
|
||||
<p className="mb-4 text-sm text-red-600" role="alert">
|
||||
{customerError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGoogleSignIn}
|
||||
className="flex w-full items-center justify-center gap-3 rounded-2xl border border-[#EDE6D9] bg-white px-5 py-4 text-base font-semibold text-[#101724] shadow-sm transition hover:border-[#c99a2e]/40 hover:bg-[#FFFCF7] active:scale-[0.985] min-h-[52px] touch-manipulation"
|
||||
>
|
||||
<GoogleIcon className="h-5 w-5" />
|
||||
{t.auth.customer.signInWithGoogle}
|
||||
</button>
|
||||
|
||||
<p className="mt-8 text-center text-[11px] tracking-wide text-[#8A8478]">
|
||||
{t.auth.customer.footerNote}
|
||||
</p>
|
||||
</motion.div>
|
||||
) : !isSubmitted ? (
|
||||
<motion.div
|
||||
key="staff-login-form"
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -8 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Username */}
|
||||
<form onSubmit={handleStaffSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="username" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">
|
||||
{t.auth.usernameLabel}
|
||||
@@ -151,7 +277,6 @@ export default function LoginPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div>
|
||||
<label htmlFor="password" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">
|
||||
{t.auth.passwordLabel}
|
||||
@@ -171,7 +296,6 @@ export default function LoginPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Site / Location dropdown — Askim or Backaplan */}
|
||||
<div>
|
||||
<label htmlFor="site" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">
|
||||
{t.auth.siteLabel}
|
||||
@@ -190,19 +314,22 @@ export default function LoginPage() {
|
||||
<option value="askim">{t.auth.askim}</option>
|
||||
<option value="backaplan">{t.auth.backaplan}</option>
|
||||
</select>
|
||||
{/* Custom dropdown arrow for polish */}
|
||||
<div className="pointer-events-none absolute end-4 top-4 text-[#B38B4D]">▾</div>
|
||||
</div>
|
||||
<p className="mt-1.5 text-[11px] text-[#8A8478]">Select the location you are working at today.</p>
|
||||
</div>
|
||||
|
||||
{/* Primary gold action button — large & mobile friendly */}
|
||||
{loginError && (
|
||||
<p className="text-sm text-red-600" role="alert">{loginError}</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary mt-2 flex w-full items-center justify-center gap-3 rounded-2xl py-4 text-base font-semibold tracking-[0.5px] active:scale-[0.985] transition"
|
||||
disabled={isSubmitting}
|
||||
className="btn-primary mt-2 flex w-full items-center justify-center gap-3 rounded-2xl py-4 text-base font-semibold tracking-[0.5px] active:scale-[0.985] transition disabled:opacity-60"
|
||||
>
|
||||
<LogIn className="h-5 w-5" />
|
||||
{t.auth.submit}
|
||||
{isSubmitting ? t.auth.submitting : t.auth.submit}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -211,7 +338,6 @@ export default function LoginPage() {
|
||||
</p>
|
||||
</motion.div>
|
||||
) : (
|
||||
/* Beautiful result state — always shows the construction notice */
|
||||
<motion.div
|
||||
key="construction-result"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
@@ -230,7 +356,6 @@ export default function LoginPage() {
|
||||
{t.auth.construction.message}
|
||||
</p>
|
||||
|
||||
{/* Helpful contact actions (real numbers from the site) */}
|
||||
<div className="mt-8 space-y-3">
|
||||
<a
|
||||
href="tel:031288910"
|
||||
@@ -251,7 +376,7 @@ export default function LoginPage() {
|
||||
</a>
|
||||
|
||||
<button
|
||||
onClick={resetForm}
|
||||
onClick={resetStaffForm}
|
||||
className="mt-2 w-full rounded-2xl bg-[#101724] py-3.5 text-sm font-semibold text-white active:bg-black transition"
|
||||
>
|
||||
{t.auth.construction.tryAgain}
|
||||
@@ -266,7 +391,6 @@ export default function LoginPage() {
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Small trust line */}
|
||||
<p className="mt-5 text-center text-[10px] text-[#8A8478] tracking-[1px]">
|
||||
SHAHI KITCHEN • GOTHENBURG • EST 2016
|
||||
</p>
|
||||
@@ -279,3 +403,17 @@ export default function LoginPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="min-h-screen bg-[#F8F5F0] flex items-center justify-center text-[#6B665F]">
|
||||
Loading…
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<LoginPageContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user