656 lines
32 KiB
TypeScript
656 lines
32 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState } from 'react';
|
|
import Link from 'next/link';
|
|
import { Club, UserPortalPermissions } from './types';
|
|
import { updateClubProfile, sanitizePortalError } from '../../lib/club-data';
|
|
|
|
interface FounderProfileEditorProps {
|
|
club: Club;
|
|
permissions: UserPortalPermissions;
|
|
callerUid?: string;
|
|
isDemoFixture?: boolean;
|
|
onProfileUpdated?: (updated: Partial<Club>) => void;
|
|
}
|
|
|
|
export function FounderProfileEditor({
|
|
club,
|
|
permissions,
|
|
callerUid,
|
|
isDemoFixture = false,
|
|
onProfileUpdated,
|
|
}: FounderProfileEditorProps) {
|
|
// Form fields matching Section 1 & 2
|
|
const [name, setName] = useState(club.name || '');
|
|
const [description, setDescription] = useState(club.description || '');
|
|
const [homeRegion, setHomeRegion] = useState(club.homeRegion || '');
|
|
const [motorcycleFocus, setMotorcycleFocus] = useState(club.motorcycleFocus || '');
|
|
const [logoUrl, setLogoUrl] = useState(club.logoUrl || '');
|
|
const [bannerUrl, setBannerUrl] = useState(club.bannerUrl || '');
|
|
|
|
// Save states
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [saveStatus, setSaveStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
|
|
// Image URL validation (HTTPS only or empty to clear)
|
|
function isValidHttpsImageUrl(url: string): boolean {
|
|
const trimmed = url.trim();
|
|
if (!trimmed) return true;
|
|
try {
|
|
const parsed = new URL(trimmed);
|
|
return parsed.protocol === 'https:';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Validation
|
|
const trimmedName = name.trim();
|
|
const isNameValid = trimmedName.length >= 3 && trimmedName.length <= 50;
|
|
const isDescValid = description.trim().length <= 500;
|
|
const isRegionValid = homeRegion.trim().length <= 100;
|
|
const isFocusValid = motorcycleFocus.trim().length <= 100;
|
|
const isLogoUrlValid = isValidHttpsImageUrl(logoUrl);
|
|
const isBannerUrlValid = isValidHttpsImageUrl(bannerUrl);
|
|
const canSave =
|
|
isNameValid &&
|
|
isDescValid &&
|
|
isRegionValid &&
|
|
isFocusValid &&
|
|
isLogoUrlValid &&
|
|
isBannerUrlValid &&
|
|
!isSaving &&
|
|
permissions.canManageSettings;
|
|
|
|
const isDirty =
|
|
trimmedName !== (club.name || '').trim() ||
|
|
description.trim() !== (club.description || '').trim() ||
|
|
homeRegion.trim() !== (club.homeRegion || '').trim() ||
|
|
motorcycleFocus.trim() !== (club.motorcycleFocus || '').trim() ||
|
|
logoUrl.trim() !== (club.logoUrl || '').trim() ||
|
|
bannerUrl.trim() !== (club.bannerUrl || '').trim();
|
|
|
|
// Short initials for preview emblem
|
|
const initials = name
|
|
.split(/\s+/)
|
|
.filter(Boolean)
|
|
.map((w) => w[0].toUpperCase())
|
|
.slice(0, 3)
|
|
.join('') || 'MSR';
|
|
|
|
async function handleSave(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
if (!canSave) return;
|
|
|
|
setIsSaving(true);
|
|
setSaveStatus('idle');
|
|
setErrorMessage(null);
|
|
|
|
const trimmedLogo = logoUrl.trim();
|
|
const trimmedBanner = bannerUrl.trim();
|
|
|
|
try {
|
|
if (isDemoFixture) {
|
|
// Simulated latency in development demo preview mode
|
|
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
setSaveStatus('success');
|
|
onProfileUpdated?.({
|
|
name: trimmedName,
|
|
description: description.trim(),
|
|
homeRegion: homeRegion.trim(),
|
|
motorcycleFocus: motorcycleFocus.trim(),
|
|
logoUrl: trimmedLogo || null,
|
|
bannerUrl: trimmedBanner || null,
|
|
});
|
|
} else {
|
|
if (!callerUid) {
|
|
throw new Error('Authentication required to save profile.');
|
|
}
|
|
await updateClubProfile(club.id, callerUid, {
|
|
name: trimmedName,
|
|
description: description.trim(),
|
|
homeRegion: homeRegion.trim(),
|
|
motorcycleFocus: motorcycleFocus.trim(),
|
|
logoUrl: trimmedLogo,
|
|
bannerUrl: trimmedBanner,
|
|
});
|
|
setSaveStatus('success');
|
|
onProfileUpdated?.({
|
|
name: trimmedName,
|
|
description: description.trim(),
|
|
homeRegion: homeRegion.trim(),
|
|
motorcycleFocus: motorcycleFocus.trim(),
|
|
logoUrl: trimmedLogo || null,
|
|
bannerUrl: trimmedBanner || null,
|
|
});
|
|
}
|
|
} catch (err) {
|
|
setSaveStatus('error');
|
|
setErrorMessage(sanitizePortalError(err, 'Failed to update club profile. Please try again.'));
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="w-full space-y-6 pb-12">
|
|
{/* 3. PAGE HEADER */}
|
|
<section className="border-b border-brand-border/60 pb-6 flex flex-col md:flex-row md:items-end justify-between gap-4">
|
|
<div>
|
|
<div className="flex items-center gap-3">
|
|
<span className="font-mono text-xs text-brand-cyan tracking-widest bg-brand-cyan/10 px-2.5 py-0.5 rounded-full border border-brand-cyan/30 font-medium">
|
|
Precision, Not Aggression
|
|
</span>
|
|
<span className="font-mono text-xs text-slate-400 tracking-wider">
|
|
// {homeRegion ? `${homeRegion} Zone` : 'Regional Squadron'}
|
|
</span>
|
|
</div>
|
|
<h1 className="font-headline font-bold text-2xl sm:text-3xl text-white mt-2 tracking-tight">
|
|
Club Profile
|
|
</h1>
|
|
<p className="font-body text-sm text-slate-400 mt-1">
|
|
Manage public-facing club identity, branding presentation, and discovery presence.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
{/* 4. TOP 4 METRIC CARDS */}
|
|
<section className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{/* Card 1: Club Name */}
|
|
<div className="relative rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 shadow-lg flex flex-col justify-between overflow-hidden">
|
|
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-brand-cyan/50 via-transparent to-transparent" />
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-mono text-xs text-slate-400 uppercase tracking-wider font-medium">Club Name</span>
|
|
<span className="w-1.5 h-1.5 bg-brand-cyan rounded-full" />
|
|
</div>
|
|
<div className="mt-3">
|
|
<div className="font-headline text-base font-bold text-white truncate">{name || club.name}</div>
|
|
<div className="font-mono text-[11px] text-brand-cyan mt-1">3-50 chars valid</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Card 2: Home Region */}
|
|
<div className="relative rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 shadow-lg flex flex-col justify-between overflow-hidden">
|
|
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-brand-cyan/50 via-transparent to-transparent" />
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-mono text-xs text-slate-400 uppercase tracking-wider font-medium">Home Region</span>
|
|
<span className="material-symbols-outlined text-slate-400 text-base">location_on</span>
|
|
</div>
|
|
<div className="mt-3">
|
|
<div className="font-headline text-base font-bold text-white truncate">
|
|
{homeRegion || 'Not specified'}
|
|
</div>
|
|
<div className="font-mono text-[11px] text-slate-400 mt-1">Home Region</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Card 3: Motorcycle Focus */}
|
|
<div className="relative rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 shadow-lg flex flex-col justify-between overflow-hidden">
|
|
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-brand-cyan/50 via-transparent to-transparent" />
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-mono text-xs text-slate-400 uppercase tracking-wider font-medium">Motorcycle Focus</span>
|
|
<span className="material-symbols-outlined text-slate-400 text-base">sports_motorsports</span>
|
|
</div>
|
|
<div className="mt-3">
|
|
<div className="font-headline text-base font-bold text-white truncate">
|
|
{motorcycleFocus || 'General'}
|
|
</div>
|
|
<div className="font-mono text-[11px] text-slate-400 mt-1">Motorcycle Focus</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Card 4: Discovery Presence */}
|
|
<div className="relative rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 shadow-lg flex flex-col justify-between overflow-hidden">
|
|
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-brand-cyan/50 via-transparent to-transparent" />
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-mono text-xs text-slate-400 uppercase tracking-wider font-medium">Discovery Presence</span>
|
|
<span
|
|
className={`w-2 h-2 rounded-full ${
|
|
club.visibility === 'public' ? 'bg-brand-cyan' : club.visibility === 'unlisted' ? 'bg-amber-400' : 'bg-slate-500'
|
|
}`}
|
|
/>
|
|
</div>
|
|
<div className="mt-3">
|
|
<div
|
|
className={`font-headline text-base font-bold capitalize ${
|
|
club.visibility === 'public' ? 'text-brand-cyan' : club.visibility === 'unlisted' ? 'text-amber-400' : 'text-white'
|
|
}`}
|
|
>
|
|
{club.visibility}
|
|
</div>
|
|
<div className="font-mono text-[11px] text-slate-400 mt-1">
|
|
{club.visibility === 'public'
|
|
? 'Listed in public club index & directory'
|
|
: club.visibility === 'unlisted'
|
|
? 'Accessible via direct handle only'
|
|
: 'Restricted private club'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* 5. MAIN WORKSPACE (12-Column Grid) */}
|
|
<form onSubmit={handleSave} className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
|
{/* LEFT MAIN (8 cols) */}
|
|
<div className="lg:col-span-8 space-y-6">
|
|
{/* Status Feedback Banner */}
|
|
{saveStatus === 'success' && (
|
|
<div className="p-4 rounded bg-secondary/10 border border-secondary/30 flex items-center gap-3 text-secondary">
|
|
<span className="material-symbols-outlined text-xl">check_circle</span>
|
|
<div className="text-xs font-body font-medium">
|
|
Club profile changes have been saved successfully.
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{saveStatus === 'error' && errorMessage && (
|
|
<div className="p-4 rounded bg-error-container border border-error/30 flex items-center gap-3 text-error">
|
|
<span className="material-symbols-outlined text-xl">error</span>
|
|
<div className="text-xs font-body font-medium">{errorMessage}</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* SECTION 1: PUBLIC CLUB IDENTITY */}
|
|
<div className="rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 space-y-5 shadow-xl">
|
|
<div className="flex items-center justify-between border-b border-brand-border/60 pb-3.5">
|
|
<div className="flex items-center gap-2">
|
|
<span className="material-symbols-outlined text-brand-cyan text-xl">badge</span>
|
|
<h2 className="font-headline text-base font-bold text-white">
|
|
Section 1: Public Club Identity
|
|
</h2>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Club Name */}
|
|
<div className="space-y-1.5">
|
|
<label htmlFor="club-name-input" className="font-mono text-xs text-slate-400 block font-medium">
|
|
Club Name
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
id="club-name-input"
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
disabled={!permissions.canManageSettings || isSaving}
|
|
placeholder="e.g. MotorSignal Riders Atlanta"
|
|
className="w-full bg-surface-container-low/90 border border-brand-border/80 focus:border-brand-cyan rounded-xl px-3.5 py-2.5 text-sm font-body text-white outline-none transition-colors"
|
|
/>
|
|
<span
|
|
className={`absolute right-3.5 top-3 font-mono text-[11px] font-semibold ${
|
|
isNameValid ? 'text-brand-cyan' : 'text-error'
|
|
}`}
|
|
>
|
|
{isNameValid ? 'Valid' : `${trimmedName.length}/50`}
|
|
</span>
|
|
</div>
|
|
<p className="font-body text-xs text-slate-400">
|
|
3-50 characters. Displayed on public discovery, ride manifests, and club roster.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Club Handle / Identifier (Locked) */}
|
|
<div className="space-y-1.5">
|
|
<div className="flex items-center justify-between">
|
|
<label className="font-mono text-xs text-slate-400 font-medium">
|
|
Club Handle / Identifier
|
|
</label>
|
|
<span className="font-mono text-[10px] px-2 py-0.5 bg-surface-container text-slate-400 border border-brand-border/60 rounded-lg">
|
|
Immutable
|
|
</span>
|
|
</div>
|
|
<div className="relative flex items-center">
|
|
<span className="material-symbols-outlined absolute left-3.5 text-slate-400 text-base">lock</span>
|
|
<input
|
|
type="text"
|
|
value={`@${club.slug}`}
|
|
readOnly
|
|
disabled
|
|
className="w-full bg-surface-container-lowest/80 border border-brand-border/60 rounded-xl pl-10 pr-3.5 py-2.5 text-xs font-mono text-slate-400 cursor-not-allowed opacity-90"
|
|
/>
|
|
</div>
|
|
<p className="font-body text-xs text-slate-400">
|
|
Club handles are permanently reserved and cannot be renamed or reassigned.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Club Description */}
|
|
<div className="space-y-1.5">
|
|
<div className="flex items-center justify-between">
|
|
<label htmlFor="club-desc-input" className="font-mono text-xs text-slate-400 block font-medium">
|
|
Club Description
|
|
</label>
|
|
<span className="font-mono text-[11px] text-slate-400">
|
|
{description.length}/500
|
|
</span>
|
|
</div>
|
|
<textarea
|
|
id="club-desc-input"
|
|
rows={3}
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
disabled={!permissions.canManageSettings || isSaving}
|
|
placeholder="Describe your club's ethos, riding discipline, and mission..."
|
|
className="w-full bg-surface-container-low/90 border border-brand-border/80 focus:border-brand-cyan rounded-xl p-3.5 text-sm font-body text-white outline-none transition-colors leading-relaxed"
|
|
/>
|
|
<p className="font-body text-xs text-slate-400">
|
|
Displayed on your public Club profile.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SECTION 2: REGIONAL & MOTORCYCLE FOCUS */}
|
|
<div className="rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 space-y-5 shadow-xl">
|
|
<div className="flex items-center justify-between border-b border-brand-border/60 pb-3.5">
|
|
<div className="flex items-center gap-2">
|
|
<span className="material-symbols-outlined text-brand-cyan text-xl">explore</span>
|
|
<h2 className="font-headline text-base font-bold text-white">
|
|
Section 2: Regional & Motorcycle Focus
|
|
</h2>
|
|
</div>
|
|
<span className="font-mono text-xs text-slate-400">Geo / Discipline</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{/* Home Region */}
|
|
<div className="space-y-1.5">
|
|
<label htmlFor="club-region-input" className="font-mono text-xs text-slate-400 block font-medium">
|
|
Home Region
|
|
</label>
|
|
<input
|
|
id="club-region-input"
|
|
type="text"
|
|
value={homeRegion}
|
|
onChange={(e) => setHomeRegion(e.target.value)}
|
|
disabled={!permissions.canManageSettings || isSaving}
|
|
placeholder="e.g. Atlanta, Georgia"
|
|
className="w-full bg-surface-container-low/90 border border-brand-border/80 focus:border-brand-cyan rounded-xl px-3.5 py-2.5 text-sm font-body text-white outline-none transition-colors"
|
|
/>
|
|
<p className="font-body text-xs text-slate-400">Primary area where your Club is based.</p>
|
|
</div>
|
|
|
|
{/* Motorcycle Focus */}
|
|
<div className="space-y-1.5">
|
|
<label htmlFor="club-focus-input" className="font-mono text-xs text-slate-400 block font-medium">
|
|
Motorcycle Focus
|
|
</label>
|
|
<input
|
|
id="club-focus-input"
|
|
type="text"
|
|
value={motorcycleFocus}
|
|
onChange={(e) => setMotorcycleFocus(e.target.value)}
|
|
disabled={!permissions.canManageSettings || isSaving}
|
|
placeholder="e.g. Sport / Touring"
|
|
className="w-full bg-surface-container-low/90 border border-brand-border/80 focus:border-brand-cyan rounded-xl px-3.5 py-2.5 text-sm font-body text-white outline-none transition-colors"
|
|
/>
|
|
<p className="font-body text-xs text-slate-400">
|
|
Main riding style or motorcycle focus for your Club.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SECTION 3: LOGO & BANNER */}
|
|
<div className="rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 space-y-5 shadow-xl">
|
|
<div className="flex items-center justify-between border-b border-brand-border/60 pb-3.5">
|
|
<div className="flex items-center gap-2">
|
|
<span className="material-symbols-outlined text-brand-cyan text-xl">palette</span>
|
|
<h2 className="font-headline text-base font-bold text-white">
|
|
Section 3: Logo & Banner
|
|
</h2>
|
|
</div>
|
|
<span className="font-mono text-xs text-slate-400">Visual Assets</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
{/* Logo Presentation Card */}
|
|
<div className="bg-surface-container-low/70 border border-brand-border/60 rounded-xl p-4 flex flex-col justify-between space-y-3">
|
|
<div className="flex items-center justify-between font-mono text-xs text-slate-400">
|
|
<span>Club Emblem</span>
|
|
{logoUrl && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setLogoUrl('')}
|
|
disabled={!permissions.canManageSettings || isSaving}
|
|
className="text-rose-400 hover:text-rose-300 transition-colors text-[11px] font-mono flex items-center gap-1 cursor-pointer"
|
|
>
|
|
<span className="material-symbols-outlined text-xs">close</span>
|
|
<span>Remove</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
<div className="h-36 bg-surface-container-lowest border border-dashed border-brand-border/80 rounded-xl flex flex-col items-center justify-center relative overflow-hidden">
|
|
{logoUrl ? (
|
|
<img src={logoUrl} alt="Club logo" className="max-h-24 max-w-24 object-contain rounded-lg" />
|
|
) : (
|
|
<div className="w-20 h-20 rounded-xl border-2 border-brand-cyan bg-surface-container-high flex flex-col items-center justify-center shadow-lg relative">
|
|
<span className="material-symbols-outlined text-brand-cyan text-2xl">two_wheeler</span>
|
|
<span className="font-headline text-brand-cyan font-bold text-xs tracking-widest mt-1">
|
|
{initials}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="space-y-2 pt-1">
|
|
<div className="space-y-1">
|
|
<input
|
|
type="url"
|
|
value={logoUrl}
|
|
onChange={(e) => setLogoUrl(e.target.value)}
|
|
disabled={!permissions.canManageSettings || isSaving}
|
|
placeholder="https://example.com/logo.png"
|
|
className={`w-full bg-surface-container-low/90 border ${
|
|
!isLogoUrlValid ? 'border-error focus:border-error' : 'border-brand-border/80 focus:border-brand-cyan'
|
|
} rounded-xl px-3 py-2 text-xs font-mono text-white outline-none transition-colors`}
|
|
/>
|
|
{!isLogoUrlValid && (
|
|
<p className="font-mono text-[10px] text-error">
|
|
Must be a valid HTTPS URL (e.g. https://example.com/logo.png).
|
|
</p>
|
|
)}
|
|
</div>
|
|
<p className="font-mono text-[10px] text-slate-400">
|
|
Direct file upload requires Cloud Storage integration (planned for future release). Enter an HTTPS image URL or leave blank.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Banner Presentation Card */}
|
|
<div className="bg-surface-container-low/70 border border-brand-border/60 rounded-xl p-4 flex flex-col justify-between space-y-3">
|
|
<div className="flex items-center justify-between font-mono text-xs text-slate-400">
|
|
<span>Banner Presentation</span>
|
|
{bannerUrl && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setBannerUrl('')}
|
|
disabled={!permissions.canManageSettings || isSaving}
|
|
className="text-rose-400 hover:text-rose-300 transition-colors text-[11px] font-mono flex items-center gap-1 cursor-pointer"
|
|
>
|
|
<span className="material-symbols-outlined text-xs">close</span>
|
|
<span>Remove</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
<div className="h-36 bg-surface-container-lowest border border-dashed border-brand-border/80 rounded-xl relative overflow-hidden flex items-center justify-center">
|
|
{bannerUrl ? (
|
|
<img
|
|
src={bannerUrl}
|
|
alt="Club banner presentation"
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full bg-surface-container-low flex items-center justify-center relative overflow-hidden">
|
|
<div className="absolute inset-0 bg-[radial-gradient(#00f2fe_1px,transparent_1px)] [background-size:16px_16px] opacity-10" />
|
|
<div className="flex items-center gap-2 font-mono text-xs text-slate-400 z-10">
|
|
<span className="material-symbols-outlined text-base text-brand-cyan">image</span>
|
|
<span className="tracking-wider font-semibold text-white">{name || club.name}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="space-y-2 pt-1">
|
|
<div className="space-y-1">
|
|
<input
|
|
type="url"
|
|
value={bannerUrl}
|
|
onChange={(e) => setBannerUrl(e.target.value)}
|
|
disabled={!permissions.canManageSettings || isSaving}
|
|
placeholder="https://example.com/banner.jpg"
|
|
className={`w-full bg-surface-container-low/90 border ${
|
|
!isBannerUrlValid ? 'border-error focus:border-error' : 'border-brand-border/80 focus:border-brand-cyan'
|
|
} rounded-xl px-3 py-2 text-xs font-mono text-white outline-none transition-colors`}
|
|
/>
|
|
{!isBannerUrlValid && (
|
|
<p className="font-mono text-[10px] text-error">
|
|
Must be a valid HTTPS URL (e.g. https://example.com/banner.jpg).
|
|
</p>
|
|
)}
|
|
</div>
|
|
<p className="font-mono text-[10px] text-slate-400">
|
|
Direct file upload requires Cloud Storage integration (planned for future release). Enter an HTTPS image URL or leave blank.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ACTION DOCKED FOOTER */}
|
|
<div className="rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 flex flex-col sm:flex-row items-center justify-between gap-4 shadow-xl">
|
|
<div className="flex items-center gap-2 font-body text-xs text-slate-300">
|
|
<span className="material-symbols-outlined text-brand-cyan text-base">check_circle</span>
|
|
<span>Saves public club profile information.</span>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
disabled={!canSave || (!isDirty && saveStatus !== 'error')}
|
|
className={`w-full sm:w-auto flex items-center justify-center gap-2 px-6 py-2.5 rounded-xl font-headline text-xs font-bold tracking-wider transition-all duration-150 ${
|
|
canSave && (isDirty || saveStatus === 'error')
|
|
? 'bg-primary-container hover:brightness-110 text-on-primary-container shadow-[0_0_16px_rgba(0,242,254,0.3)] cursor-pointer'
|
|
: 'bg-surface-container-high text-slate-500 cursor-not-allowed opacity-60'
|
|
}`}
|
|
>
|
|
{isSaving ? (
|
|
<>
|
|
<span className="material-symbols-outlined text-base animate-spin">progress_activity</span>
|
|
<span>Saving Changes...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<span className="material-symbols-outlined text-base">check</span>
|
|
<span>Save Profile Changes</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* RIGHT RAIL (4 cols) */}
|
|
<div className="lg:col-span-4 space-y-6">
|
|
{/* CARD A: PUBLIC DIRECTORY PREVIEW (Live Simulation) */}
|
|
<div className="rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 space-y-4 shadow-xl">
|
|
<div className="flex items-center justify-between border-b border-brand-border/60 pb-3">
|
|
<div className="flex items-center gap-2">
|
|
<span className="material-symbols-outlined text-brand-cyan text-base">visibility</span>
|
|
<span data-section="DIRECTORY PREVIEW" className="font-headline text-sm text-white font-bold">
|
|
Directory Preview
|
|
</span>
|
|
</div>
|
|
<span data-badge="LIVE SIMULATION" className="font-mono text-[10px] bg-brand-cyan/10 text-brand-cyan px-2.5 py-0.5 rounded-full border border-brand-cyan/30 font-semibold">
|
|
Live Simulation
|
|
</span>
|
|
</div>
|
|
|
|
{/* Embedded Public Card Mockup (IDENTITY ONLY, ZERO METRICS) */}
|
|
<div className="rounded-xl bg-surface-container-low border border-brand-border/60 overflow-hidden shadow-lg">
|
|
{/* Banner Mockup */}
|
|
<div className="h-24 bg-surface-container-high relative overflow-hidden">
|
|
{bannerUrl ? (
|
|
<img
|
|
src={bannerUrl}
|
|
alt="Directory banner preview"
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full bg-surface-container-low flex items-center justify-center relative overflow-hidden">
|
|
<div className="absolute inset-0 bg-[radial-gradient(#00f2fe_1px,transparent_1px)] [background-size:16px_16px] opacity-10" />
|
|
</div>
|
|
)}
|
|
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-surface-container-low" />
|
|
</div>
|
|
|
|
<div className="p-4 pt-0 relative">
|
|
{/* Emblem overlapping */}
|
|
<div className="-mt-8 w-14 h-14 rounded-xl bg-surface-container-high border-2 border-brand-cyan flex flex-col items-center justify-center text-brand-cyan shadow-md overflow-hidden">
|
|
{logoUrl ? (
|
|
<img src={logoUrl} alt="Emblem preview" className="w-full h-full object-cover" />
|
|
) : (
|
|
<>
|
|
<span className="material-symbols-outlined text-xl">two_wheeler</span>
|
|
<span className="font-mono text-[10px] font-bold leading-none">{initials}</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-2 flex items-center">
|
|
<h3 className="font-headline text-sm text-white font-bold truncate">
|
|
{name || club.name}
|
|
</h3>
|
|
</div>
|
|
|
|
<div className="font-mono text-[11px] text-brand-cyan mt-0.5">@{club.slug}</div>
|
|
|
|
<div className="mt-2 inline-flex items-center gap-1.5 font-mono text-[10px] text-brand-cyan bg-brand-cyan/10 px-2 py-0.5 rounded-full border border-brand-cyan/20">
|
|
<span className="material-symbols-outlined text-xs">location_on</span>
|
|
<span className="truncate">
|
|
{homeRegion || 'Global'} • {motorcycleFocus || 'General'}
|
|
</span>
|
|
</div>
|
|
|
|
<p className="mt-3 font-body text-xs text-slate-300 line-clamp-2 leading-relaxed">
|
|
{description || 'No club description provided yet.'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="font-mono text-[10px] text-slate-400 text-center">
|
|
Rendered in standard public directory feed (1:1 responsive layout)
|
|
</p>
|
|
</div>
|
|
|
|
{/* CARD B: ACCESS & DISCOVERY BOUNDARY */}
|
|
<div className="rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 space-y-3 shadow-xl">
|
|
<div className="flex items-center gap-2 text-white border-b border-brand-border/60 pb-3">
|
|
<span className="material-symbols-outlined text-brand-cyan text-base">admin_panel_settings</span>
|
|
<h3 className="font-headline text-sm font-bold">Access Controls</h3>
|
|
</div>
|
|
<p className="font-body text-xs text-slate-300 leading-relaxed">
|
|
Access settings (Visibility & Admission Policy) are managed separately under Club Settings.
|
|
</p>
|
|
<div className="pt-1 flex items-center">
|
|
<Link href={`/clubs/${club.slug || club.id}/manage/settings`} className="inline-flex items-center gap-1.5 text-brand-cyan hover:underline font-mono text-xs font-semibold">
|
|
<span>Manage Access Settings</span>
|
|
<span className="material-symbols-outlined text-sm">arrow_forward</span>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* CARD C: PRIVACY CONTRACT */}
|
|
<div className="rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 space-y-3 shadow-xl">
|
|
<div className="flex items-center gap-2 text-brand-cyan">
|
|
<span className="material-symbols-outlined text-base">lock</span>
|
|
<span data-section="PRIVACY CONTRACT" className="font-mono text-xs font-bold tracking-wider uppercase">Privacy Contract</span>
|
|
</div>
|
|
<blockquote className="font-body text-xs italic text-slate-200 border-l-2 border-brand-cyan pl-3 py-1 bg-surface-container-lowest/60 rounded-r leading-relaxed">
|
|
“Public Club Profile is organizational identity only. Member telemetry, GPS tracks, personal ride logs, and private account data are not included.”
|
|
</blockquote>
|
|
<p className="font-mono text-[10px] text-slate-400">
|
|
Telemetry and member activity are strictly isolated.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|