feat ; adding chart viewing type to weapon
This commit is contained in:
+2
-2
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user