MotorSignal-App/website/components/founder-portal/founder-settings-editor.tsx

1037 lines
52 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import {
Club,
UserPortalPermissions,
ClubVisibility,
ClubAdmissionPolicy,
ClubMember,
} from './types';
import {
updateClubAccessSettings,
fetchEligibleTransferMembers,
transferClubOwnership,
archiveClub,
sanitizePortalError,
} from '../../lib/club-data';
interface FounderSettingsEditorProps {
club: Club;
permissions: UserPortalPermissions;
callerUid?: string;
isDemoFixture?: boolean;
onClubUpdated?: (updated: Partial<Club>) => void;
}
export function FounderSettingsEditor({
club,
permissions,
callerUid,
isDemoFixture = false,
onClubUpdated,
}: FounderSettingsEditorProps) {
const router = useRouter();
const searchParams = useSearchParams();
const isDemo = searchParams?.get('preview') === 'demo' || isDemoFixture;
const queryParam = isDemo ? '?preview=demo' : '';
const clubParam = club.slug || club.id;
// Access Settings Form State
const [visibility, setVisibility] = useState<ClubVisibility>(club.visibility || 'public');
const [admissionPolicy, setAdmissionPolicy] = useState<ClubAdmissionPolicy>(club.admissionPolicy || 'request');
// Mutation / UI feedback states
const [isSavingAccess, setIsSavingAccess] = useState(false);
const [accessSaveStatus, setAccessSaveStatus] = useState<'idle' | 'success' | 'error'>('idle');
const [accessErrorMessage, setAccessErrorMessage] = useState<string | null>(null);
// Ownership Transfer Modal State
const [isTransferModalOpen, setIsTransferModalOpen] = useState(false);
const [eligibleMembers, setEligibleMembers] = useState<ClubMember[]>([]);
const [loadingMembers, setLoadingMembers] = useState(false);
const [selectedTargetUid, setSelectedTargetUid] = useState<string>('');
const [isTransferring, setIsTransferring] = useState(false);
const [transferError, setTransferError] = useState<string | null>(null);
// Archive Modal State
const [isArchiveModalOpen, setIsArchiveModalOpen] = useState(false);
const [isArchiving, setIsArchiving] = useState(false);
const [archiveError, setArchiveError] = useState<string | null>(null);
const isOwner = permissions.role === 'owner' || permissions.canTransferOwnership;
const isAdmin = permissions.role === 'admin' || isOwner;
const canManageAccess = isAdmin && permissions.canManageSettings;
const isArchived = Boolean(club.archivedAt);
const isAccessDirty =
visibility !== club.visibility ||
admissionPolicy !== club.admissionPolicy;
// Load eligible members for ownership transfer when modal opens
useEffect(() => {
if (isTransferModalOpen && callerUid) {
setLoadingMembers(true);
setTransferError(null);
fetchEligibleTransferMembers(club.id, callerUid, { allowDemoPreview: isDemo })
.then((members) => {
setEligibleMembers(members);
if (members.length > 0) {
setSelectedTargetUid(members[0].uid);
}
})
.catch((err) => {
setTransferError(sanitizePortalError(err, 'Failed to load eligible active members.'));
})
.finally(() => {
setLoadingMembers(false);
});
}
}, [isTransferModalOpen, club.id, callerUid, isDemo]);
// Handle Save Access Settings
async function handleSaveAccess(e: React.FormEvent) {
e.preventDefault();
if (!canManageAccess || isArchived || isSavingAccess || !isAccessDirty) return;
setIsSavingAccess(true);
setAccessSaveStatus('idle');
setAccessErrorMessage(null);
try {
if (isDemo) {
await new Promise((resolve) => setTimeout(resolve, 600));
setAccessSaveStatus('success');
onClubUpdated?.({ visibility, admissionPolicy });
} else {
if (!callerUid) {
throw new Error('Authentication required to update access settings.');
}
await updateClubAccessSettings(club.id, callerUid, {
visibility,
admissionPolicy,
});
setAccessSaveStatus('success');
onClubUpdated?.({ visibility, admissionPolicy });
}
} catch (err) {
setAccessSaveStatus('error');
setAccessErrorMessage(sanitizePortalError(err, 'Failed to update access settings. Please try again.'));
} finally {
setIsSavingAccess(false);
}
}
// Handle Ownership Transfer
async function handleConfirmTransfer() {
if (!isOwner || isArchived || isTransferring || !selectedTargetUid) return;
setIsTransferring(true);
setTransferError(null);
try {
if (isDemo) {
await new Promise((resolve) => setTimeout(resolve, 800));
setIsTransferModalOpen(false);
onClubUpdated?.({
ownerUid: selectedTargetUid,
founderName: 'Transferred Owner',
});
} else {
if (!callerUid) {
throw new Error('Authentication required to transfer ownership.');
}
await transferClubOwnership(club.id, callerUid, selectedTargetUid);
setIsTransferModalOpen(false);
onClubUpdated?.({
ownerUid: selectedTargetUid,
});
router.refresh();
}
} catch (err) {
setTransferError(sanitizePortalError(err, 'Failed to transfer ownership. Please try again.'));
} finally {
setIsTransferring(false);
}
}
// Handle Archive Club
async function handleConfirmArchive() {
if (!isOwner || isArchived || isArchiving) return;
setIsArchiving(true);
setArchiveError(null);
try {
if (isDemo) {
await new Promise((resolve) => setTimeout(resolve, 800));
setIsArchiveModalOpen(false);
onClubUpdated?.({ archivedAt: new Date().toISOString() });
} else {
if (!callerUid) {
throw new Error('Authentication required to archive club.');
}
await archiveClub(club.id, callerUid);
setIsArchiveModalOpen(false);
onClubUpdated?.({ archivedAt: new Date().toISOString() });
router.refresh();
}
} catch (err) {
setArchiveError(sanitizePortalError(err, 'Failed to archive club. Please try again.'));
} finally {
setIsArchiving(false);
}
}
return (
<div className="max-w-6xl mx-auto space-y-6 pb-12">
{/* 2. HEADER */}
<section className="p-6 rounded-2xl glass-card border border-brand-border/60 relative overflow-hidden flex flex-col md:flex-row md:items-end justify-between gap-4 shadow-lg">
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-brand-cyan/60 via-brand-cyan/20 to-transparent" />
<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">
// {club.homeRegion ? `${club.homeRegion} Zone` : 'Regional Squadron'}
</span>
</div>
<h1 className="font-headline font-bold text-2xl sm:text-3xl text-white mt-2 tracking-tight">
Settings
</h1>
<p className="font-body text-sm text-slate-400 mt-1">
Configure Club access, governance, and administrative lifecycle.
</p>
</div>
</section>
{/* 3. TOP SUMMARY KPI CARDS (Row of 4) */}
<section className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Card 1: Club Visibility */}
<div className="rounded-2xl glass-card border border-brand-border/60 p-4 sm:p-5 flex flex-col justify-between shadow-lg relative 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-[11px] text-slate-400">Club Visibility</span>
<span className="material-symbols-outlined text-brand-cyan text-sm">public</span>
</div>
<div className="mt-2">
<div className="font-headline text-sm font-semibold capitalize text-white truncate">
{visibility}
</div>
<div className="flex items-center gap-1.5 mt-1 text-[11px] font-mono text-slate-400">
<span
className={`w-1.5 h-1.5 rounded-full ${
visibility === 'public'
? 'bg-emerald-400'
: visibility === 'unlisted'
? 'bg-amber-400'
: 'bg-slate-500'
}`}
/>
<span>
{visibility === 'public'
? 'Listed in directory & search'
: visibility === 'unlisted'
? 'Direct handle only'
: 'Restricted private club'}
</span>
</div>
</div>
</div>
{/* Card 2: Admission Policy */}
<div className="rounded-2xl glass-card border border-brand-border/60 p-4 sm:p-5 flex flex-col justify-between shadow-lg relative overflow-hidden">
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-amber-400/50 via-transparent to-transparent" />
<div className="flex items-center justify-between">
<span className="font-mono text-[11px] text-slate-400">Admission Policy</span>
<span className="material-symbols-outlined text-amber-400 text-sm">assignment_ind</span>
</div>
<div className="mt-2">
<div className="font-headline text-sm font-semibold text-white truncate">
{admissionPolicy === 'open'
? 'Open'
: admissionPolicy === 'request'
? 'Request'
: 'Invite Only'}
</div>
<div className="flex items-center gap-1.5 mt-1 text-[11px] font-mono text-slate-400">
<span
className={`w-1.5 h-1.5 rounded-full ${
admissionPolicy === 'open'
? 'bg-emerald-400'
: admissionPolicy === 'request'
? 'bg-amber-400'
: 'bg-brand-cyan'
}`}
/>
<span>
{admissionPolicy === 'open'
? 'Open join without review'
: admissionPolicy === 'request'
? 'Leadership review required'
: 'Direct invitation only'}
</span>
</div>
</div>
</div>
{/* Card 3: Club Handle */}
<div className="rounded-2xl glass-card border border-brand-border/60 p-4 sm:p-5 flex flex-col justify-between shadow-lg relative 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-[11px] text-slate-400">Club Handle</span>
<span className="material-symbols-outlined text-brand-cyan text-sm">lock</span>
</div>
<div className="mt-2">
<div className="font-mono text-sm font-semibold text-brand-cyan truncate">
@{club.slug}
</div>
<div className="flex items-center gap-1.5 mt-1 text-[11px] font-mono text-slate-400">
<span className="w-1.5 h-1.5 rounded-full bg-brand-cyan" />
<span>Immutable reservation</span>
</div>
</div>
</div>
{/* Card 4: Lifecycle State */}
<div className="rounded-2xl glass-card border border-brand-border/60 p-4 sm:p-5 flex flex-col justify-between shadow-lg relative overflow-hidden">
<div
className={`absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r ${
isArchived ? 'from-amber-400/50' : 'from-emerald-400/50'
} via-transparent to-transparent`}
/>
<div className="flex items-center justify-between">
<span className="font-mono text-[11px] text-slate-400">Lifecycle State</span>
<span
className={`material-symbols-outlined text-sm ${
isArchived ? 'text-amber-400' : 'text-emerald-400'
}`}
>
{isArchived ? 'archive' : 'check_circle'}
</span>
</div>
<div className="mt-2">
<div
className={`font-headline text-sm font-semibold ${
isArchived ? 'text-amber-400' : 'text-emerald-400'
}`}
>
{isArchived ? 'Archived' : 'Active'}
</div>
<div className="flex items-center gap-1.5 mt-1 text-[11px] font-mono text-slate-400">
<span
className={`w-1.5 h-1.5 rounded-full ${
isArchived ? 'bg-amber-400' : 'bg-emerald-400'
}`}
/>
<span>{isArchived ? 'Discovery Removed • Retained' : 'Operational • Nominal'}</span>
</div>
</div>
</div>
</section>
{/* Archived Status Warning Banner */}
{isArchived && (
<div className="p-4 rounded bg-amber-500/10 border border-amber-500/30 flex items-start gap-3 text-amber-200">
<span className="material-symbols-outlined text-xl text-amber-400 shrink-0 mt-0.5">archive</span>
<div className="text-xs font-body space-y-1">
<div className="font-semibold tracking-wider font-headline text-amber-300">
Club is Archived
</div>
<div>
This Club has been archived. It is removed from public discovery queries and does not accept new join requests. Existing organizational records are retained.
</div>
</div>
</div>
)}
{/* 4. MAIN WORKSPACE (12-Column Grid) */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
{/* Left Column (8 cols) */}
<div className="lg:col-span-8 space-y-6">
{/* SECTION 1: CLUB ACCESS & DISCOVERY */}
<form onSubmit={handleSaveAccess} className="rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 space-y-6 shadow-lg relative overflow-hidden">
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-brand-cyan/60 via-brand-cyan/20 to-transparent" />
<div className="border-b border-brand-border/60 pb-3 flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-brand-cyan text-lg">travel_explore</span>
<h2 className="font-headline text-sm sm:text-base font-semibold text-white">
Section 1: Club Access &amp; Discovery
</h2>
</div>
<p className="font-body text-xs text-slate-400 mt-1">
Controls visibility across public index registries and member admission requirements.
</p>
</div>
<span className="font-mono text-[10px] text-brand-cyan bg-brand-cyan/10 border border-brand-cyan/30 px-2.5 py-0.5 rounded-full font-medium">
{canManageAccess ? 'Leadership Action' : 'Leadership Only'}
</span>
</div>
{/* Save Feedback Alerts */}
{accessSaveStatus === 'success' && (
<div className="p-3.5 rounded-xl bg-brand-cyan/10 border border-brand-cyan/30 flex items-center gap-2.5 text-brand-cyan text-xs font-medium">
<span className="material-symbols-outlined text-lg">check_circle</span>
<span>Club access settings saved successfully.</span>
</div>
)}
{accessSaveStatus === 'error' && accessErrorMessage && (
<div className="p-3.5 rounded-xl bg-error-container border border-error/30 flex items-center gap-2.5 text-error text-xs font-medium">
<span className="material-symbols-outlined text-lg">error</span>
<span>{accessErrorMessage}</span>
</div>
)}
{/* Subsection A: Visibility */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="font-mono text-[11px] text-slate-400 font-medium">
Club Visibility
</label>
<span className="font-mono text-[10px] text-slate-400 capitalize">
Current: {club.visibility}
</span>
</div>
<div className="grid grid-cols-1 xl:grid-cols-3 gap-3">
{/* PUBLIC */}
<button
type="button"
disabled={!canManageAccess || isArchived || isSavingAccess}
onClick={() => setVisibility('public')}
className={`p-3.5 rounded-xl text-left transition-all relative ${
visibility === 'public'
? 'bg-brand-cyan/10 border-2 border-brand-cyan text-white shadow-[0_0_12px_rgba(0,242,254,0.15)]'
: 'bg-surface-container-low/60 border border-brand-border/70 hover:border-brand-border text-slate-300 opacity-80 hover:opacity-100'
} ${!canManageAccess || isArchived ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`}
>
<div className="flex items-center justify-between mb-1.5">
<span className="font-headline text-xs font-bold text-white flex items-center gap-1.5">
<span className="material-symbols-outlined text-sm text-brand-cyan">
{visibility === 'public' ? 'radio_button_checked' : 'radio_button_unchecked'}
</span>
Public
</span>
{visibility === 'public' && (
<span className="font-mono text-[9px] bg-brand-cyan/20 text-brand-cyan px-1.5 py-0.5 rounded font-semibold">
Selected
</span>
)}
</div>
<p className="font-body text-[11px] text-slate-400 leading-snug">
Discoverable in public directory and search. Visible to all riders.
</p>
</button>
{/* UNLISTED */}
<button
type="button"
disabled={!canManageAccess || isArchived || isSavingAccess}
onClick={() => setVisibility('unlisted')}
className={`p-3.5 rounded-xl text-left transition-all relative ${
visibility === 'unlisted'
? 'bg-brand-cyan/10 border-2 border-brand-cyan text-white shadow-[0_0_12px_rgba(0,242,254,0.15)]'
: 'bg-surface-container-low/60 border border-brand-border/70 hover:border-brand-border text-slate-300 opacity-80 hover:opacity-100'
} ${!canManageAccess || isArchived ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`}
>
<div className="flex items-center justify-between mb-1.5">
<span className="font-headline text-xs font-bold text-white flex items-center gap-1.5">
<span className="material-symbols-outlined text-sm text-brand-cyan">
{visibility === 'unlisted' ? 'radio_button_checked' : 'radio_button_unchecked'}
</span>
Unlisted
</span>
{visibility === 'unlisted' && (
<span className="font-mono text-[9px] bg-brand-cyan/20 text-brand-cyan px-1.5 py-0.5 rounded font-semibold">
Selected
</span>
)}
</div>
<p className="font-body text-[11px] text-slate-400 leading-snug">
Accessible via direct handle or link only. Excluded from public discovery queries.
</p>
</button>
{/* PRIVATE */}
<button
type="button"
disabled={!canManageAccess || isArchived || isSavingAccess}
onClick={() => setVisibility('private')}
className={`p-3.5 rounded-xl text-left transition-all relative ${
visibility === 'private'
? 'bg-brand-cyan/10 border-2 border-brand-cyan text-white shadow-[0_0_12px_rgba(0,242,254,0.15)]'
: 'bg-surface-container-low/60 border border-brand-border/70 hover:border-brand-border text-slate-300 opacity-80 hover:opacity-100'
} ${!canManageAccess || isArchived ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`}
>
<div className="flex items-center justify-between mb-1.5">
<span className="font-headline text-xs font-bold text-white flex items-center gap-1.5">
<span className="material-symbols-outlined text-sm text-brand-cyan">
{visibility === 'private' ? 'radio_button_checked' : 'radio_button_unchecked'}
</span>
Private
</span>
{visibility === 'private' && (
<span className="font-mono text-[9px] bg-brand-cyan/20 text-brand-cyan px-1.5 py-0.5 rounded font-semibold">
Selected
</span>
)}
</div>
<p className="font-body text-[11px] text-slate-400 leading-snug">
Visible only to active members, invited riders, and Club leadership.
</p>
</button>
</div>
<div className="font-mono text-[11px] text-slate-400 flex items-center gap-1.5 pt-1">
<span className="material-symbols-outlined text-xs text-brand-cyan">info</span>
<span>Unlisted and private clubs are hidden from public directory and unauthenticated discovery.</span>
</div>
</div>
<div className="h-px bg-brand-border/60" />
{/* Subsection B: Admission Policy */}
<div className="space-y-3 pt-2">
<div className="flex items-center justify-between">
<label className="font-mono text-[11px] text-slate-400 font-medium">
Member Admission Policy
</label>
<span className="font-mono text-[10px] text-slate-400 capitalize">
Current: {club.admissionPolicy}
</span>
</div>
<div className="grid grid-cols-1 xl:grid-cols-3 gap-3">
{/* OPEN */}
<button
type="button"
disabled={!canManageAccess || isArchived || isSavingAccess}
onClick={() => setAdmissionPolicy('open')}
className={`p-3.5 rounded-xl text-left transition-all relative ${
admissionPolicy === 'open'
? 'bg-brand-cyan/10 border-2 border-brand-cyan text-white shadow-[0_0_12px_rgba(0,242,254,0.15)]'
: 'bg-surface-container-low/60 border border-brand-border/70 hover:border-brand-border text-slate-300 opacity-80 hover:opacity-100'
} ${!canManageAccess || isArchived ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`}
>
<div className="flex items-center justify-between mb-1.5">
<span className="font-headline text-xs font-bold text-white flex items-center gap-1.5">
<span className="material-symbols-outlined text-sm text-brand-cyan">
{admissionPolicy === 'open' ? 'radio_button_checked' : 'radio_button_unchecked'}
</span>
Open
</span>
{admissionPolicy === 'open' && (
<span className="font-mono text-[9px] bg-brand-cyan/20 text-brand-cyan px-1.5 py-0.5 rounded font-semibold">
Selected
</span>
)}
</div>
<p className="font-body text-[11px] text-slate-400 leading-snug">
Any registered rider may join immediately without review.
</p>
</button>
{/* REQUEST TO JOIN */}
<button
type="button"
disabled={!canManageAccess || isArchived || isSavingAccess}
onClick={() => setAdmissionPolicy('request')}
className={`p-3.5 rounded-xl text-left transition-all relative ${
admissionPolicy === 'request'
? 'bg-brand-cyan/10 border-2 border-brand-cyan text-white shadow-[0_0_12px_rgba(0,242,254,0.15)]'
: 'bg-surface-container-low/60 border border-brand-border/70 hover:border-brand-border text-slate-300 opacity-80 hover:opacity-100'
} ${!canManageAccess || isArchived ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`}
>
<div className="flex items-center justify-between mb-1.5">
<span className="font-headline text-xs font-bold text-white flex items-center gap-1.5">
<span className="material-symbols-outlined text-sm text-brand-cyan">
{admissionPolicy === 'request' ? 'radio_button_checked' : 'radio_button_unchecked'}
</span>
Request to Join
</span>
{admissionPolicy === 'request' && (
<span className="font-mono text-[9px] bg-brand-cyan/20 text-brand-cyan px-1.5 py-0.5 rounded font-semibold">
Selected
</span>
)}
</div>
<p className="font-body text-[11px] text-slate-400 leading-snug">
Riders submit join requests for Founder/Admin approval.
</p>
</button>
{/* INVITE ONLY */}
<button
type="button"
disabled={!canManageAccess || isArchived || isSavingAccess}
onClick={() => setAdmissionPolicy('inviteOnly')}
className={`p-3.5 rounded-xl text-left transition-all relative ${
admissionPolicy === 'inviteOnly'
? 'bg-brand-cyan/10 border-2 border-brand-cyan text-white shadow-[0_0_12px_rgba(0,242,254,0.15)]'
: 'bg-surface-container-low/60 border border-brand-border/70 hover:border-brand-border text-slate-300 opacity-80 hover:opacity-100'
} ${!canManageAccess || isArchived ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`}
>
<div className="flex items-center justify-between mb-1.5">
<span className="font-headline text-xs font-bold text-white flex items-center gap-1.5">
<span className="material-symbols-outlined text-sm text-brand-cyan">
{admissionPolicy === 'inviteOnly' ? 'radio_button_checked' : 'radio_button_unchecked'}
</span>
Invite Only
</span>
{admissionPolicy === 'inviteOnly' && (
<span className="font-mono text-[9px] bg-brand-cyan/20 text-brand-cyan px-1.5 py-0.5 rounded font-semibold">
Selected
</span>
)}
</div>
<p className="font-body text-[11px] text-slate-400 leading-snug">
Riders can join only via direct invitation from Club leadership.
</p>
</button>
</div>
<div className="font-mono text-[11px] text-slate-400 flex items-center gap-1.5 pt-1">
<span className="material-symbols-outlined text-xs text-amber-400">lock</span>
<span>Admission policy and visibility are independent dimensions.</span>
</div>
</div>
{/* Action Bar */}
<div className="pt-4 border-t border-brand-border/60 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="flex items-center gap-2 text-[11px] font-mono text-slate-400">
<span className="material-symbols-outlined text-brand-cyan text-sm">sync_saved_locally</span>
<span>Persists visibility and admission policy changes.</span>
</div>
<button
type="submit"
disabled={!canManageAccess || isArchived || !isAccessDirty || isSavingAccess}
className={`px-5 py-2.5 rounded-xl font-headline text-xs font-bold tracking-wider transition-all flex items-center justify-center gap-1.5 ${
canManageAccess && !isArchived && isAccessDirty && !isSavingAccess
? 'bg-brand-cyan hover:bg-cyan-300 text-obsidian-darkest cursor-pointer shadow-[0_0_15px_rgba(0,242,254,0.3)]'
: 'bg-surface-container-high text-slate-500 cursor-not-allowed opacity-60'
}`}
>
{isSavingAccess ? (
<>
<span className="material-symbols-outlined text-sm animate-spin">progress_activity</span>
<span>Saving Settings...</span>
</>
) : (
<>
<span className="material-symbols-outlined text-sm">check_circle</span>
<span>Save Access Settings</span>
</>
)}
</button>
</div>
</form>
{/* SECTION 2: CLUB IDENTIFIER & PUBLIC PROFILE */}
<div className="rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 space-y-4 shadow-lg relative overflow-hidden">
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-brand-cyan/60 via-brand-cyan/20 to-transparent" />
<div className="border-b border-brand-border/60 pb-3">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-brand-cyan text-lg">fingerprint</span>
<h2 className="font-headline text-sm sm:text-base font-semibold text-white">
Section 2: Club Identifier &amp; Public Profile
</h2>
</div>
<p className="font-body text-xs text-slate-400 mt-1">
Permanent club handle and navigation to public profile.
</p>
</div>
<div className="p-4 sm:p-5 rounded-xl bg-surface-container-low/60 border border-brand-border/60 space-y-3">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-sm text-brand-cyan">lock</span>
<span className="font-mono text-xs font-semibold text-white">
Permanent Club Handle
</span>
<span data-badge="IMMUTABLE RESERVATION" className="font-mono text-[10px] bg-brand-cyan/10 text-brand-cyan border border-brand-cyan/30 px-2 py-0.5 rounded-full font-bold">
Immutable Reservation
</span>
</div>
<div className="font-mono text-sm font-bold text-brand-cyan bg-surface-container px-3.5 py-1.5 rounded-xl border border-brand-border/80 inline-block tracking-wide">
@{club.slug}
</div>
</div>
<div>
<Link
href={`/clubs/${clubParam}/manage/profile${queryParam}`}
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-surface-container hover:bg-surface-container-high border border-brand-border/80 text-xs font-mono text-brand-cyan hover:text-white transition-colors cursor-pointer"
>
<span>Edit Club Profile</span>
<span className="material-symbols-outlined text-xs">arrow_forward</span>
</Link>
</div>
</div>
<div className="pt-2 border-t border-brand-border/60 text-[11px] font-mono text-slate-400 flex items-center justify-between">
<span>Club handles are permanently reserved and cannot be renamed or reassigned.</span>
</div>
</div>
</div>
{/* SECTION 3: CLUB OWNERSHIP & GOVERNANCE */}
<div className="rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 space-y-4 shadow-lg relative overflow-hidden">
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-amber-400/60 via-amber-400/20 to-transparent" />
<div className="border-b border-brand-border/60 pb-3">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-amber-400 text-lg">admin_panel_settings</span>
<h2 className="font-headline text-sm sm:text-base font-semibold text-white">
Section 3: Club Ownership &amp; Governance
</h2>
</div>
<p className="font-body text-xs text-slate-400 mt-1">
Root administrative authority holder and ownership assignment ledger.
</p>
</div>
<div className="p-4 sm:p-5 rounded-xl bg-surface-container-low/60 border border-brand-border/60 space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-amber-400/15 border border-amber-400/40 flex items-center justify-center font-mono font-bold text-sm text-amber-400">
{club.founderName
? club.founderName
.split(/\s+/)
.map((w) => w[0])
.join('')
.slice(0, 2)
.toUpperCase()
: 'FO'}
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-mono text-sm font-bold text-white">
{club.founderName || 'Current Owner'}
</span>
{club.founderHandle && (
<span className="font-mono text-xs text-slate-400">{club.founderHandle}</span>
)}
<span className="font-mono text-[10px] px-2.5 py-0.5 rounded-full bg-amber-400/15 border border-amber-400/40 text-amber-400 font-bold">
Owner
</span>
</div>
<p className="font-body text-xs text-slate-400 mt-0.5">
Primary administrative account holder since {club.foundedYear || '2024'}
</p>
</div>
</div>
<button
type="button"
disabled={!isOwner || isArchived}
onClick={() => setIsTransferModalOpen(true)}
className={`px-3.5 py-2 rounded-xl font-mono text-xs font-semibold transition-colors self-start sm:self-center ${
isOwner && !isArchived
? 'bg-surface-container hover:bg-surface-container-high border border-brand-border/80 text-white hover:border-brand-cyan/60 cursor-pointer'
: 'bg-surface-container text-slate-500 border border-brand-border/40 cursor-not-allowed opacity-60'
}`}
>
Transfer Ownership...
</button>
</div>
{/* Warning Notice */}
<div className="p-3.5 rounded-xl bg-amber-400/10 border border-amber-400/30 text-[11px] font-mono text-amber-200/90 flex items-start gap-2.5">
<span className="material-symbols-outlined text-sm text-amber-400 shrink-0 mt-0.5">warning</span>
<span>
<strong>Notice:</strong> The selected active member will become Club Owner. You will become a Club Admin. Ownership controls will transfer immediately after confirmation.
</span>
</div>
</div>
</div>
{/* SECTION 4: CLUB LIFECYCLE */}
<div className="rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 space-y-4 shadow-lg relative overflow-hidden">
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-rose-500/60 via-rose-500/20 to-transparent" />
<div className="border-b border-brand-border/60 pb-3">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-rose-400 text-lg">archive</span>
<h2 className="font-headline text-sm sm:text-base font-semibold text-white">
Section 4: Club Lifecycle
</h2>
</div>
<p className="font-body text-xs text-slate-400 mt-1">
Administrative lifecycle state.
</p>
</div>
<div className="p-4 sm:p-5 rounded-xl bg-surface-container-low/60 border border-brand-border/60 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="space-y-1 max-w-xl">
<div className="font-headline text-xs font-bold text-white tracking-wider">
Archive Club
</div>
<p className="font-body text-xs text-slate-400 leading-relaxed">
Archiving removes the Club from public discovery and prevents new open joins and join requests. Existing Club records are not deleted.
</p>
</div>
<button
type="button"
disabled={!isOwner || isArchived}
onClick={() => setIsArchiveModalOpen(true)}
className={`px-4 py-2 rounded-xl font-mono text-xs font-bold transition-colors whitespace-nowrap self-start sm:self-center ${
isOwner && !isArchived
? 'bg-surface-container hover:bg-rose-500/10 border border-rose-500/40 text-rose-400 cursor-pointer'
: 'bg-surface-container text-slate-500 border border-brand-border/40 cursor-not-allowed opacity-60'
}`}
>
{isArchived ? 'Club Archived' : 'Archive Club'}
</button>
</div>
<div className="p-3 rounded-xl bg-surface-container-low/60 border border-brand-border/60 text-[11px] font-mono text-slate-400 flex items-start gap-2.5">
<span className="material-symbols-outlined text-sm text-brand-cyan shrink-0 mt-0.5">verified_user</span>
<span>
<strong>Invariant Guarantee:</strong> Club deletion is not supported. Archiving preserves existing Club records while removing the Club from public discovery queries.
</span>
</div>
</div>
</div>
{/* Right Column (4 cols) - GOVERNANCE RAIL */}
<div className="lg:col-span-4 space-y-4">
<div className="flex items-center gap-2 text-xs font-mono text-slate-400 pb-1 border-b border-brand-border/60">
<span className="material-symbols-outlined text-sm text-brand-cyan">security</span>
<span className="tracking-wider font-semibold text-white">Governance &amp; Boundaries</span>
</div>
{/* Card 1: CLUB VS. PERSONAL SCOPE */}
<div className="p-5 rounded-2xl glass-card border border-brand-border/60 space-y-2.5 shadow-lg">
<div className="flex items-center gap-2 text-brand-cyan">
<span className="material-symbols-outlined text-base">domain</span>
<h3 className="font-headline font-bold text-xs tracking-wider text-white">
Club vs. Personal Scope
</h3>
</div>
<p className="font-body text-xs text-slate-400 leading-relaxed">
Club settings apply strictly to {club.name}. Club leadership cannot configure or override member telemetry preferences, GPS tracking, personal ride history, or individual account security.
</p>
<div className="pt-2 border-t border-brand-border/60 flex items-center justify-between text-[10px] font-mono text-slate-400">
<span>Least-Privilege RBAC</span>
<span className="text-emerald-400 font-semibold">Enforced</span>
</div>
</div>
{/* Card 2: LEADERSHIP GOVERNANCE */}
<div className="p-5 rounded-2xl glass-card border border-brand-border/60 space-y-2.5 shadow-lg">
<div className="flex items-center gap-2 text-amber-400">
<span className="material-symbols-outlined text-base">vpn_key</span>
<h3 className="font-headline font-bold text-xs tracking-wider text-white">
Leadership Governance
</h3>
</div>
<p className="font-body text-xs text-slate-400 leading-relaxed">
Club Owners and Admins may update visibility and admission policy. Only the Club Owner can transfer ownership or archive the Club. Admins cannot seize ownership or archive the Club.
</p>
<div className="pt-2 border-t border-brand-border/60 flex items-center justify-between text-[10px] font-mono text-slate-400">
<span>Owner Seat</span>
<span className="text-amber-400 font-mono">1 Owner</span>
</div>
</div>
{/* Card 3: CLUB HANDLE */}
<div className="p-5 rounded-2xl glass-card border border-brand-border/60 space-y-2.5 shadow-lg">
<div className="flex items-center gap-2 text-brand-cyan">
<span className="material-symbols-outlined text-base">link_off</span>
<h3 className="font-headline font-bold text-xs tracking-wider text-white">
Club Handle
</h3>
</div>
<p className="font-body text-xs text-slate-400 leading-relaxed">
Club handles are permanently reserved and cannot be renamed or reassigned.
</p>
<div className="pt-2 border-t border-brand-border/60 flex items-center justify-between text-[10px] font-mono text-slate-400">
<span>Canonical Slug</span>
<span className="text-brand-cyan font-mono">Permanent</span>
</div>
</div>
{/* Card 4: PRIVACY CONTRACT */}
<div className="p-5 rounded-2xl glass-card border border-brand-border/60 space-y-2.5 shadow-lg border-l-4 border-l-brand-cyan">
<div className="flex items-center gap-2 text-brand-cyan">
<span className="material-symbols-outlined text-base">shield</span>
<span data-section="PRIVACY CONTRACT" className="font-mono text-xs font-bold tracking-wider">Privacy Contract</span>
</div>
<blockquote className="font-body text-xs text-slate-200 border-l-2 border-brand-cyan pl-2.5 italic leading-relaxed">
&ldquo;Public Club Profile is organizational identity only. Member telemetry, GPS tracks, personal ride logs, and private account data are not included.&rdquo;
</blockquote>
<p className="font-mono text-[10px] text-slate-400">
Contract enforced across all 12 Founder Portal administrative modules.
</p>
</div>
</div>
</div>
{/* OWNERSHIP TRANSFER MODAL */}
{isTransferModalOpen && (
<div className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4">
<div className="rounded-2xl glass-card border border-brand-border/80 bg-surface-container-low/95 backdrop-blur-xl max-w-lg w-full p-6 space-y-5 shadow-2xl">
<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-amber-400 text-xl">swap_horiz</span>
<h3 className="font-headline text-base font-bold text-white">
Transfer Club Ownership
</h3>
</div>
<button
type="button"
disabled={isTransferring}
onClick={() => setIsTransferModalOpen(false)}
className="text-slate-400 hover:text-white p-1 rounded-lg"
>
<span className="material-symbols-outlined text-base">close</span>
</button>
</div>
<div className="space-y-3 font-body text-xs text-slate-300 leading-relaxed">
<div className="p-3.5 rounded-xl bg-amber-400/10 border border-amber-400/30 text-amber-200">
<strong>Irreversible Authority Transfer:</strong> Transferring ownership gives complete administrative control of <strong>{club.name}</strong> to the selected member. You will immediately step down to Club Admin.
</div>
{transferError && (
<div className="p-3 rounded-xl bg-error-container border border-error/30 text-error">
{transferError}
</div>
)}
<div className="space-y-1.5 pt-2">
<label className="font-mono text-[11px] text-slate-400 block font-medium">
Select Active Member
</label>
{loadingMembers ? (
<div className="p-3 text-center text-slate-400 font-mono text-xs flex items-center justify-center gap-2">
<span className="material-symbols-outlined text-base animate-spin text-brand-cyan">progress_activity</span>
<span>Loading eligible active members...</span>
</div>
) : eligibleMembers.length === 0 ? (
<div className="p-3 text-center text-slate-400 font-mono text-xs bg-surface-container-high rounded-xl border border-brand-border/60">
No eligible active members found in this Club to receive ownership.
</div>
) : (
<select
value={selectedTargetUid}
disabled={isTransferring}
onChange={(e) => setSelectedTargetUid(e.target.value)}
className="w-full bg-surface-container-high border border-brand-border/80 rounded-xl px-3.5 py-2.5 text-xs font-mono text-white focus:border-brand-cyan outline-none"
>
{eligibleMembers.map((m) => (
<option key={m.uid} value={m.uid}>
{m.displayName || m.handle || `Member (${m.uid.slice(0, 8)})`} — {m.displayRank || (m.role.charAt(0).toUpperCase() + m.role.slice(1))}
</option>
))}
</select>
)}
</div>
</div>
<div className="flex items-center justify-end gap-3 pt-3 border-t border-brand-border/60">
<button
type="button"
disabled={isTransferring}
onClick={() => setIsTransferModalOpen(false)}
className="px-4 py-2 rounded-xl bg-surface-container hover:bg-surface-container-high text-slate-300 hover:text-white font-mono text-xs font-semibold tracking-wider transition-colors"
>
Cancel
</button>
<button
type="button"
disabled={isTransferring || !selectedTargetUid || eligibleMembers.length === 0}
onClick={handleConfirmTransfer}
className={`px-4 py-2 rounded-xl font-mono text-xs font-bold tracking-wider flex items-center gap-1.5 transition-all ${
!isTransferring && selectedTargetUid && eligibleMembers.length > 0
? 'bg-amber-400 hover:bg-amber-300 text-black cursor-pointer shadow-[0_0_12px_rgba(245,158,11,0.3)]'
: 'bg-surface-container text-slate-500 cursor-not-allowed opacity-50'
}`}
>
{isTransferring ? (
<>
<span className="material-symbols-outlined text-sm animate-spin">progress_activity</span>
<span>Transferring...</span>
</>
) : (
<span>Confirm Transfer</span>
)}
</button>
</div>
</div>
</div>
)}
{/* ARCHIVE CONFIRMATION MODAL */}
{isArchiveModalOpen && (
<div className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4">
<div className="rounded-2xl glass-card border border-brand-border/80 bg-surface-container-low/95 backdrop-blur-xl max-w-lg w-full p-6 space-y-5 shadow-2xl">
<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-rose-400 text-xl">archive</span>
<h3 className="font-headline text-base font-bold text-white">
Archive Club
</h3>
</div>
<button
type="button"
disabled={isArchiving}
onClick={() => setIsArchiveModalOpen(false)}
className="text-slate-400 hover:text-white p-1 rounded-lg"
>
<span className="material-symbols-outlined text-base">close</span>
</button>
</div>
<div className="space-y-3 font-body text-xs text-slate-300 leading-relaxed">
<div className="p-3.5 rounded-xl bg-rose-500/10 border border-rose-500/30 text-rose-200">
Archiving removes the Club from discovery and prevents new membership requests. Existing Club records are retained.
</div>
<p className="font-body text-xs text-slate-400">
This action cannot be undone through the portal. Archiving is not deletion: historical ride records, club achievements, and membership history remain safely preserved.
</p>
{archiveError && (
<div className="p-3 rounded-xl bg-error-container border border-error/30 text-error">
{archiveError}
</div>
)}
</div>
<div className="flex items-center justify-end gap-3 pt-3 border-t border-brand-border/60">
<button
type="button"
disabled={isArchiving}
onClick={() => setIsArchiveModalOpen(false)}
className="px-4 py-2 rounded-xl bg-surface-container hover:bg-surface-container-high text-slate-300 hover:text-white font-mono text-xs font-semibold tracking-wider transition-colors"
>
Cancel
</button>
<button
type="button"
disabled={isArchiving}
onClick={handleConfirmArchive}
className="px-4 py-2 rounded-xl bg-rose-500 hover:bg-rose-400 text-white font-mono text-xs font-bold tracking-wider flex items-center gap-1.5 cursor-pointer shadow-[0_0_12px_rgba(244,63,94,0.3)] transition-all"
>
{isArchiving ? (
<>
<span className="material-symbols-outlined text-sm animate-spin">progress_activity</span>
<span>Archiving...</span>
</>
) : (
<span>Confirm Archive</span>
)}
</button>
</div>
</div>
</div>
)}
</div>
);
}