1024 lines
50 KiB
TypeScript
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 & 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 & 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 & 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 & 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">
|
|
“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-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>
|
|
);
|
|
}
|