diff --git a/frontend/package.json b/frontend/package.json index f2908885..d78fdec2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,7 +40,8 @@ "react-select": "^5.8.0", "recharts": "2", "swr": "^2.2.5", - "web-vitals": "^3.5.2" + "web-vitals": "^3.5.2", + "yet-another-react-lightbox": "^3.18.0" }, "scripts": { "start": "react-scripts start", @@ -72,7 +73,6 @@ ] }, "devDependencies": { - "typescript": "^5.4.5", "@testing-library/jest-dom": "^6.4.5", "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.5.2", @@ -87,7 +87,8 @@ "i18next-parser": "^8.13.0", "lint-staged": "^15.2.2", "prettier": "^3.2.5", - "source-map-explorer": "^2.5.3" + "source-map-explorer": "^2.5.3", + "typescript": "^5.4.5" }, "husky": { "hooks": { diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 679271b7..ede50b1a 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -119,6 +119,9 @@ dependencies: web-vitals: specifier: ^3.5.2 version: 3.5.2 + yet-another-react-lightbox: + specifier: ^3.18.0 + version: 3.18.0(react-dom@18.3.1)(react@18.3.1) devDependencies: '@testing-library/jest-dom': @@ -12808,6 +12811,17 @@ packages: y18n: 5.0.8 yargs-parser: 20.2.9 + /yet-another-react-lightbox@3.18.0(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-Zf3Stgp+gSAZe5Uy45OJDDxxXoE+fENZjJnAa7HD+yb/5xAhBQqqz8K7u4kAuj12lexEOASks2wHeYaZ+yUVnQ==} + engines: {node: '>=14'} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} diff --git a/frontend/src/components/ImageLightBox.tsx b/frontend/src/components/ImageLightBox.tsx new file mode 100644 index 00000000..5f9de304 --- /dev/null +++ b/frontend/src/components/ImageLightBox.tsx @@ -0,0 +1,27 @@ +import { serverBaseUrl } from '../index'; +import { Buffer } from 'buffer'; +import React from 'react'; +import Lightbox from 'yet-another-react-lightbox'; + +interface Props { + src?: string; + onChange: (src: string | undefined) => void; +} +export function ImageLightBox(props: Props) { + return ( + props.onChange(undefined)} + render={{ + buttonPrev: () => null, + buttonNext: () => null, + }} + /> + ); +} diff --git a/frontend/src/components/documentLines/AccountDocumentLine.tsx b/frontend/src/components/documentLines/AccountDocumentLine.tsx index 6f45eec5..890fcd71 100644 --- a/frontend/src/components/documentLines/AccountDocumentLine.tsx +++ b/frontend/src/components/documentLines/AccountDocumentLine.tsx @@ -1,9 +1,9 @@ import { Box, Card, Text } from '@mantine/core'; -import { openContextModal } from '@mantine/modals'; import { Buffer } from 'buffer'; import { serverBaseUrl } from '../../index'; import { Document } from '../../rest-model'; import { createStyles } from '@mantine/emotion'; +import { isDocumentAnImage } from '../../utils/documents'; const useStyles = createStyles((theme, _, u) => ({ imgBox: { @@ -60,31 +60,17 @@ const useStyles = createStyles((theme, _, u) => ({ }, })); -export interface Props extends Document {} - -export const EXTENSIONS_SUPPORT_PREVIEW = ['image/png', 'image/jpg', 'image/jpeg', 'image/gif']; +export interface Props extends Document { + onClick: (path: string) => void; +} export default function AccountDocumentLine(props: Props) { const { classes } = useStyles(); - const extension = (props.extension ?? '').toLowerCase(); - const canPreview = EXTENSIONS_SUPPORT_PREVIEW.includes(extension); - const openPreviewModal = () => { - if (canPreview) { - openContextModal({ - modal: 'documentPreviewModal', - title: props.filename, - size: 'lg', - centered: true, - innerProps: { - filename: props.filename, - path: props.path, - }, - }); - } - }; + const canPreview = isDocumentAnImage(props.path); + return ( - + props.onClick(props.path) : undefined}> {canPreview ? ( ({ imgBox: { overflow: 'hidden', @@ -64,30 +63,21 @@ const useStyles = createStyles((theme, _, u) => ({ interface Props { uri: string; filename: string; + onClick: (path: string) => void; } -export default function DocumentPreview({ filename }: Props) { +export default function DocumentPreview(props: Props) { const { classes } = useStyles(); - const extension = filename.split('.').pop()?.toLocaleLowerCase() || ''; - const simpleFilename = filename.split('/').pop() || ''; - const canPreview = EXTENSIONS_SUPPORT_PREVIEW.includes(extension); + const canPreview = isDocumentAnImage(props.filename); - const openDocumentModal = () => { - openContextModal({ - modal: 'documentPreviewModal', - title: simpleFilename, - size: 'lg', - centered: true, - innerProps: { - filename: simpleFilename, - path: filename, - }, - }); - }; return ( - + props.onClick(props.filename) : undefined}> {canPreview ? ( - {filename} + {props.filename} ) : ( This document cannot be previewed )} diff --git a/frontend/src/components/journalPreview/TransactionPreview.tsx b/frontend/src/components/journalPreview/TransactionPreview.tsx index 46bf04d4..9d38614b 100644 --- a/frontend/src/components/journalPreview/TransactionPreview.tsx +++ b/frontend/src/components/journalPreview/TransactionPreview.tsx @@ -7,6 +7,8 @@ import Section from '../Section'; import DocumentPreview from './DocumentPreview'; import AccountDocumentUpload from '../AccountDocumentUpload'; import { createStyles } from '@mantine/emotion'; +import { ImageLightBox } from '../ImageLightBox'; +import { useState } from 'react'; const useStyles = createStyles((theme, _, u) => ({ amount: { @@ -25,6 +27,8 @@ interface Props { } export default function TransactionPreview(props: Props) { + const [lightboxSrc, setLightboxSrc] = useState(undefined); + const { classes } = useStyles(); return (
@@ -137,12 +141,13 @@ export default function TransactionPreview(props: Props) { )} +
meta.key === 'document').length} Documents`}> {props.data.metas .filter((meta) => meta.key === 'document') .map((meta, idx) => ( - + setLightboxSrc(meta.value)} key={idx} uri={meta.value} filename={meta.value} /> ))} diff --git a/frontend/src/components/modals/DocumentPreviewModal.tsx b/frontend/src/components/modals/DocumentPreviewModal.tsx deleted file mode 100644 index b4c3f657..00000000 --- a/frontend/src/components/modals/DocumentPreviewModal.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Image } from '@mantine/core'; -import { ContextModalProps } from '@mantine/modals'; -import { Buffer } from 'buffer'; -import { serverBaseUrl } from '../../index'; - -export const DocumentPreviewModal = ({ context, id, innerProps }: ContextModalProps<{ path: string; filename: string }>) => ( - <> - {/*{innerProps.modalBody}*/} - - -); diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 0e300de7..b6ff477f 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -7,7 +7,6 @@ import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; import App from './App'; -import { DocumentPreviewModal } from './components/modals/DocumentPreviewModal'; import { TransactionPreviewModal } from './components/modals/TransactionPreviewModal'; import './i18n'; import { store } from './states'; @@ -47,7 +46,6 @@ root.render( ('/api/documents', fetcher); + const [lightboxSrc, setLightboxSrc] = useState(undefined); const ledgerTitle = useAppSelector((state) => state.basic.title ?? 'Zhang Accounting'); @@ -23,18 +27,6 @@ export default function Documents() { if (error) return
failed to load
; if (!documents) return
loading...
; - const openDocumentPreviewModal = (filename: string, path: string) => { - openContextModal({ - modal: 'documentPreviewModal', - title: filename, - size: 'lg', - centered: true, - innerProps: { - filename: filename, - path: path, - }, - }); - }; const groupedDocuments = reverse( sortBy( @@ -48,7 +40,7 @@ export default function Documents() { - + {layout === 'Grid' ? ( <> {groupedDocuments.map((targetMonthDocuments, idx) => ( @@ -58,7 +50,7 @@ export default function Documents() { {targetMonthDocuments.map((document, idx) => ( - + ))} @@ -77,7 +69,7 @@ export default function Documents() { {documents.map((document, idx) => ( - openDocumentPreviewModal(document.filename, document.path)}> + setLightboxSrc(document.path) : undefined}>
{document.filename}
diff --git a/frontend/src/pages/SingleAccount.tsx b/frontend/src/pages/SingleAccount.tsx index 861ddec8..e5da847d 100644 --- a/frontend/src/pages/SingleAccount.tsx +++ b/frontend/src/pages/SingleAccount.tsx @@ -15,6 +15,8 @@ import { useAppSelector } from '../states'; import { useDocumentTitle } from '@mantine/hooks'; import { createStyles } from '@mantine/emotion'; import { AccountBalanceHistoryGraph } from '../components/AccountBalanceHistoryGraph'; +import { useState } from 'react'; +import { ImageLightBox } from '../components/ImageLightBox'; const useStyles = createStyles((theme, _) => ({ calculatedAmount: { @@ -30,6 +32,8 @@ function SingleAccount() { let { accountName } = useParams(); const { classes } = useStyles(); + const [lightboxSrc, setLightboxSrc] = useState(undefined); + const { data: account, error } = useSWR(`/api/accounts/${accountName}`, fetcher); const { data: account_balance_data, error: account_balance_error } = useSWR(`/api/accounts/${accountName}/balances`, fetcher); @@ -127,10 +131,11 @@ function SingleAccount() { skeleton={
loading
} render={(data: Document[]) => ( <> + {data.map((document, idx) => ( - + setLightboxSrc(path)} key={idx} uri={document.path} filename={document.path} /> ))} diff --git a/frontend/src/utils/documents.ts b/frontend/src/utils/documents.ts new file mode 100644 index 00000000..8dd18ab6 --- /dev/null +++ b/frontend/src/utils/documents.ts @@ -0,0 +1,6 @@ +export const EXTENSIONS_SUPPORT_PREVIEW = ['png', 'jpg', 'jpeg', 'gif']; + +export function isDocumentAnImage(path: string): boolean { + const extension = path.split('.').pop()?.toLocaleLowerCase() || ''; + return EXTENSIONS_SUPPORT_PREVIEW.includes(extension); +}