playlist page
This commit is contained in:
parent
f24c394388
commit
8c12f47d5d
21
src/pages/playlist/StrictModeDroppable.jsx
Normal file
21
src/pages/playlist/StrictModeDroppable.jsx
Normal file
@ -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 <Droppable {...props}>{children}</Droppable>;
|
||||
};
|
103
src/pages/playlist/content.jsx
Normal file
103
src/pages/playlist/content.jsx
Normal file
@ -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) => (
|
||||
<Draggable key={index} index={index} draggableId={index.toString()}>
|
||||
{(provided) => (
|
||||
<Center>
|
||||
<Group ref={provided.innerRef} mt="xs" {...provided.draggableProps}>
|
||||
<Center {...provided.dragHandleProps}>
|
||||
<IconGripVertical size="1.2rem" />
|
||||
</Center>
|
||||
<Paper p="xs" radius="sm" shadow="sm" withBorder spacing="xs">
|
||||
<Grid columns={10}>
|
||||
<Grid.Col span={4}>
|
||||
<Text>{form.getInputProps(`files.${index}.name`).value}</Text>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4}>
|
||||
<NumberInput
|
||||
required
|
||||
hideControls
|
||||
description="Seconds to display"
|
||||
{...form.getInputProps(`files.${index}.seconds`)}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={2}>
|
||||
<ActionIcon
|
||||
color="red"
|
||||
variant="light"
|
||||
size="lg"
|
||||
onClick={() => handleDelete(el.id)}
|
||||
>
|
||||
<IconTrash size="1rem" />
|
||||
</ActionIcon>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Group>
|
||||
</Center>
|
||||
)}
|
||||
</Draggable>
|
||||
));
|
||||
|
||||
return (
|
||||
<Box mx="auto">
|
||||
<DragDropContext
|
||||
onDragEnd={({ destination, source }) =>
|
||||
form.reorderListItem('files', { from: source.index, to: destination.index })
|
||||
}
|
||||
>
|
||||
<StrictModeDroppable droppableId="dnd-list" direction="vertical">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{fields}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
</DragDropContext>
|
||||
|
||||
<Group position="center" mt="md">
|
||||
<Button vairant="light" onClick={toggleFileSelector}>
|
||||
Select File(s)
|
||||
</Button>
|
||||
</Group>
|
||||
<ModalFileSelector
|
||||
opened={fileSelector}
|
||||
multi
|
||||
handleClose={toggleFileSelector}
|
||||
handleSubmit={(files) => handleAddFiles(files)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Content;
|
86
src/pages/playlist/index.jsx
Normal file
86
src/pages/playlist/index.jsx
Normal file
@ -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 (
|
||||
<>
|
||||
<Group position="apart" mt="md">
|
||||
<Title m="md" order={2}>
|
||||
{playlist?.name}
|
||||
</Title>
|
||||
<Text>
|
||||
Playlist duration:{' '}
|
||||
<Text span fw={700}>
|
||||
{parseTime(duration)}
|
||||
</Text>
|
||||
</Text>
|
||||
<Button variant="light" mt="sm" onClick={toggleUpdate}>
|
||||
Edit
|
||||
</Button>
|
||||
</Group>
|
||||
<Paper p="xs" radius="sm" shadow="sm" withBorder my="md">
|
||||
<Content form={form} />
|
||||
</Paper>
|
||||
<ModalUpdate open={update} handler={toggleUpdate} id={playlist?.id} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Playlist;
|
35
src/pages/playlists/create.jsx
Normal file
35
src/pages/playlists/create.jsx
Normal file
@ -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 (
|
||||
<Modal.Root opened={opened} onClose={handler}>
|
||||
<Modal.Overlay />
|
||||
<Modal.Content>
|
||||
<Modal.Header>
|
||||
<Modal.Title>
|
||||
<Text fw={700} fz="lg">
|
||||
Create Playlist
|
||||
</Text>
|
||||
</Modal.Title>
|
||||
<Modal.CloseButton />
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<PlaylistViewEditor
|
||||
buttonText="Create"
|
||||
APICall={API.createPlaylist}
|
||||
handler={(item) => validated(item)}
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal.Content>
|
||||
</Modal.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalCreatePlaylist;
|
66
src/pages/playlists/index.jsx
Normal file
66
src/pages/playlists/index.jsx
Normal file
@ -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 (
|
||||
<>
|
||||
<NavbarSignage data={navbar} />
|
||||
<PlaylistTable
|
||||
data={playlists}
|
||||
updateItem={setItem}
|
||||
// eslint-disable-next-line eqeqeq
|
||||
onDelete={(id) => setPlaylist(playlists.filter((item) => item._id != id))}
|
||||
updateHandler={toggleModalUpdate}
|
||||
loadMore={loadMore}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Playlists;
|
36
src/pages/playlists/playlist-table.jsx
Normal file
36
src/pages/playlists/playlist-table.jsx
Normal file
@ -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) => (
|
||||
<tr key={playlist.id}>
|
||||
<td>{playlist.name}</td>
|
||||
<td><Button onClick={() => navigate(`/playlist/${playlist.id}`)}>Open</Button></td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
return (
|
||||
<Paper shadow="sm" p="md" withBorder mb="md">
|
||||
<ScrollArea.Autosize mah={700} offsetScrollbars>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</Table>
|
||||
</ScrollArea.Autosize>
|
||||
<Center>
|
||||
<Button onClick={props.loadMore} mt="md">
|
||||
Load More
|
||||
</Button>
|
||||
</Center>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaylistTable;
|
57
src/pages/playlists/playlist-view-editor.jsx
Normal file
57
src/pages/playlists/playlist-view-editor.jsx
Normal file
@ -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 (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<TextInput label="Name" placeholder="Name" withAsterisk {...form.getInputProps('name')} mb="sm" />
|
||||
<Group position="right" mt="md">
|
||||
<Button variant="light" color="red" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="light" color="green" loading={isLoading}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaylistViewEditor;
|
36
src/pages/playlists/update.jsx
Normal file
36
src/pages/playlists/update.jsx
Normal file
@ -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 (
|
||||
<Modal.Root opened={opened} onClose={handler}>
|
||||
<Modal.Overlay />
|
||||
<Modal.Content>
|
||||
<Modal.Header>
|
||||
<Modal.Title>
|
||||
<Text fw={700} fz="lg">
|
||||
Update Playlist
|
||||
</Text>
|
||||
</Modal.Title>
|
||||
<Modal.CloseButton />
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<PlaylistViewEditor
|
||||
item={item}
|
||||
buttonText="Update"
|
||||
APICall={API.updatePlaylist}
|
||||
handler={(playlist) => validated(playlist)}
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal.Content>
|
||||
</Modal.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalUpdatePlaylist;
|
Loading…
Reference in New Issue
Block a user