diff --git a/src/components/Container/index.tsx b/src/components/Container/index.tsx index 57857b9..5b955bb 100644 --- a/src/components/Container/index.tsx +++ b/src/components/Container/index.tsx @@ -6,6 +6,18 @@ interface ContainerProps { className?: string } +/** + * Container component with pre-defined width, padding and margins for top level layout. + * + * **Important:** To avoid conflicts with `defaultStyle` (changing the `width`, `max-width`, `padding-inline`, and/or `margin-inline`) make sure to either: + * - When using *className* override, that styles are imported after the actual `Container` component + * ``` + * import { Container } from './components/Container' + * import styles from './style.module.scss' + * ``` + * - or add *!important* to imported styles + * - or override styles with *CSSProperties* object + */ export const Container = ({ style = {}, className = '', diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx new file mode 100644 index 0000000..2b49f2f --- /dev/null +++ b/src/components/DisplaySigit/index.tsx @@ -0,0 +1,203 @@ +import { Dispatch, SetStateAction, useEffect } from 'react' +import { Meta, ProfileMetadata } from '../../types' +import { SignedStatus, useSigitMeta } from '../../hooks/useSigitMeta' +import { Event, kinds } from 'nostr-tools' +import { Link } from 'react-router-dom' +import { MetadataController } from '../../controllers' +import { hexToNpub, npubToHex, shorten } from '../../utils' +import { appPublicRoutes, appPrivateRoutes } from '../../routes' +import { Button, Divider, Tooltip } from '@mui/material' +import { DisplaySigner } from '../DisplaySigner' +import { + faArchive, + faCalendar, + faCopy, + faEye, + faFilePdf +} from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { UserAvatar } from '../UserAvatar' +import { UserAvatarGroup } from '../UserAvatarGroup' + +import styles from './style.module.scss' +import { TooltipChild } from '../TooltipChild' + +type SigitProps = { + meta: Meta + profiles: { [key: string]: ProfileMetadata } + setProfiles: Dispatch> +} + +// function +// const ExtensionIconMapper = new Map([ +// [ +// 'pdf', +// <> +// PDF +// +// ], +// [ +// 'csv', +// <> +// CSV +// +// ] +// ]) + +export const DisplaySigit = ({ meta, profiles, setProfiles }: SigitProps) => { + const { + title, + createdAt, + submittedBy, + signers, + signedStatus + // fileExtensions + } = useSigitMeta(meta) + + useEffect(() => { + const hexKeys: string[] = [] + + if (submittedBy) { + hexKeys.push(npubToHex(submittedBy)!) + } + hexKeys.push(...signers.map((signer) => npubToHex(signer)!)) + + const metadataController = new MetadataController() + hexKeys.forEach((key) => { + if (!(key in profiles)) { + const handleMetadataEvent = (event: Event) => { + const metadataContent = + metadataController.extractProfileMetadataContent(event) + + if (metadataContent) + setProfiles((prev) => ({ + ...prev, + [key]: metadataContent + })) + } + + metadataController.on(key, (kind: number, event: Event) => { + if (kind === kinds.Metadata) { + handleMetadataEvent(event) + } + }) + + metadataController + .findMetadata(key) + .then((metadataEvent) => { + if (metadataEvent) handleMetadataEvent(metadataEvent) + }) + .catch((err) => { + console.error(`error occurred in finding metadata for: ${key}`, err) + }) + } + }) + }, [submittedBy, signers, profiles, setProfiles]) + + return ( + +

{title}

+
+ {submittedBy && + (function () { + const profile = profiles[submittedBy] + return ( + + + + + + ) + })()} + {submittedBy && signers.length ? ( + + ) : null} + + {signers.map((signer) => { + const pubkey = npubToHex(signer)! + const profile = profiles[pubkey] + + return ( + + + + + + ) + })} + +
+
+ + {createdAt} +
+
+ + {signedStatus} + + + {/* {fileExtensions.map(ext => + + return + )} */} + {'PDF'} + +
+
+ + + + + + +
+ + ) +} diff --git a/src/components/DisplaySigit/style.module.scss b/src/components/DisplaySigit/style.module.scss new file mode 100644 index 0000000..a615713 --- /dev/null +++ b/src/components/DisplaySigit/style.module.scss @@ -0,0 +1,129 @@ +@import '../../styles/colors.scss'; + +.itemWrapper { + position: relative; + overflow: hidden; + background-color: $overlay-background-color; + border-radius: 4px; + + display: flex; + padding: 15px; + gap: 15px; + flex-direction: column; + + cursor: pointer; + + &:only-child { + max-width: 600px; + } + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + bottom: 0; + transition: opacity ease 0.2s; + opacity: 0; + width: 4px; + background-color: $primary-main; + pointer-events: none; + } + + &:hover, + &:focus-within { + &::before { + opacity: 1; + } + + .itemActions { + transform: translateX(0); + } + } +} + +.itemActions { + display: flex; + gap: 10px; + padding: 10px; + + > * { + flex-grow: 1; + } + + @media (hover: hover) { + transition: ease 0.2s; + transform: translateX(100%); + position: absolute; + right: 0; + top: 0; + bottom: 0; + + flex-direction: column; + background: $overlay-background-color; + border-left: solid 1px rgba(0, 0, 0, 0.1); + + &:hover, + &:focus-within { + transform: translateX(0); + } + } + + @media (hover: none) { + border-top: solid 1px rgba(0, 0, 0, 0.1); + padding-top: 10px; + margin-inline: -15px; + margin-bottom: -15px; + } +} + +.title { + font-size: 20px; +} + +.users { + margin-top: auto; + + display: flex; + grid-gap: 10px; +} + +.signers { + padding: 0 0 0 10px; + + > * { + transition: margin ease 0.2s; + margin: 0 0 0 -10px; + position: relative; + z-index: 1; + &:first-child { + margin-left: -10px !important; + } + } + + > *:hover, + > *:focus-within { + margin: 0 15px 0 5px; + z-index: 2; + } +} + +.details { + color: rgba(0, 0, 0, 0.3); + font-size: 14px; +} + +.iconLabel { + display: flex; + grid-gap: 10px; + align-items: center; +} + +.status { + display: flex; + grid-gap: 25px; +} + +a.itemWrapper:hover { + text-decoration: none; +} diff --git a/src/components/DisplaySigner/index.tsx b/src/components/DisplaySigner/index.tsx new file mode 100644 index 0000000..a7d3f5b --- /dev/null +++ b/src/components/DisplaySigner/index.tsx @@ -0,0 +1,76 @@ +import { Badge } from '@mui/material' +import { Event, verifyEvent } from 'nostr-tools' +import { useState, useEffect } from 'react' +import { Meta, ProfileMetadata } from '../../types' +import { hexToNpub, parseJson } from '../../utils' +import styles from './style.module.scss' +import { UserAvatar } from '../UserAvatar' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCheck, faExclamation } from '@fortawesome/free-solid-svg-icons' + +enum SignStatus { + Signed = 'Signed', + Pending = 'Pending', + Invalid = 'Invalid Sign' +} + +type DisplaySignerProps = { + meta: Meta + profile: ProfileMetadata + pubkey: string +} + +export const DisplaySigner = ({ + meta, + profile, + pubkey +}: DisplaySignerProps) => { + const [signStatus, setSignedStatus] = useState() + + useEffect(() => { + const updateSignStatus = async () => { + const npub = hexToNpub(pubkey) + if (npub in meta.docSignatures) { + parseJson(meta.docSignatures[npub]) + .then((event) => { + const isValidSignature = verifyEvent(event) + if (isValidSignature) { + setSignedStatus(SignStatus.Signed) + } else { + setSignedStatus(SignStatus.Invalid) + } + }) + .catch((err) => { + console.log(`err in parsing the docSignatures for ${npub}:>> `, err) + setSignedStatus(SignStatus.Invalid) + }) + } else { + setSignedStatus(SignStatus.Pending) + } + } + + updateSignStatus() + }, [meta, pubkey]) + + return ( + + {signStatus === SignStatus.Signed && ( + + )} + {signStatus === SignStatus.Invalid && ( + + )} + + ) + } + > + + + ) +} diff --git a/src/components/DisplaySigner/style.module.scss b/src/components/DisplaySigner/style.module.scss new file mode 100644 index 0000000..fa62cab --- /dev/null +++ b/src/components/DisplaySigner/style.module.scss @@ -0,0 +1,23 @@ +@import '../../styles/colors.scss'; + +.statusBadge { + width: 22px; + height: 22px; + border-radius: 50%; + + color: white; + + display: flex; + align-items: center; + justify-content: center; + + font-size: 10px; + + background-color: $primary-main; +} + +.signer { + background-color: white; + border-radius: 50%; + z-index: 1; +} diff --git a/src/components/TooltipChild.tsx b/src/components/TooltipChild.tsx new file mode 100644 index 0000000..d70f916 --- /dev/null +++ b/src/components/TooltipChild.tsx @@ -0,0 +1,11 @@ +import { forwardRef, PropsWithChildren } from 'react' + +export const TooltipChild = forwardRef( + ({ children, ...rest }, ref) => { + return ( + + {children} + + ) + } +) diff --git a/src/components/UserAvatar/index.tsx b/src/components/UserAvatar/index.tsx index 65e993d..5382475 100644 --- a/src/components/UserAvatar/index.tsx +++ b/src/components/UserAvatar/index.tsx @@ -1,12 +1,11 @@ -import { useNavigate } from 'react-router-dom' import { getProfileRoute } from '../../routes' import styles from './styles.module.scss' -import React from 'react' import { AvatarIconButton } from '../UserAvatarIconButton' +import { Link } from 'react-router-dom' interface UserAvatarProps { - name: string + name?: string pubkey: string image?: string } @@ -16,27 +15,18 @@ interface UserAvatarProps { * Clicking will navigate to the user's profile. */ export const UserAvatar = ({ pubkey, name, image }: UserAvatarProps) => { - const navigate = useNavigate() - - const handleClick = (e: React.MouseEvent) => { - e.stopPropagation() - navigate(getProfileRoute(pubkey)) - } - return ( -
+ - {name ? ( - - ) : null} -
+ {name ? : null} + ) } diff --git a/src/components/UserAvatar/styles.module.scss b/src/components/UserAvatar/styles.module.scss index 6147819..fbe8cf5 100644 --- a/src/components/UserAvatar/styles.module.scss +++ b/src/components/UserAvatar/styles.module.scss @@ -2,7 +2,7 @@ display: flex; align-items: center; gap: 10px; - flex-grow: 1; + // flex-grow: 1; } .username { diff --git a/src/components/UserAvatarGroup/index.tsx b/src/components/UserAvatarGroup/index.tsx new file mode 100644 index 0000000..13f8b25 --- /dev/null +++ b/src/components/UserAvatarGroup/index.tsx @@ -0,0 +1,38 @@ +import { Children, PropsWithChildren } from 'react' + +import styles from './style.module.scss' + +interface UserAvatarGroupProps extends React.HTMLAttributes { + max: number + renderSurplus?: ((surplus: number) => React.ReactNode) | undefined +} + +const defaultSurplus = (surplus: number) => { + return +{surplus} +} + +/** + * Renders children with the `max` limit (including surplus if available). + * The children are wrapped with a `div` (accepts standard `HTMLDivElement` attributes) + * @param max The maximum number of children rendered in a div. + * @param renderSurplus Custom render for surplus children (accepts surplus number). + */ +export const UserAvatarGroup = ({ + max, + renderSurplus = defaultSurplus, + children, + ...rest +}: PropsWithChildren) => { + const total = Children.count(children) + const surplus = total - max + 1 + + const childrenArray = Children.toArray(children) + return ( +
+ {surplus > 1 + ? childrenArray.slice(0, surplus * -1).map((c) => c) + : children} + {surplus > 1 && renderSurplus(surplus)} +
+ ) +} diff --git a/src/components/UserAvatarGroup/style.module.scss b/src/components/UserAvatarGroup/style.module.scss new file mode 100644 index 0000000..9604202 --- /dev/null +++ b/src/components/UserAvatarGroup/style.module.scss @@ -0,0 +1,19 @@ +@import '../../styles/colors.scss'; + +.icon { + width: 40px; + height: 40px; + border-radius: 50%; + border-width: 2px; + overflow: hidden; + + display: inline-flex; + align-items: center; + justify-content: center; + + background: white; + color: rgba(0, 0, 0, 0.5); + font-weight: bold; + font-size: 14px; + border: solid 2px $primary-main; +} diff --git a/src/components/UserAvatarIconButton/style.module.scss b/src/components/UserAvatarIconButton/style.module.scss index f8213f0..57f2688 100644 --- a/src/components/UserAvatarIconButton/style.module.scss +++ b/src/components/UserAvatarIconButton/style.module.scss @@ -2,6 +2,6 @@ width: 40px; height: 40px; border-radius: 50%; - border-width: 3px; + border-width: 2px; overflow: hidden; } diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx new file mode 100644 index 0000000..b87f3b3 --- /dev/null +++ b/src/hooks/useSigitMeta.tsx @@ -0,0 +1,86 @@ +import { useEffect, useState } from 'react' +import { toast } from 'react-toastify' +import { CreateSignatureEventContent, Meta } from '../types' +import { parseJson, formatTimestamp } from '../utils' +import { Event } from 'nostr-tools' + +export enum SignedStatus { + Partial = 'In-Progress', + Complete = 'Completed' +} + +export const useSigitMeta = (meta: Meta) => { + const [title, setTitle] = useState() + const [createdAt, setCreatedAt] = useState('') + const [submittedBy, setSubmittedBy] = useState() + const [signers, setSigners] = useState<`npub1${string}`[]>([]) + const [signedStatus, setSignedStatus] = useState( + SignedStatus.Partial + ) + const [fileExtensions, setFileExtensions] = useState([]) + + useEffect(() => { + const extractInfo = async () => { + const createSignatureEvent = await parseJson( + meta.createSignature + ).catch((err) => { + console.log('err in parsing the createSignature event:>> ', err) + toast.error( + err.message || 'error occurred in parsing the create signature event' + ) + return null + }) + + if (!createSignatureEvent) return + + // created_at in nostr events are stored in seconds + // convert it to ms before formatting + setCreatedAt(formatTimestamp(createSignatureEvent.created_at * 1000)) + + const createSignatureContent = + await parseJson( + createSignatureEvent.content + ).catch((err) => { + console.log( + `err in parsing the createSignature event's content :>> `, + err + ) + return null + }) + + if (!createSignatureContent) return + + const files = Object.keys(createSignatureContent.fileHashes) + const extensions = files.reduce((result: string[], file: string) => { + const extension = file.split('.').pop() + if (extension) { + result.push(extension) + } + return result + }, []) + + setTitle(createSignatureContent.title) + setSubmittedBy(createSignatureEvent.pubkey) + setSigners(createSignatureContent.signers) + setFileExtensions(extensions) + + const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[] + const isCompletelySigned = createSignatureContent.signers.every( + (signer) => signedBy.includes(signer) + ) + if (isCompletelySigned) { + setSignedStatus(SignedStatus.Complete) + } + } + extractInfo() + }, [meta]) + + return { + title, + createdAt, + submittedBy, + signers, + signedStatus, + fileExtensions + } +} diff --git a/src/index.css b/src/index.css index 76373ff..7ee0eea 100644 --- a/src/index.css +++ b/src/index.css @@ -101,6 +101,15 @@ button:disabled { color: inherit !important; } +.line-clamp-2 { + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 2; + line-clamp: 2; +} + .profile-image { width: 40px; height: 40px; diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 4ad3c3b..07304b6 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -1,24 +1,42 @@ -import { CalendarMonth, Description, Upload } from '@mui/icons-material' -import { Box, Button, Tooltip, Typography } from '@mui/material' +import { Button, Divider, TextField, Tooltip } from '@mui/material' import JSZip from 'jszip' -import { Event, kinds, verifyEvent } from 'nostr-tools' -import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { toast } from 'react-toastify' -import { UserAvatar } from '../../components/UserAvatar' -import { MetadataController } from '../../controllers' import { useAppSelector } from '../../hooks' import { appPrivateRoutes, appPublicRoutes } from '../../routes' -import { CreateSignatureEventContent, Meta, ProfileMetadata } from '../../types' +import { Meta, ProfileMetadata } from '../../types' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { - formatTimestamp, - hexToNpub, - npubToHex, - parseJson, - shorten -} from '../../utils' -import styles from './style.module.scss' + faAdd, + faFilter, + faFilterCircleXmark, + faSearch +} from '@fortawesome/free-solid-svg-icons' +import { Select } from '../../components/Select' +import { DisplaySigit } from '../../components/DisplaySigit' + import { Container } from '../../components/Container' +import styles from './style.module.scss' + +// Unsupported Filter options are commented +const FILTERS = [ + 'Show all', + // 'Drafts', + 'In-progress', + 'Completed' + // 'Archived' +] as const +type Filter = (typeof FILTERS)[number] + +const SORT_BY = [ + { + label: 'Newest', + value: 'desc' + }, + { label: 'Oldest', value: 'asc' } +] as const +type Sort = (typeof SORT_BY)[number]['value'] export const HomePage = () => { const navigate = useNavigate() @@ -81,54 +99,112 @@ export const HomePage = () => { } } + const [filter, setFilter] = useState('Show all') + const [isFilterVisible, setIsFilterVisible] = useState(true) + const [sort, setSort] = useState('asc') + + console.log(filter, sort) + return ( - - - Sigits - - {/* This is for desktop view */} - - - - - {/* This is for mobile view */} - - - + + + + - - - + + + + + + +
+
Click or drag files to upload!
+
+
{sigits.map((sigit, index) => ( { setProfiles={setProfiles} /> ))} - +
) } - -type SigitProps = { - meta: Meta - profiles: { [key: string]: ProfileMetadata } - setProfiles: Dispatch> -} - -enum SignedStatus { - Partial = 'Partially Signed', - Complete = 'Completely Signed' -} - -const DisplaySigit = ({ meta, profiles, setProfiles }: SigitProps) => { - const navigate = useNavigate() - - const [title, setTitle] = useState() - const [createdAt, setCreatedAt] = useState('') - const [submittedBy, setSubmittedBy] = useState() - const [signers, setSigners] = useState<`npub1${string}`[]>([]) - const [signedStatus, setSignedStatus] = useState( - SignedStatus.Partial - ) - - useEffect(() => { - const extractInfo = async () => { - const createSignatureEvent = await parseJson( - meta.createSignature - ).catch((err) => { - console.log('err in parsing the createSignature event:>> ', err) - toast.error( - err.message || 'error occurred in parsing the create signature event' - ) - return null - }) - - if (!createSignatureEvent) return - - // created_at in nostr events are stored in seconds - // convert it to ms before formatting - setCreatedAt(formatTimestamp(createSignatureEvent.created_at * 1000)) - - const createSignatureContent = - await parseJson( - createSignatureEvent.content - ).catch((err) => { - console.log( - `err in parsing the createSignature event's content :>> `, - err - ) - return null - }) - - if (!createSignatureContent) return - - setTitle(createSignatureContent.title) - setSubmittedBy(createSignatureEvent.pubkey) - setSigners(createSignatureContent.signers) - - const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[] - const isCompletelySigned = createSignatureContent.signers.every( - (signer) => signedBy.includes(signer) - ) - if (isCompletelySigned) { - setSignedStatus(SignedStatus.Complete) - } - } - extractInfo() - }, [meta]) - - useEffect(() => { - const hexKeys: string[] = [] - - if (submittedBy) { - hexKeys.push(npubToHex(submittedBy)!) - } - hexKeys.push(...signers.map((signer) => npubToHex(signer)!)) - - const metadataController = new MetadataController() - hexKeys.forEach((key) => { - if (!(key in profiles)) { - const handleMetadataEvent = (event: Event) => { - const metadataContent = - metadataController.extractProfileMetadataContent(event) - - if (metadataContent) - setProfiles((prev) => ({ - ...prev, - [key]: metadataContent - })) - } - - metadataController.on(key, (kind: number, event: Event) => { - if (kind === kinds.Metadata) { - handleMetadataEvent(event) - } - }) - - metadataController - .findMetadata(key) - .then((metadataEvent) => { - if (metadataEvent) handleMetadataEvent(metadataEvent) - }) - .catch((err) => { - console.error(`error occurred in finding metadata for: ${key}`, err) - }) - } - }) - }, [submittedBy, signers]) - - const handleNavigation = () => { - if (signedStatus === SignedStatus.Complete) { - navigate(appPublicRoutes.verify, { state: { meta } }) - } else { - navigate(appPrivateRoutes.sign, { state: { meta } }) - } - } - - return ( - - - - - {title} - - {submittedBy && - (function () { - const profile = profiles[submittedBy] - return ( - - ) - })()} - - - {createdAt} - - - - {signers.map((signer) => { - const pubkey = npubToHex(signer)! - const profile = profiles[pubkey] - - return ( - - ) - })} - - - ) -} - -enum SignStatus { - Signed = 'Signed', - Pending = 'Pending', - Invalid = 'Invalid Sign' -} - -type DisplaySignerProps = { - meta: Meta - profile: ProfileMetadata - pubkey: string -} - -const DisplaySigner = ({ meta, profile, pubkey }: DisplaySignerProps) => { - const [signStatus, setSignedStatus] = useState() - - useEffect(() => { - const updateSignStatus = async () => { - const npub = hexToNpub(pubkey) - if (npub in meta.docSignatures) { - parseJson(meta.docSignatures[npub]) - .then((event) => { - const isValidSignature = verifyEvent(event) - if (isValidSignature) { - setSignedStatus(SignStatus.Signed) - } else { - setSignedStatus(SignStatus.Invalid) - } - }) - .catch((err) => { - console.log(`err in parsing the docSignatures for ${npub}:>> `, err) - setSignedStatus(SignStatus.Invalid) - }) - } else { - setSignedStatus(SignStatus.Pending) - } - } - - updateSignStatus() - }, [meta, pubkey]) - - return ( - - - {signStatus} - - - - ) -} diff --git a/src/pages/home/style.module.scss b/src/pages/home/style.module.scss index 132097e..21aeb30 100644 --- a/src/pages/home/style.module.scss +++ b/src/pages/home/style.module.scss @@ -1,94 +1,78 @@ +@import '../../styles/colors.scss'; + .container { display: flex; flex-direction: column; gap: 25px; + container-type: inline-size; } .header { display: flex; + gap: 10px; + align-items: center; - .title { - color: var(--mui-palette-primary-light); - flex: 1; + @container (width < 610px) { + flex-wrap: wrap; + } +} + +.actionButtons { + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + margin-left: auto; + padding: 1.5px 0; +} + +.search { + display: flex; + align-items: center; + justify-content: end; + + height: 34px; + overflow: hidden; + border-radius: 4px; + outline: solid 1px #dddddd; + background: white; + + &:focus-within { + outline-color: $primary-main; + } +} + +.dropzone { + background-color: $overlay-background-color; + height: 250px; + transition: padding ease 0.2s; + padding: 15px; + + &:hover { + padding: 10px; + + > div { + background: rgba(0, 0, 0, 0.15); + } } - .actionButtons { + > div { + transition: background-color ease 0.2s; + display: flex; + flex-direction: column; justify-content: center; align-items: center; - gap: 10px; + background: rgba(0, 0, 0, 0.1); + color: rgba(0, 0, 0, 0.25); + height: 100%; + border-radius: 2px; + border: dashed 3px rgba(0, 0, 0, 0.1); + font-size: 16px; } } .submissions { - display: flex; - flex-direction: column; - gap: 10px; - - .item { - display: flex; - gap: 10px; - background-color: #efeae6; - border-radius: 1rem; - cursor: pointer; - - .titleBox { - display: flex; - flex: 4; - flex-direction: column; - align-items: center; - overflow-wrap: anywhere; - gap: 10px; - padding: 10px; - background-color: #cdc8c499; - border-top-left-radius: inherit; - - .title { - display: flex; - justify-content: center; - align-items: center; - color: var(--mui-palette-primary-light); - font-size: 1.5rem; - - svg { - font-size: 1.5rem; - } - } - - .date { - display: flex; - justify-content: center; - align-items: center; - color: var(--mui-palette-primary-light); - font-size: 1rem; - - svg { - font-size: 1rem; - } - } - } - - .signers { - display: flex; - flex-direction: column; - flex: 6; - justify-content: center; - gap: 10px; - padding: 10px; - color: var(--mui-palette-primary-light); - - .signerItem { - display: flex; - justify-content: center; - align-items: center; - gap: 10px; - - .status { - border-radius: 2rem; - width: 100px; - text-align: center; - background-color: var(--mui-palette-info-light); - } - } - } - } + display: grid; + gap: 25px; + grid-template-columns: repeat(auto-fit, minmax(365px, 1fr)); }