mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Download tier list as img (#2638)
Co-authored-by: hfcRed <hfcred@gmx.net>
This commit is contained in:
parent
187e1aa105
commit
84b4c1d67f
|
|
@ -34,6 +34,7 @@ interface ImageProps {
|
|||
testId?: string;
|
||||
onClick?: () => void;
|
||||
loading?: "lazy";
|
||||
forcePng?: boolean;
|
||||
}
|
||||
|
||||
export function Image({
|
||||
|
|
@ -50,7 +51,32 @@ export function Image({
|
|||
containerStyle,
|
||||
onClick,
|
||||
loading,
|
||||
forcePng,
|
||||
}: ImageProps) {
|
||||
if (forcePng) {
|
||||
return (
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration
|
||||
<div
|
||||
title={title}
|
||||
className={containerClassName}
|
||||
style={containerStyle}
|
||||
onClick={onClick}
|
||||
>
|
||||
<img
|
||||
alt={alt}
|
||||
src={`${path}.png`}
|
||||
className={className}
|
||||
width={size ?? width}
|
||||
height={size ?? height}
|
||||
style={style}
|
||||
draggable="false"
|
||||
loading={loading}
|
||||
data-testid={testId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration
|
||||
<picture
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ import { TierListItemImage } from "./TierListItemImage";
|
|||
|
||||
interface DraggableItemProps {
|
||||
item: TierListItem;
|
||||
forcePng?: boolean;
|
||||
}
|
||||
|
||||
export function DraggableItem({ item }: DraggableItemProps) {
|
||||
export function DraggableItem({ item, forcePng }: DraggableItemProps) {
|
||||
const uniqueId = tierListItemId(item);
|
||||
|
||||
const {
|
||||
|
|
@ -32,7 +33,7 @@ export function DraggableItem({ item }: DraggableItemProps) {
|
|||
return (
|
||||
<div ref={setNodeRef} className={styles.item} style={style}>
|
||||
<div data-item-id={uniqueId} {...listeners} {...attributes}>
|
||||
<TierListItemImage item={item} />
|
||||
<TierListItemImage item={item} forcePng={forcePng} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,38 +11,58 @@ import styles from "./TierListItemImage.module.css";
|
|||
|
||||
interface TierListItemImageProps {
|
||||
item: TierListItem;
|
||||
forcePng?: boolean;
|
||||
}
|
||||
|
||||
export function TierListItemImage({ item }: TierListItemImageProps) {
|
||||
export function TierListItemImage({ item, forcePng }: TierListItemImageProps) {
|
||||
switch (item.type) {
|
||||
case "main-weapon":
|
||||
return (
|
||||
<div className={styles.imageWrapper}>
|
||||
<WeaponImage weaponSplId={item.id} variant="badge" size={48} />
|
||||
<WeaponImage
|
||||
weaponSplId={item.id}
|
||||
variant="badge"
|
||||
size={48}
|
||||
forcePng={forcePng}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "sub-weapon":
|
||||
return (
|
||||
<div className={styles.imageWrapper}>
|
||||
<SubWeaponImage subWeaponId={item.id} size={48} />
|
||||
<SubWeaponImage subWeaponId={item.id} size={48} forcePng={forcePng} />
|
||||
</div>
|
||||
);
|
||||
case "special-weapon":
|
||||
return (
|
||||
<div className={styles.imageWrapper}>
|
||||
<SpecialWeaponImage specialWeaponId={item.id} size={48} />
|
||||
<SpecialWeaponImage
|
||||
specialWeaponId={item.id}
|
||||
size={48}
|
||||
forcePng={forcePng}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "stage":
|
||||
return (
|
||||
<div className={styles.imageWrapper}>
|
||||
<StageImage stageId={item.id} width={80} className="rounded-sm" />
|
||||
<StageImage
|
||||
stageId={item.id}
|
||||
width={80}
|
||||
className="rounded-sm"
|
||||
forcePng={forcePng}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "mode":
|
||||
return (
|
||||
<div className={styles.imageWrapper}>
|
||||
<ModeImage mode={item.id} width={48} height={48} />
|
||||
<ModeImage
|
||||
mode={item.id}
|
||||
width={48}
|
||||
height={48}
|
||||
forcePng={forcePng}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "stage-mode": {
|
||||
|
|
@ -51,12 +71,18 @@ export function TierListItemImage({ item }: TierListItemImageProps) {
|
|||
return (
|
||||
<div className={styles.imageWrapper}>
|
||||
<div className="relative">
|
||||
<StageImage stageId={stageId} width={80} className="rounded-sm" />
|
||||
<StageImage
|
||||
stageId={stageId}
|
||||
width={80}
|
||||
className="rounded-sm"
|
||||
forcePng={forcePng}
|
||||
/>
|
||||
<ModeImage
|
||||
mode={mode as ModeShort}
|
||||
width={24}
|
||||
height={24}
|
||||
className={styles.modeOverlay}
|
||||
forcePng={forcePng}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
flex-wrap: wrap;
|
||||
gap: var(--s-1-5);
|
||||
padding: var(--s-2);
|
||||
background: var(--bg-lighter);
|
||||
background: var(--bg-lighter-solid);
|
||||
border: 2px dashed transparent;
|
||||
border-radius: var(--rounded-sm) 0 0 var(--rounded-sm);
|
||||
align-content: flex-start;
|
||||
|
|
@ -58,10 +58,6 @@
|
|||
background: var(--bg-lightest);
|
||||
}
|
||||
|
||||
.targetZoneFullRadius {
|
||||
border-radius: var(--rounded-sm);
|
||||
}
|
||||
|
||||
.emptyMessage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export function TierRow({ tier }: TierRowProps) {
|
|||
handleMoveTierUp,
|
||||
handleMoveTierDown,
|
||||
showTierHeaders,
|
||||
showArrowControls,
|
||||
screenshotMode,
|
||||
} = useTierListState();
|
||||
|
||||
const items = getItemsInTier(tier.id);
|
||||
|
|
@ -95,28 +95,34 @@ export function TierRow({ tier }: TierRowProps) {
|
|||
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={{
|
||||
borderRadius: screenshotMode ? "var(--rounded-sm)" : undefined,
|
||||
}}
|
||||
className={clsx(styles.targetZone, {
|
||||
[styles.targetZoneOver]: isOver,
|
||||
[styles.targetZoneFullRadius]: !showArrowControls,
|
||||
})}
|
||||
>
|
||||
{items.length === 0 ? (
|
||||
{items.length === 0 && !screenshotMode ? (
|
||||
<div className={styles.emptyMessage}>
|
||||
{t("tier-list-maker:dropItems")}
|
||||
</div>
|
||||
) : (
|
||||
) : items.length > 0 ? (
|
||||
<SortableContext
|
||||
items={items.map(tierListItemId)}
|
||||
strategy={horizontalListSortingStrategy}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<DraggableItem key={tierListItemId(item)} item={item} />
|
||||
<DraggableItem
|
||||
key={tierListItemId(item)}
|
||||
item={item}
|
||||
forcePng={screenshotMode}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{showArrowControls ? (
|
||||
{!screenshotMode ? (
|
||||
<div className={styles.arrowControls}>
|
||||
<button
|
||||
className={clsx(styles.arrowButton, styles.arrowButtonUpper)}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,22 @@
|
|||
import type { ReactNode } from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
import { createContext, useContext, useState } from "react";
|
||||
import { useTierList } from "../hooks/useTierListState";
|
||||
|
||||
type TierListContextType = ReturnType<typeof useTierList>;
|
||||
type TierListContextType = ReturnType<typeof useTierList> & {
|
||||
screenshotMode: boolean;
|
||||
setScreenshotMode: (value: boolean) => void;
|
||||
};
|
||||
|
||||
const TierListContext = createContext<TierListContextType | null>(null);
|
||||
|
||||
export function TierListProvider({ children }: { children: ReactNode }) {
|
||||
const state = useTierList();
|
||||
const [screenshotMode, setScreenshotMode] = useState(false);
|
||||
|
||||
return (
|
||||
<TierListContext.Provider value={state}>
|
||||
<TierListContext.Provider
|
||||
value={{ ...state, screenshotMode, setScreenshotMode }}
|
||||
>
|
||||
{children}
|
||||
</TierListContext.Provider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -66,10 +66,10 @@ export function useTierList() {
|
|||
revive: (value) => value === "true",
|
||||
});
|
||||
|
||||
const [showArrowControls, setShowArrowControls] = useSearchParamState({
|
||||
name: "showArrowControls",
|
||||
defaultValue: true,
|
||||
revive: (value) => value === "true",
|
||||
const [title, setTitle] = useSearchParamState({
|
||||
name: "title",
|
||||
defaultValue: "",
|
||||
revive: (value) => value,
|
||||
});
|
||||
|
||||
const parseItemFromId = (id: string): TierListItem | null => {
|
||||
|
|
@ -423,8 +423,8 @@ export function useTierList() {
|
|||
setCanAddDuplicates,
|
||||
showTierHeaders,
|
||||
setShowTierHeaders,
|
||||
showArrowControls,
|
||||
setShowArrowControls,
|
||||
title,
|
||||
setTitle,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,63 @@
|
|||
margin-bottom: var(--s-6);
|
||||
}
|
||||
|
||||
.tierListScreenshotMode {
|
||||
padding: var(--s-4);
|
||||
max-width: 720px;
|
||||
min-width: 720px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: var(--s-4);
|
||||
}
|
||||
|
||||
.titleInput {
|
||||
width: 100%;
|
||||
padding: 0 var(--s-2) var(--s-2) var(--s-2);
|
||||
font-size: var(--fonts-lg);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.titleInput::placeholder {
|
||||
color: var(--text-lighter);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.titleInput:focus::placeholder {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.authorSection {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--s-0-5);
|
||||
margin-bottom: var(--s-3);
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.authorBy {
|
||||
font-size: var(--fonts-xxs);
|
||||
font-weight: var(--bold);
|
||||
color: var(--text-lighter);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.authorInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.authorUsername {
|
||||
font-weight: var(--bold);
|
||||
font-size: var(--fonts-xs);
|
||||
color: var(--text);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,12 @@ import {
|
|||
} from "@dnd-kit/core";
|
||||
import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
|
||||
import type { MetaFunction } from "@remix-run/react";
|
||||
import { snapdom } from "@zumer/snapdom";
|
||||
import clsx from "clsx";
|
||||
import { useRef } from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { SendouPopover } from "~/components/elements/Popover";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
|
|
@ -20,10 +24,12 @@ import {
|
|||
SendouTabPanel,
|
||||
SendouTabs,
|
||||
} from "~/components/elements/Tabs";
|
||||
import { DownloadIcon } from "~/components/icons/Download";
|
||||
import { PlusIcon } from "~/components/icons/Plus";
|
||||
import { RefreshIcon } from "~/components/icons/Refresh";
|
||||
import { Main } from "~/components/Main";
|
||||
import { Placeholder } from "~/components/Placeholder";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
|
|
@ -76,6 +82,8 @@ export default function TierListMakerPage() {
|
|||
|
||||
function TierListMakerContent() {
|
||||
const { t } = useTranslation(["tier-list-maker"]);
|
||||
const user = useUser();
|
||||
|
||||
const {
|
||||
itemType,
|
||||
setItemType,
|
||||
|
|
@ -94,8 +102,10 @@ function TierListMakerContent() {
|
|||
setCanAddDuplicates,
|
||||
showTierHeaders,
|
||||
setShowTierHeaders,
|
||||
showArrowControls,
|
||||
setShowArrowControls,
|
||||
title,
|
||||
setTitle,
|
||||
screenshotMode,
|
||||
setScreenshotMode,
|
||||
} = useTierListState();
|
||||
|
||||
const sensors = useSensors(
|
||||
|
|
@ -105,12 +115,44 @@ function TierListMakerContent() {
|
|||
}),
|
||||
);
|
||||
|
||||
const tierListRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!tierListRef.current) return;
|
||||
|
||||
flushSync(() => setScreenshotMode(true));
|
||||
|
||||
await snapdom.download(tierListRef.current, {
|
||||
format: "png",
|
||||
filename: "tier-list",
|
||||
quality: 1,
|
||||
scale: 1.75,
|
||||
embedFonts: true,
|
||||
backgroundColor: getComputedStyle(document.body).backgroundColor,
|
||||
});
|
||||
|
||||
setScreenshotMode(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Main bigger className={clsx(styles.container, "stack lg")}>
|
||||
<div className={styles.header}>
|
||||
<SendouButton onPress={handleAddTier} size="small" icon={<PlusIcon />}>
|
||||
{t("tier-list-maker:addTier")}
|
||||
</SendouButton>
|
||||
<div className="stack horizontal md">
|
||||
<SendouButton
|
||||
onPress={handleAddTier}
|
||||
size="small"
|
||||
icon={<PlusIcon />}
|
||||
>
|
||||
{t("tier-list-maker:addTier")}
|
||||
</SendouButton>
|
||||
<SendouButton
|
||||
onPress={handleDownload}
|
||||
size="small"
|
||||
icon={<DownloadIcon />}
|
||||
>
|
||||
{t("tier-list-maker:download")}
|
||||
</SendouButton>
|
||||
</div>
|
||||
<ResetPopover key={state.tierItems.size} handleReset={handleReset} />
|
||||
</div>
|
||||
|
||||
|
|
@ -122,7 +164,30 @@ function TierListMakerContent() {
|
|||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className="stack">
|
||||
<div className={styles.tierList}>
|
||||
<div
|
||||
className={clsx(styles.tierList, {
|
||||
[styles.tierListScreenshotMode]: screenshotMode,
|
||||
})}
|
||||
ref={tierListRef}
|
||||
>
|
||||
{title || !screenshotMode ? (
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder={t("tier-list-maker:titlePlaceholder")}
|
||||
className={clsx(styles.titleInput, "plain")}
|
||||
/>
|
||||
) : null}
|
||||
{screenshotMode && title && user ? (
|
||||
<div className={styles.authorSection}>
|
||||
<div className={styles.authorBy}>{t("tier-list-maker:by")}</div>
|
||||
<div className={styles.authorInfo}>
|
||||
<Avatar user={user} size="xxxs" alt={user.username} />
|
||||
<span className={styles.authorUsername}>{user.username}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{state.tiers.map((tier) => (
|
||||
<TierRow key={tier.id} tier={tier} />
|
||||
))}
|
||||
|
|
@ -143,13 +208,6 @@ function TierListMakerContent() {
|
|||
>
|
||||
{t("tier-list-maker:showTierHeaders")}
|
||||
</SendouSwitch>
|
||||
<SendouSwitch
|
||||
isSelected={showArrowControls}
|
||||
onChange={setShowArrowControls}
|
||||
size="small"
|
||||
>
|
||||
{t("tier-list-maker:showArrowControls")}
|
||||
</SendouSwitch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"addTier": "",
|
||||
"download": "",
|
||||
"mainWeapons": "",
|
||||
"subWeapons": "",
|
||||
"specialWeapons": "",
|
||||
|
|
@ -13,5 +14,6 @@
|
|||
"resetConfirmation": "",
|
||||
"allowDuplicates": "",
|
||||
"showTierHeaders": "",
|
||||
"showArrowControls": ""
|
||||
"titlePlaceholder": "",
|
||||
"by": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"addTier": "",
|
||||
"download": "",
|
||||
"mainWeapons": "",
|
||||
"subWeapons": "",
|
||||
"specialWeapons": "",
|
||||
|
|
@ -13,5 +14,6 @@
|
|||
"resetConfirmation": "",
|
||||
"allowDuplicates": "",
|
||||
"showTierHeaders": "",
|
||||
"showArrowControls": ""
|
||||
"titlePlaceholder": "",
|
||||
"by": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"addTier": "Add tier",
|
||||
"download": "Download (.png)",
|
||||
"mainWeapons": "Main Weapons",
|
||||
"subWeapons": "Sub Weapons",
|
||||
"specialWeapons": "Special Weapons",
|
||||
|
|
@ -13,5 +14,6 @@
|
|||
"resetConfirmation": "Are you sure you want to reset the tier list? This will remove all items and restore default tiers.",
|
||||
"allowDuplicates": "Allow duplicates",
|
||||
"showTierHeaders": "Show tier headers",
|
||||
"showArrowControls": "Show arrow controls"
|
||||
"titlePlaceholder": "Click to add title...",
|
||||
"by": "Made by"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"addTier": "",
|
||||
"download": "",
|
||||
"mainWeapons": "",
|
||||
"subWeapons": "",
|
||||
"specialWeapons": "",
|
||||
|
|
@ -13,5 +14,6 @@
|
|||
"resetConfirmation": "",
|
||||
"allowDuplicates": "",
|
||||
"showTierHeaders": "",
|
||||
"showArrowControls": ""
|
||||
"titlePlaceholder": "",
|
||||
"by": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"addTier": "",
|
||||
"download": "",
|
||||
"mainWeapons": "",
|
||||
"subWeapons": "",
|
||||
"specialWeapons": "",
|
||||
|
|
@ -13,5 +14,6 @@
|
|||
"resetConfirmation": "",
|
||||
"allowDuplicates": "",
|
||||
"showTierHeaders": "",
|
||||
"showArrowControls": ""
|
||||
"titlePlaceholder": "",
|
||||
"by": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"addTier": "",
|
||||
"download": "",
|
||||
"mainWeapons": "",
|
||||
"subWeapons": "",
|
||||
"specialWeapons": "",
|
||||
|
|
@ -13,5 +14,6 @@
|
|||
"resetConfirmation": "",
|
||||
"allowDuplicates": "",
|
||||
"showTierHeaders": "",
|
||||
"showArrowControls": ""
|
||||
"titlePlaceholder": "",
|
||||
"by": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"addTier": "",
|
||||
"download": "",
|
||||
"mainWeapons": "",
|
||||
"subWeapons": "",
|
||||
"specialWeapons": "",
|
||||
|
|
@ -13,5 +14,6 @@
|
|||
"resetConfirmation": "",
|
||||
"allowDuplicates": "",
|
||||
"showTierHeaders": "",
|
||||
"showArrowControls": ""
|
||||
"titlePlaceholder": "",
|
||||
"by": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"addTier": "",
|
||||
"download": "",
|
||||
"mainWeapons": "",
|
||||
"subWeapons": "",
|
||||
"specialWeapons": "",
|
||||
|
|
@ -13,5 +14,6 @@
|
|||
"resetConfirmation": "",
|
||||
"allowDuplicates": "",
|
||||
"showTierHeaders": "",
|
||||
"showArrowControls": ""
|
||||
"titlePlaceholder": "",
|
||||
"by": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"addTier": "",
|
||||
"download": "",
|
||||
"mainWeapons": "",
|
||||
"subWeapons": "",
|
||||
"specialWeapons": "",
|
||||
|
|
@ -13,5 +14,6 @@
|
|||
"resetConfirmation": "",
|
||||
"allowDuplicates": "",
|
||||
"showTierHeaders": "",
|
||||
"showArrowControls": ""
|
||||
"titlePlaceholder": "",
|
||||
"by": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"addTier": "",
|
||||
"download": "",
|
||||
"mainWeapons": "",
|
||||
"subWeapons": "",
|
||||
"specialWeapons": "",
|
||||
|
|
@ -13,5 +14,6 @@
|
|||
"resetConfirmation": "",
|
||||
"allowDuplicates": "",
|
||||
"showTierHeaders": "",
|
||||
"showArrowControls": ""
|
||||
"titlePlaceholder": "",
|
||||
"by": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"addTier": "",
|
||||
"download": "",
|
||||
"mainWeapons": "",
|
||||
"subWeapons": "",
|
||||
"specialWeapons": "",
|
||||
|
|
@ -13,5 +14,6 @@
|
|||
"resetConfirmation": "",
|
||||
"allowDuplicates": "",
|
||||
"showTierHeaders": "",
|
||||
"showArrowControls": ""
|
||||
"titlePlaceholder": "",
|
||||
"by": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"addTier": "",
|
||||
"download": "",
|
||||
"mainWeapons": "",
|
||||
"subWeapons": "",
|
||||
"specialWeapons": "",
|
||||
|
|
@ -13,5 +14,6 @@
|
|||
"resetConfirmation": "",
|
||||
"allowDuplicates": "",
|
||||
"showTierHeaders": "",
|
||||
"showArrowControls": ""
|
||||
"titlePlaceholder": "",
|
||||
"by": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"addTier": "",
|
||||
"download": "",
|
||||
"mainWeapons": "",
|
||||
"subWeapons": "",
|
||||
"specialWeapons": "",
|
||||
|
|
@ -13,5 +14,6 @@
|
|||
"resetConfirmation": "",
|
||||
"allowDuplicates": "",
|
||||
"showTierHeaders": "",
|
||||
"showArrowControls": ""
|
||||
"titlePlaceholder": "",
|
||||
"by": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"addTier": "",
|
||||
"download": "",
|
||||
"mainWeapons": "",
|
||||
"subWeapons": "",
|
||||
"specialWeapons": "",
|
||||
|
|
@ -13,5 +14,6 @@
|
|||
"resetConfirmation": "",
|
||||
"allowDuplicates": "",
|
||||
"showTierHeaders": "",
|
||||
"showArrowControls": ""
|
||||
"titlePlaceholder": "",
|
||||
"by": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"addTier": "",
|
||||
"download": "",
|
||||
"mainWeapons": "",
|
||||
"subWeapons": "",
|
||||
"specialWeapons": "",
|
||||
|
|
@ -13,5 +14,6 @@
|
|||
"resetConfirmation": "",
|
||||
"allowDuplicates": "",
|
||||
"showTierHeaders": "",
|
||||
"showArrowControls": ""
|
||||
"titlePlaceholder": "",
|
||||
"by": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"addTier": "",
|
||||
"download": "",
|
||||
"mainWeapons": "",
|
||||
"subWeapons": "",
|
||||
"specialWeapons": "",
|
||||
|
|
@ -13,5 +14,6 @@
|
|||
"resetConfirmation": "",
|
||||
"allowDuplicates": "",
|
||||
"showTierHeaders": "",
|
||||
"showArrowControls": ""
|
||||
"titlePlaceholder": "",
|
||||
"by": ""
|
||||
}
|
||||
|
|
|
|||
7
package-lock.json
generated
7
package-lock.json
generated
|
|
@ -21,6 +21,7 @@
|
|||
"@remix-run/react": "^2.16.8",
|
||||
"@remix-run/serve": "^2.16.8",
|
||||
"@tldraw/tldraw": "^3.12.1",
|
||||
"@zumer/snapdom": "^1.9.14",
|
||||
"aws-sdk": "^2.1692.0",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
|
@ -7986,6 +7987,12 @@
|
|||
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@zumer/snapdom": {
|
||||
"version": "1.9.14",
|
||||
"resolved": "https://registry.npmjs.org/@zumer/snapdom/-/snapdom-1.9.14.tgz",
|
||||
"integrity": "sha512-kQARFS/jf+fsIFv9qxfNp8YMeun5tTyhFDk3iv47Lywk5YRldhINWicEZI15fP3FDUCeo8ok+BP0CtnDRFMFRg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@zxing/text-encoding": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"@remix-run/react": "^2.16.8",
|
||||
"@remix-run/serve": "^2.16.8",
|
||||
"@tldraw/tldraw": "^3.12.1",
|
||||
"@zumer/snapdom": "^1.9.14",
|
||||
"aws-sdk": "^2.1692.0",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user