Compare commits
10 Commits
cc4c23ad19
...
8a11b54421
Author | SHA1 | Date | |
---|---|---|---|
|
8a11b54421 | ||
|
3f57fde410 | ||
|
b75f2e3b49 | ||
|
44cdba6b7d | ||
|
24fa13c870 | ||
|
8c80cef8d0 | ||
|
35ff9e643d | ||
|
f8c9b93dad | ||
|
25eb25b9c3 | ||
|
30dc2b5f31 |
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "signage",
|
"name": "Artemio",
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
@ -5,11 +5,11 @@
|
|||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description" content="Signage" />
|
<meta name="description" content="Artemio" />
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
<meta name="darkreader" content="disable" />
|
<meta name="darkreader" content="disable" />
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
<title>Signage</title>
|
<title>Artemio</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
BIN
public/logo192.png
Normal file
BIN
public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
BIN
public/logo512.png
Normal file
BIN
public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.0 KiB |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"short_name": "signage",
|
"short_name": "artemio",
|
||||||
"name": "signage",
|
"name": "artemio",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
|
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 69 KiB |
25
src/components/ModalEditor.jsx
Normal file
25
src/components/ModalEditor.jsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Modal, Text } from '@mantine/core';
|
||||||
|
|
||||||
|
const ModalEditor = ({ title, opened, handlerClose }) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal.Root opened={opened} onClose={handlerClose}>
|
||||||
|
<Modal.Overlay />
|
||||||
|
<Modal.Content>
|
||||||
|
<Modal.Header>
|
||||||
|
<Modal.Title>
|
||||||
|
<Text fw={700} fz="lg">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</Modal.Title>
|
||||||
|
<Modal.CloseButton />
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
{content}
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalEditor;
|
@ -70,7 +70,7 @@ const HeaderSearch = () => {
|
|||||||
<Group>
|
<Group>
|
||||||
<Avatar src={Logo} size={30} />
|
<Avatar src={Logo} size={30} />
|
||||||
<UnstyledButton onClick={() => navigate('/')} className={classes.link}>
|
<UnstyledButton onClick={() => navigate('/')} className={classes.link}>
|
||||||
<Title order={1}>Signage</Title>
|
<Title order={1}>Artemio</Title>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
24
src/components/media-player.jsx
Normal file
24
src/components/media-player.jsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Image } from '@mantine/core';
|
||||||
|
import { isImage } from '../tools/fileUtil';
|
||||||
|
|
||||||
|
const MediaPlayer = ({ file, fileId, shouldContain }) => {
|
||||||
|
return isImage(file.type) ? (
|
||||||
|
<Image
|
||||||
|
src={'/api/files/' + (fileId ?? file.id)}
|
||||||
|
alt={file?.name ?? ''}
|
||||||
|
width={shouldContain ? undefined : 150}
|
||||||
|
fit={shouldContain ? 'contain' : 'cover'}
|
||||||
|
radius="md"
|
||||||
|
withPlaceholder
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<video controls width={150}>
|
||||||
|
<source src={'/api/files/' + (fileId ?? file.id)} type={file.type} />
|
||||||
|
Sorry, your browser doesn't support videos.
|
||||||
|
</video>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MediaPlayer;
|
@ -1,5 +1,6 @@
|
|||||||
import { Card, Grid, Text, Image, Button, Center } from '@mantine/core';
|
import { Card, Grid, Text, Button, Center } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import MediaPlayer from './media-player';
|
||||||
|
|
||||||
const SelectorItem = ({ file, clickHandler }) => {
|
const SelectorItem = ({ file, clickHandler }) => {
|
||||||
const [selected, setSelected] = useState(false);
|
const [selected, setSelected] = useState(false);
|
||||||
@ -12,14 +13,7 @@ const SelectorItem = ({ file, clickHandler }) => {
|
|||||||
<Grid.Col span={1} key={file.id}>
|
<Grid.Col span={1} key={file.id}>
|
||||||
<Card shadow="sm">
|
<Card shadow="sm">
|
||||||
<Card.Section>
|
<Card.Section>
|
||||||
<Image
|
<MediaPlayer file={file} />
|
||||||
src={"/api/file/"+file.id}
|
|
||||||
alt={file.name}
|
|
||||||
height={100}
|
|
||||||
fit="cover"
|
|
||||||
radius="md"
|
|
||||||
withPlaceholder
|
|
||||||
/>
|
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
<Center>
|
<Center>
|
||||||
<Text order={4} mt="md">
|
<Text order={4} mt="md">
|
||||||
|
@ -62,7 +62,7 @@ const Authentication = ({ redirect }) => {
|
|||||||
align="center"
|
align="center"
|
||||||
sx={(theme) => ({ fontFamily: `Greycliff CF, ${theme.fontFamily}`, fontWeight: 900 })}
|
sx={(theme) => ({ fontFamily: `Greycliff CF, ${theme.fontFamily}`, fontWeight: 900 })}
|
||||||
>
|
>
|
||||||
Connect to signage
|
Connect to Artemio
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
||||||
|
@ -25,8 +25,11 @@ const ModalAddFile = ({ opened, handler, addFiles }) => {
|
|||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
files.forEach((file) => formData.append('file', file));
|
|
||||||
API.files.upload(formData)
|
files.forEach((file) => formData.append(`${file.name}`, file, file.name));
|
||||||
|
|
||||||
|
API.files
|
||||||
|
.upload(formData)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
validate(res.data);
|
validate(res.data);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Card, Text, Image, Button, Group } from '@mantine/core';
|
import { Card, Text, Image, Button, Group, Center } from '@mantine/core';
|
||||||
|
import MediaPlayer from '../../components/media-player';
|
||||||
|
|
||||||
const FileView = ({ file, onSelect, onDelete, ...props }) => {
|
const FileView = ({ file, onSelect, onDelete, ...props }) => {
|
||||||
// const deleteHandler = async () => {
|
// const deleteHandler = async () => {
|
||||||
@ -13,7 +14,9 @@ const FileView = ({ file, onSelect, onDelete, ...props }) => {
|
|||||||
return (
|
return (
|
||||||
<Card shadow="sm" padding="md" withBorder>
|
<Card shadow="sm" padding="md" withBorder>
|
||||||
<Card.Section>
|
<Card.Section>
|
||||||
<Image src={'/api/file/' + file?.id ?? ''} alt={file?.name ?? ''} withPlaceholder fit="contain" />
|
<Center>
|
||||||
|
<MediaPlayer file={file} shouldContain={true} />
|
||||||
|
</Center>
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
<Text>{file?.name ?? 'File Name'}</Text>
|
<Text>{file?.name ?? 'File Name'}</Text>
|
||||||
<Group position="center" grow>
|
<Group position="center" grow>
|
||||||
|
@ -8,9 +8,14 @@ import { useEffect, useState } from 'react';
|
|||||||
import ModalFileSelector from '../files/file-selector';
|
import ModalFileSelector from '../files/file-selector';
|
||||||
import API from '../../services/api';
|
import API from '../../services/api';
|
||||||
import setNotification from '../errors/error-notification';
|
import setNotification from '../errors/error-notification';
|
||||||
import GrantAccess, { Perm, checkPerm } from '../../tools/grant-access';
|
import { Perm, checkPerm } from '../../tools/grant-access';
|
||||||
import { useAuth } from '../../tools/auth-provider';
|
import { useAuth } from '../../tools/auth-provider';
|
||||||
import { parseTime } from '../../tools/timeUtil';
|
import { parseTime } from '../../tools/timeUtil';
|
||||||
|
import MediaPlayer from '../../components/media-player';
|
||||||
|
import { isVideo } from '../../tools/fileUtil';
|
||||||
|
|
||||||
|
const DEFAULT_FILE_TIME = 2;
|
||||||
|
const INCREMENT_POSITION = 100;
|
||||||
|
|
||||||
const Content = ({ form, playlistId, playlist }) => {
|
const Content = ({ form, playlistId, playlist }) => {
|
||||||
const [fileSelector, setFileSelector] = useState(false);
|
const [fileSelector, setFileSelector] = useState(false);
|
||||||
@ -21,32 +26,32 @@ const Content = ({ form, playlistId, playlist }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user || !playlist) return;
|
if (!user || !playlist) return;
|
||||||
const canEditTmp = checkPerm(Perm.EDIT_PLAYLIST, user, playlist);
|
const canEditTmp = checkPerm(Perm.EDIT_PLAYLIST, user, playlist);
|
||||||
if (canEditTmp != canEdit) {
|
if (canEditTmp !== canEdit) {
|
||||||
setCanEdit(canEditTmp);
|
setCanEdit(canEditTmp);
|
||||||
}
|
}
|
||||||
return () => {};
|
return () => {};
|
||||||
|
// eslint-disable-next-line
|
||||||
}, [playlist, user]);
|
}, [playlist, user]);
|
||||||
|
|
||||||
const handleAddFiles = (files) => {
|
const handleAddFiles = (files) => {
|
||||||
let formFiles = form.values.files;
|
let formFiles = form.values.files;
|
||||||
let max_position = formFiles[formFiles.length - 1]?.position ?? 0;
|
let max_position = formFiles[formFiles.length - 1]?.position ?? 0;
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
max_position++;
|
max_position += INCREMENT_POSITION;
|
||||||
|
|
||||||
file.position = max_position;
|
file.position = max_position;
|
||||||
file.seconds = 10;
|
file.seconds = DEFAULT_FILE_TIME;
|
||||||
const index = form.values.files.length;
|
|
||||||
form.insertListItem('files', file);
|
|
||||||
API.playlists
|
API.playlists
|
||||||
.addFile(playlistId, { position: file.position, file_id: file.id, seconds: file.seconds })
|
.addFile(playlistId, { position: file.position, file_id: file.id, seconds: file.seconds })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
setNotification(true, `Error when adding file (${res.status})`);
|
setNotification(true, `Error when adding file (${res.status})`);
|
||||||
|
} else {
|
||||||
|
file.pfid = res.data.pfid;
|
||||||
|
form.insertListItem('files', file);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('here');
|
// form.removeListItem('files', index);
|
||||||
form.removeListItem('files', index);
|
|
||||||
setNotification(true, err);
|
setNotification(true, err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -54,13 +59,15 @@ const Content = ({ form, playlistId, playlist }) => {
|
|||||||
|
|
||||||
const changePositionValue = (from, to) => {
|
const changePositionValue = (from, to) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
if (from == to)
|
||||||
|
resolve(true);
|
||||||
const formFiles = form.values.files;
|
const formFiles = form.values.files;
|
||||||
let below_position = to === 0 ? 0 : formFiles[to].position;
|
let below_position = to === 0 ? 0 : formFiles[to - 1].position;
|
||||||
let above_position = formFiles[to].position;
|
let above_position = formFiles[to].position;
|
||||||
if (to > from) {
|
if (to > from) {
|
||||||
if (to === formFiles.length - 1) {
|
if (to === formFiles.length - 1) {
|
||||||
// last element so nothing above
|
// last element so nothing above
|
||||||
above_position = formFiles.length + 1;
|
above_position = formFiles[to].position + INCREMENT_POSITION;
|
||||||
} else {
|
} else {
|
||||||
// not last to taking element above
|
// not last to taking element above
|
||||||
above_position = formFiles[to + 1].position;
|
above_position = formFiles[to + 1].position;
|
||||||
@ -70,9 +77,10 @@ const Content = ({ form, playlistId, playlist }) => {
|
|||||||
|
|
||||||
// sending modification to server
|
// sending modification to server
|
||||||
API.playlists
|
API.playlists
|
||||||
.changeOrder(playlistId, { file_id: formFiles[from].id, position: newPosition })
|
.changeOrder(playlistId, { pfid: formFiles[from].pfid, position: newPosition })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
|
formFiles[from].position = newPosition;
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} else {
|
} else {
|
||||||
setNotification(true, `Error when changing order (${res.status})`);
|
setNotification(true, `Error when changing order (${res.status})`);
|
||||||
@ -109,9 +117,9 @@ const Content = ({ form, playlistId, playlist }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const changeSeconds = (seconds, index) => {
|
const changeSeconds = (seconds, index) => {
|
||||||
const fileId = form.values.files[index].id;
|
const filePfid = form.values.files[index].pfid;
|
||||||
API.playlists
|
API.playlists
|
||||||
.changeSeconds(playlistId, { file_id: fileId, seconds: seconds })
|
.changeSeconds(playlistId, { pfid: filePfid, seconds: seconds })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
setOriginSecs();
|
setOriginSecs();
|
||||||
@ -128,7 +136,9 @@ const Content = ({ form, playlistId, playlist }) => {
|
|||||||
|
|
||||||
const handleDelete = (index) => {
|
const handleDelete = (index) => {
|
||||||
API.playlists
|
API.playlists
|
||||||
.removeFile(playlistId, { file_id: form.values.files[index].pfid })
|
.removeFile(playlistId, {
|
||||||
|
pfid: form.values.files[index].pfid,
|
||||||
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
form.removeListItem('files', index);
|
form.removeListItem('files', index);
|
||||||
@ -141,48 +151,60 @@ const Content = ({ form, playlistId, playlist }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const fields = form.values.files.map((_, index) => (
|
const fields = form.values.files.map((_, index) =>
|
||||||
|
canEdit ? (
|
||||||
<Draggable key={index} index={index} draggableId={index.toString()}>
|
<Draggable key={index} index={index} draggableId={index.toString()}>
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<Group ref={provided.innerRef} mt="xs" {...provided.draggableProps} position="center">
|
<Group ref={provided.innerRef} mt="xs" {...provided.draggableProps} position="center">
|
||||||
<Paper p="xs" radius="sm" shadow="sm" withBorder spacing="xs" style={{ width: '90%' }}>
|
<Paper p="xs" radius="sm" shadow="sm" withBorder spacing="xs" style={{ width: '90%' }}>
|
||||||
<Flex direction="row" align="center" gap="lg" justify="flex-end">
|
<Flex direction="row" align="center" gap="lg" justify="flex-end">
|
||||||
<Text>{form.getInputProps(`files.${index}.name`).value}</Text>
|
<Text>{form.getInputProps(`files.${index}.name`).value}</Text>
|
||||||
<Image width={150} src={'/api/file/' + form.getInputProps(`files.${index}.id`).value} />
|
<MediaPlayer file={form.getInputProps(`files.${index}`).value} />
|
||||||
{ canEdit ?
|
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
hideControls
|
{...(isVideo(form.getInputProps(`files.${index}.type`).value)
|
||||||
description="Seconds to display"
|
? {
|
||||||
value={form.getInputProps(`files.${index}.seconds`).value}
|
disabled: true,
|
||||||
onChange={(secs) => handleChangeSeconds(secs, index)}
|
value: 0,
|
||||||
error={form.getInputProps(`files.${index}.seconds`).errors && 'This field is required'}
|
description: 'Default to video duration',
|
||||||
/>
|
|
||||||
: <Text>Display time: {parseTime(form.getInputProps(`files.${index}.seconds`).value)}</Text>
|
|
||||||
}
|
}
|
||||||
{canEdit ? (
|
: {
|
||||||
|
value: form.getInputProps(`files.${index}.seconds`).value,
|
||||||
|
onChange: (secs) => handleChangeSeconds(secs, index),
|
||||||
|
})}
|
||||||
|
hideControls
|
||||||
|
label="Seconds to display"
|
||||||
|
error={
|
||||||
|
form.getInputProps(`files.${index}.seconds`).errors && 'This field is required'
|
||||||
|
}
|
||||||
|
/>
|
||||||
<ActionIcon color="red" variant="light" size="lg" onClick={() => handleDelete(index)}>
|
<ActionIcon color="red" variant="light" size="lg" onClick={() => handleDelete(index)}>
|
||||||
<IconTrash size="1rem" />
|
<IconTrash size="1rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Paper>
|
</Paper>
|
||||||
{canEdit ? (
|
|
||||||
<Center {...provided.dragHandleProps}>
|
<Center {...provided.dragHandleProps}>
|
||||||
<IconGripVertical size="1.2rem" />
|
<IconGripVertical size="1.2rem" />
|
||||||
</Center>
|
</Center>
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
));
|
) : (
|
||||||
|
<Group key={index} mt="xs" position="center">
|
||||||
|
<Paper p="xs" radius="sm" shadow="sm" withBorder spacing="xs" style={{ width: '90%' }}>
|
||||||
|
<Flex direction="row" align="center" gap="lg" justify="flex-end">
|
||||||
|
<Text>{form.getInputProps(`files.${index}.name`).value}</Text>
|
||||||
|
<MediaPlayer file={form.getInputProps(`files.${index}`).value} />
|
||||||
|
<Text>Display time: {parseTime(form.getInputProps(`files.${index}.seconds`).value)}</Text>
|
||||||
|
</Flex>
|
||||||
|
</Paper>
|
||||||
|
</Group>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box mx="auto" maw={1200}>
|
<Box mx="auto" maw={1200}>
|
||||||
|
{canEdit ? (
|
||||||
<DragDropContext
|
<DragDropContext
|
||||||
onDragEnd={({ destination, source }) => {
|
onDragEnd={({ destination, source }) => {
|
||||||
form.reorderListItem('files', { from: source.index, to: destination.index });
|
form.reorderListItem('files', { from: source.index, to: destination.index });
|
||||||
@ -202,6 +224,9 @@ const Content = ({ form, playlistId, playlist }) => {
|
|||||||
)}
|
)}
|
||||||
</StrictModeDroppable>
|
</StrictModeDroppable>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
|
) : (
|
||||||
|
fields
|
||||||
|
)}
|
||||||
|
|
||||||
{canEdit ? (
|
{canEdit ? (
|
||||||
<>
|
<>
|
||||||
|
@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import API from '../../services/api';
|
import API from '../../services/api';
|
||||||
import { parseTime } from '../../tools/timeUtil';
|
import { parseTime } from '../../tools/timeUtil';
|
||||||
import setNotification from '../errors/error-notification';
|
import setNotification from '../errors/error-notification';
|
||||||
import ModalUpdate from '../playlists/update';
|
import ModalUpdatePlaylist from '../playlists/update';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import Content from './content';
|
import Content from './content';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@ -22,7 +22,7 @@ const Playlist = (item) => {
|
|||||||
|
|
||||||
const toggleActivate = () => {
|
const toggleActivate = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
(isActive ? API.disactivate : API.activate)(id)
|
(isActive ? API.playlists.disactivate : API.playlists.activate)(id)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
setIsActive(!isActive);
|
setIsActive(!isActive);
|
||||||
@ -50,10 +50,6 @@ const Playlist = (item) => {
|
|||||||
setDuration(duration);
|
setDuration(duration);
|
||||||
}, [form.values]);
|
}, [form.values]);
|
||||||
|
|
||||||
const updatePlaylist = (playlist) => {
|
|
||||||
setPlaylist(playlist);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (JSON.stringify(item) !== '{}') {
|
if (JSON.stringify(item) !== '{}') {
|
||||||
setPlaylist(item);
|
setPlaylist(item);
|
||||||
@ -119,11 +115,11 @@ const Playlist = (item) => {
|
|||||||
<Paper p="xs" radius="sm" shadow="sm" withBorder my="md">
|
<Paper p="xs" radius="sm" shadow="sm" withBorder my="md">
|
||||||
<Content form={form} playlistId={id} playlist={playlist} />
|
<Content form={form} playlistId={id} playlist={playlist} />
|
||||||
</Paper>
|
</Paper>
|
||||||
<ModalUpdate
|
<ModalUpdatePlaylist
|
||||||
opened={showUpdate}
|
opened={showUpdate}
|
||||||
handler={toggleUpdate}
|
handler={toggleUpdate}
|
||||||
item={playlist}
|
item={playlist}
|
||||||
updatePlaylist={(playlist) => updatePlaylist(playlist)}
|
updatePlaylist={(playlist) => setPlaylist(playlist)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,7 @@ import PlaylistViewEditor from './playlist-view-editor';
|
|||||||
import API from '../../services/api';
|
import API from '../../services/api';
|
||||||
|
|
||||||
const ModalCreatePlaylist = ({ opened, handler, addPlaylist }) => {
|
const ModalCreatePlaylist = ({ opened, handler, addPlaylist }) => {
|
||||||
const validated = (item) => {
|
const validate = (item) => {
|
||||||
if (item) {
|
if (item) {
|
||||||
addPlaylist(item);
|
addPlaylist(item);
|
||||||
}
|
}
|
||||||
@ -26,7 +26,7 @@ const ModalCreatePlaylist = ({ opened, handler, addPlaylist }) => {
|
|||||||
<PlaylistViewEditor
|
<PlaylistViewEditor
|
||||||
buttonText="Create"
|
buttonText="Create"
|
||||||
APICall={API.playlists.create}
|
APICall={API.playlists.create}
|
||||||
handler={(item) => validated(item)}
|
handler={(item) => validate(item)}
|
||||||
/>
|
/>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
|
@ -8,7 +8,7 @@ import { Button } from '@mantine/core';
|
|||||||
import GrantAccess, { Perm } from '../../tools/grant-access';
|
import GrantAccess, { Perm } from '../../tools/grant-access';
|
||||||
|
|
||||||
const Playlists = () => {
|
const Playlists = () => {
|
||||||
const [showCreate, setShowCreate] = useState(true);
|
const [showCreate, setShowCreate] = useState(false);
|
||||||
const [showUpdate, setShowUpdate] = useState(false);
|
const [showUpdate, setShowUpdate] = useState(false);
|
||||||
const [, setItem] = useState({});
|
const [, setItem] = useState({});
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
@ -63,7 +63,7 @@ const Playlists = () => {
|
|||||||
<PlaylistTable
|
<PlaylistTable
|
||||||
data={playlists}
|
data={playlists}
|
||||||
updateItem={setItem} // todo
|
updateItem={setItem} // todo
|
||||||
// eslint-disable-next-line eqeqeq
|
// eslint-disable-next-line
|
||||||
onDelete={(id) => setPlaylist(playlists.filter((item) => item._id != id))}
|
onDelete={(id) => setPlaylist(playlists.filter((item) => item._id != id))}
|
||||||
updateHandler={toggleModalUpdate}
|
updateHandler={toggleModalUpdate}
|
||||||
loadMore={loadMore}
|
loadMore={loadMore}
|
||||||
|
@ -2,7 +2,7 @@ import { Button, TextInput, Group, Stack } from '@mantine/core';
|
|||||||
import { useForm, isNotEmpty } from '@mantine/form';
|
import { useForm, isNotEmpty } from '@mantine/form';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import setNotification from '../errors/error-notification';
|
import setNotification from '../errors/error-notification';
|
||||||
import RoleSelector from './role-selector';
|
import RoleSelector from '../roles/role-selector';
|
||||||
|
|
||||||
const PlaylistViewEditor = ({ item, handler, buttonText, APICall }) => {
|
const PlaylistViewEditor = ({ item, handler, buttonText, APICall }) => {
|
||||||
const handleClose = (playlist) => {
|
const handleClose = (playlist) => {
|
||||||
@ -11,8 +11,8 @@ const PlaylistViewEditor = ({ item, handler, buttonText, APICall }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [rolesView, setRolesView] = useState(item?.view.map((role) => role.id.toString()) ?? []);
|
const [rolesView, setRolesView] = useState(item?.view?.map((role) => role.id.toString()) ?? []);
|
||||||
const [rolesEdit, setRolesEdit] = useState(item?.edit.map((role) => role.id.toString()) ?? []);
|
const [rolesEdit, setRolesEdit] = useState(item?.edit?.map((role) => role.id.toString()) ?? []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (item) {
|
if (item) {
|
||||||
@ -37,10 +37,10 @@ const PlaylistViewEditor = ({ item, handler, buttonText, APICall }) => {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
if (item) {
|
if (item) {
|
||||||
await APICall(item?.id, { name: form.values.name });
|
const view = rolesView.map((roleId) => parseInt(roleId));
|
||||||
// todo permissions update
|
const edit = rolesEdit.map((roleId) => parseInt(roleId));
|
||||||
item.name = form.values.name;
|
const res = await APICall(item?.id, { name: form.values.name, view: view, edit: edit });
|
||||||
handleClose(item);
|
handleClose(res.data);
|
||||||
} else {
|
} else {
|
||||||
const view = rolesView.map((roleId) => parseInt(roleId));
|
const view = rolesView.map((roleId) => parseInt(roleId));
|
||||||
const edit = rolesEdit.map((roleId) => parseInt(roleId));
|
const edit = rolesEdit.map((roleId) => parseInt(roleId));
|
||||||
@ -72,7 +72,7 @@ const PlaylistViewEditor = ({ item, handler, buttonText, APICall }) => {
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Group position="right" mt="md">
|
<Group position="right" mt="md">
|
||||||
<Button variant="light" color="red" onClick={handleClose}>
|
<Button variant="light" color="red" onClick={() => handleClose()}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" variant="light" color="green" loading={isLoading}>
|
<Button type="submit" variant="light" color="green" loading={isLoading}>
|
||||||
|
@ -3,8 +3,10 @@ import API from '../../services/api';
|
|||||||
import PlaylistViewEditor from './playlist-view-editor';
|
import PlaylistViewEditor from './playlist-view-editor';
|
||||||
|
|
||||||
const ModalUpdatePlaylist = ({ item, opened, handler, updatePlaylist }) => {
|
const ModalUpdatePlaylist = ({ item, opened, handler, updatePlaylist }) => {
|
||||||
const validated = (playlist) => {
|
const validate = (playlist) => {
|
||||||
|
if (playlist) {
|
||||||
updatePlaylist(playlist);
|
updatePlaylist(playlist);
|
||||||
|
}
|
||||||
handler();
|
handler();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -25,7 +27,7 @@ const ModalUpdatePlaylist = ({ item, opened, handler, updatePlaylist }) => {
|
|||||||
item={item}
|
item={item}
|
||||||
buttonText="Update"
|
buttonText="Update"
|
||||||
APICall={API.playlists.update}
|
APICall={API.playlists.update}
|
||||||
handler={(playlist) => validated(playlist)}
|
handler={(playlist) => validate(playlist)}
|
||||||
/>
|
/>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
|
38
src/pages/roles/create.jsx
Normal file
38
src/pages/roles/create.jsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Modal, Text } from '@mantine/core';
|
||||||
|
import API from '../../services/api';
|
||||||
|
import RoleViewEditor from './role-view-editor';
|
||||||
|
|
||||||
|
const ModalCreateRole = ({ opened, handler, addRole, item }) => {
|
||||||
|
const validate = (role) => {
|
||||||
|
if (role) {
|
||||||
|
addRole(role);
|
||||||
|
}
|
||||||
|
handler();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal.Root opened={opened} onClose={handler}>
|
||||||
|
<Modal.Overlay />
|
||||||
|
<Modal.Content>
|
||||||
|
<Modal.Header>
|
||||||
|
<Modal.Title>
|
||||||
|
<Text fw={700} fz="lg">
|
||||||
|
Create Role
|
||||||
|
</Text>
|
||||||
|
</Modal.Title>
|
||||||
|
<Modal.CloseButton />
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<RoleViewEditor
|
||||||
|
buttonText="Create"
|
||||||
|
item={item}
|
||||||
|
APICall={API.roles.create}
|
||||||
|
handler={(role) => validate(role)}
|
||||||
|
/>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalCreateRole;
|
@ -2,10 +2,19 @@ import { MultiSelect } from '@mantine/core';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import setNotification from '../errors/error-notification';
|
import setNotification from '../errors/error-notification';
|
||||||
import API from '../../services/api';
|
import API from '../../services/api';
|
||||||
|
import { Perm, checkPerm } from '../../tools/grant-access';
|
||||||
|
import { useAuth } from '../../tools/auth-provider';
|
||||||
|
import ModalCreateRole from './create';
|
||||||
|
|
||||||
const RoleSelector = ({ defaultRoles, label, value, setValue }) => {
|
const RoleSelector = ({ defaultRoles, label, value, setValue }) => {
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState([]);
|
||||||
const [search, setSearch] = useState();
|
const [search, setSearch] = useState();
|
||||||
|
const [showCreateRole, setShowCreateRole] = useState(false);
|
||||||
|
const toggleCreateRole = () => setShowCreateRole(!showCreateRole);
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
|
const { user } = useAuth();
|
||||||
|
const canCreateRole = checkPerm(Perm.CREATE_ROLE, user);
|
||||||
|
|
||||||
const addRoles = (roles) => {
|
const addRoles = (roles) => {
|
||||||
if (!roles) return;
|
if (!roles) return;
|
||||||
@ -40,14 +49,8 @@ const RoleSelector = ({ defaultRoles, label, value, setValue }) => {
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [defaultRoles]);
|
}, [defaultRoles]);
|
||||||
|
|
||||||
// creatable
|
|
||||||
// getCreateLabel={(query) => `+ Create ${query}`}
|
|
||||||
// onCreate={(query) => {
|
|
||||||
// const item = { value: query, label: query };
|
|
||||||
// setData((current) => [...current, item]);
|
|
||||||
// return item;
|
|
||||||
// }}
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
label={label}
|
label={label}
|
||||||
data={data}
|
data={data}
|
||||||
@ -57,7 +60,22 @@ const RoleSelector = ({ defaultRoles, label, value, setValue }) => {
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
maxDropdownHeight={160}
|
maxDropdownHeight={160}
|
||||||
|
creatable={canCreateRole}
|
||||||
|
getCreateLabel={(query) => `+ Create ${query}`}
|
||||||
|
onCreate={(query) => {
|
||||||
|
setQuery(query);
|
||||||
|
setShowCreateRole(true);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
{canCreateRole && (
|
||||||
|
<ModalCreateRole
|
||||||
|
opened={showCreateRole}
|
||||||
|
item={{ name: query }}
|
||||||
|
addRole={(role) => addRoles([role])}
|
||||||
|
handler={toggleCreateRole}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
64
src/pages/roles/role-view-editor.jsx
Normal file
64
src/pages/roles/role-view-editor.jsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { Button, TextInput, Group, Stack } from '@mantine/core';
|
||||||
|
import { useForm, isNotEmpty } from '@mantine/form';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import setNotification from '../errors/error-notification';
|
||||||
|
|
||||||
|
const RoleViewEditor = ({ item, handler, buttonText, APICall }) => {
|
||||||
|
const handleClose = (role) => {
|
||||||
|
form.reset();
|
||||||
|
handler(role);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
name: item?.name ?? '',
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
name: isNotEmpty('Name is required'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.setFieldValue('name', item?.name);
|
||||||
|
return () => {};
|
||||||
|
}, [item]);
|
||||||
|
|
||||||
|
const handleSubmit = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (form.validate().hasErrors) return;
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
if (item?.id) {
|
||||||
|
const res = await APICall(item?.id, { name: form.values.name });
|
||||||
|
handleClose(res.data);
|
||||||
|
} else {
|
||||||
|
const res = await APICall({ name: form.values.name });
|
||||||
|
handleClose(res.data);
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
} catch (err) {
|
||||||
|
setIsLoading(false);
|
||||||
|
setNotification(true, err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<TextInput label="Name" placeholder="Name" withAsterisk {...form.getInputProps('name')} mb="sm" />
|
||||||
|
todo parent id
|
||||||
|
users
|
||||||
|
<Group position="right" mt="md">
|
||||||
|
<Button variant="light" color="red" onClick={() => handler()}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" variant="light" color="green" loading={isLoading}>
|
||||||
|
{buttonText}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoleViewEditor;
|
@ -64,10 +64,10 @@ const API = {
|
|||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
upload(data) {
|
upload(data) {
|
||||||
return caller().post('/file', data);
|
return caller().post('/files', data);
|
||||||
},
|
},
|
||||||
list() {
|
list() {
|
||||||
return caller().get('/file');
|
return caller().get('/files');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
profile() {
|
profile() {
|
||||||
|
@ -4,7 +4,7 @@ module.exports = function(app) {
|
|||||||
app.use(
|
app.use(
|
||||||
'/api',
|
'/api',
|
||||||
createProxyMiddleware({
|
createProxyMiddleware({
|
||||||
target: 'http://192.168.2.183:5500',
|
target: 'http://127.0.0.1:5500',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
10
src/tools/fileUtil.js
Normal file
10
src/tools/fileUtil.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export const isImage = (type) => {
|
||||||
|
if (!type) return false;
|
||||||
|
return type.split('/')[0] === 'image';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isVideo = (type) => {
|
||||||
|
if (!type) return false;
|
||||||
|
return type.split('/')[0] === 'video';
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user