diff --git a/src/pages/playlist/StrictModeDroppable.jsx b/src/pages/playlist/StrictModeDroppable.jsx new file mode 100644 index 0000000..f22223a --- /dev/null +++ b/src/pages/playlist/StrictModeDroppable.jsx @@ -0,0 +1,21 @@ +import { useEffect, useState } from "react"; +import { Droppable } from "react-beautiful-dnd"; + +export const StrictModeDroppable = ({ children, ...props }) => { + const [enabled, setEnabled] = useState(false); + + useEffect(() => { + const animation = requestAnimationFrame(() => setEnabled(true)); + + return () => { + cancelAnimationFrame(animation); + setEnabled(false); + }; + }, []); + + if (!enabled) { + return null; + } + + return {children}; +}; \ No newline at end of file diff --git a/src/pages/playlist/content.jsx b/src/pages/playlist/content.jsx new file mode 100644 index 0000000..96eeae0 --- /dev/null +++ b/src/pages/playlist/content.jsx @@ -0,0 +1,103 @@ +import { Box } from '@mantine/core'; +import { DragDropContext, Draggable } from 'react-beautiful-dnd'; +import { IconGripVertical } from '@tabler/icons-react'; +import { StrictModeDroppable } from './StrictModeDroppable'; +import { ActionIcon, Button, Center, Grid, Group, NumberInput, Paper, Text } from '@mantine/core'; +import { IconTrash } from '@tabler/icons-react'; +import { useState } from 'react'; +import ModalFileSelector from '../files/file-selector'; + +const Content = ({ form }) => { + console.log(form.values); + + const [fileSelector, setFileSelector] = useState(true); + const toggleFileSelector = () => setFileSelector(!fileSelector); + + const handleAddFiles = (files) => { + files.forEach((file) => { + file.seconds = 10; + form.insertListItem('files', file); + }); + }; + + const handleDelete = (fileId) => { + console.log(form.values); + const index = form.values.files.findIndex((file) => file.id === fileId); + console.log(index, fileId); + if (index) { + form.removeListItem('files', index); + } + }; + + const fields = form.values.files.map((el, index) => ( + + {(provided) => ( + + + + + + + + + {form.getInputProps(`files.${index}.name`).value} + + + + + + handleDelete(el.id)} + > + + + + + + + + )} + + )); + + return ( + + + form.reorderListItem('files', { from: source.index, to: destination.index }) + } + > + + {(provided) => ( + + {fields} + {provided.placeholder} + + )} + + + + + + Select File(s) + + + handleAddFiles(files)} + /> + + ); +}; + +export default Content; diff --git a/src/pages/playlist/index.jsx b/src/pages/playlist/index.jsx new file mode 100644 index 0000000..59b3e1c --- /dev/null +++ b/src/pages/playlist/index.jsx @@ -0,0 +1,86 @@ +import { Button, Paper, Text, Title, Group } from '@mantine/core'; +import { useEffect, useState } from 'react'; +import API from '../../services/api'; +import { parseTime } from '../../tools/timeUtil'; +import setNotification from '../errors/error-notification'; +import GrantAccess from '../../tools/grant-access'; +import ModalUpdate from '../playlists/update'; +import { useForm } from '@mantine/form'; +import Content from './content'; + +const Playlist = (item) => { + const id = window.location.href.split('/').slice(-1)[0]; + const [playlist, setPlaylist] = useState(null); + const [update, setUpdate] = useState(false); + const [addFile, setAddFile] = useState(false); + const [files, setFiles] = useState([]); + const [duration, setDuration] = useState(0); + + const toggleUpdate = () => setUpdate(!update); + + const form = useForm({ + initialValues: { + files: [{ id: 0, name: 'stuff', seconds: 60 }], + }, + }); + + useEffect(() => { + let duration = form.values.files.reduce((acc, file) => { + acc += file.seconds; + return acc; + }, 0); + setDuration(duration); + }, [form.values]); + + useEffect(() => { + if (JSON.stringify(item) !== '{}') { + setPlaylist(item); + } else { + API.getPlaylist(id) + .then((res) => { + if (res.status === 200) { + setPlaylist(res.data); + } + }) + .catch((err) => { + setNotification(true, err.response.data.error); + }); + + API.getFiles() + .then((res) => { + if (res.status === 200) { + setFiles(res.data); + } + }) + .catch((err) => { + setNotification(true, err.response.data.error); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id]); + + return ( + <> + + + {playlist?.name} + + + Playlist duration:{' '} + + {parseTime(duration)} + + + + Edit + + + + + + + > + ); +}; + +export default Playlist; diff --git a/src/pages/playlists/create.jsx b/src/pages/playlists/create.jsx new file mode 100644 index 0000000..c23ed70 --- /dev/null +++ b/src/pages/playlists/create.jsx @@ -0,0 +1,35 @@ +import { Modal, Text } from '@mantine/core'; +import PlaylistViewEditor from './playlist-view-editor'; +import API from '../../services/api'; + +const ModalCreatePlaylist = ({ opened, handler, addPlaylist }) => { + const validated = (item) => { + addPlaylist(item); + handler(); + }; + + return ( + + + + + + + Create Playlist + + + + + + validated(item)} + /> + + + + ); +}; + +export default ModalCreatePlaylist; diff --git a/src/pages/playlists/index.jsx b/src/pages/playlists/index.jsx new file mode 100644 index 0000000..86b1c58 --- /dev/null +++ b/src/pages/playlists/index.jsx @@ -0,0 +1,66 @@ +import { useEffect, useState } from 'react'; +import NavbarSignage from '../../components/navbar'; +import PlaylistTable from './playlist-table'; +import API from '../../services/api'; +import setNotification from '../errors/error-notification'; + +const Playlists = () => { + const [showCreate, setShowCreate] = useState(false); + const [showUpdate, setShowUpdate] = useState(false); + const [item, setItem] = useState({}); + const [page, setPage] = useState(0); + const limit = 6; + + const toggleModalCreate = () => setShowCreate(!showCreate); + const toggleModalUpdate = () => setShowUpdate(!showUpdate); + + const [playlists, setPlaylist] = useState([]); + + useEffect(() => { + API.listPlaylists(limit, page) + .then((res) => { + if (res.status === 200) { + if (playlists.length === 0) setPlaylist(res.data); + else setPlaylist((prev) => [...prev, ...res.data]); + } + }) + .catch((err) => { + setNotification(true, err.response.data.error); + }); + + return () => {}; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [page]); + + const [search, setSearch] = useState(''); + + const loadMore = () => { + setPage((prev) => prev + limit); + }; + + const navbar = { + title: 'Playlists', + search: search, + handlerChange: (e) => setSearch(e.target.value), + buttonCreate: { + text: 'New Playlist', + handler: toggleModalCreate, + }, + }; + + return ( + <> + + setPlaylist(playlists.filter((item) => item._id != id))} + updateHandler={toggleModalUpdate} + loadMore={loadMore} + /> + > + ); +}; + +export default Playlists; diff --git a/src/pages/playlists/playlist-table.jsx b/src/pages/playlists/playlist-table.jsx new file mode 100644 index 0000000..7a773ac --- /dev/null +++ b/src/pages/playlists/playlist-table.jsx @@ -0,0 +1,36 @@ +import { Button, Center, Table, Paper, ScrollArea } from '@mantine/core'; +import { useNavigate } from 'react-router-dom'; + +const PlaylistTable = (props) => { + const navigate = useNavigate(); + + const rows = props.data.map((playlist) => ( + + {playlist.name} + navigate(`/playlist/${playlist.id}`)}>Open + + )); + + return ( + + + + + + Name + Actions + + + {rows} + + + + + Load More + + + + ); +}; + +export default PlaylistTable; diff --git a/src/pages/playlists/playlist-view-editor.jsx b/src/pages/playlists/playlist-view-editor.jsx new file mode 100644 index 0000000..508826b --- /dev/null +++ b/src/pages/playlists/playlist-view-editor.jsx @@ -0,0 +1,57 @@ +import { Button, TextInput, Group } from '@mantine/core'; +import { useForm, isNotEmpty } from '@mantine/form'; +import { useState } from 'react'; + +const PlaylistViewEditor = ({ item, handler, buttonText, APICall }) => { + const handleClose = (playlist) => { + form.reset(); + handler(playlist); + }; + + const [isLoading, setIsLoading] = useState(false); + + // todo permissions + const form = useForm({ + initialValues: { + name: item?.name ?? '', + }, + validate: { + name: isNotEmpty('Name is required'), + }, + }); + + const handleSubmit = async (event) => { + event.preventDefault(); + if (form.validate().hasErrors) return; + try { + setIsLoading(true); + + if (item) { + form.values.id = item?.id; + } + + const res = await APICall(form.values); + setIsLoading(false); + handleClose(res.data); + } catch (error) { + setIsLoading(false); + // todo + } + }; + + return ( + + + + + Cancel + + + {buttonText} + + + + ); +}; + +export default PlaylistViewEditor; diff --git a/src/pages/playlists/update.jsx b/src/pages/playlists/update.jsx new file mode 100644 index 0000000..c7e634d --- /dev/null +++ b/src/pages/playlists/update.jsx @@ -0,0 +1,36 @@ +import { Modal, Text } from '@mantine/core'; +import API from '../../services/api'; +import PlaylistViewEditor from './playlist-view-editor'; + +const ModalUpdatePlaylist = ({ item, opened, handler, updatePlaylist }) => { + const validated = (playlist) => { + updatePlaylist(playlist); + handler(); + }; + + return ( + + + + + + + Update Playlist + + + + + + validated(playlist)} + /> + + + + ); +}; + +export default ModalUpdatePlaylist;