From fc601c83e71d51e6f9d224868d1cead77ca3993a Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Wed, 25 Sep 2024 16:54:43 +0100 Subject: [PATCH] Feat: QR code --- src/app/components/StyledDialogActions.tsx | 8 + src/app/components/StyledDialogContent.tsx | 8 + src/app/components/StyledDialogTitle.tsx | 5 + .../shared-links/Components/AddLinkDialog.tsx | 21 +- .../shared-links/Components/LinksTable.tsx | 95 +++++---- .../shared-links/Components/QRCodeDialog.tsx | 184 ++++++++++++++++++ src/app/utils/api.class.ts | 10 + 7 files changed, 279 insertions(+), 52 deletions(-) create mode 100644 src/app/components/StyledDialogActions.tsx create mode 100644 src/app/components/StyledDialogContent.tsx create mode 100644 src/app/components/StyledDialogTitle.tsx create mode 100644 src/app/shared-links/Components/QRCodeDialog.tsx diff --git a/src/app/components/StyledDialogActions.tsx b/src/app/components/StyledDialogActions.tsx new file mode 100644 index 0000000..07c2ce6 --- /dev/null +++ b/src/app/components/StyledDialogActions.tsx @@ -0,0 +1,8 @@ +import { DialogActions, styled } from '@mui/material'; + +export const StyledDialogActions = styled(DialogActions)(() => ({ + paddingTop: '15px', + paddingRight: '25px', + paddingBottom: '15px', + backgroundImage: 'linear-gradient(to top, hsla(0, 0%, 90%, .05), #e6e6e6)', +})); diff --git a/src/app/components/StyledDialogContent.tsx b/src/app/components/StyledDialogContent.tsx new file mode 100644 index 0000000..dc7bf3a --- /dev/null +++ b/src/app/components/StyledDialogContent.tsx @@ -0,0 +1,8 @@ +import { styled } from '@mui/material'; + +export const StyledDialogContent = styled('div')(() => ({ + gap: '15px', + margin: '15px', + display: 'flex', + flexDirection: 'column', +})); diff --git a/src/app/components/StyledDialogTitle.tsx b/src/app/components/StyledDialogTitle.tsx new file mode 100644 index 0000000..38f9bc4 --- /dev/null +++ b/src/app/components/StyledDialogTitle.tsx @@ -0,0 +1,5 @@ +import { DialogTitle, styled } from '@mui/material'; + +export const StyledDialogTitle = styled(DialogTitle)(() => ({ + backgroundImage: 'linear-gradient(to bottom, hsla(0, 0%, 90%, .05), #e6e6e6)', +})); diff --git a/src/app/shared-links/Components/AddLinkDialog.tsx b/src/app/shared-links/Components/AddLinkDialog.tsx index bacbb5e..4724b93 100644 --- a/src/app/shared-links/Components/AddLinkDialog.tsx +++ b/src/app/shared-links/Components/AddLinkDialog.tsx @@ -11,6 +11,9 @@ import { import { FC, useEffect } from 'react'; import { useForm } from 'react-hook-form'; +import { StyledDialogActions } from '@/app/components/StyledDialogActions'; +import { StyledDialogContent } from '@/app/components/StyledDialogContent'; +import { StyledDialogTitle } from '@/app/components/StyledDialogTitle'; import { useAuth } from '@/app/context/AuthProvider'; import { apiSharedLink } from '@/app/utils/api.class'; import { CreateSHLinkDto } from '@/domain/dtos/shlink'; @@ -19,24 +22,6 @@ const removeUndefinedValues = >( object: T, ): T => JSON.parse(JSON.stringify(object)); -const StyledDialogTitle = styled(DialogTitle)(() => ({ - backgroundImage: 'linear-gradient(to bottom, hsla(0, 0%, 90%, .05), #e6e6e6)', -})); - -const StyledDialogContent = styled('div')(() => ({ - gap: '15px', - margin: '15px', - display: 'flex', - flexDirection: 'column', -})); - -const StyledDialogActions = styled(DialogActions)(() => ({ - paddingTop: '15px', - paddingRight: '25px', - paddingBottom: '15px', - backgroundImage: 'linear-gradient(to top, hsla(0, 0%, 90%, .05), #e6e6e6)', -})); - export type TCreateSHLinkDto = Omit & { configExp?: string; }; diff --git a/src/app/shared-links/Components/LinksTable.tsx b/src/app/shared-links/Components/LinksTable.tsx index 0967e2f..d395309 100644 --- a/src/app/shared-links/Components/LinksTable.tsx +++ b/src/app/shared-links/Components/LinksTable.tsx @@ -1,5 +1,5 @@ 'use client'; - +import { QrCode } from '@mui/icons-material'; import CalendarTodayIcon from '@mui/icons-material/CalendarToday'; import LinkOffIcon from '@mui/icons-material/LinkOff'; import { @@ -14,21 +14,18 @@ import { TablePagination, TableRow, Tooltip, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, } from '@mui/material'; import { useEffect, useState } from 'react'; import React from 'react'; import { apiSharedLink } from '@/app/utils/api.class'; +import { uuid } from '@/app/utils/helpers'; import { SHLinkMiniDto } from '@/domain/dtos/shlink'; import { AddLinkDialog } from './AddLinkDialog'; import BooleanIcon from './BooleanIcon'; import ConfirmationDialog from './ConfirmationDialog'; +import { QRCodeDialog } from './QRCodeDialog'; interface Column { id: keyof SHLinkMiniDto; @@ -40,20 +37,26 @@ interface Column { ) => string | React.JSX.Element; } -interface IActionColumn extends Omit { +interface IActionColumns extends Omit { id: 'action'; label: React.JSX.Element; - action: (id: string) => void; + action: (row: SHLinkMiniDto) => void; + tooltipTitle?: string; + isDisabled?: (row: SHLinkMiniDto) => boolean; } const createActionColumn = ( label: React.JSX.Element, - action: (id: string) => void, -): IActionColumn => ({ + action: (row: SHLinkMiniDto) => void, + tooltipTitle?: string, + isDisabled?: (row: SHLinkMiniDto) => boolean, +): IActionColumns => ({ id: 'action', label, minWidth: 50, action, + tooltipTitle, + isDisabled, }); const columns: readonly Column[] = [ @@ -90,12 +93,19 @@ export default function LinksTable() { const [confirmDialogOpen, setConfirmDialogOpen] = useState(false); const [selectedLinkId, setSelectedLinkId] = useState(null); + const [qrCodeDialogOpen, setQrCodeDialogOpen] = useState(false); + const [qrCodeData, setQrCodeData] = useState<{ + id: string; + managementToken: string; + url: string; + }>(); + const handleChangePage = (_event: unknown, newPage: number) => { setPage(newPage); }; - const handleDeactivate = async (id: string) => { - setSelectedLinkId(id); + const handleDeactivate = async (row: SHLinkMiniDto) => { + setSelectedLinkId(row.id); setConfirmDialogOpen(true); }; @@ -110,10 +120,19 @@ export default function LinksTable() { } } }; + const handleQrCode = async (row: SHLinkMiniDto) => { + setQrCodeDialogOpen(true); + setQrCodeData({ id: row.id, managementToken: '', url: row.url }); + }; - const actionColumn: IActionColumn[] = [ - createActionColumn(, handleDeactivate), - // TODO: Other actions will be added + const actionColumns: IActionColumns[] = [ + createActionColumn( + , + handleDeactivate, + 'Deactivate', + (row) => row.active, + ), + createActionColumn(, handleQrCode, 'Show QR Code'), ]; const fetchLinks = async () => { @@ -136,8 +155,6 @@ export default function LinksTable() { setAddDialog(true); }; - const combinedCols = [...columns, ...actionColumn]; - return ( + {qrCodeDialogOpen && ( + { + setQrCodeDialogOpen(false); + }} + /> + )} - - - ) : column.format ? ( - column.format(value) - ) : ( - value?.toString() - )} + {column.format + ? column.format(value) + : value?.toString()} ); })} + + {actionColumns.map((actionColumn) => ( + + + + + + ))} + ))} diff --git a/src/app/shared-links/Components/QRCodeDialog.tsx b/src/app/shared-links/Components/QRCodeDialog.tsx new file mode 100644 index 0000000..c843cef --- /dev/null +++ b/src/app/shared-links/Components/QRCodeDialog.tsx @@ -0,0 +1,184 @@ +'use client'; +import { ContentCopy, Check, FileCopy } from '@mui/icons-material'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + styled, + CircularProgress, + Grid, +} from '@mui/material'; +import Image from 'next/image'; +import { FC, useEffect, useState } from 'react'; + +import { StyledDialogActions } from '@/app/components/StyledDialogActions'; +import { StyledDialogContent } from '@/app/components/StyledDialogContent'; +import { StyledDialogTitle } from '@/app/components/StyledDialogTitle'; +import { apiSharedLink } from '@/app/utils/api.class'; +import { CreateSHLinkDto } from '@/domain/dtos/shlink'; + +export type TCreateSHLinkDto = Omit & { + configExp?: string; +}; + +interface QRCodeDialogProps { + open?: boolean; + data?: { + id: string; + managementToken: string; + url: string; + }; + onClose?: () => void; +} + +type TCopyStatus = 'loading' | 'copy' | 'copied'; + +export const QRCodeDialog: FC = ({ + open, + data, + onClose, +}) => { + const [qrCodeBlob, setQrCodeBlob] = useState(); + const [qrCodeUrl, setQrCodeUrl] = useState(''); + const [QrCopyStatus, setQrCopyStatus] = useState('copy'); + const [linkCopyStatus, setLinkCopyStatus] = useState('copy'); + + useEffect(() => { + if (open && data?.id) { + apiSharedLink + .getQrCode(data.id, { + managementToken: data?.managementToken, + }) + .then(async (response) => { + setQrCodeUrl( + `data:image/png;base64,${Buffer.from(response.data, 'binary').toString('base64')}`, + ); + setQrCodeBlob(new Blob([response.data], { type: 'image/png' })); + }); + } + }, [data?.id, data?.managementToken, open]); + + return ( + onClose?.()}> + Scan your QR Code + + + + + {qrCodeUrl ? ( + + ) : ( + + )} + + + + + + + + + + + + + + + ); +}; diff --git a/src/app/utils/api.class.ts b/src/app/utils/api.class.ts index 8df3621..e35428d 100644 --- a/src/app/utils/api.class.ts +++ b/src/app/utils/api.class.ts @@ -52,6 +52,16 @@ export class ApiSHLink extends BaseApi { url: `/users/${userId}/ips`, }); } + + async getQrCode(linkId: string, data: object) { + return await this.create({ + url: `/${EEndpoint.shareLinks}/${linkId}/qrcode`, + data, + params: { + responseType: 'arraybuffer', + }, + }); + } } instance.interceptors.response.use(