Files
rehearshalhub/web/src/components/InviteManagement.tsx
2026-04-08 15:10:52 +02:00

259 lines
6.6 KiB
TypeScript
Executable File

import React from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { listInvites, revokeInvite } from "../api/invites";
import { BandInviteListItem } from "../types/invite";
interface InviteManagementProps {
bandId: string;
}
/**
* Component for managing band invites
* - List pending invites
* - Revoke invites
* - Show invite status
*/
export function InviteManagement({ bandId }: InviteManagementProps) {
// Fetch invites
const { data, isLoading, isError, error } = useQuery({
queryKey: ["invites", bandId],
queryFn: () => listInvites(bandId),
retry: false,
});
// Revoke mutation
const revokeMutation = useMutation({
mutationFn: (inviteId: string) => revokeInvite(inviteId),
});
// Calculate pending invites
const pendingInvites = data?.invites.filter(
(invite) => !invite.is_used && invite.expires_at !== null
) || [];
// Format expiry date
const formatExpiry = (expiresAt: string | null | undefined) => {
if (!expiresAt) return "No expiry";
try {
const date = new Date(expiresAt);
const now = new Date();
const diffHours = Math.floor((date.getTime() - now.getTime()) / (1000 * 60 * 60));
if (diffHours <= 0) {
return "Expired";
} else if (diffHours < 24) {
return `Expires in ${diffHours} hour${diffHours === 1 ? "" : "s"}`;
} else {
return `Expires in ${Math.floor(diffHours / 24)} days`;
}
} catch {
return "Invalid date";
}
};
/**
* Copy invite token to clipboard
*/
const copyToClipboard = (token: string) => {
navigator.clipboard.writeText(window.location.origin + `/invite/${token}`);
// Could add a toast notification here
};
if (isLoading) {
return (
<div style={styles.container}>
<p>Loading invites...</p>
</div>
);
}
if (isError) {
return (
<div style={styles.container}>
<p style={styles.error}>Error loading invites: {error.message}</p>
</div>
);
}
return (
<div style={styles.container}>
<div style={styles.header}>
<h3 style={styles.title}>Pending Invites</h3>
<span style={styles.count}>{pendingInvites.length} Pending</span>
</div>
{data && data.total === 0 ? (
<p style={styles.empty}>No invites yet. Create one to share with others!</p>
) : (
<>
<div style={styles.list}>
{data?.invites.map((invite: BandInviteListItem) => (
<div key={invite.id} style={styles.inviteCard}>
<div style={styles.inviteHeader}>
<span style={styles.inviteRole}>{invite.role}</span>
<span style={styles.inviteStatus}>
{invite.is_used ? "Used" : formatExpiry(invite.expires_at)}
</span>
</div>
<div style={styles.inviteDetails}>
<div style={styles.tokenContainer}>
<span style={styles.token} title={invite.token}>
{invite.token.substring(0, 8)}...{invite.token.substring(invite.token.length - 4)}
</span>
<button
style={styles.copyButton}
onClick={() => copyToClipboard(invite.token)}
>
Copy Link
</button>
</div>
</div>
{!invite.is_used && invite.expires_at && new Date(invite.expires_at) > new Date() && (
<div style={styles.inviteActions}>
<button
style={{...styles.button, background: "#dc2626", color: "white"}}
onClick={() => {
if (window.confirm("Are you sure you want to revoke this invite?")) {
revokeMutation.mutate(invite.id);
}
}}
disabled={revokeMutation.isPending}
>
{revokeMutation.isPending ? "Revoking..." : "Revoke"}
</button>
</div>
)}
</div>
))}
</div>
<div style={styles.stats}>
<span style={styles.statItem}>
Total invites: <span style={styles.highlight}>{data?.total}</span>
</span>
<span style={styles.statItem}>
Pending: <span style={styles.highlight}>{pendingInvites.length}</span>
</span>
</div>
</>
)}
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
background: "white",
borderRadius: "8px",
padding: "20px",
marginBottom: "20px",
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
},
header: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "16px",
},
title: {
fontSize: "16px",
fontWeight: "bold" as const,
margin: "0",
},
count: {
color: "#6b7280",
fontSize: "14px",
},
empty: {
color: "#6b7280",
textAlign: "center" as const,
padding: "20px",
},
list: {
display: "flex",
flexDirection: "column" as const,
gap: "12px",
},
inviteCard: {
border: "1px solid #e5e7eb",
borderRadius: "6px",
padding: "16px",
background: "white",
},
inviteHeader: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "12px",
},
inviteRole: {
padding: "4px 12px",
borderRadius: "4px",
fontSize: "12px",
fontWeight: "500",
background: "#f3f4f6",
color: "#4b5563",
},
inviteStatus: {
fontSize: "12px",
},
inviteDetails: {
marginBottom: "12px",
},
tokenContainer: {
display: "flex",
gap: "8px",
alignItems: "center",
flexWrap: "wrap" as const,
},
token: {
fontFamily: "monospace",
fontSize: "12px",
color: "#6b7280",
whiteSpace: "nowrap" as const,
overflow: "hidden",
textOverflow: "ellipsis",
},
copyButton: {
padding: "4px 12px",
borderRadius: "4px",
fontSize: "12px",
background: "#3b82f6",
color: "white",
border: "none",
cursor: "pointer",
},
inviteActions: {
display: "flex",
gap: "8px",
},
button: {
padding: "8px 16px",
borderRadius: "4px",
fontSize: "12px",
border: "none",
cursor: "pointer",
fontWeight: "500",
},
stats: {
display: "flex",
gap: "16px",
fontSize: "14px",
},
statItem: {
color: "#6b7280",
},
highlight: {
color: "#ef4444",
fontWeight: "bold" as const,
},
error: {
color: "#dc2626",
},
};