feat : 2 team per row
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useState, useTransition, type ReactNode } from "react";
|
import { useEffect, useMemo, useState, useTransition, type ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
GripVerticalIcon,
|
GripVerticalIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
SortableContext,
|
SortableContext,
|
||||||
sortableKeyboardCoordinates,
|
sortableKeyboardCoordinates,
|
||||||
useSortable,
|
useSortable,
|
||||||
verticalListSortingStrategy,
|
|
||||||
} from "@dnd-kit/sortable";
|
} from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -747,14 +746,14 @@ function SortableTeam({
|
|||||||
transform,
|
transform,
|
||||||
transition,
|
transition,
|
||||||
isDragging,
|
isDragging,
|
||||||
} = useSortable({ id });
|
} = useSortable({ id, animateLayoutChanges: () => false });
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={{ transform: CSS.Transform.toString(transform), transition }}
|
style={{ transform: CSS.Transform.toString(transform), transition }}
|
||||||
className={cn(isDragging && "relative z-10 opacity-60")}
|
className={cn("min-w-0", isDragging && "relative z-10 opacity-60")}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2 rounded-lg border bg-muted/20 p-2">
|
<div className="flex h-full flex-col gap-2 rounded-lg border bg-muted/20 p-2">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="text-sm font-medium">Team {position + 1}</span>
|
<span className="text-sm font-medium">Team {position + 1}</span>
|
||||||
<Button
|
<Button
|
||||||
@@ -799,10 +798,10 @@ function BossRecommendationEditor({
|
|||||||
const groups = recommendations.filter(
|
const groups = recommendations.filter(
|
||||||
(group) => group.level === level && group.bossPosition === boss.position,
|
(group) => group.level === level && group.bossPosition === boss.position,
|
||||||
);
|
);
|
||||||
const [teams, setTeams] = useState<StygianRecommendationItemInput[][]>(() =>
|
const [teams, setTeams] = useState<{ id: string; items: StygianRecommendationItemInput[] }[]>(() =>
|
||||||
groups
|
groups
|
||||||
.filter((group) => group.kind === "team")
|
.filter((group) => group.kind === "team")
|
||||||
.map((group) => group.items.flatMap((item) => toInput(item) ?? [])),
|
.map((group) => ({ id: group.id, items: group.items.flatMap((item) => toInput(item) ?? []) })),
|
||||||
);
|
);
|
||||||
const [units, setUnits] = useState<StygianRecommendationItemInput[]>(() =>
|
const [units, setUnits] = useState<StygianRecommendationItemInput[]>(() =>
|
||||||
groups
|
groups
|
||||||
@@ -815,13 +814,13 @@ function BossRecommendationEditor({
|
|||||||
coordinateGetter: sortableKeyboardCoordinates,
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const teamIds = teams.map((_, index) => `team-${index}`);
|
const teamIds = useMemo(() => teams.map((team) => team.id), [teams]);
|
||||||
const fingerprint = JSON.stringify({ teams, units });
|
const fingerprint = JSON.stringify({ teams: teams.map((team) => team.items), units });
|
||||||
const valid = teams.every(
|
const valid = teams.every(
|
||||||
(team) =>
|
(team) =>
|
||||||
team.length >= 1 &&
|
team.items.length >= 1 &&
|
||||||
team.length <= 4 &&
|
team.items.length <= 4 &&
|
||||||
team.every((item) => item.kind === "character"),
|
team.items.every((item) => item.kind === "character"),
|
||||||
);
|
);
|
||||||
const [savedFingerprint, setSavedFingerprint] = useState(fingerprint);
|
const [savedFingerprint, setSavedFingerprint] = useState(fingerprint);
|
||||||
const [failedFingerprint, setFailedFingerprint] = useState<string | null>(
|
const [failedFingerprint, setFailedFingerprint] = useState<string | null>(
|
||||||
@@ -896,37 +895,36 @@ function BossRecommendationEditor({
|
|||||||
collisionDetection={closestCenter}
|
collisionDetection={closestCenter}
|
||||||
onDragEnd={({ active, over }) => {
|
onDragEnd={({ active, over }) => {
|
||||||
if (!over || active.id === over.id) return;
|
if (!over || active.id === over.id) return;
|
||||||
const from = teamIds.indexOf(String(active.id));
|
setTeams((current) => {
|
||||||
const to = teamIds.indexOf(String(over.id));
|
const from = current.findIndex((team) => team.id === active.id);
|
||||||
if (from >= 0 && to >= 0)
|
const to = current.findIndex((team) => team.id === over.id);
|
||||||
setTeams((current) => arrayMove(current, from, to));
|
return from === -1 || to === -1 ? current : arrayMove(current, from, to);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={teamIds}
|
items={teamIds}
|
||||||
strategy={verticalListSortingStrategy}
|
strategy={rectSortingStrategy}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="grid grid-cols-1 gap-3 xl:grid-cols-2">
|
||||||
{teams.map((team, teamIndex) => (
|
{teams.map((team, teamIndex) => (
|
||||||
<SortableTeam
|
<SortableTeam
|
||||||
key={teamIds[teamIndex]}
|
key={team.id}
|
||||||
id={teamIds[teamIndex]}
|
id={team.id}
|
||||||
position={teamIndex}
|
position={teamIndex}
|
||||||
>
|
>
|
||||||
<VisualRecommendationGrid
|
<VisualRecommendationGrid
|
||||||
items={team}
|
items={team.items}
|
||||||
limit={4}
|
limit={4}
|
||||||
teamLayout
|
teamLayout
|
||||||
characters={characters}
|
characters={characters}
|
||||||
weapons={weapons}
|
weapons={weapons}
|
||||||
artifacts={artifacts}
|
artifacts={artifacts}
|
||||||
onChange={(items) =>
|
onChange={(items) =>
|
||||||
setTeams(
|
setTeams((current) =>
|
||||||
teams
|
current
|
||||||
.map((entry, index) =>
|
.map((entry) => entry.id === team.id ? { ...entry, items } : entry)
|
||||||
index === teamIndex ? items : entry,
|
.filter((entry) => entry.items.length),
|
||||||
)
|
|
||||||
.filter((entry) => entry.length),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -944,9 +942,7 @@ function BossRecommendationEditor({
|
|||||||
description="This team and all of its character recommendations will be removed."
|
description="This team and all of its character recommendations will be removed."
|
||||||
confirmLabel="Remove team"
|
confirmLabel="Remove team"
|
||||||
onConfirm={() =>
|
onConfirm={() =>
|
||||||
setTeams(
|
setTeams((current) => current.filter((entry) => entry.id !== team.id))
|
||||||
teams.filter((_, index) => index !== teamIndex),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Trash2Icon data-icon="inline-start" />
|
<Trash2Icon data-icon="inline-start" />
|
||||||
@@ -961,7 +957,7 @@ function BossRecommendationEditor({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="self-start"
|
className="self-start"
|
||||||
onClick={() => setTeams([...teams, []])}
|
onClick={() => setTeams((current) => [...current, { id: crypto.randomUUID(), items: [] }])}
|
||||||
>
|
>
|
||||||
<PlusIcon data-icon="inline-start" />
|
<PlusIcon data-icon="inline-start" />
|
||||||
Add team
|
Add team
|
||||||
|
|||||||
@@ -45,12 +45,12 @@ function RecommendationIcon({ item, catalog, teamLayout = false }: { item: Item;
|
|||||||
if (item.kind === "character") {
|
if (item.kind === "character") {
|
||||||
const hasEquipment = Boolean(value.weapon || value.artifact);
|
const hasEquipment = Boolean(value.weapon || value.artifact);
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex min-w-0 flex-col gap-1 rounded-lg border bg-card p-1", teamLayout && "w-full sm:p-2", teamLayout && hasEquipment && "sm:w-[164px]", teamLayout && !hasEquipment && "sm:w-[116px]", teamLayout && !hasEquipment && item.constellationLevel === null && "aspect-square justify-center")}>
|
<div className={cn("flex min-w-0 flex-col gap-1 rounded-lg border bg-card p-1", teamLayout && "w-full sm:p-2 xl:p-1", teamLayout && hasEquipment && "sm:w-[164px] xl:w-full", teamLayout && !hasEquipment && "sm:w-[116px] xl:w-full", teamLayout && !hasEquipment && item.constellationLevel === null && "aspect-square justify-center")}>
|
||||||
<div className={teamLayout && hasEquipment ? "flex flex-col gap-1 sm:grid sm:grid-cols-[100px_40px] sm:gap-2" : "flex flex-col gap-1"}>
|
<div className={teamLayout && hasEquipment ? "flex flex-col gap-1 sm:grid sm:grid-cols-[100px_40px] sm:gap-2 xl:grid-cols-1 xl:gap-1" : "flex flex-col gap-1"}>
|
||||||
<div className={cn("relative aspect-square overflow-hidden rounded-lg", teamLayout && "sm:size-[100px]", teamLayout && hasEquipment && "sm:row-span-2", characterRarityGradientClass(item.catalogKey, value.rarity))}>
|
<div className={cn("relative aspect-square overflow-hidden rounded-lg", teamLayout && "sm:size-[100px] xl:size-auto", teamLayout && hasEquipment && "sm:row-span-2 xl:row-auto", characterRarityGradientClass(item.catalogKey, value.rarity))}>
|
||||||
{value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes={teamLayout ? "(min-width: 640px) 128px, 25vw" : "128px"} className="object-contain" /> : null}
|
{value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes={teamLayout ? "(min-width: 640px) 128px, 25vw" : "128px"} className="object-contain" /> : null}
|
||||||
</div>
|
</div>
|
||||||
{hasEquipment ? <div className={teamLayout ? "grid grid-cols-2 gap-1 sm:row-span-2 sm:grid-cols-1 sm:gap-2 sm:self-center" : "grid grid-cols-2 gap-1"}>
|
{hasEquipment ? <div className={teamLayout ? "grid grid-cols-2 gap-1 sm:row-span-2 sm:grid-cols-1 sm:gap-2 sm:self-center xl:row-auto xl:grid-cols-2 xl:gap-1 xl:self-auto" : "grid grid-cols-2 gap-1"}>
|
||||||
{value.weapon ? <div className={cn("relative aspect-square overflow-hidden rounded-md border", rarityGradientClass(value.weapon.rarity))}><Image src={value.weapon.imageUrl} alt={value.weapon.name} fill sizes="64px" className="object-contain p-1" /></div> : null}
|
{value.weapon ? <div className={cn("relative aspect-square overflow-hidden rounded-md border", rarityGradientClass(value.weapon.rarity))}><Image src={value.weapon.imageUrl} alt={value.weapon.name} fill sizes="64px" className="object-contain p-1" /></div> : null}
|
||||||
{value.artifact ? <div className={cn("relative aspect-square overflow-hidden rounded-md border", rarityGradientClass(value.artifact.rarity))}><Image src={value.artifact.imageUrl} alt={value.artifact.name} fill sizes="64px" className="object-contain p-1" /></div> : null}
|
{value.artifact ? <div className={cn("relative aspect-square overflow-hidden rounded-md border", rarityGradientClass(value.artifact.rarity))}><Image src={value.artifact.imageUrl} alt={value.artifact.name} fill sizes="64px" className="object-contain p-1" /></div> : null}
|
||||||
</div> : null}
|
</div> : null}
|
||||||
@@ -82,12 +82,14 @@ function BossCard({ schedule, level, position, catalog }: { schedule: Schedule;
|
|||||||
<StygianBossCard boss={boss} level={level}>
|
<StygianBossCard boss={boss} level={level}>
|
||||||
<section className="flex flex-col gap-3" aria-label={`Recommended teams for ${boss.name}`}>
|
<section className="flex flex-col gap-3" aria-label={`Recommended teams for ${boss.name}`}>
|
||||||
<h3 className="font-heading text-lg font-semibold">Recommended team</h3>
|
<h3 className="font-heading text-lg font-semibold">Recommended team</h3>
|
||||||
{teams.length ? teams.map((team) => (
|
{teams.length ? <div className="grid grid-cols-1 items-start gap-3 xl:grid-cols-2">
|
||||||
<div key={team.id} className="grid w-full grid-cols-4 items-start gap-1 rounded-lg border bg-muted/20 p-2 sm:flex sm:w-fit sm:flex-wrap sm:gap-3">
|
{teams.map((team) => (
|
||||||
{team.items.slice(0, 4).map((item) => <RecommendationIcon key={`${item.groupId}-${item.position}`} item={item} catalog={catalog} teamLayout />)}
|
<div key={team.id} className="grid w-full min-w-0 grid-cols-4 items-start gap-1 rounded-lg border bg-muted/20 p-2 sm:flex sm:w-fit sm:flex-wrap sm:gap-3 xl:grid xl:grid-cols-4 xl:gap-1">
|
||||||
{Array.from({ length: Math.max(0, 4 - team.items.length) }, (_, index) => <div key={`empty-${index}`} className="aspect-square w-full rounded-lg border border-dashed bg-muted/30 sm:w-[116px]" aria-hidden="true" />)}
|
{team.items.slice(0, 4).map((item) => <RecommendationIcon key={`${item.groupId}-${item.position}`} item={item} catalog={catalog} teamLayout />)}
|
||||||
</div>
|
{Array.from({ length: Math.max(0, 4 - team.items.length) }, (_, index) => <div key={`empty-${index}`} className="aspect-square w-full rounded-lg border border-dashed bg-muted/30 sm:w-[116px] xl:w-full" aria-hidden="true" />)}
|
||||||
)) : <p className="text-sm text-muted-foreground">No team recommendations yet.</p>}
|
</div>
|
||||||
|
))}
|
||||||
|
</div> : <p className="text-sm text-muted-foreground">No team recommendations yet.</p>}
|
||||||
</section>
|
</section>
|
||||||
<section className="flex flex-col gap-3" aria-label={`Recommended units for ${boss.name}`}>
|
<section className="flex flex-col gap-3" aria-label={`Recommended units for ${boss.name}`}>
|
||||||
<h3 className="font-heading text-lg font-semibold">Recommended unit</h3>
|
<h3 className="font-heading text-lg font-semibold">Recommended unit</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user