All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m25s
480 lines
16 KiB
TypeScript
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'êtes membre d'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'adhésion
|
|
</h2>
|
|
<RequestList requests={pendingRequests} onUpdate={onUpdate} />
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|