Add house leaderboard feature: Integrate house leaderboard functionality in LeaderboardPage and LeaderboardSection components. Update userStatsService to fetch house leaderboard data, and enhance UI to display house rankings, scores, and member details. Update Prisma schema to include house-related models and relationships, and seed database with initial house data.
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled
This commit is contained in:
167
components/houses/HouseCard.tsx
Normal file
167
components/houses/HouseCard.tsx
Normal file
@@ -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<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(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 (
|
||||
<Card className="p-4 sm:p-6">
|
||||
<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 font-bold mb-2 break-words" style={{ color: "var(--foreground)" }}>
|
||||
{house.name}
|
||||
</h3>
|
||||
{house.description && (
|
||||
<p className="text-sm mb-2 break-words" style={{ color: "var(--muted-foreground)" }}>
|
||||
{house.description}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-4 text-xs" style={{ color: "var(--muted-foreground)" }}>
|
||||
<span>Créée par {house.creator.username}</span>
|
||||
<span className="hidden sm:inline">•</span>
|
||||
<span>{memberCount} membre{memberCount > 1 ? "s" : ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="error" className="mb-4">
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<Alert variant="success" className="mb-4">
|
||||
{success}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{session?.user?.id && !isMember && (
|
||||
<Button
|
||||
onClick={handleRequestToJoin}
|
||||
disabled={isPending}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{isPending ? "Envoi..." : "Demander à rejoindre"}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isMember && (
|
||||
<div className="text-xs mb-4" style={{ color: "var(--success)" }}>
|
||||
✓ Vous êtes membre
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Members List */}
|
||||
{house.memberships && house.memberships.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t" style={{ borderColor: "var(--border)" }}>
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider mb-3" style={{ color: "var(--muted-foreground)" }}>
|
||||
Membres ({house.memberships.length})
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{house.memberships.map((membership) => (
|
||||
<div
|
||||
key={membership.id}
|
||||
className="flex items-center gap-2 p-2 rounded"
|
||||
style={{ backgroundColor: "var(--card-hover)" }}
|
||||
title={`${membership.user.username} (${membership.role})${membership.user.score !== undefined ? ` - ${membership.user.score} pts` : ""}`}
|
||||
>
|
||||
<Avatar
|
||||
src={membership.user.avatar}
|
||||
username={membership.user.username}
|
||||
size="sm"
|
||||
className="flex-shrink-0"
|
||||
borderClassName="border-pixel-gold/30"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs font-semibold truncate" style={{ color: "var(--foreground)" }}>
|
||||
{membership.user.username}
|
||||
</span>
|
||||
{membership.role === "OWNER" && (
|
||||
<span className="text-[10px] uppercase" style={{ color: "var(--accent)" }}>
|
||||
👑
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{membership.user.score !== undefined && membership.user.level !== undefined && (
|
||||
<div className="text-[10px]" style={{ color: "var(--muted-foreground)" }}>
|
||||
{membership.user.score} pts • Lv.{membership.user.level}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
90
components/houses/HouseForm.tsx
Normal file
90
components/houses/HouseForm.tsx
Normal file
@@ -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<string | null>(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 (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{error && <Alert variant="error">{error}</Alert>}
|
||||
|
||||
<Input
|
||||
label="Nom de la maison"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={50}
|
||||
disabled={isPending}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Description (optionnelle)"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
maxLength={500}
|
||||
disabled={isPending}
|
||||
rows={4}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Button type="submit" disabled={isPending} variant="primary" className="w-full sm:w-auto">
|
||||
{isPending ? "Enregistrement..." : house ? "Modifier" : "Créer"}
|
||||
</Button>
|
||||
{onCancel && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
disabled={isPending}
|
||||
variant="secondary"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
327
components/houses/HouseManagement.tsx
Normal file
327
components/houses/HouseManagement.tsx
Normal file
@@ -0,0 +1,327 @@
|
||||
"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 SectionTitle from "@/components/ui/SectionTitle";
|
||||
import HouseForm from "./HouseForm";
|
||||
import RequestList from "./RequestList";
|
||||
import Alert from "@/components/ui/Alert";
|
||||
import { deleteHouse, leaveHouse } 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?.id, 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) {
|
||||
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) {
|
||||
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) {
|
||||
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">
|
||||
<SectionTitle>Ma Maison</SectionTitle>
|
||||
<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">
|
||||
<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">
|
||||
<SectionTitle>{house.name}</SectionTitle>
|
||||
{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>
|
||||
<h3 className="font-bold mb-3" style={{ color: "var(--foreground)" }}>
|
||||
Membres ({house.memberships?.length ?? 0})
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{(house.memberships || []).map((membership) => (
|
||||
<div
|
||||
key={membership.id}
|
||||
className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 p-2 rounded"
|
||||
style={{ backgroundColor: "var(--card-hover)" }}
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<span className="font-semibold block sm:inline" style={{ color: "var(--foreground)" }}>
|
||||
{membership.user.username}
|
||||
</span>
|
||||
<span className="text-xs block sm:inline sm:ml-2" style={{ color: "var(--muted-foreground)" }}>
|
||||
({membership.user.score} pts - Niveau {membership.user.level})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs uppercase flex-shrink-0" style={{ color: "var(--accent)" }}>
|
||||
{membership.role}
|
||||
</span>
|
||||
</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">
|
||||
<SectionTitle>Demandes d'adhésion</SectionTitle>
|
||||
<RequestList requests={pendingRequests} onUpdate={onUpdate} />
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
236
components/houses/HousesSection.tsx
Normal file
236
components/houses/HousesSection.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import Card from "@/components/ui/Card";
|
||||
import Button from "@/components/ui/Button";
|
||||
import SectionTitle from "@/components/ui/SectionTitle";
|
||||
import BackgroundSection from "@/components/ui/BackgroundSection";
|
||||
import HouseCard from "./HouseCard";
|
||||
import HouseForm from "./HouseForm";
|
||||
import HouseManagement from "./HouseManagement";
|
||||
import InvitationList from "./InvitationList";
|
||||
import Input from "@/components/ui/Input";
|
||||
|
||||
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 User {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
}
|
||||
|
||||
interface HousesSectionProps {
|
||||
initialHouses?: House[];
|
||||
initialMyHouse?: House | null;
|
||||
initialUsers?: User[];
|
||||
initialInvitations?: Array<{
|
||||
id: string;
|
||||
house: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
inviter: {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
};
|
||||
status: string;
|
||||
createdAt: string;
|
||||
}>;
|
||||
backgroundImage: string;
|
||||
}
|
||||
|
||||
export default function HousesSection({
|
||||
initialHouses = [],
|
||||
initialMyHouse = null,
|
||||
initialUsers = [],
|
||||
initialInvitations = [],
|
||||
backgroundImage,
|
||||
}: HousesSectionProps) {
|
||||
const { data: session } = useSession();
|
||||
const [houses, setHouses] = useState<House[]>(initialHouses);
|
||||
const [myHouse, setMyHouse] = useState<House | null>(initialMyHouse);
|
||||
const [invitations, setInvitations] = useState(initialInvitations);
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
const fetchHouses = async () => {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (searchTerm) {
|
||||
params.append("search", searchTerm);
|
||||
}
|
||||
params.append("include", "members,creator");
|
||||
|
||||
const response = await fetch(`/api/houses?${params}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setHouses(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching houses:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMyHouse = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/houses/my-house");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setMyHouse(data);
|
||||
} else if (response.status === 404) {
|
||||
setMyHouse(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching my house:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchInvitations = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/invitations?status=PENDING");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setInvitations(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching invitations:", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (searchTerm) {
|
||||
const timeout = setTimeout(() => {
|
||||
fetchHouses();
|
||||
}, 300);
|
||||
return () => clearTimeout(timeout);
|
||||
} else {
|
||||
fetchHouses();
|
||||
}
|
||||
}, [searchTerm]);
|
||||
|
||||
const handleUpdate = () => {
|
||||
fetchMyHouse();
|
||||
fetchHouses();
|
||||
fetchInvitations();
|
||||
};
|
||||
|
||||
const filteredHouses = houses.filter((house) => {
|
||||
if (!myHouse) return true;
|
||||
return house.id !== myHouse.id;
|
||||
});
|
||||
|
||||
return (
|
||||
<BackgroundSection backgroundImage={backgroundImage}>
|
||||
{/* Title Section */}
|
||||
<SectionTitle
|
||||
variant="gradient"
|
||||
size="lg"
|
||||
subtitle="Rejoignez une maison ou créez la vôtre"
|
||||
className="mb-12 overflow-hidden"
|
||||
>
|
||||
MAISONS
|
||||
</SectionTitle>
|
||||
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
{session?.user && (
|
||||
<>
|
||||
{invitations.length > 0 && (
|
||||
<Card className="p-4 sm:p-6">
|
||||
<SectionTitle>Mes Invitations</SectionTitle>
|
||||
<InvitationList invitations={invitations} onUpdate={handleUpdate} />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card className="p-4 sm:p-6">
|
||||
<SectionTitle>Ma Maison</SectionTitle>
|
||||
{myHouse ? (
|
||||
<HouseManagement
|
||||
house={myHouse}
|
||||
users={initialUsers}
|
||||
onUpdate={handleUpdate}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
{showCreateForm ? (
|
||||
<HouseForm
|
||||
onSuccess={() => {
|
||||
setShowCreateForm(false);
|
||||
handleUpdate();
|
||||
}}
|
||||
onCancel={() => setShowCreateForm(false)}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<p className="text-sm mb-4 break-words" style={{ color: "var(--muted-foreground)" }}>
|
||||
Vous n'êtes membre d'aucune maison. Créez-en une ou demandez à rejoindre une maison existante.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => setShowCreateForm(true)}
|
||||
variant="primary"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Créer une maison
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Card className="p-4 sm:p-6">
|
||||
<SectionTitle>Toutes les Maisons</SectionTitle>
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
placeholder="Rechercher une maison..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{filteredHouses.length === 0 ? (
|
||||
<p className="text-sm" style={{ color: "var(--muted-foreground)" }}>
|
||||
Aucune maison trouvée
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{filteredHouses.map((house) => (
|
||||
<HouseCard
|
||||
key={house.id}
|
||||
house={house}
|
||||
onRequestSent={handleUpdate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</BackgroundSection>
|
||||
);
|
||||
}
|
||||
|
||||
123
components/houses/InvitationList.tsx
Normal file
123
components/houses/InvitationList.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import Card from "@/components/ui/Card";
|
||||
import Button from "@/components/ui/Button";
|
||||
import {
|
||||
acceptInvitation,
|
||||
rejectInvitation,
|
||||
} from "@/actions/houses/invitations";
|
||||
import Alert from "@/components/ui/Alert";
|
||||
|
||||
interface Invitation {
|
||||
id: string;
|
||||
house: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
inviter: {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
};
|
||||
status: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface InvitationListProps {
|
||||
invitations: Invitation[];
|
||||
onUpdate?: () => void;
|
||||
}
|
||||
|
||||
export default function InvitationList({
|
||||
invitations,
|
||||
onUpdate,
|
||||
}: InvitationListProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleAccept = (invitationId: string) => {
|
||||
setError(null);
|
||||
startTransition(async () => {
|
||||
const result = await acceptInvitation(invitationId);
|
||||
if (result.success) {
|
||||
onUpdate?.();
|
||||
} else {
|
||||
setError(result.error || "Erreur lors de l'acceptation");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleReject = (invitationId: string) => {
|
||||
setError(null);
|
||||
startTransition(async () => {
|
||||
const result = await rejectInvitation(invitationId);
|
||||
if (result.success) {
|
||||
onUpdate?.();
|
||||
} else {
|
||||
setError(result.error || "Erreur lors du refus");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (invitations.length === 0) {
|
||||
return (
|
||||
<p className="text-sm" style={{ color: "var(--muted-foreground)" }}>
|
||||
Aucune invitation en attente
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{error && <Alert variant="error">{error}</Alert>}
|
||||
{invitations.map((invitation) => (
|
||||
<Card key={invitation.id} className="p-4">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-bold mb-1 break-words" style={{ color: "var(--foreground)" }}>
|
||||
Invitation de {invitation.inviter.username}
|
||||
</h4>
|
||||
<p className="text-sm mb-2 break-words" style={{ color: "var(--muted-foreground)" }}>
|
||||
Pour rejoindre la maison <strong>{invitation.house.name}</strong>
|
||||
</p>
|
||||
</div>
|
||||
{invitation.status === "PENDING" && (
|
||||
<div className="flex gap-2 sm:flex-nowrap">
|
||||
<Button
|
||||
onClick={() => handleAccept(invitation.id)}
|
||||
disabled={isPending}
|
||||
variant="success"
|
||||
size="sm"
|
||||
className="flex-1 sm:flex-none"
|
||||
>
|
||||
Accepter
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleReject(invitation.id)}
|
||||
disabled={isPending}
|
||||
variant="danger"
|
||||
size="sm"
|
||||
className="flex-1 sm:flex-none"
|
||||
>
|
||||
Refuser
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{invitation.status === "ACCEPTED" && (
|
||||
<span className="text-xs flex-shrink-0" style={{ color: "var(--success)" }}>
|
||||
✓ Acceptée
|
||||
</span>
|
||||
)}
|
||||
{invitation.status === "REJECTED" && (
|
||||
<span className="text-xs flex-shrink-0" style={{ color: "var(--destructive)" }}>
|
||||
✗ Refusée
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
119
components/houses/RequestList.tsx
Normal file
119
components/houses/RequestList.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import Card from "@/components/ui/Card";
|
||||
import Button from "@/components/ui/Button";
|
||||
import {
|
||||
acceptRequest,
|
||||
rejectRequest,
|
||||
} from "@/actions/houses/requests";
|
||||
import Alert from "@/components/ui/Alert";
|
||||
|
||||
interface Request {
|
||||
id: string;
|
||||
requester: {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
};
|
||||
status: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface RequestListProps {
|
||||
requests: Request[];
|
||||
onUpdate?: () => void;
|
||||
}
|
||||
|
||||
export default function RequestList({
|
||||
requests,
|
||||
onUpdate,
|
||||
}: RequestListProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleAccept = (requestId: string) => {
|
||||
setError(null);
|
||||
startTransition(async () => {
|
||||
const result = await acceptRequest(requestId);
|
||||
if (result.success) {
|
||||
onUpdate?.();
|
||||
} else {
|
||||
setError(result.error || "Erreur lors de l'acceptation");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleReject = (requestId: string) => {
|
||||
setError(null);
|
||||
startTransition(async () => {
|
||||
const result = await rejectRequest(requestId);
|
||||
if (result.success) {
|
||||
onUpdate?.();
|
||||
} else {
|
||||
setError(result.error || "Erreur lors du refus");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (requests.length === 0) {
|
||||
return (
|
||||
<p className="text-sm" style={{ color: "var(--muted-foreground)" }}>
|
||||
Aucune demande en attente
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{error && <Alert variant="error">{error}</Alert>}
|
||||
{requests.map((request) => (
|
||||
<Card key={request.id} className="p-4">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-bold mb-1 break-words" style={{ color: "var(--foreground)" }}>
|
||||
{request.requester.username}
|
||||
</h4>
|
||||
<p className="text-sm break-words" style={{ color: "var(--muted-foreground)" }}>
|
||||
souhaite rejoindre votre maison
|
||||
</p>
|
||||
</div>
|
||||
{request.status === "PENDING" && (
|
||||
<div className="flex gap-2 sm:flex-nowrap">
|
||||
<Button
|
||||
onClick={() => handleAccept(request.id)}
|
||||
disabled={isPending}
|
||||
variant="success"
|
||||
size="sm"
|
||||
className="flex-1 sm:flex-none"
|
||||
>
|
||||
Accepter
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleReject(request.id)}
|
||||
disabled={isPending}
|
||||
variant="danger"
|
||||
size="sm"
|
||||
className="flex-1 sm:flex-none"
|
||||
>
|
||||
Refuser
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{request.status === "ACCEPTED" && (
|
||||
<span className="text-xs flex-shrink-0" style={{ color: "var(--success)" }}>
|
||||
✓ Acceptée
|
||||
</span>
|
||||
)}
|
||||
{request.status === "REJECTED" && (
|
||||
<span className="text-xs flex-shrink-0" style={{ color: "var(--destructive)" }}>
|
||||
✗ Refusée
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,8 +26,29 @@ interface LeaderboardEntry {
|
||||
characterClass?: CharacterClass | null;
|
||||
}
|
||||
|
||||
interface HouseMember {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
score: number;
|
||||
level: number;
|
||||
role: string;
|
||||
}
|
||||
|
||||
interface HouseLeaderboardEntry {
|
||||
rank: number;
|
||||
houseId: string;
|
||||
houseName: string;
|
||||
totalScore: number;
|
||||
memberCount: number;
|
||||
averageScore: number;
|
||||
description: string | null;
|
||||
members: HouseMember[];
|
||||
}
|
||||
|
||||
interface LeaderboardSectionProps {
|
||||
leaderboard: LeaderboardEntry[];
|
||||
houseLeaderboard: HouseLeaderboardEntry[];
|
||||
backgroundImage: string;
|
||||
}
|
||||
|
||||
@@ -38,11 +59,15 @@ const formatScore = (score: number): string => {
|
||||
|
||||
export default function LeaderboardSection({
|
||||
leaderboard,
|
||||
houseLeaderboard,
|
||||
backgroundImage,
|
||||
}: LeaderboardSectionProps) {
|
||||
const [selectedEntry, setSelectedEntry] = useState<LeaderboardEntry | null>(
|
||||
null
|
||||
);
|
||||
const [selectedHouse, setSelectedHouse] = useState<HouseLeaderboardEntry | null>(
|
||||
null
|
||||
);
|
||||
|
||||
return (
|
||||
<BackgroundSection backgroundImage={backgroundImage}>
|
||||
@@ -56,7 +81,7 @@ export default function LeaderboardSection({
|
||||
LEADERBOARD
|
||||
</SectionTitle>
|
||||
|
||||
{/* Leaderboard Table */}
|
||||
{/* Players Leaderboard Table */}
|
||||
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg backdrop-blur-sm overflow-x-auto">
|
||||
{/* Header */}
|
||||
<div className="bg-gray-900/80 border-b border-pixel-gold/30 grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 font-bold text-[10px] sm:text-xs uppercase tracking-widest text-gray-300">
|
||||
@@ -143,6 +168,90 @@ export default function LeaderboardSection({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* House Leaderboard Table */}
|
||||
<div className="mt-12">
|
||||
<SectionTitle
|
||||
variant="gradient"
|
||||
size="md"
|
||||
subtitle="Top Houses"
|
||||
className="mb-8 overflow-hidden"
|
||||
>
|
||||
MAISONS
|
||||
</SectionTitle>
|
||||
|
||||
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg backdrop-blur-sm overflow-x-auto">
|
||||
{/* Header */}
|
||||
<div className="bg-gray-900/80 border-b border-pixel-gold/30 grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 font-bold text-[10px] sm:text-xs uppercase tracking-widest text-gray-300">
|
||||
<div className="col-span-2 sm:col-span-1 text-center">Rank</div>
|
||||
<div className="col-span-5 sm:col-span-6">Maison</div>
|
||||
<div className="col-span-3 text-right">Score Total</div>
|
||||
<div className="col-span-2 text-right">Membres</div>
|
||||
</div>
|
||||
|
||||
{/* Entries */}
|
||||
<div className="divide-y divide-pixel-gold/10 overflow-visible">
|
||||
{houseLeaderboard.map((house) => (
|
||||
<div
|
||||
key={house.houseId}
|
||||
className={`grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 hover:bg-gray-900/50 transition relative cursor-pointer ${
|
||||
house.rank <= 3
|
||||
? "bg-gradient-to-r from-pixel-gold/10 via-pixel-gold/5 to-transparent"
|
||||
: "bg-black/40"
|
||||
}`}
|
||||
onClick={() => setSelectedHouse(house)}
|
||||
>
|
||||
{/* Rank */}
|
||||
<div className="col-span-2 sm:col-span-1 flex items-center justify-center">
|
||||
<span
|
||||
className={`inline-flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-full font-bold text-xs sm:text-sm ${
|
||||
house.rank === 1
|
||||
? "bg-gradient-to-br from-pixel-gold to-orange-500 text-black shadow-lg shadow-pixel-gold/50"
|
||||
: house.rank === 2
|
||||
? "bg-gradient-to-br from-gray-400 to-gray-500 text-black"
|
||||
: house.rank === 3
|
||||
? "bg-gradient-to-br from-orange-700 to-orange-800 text-white"
|
||||
: "bg-gray-900 text-gray-400 border border-gray-800"
|
||||
}`}
|
||||
>
|
||||
{house.rank}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* House Name */}
|
||||
<div className="col-span-5 sm:col-span-6 flex items-center gap-2 sm:gap-3 min-w-0">
|
||||
<div className="flex items-center gap-1 sm:gap-2 min-w-0">
|
||||
<span
|
||||
className={`font-bold text-xs sm:text-sm break-words ${
|
||||
house.rank <= 3 ? "text-pixel-gold" : "text-white"
|
||||
}`}
|
||||
>
|
||||
{house.houseName}
|
||||
</span>
|
||||
{house.rank <= 3 && (
|
||||
<span className="text-pixel-gold text-xs">✦</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total Score */}
|
||||
<div className="col-span-3 flex items-center justify-end">
|
||||
<span className="font-mono text-gray-300 text-xs sm:text-sm">
|
||||
{formatScore(house.totalScore)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Member Count */}
|
||||
<div className="col-span-2 flex items-center justify-end">
|
||||
<span className="font-bold text-gray-400 text-xs sm:text-sm">
|
||||
{house.memberCount}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Info */}
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-gray-500 text-sm">
|
||||
@@ -151,6 +260,112 @@ export default function LeaderboardSection({
|
||||
<p className="text-gray-600 text-xs mt-2">Rankings update every hour</p>
|
||||
</div>
|
||||
|
||||
{/* House Modal */}
|
||||
{selectedHouse && (
|
||||
<Modal
|
||||
isOpen={!!selectedHouse}
|
||||
onClose={() => setSelectedHouse(null)}
|
||||
size="md"
|
||||
>
|
||||
<div className="p-4 sm:p-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl sm:text-3xl font-bold text-pixel-gold uppercase tracking-wider break-words">
|
||||
{selectedHouse.houseName}
|
||||
</h2>
|
||||
<CloseButton onClick={() => setSelectedHouse(null)} size="md" />
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
<Card variant="default" className="p-4">
|
||||
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
|
||||
Rank
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-pixel-gold">
|
||||
#{selectedHouse.rank}
|
||||
</div>
|
||||
</Card>
|
||||
<Card variant="default" className="p-4">
|
||||
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
|
||||
Score Total
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-pixel-gold">
|
||||
{formatScore(selectedHouse.totalScore)}
|
||||
</div>
|
||||
</Card>
|
||||
<Card variant="default" className="p-4">
|
||||
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
|
||||
Membres
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-pixel-gold">
|
||||
{selectedHouse.memberCount}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Members List */}
|
||||
<div className="border-t border-pixel-gold/30 pt-6 mb-6">
|
||||
<div className="text-xs text-pixel-gold uppercase tracking-widest mb-4 font-bold">
|
||||
Membres ({selectedHouse.memberCount})
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{selectedHouse.members.map((member) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className="flex items-center justify-between p-3 rounded"
|
||||
style={{ backgroundColor: "var(--card-hover)" }}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar
|
||||
src={member.avatar}
|
||||
username={member.username}
|
||||
size="sm"
|
||||
className="flex-shrink-0"
|
||||
borderClassName="border-pixel-gold/30"
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-sm" style={{ color: "var(--foreground)" }}>
|
||||
{member.username}
|
||||
</span>
|
||||
<span className="text-xs uppercase" style={{ color: "var(--accent)" }}>
|
||||
{member.role}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: "var(--muted-foreground)" }}>
|
||||
Niveau {member.level}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-mono text-sm font-bold" style={{ color: "var(--foreground)" }}>
|
||||
{formatScore(member.score)}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: "var(--muted-foreground)" }}>
|
||||
points
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{selectedHouse.description && (
|
||||
<div className="border-t border-pixel-gold/30 pt-6">
|
||||
<div className="text-xs text-pixel-gold uppercase tracking-widest mb-3 font-bold">
|
||||
Description
|
||||
</div>
|
||||
<p className="text-gray-200 leading-relaxed whitespace-pre-wrap break-words">
|
||||
{selectedHouse.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* Character Modal */}
|
||||
{selectedEntry && (
|
||||
<Modal
|
||||
|
||||
@@ -118,7 +118,22 @@ export default function Navigation({
|
||||
LEADERBOARD
|
||||
</Link>
|
||||
{isAuthenticated && (
|
||||
<ChallengeBadge initialCount={initialActiveChallengesCount} />
|
||||
<>
|
||||
<Link
|
||||
href="/houses"
|
||||
className="transition text-xs font-normal uppercase tracking-widest"
|
||||
style={{ color: "var(--foreground)" }}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.color = "var(--accent-color)")
|
||||
}
|
||||
onMouseLeave={(e) =>
|
||||
(e.currentTarget.style.color = "var(--foreground)")
|
||||
}
|
||||
>
|
||||
MAISONS
|
||||
</Link>
|
||||
<ChallengeBadge initialCount={initialActiveChallengesCount} />
|
||||
</>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Link
|
||||
@@ -279,10 +294,26 @@ export default function Navigation({
|
||||
LEADERBOARD
|
||||
</Link>
|
||||
{isAuthenticated && (
|
||||
<ChallengeBadge
|
||||
initialCount={initialActiveChallengesCount}
|
||||
onNavigate={() => setIsMenuOpen(false)}
|
||||
/>
|
||||
<>
|
||||
<Link
|
||||
href="/houses"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
className="transition text-xs font-normal uppercase tracking-widest py-2"
|
||||
style={{ color: "var(--foreground)" }}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.color = "var(--accent-color)")
|
||||
}
|
||||
onMouseLeave={(e) =>
|
||||
(e.currentTarget.style.color = "var(--foreground)")
|
||||
}
|
||||
>
|
||||
MAISONS
|
||||
</Link>
|
||||
<ChallengeBadge
|
||||
initialCount={initialActiveChallengesCount}
|
||||
onNavigate={() => setIsMenuOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Link
|
||||
|
||||
Reference in New Issue
Block a user