feat(create-page): new create page design #153

Merged
enes merged 17 commits from 148-create-page into staging 2024-08-20 12:43:20 +00:00
5 changed files with 361 additions and 195 deletions
Showing only changes of commit 48f54d8568 - Show all commits

View File

@ -535,12 +535,17 @@ export const DrawPDFFields = (props: Props) => {
<div className={styles.view}> <div className={styles.view}>
{pdfFiles.map((pdfFile, pdfFileIndex: number) => { {pdfFiles.map((pdfFile, pdfFileIndex: number) => {
return ( return (
<div key={pdfFile.file.name + pdfFileIndex}> <>
<div className={styles.fileWrapper}> <div
key={pdfFile.file.name}
className={`${styles.fileWrapper} ${styles.scrollTarget}`}
id={`file-${pdfFile.file.name}`}
>
{getPdfPages(pdfFile, pdfFileIndex)} {getPdfPages(pdfFile, pdfFileIndex)}
</div> </div>
{pdfFileIndex < pdfFiles.length - 1 && ( {pdfFileIndex < pdfFiles.length - 1 && (
<Divider <Divider
key={pdfFile.file.name + `-divider`}
sx={{ sx={{
fontSize: '12px', fontSize: '12px',
color: 'rgba(0,0,0,0.15)' color: 'rgba(0,0,0,0.15)'
@ -549,7 +554,7 @@ export const DrawPDFFields = (props: Props) => {
File Separator File Separator
</Divider> </Divider>
)} )}
</div> </>
) )
})} })}

View File

@ -1,3 +1,5 @@
@import '../../styles/sizes.scss';
.pdfFieldItem { .pdfFieldItem {
background: white; background: white;
padding: 10px; padding: 10px;
@ -135,6 +137,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 15px; gap: 15px;
position: relative;
scroll-margin-top: $header-height + $body-vertical-padding;
} }
.view { .view {

View File

@ -10,6 +10,9 @@
.sidesWrap { .sidesWrap {
position: relative; position: relative;
// HACK: Stop grid column from growing
min-width: 0;
} }
.sides { .sides {

View File

@ -1,26 +1,18 @@
import { Clear, DragHandle } from '@mui/icons-material' import { DragHandle } from '@mui/icons-material'
import { import {
Box,
Button, Button,
FormControl,
IconButton, IconButton,
InputLabel, ListItem,
ListItemIcon,
ListItemText,
MenuItem, MenuItem,
Select, Select,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField, TextField,
Tooltip, Tooltip
Typography
} from '@mui/material' } from '@mui/material'
import type { Identifier, XYCoord } from 'dnd-core' import type { Identifier, XYCoord } from 'dnd-core'
import saveAs from 'file-saver' import saveAs from 'file-saver'
import JSZip from 'jszip' import JSZip from 'jszip'
import { MuiFileInput } from 'mui-file-input'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { DndProvider, useDrag, useDrop } from 'react-dnd' import { DndProvider, useDrag, useDrop } from 'react-dnd'
@ -64,6 +56,14 @@ import { PdfFile } from '../../types/drawing'
import { DrawPDFFields } from '../../components/DrawPDFFields' import { DrawPDFFields } from '../../components/DrawPDFFields'
import { Mark } from '../../types/mark.ts' import { Mark } from '../../types/mark.ts'
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx' import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faEye,
faPen,
faPlus,
faTrash,
faUpload
} from '@fortawesome/free-solid-svg-icons'
export const CreatePage = () => { export const CreatePage = () => {
const navigate = useNavigate() const navigate = useNavigate()
@ -76,7 +76,14 @@ export const CreatePage = () => {
const [authUrl, setAuthUrl] = useState<string>() const [authUrl, setAuthUrl] = useState<string>()
const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`) const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`)
const [selectedFiles, setSelectedFiles] = useState<File[]>([]) const [selectedFiles, setSelectedFiles] = useState<File[]>([])
const fileInputRef = useRef<HTMLInputElement>(null)
const handleUploadButtonClick = () => {
if (fileInputRef.current) {
fileInputRef.current.click()
}
}
const [userInput, setUserInput] = useState('') const [userInput, setUserInput] = useState('')
const [userRole, setUserRole] = useState<UserRole>(UserRole.signer) const [userRole, setUserRole] = useState<UserRole>(UserRole.signer)
@ -268,19 +275,22 @@ export const CreatePage = () => {
}) })
} }
const handleSelectFiles = (files: File[]) => { const handleSelectFiles = (event: React.ChangeEvent<HTMLInputElement>) => {
setSelectedFiles((prev) => { if (event.target.files) {
const prevFileNames = prev.map((file) => file.name) setSelectedFiles(Array.from(event.target.files))
}
const newFiles = files.filter(
(file) => !prevFileNames.includes(file.name)
)
return [...prev, ...newFiles]
})
} }
const handleRemoveFile = (fileToRemove: File) => { const handleFileClick = (name: string) => {
document.getElementById(name)?.scrollIntoView({ behavior: 'smooth' })
}
const handleRemoveFile = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
fileToRemove: File
) => {
event.stopPropagation()
setSelectedFiles((prevFiles) => setSelectedFiles((prevFiles) =>
prevFiles.filter((file) => file.name !== fileToRemove.name) prevFiles.filter((file) => file.name !== fileToRemove.name)
) )
@ -705,71 +715,134 @@ export const CreatePage = () => {
<StickySideColumns <StickySideColumns
left={ left={
<div className={styles.flexWrap}> <div className={styles.flexWrap}>
<TextField <div className={styles.inputWrapper}>
label="Title" <TextField
value={title} placeholder="Title"
onChange={(e) => setTitle(e.target.value)} size="small"
variant="outlined" type="text"
/> value={title}
<ul className={styles.paperGroup}> onChange={(e) => setTitle(e.target.value)}
sx={{
width: '100%',
fontSize: '16px',
'& .MuiInputBase-input': {
padding: '7px 14px'
},
'& .MuiOutlinedInput-notchedOutline': {
display: 'none'
}
}}
/>
</div>
<ol className={`${styles.paperGroup} ${styles.orderedFilesList}`}>
{selectedFiles.length > 0 && {selectedFiles.length > 0 &&
selectedFiles.map((file, index) => ( selectedFiles.map((file, index) => (
<li key={index}> <Button
<Typography component="label">{file.name}</Typography> key={index}
<IconButton onClick={() => handleRemoveFile(file)}> className={styles.file}
<Clear style={{ color: 'red' }} />{' '} onClick={() => handleFileClick('file-' + file.name)}
</IconButton> >
</li> <>
<span>{file.name}</span>
<Button
variant="text"
onClick={(event) => handleRemoveFile(event, file)}
sx={{
minWidth: '44px'
}}
>
<FontAwesomeIcon icon={faTrash} />
</Button>
</>
</Button>
))} ))}
</ul> </ol>
<MuiFileInput <Button variant="contained" onClick={handleUploadButtonClick}>
fullWidth <FontAwesomeIcon icon={faUpload} />
multiple Upload new files
placeholder="Choose Files" <input
value={selectedFiles} ref={fileInputRef}
onChange={(value) => handleSelectFiles(value)} hidden={true}
/> multiple={true}
type="file"
onChange={handleSelectFiles}
/>
</Button>
</div> </div>
} }
right={ right={
<div className={styles.flexWrap}> <div className={styles.flexWrap}>
<Typography component="label" variant="h6"> <div className={styles.inputWrapper}>
Add Counterparts
</Typography>
<Box className={styles.paperGroup}>
<TextField <TextField
label="nip05 / npub" placeholder="User (nip05 / npub)"
value={userInput} value={userInput}
onChange={(e) => setUserInput(e.target.value)} onChange={(e) => setUserInput(e.target.value)}
helperText={error} helperText={error}
error={!!error} error={!!error}
fullWidth
sx={{
fontSize: '16px',
'& .MuiInputBase-input': {
padding: '7px 14px'
},
'& .MuiOutlinedInput-notchedOutline': {
display: 'none'
}
}}
/> />
<FormControl fullWidth> <Select
<InputLabel id="select-role-label">Role</InputLabel> aria-label="role"
<Select value={userRole}
labelId="select-role-label" variant="filled"
id="demo-simple-select" // Hide arrow for dropdown
value={userRole} IconComponent={() => null}
label="Role" renderValue={(value) => (
onChange={(e) => setUserRole(e.target.value as UserRole)} <FontAwesomeIcon
> icon={value === UserRole.signer ? faPen : faEye}
<MenuItem value={UserRole.signer}> />
{UserRole.signer} )}
</MenuItem> onChange={(e) => setUserRole(e.target.value as UserRole)}
<MenuItem value={UserRole.viewer}> sx={{
{UserRole.viewer} fontSize: '16px',
</MenuItem> minWidth: '44px',
</Select> '& .MuiInputBase-input': {
</FormControl> padding: '7px 14px!important',
textOverflow: 'unset!important'
}
}}
>
<MenuItem value={UserRole.signer}>
<ListItem>
<ListItemIcon>
<FontAwesomeIcon icon={faPen} />
</ListItemIcon>
<ListItemText>{UserRole.signer}</ListItemText>
</ListItem>
</MenuItem>
<MenuItem value={UserRole.viewer}>
<ListItem>
<ListItemIcon>
<FontAwesomeIcon icon={faEye} />
</ListItemIcon>
<ListItemText>{UserRole.viewer}</ListItemText>
</ListItem>
</MenuItem>
</Select>
<Button <Button
disabled={!userInput} disabled={!userInput}
onClick={handleAddUser} onClick={handleAddUser}
variant="contained" variant="contained"
aria-label="Add"
sx={{
minWidth: '44px',
padding: '11.5px 12px',
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0
}}
> >
Add <FontAwesomeIcon icon={faPlus} />
</Button> </Button>
</Box> </div>
<div className={styles.paperGroup}> <div className={styles.paperGroup}>
<DisplayUser <DisplayUser
@ -779,11 +852,11 @@ export const CreatePage = () => {
handleRemoveUser={handleRemoveUser} handleRemoveUser={handleRemoveUser}
moveSigner={moveSigner} moveSigner={moveSigner}
/> />
<Button onClick={handleCreate} variant="contained">
Create
</Button>
</div> </div>
<Button onClick={handleCreate} variant="contained">
Publish
</Button>
</div> </div>
} }
> >
@ -815,80 +888,76 @@ const DisplayUser = ({
moveSigner moveSigner
}: DisplayUsersProps) => { }: DisplayUsersProps) => {
return ( return (
<TableContainer> <>
<Table> <DndProvider backend={HTML5Backend}>
<TableHead> {users
<TableRow> .filter((user) => user.role === UserRole.signer)
<TableCell className={styles.tableHeaderCell}>User</TableCell> .map((user, index) => (
<TableCell className={styles.tableHeaderCell}>Role</TableCell> <SignerRow
<TableCell>Action</TableCell> key={`signer-${index}`}
</TableRow> userMeta={metadata[user.pubkey]}
</TableHead> user={user}
<TableBody> index={index}
<DndProvider backend={HTML5Backend}> moveSigner={moveSigner}
{users handleUserRoleChange={handleUserRoleChange}
.filter((user) => user.role === UserRole.signer) handleRemoveUser={handleRemoveUser}
.map((user, index) => ( />
<SignerRow ))}
key={`signer-${index}`} </DndProvider>
userMeta={metadata[user.pubkey]} {users
user={user} .filter((user) => user.role === UserRole.viewer)
index={index} .map((user, index) => {
moveSigner={moveSigner} const userMeta = metadata[user.pubkey]
handleUserRoleChange={handleUserRoleChange} return (
handleRemoveUser={handleRemoveUser} <div className={styles.user} key={index}>
<div className={styles.avatar}>
<UserAvatar
pubkey={user.pubkey}
name={
userMeta?.display_name ||
userMeta?.name ||
shorten(hexToNpub(user.pubkey))
}
image={userMeta?.picture}
/> />
))} </div>
</DndProvider> <Select
{users aria-label="role"
.filter((user) => user.role === UserRole.viewer) value={user.role}
.map((user, index) => { variant="outlined"
const userMeta = metadata[user.pubkey] IconComponent={() => null}
return ( renderValue={(value) => (
<TableRow key={index}> <FontAwesomeIcon
<TableCell className={styles.tableCell}> icon={value === UserRole.signer ? faPen : faEye}
<UserAvatar />
pubkey={user.pubkey} )}
name={ onChange={(e) =>
userMeta?.display_name || handleUserRoleChange(e.target.value as UserRole, user.pubkey)
userMeta?.name || }
shorten(hexToNpub(user.pubkey)) sx={{
} fontSize: '16px',
image={userMeta?.picture} minWidth: '44px',
/> '& .MuiInputBase-input': {
</TableCell> padding: '7px 14px!important',
<TableCell className={styles.tableCell}> textOverflow: 'unset!important'
<Select },
fullWidth '& .MuiOutlinedInput-notchedOutline': {
value={user.role} display: 'none'
onChange={(e) => }
handleUserRoleChange( }}
e.target.value as UserRole, >
user.pubkey <MenuItem value={UserRole.signer}>{UserRole.signer}</MenuItem>
) <MenuItem value={UserRole.viewer}>{UserRole.viewer}</MenuItem>
} </Select>
> <Tooltip title="Remove User" arrow>
<MenuItem value={UserRole.signer}> <IconButton onClick={() => handleRemoveUser(user.pubkey)}>
{UserRole.signer} <FontAwesomeIcon icon={faTrash} />
</MenuItem> </IconButton>
<MenuItem value={UserRole.viewer}> </Tooltip>
{UserRole.viewer} </div>
</MenuItem> )
</Select> })}
</TableCell> </>
<TableCell>
<Tooltip title="Remove User" arrow>
<IconButton onClick={() => handleRemoveUser(user.pubkey)}>
<Clear style={{ color: 'red' }} />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</TableContainer>
) )
} }
@ -992,16 +1061,14 @@ const SignerRow = ({
drag(drop(ref)) drag(drop(ref))
return ( return (
<TableRow <div
sx={{ cursor: 'move', opacity }} className={styles.user}
style={{ cursor: 'move', opacity }}
data-handler-id={handlerId} data-handler-id={handlerId}
ref={ref} ref={ref}
> >
<TableCell <DragHandle />
className={styles.tableCell} <div className={styles.avatar}>
sx={{ display: 'flex', alignItems: 'center', gap: '10px' }}
>
<DragHandle />
<UserAvatar <UserAvatar
pubkey={user.pubkey} pubkey={user.pubkey}
name={ name={
@ -1011,26 +1078,38 @@ const SignerRow = ({
} }
image={userMeta?.picture} image={userMeta?.picture}
/> />
</TableCell> </div>
<TableCell className={styles.tableCell}> <Select
<Select aria-label="role"
fullWidth value={user.role}
value={user.role} variant="outlined"
onChange={(e) => IconComponent={() => null}
handleUserRoleChange(e.target.value as UserRole, user.pubkey) renderValue={(value) => (
<FontAwesomeIcon icon={value === UserRole.signer ? faPen : faEye} />
)}
onChange={(e) =>
handleUserRoleChange(e.target.value as UserRole, user.pubkey)
}
sx={{
fontSize: '16px',
minWidth: '44px',
'& .MuiInputBase-input': {
padding: '7px 14px!important',
textOverflow: 'unset!important'
},
'& .MuiOutlinedInput-notchedOutline': {
display: 'none'
} }
> }}
<MenuItem value={UserRole.signer}>{UserRole.signer}</MenuItem> >
<MenuItem value={UserRole.viewer}>{UserRole.viewer}</MenuItem> <MenuItem value={UserRole.signer}>{UserRole.signer}</MenuItem>
</Select> <MenuItem value={UserRole.viewer}>{UserRole.viewer}</MenuItem>
</TableCell> </Select>
<TableCell> <Tooltip title="Remove User" arrow>
<Tooltip title="Remove User" arrow> <IconButton onClick={() => handleRemoveUser(user.pubkey)}>
<IconButton onClick={() => handleRemoveUser(user.pubkey)}> <FontAwesomeIcon icon={faTrash} />
<Clear style={{ color: 'red' }} /> </IconButton>
</IconButton> </Tooltip>
</Tooltip> </div>
</TableCell>
</TableRow>
) )
} }

View File

@ -6,6 +6,60 @@
gap: 15px; gap: 15px;
} }
.orderedFilesList {
counter-reset: item;
list-style-type: none;
margin: 0;
li {
display: flex;
align-items: center;
transition: ease 0.4s;
border-radius: 4px;
background: #ffffff;
padding: 7px 10px;
color: rgba(0, 0, 0, 0.5);
min-height: 45px;
cursor: pointer;
gap: 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&::before {
content: counter(item) ' ';
counter-increment: item;
font-size: 14px;
}
:nth-child(1) {
flex-grow: 1;
font-size: 16px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
button {
color: $primary-main;
}
&:hover,
&.active,
&:focus-within {
background: $primary-main;
color: white;
button {
color: white;
}
}
}
}
.paperGroup { .paperGroup {
border-radius: 4px; border-radius: 4px;
background: white; background: white;
@ -13,28 +67,49 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 15px; gap: 15px;
// Automatic scrolling if paper-group gets large enough
// used for files on the left and users on the right
max-height: 350px;
overflow-x: hidden;
overflow-y: auto;
} }
.subHeader { .inputWrapper {
border-bottom: 0.5px solid; display: flex;
} align-items: center;
.tableHeaderCell { height: 34px;
border-right: 1px solid rgba(224, 224, 224, 1); overflow: hidden;
} border-radius: 4px;
outline: solid 1px #dddddd;
background: white;
.tableCell { width: 100%;
border-right: 1px solid rgba(224, 224, 224, 1);
height: 56px;
.user { &:focus-within {
display: flex; outline-color: $primary-main;
align-items: center; }
gap: 10px; }
.name { .user {
text-align: center; display: flex;
cursor: pointer; gap: 10px;
}
font-size: 14px;
text-align: start;
justify-content: center;
align-items: center;
a:hover {
text-decoration: none;
}
}
.avatar {
flex-grow: 1;
&:first-child {
margin-left: 34px;
} }
} }