Skip to content

Commit 159bca4

Browse files
committed
feat(#35): add attachments to video form
1 parent 4690717 commit 159bca4

File tree

13 files changed

+449
-83
lines changed

13 files changed

+449
-83
lines changed

apps/polyflix/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<meta property="og:image" content="logo192.png" />
2222
<link rel="apple-touch-icon" href="logo192.png" />
2323

24-
<link rel="manifest" href="manifest.json" />
24+
<link rel="manifest" href="/manifest.json" />
2525
<title>Polyflix</title>
2626
</head>
2727
<body>

apps/polyflix/public/locales/en/attachments.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
}
3131
},
3232
"errors": {}
33+
},
34+
"selector": {
35+
"title": "Select attachments",
36+
"validate": "Close"
3337
}
3438
},
3539
"closeModal": "Close",

apps/polyflix/public/locales/en/videos.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,18 @@
5151
"youtubeUrl": "YouTube video URL",
5252
"description": "Description",
5353
"upload": "Drag 'n' drop your video here.",
54-
"attachments": {
55-
"label": "Label",
56-
"url": "Attachment URL",
57-
"empty": "Your video doesn't have attachments."
58-
},
5954
"submit": {
6055
"create": "Create video",
6156
"update": "Update video"
6257
}
6358
},
59+
"attachments": {
60+
"label": "Attachments",
61+
"description": "You can add or remove attachments to your video.",
62+
"add": "Add attachments",
63+
"remove": "Remove attachment",
64+
"empty": "Your video does not include any attachments yet."
65+
},
6466
"errors": {
6567
"upload": "An error occured when uploading your file. Please see the logs for more informations."
6668
},

apps/polyflix/public/locales/fr/attachments.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
}
3131
},
3232
"errors": {}
33+
},
34+
"selector": {
35+
"title": "Veuillez sélectionner les pièces jointes",
36+
"validate": "Fermer"
3337
}
3438
},
3539
"closeModal": "Fermer",
@@ -44,4 +48,4 @@
4448
"actions": {
4549
"copyToClipboard": "Copier le lien dans le presse-papier"
4650
}
47-
}
51+
}

apps/polyflix/public/locales/fr/videos.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,18 @@
5656
"youtubeUrl": "Lien de la vidéo sur YouTube",
5757
"description": "Description",
5858
"upload": "Glissez déposer votre vidéo ici",
59-
"attachments": {
60-
"label": "Nom",
61-
"url": "Lien de la pièce jointe",
62-
"empty": "Votre vidéo n'a aucune pièces jointes."
63-
},
6459
"submit": {
6560
"create": "Créer la vidéo",
6661
"update": "Mettre à jour la vidéo"
6762
}
6863
},
64+
"attachments": {
65+
"label": "Pièces jointes",
66+
"description": "Vous pouvez ajouter ou supprimer des pièces jointes à votre vidéo.",
67+
"add": "Ajouter des pièces jointes",
68+
"remove": "Supprimer la pièce jointe",
69+
"empty": "Votre vidéo n'a aucune pièces jointes."
70+
},
6971
"errors": {
7072
"upload": "Une erreur est survenue lors de l'envoi de vos fichiers."
7173
},
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Avatar, IconButton, ListItemIcon, Tooltip } from '@mui/material'
2+
import CopyToClipboard from 'react-copy-to-clipboard'
3+
import { useTranslation } from 'react-i18next'
4+
5+
import { useInjection } from '@polyflix/di'
6+
7+
import { Icon } from '@core/components/Icon/Icon.component'
8+
import { SnackbarService } from '@core/services/snackbar.service'
9+
10+
type Props = {
11+
url: string
12+
copyToClipboard?: boolean
13+
}
14+
15+
export const AttachmentAvatar = ({ url, copyToClipboard = true }: Props) => {
16+
const { t: tUsers } = useTranslation('users')
17+
const { t: tAttachments } = useTranslation('attachments')
18+
const snackbarService = useInjection<SnackbarService>(SnackbarService)
19+
20+
const avatarContent = () => (
21+
<Avatar src={'' /* TODO : Issue #466 */}>
22+
<Icon name="eva:link-outline" size={30} />
23+
</Avatar>
24+
)
25+
26+
if (copyToClipboard) {
27+
return (
28+
<ListItemIcon>
29+
<CopyToClipboard
30+
onCopy={() => {
31+
snackbarService.createSnackbar(
32+
tUsers('profile.tabs.attachments.content.list.clipboard'),
33+
{
34+
variant: 'success',
35+
}
36+
)
37+
}}
38+
text={url}
39+
>
40+
<Tooltip title={tAttachments<string>('actions.copyToClipboard')}>
41+
<IconButton>{avatarContent()}</IconButton>
42+
</Tooltip>
43+
</CopyToClipboard>
44+
</ListItemIcon>
45+
)
46+
} else {
47+
return <ListItemIcon>{avatarContent()}</ListItemIcon>
48+
}
49+
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import {
2+
Box,
3+
Button,
4+
Checkbox,
5+
Container,
6+
Fade,
7+
Link,
8+
ListItem,
9+
ListItemButton,
10+
ListItemText,
11+
Modal,
12+
Paper,
13+
Stack,
14+
SxProps,
15+
Theme,
16+
Typography,
17+
} from '@mui/material'
18+
import { useEffect, useState } from 'react'
19+
import { UseFieldArrayReturn } from 'react-hook-form'
20+
import { useTranslation } from 'react-i18next'
21+
import { Redirect } from 'react-router-dom'
22+
23+
import { NoData } from '@core/components/NoData/NoData.component'
24+
import { PaginationSynced } from '@core/components/Pagination/PaginationSynced.component'
25+
import { Scrollbar } from '@core/components/Scrollbar/Scrollbar.component'
26+
import { buildSkeletons } from '@core/utils/gui.utils'
27+
28+
import { useAuth } from '@auth/hooks/useAuth.hook'
29+
30+
import { IVideoForm } from '@videos/types/form.type'
31+
32+
import { Attachment } from '@attachments/models/attachment.model'
33+
import { AttachmentParams } from '@attachments/models/attachment.params'
34+
import { useGetUserAttachmentsQuery } from '@attachments/services/attachment.service'
35+
36+
import { AttachmentAvatar } from './AttachmentAvatar.component'
37+
38+
interface Props {
39+
attachments: UseFieldArrayReturn<IVideoForm, 'attachments'>
40+
videoId?: string
41+
isOpen: boolean
42+
onClose: () => void
43+
sx?: SxProps<Theme>
44+
}
45+
export const AttachmentSelectorModal = ({
46+
attachments,
47+
videoId,
48+
isOpen,
49+
onClose,
50+
sx: sxProps,
51+
}: Props) => {
52+
const { user } = useAuth()
53+
const { t } = useTranslation('attachments')
54+
55+
const [page] = useState(1)
56+
57+
const [filters, setFilters] = useState<AttachmentParams>({
58+
page,
59+
pageSize: 10,
60+
userId: user!.id,
61+
})
62+
63+
const { data, isLoading, refetch } = useGetUserAttachmentsQuery(filters)
64+
65+
const { fields, append, remove } = attachments
66+
67+
const handleToggle = (attachment: Attachment) => () => {
68+
const currentIndex = fields.findIndex((e) => e.id === attachment.id)
69+
if (currentIndex === -1) {
70+
append(attachment)
71+
} else {
72+
remove(currentIndex)
73+
}
74+
}
75+
76+
useEffect(() => {
77+
/* Since the attachments are not invalidated after a video update, we need to refetch them here */
78+
if (data) refetch()
79+
}, [])
80+
81+
useEffect(() => {
82+
/* On update mode, append the previously selected attachments in order to set the checkboxes ticked */
83+
if (videoId && data) {
84+
for (const f of fields) {
85+
if (
86+
f.videos.includes(videoId) &&
87+
!fields.find(({ id }) => id === f.id)
88+
) {
89+
append(f)
90+
}
91+
}
92+
}
93+
}, [videoId, data])
94+
95+
const isAttachmentSelected = (attachment: Attachment) =>
96+
fields.some((e) => e.id === attachment.id)
97+
98+
/* If the user has no attachment, he is redirected to the attachment creation form */
99+
if (data && data.totalCount === 0)
100+
return <Redirect push to="/users/profile/attachments/create" />
101+
102+
return (
103+
<Modal
104+
sx={{
105+
display: 'flex',
106+
alignItems: 'center',
107+
justifyContent: 'center',
108+
padding: 2,
109+
...sxProps,
110+
}}
111+
open={isOpen}
112+
onClose={() => onClose()}
113+
aria-labelledby="element modal"
114+
closeAfterTransition
115+
BackdropProps={{
116+
timeout: 500,
117+
}}
118+
>
119+
<Fade in={isOpen}>
120+
<Paper
121+
sx={{
122+
width: {
123+
lg: '40%',
124+
md: '50%',
125+
sm: '70%',
126+
xs: '90%',
127+
},
128+
bgcolor: 'background.default',
129+
borderRadius: 2,
130+
p: {
131+
sm: 2,
132+
xs: 1,
133+
},
134+
}}
135+
variant="outlined"
136+
>
137+
<Typography variant="h4" sx={{ mb: '2%' }}>
138+
{t('forms.selector.title')}
139+
</Typography>
140+
<Scrollbar
141+
sx={{
142+
maxHeight: (theme) => `calc(100vh - ${theme.spacing(30)})`,
143+
minHeight: '300px',
144+
}}
145+
>
146+
<Box sx={{ mt: 2 }}>
147+
{{ data }
148+
? data?.items.map((item) => (
149+
<ListItem
150+
key={item.id}
151+
secondaryAction={
152+
<Checkbox
153+
edge="end"
154+
onChange={handleToggle(item)}
155+
checked={isAttachmentSelected(item)}
156+
/>
157+
}
158+
disablePadding
159+
>
160+
<ListItemButton onClick={handleToggle(item)}>
161+
<AttachmentAvatar url={item.url} />
162+
<Link
163+
href={item.url}
164+
target="_blank"
165+
rel="noopener"
166+
color="inherit"
167+
underline="hover"
168+
>
169+
<ListItemText primary={item.title} />
170+
</Link>
171+
</ListItemButton>
172+
</ListItem>
173+
))
174+
: buildSkeletons(3)}
175+
</Box>
176+
</Scrollbar>
177+
<Stack spacing={0}>
178+
{!isLoading &&
179+
(data &&
180+
data.items.length > 0 &&
181+
data.items.length < data.totalCount ? (
182+
<Box display="flex" sx={{ mt: 3 }} justifyContent="center">
183+
<PaginationSynced
184+
filters={filters}
185+
setFilters={setFilters}
186+
pageCount={Math.ceil(data?.totalCount / filters.pageSize)}
187+
/>
188+
</Box>
189+
) : (
190+
!data ||
191+
(data.items.length === 0 && (
192+
<NoData
193+
variant="attachments"
194+
link="/users/profile/attachments/create"
195+
/>
196+
))
197+
))}
198+
</Stack>
199+
<Container
200+
sx={{ display: 'flex', justifyContent: 'center', my: '1em' }}
201+
>
202+
<Button onClick={onClose} variant="contained">
203+
{t('forms.selector.validate')}
204+
</Button>
205+
</Container>
206+
</Paper>
207+
</Fade>
208+
</Modal>
209+
)
210+
}

apps/polyflix/src/modules/attachments/services/attachment.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const attachmentsApi = createApi({
4040
}),
4141
getVideoAttachments: builder.query<PaginatedAttachments, string>({
4242
query: (videoId: string) => {
43-
return `${Endpoint.Attachments}/video/${videoId}`
43+
return `${Endpoint.Attachments}/video/${videoId}?pageSize=50&page=1` // TODO : remove pagination from attachment service (only for /video/id)
4444
},
4545
providesTags: (result) =>
4646
result

0 commit comments

Comments
 (0)