Initial commit

This commit is contained in:
2024-04-17 22:54:13 +02:00
commit a8fdb455f4
162 changed files with 49158 additions and 0 deletions
+302
View File
@@ -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;
+119
View File
@@ -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>
</>;
}
+3
View File
@@ -0,0 +1,3 @@
export function NoAccessPage() {
return <h1>No access!</h1>
}
+210
View File
@@ -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;
}
}
}
}