All checks were successful
Music Collection CI Workflow / test (./backend) (push) Successful in 37s
Music Collection CI Workflow / test (./frontend) (push) Successful in 39s
Music Collection CI Workflow / build-and-push-images (./backend/Dockerfile, git.anatid.net/tabris/music-collection-backend, ./backend) (push) Successful in 53s
Music Collection CI Workflow / build-and-push-images (./frontend/Dockerfile, git.anatid.net/tabris/music-collection-frontend, ./frontend) (push) Successful in 1m50s
Music Collection CI Workflow / deploy (push) Successful in 24s
227 lines
8.3 KiB
TypeScript
227 lines
8.3 KiB
TypeScript
'use client'
|
|
|
|
import { FormEvent, MouseEvent, useState, useEffect } from 'react';
|
|
import { useParams } from 'next/navigation'
|
|
import { Album } from '@/entities/album.entity';
|
|
import { Song } from '@/entities/song.entity';
|
|
import { TimeUtils } from '@/utils/time.util';
|
|
import { createSong, deleteSong, getAlbum, getSong, updateSong, revalidateAlbum } from '@/app/actions';
|
|
import Button from 'react-bootstrap/Button';
|
|
import Modal from 'react-bootstrap/Modal';
|
|
import { Alert, Snackbar, IconButton } from '@mui/material';
|
|
import { AddCircleOutline, Delete, Edit } from '@mui/icons-material';
|
|
|
|
export default function Page() {
|
|
const params = useParams<{ id: string }>();
|
|
const albumId = params.id;
|
|
const [album, setAlbum] = useState<Album>();
|
|
const [show, setShow] = useState(false);
|
|
const handleClose = () => setShow(false);
|
|
const handleShow = () => setShow(true);
|
|
const [formAlbumId, setFormAlbumId] = useState(albumId);
|
|
const [formSongId, setFormSongId] = useState("");
|
|
const [formSongTitle, setFormSongTitle] = useState("");
|
|
const [formSongDuration, setFormSongDuration] = useState("");
|
|
const [formSongTrackNumber, setFormSongTrackNumber] = useState("");
|
|
const [formModalTitle, setFormModalTitle] = useState("Add Song");
|
|
const [formModalButtonLabel, setFormModalButtonLabel] = useState("Add");
|
|
const [showSnackbar, setShowSnackbar] = useState(false);
|
|
const handleCloseSnackbar = () => setShowSnackbar(false);
|
|
const handleOpenSnackbar = () => setShowSnackbar(true);
|
|
const [snackbarSuccess, setSnackbarSuccess] = useState(true);
|
|
const [snackbarMessage, setSnackbarMessage] = useState("");
|
|
|
|
useEffect(() => {
|
|
async function fetchAlbum(albumId: string) {
|
|
try {
|
|
revalidateAlbum();
|
|
const data = await getAlbum(parseInt(albumId));
|
|
setAlbum(data);
|
|
} catch (error) {
|
|
console.error(`Error getting album with id ${albumId}:`, error);
|
|
}
|
|
}
|
|
fetchAlbum(albumId);
|
|
}, [albumId]);
|
|
|
|
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
event.preventDefault();
|
|
handleClose();
|
|
|
|
const formData = new FormData(event.currentTarget);
|
|
try {
|
|
if (formData.get('id') == "") {
|
|
await createSong(formData).then(response => {
|
|
if (response.ok) {
|
|
handleSnackbar(`Successfully created Song with ID ${formData.get("id")}: ${JSON.stringify(response)}`, true);
|
|
}
|
|
handleSnackbar(`Failed to create Song with ID ${formData.get("id")}: ${JSON.stringify(response)}`, false);
|
|
})
|
|
.catch(error => {handleSnackbar(`Failed to create Song with ID ${formData.get("id")}: ${JSON.stringify(error)}`, false);});
|
|
} else {
|
|
await updateSong(formData).then(response => {
|
|
if (response.ok) {
|
|
handleSnackbar(`Successfully created Song with ID ${formData.get("id")}: ${JSON.stringify(response)}`, true);
|
|
}
|
|
handleSnackbar(`Failed to create Song with ID ${formData.get("id")}: ${JSON.stringify(response)}`, false);
|
|
})
|
|
.catch(error => {handleSnackbar(`Failed to create Song with ID ${formData.get("id")}: ${JSON.stringify(error)}`, false);});
|
|
}
|
|
revalidateAlbum();
|
|
const data = await getAlbum(parseInt(albumId));
|
|
setAlbum(data);
|
|
} catch (error) {
|
|
handleSnackbar(`Error creating Song: ${JSON.stringify(error)}`, false);
|
|
}
|
|
}
|
|
|
|
const handleCreate = async () => {
|
|
setFormModalTitle("Add Song");
|
|
setFormModalButtonLabel("Add");
|
|
setFormAlbumId(albumId);
|
|
setFormSongId("");
|
|
setFormSongTitle("");
|
|
setFormSongDuration("");
|
|
setFormSongTrackNumber("");
|
|
handleShow();
|
|
}
|
|
|
|
const handleEdit = async (event: MouseEvent<HTMLElement>) => {
|
|
const songId = event.currentTarget.getAttribute('song-id');
|
|
if (songId) {
|
|
try {
|
|
const data = await getSong(parseInt(songId));
|
|
setFormModalTitle("Edit Song");
|
|
setFormModalButtonLabel("Save");
|
|
setFormSongId(songId);
|
|
setFormAlbumId(albumId);
|
|
setFormSongTitle(data.title);
|
|
setFormSongDuration(data.duration);
|
|
setFormSongTrackNumber(data.trackNumber);
|
|
handleShow();
|
|
} catch (error) {
|
|
handleSnackbar(`Error getting song with ID ${songId}: ${JSON.stringify(error)}`, false);
|
|
}
|
|
} else {
|
|
handleSnackbar(`Couldn't get ID of clicked element`, false);
|
|
console.error("Couldn't get ID of clicked element");
|
|
}
|
|
}
|
|
|
|
const handleDelete = async (event: MouseEvent<HTMLElement>) => {
|
|
const songId = event.currentTarget.getAttribute('song-id');
|
|
if (songId) {
|
|
try {
|
|
await deleteSong(parseInt(songId));
|
|
} catch (error) {
|
|
handleSnackbar(`Error deleting song with ID ${songId}: ${JSON.stringify(error)}`, false);
|
|
}
|
|
revalidateAlbum();
|
|
const data = await getAlbum(parseInt(albumId));
|
|
setAlbum(data);
|
|
} else {
|
|
handleSnackbar(`Couldn't get ID of clicked element`, false);
|
|
}
|
|
}
|
|
|
|
const handleSnackbar = async (message: string, severity: boolean) => {
|
|
setSnackbarMessage(message);
|
|
setSnackbarSuccess(severity);
|
|
handleOpenSnackbar();
|
|
}
|
|
|
|
if (!album) return <div>Loading...</div>
|
|
|
|
return (
|
|
<>
|
|
<div className="container">
|
|
<div>
|
|
<h3>{album.title}</h3>
|
|
<h4>{album.artist}</h4>
|
|
<em>({album.genre})</em>
|
|
</div>
|
|
<IconButton aria-label="Add Album" size="large" onClick={handleCreate}>
|
|
<AddCircleOutline fontSize="large" color="success"/>
|
|
</IconButton>
|
|
<table className="u-full-width">
|
|
<thead>
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Title</th>
|
|
<th>Duration</th>
|
|
<th>Controls</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{album.songs.map((song: Song) => (
|
|
<tr key={song.id}>
|
|
<td>{song.trackNumber}</td>
|
|
<td>{song.title}</td>
|
|
<td>{TimeUtils.fancyTimeFormat(song.duration)}</td>
|
|
<td>
|
|
<IconButton aria-label="Edit Song" size="small" song-id={song.id.toString()} onClick={handleEdit}>
|
|
<Edit fontSize="large" color="success"/>
|
|
</IconButton>
|
|
<IconButton aria-label="Delete Song" size="small" song-id={song.id.toString()} onClick={handleDelete}>
|
|
<Delete fontSize="large" color="error"/>
|
|
</IconButton>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<Modal
|
|
show={show}
|
|
onHide={handleClose}
|
|
backdrop="static"
|
|
keyboard={false}
|
|
>
|
|
<Modal.Header closeButton>
|
|
<Modal.Title>{formModalTitle}</Modal.Title>
|
|
</Modal.Header>
|
|
<Modal.Body>
|
|
<div>
|
|
<form onSubmit={handleSubmit}>
|
|
<input name="id" id="song-id" value={formSongId} type="hidden" />
|
|
<input name="albumId" id="album-id" value={formAlbumId} type="hidden" />
|
|
<div className="row">
|
|
<div className="six columns">
|
|
<label htmlFor="song-title">Song Title</label><input type="text" name="title" id="song-title" defaultValue={formSongTitle} />
|
|
</div>
|
|
<div className="six columns">
|
|
<label htmlFor="song-duration">Duration (seconds)</label><input type="text" name="duration" id="song-duration" defaultValue={formSongDuration} />
|
|
</div>
|
|
</div>
|
|
<div className="row">
|
|
<div className="six columns">
|
|
<label htmlFor="song-trackNumber">Track Number</label><input type="text" name="trackNumber" id="song-trackNumber" defaultValue={formSongTrackNumber} />
|
|
</div>
|
|
</div>
|
|
<Button variant="primary" type="submit">{formModalButtonLabel}</Button>
|
|
</form>
|
|
</div>
|
|
</Modal.Body>
|
|
<Modal.Footer>
|
|
<Button variant="secondary" onClick={handleClose}>
|
|
Close
|
|
</Button>
|
|
</Modal.Footer>
|
|
</Modal>
|
|
|
|
<Snackbar open={showSnackbar} autoHideDuration={6000} onClose={handleCloseSnackbar}>
|
|
<Alert
|
|
onClose={handleCloseSnackbar}
|
|
severity={snackbarSuccess ? 'success' : 'warning'}
|
|
variant="filled"
|
|
sx={{ width: '100%' }}
|
|
>
|
|
{snackbarMessage}
|
|
</Alert>
|
|
</Snackbar>
|
|
|
|
</>
|
|
);
|
|
}
|