📃 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-i18next": "^16.5.4",
"react-loader-spinner": "^8.0.2", "react-loader-spinner": "^8.0.2",
"react-modal": "^3.16.3", "react-modal": "^3.16.3",
"react-modal-sheet": "^5.6.0",
"react-router": "^7.13.1", "react-router": "^7.13.1",
"react-router-dom": "^7.13.1", "react-router-dom": "^7.13.1",
"react-select": "^5.10.2", "react-select": "^5.10.2",
@@ -15967,22 +15966,6 @@
"react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19" "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": { "node_modules/react-refresh": {
"version": "0.18.0", "version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
@@ -16138,21 +16121,6 @@
"react-dom": ">=16.6.0" "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": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",

View File

@@ -13,7 +13,8 @@
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"build-storybook": "storybook build", "build-storybook": "storybook build",
"codegen": "wait-on http-get://localhost:3000/graphql/health && graphql-codegen", "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", "author": "Rishi Ghan",
"license": "MIT", "license": "MIT",
@@ -67,7 +68,6 @@
"react-i18next": "^16.5.4", "react-i18next": "^16.5.4",
"react-loader-spinner": "^8.0.2", "react-loader-spinner": "^8.0.2",
"react-modal": "^3.16.3", "react-modal": "^3.16.3",
"react-modal-sheet": "^5.6.0",
"react-router": "^7.13.1", "react-router": "^7.13.1",
"react-router-dom": "^7.13.1", "react-router-dom": "^7.13.1",
"react-select": "^5.10.2", "react-select": "^5.10.2",
@@ -79,6 +79,7 @@
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
"styled-components": "^6.3.11", "styled-components": "^6.3.11",
"threetwo-ui-typings": "^1.0.14", "threetwo-ui-typings": "^1.0.14",
"vaul": "^1.1.2",
"vite": "^7.3.1", "vite": "^7.3.1",
"vite-plugin-html": "^3.2.2", "vite-plugin-html": "^3.2.2",
"websocket": "^1.0.35", "websocket": "^1.0.35",
@@ -111,7 +112,7 @@
"@types/ellipsize": "^0.1.3", "@types/ellipsize": "^0.1.3",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/lodash": "^4.17.24", "@types/lodash": "^4.17.24",
"@types/node": "^25.3.0", "@types/node": "^25.5.2",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@types/react-redux": "^7.1.34", "@types/react-redux": "^7.1.34",
@@ -140,7 +141,7 @@
"tailwindcss": "^4.2.1", "tailwindcss": "^4.2.1",
"ts-jest": "^29.4.6", "ts-jest": "^29.4.6",
"tui-jsdoc-template": "^1.2.2", "tui-jsdoc-template": "^1.2.2",
"typescript": "^5.9.3", "typescript": "^6.0.2",
"wait-on": "^9.0.4" "wait-on": "^9.0.4"
}, },
"resolutions": { "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 { useParams } from "react-router-dom";
import Card from "../shared/Carda"; import Card from "../shared/Carda";
import { RawFileDetails } from "./RawFileDetails"; import { RawFileDetails } from "./RawFileDetails";
@@ -141,11 +141,9 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
}); });
// Query for airdc++ // Query for airdc++
const airDCPPQuery = { const airDCPPQuery = useMemo(() => ({
issue: { issue: { name: issueName },
name: issueName, }), [issueName]);
},
};
// Create tab configuration // Create tab configuration
const openReconcilePanel = useCallback(() => { const openReconcilePanel = useCallback(() => {
@@ -153,7 +151,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
setVisible(true); setVisible(true);
}, []); }, []);
const tabGroup = createTabConfig({ const tabGroup = useMemo(() => createTabConfig({
data: data.data, data: data.data,
hasAnyMetadata, hasAnyMetadata,
areRawFileDetailsAvailable, areRawFileDetailsAvailable,
@@ -163,9 +161,9 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
issueName, issueName,
acquisition, acquisition,
onReconcileMetadata: openReconcilePanel, 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 // Sliding panel content mapping
const renderSlidingPanelContent = () => { const renderSlidingPanelContent = () => {

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import React, { ReactElement, useMemo, useState } from "react"; import React, { ReactElement, useMemo, useState } from "react";
import { isEmpty, isNil } from "lodash"; import { isEmpty, isNil } from "lodash";
import { Sheet } from "react-modal-sheet"; import { Drawer } from "vaul";
import ComicVineDetails from "../ComicVineDetails"; import ComicVineDetails from "../ComicVineDetails";
interface ComicVineMetadata { interface ComicVineMetadata {
@@ -60,10 +60,8 @@ const SOURCE_ICONS: Record<string, string> = {
const MetadataSourceChips = ({ const MetadataSourceChips = ({
sources, sources,
onReconcile,
}: { }: {
sources: string[]; sources: string[];
onReconcile: () => void;
}): ReactElement => { }): ReactElement => {
const [isSheetOpen, setSheetOpen] = useState(false); const [isSheetOpen, setSheetOpen] = useState(false);
@@ -98,17 +96,18 @@ const MetadataSourceChips = ({
Reconcile sources Reconcile sources
</button> </button>
<Sheet isOpen={isSheetOpen} onClose={() => setSheetOpen(false)}> <Drawer.Root open={isSheetOpen} onOpenChange={setSheetOpen}>
<Sheet.Container> <Drawer.Portal>
<Sheet.Header /> <Drawer.Overlay className="fixed inset-0 bg-black/40" />
<Sheet.Content> <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"> <div className="p-4">
{/* Reconciliation UI goes here */} {/* Reconciliation UI goes here */}
</div> </div>
</Sheet.Content> </Drawer.Content>
</Sheet.Container> </Drawer.Portal>
<Sheet.Backdrop /> </Drawer.Root>
</Sheet>
</> </>
); );
}; };
@@ -127,7 +126,7 @@ const MetadataSourceChips = ({
export const VolumeInformation = ( export const VolumeInformation = (
props: VolumeInformationProps, props: VolumeInformationProps,
): ReactElement => { ): ReactElement => {
const { data, onReconcile } = props; const { data } = props;
const presentSources = useMemo(() => { const presentSources = useMemo(() => {
const sources = SOURCED_METADATA_KEYS.filter((key) => { const sources = SOURCED_METADATA_KEYS.filter((key) => {
@@ -152,10 +151,7 @@ export const VolumeInformation = (
return ( return (
<div key={1}> <div key={1}>
{presentSources.length > 1 && ( {presentSources.length > 1 && (
<MetadataSourceChips <MetadataSourceChips sources={presentSources} />
sources={presentSources}
onReconcile={onReconcile ?? (() => {})}
/>
)} )}
{presentSources.length === 1 && {presentSources.length === 1 &&
data.sourcedMetadata?.comicvine?.volumeInformation && ( 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> <i className="h-5 w-5 icon-[solar--book-2-bold] text-slate-500 dark:text-slate-300"></i>
), ),
content: hasAnyMetadata ? ( content: hasAnyMetadata ? (
<VolumeInformation data={data} onReconcile={onReconcileMetadata} key={1} /> <VolumeInformation data={data} onReconcile={onReconcileMetadata} />
) : null, ) : null,
shouldShow: hasAnyMetadata, 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" /> <i className="h-5 w-5 icon-[solar--winrar-bold-duotone] text-slate-500 dark:text-slate-300" />
), ),
name: "Archive Operations", name: "Archive Operations",
content: <ArchiveOperations data={data} key={3} />, content: <ArchiveOperations data={data} />,
shouldShow: areRawFileDetailsAvailable, shouldShow: areRawFileDetailsAvailable,
}, },
{ {
@@ -71,7 +71,6 @@ export const createTabConfig = ({
comicObjectId={comicObjectId} comicObjectId={comicObjectId}
comicObject={data} comicObject={data}
settings={userSettings} settings={userSettings}
key={4}
/> />
), ),
shouldShow: true, shouldShow: true,
@@ -98,7 +97,7 @@ export const createTabConfig = ({
), ),
content: content:
!isNil(data) && !isEmpty(data) ? ( !isNil(data) && !isEmpty(data) ? (
<DownloadsPanel key={5} /> <DownloadsPanel />
) : ( ) : (
<div className="column is-three-fifths"> <div className="column is-three-fifths">
<article className="message is-info"> <article className="message is-info">

View File

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

1478
yarn.lock

File diff suppressed because it is too large Load Diff