feat: update
This commit is contained in:
@@ -25,5 +25,5 @@ export function GuideOverviewForm({ guide }: { guide: { id: string; version: num
|
||||
initialVersion: guide.version,
|
||||
persist: (next, expectedVersion) => saveGuideOverview(guide.id, { ...next, expectedVersion }),
|
||||
});
|
||||
return <Card><CardHeader><CardTitle>Overview</CardTitle><CardDescription>ภาพและข้อความนี้แสดงบนหน้าแรกและส่วนแนะนำของ Guide</CardDescription></CardHeader><CardContent><FieldGroup><Field><FieldLabel>Guide Image</FieldLabel><div className="relative aspect-square max-w-md overflow-hidden rounded-lg border"><Image src={cover.url} alt={`Guide ${guide.name}`} fill className="object-cover" /></div><MediaUploader onReady={(asset) => setCover(asset)} /></Field><Field><FieldLabel htmlFor="guide-overview">Overview</FieldLabel><Textarea id="guide-overview" value={overview} maxLength={500} onChange={(event) => setOverview(event.target.value)} /><FieldDescription>{overview.length}/500 ตัวอักษร</FieldDescription></Field></FieldGroup></CardContent><CardFooter className="justify-end"><AutosaveStatus state={autosave} issues={incompleteIssues} /></CardFooter></Card>;
|
||||
return <Card><CardHeader><CardTitle>Overview</CardTitle><CardDescription>ภาพและข้อความนี้แสดงบนหน้าแรกและส่วนแนะนำของ Guide</CardDescription></CardHeader><CardContent><FieldGroup><Field><FieldLabel>Guide Image</FieldLabel><div className="relative mx-auto aspect-square w-full max-w-md overflow-hidden rounded-lg border"><Image src={cover.url} alt={`Guide ${guide.name}`} fill className="object-cover" /></div><MediaUploader onReady={(asset) => setCover(asset)} /></Field><Field><FieldLabel htmlFor="guide-overview">Overview</FieldLabel><Textarea id="guide-overview" value={overview} maxLength={500} onChange={(event) => setOverview(event.target.value)} /><FieldDescription>{overview.length}/500 ตัวอักษร</FieldDescription></Field></FieldGroup></CardContent><CardFooter className="justify-end"><AutosaveStatus state={autosave} issues={incompleteIssues} /></CardFooter></Card>;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
arrayMove,
|
||||
horizontalListSortingStrategy,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
@@ -212,12 +213,10 @@ function TalentLevelPicker({ value, onChange }: { value?: string; onChange: (val
|
||||
|
||||
function SortableTalentCard({
|
||||
talent,
|
||||
index,
|
||||
catalogTalent,
|
||||
onLevelChange,
|
||||
}: {
|
||||
talent: TalentPriority;
|
||||
index: number;
|
||||
catalogTalent?: CatalogTalent;
|
||||
onLevelChange: (level?: string) => void;
|
||||
}) {
|
||||
@@ -231,7 +230,7 @@ function SortableTalentCard({
|
||||
<Card size="sm" className="h-full">
|
||||
<CardHeader className="flex items-center gap-2">
|
||||
{catalogTalent?.iconUrl ? <Image src={catalogTalent.iconUrl} alt="" width={40} height={40} className="size-10 shrink-0 rounded-lg bg-muted" /> : null}
|
||||
<CardTitle className="min-w-0 flex-1 truncate">{index + 1}. {catalogTalent?.name ?? talent.type}</CardTitle>
|
||||
<CardTitle className="min-w-0 flex-1 truncate">{talent.type}. {catalogTalent?.name ?? talent.type}</CardTitle>
|
||||
<CardAction>
|
||||
<Button type="button" variant="ghost" size="icon-sm" className="touch-none" {...attributes} {...listeners}>
|
||||
<GripVerticalIcon />
|
||||
@@ -460,13 +459,13 @@ function ArtifactEditor({ guide, artifacts, initial, catalogTalents }: EditorBas
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Recommended Stats</CardTitle><CardDescription>เลือกค่าจากรายการแทนปุ่ม และเลือก Main Stat ได้สูงสุด 2 ค่าต่อชิ้น</CardDescription></CardHeader>
|
||||
<CardHeader><CardTitle>Recommended Stats</CardTitle><CardDescription>เลือกค่าจากรายการแทนปุ่ม และเลือก Main Stat ได้สูงสุด 3 ค่าต่อชิ้น</CardDescription></CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<StatSelect label="Sands / นาฬิกา" options={mainStatOptions.sands} value={sands} max={2} onChange={setSands} />
|
||||
<StatSelect label="Goblet / ถ้วย" options={mainStatOptions.goblet} value={goblet} max={2} onChange={setGoblet} />
|
||||
<StatSelect label="Circlet / หมวก" options={mainStatOptions.circlet} value={circlet} max={2} onChange={setCirclet} />
|
||||
<StatSelect label="Sands / นาฬิกา" options={mainStatOptions.sands} value={sands} max={3} onChange={setSands} />
|
||||
<StatSelect label="Goblet / ถ้วย" options={mainStatOptions.goblet} value={goblet} max={3} onChange={setGoblet} />
|
||||
<StatSelect label="Circlet / หมวก" options={mainStatOptions.circlet} value={circlet} max={3} onChange={setCirclet} />
|
||||
</div>
|
||||
<StatSelect label="Substats" options={substatOptions} value={substats} max={substatOptions.length} onChange={setSubstats} />
|
||||
<FieldSet>
|
||||
@@ -486,12 +485,12 @@ function ArtifactEditor({ guide, artifacts, initial, catalogTalents }: EditorBas
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<FieldDescription>กรอกเฉพาะค่าเป็นตัวเลข เปอร์เซ็นต์ หรือข้อความ เช่น -%</FieldDescription>
|
||||
<FieldDescription>กรอกเฉพาะค่าเป็นตัวเลข เปอร์เซ็นต์ หรือข้อความ เช่น -% หากไม่กรอกค่า ระบบจะไม่แสดงส่วนค่าสถานะเป้าหมาย</FieldDescription>
|
||||
</FieldSet>
|
||||
<FieldSet>
|
||||
<FieldLegend>Talent Priority</FieldLegend>
|
||||
<DndContext sensors={talentSensors} collisionDetection={closestCenter} onDragEnd={handleTalentDragEnd}>
|
||||
<SortableContext items={orderedTalents.map((talent) => talent.type)} strategy={verticalListSortingStrategy}>
|
||||
<SortableContext items={orderedTalents.map((talent) => talent.type)} strategy={horizontalListSortingStrategy}>
|
||||
<div className="flex items-start gap-3 overflow-x-auto pb-1">
|
||||
{orderedTalents.map((talent, index) => {
|
||||
const catalogTalent = catalogTalents.find((item) => item.type === talent.type);
|
||||
@@ -499,7 +498,6 @@ function ArtifactEditor({ guide, artifacts, initial, catalogTalents }: EditorBas
|
||||
<div key={talent.type} className="flex shrink-0 items-center gap-3">
|
||||
<SortableTalentCard
|
||||
talent={talent}
|
||||
index={index}
|
||||
catalogTalent={catalogTalent}
|
||||
onLevelChange={(level) => setTalents(orderedTalents.map((item) => item.type === talent.type ? { ...item, level } : item))}
|
||||
/>
|
||||
@@ -517,6 +515,7 @@ function ArtifactEditor({ guide, artifacts, initial, catalogTalents }: EditorBas
|
||||
>
|
||||
<ToggleGroupItem className={SELECTED_TOGGLE_CLASS} value="gt" aria-label="มากกว่า">></ToggleGroupItem>
|
||||
<ToggleGroupItem className={SELECTED_TOGGLE_CLASS} value="gte" aria-label="มากกว่าหรือเท่ากับ">≥</ToggleGroupItem>
|
||||
<ToggleGroupItem className={SELECTED_TOGGLE_CLASS} value="eq" aria-label="เท่ากับ">=</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -632,7 +631,7 @@ function TeamEditor({ guide, characters, weapons, artifacts, initial }: EditorBa
|
||||
<CardHeader>
|
||||
<CardTitle>ตัวละคร {memberIndex + 1}</CardTitle>
|
||||
<CardDescription>{total ? `${((Number(member.damage) || 0) / total * 100).toFixed(2)}% ของ Team` : "0% ของ Team"}</CardDescription>
|
||||
{character ? <CardAction><Avatar size="lg"><AvatarImage src={character.imageUrl} alt={character.name} /><AvatarFallback>{character.name.slice(0, 2).toUpperCase()}</AvatarFallback></Avatar></CardAction> : null}
|
||||
{character ? <CardAction><Avatar><AvatarImage src={character.imageUrl} alt={character.name} /><AvatarFallback>{character.name.slice(0, 2).toUpperCase()}</AvatarFallback></Avatar></CardAction> : null}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
|
||||
@@ -21,7 +21,7 @@ import { rarityGradientClass, rarityTextClass } from "@/lib/catalog/rarity";
|
||||
import type { PublicGuide } from "@/lib/guides/queries";
|
||||
import { artifactTierCount } from "@/lib/guides/artifacts";
|
||||
import { navigationIconPath } from "@/lib/navigation";
|
||||
import { normalizeTargetStat, statPriorityColorClass, targetStatIconName } from "@/lib/guides/stats";
|
||||
import { normalizeTargetStat, recommendedStatIconName, statPriorityColorClass, targetStatIconName, targetStatOptions } from "@/lib/guides/stats";
|
||||
import { normalizeTalentPriorities, talentRelationSymbol, talentTypeLabel } from "@/lib/guides/talents";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -142,6 +142,13 @@ function GuideOverview({
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="mx-auto flex max-w-3xl flex-col gap-3 text-center">
|
||||
<p className="text-sm font-medium text-primary">ภาพรวมแนวทาง</p>
|
||||
<h1 className="font-heading text-4xl font-bold">{guide.name}</h1>
|
||||
<p className="text-lg font-semibold">
|
||||
{guide.overview}
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative mx-auto aspect-square w-full max-w-3xl overflow-hidden rounded-xl border bg-muted">
|
||||
<Image
|
||||
src={guide.coverUrl}
|
||||
@@ -151,13 +158,6 @@ function GuideOverview({
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-auto flex max-w-3xl flex-col gap-3 text-center">
|
||||
<p className="text-sm font-medium text-primary">ภาพรวมแนวทาง</p>
|
||||
<h1 className="font-heading text-4xl font-semibold">{guide.name}</h1>
|
||||
<p className="text-lg leading-8 text-muted-foreground">
|
||||
{guide.overview}
|
||||
</p>
|
||||
</div>
|
||||
{sections.length ? (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{sections.map((item) => (
|
||||
@@ -238,11 +238,12 @@ function WeaponSection({ guide }: { guide: PublicGuide }) {
|
||||
rarityGradientClass(row.weapon.rarity),
|
||||
)}
|
||||
>
|
||||
<div className="relative size-20 shrink-0">
|
||||
<div className="relative size-24 shrink-0 md:size-28">
|
||||
<Image
|
||||
src={row.weapon.imageUrl}
|
||||
alt={row.weapon.name}
|
||||
fill
|
||||
sizes="(min-width: 768px) 7rem, 6rem"
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
@@ -312,6 +313,30 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
|
||||
<div className="grid gap-5 lg:grid-cols-[1.2fr_1fr]">
|
||||
{" "}
|
||||
<div className="flex flex-col gap-5">
|
||||
{profile ? (
|
||||
<Card className="overflow-hidden pt-0">
|
||||
<CardHeader className="bg-primary py-4 text-center text-primary-foreground">
|
||||
<CardTitle className="text-2xl">ออฟหลักที่แนะนำ</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-3 p-0">
|
||||
<ArtifactStat iconSrc="/icon/Icon_Sands_of_Eon.webp" label="นาฬิกา" values={profile.sands} />
|
||||
<ArtifactStat iconSrc="/icon/Icon_Goblet_of_Eonothem.webp" label="ถ้วย" values={profile.goblet} />
|
||||
<ArtifactStat iconSrc="/icon/Icon_Circlet_of_Logos.webp" label="หัว" values={profile.circlet} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
{profile?.substats.length ? (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center gap-2 py-5 text-center text-sm">
|
||||
<strong>ออฟรอง</strong>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
{profile.substats.map((stat, index) => (
|
||||
<ArtifactStatValue key={`${stat}-${index}`} value={stat} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
{guide.artifactGroups.map((group) => (
|
||||
<Card key={group.id}>
|
||||
<CardHeader>
|
||||
@@ -323,7 +348,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
|
||||
<div className="flex -space-x-3">
|
||||
<div
|
||||
className={cn(
|
||||
"relative size-14 overflow-hidden rounded-lg border",
|
||||
"relative size-16 overflow-hidden rounded-lg border sm:size-20",
|
||||
rarityGradientClass(row.first.rarity),
|
||||
)}
|
||||
>
|
||||
@@ -331,13 +356,14 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
|
||||
src={row.first.imageUrl}
|
||||
alt={row.first.name}
|
||||
fill
|
||||
sizes="(min-width: 640px) 5rem, 4rem"
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
{row.second ? (
|
||||
<div
|
||||
className={cn(
|
||||
"relative size-14 overflow-hidden rounded-lg border",
|
||||
"relative size-16 overflow-hidden rounded-lg border sm:size-20",
|
||||
rarityGradientClass(row.second.rarity),
|
||||
)}
|
||||
>
|
||||
@@ -345,6 +371,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
|
||||
src={row.second.imageUrl}
|
||||
alt={row.second.name}
|
||||
fill
|
||||
sizes="(min-width: 640px) 5rem, 4rem"
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
@@ -369,30 +396,6 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
|
||||
</div>
|
||||
{profile ? (
|
||||
<div className="flex flex-col gap-5">
|
||||
<Card className="overflow-hidden pt-0">
|
||||
<CardHeader className="bg-primary py-4 text-center text-primary-foreground">
|
||||
<CardTitle className="text-2xl">ออฟหลักที่แนะนำ</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-3 p-0">
|
||||
<ArtifactStat iconSrc="/icon/Icon_Sands_of_Eon.webp" label="นาฬิกา" values={profile.sands} />
|
||||
<ArtifactStat iconSrc="/icon/Icon_Goblet_of_Eonothem.webp" label="ถ้วย" values={profile.goblet} />
|
||||
<ArtifactStat iconSrc="/icon/Icon_Circlet_of_Logos.webp" label="หัว" values={profile.circlet} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
{profile.substats.length ? (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center gap-2 py-5 text-center text-sm">
|
||||
<strong>ลำดับค่าสถานะ:</strong>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
{profile.substats.map((stat, index) => (
|
||||
<span key={`${stat}-${index}`} className={statPriorityColorClass(index)}>
|
||||
{statLabel(stat)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
<Card>
|
||||
<CardHeader><CardTitle>รายละเอียดเพิ่มเติม</CardTitle></CardHeader>
|
||||
<CardContent className="flex flex-col gap-5">
|
||||
@@ -409,7 +412,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
|
||||
)}
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
{targetStatIconName(target.stat) ? <Image src={`/icon/stats/${targetStatIconName(target.stat)}.webp`} alt="" width={28} height={28} className="size-7 shrink-0 object-contain brightness-0 invert" /> : null}
|
||||
{targetStatIconName(target.stat) ? <Image src={`/icon/stats/${targetStatIconName(target.stat)}.webp`} alt="" width={14} height={14} className="size-3.5 shrink-0 object-contain brightness-0 invert" /> : null}
|
||||
<span>{statLabel(normalizeTargetStat(target.stat) ?? target.stat)}</span>
|
||||
</span>
|
||||
<strong className="shrink-0">{target.value}</strong>
|
||||
@@ -437,7 +440,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
|
||||
<p className="text-[11px] text-muted-foreground sm:text-xs">{talentTypeLabel(talent.type)}</p>
|
||||
</div>
|
||||
{index < talents.length - 1 ? (
|
||||
<span className="mt-4 text-xl font-semibold sm:mt-6 sm:text-3xl" aria-label={talent.relationToNext === "gte" ? "มากกว่าหรือเท่ากับ" : "มากกว่า"}>
|
||||
<span className="mt-4 text-xl font-semibold sm:mt-6 sm:text-3xl" aria-label={talent.relationToNext === "gte" ? "มากกว่าหรือเท่ากับ" : talent.relationToNext === "eq" ? "เท่ากับ" : "มากกว่า"}>
|
||||
{talentRelationSymbol(talent.relationToNext)}
|
||||
</span>
|
||||
) : null}
|
||||
@@ -499,9 +502,7 @@ function ArtifactStat({ iconSrc, label, values }: { iconSrc: string; label: stri
|
||||
<div className="flex max-w-full flex-col items-center gap-1 text-xs leading-5 sm:text-sm sm:leading-6">
|
||||
{values.length
|
||||
? values.map((value, index) => (
|
||||
<span key={`${value}-${index}`} className={cn("max-w-full break-words text-center", statPriorityColorClass(index))}>
|
||||
{statLabel(value)}
|
||||
</span>
|
||||
<ArtifactStatValue key={`${value}-${index}`} value={value} index={index} />
|
||||
))
|
||||
: <span>—</span>}
|
||||
</div>
|
||||
@@ -509,6 +510,17 @@ function ArtifactStat({ iconSrc, label, values }: { iconSrc: string; label: stri
|
||||
);
|
||||
}
|
||||
|
||||
function ArtifactStatValue({ value, index }: { value: string; index: number }) {
|
||||
const iconName = recommendedStatIconName(value);
|
||||
const hasIcon = iconName && targetStatOptions.some((stat) => targetStatIconName(stat) === iconName);
|
||||
return (
|
||||
<span className={cn("flex max-w-full items-center justify-center gap-1 text-center", statPriorityColorClass(index))}>
|
||||
{hasIcon ? <Image src={`/icon/stats/${iconName}.webp`} alt="" width={20} height={20} className="size-5 shrink-0 object-contain brightness-0 invert" /> : null}
|
||||
<span className="break-words">{statLabel(value)}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ConstellationSection({ guide }: { guide: PublicGuide }) {
|
||||
const note = guide.sections.find((section) => section.slug === "constellations")?.note;
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user