diff --git a/AGENTS.md b/AGENTS.md index 07eb65d..b090747 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,9 +2,9 @@ # 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. diff --git a/README.md b/README.md index 2f47cfc..6868b1e 100644 --- a/README.md +++ b/README.md @@ -374,18 +374,18 @@ during a rolling update. This project would not be possible without the open-source software and 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 Lunaris for material references where appropriate. -- [Next.js](https://nextjs.org/) — application framework. -- [React](https://react.dev/) — user interface library. -- [Bun](https://bun.sh/) — JavaScript runtime and package manager. -- [PostgreSQL](https://www.postgresql.org/) — primary database. -- [Redis](https://redis.io/) — cache, event transport, and presence state. -- [Discord.js](https://discord.js.org/) — Discord catalog update worker. -- [Drizzle ORM](https://orm.drizzle.team/) — database schema and migrations. +- [Next.js](https://nextjs.org/) - application framework. +- [React](https://react.dev/) - user interface library. +- [Bun](https://bun.sh/) - JavaScript runtime and package manager. +- [PostgreSQL](https://www.postgresql.org/) - primary database. +- [Redis](https://redis.io/) - cache, event transport, and presence state. +- [Discord.js](https://discord.js.org/) - Discord catalog update worker. +- [Drizzle ORM](https://orm.drizzle.team/) - database schema and migrations. - [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. Special thanks are also available on the site’s [Special Thanks page](https://guide.sudloh.com/thanks). diff --git a/backend/discord.ts b/backend/discord.ts index 6b9df28..2d7c9f5 100644 --- a/backend/discord.ts +++ b/backend/discord.ts @@ -73,7 +73,7 @@ async function notify(event: string, details: Record = {}): Pro log("notification_failed", { event, error: "Discord log channel is not sendable." }); 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) { log("notification_failed", { event, diff --git a/components/public/comparison-context.test.tsx b/components/public/comparison-context.test.tsx index fdf78b4..c5f84ba 100644 --- a/components/public/comparison-context.test.tsx +++ b/components/public/comparison-context.test.tsx @@ -1,6 +1,6 @@ import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; -import { ComparisonContext, ComparisonValue } from "./comparison-context"; +import { ComparisonContext } from "./comparison-context"; describe("public comparison explanations", () => { it("keeps each damage metric's 100% rows separate", () => { @@ -24,12 +24,4 @@ describe("public comparison explanations", () => { 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()).toContain("-1.5%"); - for (const value of [null, "0", "invalid"]) { - const html = renderToStaticMarkup(); - expect(html).toContain("ไม่มีข้อมูลเปรียบเทียบ"); - expect(html).not.toContain("-100%"); - } - }); }); diff --git a/components/public/comparison-context.tsx b/components/public/comparison-context.tsx index e739955..347b6d9 100644 --- a/components/public/comparison-context.tsx +++ b/components/public/comparison-context.tsx @@ -1,4 +1,4 @@ -import { comparisonDelta, comparisonReferenceNames } from "@/lib/guides/comparison"; +import { comparisonReferenceNames } from "@/lib/guides/comparison"; type WeaponRows = Parameters[0]; @@ -8,7 +8,6 @@ export function ComparisonContext({ rows, metric }: { }) { const references = rows && metric ? comparisonReferenceNames(rows, metric) : []; return
- {!rows &&

100% = ค่าฐานของเงื่อนไขนี้ เช่น 109% หมายถึงสูงกว่าค่าฐาน 9% เปรียบเทียบภายในเงื่อนไขเดียวกัน

} {rows && metric ?

{metric === "overallPercent" ? "ดาเมจทั้งทีม" : "ดาเมจส่วนตัว"} - {references.length ? `รายการที่แสดง 100%: ${references.join(" / ")}` @@ -16,8 +15,3 @@ export function ComparisonContext({ rows, metric }: {

: null}
; } - -export function ComparisonValue({ value }: { value: string | null }) { - const delta = comparisonDelta(value); - return

{delta ?? "ไม่มีข้อมูลเปรียบเทียบ"}

; -} diff --git a/components/public/material-poster.tsx b/components/public/material-poster.tsx index d8f31ec..a511b23 100644 --- a/components/public/material-poster.tsx +++ b/components/public/material-poster.tsx @@ -208,9 +208,9 @@ export function MaterialPoster({ className="size-[3.5cqw] shrink-0 object-contain brightness-0 invert" /> ) : null} - {stat ? statLabel(stat.key) : "—"} + {stat ? statLabel(stat.key) : "-"} - {stat ? statValue(stat.key, stat.value) : "—"} + {stat ? statValue(stat.key, stat.value) : "-"} ); })} diff --git a/components/public/structured-guide.tsx b/components/public/structured-guide.tsx index 0a4c609..b04a508 100644 --- a/components/public/structured-guide.tsx +++ b/components/public/structured-guide.tsx @@ -2,11 +2,11 @@ import Image from "@/components/ui/resilient-image"; import localFont from "next/font/local"; import Link from "next/link"; import { ArrowLeftIcon, ArrowUpRightIcon } from "lucide-react"; -import type { ReactNode } from "react"; +import { Fragment, type ReactNode } from "react"; import { Badge } from "@/components/ui/badge"; 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 { Card, @@ -76,10 +76,10 @@ export function publicSections(guide: PublicGuide) { } function publicSectionTitle(slug: string, fallback: string): string { - if (slug === "weapon") return "อาวุธ"; - if (slug === "artifact") return "อาร์ติแฟกต์และค่าสถานะ"; - if (slug === "constellations") return "กลุ่มดาว"; - if (slug === "team") return "ทีมแนะนำและดาเมจ"; + if (slug === "weapon") return "Weapons"; + if (slug === "artifact") return "Artifacts"; + if (slug === "constellations") return "Constellations"; + if (slug === "team") return "Team calc"; return fallback; } @@ -96,7 +96,7 @@ export function StructuredGuideShell({ const baseHref = preview ? `/admin/${guide.characterKey}/preview` : `/${guide.slug}`; const currentPageTitle = section ? publicSectionTitle(section, sections.find((item) => item.slug === section)?.title ?? section) - : "ภาพรวม"; + : "Overview"; return (
@@ -156,7 +156,7 @@ export function StructuredGuideShell({ height={20} className="size-5 object-contain" /> - ภาพรวม + Overview {sections.map((item) => ( {Number(row.overallPercent) === 0 - ? (exporting ? "-%" : "—") + ? (exporting ? "-%" : "-") : metric(row.overallPercent)}

ดาเมจทั้งทีม

- {!exporting && }
) : null} {row.personalPercent ? ( @@ -606,7 +605,7 @@ function WeaponSectionContent({ )} > {Number(row.personalPercent) === 0 - ? (exporting ? "-%" : "—") + ? (exporting ? "-%" : "-") : metric(row.personalPercent)}

ดาเมจส่วนตัว

- {!exporting && } ) : null} @@ -777,12 +775,16 @@ function ArtifactSectionContent({ )}
{(() => { + const TierList = exporting ? Fragment : Card; + const TierGroup = exporting ? Card : Fragment; const tierCards = guide.artifactGroups.map((condition, conditionIndex) => (
{!exporting && } {exporting && guide.artifactGroups.length > 1 ?

{condition.name || }

: null} {condition.note ?

{condition.note}

: null} - {condition.tiers.map((group) => + + {condition.tiers.map((group, groupIndex) => + {!exporting && groupIndex > 0 ? : null} {group.name === "Late Game" @@ -823,14 +825,14 @@ function ArtifactSectionContent({

- {Number(row.percent) === 0 ? (exporting ? "-%" : "—") : metric(row.percent, exporting ? "en-US" : undefined)} + {Number(row.percent) === 0 ? (exporting ? "-%" : "-") : metric(row.percent, exporting ? "en-US" : undefined)}

- {!exporting && }
))} - )} + )} + )); return exporting ? tierCards : condition.name || `เงื่อนไข ${index + 1}`)} iconUrls={guide.artifactGroups.map((condition) => condition.iconUrl)}>{tierCards}; @@ -1203,7 +1205,6 @@ function ConstellationSection({ guide }: { guide: PublicGuide }) {

) : null} -

ดาเมจทั้งทีมสะสมเทียบกับ C0 = 100% เช่น 117% คือสูงกว่า C0 17% ภายใต้เงื่อนไขเดียวกัน

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) => (
@@ -1245,9 +1246,8 @@ function ConstellationSection({ guide }: { guide: PublicGuide }) { row.isGreat && "text-warning", )} > - {Number(row.cumulativePercent) === 0 ? "—" : metric(row.cumulativePercent)} + {Number(row.cumulativePercent) === 0 ? "-" : metric(row.cumulativePercent)} - C{row.level} - {row.name} @@ -1271,9 +1271,16 @@ function ConstellationSection({ guide }: { guide: PublicGuide }) { function TeamSection({ guide }: { guide: PublicGuide }) { return (
-
-

ทีมแนะนำและดาเมจ

-

ผลคำนวณสำหรับทีม อาวุธ กลุ่มดาว และเงื่อนไขที่ระบุในไกด์ เลือกดูและเปรียบเทียบทีมที่มีอยู่ได้

+
+ +

ทีมและดาเมจ

diff --git a/components/public/team-search.tsx b/components/public/team-search.tsx index 6ea31bf..8b65b7b 100644 --- a/components/public/team-search.tsx +++ b/components/public/team-search.tsx @@ -388,7 +388,6 @@ function TeamCard({

{member.character.name}

- {!exporting &&

ดาเมจต่อโรเทชั่น

}

{Number(member.damage).toLocaleString(undefined, { maximumFractionDigits: 2, @@ -398,7 +397,7 @@ function TeamCard({ {total ? ((Number(member.damage) / total) * 100).toFixed(2) : "0.00"} - %{!exporting && " ของทีม"} + %

))} diff --git a/components/public/weapon-chart.tsx b/components/public/weapon-chart.tsx index 88f58c7..d1cbee2 100644 --- a/components/public/weapon-chart.tsx +++ b/components/public/weapon-chart.tsx @@ -1,6 +1,8 @@ "use client"; 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 { ChartContainer, ChartLegend, ChartLegendContent, @@ -22,6 +24,8 @@ function percentage(value: string | null) { export function WeaponChart({ rows }: { rows: PublicGuide["weaponGroups"][number]["rows"] }) { const data = rows.map((row, index) => ({ name: `${index + 1}. ${row.weapon.name} R${row.refinement}`, + weapon: row.weapon, + refinement: row.refinement, overall: percentage(row.overallPercent), personal: percentage(row.personalPercent), })); @@ -38,30 +42,38 @@ export function WeaponChart({ rows }: { rows: PublicGuide["weaponGroups"][number - - - + + Math.min(0, minimum), (maximum: number) => Math.max(110, maximum)]} tickFormatter={(value) => `${value}%`} /> - - -
{payload.value}
-
-
} /> - + { + const row = data.find((item) => item.name === payload.value); + if (!row) return ; + const scale = Math.min(1, Math.max(0, Number(width) / data.length - 4) / 56); + return + +
+ {row.weapon.name} + R{row.refinement} +
+
+
; + }} /> +
{chartConfig[name as keyof typeof chartConfig]?.label ?? name} {value}%
} />} /> } /> - {hasPersonal && - typeof value === "number" ? `${value.toLocaleString("th-TH", { maximumFractionDigits: 2 })}%` : ""} className="fill-foreground text-xs" /> + {hasPersonal && + typeof value === "number" ? `${value.toLocaleString("th-TH", { maximumFractionDigits: 2 })}%` : ""} className="fill-foreground text-xs" /> } - {hasOverall && - typeof value === "number" ? `${value.toLocaleString("th-TH", { maximumFractionDigits: 2 })}%` : ""} className="fill-foreground text-xs" /> + {hasOverall && + typeof value === "number" ? `${value.toLocaleString("th-TH", { maximumFractionDigits: 2 })}%` : ""} className="fill-foreground text-xs" /> }