feat ; adding chart viewing type to weapon
CI / Verify (push) Successful in 1m40s
CI / Build immutable images and deploy (push) Successful in 4m46s

This commit is contained in:
2026-09-13 17:12:23 +07:00 Unverified
parent 691a14ca09
commit c48b1a962d
4 changed files with 165 additions and 7 deletions
+2 -2
View File
@@ -113,7 +113,7 @@ export default function ThanksPage() {
</CardHeader>
<CardContent className="pb-4 sm:max-w-md sm:py-4">
<p className="leading-6 text-muted-foreground">
ขอบคุณที่สนับสนุนระบบ Hosting ให้ Buzz Guide ออนไลน์และพร้อมใช้งาน
ขอบคุณที่สนับสนุนระบบ Hosting ที่สามารถให้เว็บออนไลน์และพร้อมใช้งาน
</p>
</CardContent>
<CardFooter className="justify-center sm:self-stretch sm:border-t-0 sm:border-l">
@@ -151,7 +151,7 @@ export default function ThanksPage() {
</CardHeader>
<CardContent>
<p className="text-center text-muted-foreground">
ขอบคุณที่เป็นส่วนสำคัญของ Buzz Guide
ขอบคุณที่เป็นส่วนสำคัญของเว็บไซด์นี้
</p>
</CardContent>
<CardFooter>
+6 -5
View File
@@ -4,7 +4,6 @@ import { ArrowUpRightIcon, ChevronLeftIcon } from "lucide-react";
import type { ReactNode } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardAction,
@@ -28,6 +27,8 @@ import { ScrollableNav } from "@/components/public/scrollable-nav";
import { TeamArtifactIcon, TeamSearch } from "@/components/public/team-search";
import { GuideImageExport } from "@/components/public/guide-image-export";
import { WeaponImageExport } from "@/components/public/weapon-image-export";
import { WeaponChart } from "@/components/public/weapon-chart";
import { WeaponDisplayMode, WeaponDisplayModeProvider } from "@/components/public/weapon-display-mode";
import { ArtifactImageExport } from "@/components/public/artifact-image-export";
import { GuideExportBranding } from "@/components/public/guide-export-branding";
import { ConstellationImage } from "@/components/public/constellation-image";
@@ -277,10 +278,6 @@ function GuideOverview({
className="size-full"
/>
</div>
<Button variant="outline" render={<a href={youtubeVideo.watchUrl} target="_blank" rel="noopener noreferrer" />} nativeButton={false}>
ดูวิดีโอต้นฉบับบน YouTube
<ArrowUpRightIcon data-icon="inline-end" />
</Button>
</section>
))}
</div>
@@ -438,6 +435,7 @@ function WeaponSectionContent({
) : null}
</div>
</header>
<WeaponDisplayModeProvider>
<ConditionSwitcher
label="เลือกเงื่อนไขอาวุธ"
labels={weaponGroups.map((group, index) => group.name || `Condition ${index + 1}`)}
@@ -459,6 +457,7 @@ function WeaponSectionContent({
) : null}
</div>
{group.note ? <p className="whitespace-pre-wrap text-muted-foreground">{group.note}</p> : null}
<WeaponDisplayMode exporting={exporting} chart={!exporting ? <WeaponChart rows={group.rows} /> : null}>
<div
className={cn("grid gap-3", useCompactExportGrid && "grid-cols-2")}
>
@@ -598,9 +597,11 @@ function WeaponSectionContent({
</Card>
))}
</div>
</WeaponDisplayMode>
</div>
))}
</ConditionSwitcher>
</WeaponDisplayModeProvider>
</section>
);
}
+103
View File
@@ -0,0 +1,103 @@
"use client";
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart";
import type { PublicGuide } from "@/lib/guides/queries";
const chartConfig = {
overall: { label: "Overall · ดาเมจทั้งทีม", color: "var(--chart-1)" },
personal: { label: "Personal · ดาเมจส่วนตัว", color: "var(--chart-2)" },
} satisfies ChartConfig;
function percentage(value: string | null) {
if (!value?.trim()) return null;
const number = Number(value);
// Zero is displayed as an unavailable metric in the weapon cards.
return Number.isFinite(number) && number !== 0 ? number : 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}`,
imageUrl: row.weapon.imageUrl,
overall: percentage(row.overallPercent),
personal: percentage(row.personalPercent),
}));
const hasOverall = data.some((row) => row.overall !== null);
const hasPersonal = data.some((row) => row.personal !== null);
const singleMetric = !hasOverall || !hasPersonal;
if (!hasOverall && !hasPersonal) return null;
return (
<Card className="min-w-0" size={singleMetric ? "sm" : "default"}>
<CardHeader>
<CardTitle>เปรียบเทียบดาเมจอาวุธ</CardTitle>
<CardDescription>เปอร์เซ็นต์ดาเมจของอาวุธทั้งหมดใน Condition นี้</CardDescription>
</CardHeader>
<CardContent className="min-w-0">
<ChartContainer
config={chartConfig}
className="min-w-0 aspect-auto w-full"
style={{
height: singleMetric ? 300 : 400,
}}
aria-label="กราฟเปรียบเทียบเปอร์เซ็นต์ดาเมจอาวุธ"
>
<BarChart accessibilityLayer data={data} margin={{ right: 16 }}>
<CartesianGrid vertical={false} />
<YAxis type="number" tickLine={false} axisLine={false} tickFormatter={(value) => `${value}%`} />
<XAxis
dataKey="name"
type="category"
height={60}
tickLine={false}
axisLine={false}
interval={0}
tick={({ x, y, payload, width }) => {
const weapon = data.find((row) => row.name === payload.value);
if (!weapon) return <g />;
const size = Math.max(8, Math.min(40, Number(width) / data.length - 4 || 32));
return (
<g transform={`translate(${x},${y})`}>
<title>{weapon.name}</title>
<image
href={weapon.imageUrl}
x={-size / 2}
y={4}
width={size}
height={size}
preserveAspectRatio="xMidYMid meet"
role="img"
aria-label={weapon.name}
/>
</g>
);
}}
/>
<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="font-mono font-medium">{value}%</span>
</div>
)} />
} />
<ChartLegend content={<ChartLegendContent className="flex-wrap" />} />
{hasPersonal ? <Bar dataKey="personal" fill="var(--color-personal)" radius={4} maxBarSize={32} /> : null}
{hasOverall ? <Bar dataKey="overall" fill="var(--color-overall)" radius={4} maxBarSize={32} /> : null}
</BarChart>
</ChartContainer>
</CardContent>
</Card>
);
}
+54
View File
@@ -0,0 +1,54 @@
"use client";
import { createContext, useContext, useState, type ReactNode } from "react";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
const WeaponModeContext = createContext<{
mode: string;
setMode: (mode: string) => void;
} | null>(null);
export function WeaponDisplayModeProvider({ children }: { children: ReactNode }) {
const [mode, setMode] = useState("normal");
return (
<WeaponModeContext.Provider value={{ mode, setMode }}>
{children}
</WeaponModeContext.Provider>
);
}
export function WeaponDisplayMode({
children,
chart,
exporting = false,
}: {
children: ReactNode;
chart: ReactNode;
exporting?: boolean;
}) {
const context = useContext(WeaponModeContext);
if (!context) throw new Error("WeaponDisplayMode requires WeaponDisplayModeProvider");
const { mode, setMode } = context;
if (exporting) return children;
return (
<div className="flex min-w-0 flex-col gap-3">
<ToggleGroup
aria-label="Weapon display mode"
variant="outline"
value={[mode]}
onValueChange={(values) => {
if (values[0]) setMode(values[0]);
}}
>
<ToggleGroupItem value="normal">List</ToggleGroupItem>
<ToggleGroupItem value="chart">Chart</ToggleGroupItem>
</ToggleGroup>
{mode === "chart" ? chart : children}
</div>
);
}