diff --git a/actions/houses/create.ts b/actions/houses/create.ts new file mode 100644 index 0000000..10d0bfd --- /dev/null +++ b/actions/houses/create.ts @@ -0,0 +1,48 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { auth } from "@/lib/auth"; +import { houseService } from "@/services/houses/house.service"; +import { + ValidationError, + ConflictError, +} from "@/services/errors"; + +export async function createHouse(data: { + name: string; + description?: string | null; +}) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return { + success: false, + error: "Vous devez être connecté pour créer une maison", + }; + } + + const house = await houseService.createHouse({ + name: data.name, + description: data.description, + creatorId: session.user.id, + }); + + revalidatePath("/houses"); + revalidatePath("/profile"); + + return { success: true, message: "Maison créée avec succès", data: house }; + } catch (error) { + console.error("Create house error:", error); + + if (error instanceof ValidationError || error instanceof ConflictError) { + return { success: false, error: error.message }; + } + + return { + success: false, + error: "Une erreur est survenue lors de la création de la maison", + }; + } +} + diff --git a/actions/houses/invitations.ts b/actions/houses/invitations.ts new file mode 100644 index 0000000..e38bdb0 --- /dev/null +++ b/actions/houses/invitations.ts @@ -0,0 +1,173 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { auth } from "@/lib/auth"; +import { houseService } from "@/services/houses/house.service"; +import { + ValidationError, + ConflictError, + ForbiddenError, + NotFoundError, +} from "@/services/errors"; + +export async function inviteUser(houseId: string, inviteeId: string) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return { + success: false, + error: "Vous devez être connecté", + }; + } + + const invitation = await houseService.inviteUser({ + houseId, + inviterId: session.user.id, + inviteeId, + }); + + revalidatePath("/houses"); + revalidatePath(`/houses/${houseId}`); + + return { + success: true, + message: "Invitation envoyée", + data: invitation, + }; + } catch (error) { + console.error("Invite user error:", error); + + if ( + error instanceof ValidationError || + error instanceof ConflictError || + error instanceof ForbiddenError + ) { + return { success: false, error: error.message }; + } + + return { + success: false, + error: "Une erreur est survenue lors de l'envoi de l'invitation", + }; + } +} + +export async function acceptInvitation(invitationId: string) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return { + success: false, + error: "Vous devez être connecté", + }; + } + + const membership = await houseService.acceptInvitation( + invitationId, + session.user.id + ); + + revalidatePath("/houses"); + revalidatePath("/profile"); + revalidatePath("/invitations"); + + return { + success: true, + message: "Invitation acceptée", + data: membership, + }; + } catch (error) { + console.error("Accept invitation error:", error); + + if ( + error instanceof ValidationError || + error instanceof ConflictError || + error instanceof ForbiddenError || + error instanceof NotFoundError + ) { + return { success: false, error: error.message }; + } + + return { + success: false, + error: "Une erreur est survenue lors de l'acceptation de l'invitation", + }; + } +} + +export async function rejectInvitation(invitationId: string) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return { + success: false, + error: "Vous devez être connecté", + }; + } + + await houseService.rejectInvitation(invitationId, session.user.id); + + revalidatePath("/houses"); + revalidatePath("/invitations"); + + return { success: true, message: "Invitation refusée" }; + } catch (error) { + console.error("Reject invitation error:", error); + + if ( + error instanceof ConflictError || + error instanceof ForbiddenError || + error instanceof NotFoundError + ) { + return { success: false, error: error.message }; + } + + return { + success: false, + error: "Une erreur est survenue lors du refus de l'invitation", + }; + } +} + +export async function cancelInvitation(invitationId: string) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return { + success: false, + error: "Vous devez être connecté", + }; + } + + // Récupérer l'invitation pour obtenir le houseId avant de l'annuler + const invitation = await houseService.getInvitationById(invitationId); + + await houseService.cancelInvitation(invitationId, session.user.id); + + revalidatePath("/houses"); + if (invitation?.houseId) { + revalidatePath(`/houses/${invitation.houseId}`); + } + + return { success: true, message: "Invitation annulée" }; + } catch (error) { + console.error("Cancel invitation error:", error); + + if ( + error instanceof ConflictError || + error instanceof ForbiddenError || + error instanceof NotFoundError + ) { + return { success: false, error: error.message }; + } + + return { + success: false, + error: "Une erreur est survenue lors de l'annulation de l'invitation", + }; + } +} diff --git a/actions/houses/requests.ts b/actions/houses/requests.ts new file mode 100644 index 0000000..ff4eb1f --- /dev/null +++ b/actions/houses/requests.ts @@ -0,0 +1,163 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { auth } from "@/lib/auth"; +import { houseService } from "@/services/houses/house.service"; +import { + ValidationError, + ConflictError, + ForbiddenError, + NotFoundError, +} from "@/services/errors"; + +export async function requestToJoin(houseId: string) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return { + success: false, + error: "Vous devez être connecté", + }; + } + + const request = await houseService.requestToJoin({ + houseId, + requesterId: session.user.id, + }); + + revalidatePath("/houses"); + revalidatePath(`/houses/${houseId}`); + + return { + success: true, + message: "Demande envoyée", + data: request, + }; + } catch (error) { + console.error("Request to join error:", error); + + if ( + error instanceof ValidationError || + error instanceof ConflictError + ) { + return { success: false, error: error.message }; + } + + return { + success: false, + error: "Une erreur est survenue lors de l'envoi de la demande", + }; + } +} + +export async function acceptRequest(requestId: string) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return { + success: false, + error: "Vous devez être connecté", + }; + } + + const membership = await houseService.acceptRequest( + requestId, + session.user.id + ); + + revalidatePath("/houses"); + revalidatePath("/profile"); + + return { + success: true, + message: "Demande acceptée", + data: membership, + }; + } catch (error) { + console.error("Accept request error:", error); + + if ( + error instanceof ConflictError || + error instanceof ForbiddenError || + error instanceof NotFoundError + ) { + return { success: false, error: error.message }; + } + + return { + success: false, + error: "Une erreur est survenue lors de l'acceptation de la demande", + }; + } +} + +export async function rejectRequest(requestId: string) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return { + success: false, + error: "Vous devez être connecté", + }; + } + + await houseService.rejectRequest(requestId, session.user.id); + + revalidatePath("/houses"); + + return { success: true, message: "Demande refusée" }; + } catch (error) { + console.error("Reject request error:", error); + + if ( + error instanceof ConflictError || + error instanceof ForbiddenError || + error instanceof NotFoundError + ) { + return { success: false, error: error.message }; + } + + return { + success: false, + error: "Une erreur est survenue lors du refus de la demande", + }; + } +} + +export async function cancelRequest(requestId: string) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return { + success: false, + error: "Vous devez être connecté", + }; + } + + await houseService.cancelRequest(requestId, session.user.id); + + revalidatePath("/houses"); + + return { success: true, message: "Demande annulée" }; + } catch (error) { + console.error("Cancel request error:", error); + + if ( + error instanceof ConflictError || + error instanceof ForbiddenError || + error instanceof NotFoundError + ) { + return { success: false, error: error.message }; + } + + return { + success: false, + error: "Une erreur est survenue lors de l'annulation de la demande", + }; + } +} + diff --git a/actions/houses/update.ts b/actions/houses/update.ts new file mode 100644 index 0000000..f1d3f27 --- /dev/null +++ b/actions/houses/update.ts @@ -0,0 +1,114 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { auth } from "@/lib/auth"; +import { houseService } from "@/services/houses/house.service"; +import { + ValidationError, + ConflictError, + ForbiddenError, +} from "@/services/errors"; + +export async function updateHouse( + houseId: string, + data: { + name?: string; + description?: string | null; + } +) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return { + success: false, + error: "Vous devez être connecté", + }; + } + + const house = await houseService.updateHouse(houseId, session.user.id, data); + + revalidatePath("/houses"); + revalidatePath(`/houses/${houseId}`); + + return { success: true, message: "Maison mise à jour", data: house }; + } catch (error) { + console.error("Update house error:", error); + + if ( + error instanceof ValidationError || + error instanceof ConflictError || + error instanceof ForbiddenError + ) { + return { success: false, error: error.message }; + } + + return { + success: false, + error: "Une erreur est survenue lors de la mise à jour de la maison", + }; + } +} + +export async function deleteHouse(houseId: string) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return { + success: false, + error: "Vous devez être connecté", + }; + } + + await houseService.deleteHouse(houseId, session.user.id); + + revalidatePath("/houses"); + revalidatePath("/profile"); + + return { success: true, message: "Maison supprimée" }; + } catch (error) { + console.error("Delete house error:", error); + + if (error instanceof ForbiddenError) { + return { success: false, error: error.message }; + } + + return { + success: false, + error: "Une erreur est survenue lors de la suppression de la maison", + }; + } +} + +export async function leaveHouse(houseId: string) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return { + success: false, + error: "Vous devez être connecté", + }; + } + + await houseService.leaveHouse(houseId, session.user.id); + + revalidatePath("/houses"); + revalidatePath("/profile"); + + return { success: true, message: "Vous avez quitté la maison" }; + } catch (error) { + console.error("Leave house error:", error); + + if (error instanceof ForbiddenError) { + return { success: false, error: error.message }; + } + + return { + success: false, + error: "Une erreur est survenue lors de la sortie de la maison", + }; + } +} + diff --git a/app/api/houses/[houseId]/invitations/route.ts b/app/api/houses/[houseId]/invitations/route.ts new file mode 100644 index 0000000..cfd3a71 --- /dev/null +++ b/app/api/houses/[houseId]/invitations/route.ts @@ -0,0 +1,51 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { houseService } from "@/services/houses/house.service"; + +export async function GET( + request: Request, + { params }: { params: Promise<{ houseId: string }> } +) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json( + { error: "Vous devez être connecté" }, + { status: 401 } + ); + } + + const { houseId } = await params; + + // Vérifier que l'utilisateur est membre de la maison + const isMember = await houseService.isUserMemberOfHouse( + session.user.id, + houseId + ); + + if (!isMember) { + return NextResponse.json( + { error: "Vous devez être membre de cette maison" }, + { status: 403 } + ); + } + + const { searchParams } = new URL(request.url); + const status = searchParams.get("status") as "PENDING" | "ACCEPTED" | "REJECTED" | "CANCELLED" | null; + + const invitations = await houseService.getHouseInvitations( + houseId, + status || undefined + ); + + return NextResponse.json(invitations); + } catch (error) { + console.error("Error fetching house invitations:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération des invitations" }, + { status: 500 } + ); + } +} + diff --git a/app/api/houses/[houseId]/requests/route.ts b/app/api/houses/[houseId]/requests/route.ts new file mode 100644 index 0000000..859cb4e --- /dev/null +++ b/app/api/houses/[houseId]/requests/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { houseService } from "@/services/houses/house.service"; + +export async function GET( + request: Request, + { params }: { params: Promise<{ houseId: string }> } +) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json( + { error: "Vous devez être connecté" }, + { status: 401 } + ); + } + + const { houseId } = await params; + + // Vérifier que l'utilisateur est propriétaire ou admin + const isAuthorized = await houseService.isUserOwnerOrAdmin( + session.user.id, + houseId + ); + + if (!isAuthorized) { + return NextResponse.json( + { error: "Vous n'avez pas les permissions pour voir les demandes" }, + { status: 403 } + ); + } + + const { searchParams } = new URL(request.url); + const status = searchParams.get("status") as "PENDING" | "ACCEPTED" | "REJECTED" | "CANCELLED" | null; + + const requests = await houseService.getHouseRequests(houseId, status || undefined); + + return NextResponse.json(requests); + } catch (error) { + console.error("Error fetching house requests:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération des demandes" }, + { status: 500 } + ); + } +} + diff --git a/app/api/houses/[houseId]/route.ts b/app/api/houses/[houseId]/route.ts new file mode 100644 index 0000000..a1458ca --- /dev/null +++ b/app/api/houses/[houseId]/route.ts @@ -0,0 +1,59 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { houseService } from "@/services/houses/house.service"; + +export async function GET( + request: Request, + { params }: { params: Promise<{ houseId: string }> } +) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json( + { error: "Vous devez être connecté" }, + { status: 401 } + ); + } + + const { houseId } = await params; + const house = await houseService.getHouseById(houseId, { + memberships: { + include: { + user: { + select: { + id: true, + username: true, + avatar: true, + score: true, + level: true, + }, + }, + }, + }, + creator: { + select: { + id: true, + username: true, + avatar: true, + }, + }, + }); + + if (!house) { + return NextResponse.json( + { error: "Maison non trouvée" }, + { status: 404 } + ); + } + + return NextResponse.json(house); + } catch (error) { + console.error("Error fetching house:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération de la maison" }, + { status: 500 } + ); + } +} + diff --git a/app/api/houses/my-house/route.ts b/app/api/houses/my-house/route.ts new file mode 100644 index 0000000..533695e --- /dev/null +++ b/app/api/houses/my-house/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { houseService } from "@/services/houses/house.service"; + +export async function GET() { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json( + { error: "Vous devez être connecté" }, + { status: 401 } + ); + } + + const house = await houseService.getUserHouse(session.user.id, { + memberships: { + include: { + user: { + select: { + id: true, + username: true, + avatar: true, + score: true, + level: true, + }, + }, + }, + }, + creator: { + select: { + id: true, + username: true, + avatar: true, + }, + }, + }); + + return NextResponse.json(house); + } catch (error) { + console.error("Error fetching user house:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération de votre maison" }, + { status: 500 } + ); + } +} + diff --git a/app/api/houses/route.ts b/app/api/houses/route.ts new file mode 100644 index 0000000..ca762e4 --- /dev/null +++ b/app/api/houses/route.ts @@ -0,0 +1,87 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { houseService } from "@/services/houses/house.service"; + +export async function GET(request: Request) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json( + { error: "Vous devez être connecté" }, + { status: 401 } + ); + } + + const { searchParams } = new URL(request.url); + const search = searchParams.get("search"); + const include = searchParams.get("include")?.split(",") || []; + + const includeOptions: { + memberships?: { + include: { + user: { + select: { + id: boolean; + username: boolean; + avatar: boolean; + score?: boolean; + level?: boolean; + }; + }; + }; + }; + creator?: { + select: { + id: boolean; + username: boolean; + avatar: boolean; + }; + }; + } = {}; + if (include.includes("members")) { + includeOptions.memberships = { + include: { + user: { + select: { + id: true, + username: true, + avatar: true, + score: true, + level: true, + }, + }, + }, + }; + } + if (include.includes("creator")) { + includeOptions.creator = { + select: { + id: true, + username: true, + avatar: true, + }, + }; + } + + let houses; + if (search) { + houses = await houseService.searchHouses(search, { + include: includeOptions, + }); + } else { + houses = await houseService.getAllHouses({ + include: includeOptions, + }); + } + + return NextResponse.json(houses); + } catch (error) { + console.error("Error fetching houses:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération des maisons" }, + { status: 500 } + ); + } +} + diff --git a/app/api/invitations/route.ts b/app/api/invitations/route.ts new file mode 100644 index 0000000..48958fb --- /dev/null +++ b/app/api/invitations/route.ts @@ -0,0 +1,36 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { houseService } from "@/services/houses/house.service"; + +export async function GET(request: Request) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json( + { error: "Vous devez être connecté" }, + { status: 401 } + ); + } + + const { searchParams } = new URL(request.url); + const statusParam = searchParams.get("status"); + const status = statusParam && ["PENDING", "ACCEPTED", "REJECTED", "CANCELLED"].includes(statusParam) + ? (statusParam as "PENDING" | "ACCEPTED" | "REJECTED" | "CANCELLED") + : undefined; + + const invitations = await houseService.getUserInvitations( + session.user.id, + status + ); + + return NextResponse.json(invitations); + } catch (error) { + console.error("Error fetching invitations:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération des invitations" }, + { status: 500 } + ); + } +} + diff --git a/app/api/leaderboard/houses/route.ts b/app/api/leaderboard/houses/route.ts new file mode 100644 index 0000000..71b1115 --- /dev/null +++ b/app/api/leaderboard/houses/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from "next/server"; +import { userStatsService } from "@/services/users/user-stats.service"; + +export async function GET() { + try { + const leaderboard = await userStatsService.getHouseLeaderboard(10); + + return NextResponse.json(leaderboard); + } catch (error) { + console.error("Error fetching house leaderboard:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération du leaderboard des maisons" }, + { status: 500 } + ); + } +} + diff --git a/app/houses/page.tsx b/app/houses/page.tsx new file mode 100644 index 0000000..bb70d27 --- /dev/null +++ b/app/houses/page.tsx @@ -0,0 +1,146 @@ +import { redirect } from "next/navigation"; +import { auth } from "@/lib/auth"; +import { getBackgroundImage } from "@/lib/preferences"; +import NavigationWrapper from "@/components/navigation/NavigationWrapper"; +import HousesSection from "@/components/houses/HousesSection"; +import { houseService } from "@/services/houses/house.service"; +import { userService } from "@/services/users/user.service"; + +export const dynamic = "force-dynamic"; + +export default async function HousesPage() { + const session = await auth(); + + if (!session?.user?.id) { + redirect("/login"); + } + + const [housesData, myHouseData, invitationsData, users, backgroundImage] = await Promise.all([ + // Récupérer les maisons + houseService.getAllHouses({ + include: { + memberships: { + include: { + user: { + select: { + id: true, + username: true, + avatar: true, + score: true, + level: true, + }, + }, + }, + orderBy: [ + { role: "asc" }, // OWNER, ADMIN, MEMBER + { user: { score: "desc" } }, // Puis par score décroissant + ], + }, + creator: { + select: { + id: true, + username: true, + avatar: true, + }, + }, + }, + }), + // Récupérer la maison de l'utilisateur + houseService.getUserHouse(session.user.id, { + memberships: { + include: { + user: { + select: { + id: true, + username: true, + avatar: true, + score: true, + level: true, + }, + }, + }, + }, + creator: { + select: { + id: true, + username: true, + avatar: true, + }, + }, + }), + // Récupérer les invitations de l'utilisateur + houseService.getUserInvitations(session.user.id, "PENDING"), + // Récupérer tous les utilisateurs pour les invitations + userService.getAllUsers({ + select: { + id: true, + username: true, + avatar: true, + }, + }), + getBackgroundImage("challenges", "/got-2.jpg"), + ]); + + // Sérialiser les données pour le client + const houses = (housesData as any[]).map((house: any) => ({ + id: house.id, + name: house.name, + description: house.description, + creator: house.creator || { id: house.creatorId, username: "Unknown", avatar: null }, + memberships: (house.memberships || []).map((m: any) => ({ + id: m.id, + role: m.role, + user: { + id: m.user.id, + username: m.user.username, + avatar: m.user.avatar, + score: m.user.score ?? 0, + level: m.user.level ?? 1, + }, + })), + })); + + const myHouse = myHouseData + ? { + id: myHouseData.id, + name: myHouseData.name, + description: myHouseData.description, + creator: (myHouseData as any).creator || { id: (myHouseData as any).creatorId, username: "Unknown", avatar: null }, + memberships: ((myHouseData as any).memberships || []).map((m: any) => ({ + id: m.id, + role: m.role, + user: { + id: m.user.id, + username: m.user.username, + avatar: m.user.avatar, + score: m.user.score ?? 0, + level: m.user.level ?? 1, + }, + })), + } + : null; + + const invitations = invitationsData.map((inv: any) => ({ + id: inv.id, + house: { + id: inv.house.id, + name: inv.house.name, + }, + inviter: inv.inviter, + status: inv.status, + createdAt: inv.createdAt.toISOString(), + })); + + return ( +
+ + +
+ ); +} diff --git a/app/leaderboard/page.tsx b/app/leaderboard/page.tsx index 49bb417..4b649c2 100644 --- a/app/leaderboard/page.tsx +++ b/app/leaderboard/page.tsx @@ -7,8 +7,9 @@ export const dynamic = "force-dynamic"; export default async function LeaderboardPage() { // Paralléliser les appels DB - const [leaderboard, backgroundImage] = await Promise.all([ + const [leaderboard, houseLeaderboard, backgroundImage] = await Promise.all([ userStatsService.getLeaderboard(10), + userStatsService.getHouseLeaderboard(10), getBackgroundImage("leaderboard", "/leaderboard-bg.jpg"), ]); @@ -17,6 +18,7 @@ export default async function LeaderboardPage() { diff --git a/components/houses/HouseCard.tsx b/components/houses/HouseCard.tsx new file mode 100644 index 0000000..48d8cb3 --- /dev/null +++ b/components/houses/HouseCard.tsx @@ -0,0 +1,167 @@ +"use client"; + +import { useState } from "react"; +import { useSession } from "next-auth/react"; +import Card from "@/components/ui/Card"; +import Button from "@/components/ui/Button"; +import Avatar from "@/components/ui/Avatar"; +import { requestToJoin } from "@/actions/houses/requests"; +import { useTransition } from "react"; +import Alert from "@/components/ui/Alert"; + +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; + }; + }>; + _count?: { + memberships: number; + }; +} + +interface HouseCardProps { + house: House; + onRequestSent?: () => void; +} + +export default function HouseCard({ house, onRequestSent }: HouseCardProps) { + const { data: session } = useSession(); + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + + const isMember = house.memberships?.some( + (m) => m.user.id === session?.user?.id + ); + const memberCount = house._count?.memberships || house.memberships?.length || 0; + + const handleRequestToJoin = () => { + if (!session?.user?.id) return; + + setError(null); + setSuccess(null); + + startTransition(async () => { + const result = await requestToJoin(house.id); + + if (result.success) { + setSuccess("Demande envoyée avec succès"); + onRequestSent?.(); + } else { + setError(result.error || "Erreur lors de l'envoi de la demande"); + } + }); + }; + + return ( + +
+
+

+ {house.name} +

+ {house.description && ( +

+ {house.description} +

+ )} +
+ Créée par {house.creator.username} + + {memberCount} membre{memberCount > 1 ? "s" : ""} +
+
+
+ + {error && ( + + {error} + + )} + + {success && ( + + {success} + + )} + + {session?.user?.id && !isMember && ( + + )} + + {isMember && ( +
+ ✓ Vous êtes membre +
+ )} + + {/* Members List */} + {house.memberships && house.memberships.length > 0 && ( +
+

+ Membres ({house.memberships.length}) +

+
+ {house.memberships.map((membership) => ( +
+ +
+
+ + {membership.user.username} + + {membership.role === "OWNER" && ( + + 👑 + + )} +
+ {membership.user.score !== undefined && membership.user.level !== undefined && ( +
+ {membership.user.score} pts • Lv.{membership.user.level} +
+ )} +
+
+ ))} +
+
+ )} +
+ ); +} + diff --git a/components/houses/HouseForm.tsx b/components/houses/HouseForm.tsx new file mode 100644 index 0000000..785780e --- /dev/null +++ b/components/houses/HouseForm.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { useState, useTransition } from "react"; +import Button from "@/components/ui/Button"; +import Input from "@/components/ui/Input"; +import Textarea from "@/components/ui/Textarea"; +import Alert from "@/components/ui/Alert"; +import { createHouse } from "@/actions/houses/create"; +import { updateHouse } from "@/actions/houses/update"; + +interface HouseFormProps { + house?: { + id: string; + name: string; + description: string | null; + }; + onSuccess?: () => void; + onCancel?: () => void; +} + +export default function HouseForm({ + house, + onSuccess, + onCancel, +}: HouseFormProps) { + const [name, setName] = useState(house?.name || ""); + const [description, setDescription] = useState(house?.description || ""); + const [error, setError] = useState(null); + const [isPending, startTransition] = useTransition(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + startTransition(async () => { + const result = house + ? await updateHouse(house.id, { name, description: description || null }) + : await createHouse({ name, description: description || null }); + + if (result.success) { + onSuccess?.(); + } else { + setError(result.error || "Une erreur est survenue"); + } + }); + }; + + return ( +
+ {error && {error}} + + setName(e.target.value)} + required + minLength={3} + maxLength={50} + disabled={isPending} + /> + +