📃 WIP bottom sheet

This commit is contained in:
2026-04-03 10:55:10 -04:00
parent 3e045f4c10
commit 0949ebc637
9 changed files with 1137 additions and 456 deletions

32
package-lock.json generated
View File

@@ -58,7 +58,6 @@
"react-i18next": "^16.5.4",
"react-loader-spinner": "^8.0.2",
"react-modal": "^3.16.3",
"react-modal-sheet": "^5.6.0",
"react-router": "^7.13.1",
"react-router-dom": "^7.13.1",
"react-select": "^5.10.2",
@@ -15967,22 +15966,6 @@
"react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19"
}
},
"node_modules/react-modal-sheet": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/react-modal-sheet/-/react-modal-sheet-5.6.0.tgz",
"integrity": "sha512-+WE2nVPdB/Jx0QbndIBqGvy6k0IXriW2lFaPeZSW1xOVri6rWhAwrSnArtsR1rxOxW8HBdAYeIPUcbjMvNeeyw==",
"license": "MIT",
"dependencies": {
"react-use-measure": "2.1.7"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"motion": ">=11",
"react": ">=16"
}
},
"node_modules/react-refresh": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
@@ -16138,21 +16121,6 @@
"react-dom": ">=16.6.0"
}
},
"node_modules/react-use-measure": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
"integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.13",
"react-dom": ">=16.13"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",

View File

@@ -13,7 +13,8 @@
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"codegen": "wait-on http-get://localhost:3000/graphql/health && graphql-codegen",
"codegen:watch": "graphql-codegen --config codegen.yml --watch"
"codegen:watch": "graphql-codegen --config codegen.yml --watch",
"knip": "knip"
},
"author": "Rishi Ghan",
"license": "MIT",
@@ -67,7 +68,6 @@
"react-i18next": "^16.5.4",
"react-loader-spinner": "^8.0.2",
"react-modal": "^3.16.3",
"react-modal-sheet": "^5.6.0",
"react-router": "^7.13.1",
"react-router-dom": "^7.13.1",
"react-select": "^5.10.2",
@@ -79,6 +79,7 @@
"socket.io-client": "^4.8.3",
"styled-components": "^6.3.11",
"threetwo-ui-typings": "^1.0.14",
"vaul": "^1.1.2",
"vite": "^7.3.1",
"vite-plugin-html": "^3.2.2",
"websocket": "^1.0.35",
@@ -111,7 +112,7 @@
"@types/ellipsize": "^0.1.3",
"@types/jest": "^30.0.0",
"@types/lodash": "^4.17.24",
"@types/node": "^25.3.0",
"@types/node": "^25.5.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-redux": "^7.1.34",
@@ -140,7 +141,7 @@
"tailwindcss": "^4.2.1",
"ts-jest": "^29.4.6",
"tui-jsdoc-template": "^1.2.2",
"typescript": "^5.9.3",
"typescript": "^6.0.2",
"wait-on": "^9.0.4"
},
"resolutions": {

View File

@@ -1,4 +1,4 @@
import React, { useState, ReactElement, useCallback } from "react";
import React, { useState, ReactElement, useCallback, useMemo } from "react";
import { useParams } from "react-router-dom";
import Card from "../shared/Carda";
import { RawFileDetails } from "./RawFileDetails";
@@ -141,11 +141,9 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
});
// Query for airdc++
const airDCPPQuery = {
issue: {
name: issueName,
},
};
const airDCPPQuery = useMemo(() => ({
issue: { name: issueName },
}), [issueName]);
// Create tab configuration
const openReconcilePanel = useCallback(() => {
@@ -153,7 +151,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
setVisible(true);
}, []);
const tabGroup = createTabConfig({
const tabGroup = useMemo(() => createTabConfig({
data: data.data,
hasAnyMetadata,
areRawFileDetailsAvailable,
@@ -163,9 +161,9 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
issueName,
acquisition,
onReconcileMetadata: openReconcilePanel,
});
}), [data.data, hasAnyMetadata, areRawFileDetailsAvailable, airDCPPQuery, _id, userSettings, issueName, acquisition, openReconcilePanel]);
const filteredTabs = tabGroup.filter((tab) => tab.shouldShow);
const filteredTabs = useMemo(() => tabGroup.filter((tab) => tab.shouldShow), [tabGroup]);
// Sliding panel content mapping
const renderSlidingPanelContent = () => {

View File

@@ -47,10 +47,12 @@ export const TabControls = (props): ReactElement => {
</nav>
</div>
</div>
<Suspense>
{filteredTabs.map(({ id, content }) => {
return currentActive === id ? content : null;
})}
<Suspense fallback={null}>
{filteredTabs.map(({ id, content }) => (
<React.Fragment key={id}>
{currentActive === id ? content : null}
</React.Fragment>
))}
</Suspense>
</>
);

View File

@@ -131,10 +131,12 @@ export const ArchiveOperations = (props: { data: any }): ReactElement => {
enabled: false,
});
if (isSuccess && shouldRefetchComicBookData) {
queryClient.invalidateQueries({ queryKey: ["comicBookMetadata"] });
setShouldRefetchComicBookData(false);
}
useEffect(() => {
if (isSuccess && shouldRefetchComicBookData) {
queryClient.invalidateQueries({ queryKey: ["comicBookMetadata"] });
setShouldRefetchComicBookData(false);
}
}, [isSuccess, shouldRefetchComicBookData, queryClient]);
// sliding panel init
const contentForSlidingPanel: Record<string, { content: () => JSX.Element }> = {

View File

@@ -1,6 +1,6 @@
import React, { ReactElement, useMemo, useState } from "react";
import { isEmpty, isNil } from "lodash";
import { Sheet } from "react-modal-sheet";
import { Drawer } from "vaul";
import ComicVineDetails from "../ComicVineDetails";
interface ComicVineMetadata {
@@ -60,10 +60,8 @@ const SOURCE_ICONS: Record<string, string> = {
const MetadataSourceChips = ({
sources,
onReconcile,
}: {
sources: string[];
onReconcile: () => void;
}): ReactElement => {
const [isSheetOpen, setSheetOpen] = useState(false);
@@ -98,17 +96,18 @@ const MetadataSourceChips = ({
Reconcile sources
</button>
<Sheet isOpen={isSheetOpen} onClose={() => setSheetOpen(false)}>
<Sheet.Container>
<Sheet.Header />
<Sheet.Content>
<Drawer.Root open={isSheetOpen} onOpenChange={setSheetOpen}>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 bg-black/40" />
<Drawer.Content aria-describedby={undefined} className="fixed bottom-0 left-0 right-0 rounded-t-2xl bg-white dark:bg-slate-800 p-4 outline-none">
<Drawer.Title className="sr-only">Reconcile metadata sources</Drawer.Title>
<div className="mx-auto mb-4 h-1.5 w-12 rounded-full bg-slate-300 dark:bg-slate-600" />
<div className="p-4">
{/* Reconciliation UI goes here */}
</div>
</Sheet.Content>
</Sheet.Container>
<Sheet.Backdrop />
</Sheet>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
</>
);
};
@@ -127,7 +126,7 @@ const MetadataSourceChips = ({
export const VolumeInformation = (
props: VolumeInformationProps,
): ReactElement => {
const { data, onReconcile } = props;
const { data } = props;
const presentSources = useMemo(() => {
const sources = SOURCED_METADATA_KEYS.filter((key) => {
@@ -152,10 +151,7 @@ export const VolumeInformation = (
return (
<div key={1}>
{presentSources.length > 1 && (
<MetadataSourceChips
sources={presentSources}
onReconcile={onReconcile ?? (() => {})}
/>
<MetadataSourceChips sources={presentSources} />
)}
{presentSources.length === 1 &&
data.sourcedMetadata?.comicvine?.volumeInformation && (

View File

@@ -46,7 +46,7 @@ export const createTabConfig = ({
<i className="h-5 w-5 icon-[solar--book-2-bold] text-slate-500 dark:text-slate-300"></i>
),
content: hasAnyMetadata ? (
<VolumeInformation data={data} onReconcile={onReconcileMetadata} key={1} />
<VolumeInformation data={data} onReconcile={onReconcileMetadata} />
) : null,
shouldShow: hasAnyMetadata,
},
@@ -56,7 +56,7 @@ export const createTabConfig = ({
<i className="h-5 w-5 icon-[solar--winrar-bold-duotone] text-slate-500 dark:text-slate-300" />
),
name: "Archive Operations",
content: <ArchiveOperations data={data} key={3} />,
content: <ArchiveOperations data={data} />,
shouldShow: areRawFileDetailsAvailable,
},
{
@@ -71,7 +71,6 @@ export const createTabConfig = ({
comicObjectId={comicObjectId}
comicObject={data}
settings={userSettings}
key={4}
/>
),
shouldShow: true,
@@ -98,7 +97,7 @@ export const createTabConfig = ({
),
content:
!isNil(data) && !isEmpty(data) ? (
<DownloadsPanel key={5} />
<DownloadsPanel />
) : (
<div className="column is-three-fifths">
<article className="message is-info">

View File

@@ -35,6 +35,9 @@ export default defineConfig({
},
},
},
resolve: {
dedupe: ["react", "react-dom"],
},
esbuild: {
supported: {
"top-level-await": true, //browsers can handle top-level-await features

1478
yarn.lock

File diff suppressed because it is too large Load Diff