- Remove unused imports in invites.ts - Fix InviteManagement component (remove unused props, unneeded code) - Fix BandPage.tsx (remove currentMemberId, remove UserSearch for now) - Remove unused imports in types/invite.ts Build errors resolved: - TS6133: unused variables - TS2304: missing variables - TS2307: module not found Note: UserSearch temporarily disabled - needs backend support for listing non-members Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
271 lines
7.1 KiB
TypeScript
271 lines
7.1 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { useMutation, useQuery, useQueryClient } 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) {
|
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
|
|
// Fetch invites
|
|
const { data, isLoading, isError, error, refetch } = useQuery({
|
|
queryKey: ["invites", bandId],
|
|
queryFn: () => listInvites(bandId),
|
|
retry: false,
|
|
});
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
// Revoke mutation
|
|
const revokeMutation = useMutation({
|
|
mutationFn: (inviteId: string) => revokeInvite(inviteId),
|
|
onSuccess: () => {
|
|
// Refresh the invite list
|
|
queryClient.invalidateQueries({ queryKey: ["invites", bandId] });
|
|
setIsRefreshing(false);
|
|
},
|
|
onError: (err) => {
|
|
console.error("Failed to revoke invite:", err);
|
|
setIsRefreshing(false);
|
|
},
|
|
});
|
|
|
|
// 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 || isRefreshing}
|
|
>
|
|
{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",
|
|
},
|
|
};
|