Initial commit
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
import * as React from "react";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
import {AddAPhoto, ArrowCircleLeftOutlined, MoveDown, MoveUp} from "@mui/icons-material";
|
||||
import {useRef, useState} from "react";
|
||||
import useAlbumsApi from "../services/albums-api";
|
||||
import {Button, IconButton, Input, TextField} from "@mui/material";
|
||||
import {Photo} from "../models/photo";
|
||||
import DragAndDrop from "../components/drag-and-drop/drag-and-drop";
|
||||
import {Album} from "../models/album";
|
||||
import Loader from "../components/Loader";
|
||||
|
||||
import "./album-manager.scss";
|
||||
import {useInstances} from "../services/instances/use-instances";
|
||||
|
||||
function AlbumManager() {
|
||||
const {state} = useLocation();
|
||||
const {album} = state; // Read values passed on state
|
||||
const {selectedInstance} = useInstances();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
postAlbum,
|
||||
patchAlbum,
|
||||
patchPhoto,
|
||||
uploadPhotos,
|
||||
deletePhoto,
|
||||
movePhotoDown,
|
||||
movePhotoUp
|
||||
} = useAlbumsApi();
|
||||
|
||||
const [name, setName] = useState(album.name || '');
|
||||
const [description, setDescription] = useState(album.description || '');
|
||||
const [sort] = useState(album.sort || "auto" );
|
||||
|
||||
const [photosToUpdate, setPhotosToUpdate] = useState(album.photos.map(x => new Photo(x)));
|
||||
const [uploadApiError, setUploadApiError] = useState(null);
|
||||
const [apiError, setApiError] = useState(null);
|
||||
const [uploadSuccess, setUploadSuccess] = useState(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
const fileInputRef = useRef();
|
||||
|
||||
const handleSaveEdit = async (e) => {
|
||||
try {
|
||||
if (hasChanges()) {
|
||||
setApiError(null);
|
||||
let albumToUpdate = new Album({
|
||||
...album,
|
||||
name,
|
||||
description
|
||||
});
|
||||
let updatedAlbum;
|
||||
if (album.albumId) {
|
||||
updatedAlbum = await patchAlbum(selectedInstance, albumToUpdate);
|
||||
} else {
|
||||
updatedAlbum = await postAlbum(selectedInstance, albumToUpdate);
|
||||
}
|
||||
navigate(`/${selectedInstance.instanceId}/albums/${updatedAlbum.albumId}`, {state: {album: updatedAlbum}});
|
||||
}
|
||||
} catch (err) {
|
||||
if (typeof err === "string" && err.toLowerCase().indexOf("duplicate") >= 0) {
|
||||
setApiError("Name already in use");
|
||||
} else {
|
||||
console.error(err);
|
||||
setApiError("An error occurred trying to save the album, contact the system administrator");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSavePhotoEdit = async (idx) => {
|
||||
try {
|
||||
setApiError(null);
|
||||
if (photoHasChanges(idx)) {
|
||||
const updatedAlbum = await patchPhoto(
|
||||
selectedInstance,
|
||||
album.albumId,
|
||||
photosToUpdate[idx]
|
||||
);
|
||||
navigate(`/${selectedInstance.instanceId}/albums/${updatedAlbum.albumId}`, {state: {album: updatedAlbum}});
|
||||
}
|
||||
} catch (err) {
|
||||
if (typeof err === "string" && err.toLowerCase().indexOf("duplicate") >= 0) {
|
||||
setApiError("Name already in use");
|
||||
} else {
|
||||
console.error(err);
|
||||
setApiError("An error occurred trying to save the photo, contact the system administrator");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleMovePhotoDown = async (idx) => {
|
||||
try {
|
||||
setApiError(null);
|
||||
const updatedAlbum = await movePhotoDown(
|
||||
selectedInstance,
|
||||
album.albumId,
|
||||
photosToUpdate[idx].photoId
|
||||
);
|
||||
const orgPhoto = photosToUpdate[idx];
|
||||
photosToUpdate[idx] = photosToUpdate[idx + 1];
|
||||
photosToUpdate[idx + 1] = orgPhoto;
|
||||
setPhotosToUpdate([...photosToUpdate]);
|
||||
navigate(`/${selectedInstance.instanceId}/albums/${updatedAlbum.albumId}`, {state: {album: updatedAlbum}});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setApiError("An error occurred trying to save the photo, contact the system administrator");
|
||||
}
|
||||
}
|
||||
|
||||
const handleMovePhotoUp = async (idx) => {
|
||||
try {
|
||||
setApiError(null);
|
||||
const updatedAlbum = await movePhotoUp(
|
||||
selectedInstance,
|
||||
album.albumId,
|
||||
photosToUpdate[idx].photoId
|
||||
);
|
||||
const orgPhoto = photosToUpdate[idx];
|
||||
photosToUpdate[idx] = photosToUpdate[idx - 1];
|
||||
photosToUpdate[idx - 1] = orgPhoto;
|
||||
setPhotosToUpdate([...photosToUpdate]);
|
||||
navigate(`/${selectedInstance.instanceId}/albums/${updatedAlbum.albumId}`, {state: {album: updatedAlbum}});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setApiError("An error occurred trying to save the photo, contact the system administrator");
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeletePhoto = async (e, photoId) => {
|
||||
try {
|
||||
setApiError(null);
|
||||
const updatedAlbum = await deletePhoto(selectedInstance, album.albumId, photoId);
|
||||
setPhotosToUpdate(updatedAlbum.photos);
|
||||
navigate(`/${selectedInstance.instanceId}/albums/${updatedAlbum.albumId}`, {state: {album: updatedAlbum}});
|
||||
} catch (err) {
|
||||
setApiError("An error occurred trying to save the photo, contact the system administrator");
|
||||
}
|
||||
}
|
||||
|
||||
const handlePhotoNameChange = (photoIndex, e) => {
|
||||
photosToUpdate[photoIndex].name = e.target.value;
|
||||
setPhotosToUpdate([...photosToUpdate]);
|
||||
}
|
||||
|
||||
const handlePhotoDescriptionChange = (photoIndex, e) => {
|
||||
photosToUpdate[photoIndex].description = e.target.value;
|
||||
setPhotosToUpdate([...photosToUpdate]);
|
||||
}
|
||||
|
||||
const hasChanges = () => album.sort !== sort || album.name !== name || album.description !== description;
|
||||
|
||||
const photoHasChanges = (idx) => {
|
||||
return (photosToUpdate[idx].name || "") !== (album.photos[idx].name || "") ||
|
||||
(photosToUpdate[idx].description || "") !== (album.photos[idx].description || "");
|
||||
}
|
||||
|
||||
const handleNavigateToOverview = (e) => {
|
||||
navigate(`/${selectedInstance.instanceId}`);
|
||||
}
|
||||
|
||||
const handleDrop = async (files) => {
|
||||
await handleFiles(files)
|
||||
}
|
||||
|
||||
const handleFiles = async (files) => {
|
||||
try {
|
||||
setUploadApiError(null);
|
||||
setUploadSuccess(null);
|
||||
setIsUploading(true);
|
||||
const arrFiles = [...files];
|
||||
const formData = new FormData();
|
||||
for (let file of arrFiles) {
|
||||
formData.append('photos', file);
|
||||
}
|
||||
const updatedAlbum = await uploadPhotos(selectedInstance, album.albumId, formData);
|
||||
setPhotosToUpdate(updatedAlbum.photos);
|
||||
setUploadSuccess(true);
|
||||
navigate(`/${selectedInstance.instanceId}/albums/${updatedAlbum.albumId}`, {state: {album: updatedAlbum}});
|
||||
} catch (err) {
|
||||
if (typeof err === "string" && err.toLowerCase().indexOf("already exist") >= 0) {
|
||||
setUploadApiError(err);
|
||||
} else if (typeof err === "string" && err.toLowerCase().indexOf("mimetype") >= 0) {
|
||||
setUploadApiError(err);
|
||||
} else {
|
||||
console.error(err);
|
||||
setUploadApiError("An error occurred trying to upload your photo's, contact the system administrator");
|
||||
}
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return selectedInstance && <div id="admin-page-manage-albums">
|
||||
<div id="page-title">
|
||||
<div id="left-part">
|
||||
<IconButton onClick={handleNavigateToOverview} color="primary">
|
||||
<ArrowCircleLeftOutlined id="navigate-back"/>
|
||||
</IconButton>
|
||||
<div id="edit-title" className="input-element">
|
||||
<Input onKeyUp={(e) => e.key === "Enter" && handleSaveEdit(e)}
|
||||
fullWidth={true}
|
||||
type="text"
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Name..."
|
||||
tabIndex={1}
|
||||
value={name}
|
||||
/>
|
||||
</div>
|
||||
<div className="photos-count">({album.photos.length} photos)</div>
|
||||
</div>
|
||||
</div>
|
||||
{apiError && <div className="error-box">{apiError}</div>}
|
||||
{album && <div>
|
||||
<div id="album-description">
|
||||
<div className="input-element">
|
||||
<TextField multiline={true} fullWidth={true} rows={8}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Description..."
|
||||
tabIndex={2}
|
||||
value={description}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="save-button">
|
||||
<Button id="page-save" variant="contained" color="info" disabled={!hasChanges()}
|
||||
onClick={handleSaveEdit}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
{album?.albumId &&
|
||||
<div id="photo-list">
|
||||
<div className="photo photo--new">
|
||||
<div className="cover-photo">
|
||||
<DragAndDrop onDrop={handleDrop} onClick={(e) => fileInputRef.current.click(e)}>
|
||||
{!isUploading && <AddAPhoto className="no-photos" color="primary"/>}
|
||||
<Loader loading={isUploading}/>
|
||||
</DragAndDrop>
|
||||
<input type="file" id="photosUpload" ref={fileInputRef} multiple accept="image/*"
|
||||
onChange={(e) => handleFiles(e.target.files)}></input>
|
||||
{uploadApiError && <div className="upload-error">{uploadApiError}</div>}
|
||||
{uploadSuccess && <div className="upload-success">Photo's uploaded successfully</div>}
|
||||
</div>
|
||||
</div>
|
||||
{photosToUpdate?.map((photo, idx) =>
|
||||
<div className="card-layout photo" key={photo.photoId}>
|
||||
<div className="cover-photo">
|
||||
{<img src={`${photo.url}?width=450&height=450&fit=cover`} alt={photo.name}/>}
|
||||
<div className="file-name">
|
||||
{photo.fileName}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-details">
|
||||
<div className="title input-element">
|
||||
<TextField fullWidth={true} variant="standard"
|
||||
value={photo.name}
|
||||
onChange={(e) => handlePhotoNameChange(idx, e)}
|
||||
/>
|
||||
</div>
|
||||
<div className="description input-element">
|
||||
<TextField multiline={true} fullWidth={true} rows={8}
|
||||
onChange={(e) => handlePhotoDescriptionChange(idx, e)}
|
||||
value={photo.description || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="input-buttons">
|
||||
<div className="left-buttons">
|
||||
<Button className="photo-save" variant="contained" color="error"
|
||||
onClick={(e) => handleDeletePhoto(e, photo.photoId)}>
|
||||
Delete
|
||||
</Button>
|
||||
<Button className="photo-save" variant="contained" color="info"
|
||||
disabled={!photoHasChanges(idx)}
|
||||
onClick={() => handleSavePhotoEdit(idx)}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<div className="right-buttons">
|
||||
<Button className="photo-save"
|
||||
style={{visibility: (idx > 0 ? "visible" : "hidden")}}
|
||||
startIcon={<MoveUp/>} variant="contained" color="action"
|
||||
onClick={() => handleMovePhotoUp(idx)}>
|
||||
Move up
|
||||
</Button>
|
||||
<Button className="photo-save"
|
||||
style={{visibility: (idx < photosToUpdate.length - 1 ? "visible" : "hidden")}}
|
||||
startIcon={<MoveDown/>} variant="contained" color="action"
|
||||
onClick={(e) => handleMovePhotoDown(idx)}>
|
||||
Move down
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
</div>}
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default AlbumManager;
|
||||
@@ -0,0 +1,119 @@
|
||||
@import "../variables";
|
||||
|
||||
#admin-page-manage-albums {
|
||||
#page-title {
|
||||
#left-part {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
#left-part {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
|
||||
button {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
#edit-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.photos-count {
|
||||
flex: 1 1 100%;
|
||||
margin-left: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#album-description {
|
||||
.MuiFormControl-root {
|
||||
background: white;
|
||||
border-radius: $borderRadius;
|
||||
}
|
||||
}
|
||||
|
||||
.save-button {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
#photo-list {
|
||||
margin-top: 25px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 25px;
|
||||
|
||||
.photo {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
border: 2px solid $borderColor;
|
||||
border-radius: $borderRadius;
|
||||
position: relative;
|
||||
|
||||
&.photo--new {
|
||||
border-style: dashed;
|
||||
background: #fff;
|
||||
|
||||
.cover-photo {
|
||||
height: 150px;
|
||||
flex: 1;
|
||||
|
||||
.draggable-content {
|
||||
height: 150px;
|
||||
padding-left: calc(50% - 37.5px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transition: 0.3s;
|
||||
scale: 1.2
|
||||
}
|
||||
|
||||
#page-loader {
|
||||
position: absolute;
|
||||
padding-left: 0;
|
||||
padding-top: unset;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.title {
|
||||
|
||||
.MuiInput-input {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: 968px) {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.photo-details {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import * as React from "react";
|
||||
import {useRef, useState} from "react";
|
||||
import {NavLink, useNavigate} from "react-router-dom";
|
||||
import {Button, IconButton, InputAdornment, TextField} from "@mui/material";
|
||||
import {useAuth} from "../services/authentication/use-auth";
|
||||
import {useInstances} from "../services/instances/use-instances";
|
||||
import {Instance} from "../models/instance";
|
||||
import {
|
||||
AddAPhoto, AddCircle,
|
||||
ArrowRightAltOutlined, RemoveCircle
|
||||
} from "@mui/icons-material";
|
||||
|
||||
import "./instance-selector-page.scss";
|
||||
import DragAndDrop from "../components/drag-and-drop/drag-and-drop";
|
||||
|
||||
export function InstanceSelectorPage() {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
instances,
|
||||
createInstance,
|
||||
deleteInstance,
|
||||
updateInstance,
|
||||
uploadCoverPhoto,
|
||||
instanceApiError
|
||||
} = useInstances();
|
||||
const [instancesToUpdate, setInstancesToUpdate] = useState(instances?.map(inst => new Instance(inst)) || []);
|
||||
const {userData} = useAuth();
|
||||
|
||||
const [newInstanceTitle, setNewInstanceTitle] = useState("");
|
||||
|
||||
// useEffect(() => {
|
||||
// setInstancesToUpdate(instances);
|
||||
// }, [instances]);
|
||||
|
||||
let onNewInstanceTitleChanged = (e) => {
|
||||
setNewInstanceTitle(e.target.value);
|
||||
}
|
||||
|
||||
const handleInstanceTitleChange = (e, idx) => {
|
||||
instancesToUpdate[idx].title = e.target.value;
|
||||
setInstancesToUpdate([...instancesToUpdate]);
|
||||
}
|
||||
const handleInstanceSubtitleChange = (e, idx) => {
|
||||
instancesToUpdate[idx].subtitle = e.target.value;
|
||||
setInstancesToUpdate([...instancesToUpdate]);
|
||||
}
|
||||
const handleNewInstanceUrlChange = (e, idx) => {
|
||||
instancesToUpdate[idx].newInstanceUrl = e.target.value;
|
||||
setInstancesToUpdate([...instancesToUpdate]);
|
||||
}
|
||||
const handleAddNewInstanceUrl = (e, idx) => {
|
||||
instancesToUpdate[idx].urls.push(instancesToUpdate[idx].newInstanceUrl);
|
||||
instancesToUpdate[idx].newInstanceUrl = "";
|
||||
setInstancesToUpdate([...instancesToUpdate]);
|
||||
}
|
||||
const handleDeleteUrl = (e, idx, url) => {
|
||||
instancesToUpdate[idx].urls = instancesToUpdate[idx].urls.filter(u => u !== url);
|
||||
setInstancesToUpdate([...instancesToUpdate]);
|
||||
}
|
||||
|
||||
const instanceHasChanges = (idx) => {
|
||||
return (instancesToUpdate[idx].title || "") !== (instances[idx].title || "") ||
|
||||
(instancesToUpdate[idx].subtitle || "") !== (instances[idx].subtitle || "") ||
|
||||
JSON.stringify(instancesToUpdate[idx].urls) !== JSON.stringify(instances[idx].urls);
|
||||
}
|
||||
const handleCreateInstance = async () => {
|
||||
await createInstance(new Instance({
|
||||
title: newInstanceTitle
|
||||
}));
|
||||
}
|
||||
const handleSaveInstance = async (idx) => {
|
||||
if (instanceHasChanges(idx)) {
|
||||
instancesToUpdate[idx] = await updateInstance(instancesToUpdate[idx]);
|
||||
setInstancesToUpdate([...instancesToUpdate]);
|
||||
}
|
||||
}
|
||||
const handleDeleteInstance = async (e, instanceId) => {
|
||||
await deleteInstance(instanceId);
|
||||
}
|
||||
|
||||
const [isUploading, setIsUploading] = useState({});
|
||||
const fileInputRefs = useRef([]);
|
||||
const handleFiles = async (event, idx) => {
|
||||
setIsUploading({...isUploading, [idx]: true});
|
||||
const formData = new FormData();
|
||||
formData.append('cover-photo', event.target.files[0]);
|
||||
await uploadCoverPhoto(instancesToUpdate[idx].instanceId, formData);
|
||||
setIsUploading({...isUploading, [idx]: false});
|
||||
}
|
||||
|
||||
const handleDrop = async (e, idx) => {
|
||||
await handleFiles(e, idx)
|
||||
}
|
||||
|
||||
return <div id="instance-selector-page">
|
||||
{instanceApiError && <div className="error-box">{instanceApiError}</div>}
|
||||
<div>
|
||||
{userData?.profile?.resource_access?.photos?.roles?.indexOf("admin") >= 0 && <>
|
||||
<div id="edit-title" className="input-element">
|
||||
<TextField fullWidth={true} variant="standard"
|
||||
value={newInstanceTitle}
|
||||
placeholder="Instance..."
|
||||
onChange={onNewInstanceTitleChanged}
|
||||
/>
|
||||
</div>
|
||||
<div className="input-buttons">
|
||||
<div className="right-buttons">
|
||||
<Button className="save" variant="contained" color="info"
|
||||
onClick={handleCreateInstance}>
|
||||
Create instance
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
<div id="instance-list">
|
||||
{instancesToUpdate?.map((instance, idx) =>
|
||||
<div className="card-layout instance" key={instance.instanceId}>
|
||||
<div
|
||||
className={`${idx} cover-photo ${(!isUploading[idx] && instance.coverPhoto ? "" : "cover-photo--no-img")}`}>
|
||||
<DragAndDrop onDrop={(e) => handleDrop(e, idx)}
|
||||
onClick={(e) => fileInputRefs.current[idx].click(e)}
|
||||
isLoading={isUploading[idx]}>
|
||||
{!isUploading[idx] && instance.coverPhoto
|
||||
? <img src={`${instance.coverPhoto.url}?width=450&height=450&fit=cover`}
|
||||
alt={instance.coverPhoto.name}/>
|
||||
: <AddAPhoto className="no-photos"/>
|
||||
}
|
||||
</DragAndDrop>
|
||||
<input type="file" id={`photosUpload`} ref={(element) => fileInputRefs.current[idx] = element}
|
||||
accept="image/*"
|
||||
onChange={(e) => handleFiles(e, idx)}></input>
|
||||
</div>
|
||||
<div className="card-details">
|
||||
<div className="title input-element">
|
||||
<TextField fullWidth={true} variant="standard"
|
||||
value={instance.title}
|
||||
placeholder="Title..."
|
||||
onChange={(e) => handleInstanceTitleChange(e, idx)}
|
||||
/>
|
||||
</div>
|
||||
<div className="description input-element">
|
||||
<TextField fullWidth={true} variant="standard"
|
||||
value={instance.subtitle || ""}
|
||||
placeholder="Subtitle..."
|
||||
onChange={(e) => handleInstanceSubtitleChange(e, idx)}
|
||||
/>
|
||||
</div>
|
||||
<div className="url input-element">
|
||||
<TextField fullWidth={true} variant="standard"
|
||||
value={instance.newInstanceUrl || ""}
|
||||
placeholder="https://my-photos.be"
|
||||
onChange={(e) => handleNewInstanceUrlChange(e, idx)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton color="warning"
|
||||
onClick={(e) => handleAddNewInstanceUrl(e, idx)}>
|
||||
<AddCircle/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}/>
|
||||
<div className="instance-links">
|
||||
{instance.urls.map(url => <div className="instance-link">
|
||||
<IconButton color="error" onClick={(e) => handleDeleteUrl(e, idx, url)}>
|
||||
<RemoveCircle />
|
||||
</IconButton>
|
||||
<NavLink to={url}>{url}</NavLink>
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="input-buttons">
|
||||
<div className="left-buttons">
|
||||
<Button className="photo-save" variant="contained" color="error"
|
||||
onClick={(e) => handleDeleteInstance(e, instance.instanceId)}>
|
||||
Delete
|
||||
</Button>
|
||||
<Button className="photo-save" variant="contained" color="info"
|
||||
disabled={!instanceHasChanges(idx)}
|
||||
onClick={() => handleSaveInstance(idx)}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<div className="right-buttons">
|
||||
<Button className="photo-save" variant="contained" color="info"
|
||||
onClick={() => navigate(`/${instance.instanceId}`)}>
|
||||
Albums <ArrowRightAltOutlined/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
@import "../variables";
|
||||
|
||||
#instance-selector-page {
|
||||
#instance-list {
|
||||
margin-top: 25px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 25px;
|
||||
|
||||
> .instance {
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
|
||||
.MuiFormControl-root {
|
||||
margin-right: 50px;
|
||||
}
|
||||
|
||||
.MuiInput-input {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.MuiButtonBase-root {
|
||||
flex: 0 0 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.instance-links {
|
||||
margin-top: 10px;
|
||||
}
|
||||
div.instance-link {
|
||||
padding: 2px 0;
|
||||
a {
|
||||
color: $accentColor;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: 968px) {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import {Button, TextField} from "@mui/material";
|
||||
import * as React from "react";
|
||||
import {useState} from "react";
|
||||
import useInstancesApi from "../services/instances/instances-api";
|
||||
import {useInstances} from "../services/instances/use-instances";
|
||||
|
||||
export function ManageInstancePage() {
|
||||
const {selectedInstance, setSelectedInstance} = useInstances();
|
||||
const {updateInstance} = useInstancesApi();
|
||||
let [instanceToUpdate, setInstanceToUpdate] = useState({});
|
||||
const [apiError, setApiError] = useState(null);
|
||||
|
||||
const handleTitleChange = (e) => {
|
||||
instanceToUpdate.title = e.target.value;
|
||||
setInstanceToUpdate(instanceToUpdate);
|
||||
}
|
||||
const handleSubtitleChange = (e) => {
|
||||
instanceToUpdate.subtitle = e.target.value;
|
||||
setInstanceToUpdate(instanceToUpdate);
|
||||
}
|
||||
|
||||
const instanceHasChanges = () => {
|
||||
return (instanceToUpdate.title || "") !== (selectedInstance.title || "") ||
|
||||
(instanceToUpdate.subtitle || "") !== (selectedInstance.subtitle || "");
|
||||
}
|
||||
|
||||
const handleSaveInstance = async () => {
|
||||
try {
|
||||
setApiError(null);
|
||||
if (instanceHasChanges()) {
|
||||
const updatedInstance = await updateInstance(
|
||||
instanceToUpdate
|
||||
);
|
||||
setSelectedInstance(updatedInstance);
|
||||
setInstanceToUpdate(updatedInstance);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setApiError("An error occurred trying to save the instance, contact the system administrator");
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className="card-layout photo">
|
||||
{apiError && <div className="error-box">{apiError}</div>}
|
||||
{selectedInstance && <>
|
||||
{selectedInstance.photo &&
|
||||
<div className="cover-photo">
|
||||
{<img src={`${selectedInstance.photo.url}?width=450&height=450&fit=cover`}
|
||||
alt={selectedInstance.photo.name}/>}
|
||||
<div className="file-name">
|
||||
{selectedInstance.photo.fileName}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="card-details">
|
||||
<div className="title input-element">
|
||||
<TextField fullWidth={true} variant="standard"
|
||||
value={selectedInstance.title}
|
||||
onChange={handleTitleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="description input-element">
|
||||
<TextField multiline={true} fullWidth={true} rows={8}
|
||||
onChange={handleSubtitleChange}
|
||||
value={selectedInstance.subtitle || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="input-buttons">
|
||||
<div className="left-buttons">
|
||||
<Button className="photo-save" variant="contained" color="info"
|
||||
disabled={!instanceHasChanges()}
|
||||
onClick={handleSaveInstance}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function NoAccessPage() {
|
||||
return <h1>No access!</h1>
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import * as React from "react";
|
||||
import {useCallback, useEffect, useRef, useState} from "react";
|
||||
import useAlbumsApi from "../services/albums-api";
|
||||
import {
|
||||
AddAPhoto,
|
||||
KeyboardArrowRight,
|
||||
MoveDown,
|
||||
MoveUp
|
||||
} from "@mui/icons-material";
|
||||
import {Link, useNavigate} from "react-router-dom";
|
||||
import Loader from "../components/Loader";
|
||||
|
||||
import {Button, TextField} from "@mui/material";
|
||||
import {Album} from "../models/album";
|
||||
|
||||
import "./portfolio-manager.scss"
|
||||
import {useInstances} from "../services/instances/use-instances";
|
||||
|
||||
function PortfolioManager() {
|
||||
const navigate = useNavigate();
|
||||
const {selectedInstance} = useInstances();
|
||||
|
||||
const [apiError, setApiError] = useState(null);
|
||||
const {fetchAlbums, patchAlbum, deleteAlbum, moveAlbumDown, moveAlbumUp} = useAlbumsApi();
|
||||
|
||||
const [albums, setAlbums] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [albumsToUpdate, setAlbumsToUpdate] = useState(null);
|
||||
const fetchedRef = useRef(false)
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setApiError(null);
|
||||
const albums = await fetchAlbums(selectedInstance);
|
||||
setAlbums(albums);
|
||||
setAlbumsToUpdate(albums.map(a => new Album(a)));
|
||||
} catch (err) {
|
||||
console.error("Could not fetch albums", err);
|
||||
setApiError("An error occurred trying to fetch your albums, please contact the system administrator");
|
||||
setAlbums([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [fetchAlbums, selectedInstance])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedInstance && !fetchedRef.current) {
|
||||
fetchData();
|
||||
fetchedRef.current = true;
|
||||
}
|
||||
}, [selectedInstance, fetchData]);
|
||||
|
||||
const handleAlbumNameChange = (albumIndex, e) => {
|
||||
albumsToUpdate[albumIndex].name = e.target.value;
|
||||
setAlbumsToUpdate([...albumsToUpdate]);
|
||||
}
|
||||
|
||||
const handleAlbumDescriptionChange = (albumIndex, e) => {
|
||||
albumsToUpdate[albumIndex].description = e.target.value;
|
||||
setAlbumsToUpdate([...albumsToUpdate]);
|
||||
}
|
||||
|
||||
const albumHasChanges = (idx) => {
|
||||
return albumsToUpdate[idx].name !== albums[idx].name
|
||||
|| albumsToUpdate[idx].description !== albums[idx].description;
|
||||
}
|
||||
|
||||
const handleSaveAlbum = async (idx) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
if (albumHasChanges(idx)) {
|
||||
albumsToUpdate[idx] = await patchAlbum(selectedInstance, albumsToUpdate[idx]);
|
||||
setAlbums([
|
||||
...albumsToUpdate.map(a => new Album(a))
|
||||
]);
|
||||
}
|
||||
} catch (err) {
|
||||
if (typeof err === "string" && err.toLowerCase().indexOf("duplicate") >= 0) {
|
||||
setApiError("Name already in use");
|
||||
} else {
|
||||
console.error(err);
|
||||
navigate(`/${selectedInstance.instanceId}/albums`, {state: {error: err}});
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteAlbum = async (e, albumId) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const updatedAlbums = await deleteAlbum(selectedInstance, albumId);
|
||||
setAlbums(updatedAlbums);
|
||||
setAlbumsToUpdate(albumsToUpdate.filter(a => a.albumId !== albumId));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const handleMoveAlbumDown = async (idx) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const updatedAlbums = await moveAlbumDown(selectedInstance, albumsToUpdate[idx].albumId);
|
||||
const origAlbum = albumsToUpdate[idx];
|
||||
albumsToUpdate[idx] = albumsToUpdate[idx + 1];
|
||||
albumsToUpdate[idx + 1] = origAlbum;
|
||||
setAlbums(updatedAlbums);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const handleMoveAlbumUp = async (idx) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const updatedAlbums = await moveAlbumUp(selectedInstance, albumsToUpdate[idx].albumId);
|
||||
const origAlbum = albumsToUpdate[idx];
|
||||
albumsToUpdate[idx] = albumsToUpdate[idx - 1];
|
||||
albumsToUpdate[idx - 1] = origAlbum;
|
||||
setAlbums(updatedAlbums);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return <div id="portfolio-manager">
|
||||
<div id="page-title">
|
||||
<div id="left-part">
|
||||
<div className="title">Albums</div>
|
||||
{albums && <div className="photos-count">({albums.length} albums)</div>}
|
||||
</div>
|
||||
<div id="right-part">
|
||||
<Button id="page-save" variant="contained" color="success"
|
||||
onClick={() => navigate("new", {state: {album: new Album()}})}>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{apiError && <div className="error-box">{apiError}</div>}
|
||||
{!apiError && albums?.length > 0 && <div id="album-list">
|
||||
{albumsToUpdate.map((album, idx) =>
|
||||
<div className="card-layout album-list-item" key={album.albumId}>
|
||||
<div className={`cover-photo ${(album.coverPhoto ? "" : "cover-photo--no-img")}`}
|
||||
onClick={() => navigate(album.albumId, {state: {album: albums[idx]}})}>
|
||||
{album.coverPhoto
|
||||
? <img src={`${album.coverPhoto.url}?width=450&height=450&fit=cover`}
|
||||
alt={album.coverPhoto.name}/>
|
||||
: <AddAPhoto className="no-photos"/>
|
||||
}
|
||||
</div>
|
||||
<div className="card-details">
|
||||
<div className="title input-element">
|
||||
<TextField fullWidth={true} variant="standard"
|
||||
value={album.name} onChange={(e) => handleAlbumNameChange(idx, e)}
|
||||
/>
|
||||
<Button endIcon={<KeyboardArrowRight/>} variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => navigate(album.albumId, {state: {album: albums[idx]}})}>
|
||||
Photos
|
||||
</Button>
|
||||
</div>
|
||||
<div className="description input-element">
|
||||
<TextField multiline={true} fullWidth={true} rows={8}
|
||||
onChange={(e) => handleAlbumDescriptionChange(idx, e)}
|
||||
value={album.description || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="input-buttons">
|
||||
<div className="left-buttons">
|
||||
<Button className="photo-save" variant="contained" color="error"
|
||||
onClick={(e) => handleDeleteAlbum(e, album.albumId)}>
|
||||
Delete
|
||||
</Button>
|
||||
<Button className="photo-save" variant="contained" color="info" disabled={!albumHasChanges(idx)}
|
||||
onClick={() => handleSaveAlbum(idx)}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<div className="right-buttons">
|
||||
<Button
|
||||
style={{visibility: (idx > 0 ? "visible" : "hidden")}}
|
||||
startIcon={<MoveUp/>} variant="contained" color="action"
|
||||
onClick={() => handleMoveAlbumUp(idx)}>
|
||||
Move up
|
||||
</Button>
|
||||
<Button
|
||||
style={{visibility: (idx < albumsToUpdate.length - 1 ? "visible" : "hidden")}}
|
||||
startIcon={<MoveDown/>} variant="contained" color="action"
|
||||
onClick={(e) => handleMoveAlbumDown(idx)}>
|
||||
Move down
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>}
|
||||
{!apiError && albums?.length === 0 && <div><Link to="new" state={{album: new Album()}}>Click here</Link> to create your first album!</div>}
|
||||
<Loader loading={isLoading} />
|
||||
</div>
|
||||
}
|
||||
|
||||
export default PortfolioManager;
|
||||
@@ -0,0 +1,41 @@
|
||||
@import "../variables";
|
||||
|
||||
#portfolio-manager {
|
||||
|
||||
#page-title {
|
||||
#left-part {
|
||||
align-items: first baseline;
|
||||
}
|
||||
}
|
||||
|
||||
#album-list {
|
||||
margin-top: 25px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 25px;
|
||||
|
||||
> .album-list-item {
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
|
||||
.MuiFormControl-root {
|
||||
margin-right: 50px;
|
||||
}
|
||||
|
||||
.MuiInput-input {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.MuiButtonBase-root {
|
||||
flex: 0 0 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: 968px) {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user