feat : create share function item with rareity bg

This commit is contained in:
2026-10-11 20:17:16 +07:00 Unverified
parent c84c691fe9
commit 99c129eb26
12 changed files with 185 additions and 158 deletions
+2 -3
View File
@@ -10,6 +10,7 @@ import { toast } from "sonner";
import { createStructuredGuide } from "@/app/admin/actions";
import { MediaUploader, type UploadedMedia } from "@/components/admin/media-uploader";
import { CatalogItemImage } from "@/components/catalog-item-image";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
@@ -18,8 +19,6 @@ import { InputGroup, InputGroupAddon } from "@/components/ui/input-group";
import { ClearableInputGroupInput as InputGroupInput } from "@/components/admin/ui/clearable-input";
import { Spinner } from "@/components/ui/spinner";
import { Textarea } from "@/components/ui/textarea";
import { characterRarityGradientClass } from "@/lib/catalog/rarity";
import { cn } from "@/lib/utils";
type Character = { key: string; name: string; aliases: string[]; element: string | null; rarity: number | null; imageUrl: string };
@@ -86,7 +85,7 @@ export function CreateGuideFlow({ characters, existingKeys }: { characters: Char
const disabled = existing.has(character.key);
return (
<Card key={character.key} className="overflow-hidden py-0">
<div className={cn("relative aspect-square", characterRarityGradientClass(character.key, character.rarity))}><Image src={character.imageUrl} alt={character.name} fill className="object-cover" /></div>
<CatalogItemImage item={character} kind="character" alt={character.name} className="aspect-square" imageClassName="object-cover" />
<CardHeader className="pt-4">
<CardTitle>{character.name}</CardTitle>
<CardDescription>{character.element ?? "Unknown element"}</CardDescription>
+3 -5
View File
@@ -13,6 +13,7 @@ import { deleteGuideExtra, saveGuideExtra } from "@/app/admin/actions";
import { SaveStatus } from "@/components/admin/save-status";
import { DeleteConfirmationDialog } from "@/components/admin/ui/delete-confirmation-dialog";
import { CatalogDropdown } from "@/components/admin/ui/catalog-select";
import { CatalogItemImage } from "@/components/catalog-item-image";
import { MediaUploader } from "@/components/admin/media-uploader";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
@@ -26,7 +27,6 @@ import { Skeleton } from "@/components/ui/skeleton";
import { useManualSave } from "@/hooks/use-manual-save";
import { adminDraftKey } from "@/lib/editor/local-draft";
import { extraSectionSchema } from "@/lib/guides/schemas";
import { rarityGradientClass } from "@/lib/catalog/rarity";
import { finalizePercentInput, normalizePercentInput } from "@/lib/guides/percent-input";
import { formatGuideValidationIssues } from "@/lib/guides/validation-messages";
import { normalizeExtraRichText } from "@/lib/rich-text";
@@ -156,9 +156,7 @@ export function ExtraCatalogItemsEditor({
<Card size="sm">
<CardContent className="flex items-center gap-3">
{item ? (
<div className={cn("relative size-12 shrink-0 overflow-hidden rounded-md", rarityGradientClass(item.rarity))}>
<Image src={item.imageUrl} alt="" fill sizes="48px" className="object-contain" />
</div>
<CatalogItemImage item={item} kind={reference.kind} alt="" className="size-12 shrink-0 rounded-md" sizes="48px" />
) : null}
<div className="min-w-0 flex-1">
<p className="truncate font-medium">{label}</p>
@@ -205,7 +203,7 @@ export function ExtraCatalogItemsEditor({
placeholder={`เลือก${labels[kind]}`}
showSelectedImage
rarityBackground
characterBackground={kind === "character"}
itemKind={kind}
/>
</div>
</div>
+14 -8
View File
@@ -15,6 +15,7 @@ import { ArtifactSelect, CatalogDropdown, CharacterSelect, WeaponSelect } from "
import { ConstellationSelect, RefinementSelect } from "@/components/admin/ui/option-select";
import { ScrollableNav } from "@/components/public/scrollable-nav";
import { TeamArtifactIcon } from "@/components/public/team-search";
import { CatalogItemImage } from "@/components/catalog-item-image";
import { SimpleTooltip } from "@/components/tooltip";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@@ -37,7 +38,6 @@ import { adminDraftKey } from "@/lib/editor/local-draft";
import { teamSectionSchema } from "@/lib/guides/schemas";
import { elementBackgroundColor } from "@/lib/catalog/element";
import { supportsWeaponType } from "@/lib/catalog/lunaris";
import { characterRarityGradientClass, rarityGradientClass } from "@/lib/catalog/rarity";
import { isCustomTravelerKey } from "@/lib/catalog/travelers";
import { toTeamDraft } from "@/lib/guides/team-draft";
import { applyPresetFields, applyTeamFields, cloneTeamImports, filterTeamImportsByName, findInvalidTeamCatalogReference, findUnsupportedTeamWeapon, limitPresets, parseTeamPresets, parseTeamTransfer, serializeTeamTransfer, teamConstellationLabel, teamRefinementLabel, teamTransferFileName, teamPresetCookieKey, teamPresetStorageKey, type TeamApplySelection, type TeamMemberApplyField, type TeamToolMember, type TeamPreset, type TeamToolValue } from "@/lib/guides/team-tools";
@@ -224,14 +224,20 @@ export function TeamImportPreviewCard({
return (
<div key={`${member.characterKey}-${index}`} className="min-w-0 text-center">
<div className="flex items-center justify-center gap-2">
<div className={cn("relative size-16 shrink-0 overflow-hidden rounded-lg bg-muted", characterRarityGradientClass(member.characterKey, isCustomTravelerKey(member.characterKey) ? 5 : character?.rarity))}>
{character ? <Image src={character.imageUrl} alt={character.name} fill sizes="64px" className="object-contain" /> : null}
</div>
<CatalogItemImage
item={{
key: member.characterKey,
imageUrl: character?.imageUrl ?? null,
rarity: isCustomTravelerKey(member.characterKey) ? 5 : character?.rarity,
}}
kind="character"
alt={character?.name ?? ""}
className="size-16 shrink-0 rounded-lg bg-muted"
sizes="64px"
/>
<div className="flex flex-col gap-1">
{weapon ? (
<div className={cn("relative size-7 overflow-hidden rounded-md", rarityGradientClass(weapon.rarity))}>
<Image src={weapon.imageUrl} alt={weapon.name} fill sizes="28px" className="object-contain p-0.5" />
</div>
<CatalogItemImage item={weapon} kind="weapon" alt={weapon.name} className="size-7 rounded-md" imageClassName="p-0.5" sizes="28px" />
) : null}
{artifact || secondArtifact ? (
<TeamArtifactIcon
@@ -1545,7 +1551,7 @@ export function TeamEditor({
label="เลือกตัวละครที่อยู่ใน Team"
showSelectedImage
rarityBackground
characterBackground
itemKind="character"
/>
</Field>
<Field orientation="horizontal">
+2 -16
View File
@@ -1,6 +1,5 @@
"use client";
import Image from "@/components/ui/resilient-image";
import { GripVerticalIcon, PlusIcon, Trash2Icon } from "lucide-react";
import { useState, type ReactNode } from "react";
import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core";
@@ -10,6 +9,7 @@ import { saveWeaponSection } from "@/app/admin/actions";
import { SaveStatus } from "@/components/admin/save-status";
import { DeleteConfirmationDialog } from "@/components/admin/ui/delete-confirmation-dialog";
import { WeaponSelect } from "@/components/admin/ui/catalog-select";
import { CatalogItemImage } from "@/components/catalog-item-image";
import { Button } from "@/components/ui/button";
import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
@@ -20,7 +20,6 @@ import { useManualSave } from "@/hooks/use-manual-save";
import { adminDraftKey } from "@/lib/editor/local-draft";
import { weaponSectionSchema } from "@/lib/guides/schemas";
import { supportsWeaponType } from "@/lib/catalog/lunaris";
import { rarityGradientClass } from "@/lib/catalog/rarity";
import { finalizePercentInput, normalizePercentInput } from "@/lib/guides/percent-input";
import { formatGuideValidationIssues } from "@/lib/guides/validation-messages";
import { cn } from "@/lib/utils";
@@ -255,20 +254,7 @@ export function WeaponEditor({ guide, weapons, characters, initial }: EditorBase
<CardHeader>
<CardTitle className="flex items-center gap-3">
{selectedWeapon ? (
<div
className={cn(
"relative size-10 shrink-0 overflow-hidden rounded-lg",
rarityGradientClass(selectedWeapon.rarity),
)}
>
<Image
src={selectedWeapon.imageUrl}
alt=""
fill
sizes="40px"
className="object-contain"
/>
</div>
<CatalogItemImage item={selectedWeapon} kind="weapon" alt="" className="size-10 shrink-0 rounded-lg" sizes="40px" />
) : null}
<span>อันดับ {rowIndex + 1}</span>
</CardTitle>
+3 -9
View File
@@ -2,7 +2,6 @@
import { searchItems } from "@/lib/search";
import Image from "@/components/ui/resilient-image";
import { SearchIcon, Trash2Icon } from "lucide-react";
import { useMemo, useState, useTransition } from "react";
import { useRouter } from "next/navigation";
@@ -18,8 +17,8 @@ import { ClearableInput as Input } from "@/components/admin/ui/clearable-input";
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Spinner } from "@/components/ui/spinner";
import { CatalogItemImage } from "@/components/catalog-item-image";
import type { GlossaryKind } from "@/lib/glossary";
import { characterRarityGradientClass, rarityGradientClass } from "@/lib/catalog/rarity";
import { cn } from "@/lib/utils";
type CatalogItem = { key: string; name: string; imageUrl: string; rarity?: number | null };
@@ -113,7 +112,7 @@ export function GlossaryManager({
onValueChange={(key) => setItem(catalogs[kind].find((value) => value.key === key) ?? null)}
placeholder="ค้นหารายการ…"
rarityBackground
characterBackground={kind === "character"}
itemKind={kind}
/>
</Field>
<Field>
@@ -153,12 +152,7 @@ export function GlossaryManager({
return (
<div key={entry.id} className="flex items-center gap-3 rounded-lg border p-3">
{catalogItem ? (
<span className={cn(
"relative size-10 shrink-0 overflow-hidden rounded-md bg-muted",
entry.kind === "character" ? characterRarityGradientClass(catalogItem.key, catalogItem.rarity) : rarityGradientClass(catalogItem.rarity),
)}>
<Image src={catalogItem.imageUrl} alt="" fill sizes="40px" className="object-contain" />
</span>
<CatalogItemImage item={catalogItem} kind={entry.kind as GlossaryKind} alt="" className="size-10 shrink-0 rounded-md bg-muted" sizes="40px" />
) : null}
<div className="min-w-0 flex-1">
<p className="font-medium">{entry.alias}</p>
+1 -1
View File
@@ -221,7 +221,7 @@ function ItemPicker({
placeholder={`Search ${kind}…`}
emptyLabel="No matches"
rarityBackground
characterBackground={kind === "character"}
itemKind={kind}
/>
</Field>
) : null}
+42 -34
View File
@@ -1,9 +1,9 @@
"use client";
import Image from "@/components/ui/resilient-image";
import { CircleHelpIcon } from "lucide-react";
import type { ReactNode } from "react";
import { CatalogItemImage, type CatalogItemKind } from "@/components/catalog-item-image";
import { TeamArtifactIcon } from "@/components/public/team-search";
import {
ComboboxContent,
@@ -14,7 +14,6 @@ import {
ComboboxTrigger,
FuzzyCombobox,
} from "@/components/ui/combobox";
import { characterRarityGradientClass, rarityGradientClass } from "@/lib/catalog/rarity";
import { cn } from "@/lib/utils";
export interface AdminCatalogItem {
@@ -51,7 +50,7 @@ type CommonSelectProps<Item extends AdminCatalogItem> = {
emptyLabel?: string;
invalid?: boolean;
rarityBackground?: boolean;
characterBackground?: boolean;
itemKind?: CatalogItemKind;
searchNames?: (item: Item) => string[];
};
@@ -70,8 +69,8 @@ export function CatalogDropdown<Item extends AdminCatalogItem>({
placeholder,
emptyLabel = "ไม่พบรายการ",
invalid = false,
rarityBackground = false,
characterBackground = false,
rarityBackground = true,
itemKind = "weapon",
showSelectedImage = false,
showClear = true,
label,
@@ -82,14 +81,14 @@ export function CatalogDropdown<Item extends AdminCatalogItem>({
return (
<div className="flex min-w-0 items-center gap-2">
{showSelectedImage && selected ? (
<div
className={cn(
"relative size-10 shrink-0 overflow-hidden rounded-md",
rarityBackground && (characterBackground ? characterRarityGradientClass(selected.key, selected.rarity) : rarityGradientClass(selected.rarity)),
)}
>
<Image src={selected.imageUrl} alt="" fill sizes="40px" className="object-contain" />
</div>
<CatalogItemImage
item={selected}
kind={itemKind}
alt=""
rarityBackground={rarityBackground}
className="size-10 shrink-0 rounded-md"
sizes="40px"
/>
) : null}
<div className="min-w-0 flex-1">
<FuzzyCombobox
@@ -113,7 +112,7 @@ export function CatalogDropdown<Item extends AdminCatalogItem>({
<ComboboxList>
{(item) => (
<ComboboxItem key={item.key} value={item}>
<CatalogOptionImage item={item} rarityBackground={rarityBackground} characterBackground={characterBackground} />
<CatalogOptionImage item={item} itemKind={itemKind} rarityBackground={rarityBackground} />
<span className="whitespace-nowrap">{item.name}</span>
</ComboboxItem>
)}
@@ -142,7 +141,7 @@ function CatalogSlotSelect<Item extends AdminCatalogItem>({
emptyLabel = "ไม่พบรายการ",
invalid = false,
rarityBackground = true,
characterBackground = false,
itemKind = "weapon",
searchNames = (item) => [item.name, ...(item.aliases ?? [])],
className,
placeholderIcon,
@@ -164,12 +163,19 @@ function CatalogSlotSelect<Item extends AdminCatalogItem>({
aria-invalid={invalid}
className={cn(
"relative flex aspect-square w-full items-center justify-center overflow-hidden rounded-md border bg-muted p-0 outline-none focus-visible:ring-2 focus-visible:ring-ring aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 [&>svg]:hidden",
rarityBackground && selected && (characterBackground ? characterRarityGradientClass(selected.key, selected.rarity) : rarityGradientClass(selected.rarity)),
className,
)}
>
{selected ? (
<Image src={selected.imageUrl} alt="" fill sizes="96px" className="object-contain p-0.5" />
<CatalogItemImage
item={selected}
kind={itemKind}
alt=""
rarityBackground={rarityBackground}
className="absolute inset-0"
imageClassName="p-0.5"
sizes="96px"
/>
) : (
<span className="flex size-full items-center justify-center text-muted-foreground">
{placeholderIcon ?? <CircleHelpIcon className="size-8" aria-hidden="true" />}
@@ -182,7 +188,7 @@ function CatalogSlotSelect<Item extends AdminCatalogItem>({
<ComboboxList>
{(item) => (
<ComboboxItem key={item.key} value={item}>
<CatalogOptionImage item={item} rarityBackground={rarityBackground} characterBackground={characterBackground} />
<CatalogOptionImage item={item} itemKind={itemKind} rarityBackground={rarityBackground} />
<span className="whitespace-nowrap">{item.name}</span>
</ComboboxItem>
)}
@@ -210,6 +216,7 @@ function DomainCatalogSelect<Item extends AdminCatalogItem>({
showSelectedImage,
placeholderIcon,
rarityBackground = true,
itemKind = "weapon",
...props
}: DomainSelectProps<Item>) {
if (presentation === "slot") {
@@ -217,21 +224,22 @@ function DomainCatalogSelect<Item extends AdminCatalogItem>({
<CatalogSlotSelect
{...props}
rarityBackground={rarityBackground}
itemKind={itemKind}
label={label ?? props.placeholder}
className={className}
placeholderIcon={placeholderIcon}
/>
);
}
return <CatalogDropdown {...props} label={label} rarityBackground={rarityBackground} showSelectedImage={showSelectedImage} />;
return <CatalogDropdown {...props} label={label} rarityBackground={rarityBackground} itemKind={itemKind} showSelectedImage={showSelectedImage} />;
}
export function CharacterSelect<Item extends AdminCatalogItem>(props: DomainSelectProps<Item>) {
return <DomainCatalogSelect {...props} characterBackground />;
return <DomainCatalogSelect {...props} itemKind="character" />;
}
export function WeaponSelect<Item extends AdminCatalogItem>(props: DomainSelectProps<Item>) {
return <DomainCatalogSelect {...props} />;
return <DomainCatalogSelect {...props} itemKind="weapon" />;
}
type SingleArtifactSelectProps<Item extends AdminCatalogItem> = DomainSelectProps<Item> & {
@@ -240,7 +248,7 @@ type SingleArtifactSelectProps<Item extends AdminCatalogItem> = DomainSelectProp
type MultipleArtifactSelectProps<Item extends AdminCatalogItem> = Omit<
CommonSelectProps<Item>,
"rarityBackground"
"rarityBackground" | "itemKind"
> & {
multiple: true;
values: string[];
@@ -253,7 +261,7 @@ type MultipleArtifactSelectProps<Item extends AdminCatalogItem> = Omit<
export function ArtifactSelect<Item extends AdminCatalogItem>(
props: SingleArtifactSelectProps<Item> | MultipleArtifactSelectProps<Item>,
) {
if (!props.multiple) return <DomainCatalogSelect {...props} />;
if (!props.multiple) return <DomainCatalogSelect {...props} itemKind="artifact" />;
const {
items,
@@ -316,7 +324,7 @@ export function ArtifactSelect<Item extends AdminCatalogItem>(
value={item}
disabled={selected.length >= max && !selected.some((candidate) => candidate.key === item.key)}
>
<CatalogOptionImage item={item} rarityBackground />
<CatalogOptionImage item={item} itemKind="artifact" rarityBackground />
<span className="whitespace-nowrap">{item.name}</span>
</ComboboxItem>
)}
@@ -328,21 +336,21 @@ export function ArtifactSelect<Item extends AdminCatalogItem>(
function CatalogOptionImage({
item,
itemKind,
rarityBackground,
characterBackground = false,
}: {
item: AdminCatalogItem;
itemKind: CatalogItemKind;
rarityBackground: boolean;
characterBackground?: boolean;
}) {
return (
<div
className={cn(
"relative size-8 shrink-0 overflow-hidden rounded-md",
rarityBackground && (characterBackground ? characterRarityGradientClass(item.key, item.rarity) : rarityGradientClass(item.rarity)),
)}
>
<Image src={item.imageUrl} alt="" fill sizes="32px" className="object-contain" />
</div>
<CatalogItemImage
item={item}
kind={itemKind}
alt=""
rarityBackground={rarityBackground}
className="size-8 shrink-0 rounded-md"
sizes="32px"
/>
);
}
+54
View File
@@ -0,0 +1,54 @@
import Image from "@/components/ui/resilient-image";
import { characterRarityGradientClass, rarityGradientClass } from "@/lib/catalog/rarity";
import { cn } from "@/lib/utils";
export type CatalogItemKind = "character" | "weapon" | "artifact";
export type CatalogItemImageData = {
key: string;
imageUrl: string | null;
rarity?: number | null;
};
export function CatalogItemImage({
item,
kind,
alt,
rarityBackground = true,
className,
imageClassName,
sizes,
loading,
title,
}: {
item: CatalogItemImageData;
kind: CatalogItemKind;
alt: string;
rarityBackground?: boolean;
className?: string;
imageClassName?: string;
sizes?: string;
loading?: "eager" | "lazy";
title?: string;
}) {
const backgroundClass = rarityBackground
? kind === "character"
? characterRarityGradientClass(item.key, item.rarity)
: rarityGradientClass(item.rarity)
: undefined;
return (
<div title={title} className={cn("relative overflow-hidden", backgroundClass, className)}>
{item.imageUrl ? (
<Image
src={item.imageUrl}
alt={alt}
fill
sizes={sizes}
loading={loading}
className={cn("object-contain", imageClassName)}
/>
) : null}
</div>
);
}
+7 -11
View File
@@ -1,21 +1,17 @@
import Image from "@/components/ui/resilient-image";
import { ArrowUpRightIcon } from "lucide-react";
import type { ReactNode } from "react";
import { Badge } from "@/components/ui/badge";
import { CatalogItemImage, type CatalogItemKind } from "@/components/catalog-item-image";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Kbd } from "@/components/ui/kbd";
import { elementBackgroundColor } from "@/lib/catalog/element";
import { characterRarityGradientClass, rarityGradientClass } from "@/lib/catalog/rarity";
import { hasCustomStats, type CommissionRequest } from "@/lib/commission/request";
import { cn } from "@/lib/utils";
type Item = { key: string; name: string; imageUrl: string; rarity?: number | null; element?: string | null };
type Catalog = { characters: Item[]; weapons: Item[]; artifacts: Item[] };
function ItemImage({ item, size = "small", character = false }: { item: Item; size?: "small" | "large"; character?: boolean }) {
return <div title={item.name} className={cn("relative aspect-square min-w-0 overflow-hidden rounded-lg", character ? characterRarityGradientClass(item.key, item.rarity) : rarityGradientClass(item.rarity))}>
<Image src={item.imageUrl} alt={item.name} fill sizes={size === "large" ? "160px" : "64px"} className="object-contain" />
</div>;
function ItemImage({ item, size = "small", kind = "weapon" }: { item: Item; size?: "small" | "large"; kind?: CatalogItemKind }) {
return <CatalogItemImage item={item} kind={kind} alt={item.name} title={item.name} className="aspect-square min-w-0 rounded-lg" sizes={size === "large" ? "160px" : "64px"} />;
}
function LunarisItemLink({ item, kind, children }: {
@@ -25,7 +21,7 @@ function LunarisItemLink({ item, kind, children }: {
target="_blank" rel="noreferrer"
aria-label={`เปิดข้อมูล${kind === "weapon" ? "อาวุธ" : "อาร์ติแฟกต์"} ${item.name} บน Lunaris`}
className="block min-w-0 rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-ring">
<div className="relative"><ItemImage item={item} />
<div className="relative"><ItemImage item={item} kind={kind} />
<Kbd className="absolute top-0 right-0 h-3 min-w-3 bg-transparent! p-0 text-white">
<ArrowUpRightIcon aria-hidden="true" />
</Kbd>
@@ -49,7 +45,7 @@ function CommissionTeamCard({ team, index, catalog }: { team: CommissionRequest[
const artifact = artifacts.get(member.artifactKey);
return <div key={memberIndex} className="min-w-0 rounded-lg p-1 lg:p-1.5" style={{ backgroundColor: elementBackgroundColor(character?.element) }}>
<div className="grid grid-cols-1 gap-1 lg:grid-cols-[2fr_1fr]">
{character ? <ItemImage item={character} size="large" character /> : <div className="aspect-square rounded-lg bg-muted" />}
{character ? <ItemImage item={character} size="large" kind="character" /> : <div className="aspect-square rounded-lg bg-muted" />}
<div className="grid min-w-0 grid-cols-2 gap-0.5 lg:grid-cols-1 lg:grid-rows-2 lg:gap-1">
{weapon ? <LunarisItemLink item={weapon} kind="weapon" /> : <div title={member.weaponKey} className="aspect-square rounded-lg bg-muted" />}
{artifact ? <LunarisItemLink item={artifact} kind="artifact" /> : <div title={member.artifactKey} className="aspect-square rounded-lg bg-muted" />}
@@ -109,7 +105,7 @@ export function CommissionRequestSummary({ request, catalog, amountBaht }: {
: <p key={key} className="w-16 wrap-break-word text-xs">{key}</p>;
})}
</div>
{character ? <div className="w-16 min-w-0"><ItemImage item={character} size="large" character /><p className="mt-1 truncate text-center text-xs" title={character.name}>{character.name}</p></div> : null}
{character ? <div className="w-16 min-w-0"><ItemImage item={character} size="large" kind="character" /><p className="mt-1 truncate text-center text-xs" title={character.name}>{character.name}</p></div> : null}
{set.customStats !== undefined && <div className="col-span-2 flex flex-col gap-1">
<Badge variant="secondary">Custom stats ของ {character?.name ?? set.characterKey}</Badge>
<p className="whitespace-pre-wrap wrap-break-word text-sm">{set.customStats}</p>
@@ -125,7 +121,7 @@ export function CommissionRequestSummary({ request, catalog, amountBaht }: {
</CardHeader>
<CardContent className="grid grid-cols-[minmax(0,1fr)_64px] items-center gap-3">
<div className="flex min-w-0 flex-wrap gap-2">{set.levels.map((level) => <Badge key={level} variant="outline">C{level}</Badge>)}</div>
{character ? <div className="w-16 min-w-0"><ItemImage item={character} size="large" character /><p className="mt-1 truncate text-center text-xs" title={character.name}>{character.name}</p></div> : null}
{character ? <div className="w-16 min-w-0"><ItemImage item={character} size="large" kind="character" /><p className="mt-1 truncate text-center text-xs" title={character.name}>{character.name}</p></div> : null}
{set.customStats !== undefined && <div className="col-span-2 flex flex-col gap-1">
<Badge variant="secondary">Custom stats ของ {character?.name ?? set.characterKey}</Badge>
<p className="whitespace-pre-wrap wrap-break-word text-sm">{set.customStats}</p>
+11 -13
View File
@@ -5,6 +5,7 @@ import { ArrowUpRightIcon } from "lucide-react";
import { Fragment, type ReactNode } from "react";
import { Badge } from "@/components/ui/badge";
import { CatalogItemImage } from "@/components/catalog-item-image";
import { ComparisonContext } from "@/components/public/comparison-context";
import {
Card,
@@ -417,16 +418,15 @@ function WeaponSectionContent({
rarityGradientClass(row.weapon.rarity),
)}
>
<div className="relative size-20 shrink-0">
<Image
src={row.weapon.imageUrl}
alt={row.weapon.name}
fill
loading={exporting ? "eager" : undefined}
sizes="(min-width: 768px) 5rem, 4rem"
className="object-contain"
/>
</div>
<CatalogItemImage
item={row.weapon}
kind="weapon"
alt={row.weapon.name}
rarityBackground={false}
className="size-20 shrink-0"
loading={exporting ? "eager" : undefined}
sizes="(min-width: 768px) 5rem, 4rem"
/>
<div className="flex min-w-0 flex-col gap-1">
<p
className={cn(
@@ -1273,9 +1273,7 @@ function ExtraSectionContent({
className="flex min-w-0 items-center gap-2 rounded-lg border bg-background/80 p-2 outline-none hover:bg-background focus-visible:ring-2 focus-visible:ring-ring"
>
{item.imageUrl ? (
<div className={cn("relative size-12 shrink-0 overflow-hidden rounded-md", rarityGradientClass(item.rarity))}>
<Image src={item.imageUrl} alt="" fill className="object-contain" />
</div>
<CatalogItemImage item={item} kind={item.kind} alt="" className="size-12 shrink-0 rounded-md" />
) : null}
<div className="min-w-0">
<p className="truncate text-sm font-medium">{item.name}</p>
+12 -8
View File
@@ -4,6 +4,7 @@ import Link from "next/link";
import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { buttonVariants } from "@/components/ui/button";
import { CatalogItemImage } from "@/components/catalog-item-image";
import { TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { StygianBossTabs, StygianNavigation } from "@/components/stygian/stygian-navigation";
import {
@@ -11,7 +12,6 @@ import {
StygianBossTab,
} from "@/components/stygian/stygian-boss-display";
import { elementIconFilter } from "@/lib/catalog/element";
import { characterRarityGradientClass, rarityGradientClass } from "@/lib/catalog/rarity";
import type { getCatalogOptions } from "@/lib/guides/queries";
import { stygianBossPositionLabels, stygianBossPositions, stygianLevels } from "@/lib/stygian/constants";
import type { getStygianSchedule } from "@/lib/stygian/repository";
@@ -52,12 +52,16 @@ function RecommendationIcon({ item, catalog, teamLayout = false }: { item: Item;
return (
<div className={cn("flex min-w-0 flex-col gap-1 rounded-lg border bg-card p-1", teamLayout && "w-full gap-2 sm:p-2")}>
<div className="flex flex-col gap-1">
<div className={cn("relative aspect-square overflow-hidden rounded-lg", characterRarityGradientClass(item.catalogKey, value.rarity))}>
{value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes={teamLayout ? "(min-width: 1280px) 128px, (min-width: 640px) 25vw, 50vw" : "128px"} className="object-contain" /> : null}
</div>
<CatalogItemImage
item={{ key: item.catalogKey ?? "", imageUrl: value.imageUrl, rarity: value.rarity }}
kind="character"
alt={value.name}
className="aspect-square rounded-lg"
sizes={teamLayout ? "(min-width: 1280px) 128px, (min-width: 640px) 25vw, 50vw" : "128px"}
/>
{hasEquipment ? <div className="grid grid-cols-2 gap-1">
{value.weapon ? <a href={`https://lunaris.moe/weapon/${encodeURIComponent(value.weapon.key)}`} target="_blank" rel="noreferrer" aria-label={`View ${value.weapon.name} on Lunaris`} title={value.weapon.name} className={cn("relative aspect-square overflow-hidden rounded-md border transition-opacity hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", rarityGradientClass(value.weapon.rarity))}><Image src={value.weapon.imageUrl} alt={value.weapon.name} fill sizes={teamLayout ? "(min-width: 1280px) 64px, (min-width: 640px) 12vw, 25vw" : "64px"} className="object-contain p-1" /></a> : null}
{value.artifact ? <a href={`https://lunaris.moe/artifact/${encodeURIComponent(value.artifact.key)}`} target="_blank" rel="noreferrer" aria-label={`View ${value.artifact.name} on Lunaris`} title={value.artifact.name} className={cn("relative aspect-square overflow-hidden rounded-md border transition-opacity hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", rarityGradientClass(value.artifact.rarity))}><Image src={value.artifact.imageUrl} alt={value.artifact.name} fill sizes={teamLayout ? "(min-width: 1280px) 64px, (min-width: 640px) 12vw, 25vw" : "64px"} className="object-contain p-1" /></a> : null}
{value.weapon ? <a href={`https://lunaris.moe/weapon/${encodeURIComponent(value.weapon.key)}`} target="_blank" rel="noreferrer" aria-label={`View ${value.weapon.name} on Lunaris`} title={value.weapon.name} className="relative aspect-square overflow-hidden rounded-md border transition-opacity hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"><CatalogItemImage item={value.weapon} kind="weapon" alt={value.weapon.name} className="absolute inset-0" imageClassName="p-1" sizes={teamLayout ? "(min-width: 1280px) 64px, (min-width: 640px) 12vw, 25vw" : "64px"} /></a> : null}
{value.artifact ? <a href={`https://lunaris.moe/artifact/${encodeURIComponent(value.artifact.key)}`} target="_blank" rel="noreferrer" aria-label={`View ${value.artifact.name} on Lunaris`} title={value.artifact.name} className="relative aspect-square overflow-hidden rounded-md border transition-opacity hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"><CatalogItemImage item={value.artifact} kind="artifact" alt={value.artifact.name} className="absolute inset-0" imageClassName="p-1" sizes={teamLayout ? "(min-width: 1280px) 64px, (min-width: 640px) 12vw, 25vw" : "64px"} /></a> : null}
</div> : null}
</div>
{teamLayout ? item.constellationLevel !== null || value.weapon && value.refinement ? <div className="flex justify-center gap-1">
@@ -70,8 +74,8 @@ function RecommendationIcon({ item, catalog, teamLayout = false }: { item: Item;
return (
<div className="flex min-w-0 flex-col items-center gap-1 text-center">
{item.kind === "weapon" || item.kind === "artifact" ? (
<a href={`https://lunaris.moe/${item.kind}/${encodeURIComponent(item.catalogKey ?? "")}`} target="_blank" rel="noreferrer" aria-label={`View ${value.name} on Lunaris`} title={value.name} className={cn("relative aspect-square w-full overflow-hidden rounded-lg border transition-opacity hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", rarityGradientClass(value.rarity))}>
{value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes="100px" className="object-contain" /> : null}
<a href={`https://lunaris.moe/${item.kind}/${encodeURIComponent(item.catalogKey ?? "")}`} target="_blank" rel="noreferrer" aria-label={`View ${value.name} on Lunaris`} title={value.name} className="relative aspect-square w-full overflow-hidden rounded-lg border transition-opacity hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring">
<CatalogItemImage item={{ key: item.catalogKey ?? "", imageUrl: value.imageUrl, rarity: value.rarity }} kind={item.kind} alt={value.name} className="absolute inset-0" sizes="100px" />
</a>
) : (
<div className="relative aspect-square w-full overflow-hidden rounded-lg border">
+34 -50
View File
@@ -1,7 +1,6 @@
"use client";
import Image from "@/components/ui/resilient-image";
import Link from "next/link";
import { ArrowUpRightIcon, SearchIcon, XIcon } from "lucide-react";
import { Fragment, useMemo, useState, type ReactNode } from "react";
@@ -9,6 +8,7 @@ import { Fragment, useMemo, useState, type ReactNode } from "react";
import { GuideImageExport } from "@/components/public/guide-image-export";
import { ConditionTabIcon } from "@/components/public/condition-switcher";
import { ExportText } from "@/components/public/export-language";
import { CatalogItemImage } from "@/components/catalog-item-image";
import { RotationHelp } from "@/components/public/rotation-help";
import { ScrollableNav } from "@/components/public/scrollable-nav";
import { Button } from "@/components/ui/button";
@@ -28,7 +28,7 @@ import { Separator } from "@/components/ui/separator";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { elementBackgroundColor } from "@/lib/catalog/element";
import { characterRarityGradientClass, rarityGradientClass } from "@/lib/catalog/rarity";
import { rarityGradientClass } from "@/lib/catalog/rarity";
import { isCustomTravelerKey } from "@/lib/catalog/travelers";
import type { PublicGuide } from "@/lib/guides/queries";
import { teamConstellationLabel, teamRefinementLabel } from "@/lib/guides/team-tools";
@@ -97,16 +97,16 @@ function ArtifactHalf({
sizes: string;
}) {
const content = (
<span className="relative block size-full">
<Image
src={artifact.imageUrl}
alt={linked ? artifact.name : ""}
fill
loading={eager ? "eager" : undefined}
sizes={sizes}
className="object-contain p-1"
/>
</span>
<CatalogItemImage
item={artifact}
kind="artifact"
alt={linked ? artifact.name : ""}
rarityBackground={false}
className="size-full"
imageClassName="p-1"
loading={eager ? "eager" : undefined}
sizes={sizes}
/>
);
const className = cn(
"absolute inset-0 overflow-hidden",
@@ -156,13 +156,14 @@ export function TeamArtifactIcon({
if (!secondary) {
const content = (
<>
<Image
src={primary.imageUrl}
<CatalogItemImage
item={primary}
kind="artifact"
alt={linked ? primary.name : ""}
fill
className="absolute inset-0"
imageClassName="p-1"
loading={eager ? "eager" : undefined}
sizes={sizes}
className="object-contain p-1"
/>
{linked ? (
<Kbd className="absolute top-0 right-0 h-3 min-w-3 bg-transparent p-0 text-white">
@@ -173,7 +174,6 @@ export function TeamArtifactIcon({
);
const artifactClassName = cn(
"relative aspect-square scale-110 overflow-hidden rounded-md bg-muted",
rarityGradientClass(primary.rarity),
className,
);
@@ -229,11 +229,9 @@ export function TeamExportImage({ team }: { team: TeamExportData }) {
const percent = total > 0 ? damage / total * 100 : 0;
return (
<div key={member.position} className="flex h-43.5 items-center gap-6 rounded-3xl p-3.5" style={{ backgroundColor: elementBackgroundColor(member.character.element) }}>
<div className={cn("relative size-35 shrink-0 overflow-hidden rounded-3xl border-[3px] border-white/20", characterRarityGradientClass(member.character.key, isCustomTravelerKey(member.character.key) ? 5 : member.character.rarity))}>
<Image src={member.character.imageUrl} alt="" fill loading="eager" sizes="140px" className="object-contain" />
</div>
<CatalogItemImage item={{ ...member.character, rarity: isCustomTravelerKey(member.character.key) ? 5 : member.character.rarity }} kind="character" alt="" className="size-35 shrink-0 rounded-3xl border-[3px] border-white/20" loading="eager" sizes="140px" />
<div className="grid w-44 shrink-0 grid-cols-2 gap-3">
{member.weapon ? <div className={cn("relative aspect-square min-w-0 scale-110 overflow-hidden rounded-2xl", rarityGradientClass(member.weapon.rarity))}><Image src={member.weapon.imageUrl} alt="" fill loading="eager" sizes="82px" className="object-contain p-1" /></div> : null}
{member.weapon ? <CatalogItemImage item={member.weapon} kind="weapon" alt="" className="aspect-square min-w-0 scale-110 rounded-2xl" imageClassName="p-1" loading="eager" sizes="82px" /> : null}
<TeamArtifactIcon
first={member.artifact}
second={member.secondArtifact}
@@ -313,29 +311,17 @@ function TeamCard({
: "flex flex-col sm:grid sm:grid-cols-3",
)}
>
<div
<CatalogItemImage
item={{ ...member.character, rarity: isCustomTravelerKey(member.character.key) ? 5 : member.character.rarity }}
kind="character"
alt={member.character.name}
className={cn(
"relative aspect-square min-h-0 overflow-hidden rounded-lg bg-muted",
exporting
? "col-span-2 row-span-2"
: "sm:col-span-2 sm:row-span-2",
characterRarityGradientClass(
member.character.key,
isCustomTravelerKey(member.character.key)
? 5
: member.character.rarity,
),
"aspect-square min-h-0 rounded-lg bg-muted",
exporting ? "col-span-2 row-span-2" : "sm:col-span-2 sm:row-span-2",
)}
>
<Image
src={member.character.imageUrl}
alt={member.character.name}
fill
loading={exporting ? "eager" : undefined}
sizes="(min-width: 1280px) 184px, (min-width: 640px) 16vw, 25vw"
className="object-contain"
/>
</div>
loading={exporting ? "eager" : undefined}
sizes="(min-width: 1280px) 184px, (min-width: 640px) 16vw, 25vw"
/>
<div
className={cn(
"min-w-0 grid grid-cols-2 gap-2",
@@ -351,18 +337,16 @@ function TeamCard({
rel="noreferrer"
tabIndex={exporting ? -1 : undefined}
aria-label={`เปิดข้อมูลอาวุธ ${member.weapon.name} บน Lunaris`}
className={cn(
"relative aspect-square scale-110 overflow-hidden rounded-md bg-muted outline-none sm:col-span-2",
rarityGradientClass(member.weapon.rarity),
)}
className="relative aspect-square scale-110 overflow-hidden rounded-md bg-muted outline-none sm:col-span-2"
>
<Image
src={member.weapon.imageUrl}
<CatalogItemImage
item={member.weapon}
kind="weapon"
alt={member.weapon.name}
fill
className="absolute inset-0"
imageClassName="p-1"
loading={exporting ? "eager" : undefined}
sizes="(min-width: 1280px) 86px, (min-width: 640px) 8vw, 12.5vw"
className="object-contain p-1"
/>
<Kbd className="absolute top-0 right-0 h-3 min-w-3 bg-transparent p-0 text-white">
<ArrowUpRightIcon aria-hidden="true" />