Files
got-gaming/components/houses/HouseManagement.tsx

480 lines
16 KiB
TypeScript

"use client";
import { useState, useEffect, useTransition } from "react";
import { useSession } from "next-auth/react";
import Card from "@/components/ui/Card";
import Button from "@/components/ui/Button";
import HouseForm from "./HouseForm";
import RequestList from "./RequestList";
import Alert from "@/components/ui/Alert";
import { deleteHouse, leaveHouse, removeMember } from "@/actions/houses/update";
import { inviteUser } from "@/actions/houses/invitations";
interface House {
id: string;
name: string;
description: string | null;
creator: {
id: string;
username: string;
avatar: string | null;
};
memberships?: Array<{
id: string;
role: string;
user: {
id: string;
username: string;
avatar: string | null;
score?: number;
level?: number;
};
}>;
}
interface User {
id: string;
username: string;
avatar: string | null;
}
interface HouseManagementProps {
house: House | null;
users?: User[];
requests?: Array<{
id: string;
requester: {
id: string;
username: string;
avatar: string | null;
};
status: string;
createdAt: string;
}>;
onUpdate?: () => void;
}
interface Request {
id: string;
requester: {
id: string;
username: string;
avatar: string | null;
};
status: string;
createdAt: string;
}
export default function HouseManagement({
house,
users = [],
requests: initialRequests = [],
onUpdate,
}: HouseManagementProps) {
const { data: session } = useSession();
const [isEditing, setIsEditing] = useState(false);
const [showInviteForm, setShowInviteForm] = useState(false);
const [selectedUserId, setSelectedUserId] = useState("");
const [requests, setRequests] = useState<Request[]>(initialRequests);
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const userRole = house?.memberships?.find(
(m) => m.user.id === session?.user?.id
)?.role;
const isOwner = userRole === "OWNER";
const isAdmin = userRole === "ADMIN" || isOwner;
const pendingRequests = requests.filter((r) => r.status === "PENDING");
useEffect(() => {
const fetchRequests = async () => {
if (!house || !isAdmin) return;
try {
const response = await fetch(
`/api/houses/${house.id}/requests?status=PENDING`
);
if (response.ok) {
const data = await response.json();
setRequests(data);
}
} catch (error) {
console.error("Error fetching requests:", error);
}
};
fetchRequests();
}, [house, isAdmin]);
const handleDelete = () => {
if (
!house ||
!confirm("Êtes-vous sûr de vouloir supprimer cette maison ?")
) {
return;
}
setError(null);
startTransition(async () => {
const result = await deleteHouse(house.id);
if (result.success) {
// Rafraîchir le score dans le header (le créateur perd des points)
window.dispatchEvent(new Event("refreshUserScore"));
onUpdate?.();
} else {
setError(result.error || "Erreur lors de la suppression");
}
});
};
const handleLeave = () => {
if (!house || !confirm("Êtes-vous sûr de vouloir quitter cette maison ?")) {
return;
}
setError(null);
startTransition(async () => {
const result = await leaveHouse(house.id);
if (result.success) {
window.dispatchEvent(new Event("refreshUserScore"));
onUpdate?.();
} else {
setError(result.error || "Erreur lors de la sortie");
}
});
};
const handleInvite = () => {
if (!house || !selectedUserId) return;
setError(null);
setSuccess(null);
startTransition(async () => {
const result = await inviteUser(house.id, selectedUserId);
if (result.success) {
// Rafraîchir le badge d'invitations/demandes dans le header (pour l'invité)
window.dispatchEvent(new Event("refreshInvitations"));
setSuccess("Invitation envoyée");
setShowInviteForm(false);
setSelectedUserId("");
onUpdate?.();
} else {
setError(result.error || "Erreur lors de l'envoi de l'invitation");
}
});
};
const availableUsers = users.filter(
(u) =>
u.id !== session?.user?.id &&
!house?.memberships?.some((m) => m.user.id === u.id)
);
if (!house) {
return (
<Card className="p-6">
<h2
className="text-lg sm:text-xl font-bold mb-4"
style={{ color: "var(--foreground)" }}
>
Ma Maison
</h2>
<p
className="text-sm mb-4"
style={{ color: "var(--muted-foreground)" }}
>
Vous n&apos;êtes membre d&apos;aucune maison pour le moment.
</p>
</Card>
);
}
return (
<div className="space-y-6">
<Card
className="p-4 sm:p-6"
style={{
borderColor: `color-mix(in srgb, var(--accent) 40%, var(--border))`,
borderWidth: "2px",
boxShadow: `0 0 20px color-mix(in srgb, var(--accent) 10%, transparent)`,
}}
>
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-4 mb-4">
<div className="flex-1 min-w-0">
<h3
className="text-xl sm:text-2xl font-bold mb-2 break-words"
style={{
color: "var(--accent)",
textShadow: `0 0 10px color-mix(in srgb, var(--accent) 30%, transparent)`,
}}
>
{house.name}
</h3>
{house.description && (
<p
className="text-sm mt-2 break-words"
style={{ color: "var(--muted-foreground)" }}
>
{house.description}
</p>
)}
</div>
<div className="flex flex-wrap gap-2 sm:flex-nowrap">
{isAdmin && (
<>
<Button
onClick={() => setIsEditing(!isEditing)}
variant="secondary"
size="sm"
className="flex-1 sm:flex-none"
>
{isEditing ? "Annuler" : "Modifier"}
</Button>
{isOwner && (
<Button
onClick={handleDelete}
variant="danger"
size="sm"
className="flex-1 sm:flex-none"
>
Supprimer
</Button>
)}
</>
)}
{!isOwner && (
<Button
onClick={handleLeave}
variant="danger"
size="sm"
className="flex-1 sm:flex-none"
>
Quitter
</Button>
)}
</div>
</div>
{error && (
<Alert variant="error" className="mb-4">
{error}
</Alert>
)}
{success && (
<Alert variant="success" className="mb-4">
{success}
</Alert>
)}
{isEditing ? (
<HouseForm
house={house}
onSuccess={() => {
setIsEditing(false);
onUpdate?.();
}}
onCancel={() => setIsEditing(false)}
/>
) : (
<div>
<h4
className="text-sm font-semibold uppercase tracking-wider mb-3"
style={{
color: "var(--primary)",
borderBottom: `2px solid color-mix(in srgb, var(--primary) 30%, transparent)`,
paddingBottom: "0.5rem",
}}
>
Membres ({house.memberships?.length ?? 0})
</h4>
<div className="space-y-2">
{(house.memberships || []).map((membership) => {
const isCurrentUser = membership.user.id === session?.user?.id;
const roleColor =
membership.role === "OWNER"
? "var(--accent)"
: membership.role === "ADMIN"
? "var(--primary)"
: "var(--muted-foreground)";
return (
<div
key={membership.id}
className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 p-3 rounded"
style={{
backgroundColor: isCurrentUser
? "color-mix(in srgb, var(--primary) 10%, var(--card-hover))"
: "var(--card-hover)",
borderLeft: `3px solid ${roleColor}`,
borderColor: isCurrentUser
? "var(--primary)"
: "transparent",
}}
>
<div className="flex items-center gap-2 min-w-0 flex-1">
{membership.user.avatar && (
<img
src={membership.user.avatar}
alt={membership.user.username}
className="w-8 h-8 rounded-full flex-shrink-0 border-2"
style={{ borderColor: roleColor }}
/>
)}
<div className="min-w-0">
<span
className="font-semibold block sm:inline"
style={{
color: isCurrentUser
? "var(--primary)"
: "var(--foreground)",
}}
>
{membership.user.username}
{isCurrentUser && " (Vous)"}
</span>
<span
className="text-xs block sm:inline sm:ml-2"
style={{ color: "var(--muted-foreground)" }}
>
<span style={{ color: "var(--success)" }}>
{membership.user.score} pts
</span>
{" • "}
<span style={{ color: "var(--blue)" }}>
Niveau {membership.user.level}
</span>
</span>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<span
className="text-xs uppercase px-2 py-1 rounded font-bold"
style={{
color: roleColor,
backgroundColor: `color-mix(in srgb, ${roleColor} 15%, transparent)`,
border: `1px solid color-mix(in srgb, ${roleColor} 30%, transparent)`,
}}
>
{membership.role === "OWNER" && "👑 "}
{membership.role}
</span>
{isAdmin &&
!isCurrentUser &&
(isOwner || membership.role === "MEMBER") &&
membership.role !== "OWNER" && (
<Button
onClick={() => {
if (
confirm(
`Êtes-vous sûr de vouloir retirer ${membership.user.username} de la maison ?`
)
) {
startTransition(async () => {
const result = await removeMember(
house.id,
membership.user.id
);
if (result.success) {
// Rafraîchir le score dans le header (le membre retiré perd des points)
window.dispatchEvent(
new Event("refreshUserScore")
);
onUpdate?.();
} else {
setError(
result.error ||
"Erreur lors du retrait du membre"
);
}
});
}
}}
disabled={isPending}
variant="danger"
size="sm"
>
Retirer
</Button>
)}
</div>
</div>
);
})}
</div>
{isAdmin && (
<div className="mt-4">
{showInviteForm ? (
<div className="space-y-2">
<select
value={selectedUserId}
onChange={(e) => setSelectedUserId(e.target.value)}
className="w-full p-2 rounded border"
style={{
backgroundColor: "var(--input)",
borderColor: "var(--border)",
color: "var(--foreground)",
}}
>
<option value="">Sélectionner un utilisateur</option>
{availableUsers.map((user) => (
<option key={user.id} value={user.id}>
{user.username}
</option>
))}
</select>
<div className="flex gap-2">
<Button
onClick={handleInvite}
disabled={!selectedUserId || isPending}
variant="primary"
size="sm"
>
{isPending ? "Envoi..." : "Inviter"}
</Button>
<Button
onClick={() => {
setShowInviteForm(false);
setSelectedUserId("");
}}
variant="secondary"
size="sm"
>
Annuler
</Button>
</div>
</div>
) : (
<Button
onClick={() => setShowInviteForm(true)}
variant="primary"
size="sm"
>
Inviter un utilisateur
</Button>
)}
</div>
)}
</div>
)}
</Card>
{isAdmin && pendingRequests.length > 0 && (
<Card className="p-4 sm:p-6">
<h2
className="text-lg sm:text-xl font-bold mb-4"
style={{
color: "var(--purple)",
borderBottom: `2px solid color-mix(in srgb, var(--purple) 30%, transparent)`,
paddingBottom: "0.5rem",
}}
>
Demandes d&apos;adhésion
</h2>
<RequestList requests={pendingRequests} onUpdate={onUpdate} />
</Card>
)}
</div>
);
}