feat(landing+auth): add marketing landing page and admin auth guards
continuous-integration/drone/push Build is passing Details

Per Chief Cole's guidance (Chief_Cole-20251107080515), enhanced manager
portal with showcase landing and auth protection.

Changes:
1. Marketing Landing Page:
   - Hero section with gradient backgrounds and value props
   - Feature tiles showcasing club management, scheduling, booking insights
   - CTA buttons linking to login
   - Marketing copy focused on venue admin benefits
   - Responsive design matching consumer app style

2. Auth Guards for /admin Routes:
   - Created AdminAuthGuard component using useSwissOIDAuth hook
   - Redirects unauthenticated users to login immediately
   - Shows loading state during auth check
   - Preserves locale in redirect flow
   - Wrapped all /admin/clubs pages with guard

3. Protected Routes:
   - /admin/clubs - guarded
   - /admin/clubs/[club_id] - guarded
   - / - public landing (no guard needed)

Result: Unauthenticated users see marketing showcase at root, and are
redirected to login if they try to access /admin routes directly.

Refs: docs/owners/Frontend_Faye-needs-to-read-from-Chief_Cole-20251107080515.md
master
Guillermo Pages 1 month ago
parent 4df827bce6
commit 8e0bbd58d9

4
package-lock.json generated

@ -1,11 +1,11 @@
{
"name": "playchoo",
"name": "playchoo-manager",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "playchoo",
"name": "playchoo-manager",
"version": "0.0.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",

@ -1,4 +1,5 @@
import AdminClubDetailComponent from './AdminClubDetail';
import AdminAuthGuard from '@/src/components/AdminAuthGuard';
export default async function AdminClubDetailPage({
params
@ -8,5 +9,9 @@ export default async function AdminClubDetailPage({
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return <AdminClubDetailComponent clubId={clubId} />;
return (
<AdminAuthGuard>
<AdminClubDetailComponent clubId={clubId} />
</AdminAuthGuard>
);
}

@ -1,5 +1,10 @@
import AdminClubsList from './AdminClubsList';
import AdminAuthGuard from '@/src/components/AdminAuthGuard';
export default async function AdminClubsPage() {
return <AdminClubsList />;
return (
<AdminAuthGuard>
<AdminClubsList />
</AdminAuthGuard>
);
}

@ -1,5 +1,5 @@
import Link from 'next/link';
import { Building, Users, Calendar } from 'lucide-react';
import { Building, Users, Calendar, ArrowRight, Zap, BarChart3, Clock, Shield, CheckCircle2 } from 'lucide-react';
import { Locale } from '@/i18n-config';
import { getTranslate } from './dictionaries';
@ -7,64 +7,168 @@ export default async function AdminHome({ params }: { params: Promise<{ locale:
const { locale } = await params;
const {t} = await getTranslate(locale);
// Auth backend URL for login CTA
const loginUrl = process.env.NEXT_PUBLIC_AUTH_BACKEND_URL
? `${process.env.NEXT_PUBLIC_AUTH_BACKEND_URL}/login`
: '/login';
return (
<div className="flex-1 overflow-x-hidden">
<div className="relative bg-gradient-to-br from-indigo-50 via-white to-purple-50 min-h-[80vh]">
{/* Hero Section */}
<div className="relative bg-gradient-to-br from-indigo-50 via-white to-purple-50">
{/* Background decorations */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-gradient-to-br from-indigo-600/20 to-purple-600/20 rounded-full blur-3xl"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-gradient-to-tr from-pink-600/20 to-indigo-600/20 rounded-full blur-3xl"></div>
</div>
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-20">
<div className="text-center mb-12">
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-24">
<div className="text-center max-w-4xl mx-auto">
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 leading-tight mb-6">
{t('Venue Management')}
{t('Manage Your Venue.')}
<br />
<span className="bg-gradient-to-r from-indigo-600 via-purple-600 to-pink-600 bg-clip-text text-transparent">
{t('Admin Portal')}
{t('Effortlessly.')}
</span>
</h1>
<p className="text-lg sm:text-xl text-gray-600 font-medium max-w-2xl mx-auto">
{t('Manage your clubs, courts, schedules, and bookings all in one place.')}
<p className="text-lg sm:text-xl text-gray-600 font-medium mb-8">
{t('Streamline court scheduling, track bookings, and optimize operations—all from one powerful dashboard.')}
</p>
{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center mb-12">
<a
href={loginUrl}
className="inline-flex items-center justify-center px-8 py-4 bg-gradient-to-r from-indigo-600 to-purple-600 text-white font-semibold rounded-xl hover:shadow-xl hover:scale-105 transition-all duration-300"
>
{t('Get Started')}
<ArrowRight className="ml-2 w-5 h-5" />
</a>
<Link
href="#features"
className="inline-flex items-center justify-center px-8 py-4 bg-white text-gray-900 font-semibold rounded-xl border-2 border-gray-200 hover:border-indigo-300 hover:shadow-lg transition-all duration-300"
>
{t('Learn More')}
</Link>
</div>
{/* Value Props */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="flex items-center justify-center space-x-2 p-3 bg-white/60 backdrop-blur-sm rounded-xl border border-gray-200/50">
<Zap className="w-5 h-5 text-yellow-600" />
<span className="text-sm font-medium text-gray-700">{t('Real-time updates')}</span>
</div>
<div className="flex items-center justify-center space-x-2 p-3 bg-white/60 backdrop-blur-sm rounded-xl border border-gray-200/50">
<BarChart3 className="w-5 h-5 text-blue-600" />
<span className="text-sm font-medium text-gray-700">{t('Analytics')}</span>
</div>
<div className="flex items-center justify-center space-x-2 p-3 bg-white/60 backdrop-blur-sm rounded-xl border border-gray-200/50">
<Clock className="w-5 h-5 text-green-600" />
<span className="text-sm font-medium text-gray-700">{t('Save hours')}</span>
</div>
<div className="flex items-center justify-center space-x-2 p-3 bg-white/60 backdrop-blur-sm rounded-xl border border-gray-200/50">
<Shield className="w-5 h-5 text-purple-600" />
<span className="text-sm font-medium text-gray-700">{t('Secure')}</span>
</div>
</div>
</div>
</div>
</div>
{/* Features Section */}
<div id="features" className="py-16 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl sm:text-4xl font-bold text-gray-900 mb-4">
{t('Everything you need to run your venue')}
</h2>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
{t('From court management to booking insights, all the tools you need in one place.')}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto">
<Link
href={`/${locale}/admin/clubs`}
className="group bg-white/80 backdrop-blur-sm border border-gray-200/50 rounded-xl p-6 hover:shadow-xl hover:border-indigo-300 transition-all duration-300"
>
<div className="flex flex-col items-center text-center space-y-4">
<div className="p-4 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-full text-white group-hover:scale-110 transition-transform duration-300">
<Building className="w-8 h-8" />
</div>
<h3 className="text-xl font-bold text-gray-900">{t('Clubs')}</h3>
<p className="text-gray-600">{t('View and manage your venue locations')}</p>
<div className="grid md:grid-cols-3 gap-8">
<div className="bg-gradient-to-br from-indigo-50 to-purple-50 rounded-2xl p-8 border border-indigo-100">
<div className="w-14 h-14 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center mb-6">
<Building className="w-7 h-7 text-white" />
</div>
</Link>
<h3 className="text-xl font-bold text-gray-900 mb-3">{t('Club Management')}</h3>
<p className="text-gray-600 mb-4">
{t('Manage multiple venues, courts, and facilities from a single dashboard. View real-time availability and status.')}
</p>
<ul className="space-y-2">
<li className="flex items-center text-sm text-gray-700">
<CheckCircle2 className="w-4 h-4 text-green-600 mr-2" />
{t('Multi-venue support')}
</li>
<li className="flex items-center text-sm text-gray-700">
<CheckCircle2 className="w-4 h-4 text-green-600 mr-2" />
{t('Court configuration')}
</li>
</ul>
</div>
<div className="bg-white/40 backdrop-blur-sm border border-gray-200/30 rounded-xl p-6 opacity-60">
<div className="flex flex-col items-center text-center space-y-4">
<div className="p-4 bg-gray-300 rounded-full text-white">
<Calendar className="w-8 h-8" />
</div>
<h3 className="text-xl font-bold text-gray-900">{t('Schedules')}</h3>
<p className="text-gray-600">{t('Coming soon')}</p>
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-2xl p-8 border border-blue-100">
<div className="w-14 h-14 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center mb-6">
<Calendar className="w-7 h-7 text-white" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3">{t('Schedule Control')}</h3>
<p className="text-gray-600 mb-4">
{t('Create flexible schedules, manage slot availability, and automate recurring bookings with ease.')}
</p>
<ul className="space-y-2">
<li className="flex items-center text-sm text-gray-700">
<CheckCircle2 className="w-4 h-4 text-green-600 mr-2" />
{t('Automated scheduling')}
</li>
<li className="flex items-center text-sm text-gray-700">
<CheckCircle2 className="w-4 h-4 text-green-600 mr-2" />
{t('Recurring patterns')}
</li>
</ul>
</div>
<div className="bg-white/40 backdrop-blur-sm border border-gray-200/30 rounded-xl p-6 opacity-60">
<div className="flex flex-col items-center text-center space-y-4">
<div className="p-4 bg-gray-300 rounded-full text-white">
<Users className="w-8 h-8" />
</div>
<h3 className="text-xl font-bold text-gray-900">{t('Bookings')}</h3>
<p className="text-gray-600">{t('Coming soon')}</p>
<div className="bg-gradient-to-br from-purple-50 to-pink-50 rounded-2xl p-8 border border-purple-100">
<div className="w-14 h-14 bg-gradient-to-br from-purple-500 to-pink-600 rounded-xl flex items-center justify-center mb-6">
<Users className="w-7 h-7 text-white" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3">{t('Booking Insights')}</h3>
<p className="text-gray-600 mb-4">
{t('Track reservations, analyze usage patterns, and optimize your venue operations with detailed analytics.')}
</p>
<ul className="space-y-2">
<li className="flex items-center text-sm text-gray-700">
<CheckCircle2 className="w-4 h-4 text-green-600 mr-2" />
{t('Usage analytics')}
</li>
<li className="flex items-center text-sm text-gray-700">
<CheckCircle2 className="w-4 h-4 text-green-600 mr-2" />
{t('Performance metrics')}
</li>
</ul>
</div>
</div>
</div>
</div>
{/* CTA Section */}
<div className="py-16 bg-gradient-to-br from-indigo-600 via-purple-600 to-pink-600">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl sm:text-4xl font-bold text-white mb-6">
{t('Ready to streamline your venue operations?')}
</h2>
<p className="text-xl text-indigo-100 mb-8">
{t('Join venue managers who trust Playchoo to run their facilities efficiently.')}
</p>
<a
href={loginUrl}
className="inline-flex items-center justify-center px-8 py-4 bg-white text-indigo-600 font-semibold rounded-xl hover:shadow-2xl hover:scale-105 transition-all duration-300"
>
{t('Access Your Dashboard')}
<ArrowRight className="ml-2 w-5 h-5" />
</a>
</div>
</div>
</div>
);
}

@ -0,0 +1,56 @@
'use client';
import { useEffect, ReactNode } from 'react';
import { useRouter } from 'next/navigation';
import { useSwissOIDAuth } from 'swissoid-front';
import { Loader2 } from 'lucide-react';
import useTranslation from '@/src/hooks/useTranslation';
interface AdminAuthGuardProps {
children: ReactNode;
}
/**
* Auth guard for /admin routes
* Redirects unauthenticated users to login while preserving locale
*/
export default function AdminAuthGuard({ children }: AdminAuthGuardProps) {
const { user, loading } = useSwissOIDAuth();
const router = useRouter();
const { locale, t } = useTranslation();
useEffect(() => {
if (!loading && !user) {
// Build login URL with locale-aware return path
const loginUrl = process.env.NEXT_PUBLIC_AUTH_BACKEND_URL
? `${process.env.NEXT_PUBLIC_AUTH_BACKEND_URL}/login`
: '/login';
// Redirect to login
window.location.href = loginUrl;
}
}, [user, loading, locale, router]);
// Show loading state while checking auth
if (loading) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<Loader2 className="w-12 h-12 text-indigo-600 animate-spin mb-4" />
<p className="text-gray-600">{t('Checking authentication...')}</p>
</div>
);
}
// Show loading state while redirecting
if (!user) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<Loader2 className="w-12 h-12 text-indigo-600 animate-spin mb-4" />
<p className="text-gray-600">{t('Redirecting to login...')}</p>
</div>
);
}
// User is authenticated, render children
return <>{children}</>;
}
Loading…
Cancel
Save