feat : dd up up
This commit is contained in:
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
# This is NOT the Next.js you know
|
# This is NOT the Next.js you know
|
||||||
|
|
||||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
This version has breaking changes - APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||||
|
|
||||||
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
This block is written and re-added by `next dev` - verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
||||||
|
|
||||||
<!-- END:nextjs-agent-rules -->
|
<!-- END:nextjs-agent-rules -->
|
||||||
|
|
||||||
|
|||||||
@@ -374,18 +374,18 @@ during a rolling update.
|
|||||||
This project would not be possible without the open-source software and
|
This project would not be possible without the open-source software and
|
||||||
community services it builds on:
|
community services it builds on:
|
||||||
|
|
||||||
- [Lunaris](https://lunaris.moe/) — character, talent, material, and game data
|
- [Lunaris](https://lunaris.moe/) - character, talent, material, and game data
|
||||||
used by catalog synchronization and guide tools. Buzz Guide links back to
|
used by catalog synchronization and guide tools. Buzz Guide links back to
|
||||||
Lunaris for material references where appropriate.
|
Lunaris for material references where appropriate.
|
||||||
- [Next.js](https://nextjs.org/) — application framework.
|
- [Next.js](https://nextjs.org/) - application framework.
|
||||||
- [React](https://react.dev/) — user interface library.
|
- [React](https://react.dev/) - user interface library.
|
||||||
- [Bun](https://bun.sh/) — JavaScript runtime and package manager.
|
- [Bun](https://bun.sh/) - JavaScript runtime and package manager.
|
||||||
- [PostgreSQL](https://www.postgresql.org/) — primary database.
|
- [PostgreSQL](https://www.postgresql.org/) - primary database.
|
||||||
- [Redis](https://redis.io/) — cache, event transport, and presence state.
|
- [Redis](https://redis.io/) - cache, event transport, and presence state.
|
||||||
- [Discord.js](https://discord.js.org/) — Discord catalog update worker.
|
- [Discord.js](https://discord.js.org/) - Discord catalog update worker.
|
||||||
- [Drizzle ORM](https://orm.drizzle.team/) — database schema and migrations.
|
- [Drizzle ORM](https://orm.drizzle.team/) - database schema and migrations.
|
||||||
- [shadcn/ui](https://ui.shadcn.com/), [Tailwind CSS](https://tailwindcss.com/),
|
- [shadcn/ui](https://ui.shadcn.com/), [Tailwind CSS](https://tailwindcss.com/),
|
||||||
[Lucide](https://lucide.dev/), and [dnd-kit](https://dndkit.com/) — interface
|
[Lucide](https://lucide.dev/), and [dnd-kit](https://dndkit.com/) - interface
|
||||||
components, styling, icons, and drag-and-drop interactions.
|
components, styling, icons, and drag-and-drop interactions.
|
||||||
|
|
||||||
Special thanks are also available on the site’s [Special Thanks page](https://guide.sudloh.com/thanks).
|
Special thanks are also available on the site’s [Special Thanks page](https://guide.sudloh.com/thanks).
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@ async function notify(event: string, details: Record<string, unknown> = {}): Pro
|
|||||||
log("notification_failed", { event, error: "Discord log channel is not sendable." });
|
log("notification_failed", { event, error: "Discord log channel is not sendable." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await channel.send(`**Lunaris catalog worker — ${event}**\n\`\`\`json\n${JSON.stringify(details, null, 2)}\n\`\`\`${typeof details.messageUrl === "string" ? `\nTrigger message: ${details.messageUrl}` : ""}`);
|
await channel.send(`**Lunaris catalog worker - ${event}**\n\`\`\`json\n${JSON.stringify(details, null, 2)}\n\`\`\`${typeof details.messageUrl === "string" ? `\nTrigger message: ${details.messageUrl}` : ""}`);
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
log("notification_failed", {
|
log("notification_failed", {
|
||||||
event,
|
event,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { ComparisonContext, ComparisonValue } from "./comparison-context";
|
import { ComparisonContext } from "./comparison-context";
|
||||||
|
|
||||||
describe("public comparison explanations", () => {
|
describe("public comparison explanations", () => {
|
||||||
it("keeps each damage metric's 100% rows separate", () => {
|
it("keeps each damage metric's 100% rows separate", () => {
|
||||||
@@ -24,12 +24,4 @@ describe("public comparison explanations", () => {
|
|||||||
expect(html).not.toContain("Only choice");
|
expect(html).not.toContain("Only choice");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders a loss relative to the baseline but never treats an unavailable value as a loss", () => {
|
|
||||||
expect(renderToStaticMarkup(<ComparisonValue value="98.5" />)).toContain("-1.5%");
|
|
||||||
for (const value of [null, "0", "invalid"]) {
|
|
||||||
const html = renderToStaticMarkup(<ComparisonValue value={value} />);
|
|
||||||
expect(html).toContain("ไม่มีข้อมูลเปรียบเทียบ");
|
|
||||||
expect(html).not.toContain("-100%");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { comparisonDelta, comparisonReferenceNames } from "@/lib/guides/comparison";
|
import { comparisonReferenceNames } from "@/lib/guides/comparison";
|
||||||
|
|
||||||
type WeaponRows = Parameters<typeof comparisonReferenceNames>[0];
|
type WeaponRows = Parameters<typeof comparisonReferenceNames>[0];
|
||||||
|
|
||||||
@@ -8,7 +8,6 @@ export function ComparisonContext({ rows, metric }: {
|
|||||||
}) {
|
}) {
|
||||||
const references = rows && metric ? comparisonReferenceNames(rows, metric) : [];
|
const references = rows && metric ? comparisonReferenceNames(rows, metric) : [];
|
||||||
return <div className="flex flex-col gap-1 text-sm text-muted-foreground">
|
return <div className="flex flex-col gap-1 text-sm text-muted-foreground">
|
||||||
{!rows && <p>100% = ค่าฐานของเงื่อนไขนี้ เช่น 109% หมายถึงสูงกว่าค่าฐาน 9% เปรียบเทียบภายในเงื่อนไขเดียวกัน</p>}
|
|
||||||
{rows && metric ? <p>
|
{rows && metric ? <p>
|
||||||
{metric === "overallPercent" ? "ดาเมจทั้งทีม" : "ดาเมจส่วนตัว"} - {references.length
|
{metric === "overallPercent" ? "ดาเมจทั้งทีม" : "ดาเมจส่วนตัว"} - {references.length
|
||||||
? `รายการที่แสดง 100%: ${references.join(" / ")}`
|
? `รายการที่แสดง 100%: ${references.join(" / ")}`
|
||||||
@@ -16,8 +15,3 @@ export function ComparisonContext({ rows, metric }: {
|
|||||||
</p> : null}
|
</p> : null}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ComparisonValue({ value }: { value: string | null }) {
|
|
||||||
const delta = comparisonDelta(value);
|
|
||||||
return <p className="text-xs opacity-75">{delta ?? "ไม่มีข้อมูลเปรียบเทียบ"}</p>;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -208,9 +208,9 @@ export function MaterialPoster({
|
|||||||
className="size-[3.5cqw] shrink-0 object-contain brightness-0 invert"
|
className="size-[3.5cqw] shrink-0 object-contain brightness-0 invert"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<span>{stat ? statLabel(stat.key) : "—"}</span>
|
<span>{stat ? statLabel(stat.key) : "-"}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="whitespace-nowrap text-[2.7cqw]">{stat ? statValue(stat.key, stat.value) : "—"}</span>
|
<span className="whitespace-nowrap text-[2.7cqw]">{stat ? statValue(stat.key, stat.value) : "-"}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import Image from "@/components/ui/resilient-image";
|
|||||||
import localFont from "next/font/local";
|
import localFont from "next/font/local";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowLeftIcon, ArrowUpRightIcon } from "lucide-react";
|
import { ArrowLeftIcon, ArrowUpRightIcon } from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import { Fragment, type ReactNode } from "react";
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { ComparisonContext, ComparisonValue } from "@/components/public/comparison-context";
|
import { ComparisonContext } from "@/components/public/comparison-context";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -76,10 +76,10 @@ export function publicSections(guide: PublicGuide) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function publicSectionTitle(slug: string, fallback: string): string {
|
function publicSectionTitle(slug: string, fallback: string): string {
|
||||||
if (slug === "weapon") return "อาวุธ";
|
if (slug === "weapon") return "Weapons";
|
||||||
if (slug === "artifact") return "อาร์ติแฟกต์และค่าสถานะ";
|
if (slug === "artifact") return "Artifacts";
|
||||||
if (slug === "constellations") return "กลุ่มดาว";
|
if (slug === "constellations") return "Constellations";
|
||||||
if (slug === "team") return "ทีมแนะนำและดาเมจ";
|
if (slug === "team") return "Team calc";
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ export function StructuredGuideShell({
|
|||||||
const baseHref = preview ? `/admin/${guide.characterKey}/preview` : `/${guide.slug}`;
|
const baseHref = preview ? `/admin/${guide.characterKey}/preview` : `/${guide.slug}`;
|
||||||
const currentPageTitle = section
|
const currentPageTitle = section
|
||||||
? publicSectionTitle(section, sections.find((item) => item.slug === section)?.title ?? section)
|
? publicSectionTitle(section, sections.find((item) => item.slug === section)?.title ?? section)
|
||||||
: "ภาพรวม";
|
: "Overview";
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-svh flex-col bg-background">
|
<div className="flex min-h-svh flex-col bg-background">
|
||||||
<LiveRefresh endpoint={`/api/events/page/${guide.id}`} />
|
<LiveRefresh endpoint={`/api/events/page/${guide.id}`} />
|
||||||
@@ -156,7 +156,7 @@ export function StructuredGuideShell({
|
|||||||
height={20}
|
height={20}
|
||||||
className="size-5 object-contain"
|
className="size-5 object-contain"
|
||||||
/>
|
/>
|
||||||
ภาพรวม
|
Overview
|
||||||
</Link>
|
</Link>
|
||||||
{sections.map((item) => (
|
{sections.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
@@ -574,7 +574,7 @@ function WeaponSectionContent({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{Number(row.overallPercent) === 0
|
{Number(row.overallPercent) === 0
|
||||||
? (exporting ? "-%" : "—")
|
? (exporting ? "-%" : "-")
|
||||||
: metric(row.overallPercent)}
|
: metric(row.overallPercent)}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
@@ -589,7 +589,6 @@ function WeaponSectionContent({
|
|||||||
>
|
>
|
||||||
ดาเมจทั้งทีม
|
ดาเมจทั้งทีม
|
||||||
</p>
|
</p>
|
||||||
{!exporting && <ComparisonValue value={row.overallPercent} />}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{row.personalPercent ? (
|
{row.personalPercent ? (
|
||||||
@@ -606,7 +605,7 @@ function WeaponSectionContent({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{Number(row.personalPercent) === 0
|
{Number(row.personalPercent) === 0
|
||||||
? (exporting ? "-%" : "—")
|
? (exporting ? "-%" : "-")
|
||||||
: metric(row.personalPercent)}
|
: metric(row.personalPercent)}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
@@ -621,7 +620,6 @@ function WeaponSectionContent({
|
|||||||
>
|
>
|
||||||
ดาเมจส่วนตัว
|
ดาเมจส่วนตัว
|
||||||
</p>
|
</p>
|
||||||
{!exporting && <ComparisonValue value={row.personalPercent} />}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -777,12 +775,16 @@ function ArtifactSectionContent({
|
|||||||
)}
|
)}
|
||||||
<div data-artifact-export-section="tierlist" className={exporting ? "order-first" : "lg:order-first"}>
|
<div data-artifact-export-section="tierlist" className={exporting ? "order-first" : "lg:order-first"}>
|
||||||
{(() => {
|
{(() => {
|
||||||
|
const TierList = exporting ? Fragment : Card;
|
||||||
|
const TierGroup = exporting ? Card : Fragment;
|
||||||
const tierCards = guide.artifactGroups.map((condition, conditionIndex) => (
|
const tierCards = guide.artifactGroups.map((condition, conditionIndex) => (
|
||||||
<div key={condition.id} data-tier-condition={conditionIndex} className="artifact-export-tierlist flex flex-col gap-5">
|
<div key={condition.id} data-tier-condition={conditionIndex} className="artifact-export-tierlist flex flex-col gap-5">
|
||||||
{!exporting && <ComparisonContext />}
|
{!exporting && <ComparisonContext />}
|
||||||
{exporting && guide.artifactGroups.length > 1 ? <p className="text-sm font-medium">{condition.name || <ExportText th={`เงื่อนไข ${conditionIndex + 1}`} en={`Condition ${conditionIndex + 1}`} />}</p> : null}
|
{exporting && guide.artifactGroups.length > 1 ? <p className="text-sm font-medium">{condition.name || <ExportText th={`เงื่อนไข ${conditionIndex + 1}`} en={`Condition ${conditionIndex + 1}`} />}</p> : null}
|
||||||
{condition.note ? <p data-export-note className="whitespace-pre-wrap text-sm text-muted-foreground">{condition.note}</p> : null}
|
{condition.note ? <p data-export-note className="whitespace-pre-wrap text-sm text-muted-foreground">{condition.note}</p> : null}
|
||||||
{condition.tiers.map((group) => <Card key={group.id}>
|
<TierList>
|
||||||
|
{condition.tiers.map((group, groupIndex) => <TierGroup key={group.id}>
|
||||||
|
{!exporting && groupIndex > 0 ? <Separator /> : null}
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
{group.name === "Late Game"
|
{group.name === "Late Game"
|
||||||
@@ -823,14 +825,14 @@ function ArtifactSectionContent({
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-24 shrink-0 text-right sm:w-28">
|
<div className="w-24 shrink-0 text-right sm:w-28">
|
||||||
<p className="text-2xl font-semibold text-primary">
|
<p className="text-2xl font-semibold text-primary">
|
||||||
{Number(row.percent) === 0 ? (exporting ? "-%" : "—") : metric(row.percent, exporting ? "en-US" : undefined)}
|
{Number(row.percent) === 0 ? (exporting ? "-%" : "-") : metric(row.percent, exporting ? "en-US" : undefined)}
|
||||||
</p>
|
</p>
|
||||||
{!exporting && <ComparisonValue value={row.percent} />}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>)}
|
</TierGroup>)}
|
||||||
|
</TierList>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
return exporting ? tierCards : <ConditionSwitcher label="เลือกเงื่อนไขอันดับอาร์ติแฟกต์" labels={guide.artifactGroups.map((condition, index) => condition.name || `เงื่อนไข ${index + 1}`)} iconUrls={guide.artifactGroups.map((condition) => condition.iconUrl)}>{tierCards}</ConditionSwitcher>;
|
return exporting ? tierCards : <ConditionSwitcher label="เลือกเงื่อนไขอันดับอาร์ติแฟกต์" labels={guide.artifactGroups.map((condition, index) => condition.name || `เงื่อนไข ${index + 1}`)} iconUrls={guide.artifactGroups.map((condition) => condition.iconUrl)}>{tierCards}</ConditionSwitcher>;
|
||||||
@@ -1203,7 +1205,6 @@ function ConstellationSection({ guide }: { guide: PublicGuide }) {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</header>
|
</header>
|
||||||
<p className="text-sm text-muted-foreground">ดาเมจทั้งทีมสะสมเทียบกับ C0 = 100% เช่น 117% คือสูงกว่า C0 17% ภายใต้เงื่อนไขเดียวกัน</p>
|
|
||||||
<ConditionSwitcher label="เลือกเงื่อนไขกลุ่มดาว" labels={guide.constellationGroups.filter((group) => group.rows.length > 0).map((group, index) => group.name || `เงื่อนไข ${index + 1}`)} iconUrls={guide.constellationGroups.filter((group) => group.rows.length > 0).map((group) => group.iconUrl)}>
|
<ConditionSwitcher label="เลือกเงื่อนไขกลุ่มดาว" labels={guide.constellationGroups.filter((group) => group.rows.length > 0).map((group, index) => group.name || `เงื่อนไข ${index + 1}`)} iconUrls={guide.constellationGroups.filter((group) => group.rows.length > 0).map((group) => group.iconUrl)}>
|
||||||
{guide.constellationGroups.filter((group) => group.rows.length > 0).map((group, groupIndex, groups) => (
|
{guide.constellationGroups.filter((group) => group.rows.length > 0).map((group, groupIndex, groups) => (
|
||||||
<div key={group.id} className="flex flex-col gap-4">
|
<div key={group.id} className="flex flex-col gap-4">
|
||||||
@@ -1245,9 +1246,8 @@ function ConstellationSection({ guide }: { guide: PublicGuide }) {
|
|||||||
row.isGreat && "text-warning",
|
row.isGreat && "text-warning",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{Number(row.cumulativePercent) === 0 ? "—" : metric(row.cumulativePercent)}
|
{Number(row.cumulativePercent) === 0 ? "-" : metric(row.cumulativePercent)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<ComparisonValue value={row.cumulativePercent} />
|
|
||||||
<CardDescription className="text-base font-medium text-white">
|
<CardDescription className="text-base font-medium text-white">
|
||||||
C{row.level} - {row.name}
|
C{row.level} - {row.name}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
@@ -1271,9 +1271,16 @@ function ConstellationSection({ guide }: { guide: PublicGuide }) {
|
|||||||
function TeamSection({ guide }: { guide: PublicGuide }) {
|
function TeamSection({ guide }: { guide: PublicGuide }) {
|
||||||
return (
|
return (
|
||||||
<section className="flex flex-col gap-6">
|
<section className="flex flex-col gap-6">
|
||||||
<header className="text-center">
|
<header className="flex items-center justify-center gap-3">
|
||||||
<h1 className="font-heading text-3xl font-semibold sm:text-4xl">ทีมแนะนำและดาเมจ</h1>
|
<Image
|
||||||
<p className="mt-3 text-sm text-muted-foreground">ผลคำนวณสำหรับทีม อาวุธ กลุ่มดาว และเงื่อนไขที่ระบุในไกด์ เลือกดูและเปรียบเทียบทีมที่มีอยู่ได้</p>
|
src="/icon/nav/Team-calc.webp"
|
||||||
|
alt=""
|
||||||
|
width={56}
|
||||||
|
height={56}
|
||||||
|
loading="lazy"
|
||||||
|
className="size-14 shrink-0 object-contain"
|
||||||
|
/>
|
||||||
|
<h1 className="font-heading text-3xl font-semibold sm:text-4xl">ทีมและดาเมจ</h1>
|
||||||
</header>
|
</header>
|
||||||
<TeamSearch groups={guide.teamGroups} />
|
<TeamSearch groups={guide.teamGroups} />
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -388,7 +388,6 @@ function TeamCard({
|
|||||||
<p className="mt-1 truncate text-sm font-medium">
|
<p className="mt-1 truncate text-sm font-medium">
|
||||||
{member.character.name}
|
{member.character.name}
|
||||||
</p>
|
</p>
|
||||||
{!exporting && <p className="text-xs text-muted-foreground">ดาเมจต่อโรเทชั่น</p>}
|
|
||||||
<p className="text-sm font-semibold">
|
<p className="text-sm font-semibold">
|
||||||
{Number(member.damage).toLocaleString(undefined, {
|
{Number(member.damage).toLocaleString(undefined, {
|
||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
@@ -398,7 +397,7 @@ function TeamCard({
|
|||||||
{total
|
{total
|
||||||
? ((Number(member.damage) / total) * 100).toFixed(2)
|
? ((Number(member.damage) / total) * 100).toFixed(2)
|
||||||
: "0.00"}
|
: "0.00"}
|
||||||
%{!exporting && " ของทีม"}
|
%
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Bar, BarChart, CartesianGrid, LabelList, ReferenceLine, XAxis, YAxis } from "recharts";
|
import { Bar, BarChart, CartesianGrid, LabelList, ReferenceLine, XAxis, YAxis } from "recharts";
|
||||||
|
import Image from "@/components/ui/resilient-image";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
ChartContainer, ChartLegend, ChartLegendContent,
|
ChartContainer, ChartLegend, ChartLegendContent,
|
||||||
@@ -22,6 +24,8 @@ function percentage(value: string | null) {
|
|||||||
export function WeaponChart({ rows }: { rows: PublicGuide["weaponGroups"][number]["rows"] }) {
|
export function WeaponChart({ rows }: { rows: PublicGuide["weaponGroups"][number]["rows"] }) {
|
||||||
const data = rows.map((row, index) => ({
|
const data = rows.map((row, index) => ({
|
||||||
name: `${index + 1}. ${row.weapon.name} R${row.refinement}`,
|
name: `${index + 1}. ${row.weapon.name} R${row.refinement}`,
|
||||||
|
weapon: row.weapon,
|
||||||
|
refinement: row.refinement,
|
||||||
overall: percentage(row.overallPercent),
|
overall: percentage(row.overallPercent),
|
||||||
personal: percentage(row.personalPercent),
|
personal: percentage(row.personalPercent),
|
||||||
}));
|
}));
|
||||||
@@ -38,30 +42,38 @@ export function WeaponChart({ rows }: { rows: PublicGuide["weaponGroups"][number
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="min-w-0">
|
<CardContent className="min-w-0">
|
||||||
<ChartContainer config={chartConfig} className="aspect-auto min-w-0 w-full"
|
<ChartContainer config={chartConfig} className="aspect-auto min-w-0 w-full"
|
||||||
style={{ height: Math.max(240, data.length * 72 + 80) }}
|
style={{ height: 420 }}
|
||||||
aria-label="กราฟเปรียบเทียบเปอร์เซ็นต์ดาเมจอาวุธ">
|
aria-label="กราฟเปรียบเทียบเปอร์เซ็นต์ดาเมจอาวุธ">
|
||||||
<BarChart accessibilityLayer data={data} layout="vertical" margin={{ right: 42, left: 0 }}>
|
<BarChart accessibilityLayer data={data} layout="horizontal" barGap={4} margin={{ top: 28, right: 12, left: 0 }}>
|
||||||
<CartesianGrid horizontal={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<XAxis type="number" tickLine={false} axisLine={false}
|
<YAxis type="number" tickLine={false} axisLine={false}
|
||||||
domain={[(minimum: number) => Math.min(0, minimum), (maximum: number) => Math.max(110, maximum)]}
|
domain={[(minimum: number) => Math.min(0, minimum), (maximum: number) => Math.max(110, maximum)]}
|
||||||
tickFormatter={(value) => `${value}%`} />
|
tickFormatter={(value) => `${value}%`} />
|
||||||
<YAxis dataKey="name" type="category" tickLine={false} axisLine={false} width={142} interval={0}
|
<XAxis dataKey="name" type="category" tickLine={false} axisLine={false} height={96} interval={0}
|
||||||
tick={({ x, y, payload }) => <g transform={`translate(${x},${y})`}>
|
tick={({ x, y, width, payload }) => {
|
||||||
<foreignObject x={-140} y={-32} width={132} height={64}>
|
const row = data.find((item) => item.name === payload.value);
|
||||||
<div className="flex h-full items-center justify-end text-right text-xs leading-4 wrap-anywhere text-foreground">{payload.value}</div>
|
if (!row) return <g />;
|
||||||
</foreignObject>
|
const scale = Math.min(1, Math.max(0, Number(width) / data.length - 4) / 56);
|
||||||
</g>} />
|
return <g transform={`translate(${x},${Number(y) + 8}) scale(${scale})`}>
|
||||||
<ReferenceLine x={100} stroke="var(--muted-foreground)" strokeDasharray="4 4" />
|
<foreignObject x={-28} y={0} width={56} height={80}>
|
||||||
|
<div className="flex flex-col items-center gap-1" title={row.name}>
|
||||||
|
<Image src={row.weapon.imageUrl} alt={row.weapon.name} width={56} height={56} className="size-14 object-contain" />
|
||||||
|
<Badge variant="secondary">R{row.refinement}</Badge>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
</g>;
|
||||||
|
}} />
|
||||||
|
<ReferenceLine y={100} stroke="var(--muted-foreground)" strokeDasharray="4 4" />
|
||||||
<ChartTooltip content={<ChartTooltipContent formatter={(value, name) => <div className="flex flex-1 items-center justify-between gap-4">
|
<ChartTooltip content={<ChartTooltipContent formatter={(value, name) => <div className="flex flex-1 items-center justify-between gap-4">
|
||||||
<span className="text-muted-foreground">{chartConfig[name as keyof typeof chartConfig]?.label ?? name}</span>
|
<span className="text-muted-foreground">{chartConfig[name as keyof typeof chartConfig]?.label ?? name}</span>
|
||||||
<span className="font-mono font-medium">{value}%</span>
|
<span className="font-mono font-medium">{value}%</span>
|
||||||
</div>} />} />
|
</div>} />} />
|
||||||
<ChartLegend content={<ChartLegendContent className="flex-wrap" />} />
|
<ChartLegend content={<ChartLegendContent className="flex-wrap" />} />
|
||||||
{hasPersonal && <Bar dataKey="personal" fill="var(--color-personal)" radius={4} maxBarSize={singleMetric ? 26 : 18} isAnimationActive={false}>
|
{hasPersonal && <Bar dataKey="personal" fill="var(--color-personal)" radius={4} maxBarSize={singleMetric ? 48 : 40} isAnimationActive={false}>
|
||||||
<LabelList position="right" formatter={(value) => typeof value === "number" ? `${value.toLocaleString("th-TH", { maximumFractionDigits: 2 })}%` : ""} className="fill-foreground text-xs" />
|
<LabelList position="top" formatter={(value) => typeof value === "number" ? `${value.toLocaleString("th-TH", { maximumFractionDigits: 2 })}%` : ""} className="fill-foreground text-xs" />
|
||||||
</Bar>}
|
</Bar>}
|
||||||
{hasOverall && <Bar dataKey="overall" fill="var(--color-overall)" radius={4} maxBarSize={singleMetric ? 26 : 18} isAnimationActive={false}>
|
{hasOverall && <Bar dataKey="overall" fill="var(--color-overall)" radius={4} maxBarSize={singleMetric ? 48 : 40} isAnimationActive={false}>
|
||||||
<LabelList position="right" formatter={(value) => typeof value === "number" ? `${value.toLocaleString("th-TH", { maximumFractionDigits: 2 })}%` : ""} className="fill-foreground text-xs" />
|
<LabelList position="top" formatter={(value) => typeof value === "number" ? `${value.toLocaleString("th-TH", { maximumFractionDigits: 2 })}%` : ""} className="fill-foreground text-xs" />
|
||||||
</Bar>}
|
</Bar>}
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
|
|||||||
Reference in New Issue
Block a user