playlist page

This commit is contained in:
grimhilt 2023-07-30 19:16:02 +02:00
parent f24c394388
commit 8c12f47d5d
8 changed files with 440 additions and 0 deletions

View 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>;
};

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;