Skip to content

Commit

Permalink
feat(#35): add attachments to video form
Browse files Browse the repository at this point in the history
  • Loading branch information
jlabatut committed Dec 16, 2022
1 parent 4690717 commit 159bca4
Show file tree
Hide file tree
Showing 13 changed files with 449 additions and 83 deletions.
2 changes: 1 addition & 1 deletion apps/polyflix/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<meta property="og:image" content="logo192.png" />
<link rel="apple-touch-icon" href="logo192.png" />

<link rel="manifest" href="manifest.json" />
<link rel="manifest" href="/manifest.json" />
<title>Polyflix</title>
</head>
<body>
Expand Down
4 changes: 4 additions & 0 deletions apps/polyflix/public/locales/en/attachments.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
}
},
"errors": {}
},
"selector": {
"title": "Select attachments",
"validate": "Close"
}
},
"closeModal": "Close",
Expand Down
12 changes: 7 additions & 5 deletions apps/polyflix/public/locales/en/videos.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,18 @@
"youtubeUrl": "YouTube video URL",
"description": "Description",
"upload": "Drag 'n' drop your video here.",
"attachments": {
"label": "Label",
"url": "Attachment URL",
"empty": "Your video doesn't have attachments."
},
"submit": {
"create": "Create video",
"update": "Update video"
}
},
"attachments": {
"label": "Attachments",
"description": "You can add or remove attachments to your video.",
"add": "Add attachments",
"remove": "Remove attachment",
"empty": "Your video does not include any attachments yet."
},
"errors": {
"upload": "An error occured when uploading your file. Please see the logs for more informations."
},
Expand Down
6 changes: 5 additions & 1 deletion apps/polyflix/public/locales/fr/attachments.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
}
},
"errors": {}
},
"selector": {
"title": "Veuillez sélectionner les pièces jointes",
"validate": "Fermer"
}
},
"closeModal": "Fermer",
Expand All @@ -44,4 +48,4 @@
"actions": {
"copyToClipboard": "Copier le lien dans le presse-papier"
}
}
}
12 changes: 7 additions & 5 deletions apps/polyflix/public/locales/fr/videos.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,18 @@
"youtubeUrl": "Lien de la vidéo sur YouTube",
"description": "Description",
"upload": "Glissez déposer votre vidéo ici",
"attachments": {
"label": "Nom",
"url": "Lien de la pièce jointe",
"empty": "Votre vidéo n'a aucune pièces jointes."
},
"submit": {
"create": "Créer la vidéo",
"update": "Mettre à jour la vidéo"
}
},
"attachments": {
"label": "Pièces jointes",
"description": "Vous pouvez ajouter ou supprimer des pièces jointes à votre vidéo.",
"add": "Ajouter des pièces jointes",
"remove": "Supprimer la pièce jointe",
"empty": "Votre vidéo n'a aucune pièces jointes."
},
"errors": {
"upload": "Une erreur est survenue lors de l'envoi de vos fichiers."
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Avatar, IconButton, ListItemIcon, Tooltip } from '@mui/material'
import CopyToClipboard from 'react-copy-to-clipboard'
import { useTranslation } from 'react-i18next'

import { useInjection } from '@polyflix/di'

import { Icon } from '@core/components/Icon/Icon.component'
import { SnackbarService } from '@core/services/snackbar.service'

type Props = {
url: string
copyToClipboard?: boolean
}

export const AttachmentAvatar = ({ url, copyToClipboard = true }: Props) => {
const { t: tUsers } = useTranslation('users')
const { t: tAttachments } = useTranslation('attachments')
const snackbarService = useInjection<SnackbarService>(SnackbarService)

const avatarContent = () => (
<Avatar src={'' /* TODO : Issue #466 */}>
<Icon name="eva:link-outline" size={30} />
</Avatar>
)

if (copyToClipboard) {
return (
<ListItemIcon>
<CopyToClipboard
onCopy={() => {
snackbarService.createSnackbar(
tUsers('profile.tabs.attachments.content.list.clipboard'),
{
variant: 'success',
}
)
}}
text={url}
>
<Tooltip title={tAttachments<string>('actions.copyToClipboard')}>
<IconButton>{avatarContent()}</IconButton>
</Tooltip>
</CopyToClipboard>
</ListItemIcon>
)
} else {
return <ListItemIcon>{avatarContent()}</ListItemIcon>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import {
Box,
Button,
Checkbox,
Container,
Fade,
Link,
ListItem,
ListItemButton,
ListItemText,
Modal,
Paper,
Stack,
SxProps,
Theme,
Typography,
} from '@mui/material'
import { useEffect, useState } from 'react'
import { UseFieldArrayReturn } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { Redirect } from 'react-router-dom'

import { NoData } from '@core/components/NoData/NoData.component'
import { PaginationSynced } from '@core/components/Pagination/PaginationSynced.component'
import { Scrollbar } from '@core/components/Scrollbar/Scrollbar.component'
import { buildSkeletons } from '@core/utils/gui.utils'

import { useAuth } from '@auth/hooks/useAuth.hook'

import { IVideoForm } from '@videos/types/form.type'

import { Attachment } from '@attachments/models/attachment.model'
import { AttachmentParams } from '@attachments/models/attachment.params'
import { useGetUserAttachmentsQuery } from '@attachments/services/attachment.service'

import { AttachmentAvatar } from './AttachmentAvatar.component'

interface Props {
attachments: UseFieldArrayReturn<IVideoForm, 'attachments'>
videoId?: string
isOpen: boolean
onClose: () => void
sx?: SxProps<Theme>
}
export const AttachmentSelectorModal = ({
attachments,
videoId,
isOpen,
onClose,
sx: sxProps,
}: Props) => {
const { user } = useAuth()
const { t } = useTranslation('attachments')

const [page] = useState(1)

const [filters, setFilters] = useState<AttachmentParams>({
page,
pageSize: 10,
userId: user!.id,
})

const { data, isLoading, refetch } = useGetUserAttachmentsQuery(filters)

const { fields, append, remove } = attachments

const handleToggle = (attachment: Attachment) => () => {
const currentIndex = fields.findIndex((e) => e.id === attachment.id)
if (currentIndex === -1) {
append(attachment)
} else {
remove(currentIndex)
}
}

useEffect(() => {
/* Since the attachments are not invalidated after a video update, we need to refetch them here */
if (data) refetch()
}, [])

useEffect(() => {
/* On update mode, append the previously selected attachments in order to set the checkboxes ticked */
if (videoId && data) {
for (const f of fields) {
if (
f.videos.includes(videoId) &&
!fields.find(({ id }) => id === f.id)
) {
append(f)
}
}
}
}, [videoId, data])

const isAttachmentSelected = (attachment: Attachment) =>
fields.some((e) => e.id === attachment.id)

/* If the user has no attachment, he is redirected to the attachment creation form */
if (data && data.totalCount === 0)
return <Redirect push to="/users/profile/attachments/create" />

return (
<Modal
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 2,
...sxProps,
}}
open={isOpen}
onClose={() => onClose()}
aria-labelledby="element modal"
closeAfterTransition
BackdropProps={{
timeout: 500,
}}
>
<Fade in={isOpen}>
<Paper
sx={{
width: {
lg: '40%',
md: '50%',
sm: '70%',
xs: '90%',
},
bgcolor: 'background.default',
borderRadius: 2,
p: {
sm: 2,
xs: 1,
},
}}
variant="outlined"
>
<Typography variant="h4" sx={{ mb: '2%' }}>
{t('forms.selector.title')}
</Typography>
<Scrollbar
sx={{
maxHeight: (theme) => `calc(100vh - ${theme.spacing(30)})`,
minHeight: '300px',
}}
>
<Box sx={{ mt: 2 }}>
{{ data }
? data?.items.map((item) => (
<ListItem
key={item.id}
secondaryAction={
<Checkbox
edge="end"
onChange={handleToggle(item)}
checked={isAttachmentSelected(item)}
/>
}
disablePadding
>
<ListItemButton onClick={handleToggle(item)}>
<AttachmentAvatar url={item.url} />
<Link
href={item.url}
target="_blank"
rel="noopener"
color="inherit"
underline="hover"
>
<ListItemText primary={item.title} />
</Link>
</ListItemButton>
</ListItem>
))
: buildSkeletons(3)}
</Box>
</Scrollbar>
<Stack spacing={0}>
{!isLoading &&
(data &&
data.items.length > 0 &&
data.items.length < data.totalCount ? (
<Box display="flex" sx={{ mt: 3 }} justifyContent="center">
<PaginationSynced
filters={filters}
setFilters={setFilters}
pageCount={Math.ceil(data?.totalCount / filters.pageSize)}
/>
</Box>
) : (
!data ||
(data.items.length === 0 && (
<NoData
variant="attachments"
link="/users/profile/attachments/create"
/>
))
))}
</Stack>
<Container
sx={{ display: 'flex', justifyContent: 'center', my: '1em' }}
>
<Button onClick={onClose} variant="contained">
{t('forms.selector.validate')}
</Button>
</Container>
</Paper>
</Fade>
</Modal>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const attachmentsApi = createApi({
}),
getVideoAttachments: builder.query<PaginatedAttachments, string>({
query: (videoId: string) => {
return `${Endpoint.Attachments}/video/${videoId}`
return `${Endpoint.Attachments}/video/${videoId}?pageSize=50&page=1` // TODO : remove pagination from attachment service (only for /video/id)
},
providesTags: (result) =>
result
Expand Down
Loading

0 comments on commit 159bca4

Please sign in to comment.