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

1024 lines
50 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="space-y-6 pb-12">
{/* 2. HEADER */}
<section className="border-b border-outline-variant 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-secondary tracking-widest uppercase bg-surface-container px-2 py-0.5 rounded border border-secondary/30">
PRECISION, NOT AGGRESSION
</span>
<span className="font-mono text-xs text-outline tracking-wider uppercase">
// {club.homeRegion ? `${club.homeRegion.toUpperCase()} ZONE` : 'REGIONAL SQUADRON'}
</span>
</div>
<h1 className="font-headline font-bold text-2xl sm:text-3xl text-on-surface mt-2 uppercase tracking-tight">
SETTINGS
</h1>
<p className="font-body text-sm text-on-surface-variant 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="bg-surface-container-low border border-outline-variant p-3.5 rounded flex flex-col justify-between">
<div className="flex items-center justify-between">
<span className="font-mono text-[11px] uppercase text-outline">CLUB VISIBILITY</span>
<span className="material-symbols-outlined text-secondary text-sm">public</span>
</div>
<div className="mt-2">
<div className="font-headline text-sm font-semibold uppercase text-on-surface truncate">
{visibility}
</div>
<div className="flex items-center gap-1.5 mt-1 text-[11px] font-mono text-outline">
<span
className={`w-1.5 h-1.5 rounded-full ${
visibility === 'public'
? 'bg-emerald-400'
: visibility === 'unlisted'
? 'bg-amber-400'
: 'bg-outline'
}`}
/>
<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="bg-surface-container-low border border-outline-variant p-3.5 rounded flex flex-col justify-between">
<div className="flex items-center justify-between">
<span className="font-mono text-[11px] uppercase text-outline">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 uppercase text-on-surface 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-outline">
<span
className={`w-1.5 h-1.5 rounded-full ${
admissionPolicy === 'open'
? 'bg-emerald-400'
: admissionPolicy === 'request'
? 'bg-amber-400'
: 'bg-secondary'
}`}
/>
<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="bg-surface-container-low border border-outline-variant p-3.5 rounded flex flex-col justify-between">
<div className="flex items-center justify-between">
<span className="font-mono text-[11px] uppercase text-outline">CLUB HANDLE</span>
<span className="material-symbols-outlined text-secondary text-sm">lock</span>
</div>
<div className="mt-2">
<div className="font-mono text-sm font-semibold text-secondary truncate">
@{club.slug}
</div>
<div className="flex items-center gap-1.5 mt-1 text-[11px] font-mono text-outline">
<span className="w-1.5 h-1.5 rounded-full bg-secondary" />
<span>Immutable reservation</span>
</div>
</div>
</div>
{/* Card 4: Lifecycle State */}
<div className="bg-surface-container-low border border-outline-variant p-3.5 rounded flex flex-col justify-between">
<div className="flex items-center justify-between">
<span className="font-mono text-[11px] uppercase text-outline">LIFECYCLE STATE</span>
<span
className={`material-symbols-outlined text-sm ${
isArchived ? 'text-amber-400' : 'text-emerald-400'
}`}
>
{isArchived ? 'archive' : 'verified'}
</span>
</div>
<div className="mt-2">
<div
className={`font-headline text-sm font-semibold uppercase ${
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-outline">
<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 uppercase 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="bg-surface-container-low border border-outline-variant rounded p-5 space-y-6">
<div className="border-b border-outline-variant pb-3 flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-secondary text-lg">travel_explore</span>
<h2 className="font-headline text-sm uppercase font-semibold text-on-surface">
SECTION 1: CLUB ACCESS &amp; DISCOVERY
</h2>
</div>
<p className="font-body text-xs text-on-surface-variant mt-1">
Controls visibility across public index registries and member admission requirements.
</p>
</div>
<span className="font-mono text-[10px] text-secondary bg-secondary/10 border border-secondary/30 px-2 py-0.5 rounded uppercase">
{canManageAccess ? 'LEADERSHIP ACTION' : 'LEADERSHIP ONLY'}
</span>
</div>
{/* Save Feedback Alerts */}
{accessSaveStatus === 'success' && (
<div className="p-3.5 rounded bg-secondary/10 border border-secondary/30 flex items-center gap-2.5 text-secondary 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 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-outline uppercase font-medium">
Club Visibility
</label>
<span className="font-mono text-[10px] text-outline">
CURRENT: {club.visibility.toUpperCase()}
</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{/* PUBLIC */}
<button
type="button"
disabled={!canManageAccess || isArchived || isSavingAccess}
onClick={() => setVisibility('public')}
className={`p-3.5 rounded text-left transition-all relative ${
visibility === 'public'
? 'bg-surface-dim border-2 border-secondary shadow-[0_0_12px_rgba(52,145,255,0.15)]'
: 'bg-surface-container border border-outline-variant hover:border-outline opacity-75 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-on-surface flex items-center gap-1.5">
<span className="material-symbols-outlined text-sm text-secondary">
{visibility === 'public' ? 'radio_button_checked' : 'radio_button_unchecked'}
</span>
PUBLIC
</span>
{visibility === 'public' && (
<span className="font-mono text-[9px] bg-secondary/20 text-secondary px-1.5 py-0.5 rounded font-semibold">
SELECTED
</span>
)}
</div>
<p className="font-body text-[11px] text-on-surface-variant 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 text-left transition-all relative ${
visibility === 'unlisted'
? 'bg-surface-dim border-2 border-secondary shadow-[0_0_12px_rgba(52,145,255,0.15)]'
: 'bg-surface-container border border-outline-variant hover:border-outline opacity-75 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-on-surface flex items-center gap-1.5">
<span className="material-symbols-outlined text-sm text-secondary">
{visibility === 'unlisted' ? 'radio_button_checked' : 'radio_button_unchecked'}
</span>
UNLISTED
</span>
{visibility === 'unlisted' && (
<span className="font-mono text-[9px] bg-secondary/20 text-secondary px-1.5 py-0.5 rounded font-semibold">
SELECTED
</span>
)}
</div>
<p className="font-body text-[11px] text-on-surface-variant 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 text-left transition-all relative ${
visibility === 'private'
? 'bg-surface-dim border-2 border-secondary shadow-[0_0_12px_rgba(52,145,255,0.15)]'
: 'bg-surface-container border border-outline-variant hover:border-outline opacity-75 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-on-surface flex items-center gap-1.5">
<span className="material-symbols-outlined text-sm text-secondary">
{visibility === 'private' ? 'radio_button_checked' : 'radio_button_unchecked'}
</span>
PRIVATE
</span>
{visibility === 'private' && (
<span className="font-mono text-[9px] bg-secondary/20 text-secondary px-1.5 py-0.5 rounded font-semibold">
SELECTED
</span>
)}
</div>
<p className="font-body text-[11px] text-on-surface-variant leading-snug">
Visible only to active members, invited riders, and Club leadership.
</p>
</button>
</div>
<div className="font-mono text-[11px] text-outline flex items-center gap-1.5 pt-1">
<span className="material-symbols-outlined text-xs text-secondary">info</span>
<span>Unlisted and private clubs are hidden from public directory and unauthenticated discovery.</span>
</div>
</div>
<div className="h-px bg-outline-variant" />
{/* 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-outline uppercase font-medium">
Member Admission Policy
</label>
<span className="font-mono text-[10px] text-outline">
CURRENT: {club.admissionPolicy.toUpperCase()}
</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{/* OPEN */}
<button
type="button"
disabled={!canManageAccess || isArchived || isSavingAccess}
onClick={() => setAdmissionPolicy('open')}
className={`p-3.5 rounded text-left transition-all relative ${
admissionPolicy === 'open'
? 'bg-surface-dim border-2 border-secondary shadow-[0_0_12px_rgba(52,145,255,0.15)]'
: 'bg-surface-container border border-outline-variant hover:border-outline opacity-75 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-on-surface flex items-center gap-1.5">
<span className="material-symbols-outlined text-sm text-secondary">
{admissionPolicy === 'open' ? 'radio_button_checked' : 'radio_button_unchecked'}
</span>
OPEN
</span>
{admissionPolicy === 'open' && (
<span className="font-mono text-[9px] bg-secondary/20 text-secondary px-1.5 py-0.5 rounded font-semibold">
SELECTED
</span>
)}
</div>
<p className="font-body text-[11px] text-on-surface-variant 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 text-left transition-all relative ${
admissionPolicy === 'request'
? 'bg-surface-dim border-2 border-secondary shadow-[0_0_12px_rgba(52,145,255,0.15)]'
: 'bg-surface-container border border-outline-variant hover:border-outline opacity-75 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-on-surface flex items-center gap-1.5">
<span className="material-symbols-outlined text-sm text-secondary">
{admissionPolicy === 'request' ? 'radio_button_checked' : 'radio_button_unchecked'}
</span>
REQUEST TO JOIN
</span>
{admissionPolicy === 'request' && (
<span className="font-mono text-[9px] bg-secondary/20 text-secondary px-1.5 py-0.5 rounded font-semibold">
SELECTED
</span>
)}
</div>
<p className="font-body text-[11px] text-on-surface-variant 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 text-left transition-all relative ${
admissionPolicy === 'inviteOnly'
? 'bg-surface-dim border-2 border-secondary shadow-[0_0_12px_rgba(52,145,255,0.15)]'
: 'bg-surface-container border border-outline-variant hover:border-outline opacity-75 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-on-surface flex items-center gap-1.5">
<span className="material-symbols-outlined text-sm text-secondary">
{admissionPolicy === 'inviteOnly' ? 'radio_button_checked' : 'radio_button_unchecked'}
</span>
INVITE ONLY
</span>
{admissionPolicy === 'inviteOnly' && (
<span className="font-mono text-[9px] bg-secondary/20 text-secondary px-1.5 py-0.5 rounded font-semibold">
SELECTED
</span>
)}
</div>
<p className="font-body text-[11px] text-on-surface-variant leading-snug">
Riders can join only via direct invitation from Club leadership.
</p>
</button>
</div>
<div className="font-mono text-[11px] text-outline 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-outline-variant 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-outline">
<span className="material-symbols-outlined text-secondary 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-4 py-2 rounded font-headline text-xs font-semibold uppercase tracking-wider transition-all flex items-center justify-center gap-1.5 ${
canManageAccess && !isArchived && isAccessDirty && !isSavingAccess
? 'bg-primary-container hover:bg-primary text-on-primary cursor-pointer shadow-[0_0_12px_rgba(52,145,255,0.3)]'
: 'bg-surface-container-high text-outline 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="p-5 rounded-lg bg-surface-container-low border border-outline-variant space-y-4">
<div className="border-b border-outline-variant pb-3">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-secondary text-lg">fingerprint</span>
<h2 className="font-headline text-sm uppercase font-semibold text-on-surface">
SECTION 2: CLUB IDENTIFIER &amp; PUBLIC PROFILE
</h2>
</div>
<p className="font-body text-xs text-on-surface-variant mt-1">
Permanent club handle and navigation to public profile.
</p>
</div>
<div className="p-4 rounded bg-surface-dim border border-outline-variant 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-secondary">lock</span>
<span className="font-mono text-xs font-semibold text-on-surface uppercase">
Permanent Club Handle
</span>
<span className="font-mono text-[9px] bg-surface-container text-secondary border border-secondary/30 px-1.5 py-0.5 rounded font-bold uppercase">
IMMUTABLE RESERVATION
</span>
</div>
<div className="font-mono text-sm font-bold text-on-surface bg-surface-container px-3 py-1.5 rounded border border-outline-variant 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 py-1.5 rounded bg-surface-container hover:bg-surface-container-high border border-outline-variant text-xs font-mono text-secondary 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-outline-variant text-[11px] font-mono text-outline 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="p-5 rounded-lg bg-surface-container-low border border-outline-variant space-y-4">
<div className="border-b border-outline-variant 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 uppercase font-semibold text-on-surface">
SECTION 3: CLUB OWNERSHIP &amp; GOVERNANCE
</h2>
</div>
<p className="font-body text-xs text-on-surface-variant mt-1">
Root administrative authority holder and ownership assignment ledger.
</p>
</div>
<div className="p-4 rounded bg-surface-dim border border-outline-variant 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 bg-surface-container border border-amber-400/50 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-on-surface">
{club.founderName || 'Current Owner'}
</span>
{club.founderHandle && (
<span className="font-mono text-xs text-outline">{club.founderHandle}</span>
)}
<span className="font-mono text-[10px] px-2 py-0.5 rounded bg-amber-400/15 border border-amber-400/40 text-amber-400 font-bold uppercase">
OWNER
</span>
</div>
<p className="font-body text-xs text-outline 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 py-1.5 rounded font-mono text-xs transition-colors self-start sm:self-center ${
isOwner && !isArchived
? 'bg-surface-container hover:bg-surface-container-high border border-outline-variant text-on-surface hover:text-white cursor-pointer'
: 'bg-surface-container text-outline border border-outline-variant cursor-not-allowed opacity-60'
}`}
>
Transfer Ownership...
</button>
</div>
{/* Warning Notice */}
<div className="p-3 rounded bg-amber-400/5 border border-amber-400/20 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="p-5 rounded-lg bg-surface-container-low border border-outline-variant space-y-4">
<div className="border-b border-outline-variant 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 uppercase font-semibold text-on-surface">
SECTION 4: CLUB LIFECYCLE
</h2>
</div>
<p className="font-body text-xs text-on-surface-variant mt-1">
Administrative lifecycle state.
</p>
</div>
<div className="p-4 rounded bg-surface-dim border border-outline-variant 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-on-surface uppercase tracking-wider">
ARCHIVE CLUB
</div>
<p className="font-body text-xs text-outline 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 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-outline border border-outline-variant cursor-not-allowed opacity-60'
}`}
>
{isArchived ? 'Club Archived' : 'Archive Club'}
</button>
</div>
<div className="p-3 rounded bg-surface-dim border border-outline-variant text-[11px] font-mono text-outline flex items-start gap-2.5">
<span className="material-symbols-outlined text-sm text-secondary 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-outline pb-1 border-b border-outline-variant">
<span className="material-symbols-outlined text-sm text-secondary">security</span>
<span className="uppercase tracking-wider font-semibold text-on-surface">GOVERNANCE &amp; BOUNDARIES</span>
</div>
{/* Card 1: CLUB VS. PERSONAL SCOPE */}
<div className="p-4 rounded-lg bg-surface-container-low border border-outline-variant space-y-2">
<div className="flex items-center gap-2 text-secondary">
<span className="material-symbols-outlined text-base">domain</span>
<h3 className="font-headline font-bold text-xs uppercase tracking-wider text-on-surface">
CLUB VS. PERSONAL SCOPE
</h3>
</div>
<p className="font-body text-xs text-on-surface-variant 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-outline-variant flex items-center justify-between text-[10px] font-mono text-outline">
<span>LEAST-PRIVILEGE RBAC</span>
<span className="text-emerald-400 font-semibold">ENFORCED</span>
</div>
</div>
{/* Card 2: LEADERSHIP GOVERNANCE */}
<div className="p-4 rounded-lg bg-surface-container-low border border-outline-variant space-y-2">
<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 uppercase tracking-wider text-on-surface">
LEADERSHIP GOVERNANCE
</h3>
</div>
<p className="font-body text-xs text-on-surface-variant 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-outline-variant flex items-center justify-between text-[10px] font-mono text-outline">
<span>OWNER SEAT</span>
<span className="text-amber-400 font-mono">1 OWNER</span>
</div>
</div>
{/* Card 3: CLUB HANDLE */}
<div className="p-4 rounded-lg bg-surface-container-low border border-outline-variant space-y-2">
<div className="flex items-center gap-2 text-secondary">
<span className="material-symbols-outlined text-base">link_off</span>
<h3 className="font-headline font-bold text-xs uppercase tracking-wider text-on-surface">
CLUB HANDLE
</h3>
</div>
<p className="font-body text-xs text-on-surface-variant leading-relaxed">
Club handles are permanently reserved and cannot be renamed or reassigned.
</p>
<div className="pt-2 border-t border-outline-variant flex items-center justify-between text-[10px] font-mono text-outline">
<span>CANONICAL SLUG</span>
<span className="text-secondary">PERMANENT</span>
</div>
</div>
{/* Card 4: PRIVACY CONTRACT */}
<div className="p-4 rounded-lg bg-surface-dim border border-secondary/20 space-y-2">
<div className="flex items-center gap-2 text-secondary">
<span className="material-symbols-outlined text-base">shield</span>
<span className="font-mono text-xs font-bold uppercase tracking-wider">PRIVACY CONTRACT</span>
</div>
<blockquote className="font-body text-xs text-on-surface border-l-2 border-secondary 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-outline">
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="bg-surface-container-low border border-outline-variant rounded-lg max-w-lg w-full p-6 space-y-5 shadow-2xl">
<div className="flex items-center justify-between border-b border-outline-variant 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-on-surface uppercase">
Transfer Club Ownership
</h3>
</div>
<button
type="button"
disabled={isTransferring}
onClick={() => setIsTransferModalOpen(false)}
className="text-outline hover:text-on-surface p-1 rounded"
>
<span className="material-symbols-outlined text-base">close</span>
</button>
</div>
<div className="space-y-3 font-body text-xs text-on-surface-variant leading-relaxed">
<div className="p-3.5 rounded 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 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-outline uppercase block font-medium">
Select Active Member
</label>
{loadingMembers ? (
<div className="p-3 text-center text-outline font-mono text-xs flex items-center justify-center gap-2">
<span className="material-symbols-outlined text-base animate-spin">progress_activity</span>
<span>Loading eligible active members...</span>
</div>
) : eligibleMembers.length === 0 ? (
<div className="p-3 text-center text-outline font-mono text-xs bg-surface-dim rounded border border-outline-variant">
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-dim border border-outline-variant rounded px-3 py-2 text-xs font-mono text-on-surface focus:border-secondary 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.toUpperCase()}
</option>
))}
</select>
)}
</div>
</div>
<div className="flex items-center justify-end gap-3 pt-3 border-t border-outline-variant">
<button
type="button"
disabled={isTransferring}
onClick={() => setIsTransferModalOpen(false)}
className="px-4 py-2 rounded bg-surface-container hover:bg-surface-container-high text-on-surface font-mono text-xs font-semibold uppercase tracking-wider"
>
Cancel
</button>
<button
type="button"
disabled={isTransferring || !selectedTargetUid || eligibleMembers.length === 0}
onClick={handleConfirmTransfer}
className={`px-4 py-2 rounded font-mono text-xs font-bold uppercase tracking-wider flex items-center gap-1.5 ${
!isTransferring && selectedTargetUid && eligibleMembers.length > 0
? 'bg-amber-400 hover:bg-amber-300 text-black cursor-pointer'
: 'bg-surface-container text-outline 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="bg-surface-container-low border border-outline-variant rounded-lg max-w-lg w-full p-6 space-y-5 shadow-2xl">
<div className="flex items-center justify-between border-b border-outline-variant 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-on-surface uppercase">
Archive Club
</h3>
</div>
<button
type="button"
disabled={isArchiving}
onClick={() => setIsArchiveModalOpen(false)}
className="text-outline hover:text-on-surface p-1 rounded"
>
<span className="material-symbols-outlined text-base">close</span>
</button>
</div>
<div className="space-y-3 font-body text-xs text-on-surface-variant leading-relaxed">
<div className="p-3.5 rounded 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-outline">
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 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-outline-variant">
<button
type="button"
disabled={isArchiving}
onClick={() => setIsArchiveModalOpen(false)}
className="px-4 py-2 rounded bg-surface-container hover:bg-surface-container-high text-on-surface font-mono text-xs font-semibold uppercase tracking-wider"
>
Cancel
</button>
<button
type="button"
disabled={isArchiving}
onClick={handleConfirmArchive}
className="px-4 py-2 rounded bg-rose-500 hover:bg-rose-400 text-white font-mono text-xs font-bold uppercase tracking-wider flex items-center gap-1.5 cursor-pointer"
>
{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>
);
}