feat ; adding chart viewing type to weapon
This commit is contained in:
+2
-2
@@ -113,7 +113,7 @@ export default function ThanksPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pb-4 sm:max-w-md sm:py-4">
|
<CardContent className="pb-4 sm:max-w-md sm:py-4">
|
||||||
<p className="leading-6 text-muted-foreground">
|
<p className="leading-6 text-muted-foreground">
|
||||||
ขอบคุณที่สนับสนุนระบบ Hosting ให้ Buzz Guide ออนไลน์และพร้อมใช้งาน
|
ขอบคุณที่สนับสนุนระบบ Hosting ที่สามารถให้เว็บออนไลน์และพร้อมใช้งาน
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="justify-center sm:self-stretch sm:border-t-0 sm:border-l">
|
<CardFooter className="justify-center sm:self-stretch sm:border-t-0 sm:border-l">
|
||||||
@@ -151,7 +151,7 @@ export default function ThanksPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-center text-muted-foreground">
|
<p className="text-center text-muted-foreground">
|
||||||
ขอบคุณที่เป็นส่วนสำคัญของ Buzz Guide
|
ขอบคุณที่เป็นส่วนสำคัญของเว็บไซด์นี้
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { ArrowUpRightIcon, ChevronLeftIcon } from "lucide-react";
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardAction,
|
CardAction,
|
||||||
@@ -28,6 +27,8 @@ import { ScrollableNav } from "@/components/public/scrollable-nav";
|
|||||||
import { TeamArtifactIcon, TeamSearch } from "@/components/public/team-search";
|
import { TeamArtifactIcon, TeamSearch } from "@/components/public/team-search";
|
||||||
import { GuideImageExport } from "@/components/public/guide-image-export";
|
import { GuideImageExport } from "@/components/public/guide-image-export";
|
||||||
import { WeaponImageExport } from "@/components/public/weapon-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 { ArtifactImageExport } from "@/components/public/artifact-image-export";
|
||||||
import { GuideExportBranding } from "@/components/public/guide-export-branding";
|
import { GuideExportBranding } from "@/components/public/guide-export-branding";
|
||||||
import { ConstellationImage } from "@/components/public/constellation-image";
|
import { ConstellationImage } from "@/components/public/constellation-image";
|
||||||
@@ -277,10 +278,6 @@ function GuideOverview({
|
|||||||
className="size-full"
|
className="size-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" render={<a href={youtubeVideo.watchUrl} target="_blank" rel="noopener noreferrer" />} nativeButton={false}>
|
|
||||||
ดูวิดีโอต้นฉบับบน YouTube
|
|
||||||
<ArrowUpRightIcon data-icon="inline-end" />
|
|
||||||
</Button>
|
|
||||||
</section>
|
</section>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -438,6 +435,7 @@ function WeaponSectionContent({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<WeaponDisplayModeProvider>
|
||||||
<ConditionSwitcher
|
<ConditionSwitcher
|
||||||
label="เลือกเงื่อนไขอาวุธ"
|
label="เลือกเงื่อนไขอาวุธ"
|
||||||
labels={weaponGroups.map((group, index) => group.name || `Condition ${index + 1}`)}
|
labels={weaponGroups.map((group, index) => group.name || `Condition ${index + 1}`)}
|
||||||
@@ -459,6 +457,7 @@ function WeaponSectionContent({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{group.note ? <p className="whitespace-pre-wrap text-muted-foreground">{group.note}</p> : null}
|
{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
|
<div
|
||||||
className={cn("grid gap-3", useCompactExportGrid && "grid-cols-2")}
|
className={cn("grid gap-3", useCompactExportGrid && "grid-cols-2")}
|
||||||
>
|
>
|
||||||
@@ -598,9 +597,11 @@ function WeaponSectionContent({
|
|||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</WeaponDisplayMode>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</ConditionSwitcher>
|
</ConditionSwitcher>
|
||||||
|
</WeaponDisplayModeProvider>
|
||||||
</section>
|
</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