-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Display user's shared links table + Add new links #64
Changes from all commits
82bee82
29ef85f
e683626
734e8d7
1a67c9d
a414246
f029a89
8565d0a
4950d31
40c3e33
e7f77ae
dd86e30
05e3e9b
7ae343b
e9d0d34
004fde7
0616346
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { loadEnvConfig } from '@next/env'; | ||
|
||
const projectDir = process.cwd(); | ||
loadEnvConfig(projectDir); | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
'use client'; | ||
import { type Session } from 'next-auth'; | ||
import { getSession } from 'next-auth/react'; | ||
import { useCallback, useEffect, useState } from 'react'; | ||
|
||
export const useSession = () => { | ||
const [session, setSession] = useState<Session>(); | ||
|
||
const fetchSession = useCallback(async () => { | ||
const serverSession = await getSession(); | ||
if (serverSession) setSession(serverSession); | ||
}, []); | ||
|
||
useEffect(() => { | ||
fetchSession(); | ||
}, [fetchSession]); | ||
|
||
return session; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
'use client'; | ||
import { | ||
Dialog, | ||
DialogTitle, | ||
DialogContent, | ||
TextField, | ||
DialogActions, | ||
Button, | ||
styled, | ||
} from '@mui/material'; | ||
import { FC, useEffect } from 'react'; | ||
import { useForm } from 'react-hook-form'; | ||
|
||
import { useSession } from '@/app/hooks/useSession'; | ||
import { apiSharedLink } from '@/app/utils/api.class'; | ||
import { CreateSHLinkDto } from '@/domain/dtos/shlink'; | ||
|
||
const removeUndefinedValues = <T extends Record<string, unknown>>( | ||
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<CreateSHLinkDto, 'configExp'> & { | ||
configExp?: string; | ||
}; | ||
|
||
interface AddLinkDialogProps { | ||
open?: boolean; | ||
onClose?: () => void; | ||
callback?: () => void; | ||
} | ||
|
||
export const AddLinkDialog: FC<AddLinkDialogProps> = ({ | ||
open, | ||
onClose, | ||
callback, | ||
}) => { | ||
const data = useSession(); | ||
const { | ||
reset, | ||
register, | ||
formState: { errors }, | ||
resetField, | ||
handleSubmit, | ||
} = useForm<TCreateSHLinkDto>(); | ||
|
||
const onSubmitForm = async (data: TCreateSHLinkDto) => { | ||
try { | ||
const transformedData = removeUndefinedValues(data); | ||
await apiSharedLink.createLink(transformedData); | ||
callback?.(); | ||
} catch (error) { | ||
console.error('Failed to create link:', error); | ||
} | ||
}; | ||
|
||
useEffect(() => { | ||
if (open) reset(); | ||
}, [open]); | ||
|
||
useEffect(() => { | ||
if (data?.token?.sub) | ||
resetField('userId', { defaultValue: data.token.sub }); | ||
}, [data?.token?.sub]); | ||
|
||
return ( | ||
<Dialog open={!!open} fullWidth maxWidth="xs" onClose={() => onClose?.()}> | ||
<StyledDialogTitle>Create a new Link</StyledDialogTitle> | ||
<DialogContent style={{ padding: '5px 8px' }}> | ||
<form onSubmit={handleSubmit(onSubmitForm)}> | ||
<StyledDialogContent> | ||
<TextField | ||
label="User id" | ||
error={!!errors.userId} | ||
disabled | ||
required | ||
inputProps={{ readOnly: true }} | ||
{...register('userId', {})} | ||
/> | ||
<TextField | ||
label="Name" | ||
error={!!errors.name} | ||
required | ||
helperText={errors.name ? errors.name.message : null} | ||
placeholder="Name" | ||
{...register('name', { required: 'Required field' })} | ||
/> | ||
<TextField | ||
type="number" | ||
label="PIN Code" | ||
{...register('configPasscode', { | ||
setValueAs: (value) => value || undefined, | ||
})} | ||
/> | ||
<TextField | ||
type="date" | ||
label="Expiration Date" | ||
InputLabelProps={{ shrink: true }} | ||
{...register('configExp', { | ||
setValueAs: (value) => value || undefined, | ||
})} | ||
/> | ||
</StyledDialogContent> | ||
</form> | ||
</DialogContent> | ||
<StyledDialogActions> | ||
<Button color="inherit" variant="contained" onClick={onClose}> | ||
Cancel | ||
</Button> | ||
<Button | ||
type="submit" | ||
color="success" | ||
variant="contained" | ||
onClick={handleSubmit(onSubmitForm)} | ||
> | ||
Create | ||
</Button> | ||
</StyledDialogActions> | ||
</Dialog> | ||
); | ||
Comment on lines
+83
to
+137
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enhance accessibility for form inputs The form inputs could benefit from improved accessibility attributes. Consider adding <TextField
label="Name"
error={!!errors.name}
required
helperText={errors.name ? errors.name.message : null}
placeholder="Name"
aria-describedby={errors.name ? "name-error" : undefined}
{...register('name', { required: 'Required field' })}
/>
{errors.name && <span id="name-error" style={{ display: 'none' }}>{errors.name.message}</span>} Apply similar changes to other form inputs to improve screen reader support. |
||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import CancelIcon from '@mui/icons-material/Cancel'; | ||
import CheckCircleIcon from '@mui/icons-material/CheckCircle'; | ||
import { green, red } from '@mui/material/colors'; | ||
|
||
export default function BooleanIcon({ | ||
status, | ||
}: { | ||
status: boolean; | ||
}): JSX.Element { | ||
return status ? ( | ||
<CheckCircleIcon style={{ color: green[500] }} /> | ||
) : ( | ||
<CancelIcon style={{ color: red[500] }} /> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
'use client'; | ||
import CalendarTodayIcon from '@mui/icons-material/CalendarToday'; | ||
import { | ||
Button, | ||
Grid, | ||
Paper, | ||
Table, | ||
TableBody, | ||
TableCell, | ||
TableContainer, | ||
TableHead, | ||
TablePagination, | ||
TableRow, | ||
} from '@mui/material'; | ||
import { useEffect, useState } from 'react'; | ||
import React from 'react'; | ||
|
||
import { useSession } from '@/app/hooks/useSession'; | ||
import { apiSharedLink } from '@/app/utils/api.class'; | ||
import { SHLinkMiniDto } from '@/domain/dtos/shlink'; | ||
|
||
import { AddLinkDialog } from './AddLinkDialog'; | ||
import BooleanIcon from './BooleanIcon'; | ||
|
||
interface Column { | ||
id: keyof SHLinkMiniDto; | ||
label: string; | ||
minWidth?: number; | ||
align?: 'right' | 'left' | 'center'; | ||
format?: ( | ||
value: SHLinkMiniDto[keyof SHLinkMiniDto], | ||
) => string | React.JSX.Element; | ||
} | ||
|
||
const columns: readonly Column[] = [ | ||
{ id: 'name', label: 'Name', minWidth: 100 }, | ||
{ | ||
id: 'expiryDate', | ||
label: 'Expiry Date', | ||
minWidth: 80, | ||
format: (value?: Date) => | ||
value?.toString()?.substring(0, 10) || ( | ||
<span | ||
style={{ display: 'flex', alignItems: 'center', color: '#9e9e9e' }} | ||
> | ||
<CalendarTodayIcon fontSize="small" style={{ marginRight: '4px' }} /> | ||
Not Defined | ||
</span> | ||
), | ||
}, | ||
{ | ||
id: 'passwordRequired', | ||
label: 'Passcode enabled', | ||
minWidth: 50, | ||
format: (value?: boolean) => <BooleanIcon status={!!value} />, | ||
}, | ||
]; | ||
|
||
export default function LinksTable() { | ||
const session = useSession(); | ||
const [links, setLinks] = useState<SHLinkMiniDto[]>([]); | ||
const [page, setPage] = useState(0); | ||
const [rowsPerPage, setRowsPerPage] = useState(10); | ||
const [addDialog, setAddDialog] = React.useState<boolean>(); | ||
|
||
const handleChangePage = (_event: unknown, newPage: number) => { | ||
setPage(newPage); | ||
}; | ||
|
||
useEffect(() => { | ||
apiSharedLink.findLinks().then(({ data }) => setLinks(data)); | ||
}, []); | ||
|
||
const handleChangeRowsPerPage = ( | ||
event: React.ChangeEvent<HTMLInputElement>, | ||
) => { | ||
setRowsPerPage(Number(event.target.value)); | ||
setPage(0); | ||
}; | ||
|
||
const handleCreateLink = (_event: unknown) => { | ||
setAddDialog(true); | ||
}; | ||
|
||
return ( | ||
<Paper sx={{ width: '100%', overflow: 'hidden' }}> | ||
<AddLinkDialog | ||
open={addDialog} | ||
onClose={() => setAddDialog(false)} | ||
callback={() => { | ||
apiSharedLink.findLinks().then(({ data }) => { | ||
setLinks(data); | ||
setAddDialog(false); | ||
}); | ||
}} | ||
/> | ||
<Grid container justifyContent="end"> | ||
<Grid item> | ||
<Button variant="contained" onClick={handleCreateLink}> | ||
Add new link | ||
</Button> | ||
</Grid> | ||
</Grid> | ||
<TableContainer sx={{ maxHeight: '50vh' }}> | ||
<Table stickyHeader aria-label="sticky table"> | ||
<TableHead> | ||
<TableRow> | ||
{columns.map((column) => ( | ||
<TableCell | ||
key={column.id} | ||
align={column.align} | ||
style={{ minWidth: column.minWidth }} | ||
> | ||
{column.label} | ||
</TableCell> | ||
))} | ||
</TableRow> | ||
</TableHead> | ||
<TableBody> | ||
{links | ||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) | ||
.map((row) => ( | ||
<TableRow hover tabIndex={-1} key={row.id}> | ||
{columns.map((column) => { | ||
const value = row[column.id]; | ||
return ( | ||
<TableCell key={column.id} align={column.align}> | ||
{column.format | ||
? column.format(value) | ||
: value?.toString()} | ||
</TableCell> | ||
); | ||
})} | ||
</TableRow> | ||
))} | ||
</TableBody> | ||
</Table> | ||
</TableContainer> | ||
<TablePagination | ||
rowsPerPageOptions={[10, 25, 100]} | ||
component="div" | ||
count={links.length} | ||
rowsPerPage={rowsPerPage} | ||
page={page} | ||
onPageChange={handleChangePage} | ||
onRowsPerPageChange={handleChangeRowsPerPage} | ||
/> | ||
</Paper> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { Box, Container } from '@mui/material'; | ||
|
||
import LinksTable from './Components/LinksTable'; | ||
|
||
export default async function SharedLinksPage() { | ||
return ( | ||
<Container maxWidth={false}> | ||
<Box | ||
display="flex" | ||
flexDirection="column" | ||
alignItems="center" | ||
justifyContent="center" | ||
paddingTop={8} | ||
paddingBottom={8} | ||
></Box> | ||
<LinksTable /> | ||
</Container> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add error handling and logging.
While the current implementation is functional, it doesn't handle potential errors or provide any logging. This could make debugging issues related to environment configuration more difficult.
Consider adding error handling and logging:
This modification adds error handling and basic logging, which can help with debugging and provide more information about the environment configuration loading process.
LGTM! Consider exporting the loaded configuration.
The implementation looks correct. It imports the necessary function, gets the current working directory, and loads the environment configuration. However, you might want to consider exporting the result of
loadEnvConfig
for use in other parts of the application.Consider modifying the code as follows:
This change would allow other parts of your application to import and use the loaded environment configuration.
Committable suggestion