Fixed admin client
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
import {useMatches} from "react-router-dom";
|
||||||
|
import "./breadcrumb.scss";
|
||||||
|
|
||||||
|
export function Breadcrumb() {
|
||||||
|
let matches = useMatches();
|
||||||
|
let crumbs = matches
|
||||||
|
// first get rid of any matches that don't have handle and crumb
|
||||||
|
.filter((match) => Boolean(match.handle?.crumb))
|
||||||
|
// now map them into an array of elements, passing the loader
|
||||||
|
// data to each one
|
||||||
|
.map((match) => match.handle.crumb(match));
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div id="breadcrumb">
|
||||||
|
{crumbs.map((crumb, index) => <span key={index}>
|
||||||
|
<span className="crumb">{crumb}</span>
|
||||||
|
{index > 0 && index < crumbs.length - 1
|
||||||
|
? <span className="separator">/</span>
|
||||||
|
: <></>}
|
||||||
|
</span>)}
|
||||||
|
</div>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
@import "../variables";
|
||||||
|
|
||||||
|
#breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $accentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.crumb {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,16 +2,15 @@ import {useOutsideAlerter} from "../hooks/outside-click-alerter";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {useNavigate} from "react-router-dom";
|
import {useNavigate} from "react-router-dom";
|
||||||
import {useRef, useState} from "react";
|
import {useRef, useState} from "react";
|
||||||
import {Edit, Person} from '@mui/icons-material';
|
import {Person} from '@mui/icons-material';
|
||||||
import {Logo} from "./logo";
|
import {Logo} from "./logo";
|
||||||
import {Button, IconButton} from "@mui/material";
|
import {Button, IconButton} from "@mui/material";
|
||||||
|
|
||||||
import "./header.scss";
|
import "./header.scss";
|
||||||
import {useAuth} from "../services/authentication/use-auth";
|
import {useAuth} from "../services/authentication/use-auth";
|
||||||
import {useInstances} from "../services/instances/use-instances";
|
import {Breadcrumb} from "./breadcrumb";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const {selectedInstance} = useInstances();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {userData, userManager} = useAuth();
|
const {userData, userManager} = useAuth();
|
||||||
const [showUserDropout, setShowUserDropout] = useState(false);
|
const [showUserDropout, setShowUserDropout] = useState(false);
|
||||||
@@ -32,9 +31,6 @@ export default function Header() {
|
|||||||
|
|
||||||
return <header id="admin-header" ref={wrapperRef}>
|
return <header id="admin-header" ref={wrapperRef}>
|
||||||
<div id="logo" onClick={() => navigate("/")}><Logo/></div>
|
<div id="logo" onClick={() => navigate("/")}><Logo/></div>
|
||||||
{selectedInstance && <Button variant="contained" onClick={() => navigate("/")} color="warning">
|
|
||||||
<Edit /> {selectedInstance.title}
|
|
||||||
</Button>}
|
|
||||||
{userData &&
|
{userData &&
|
||||||
<div id="user-icon">
|
<div id="user-icon">
|
||||||
<IconButton aria-label="person" onClick={handleUserIconClicked}>
|
<IconButton aria-label="person" onClick={handleUserIconClicked}>
|
||||||
|
|||||||
+44
-38
@@ -1,7 +1,13 @@
|
|||||||
import React, {useLayoutEffect} from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import {BrowserRouter, Navigate, Route, Routes, useLocation} from "react-router-dom";
|
import {
|
||||||
|
createBrowserRouter,
|
||||||
|
createRoutesFromElements, Link,
|
||||||
|
Navigate, Outlet,
|
||||||
|
Route,
|
||||||
|
RouterProvider,
|
||||||
|
} from "react-router-dom";
|
||||||
|
|
||||||
import '@fontsource/roboto/300.css';
|
import '@fontsource/roboto/300.css';
|
||||||
import '@fontsource/roboto/400.css';
|
import '@fontsource/roboto/400.css';
|
||||||
@@ -11,45 +17,45 @@ import '@fontsource/roboto/700.css';
|
|||||||
import Keycloak from "./services/authentication/keycloak";
|
import Keycloak from "./services/authentication/keycloak";
|
||||||
import PortfolioManager from "./pages/portfolio-manager";
|
import PortfolioManager from "./pages/portfolio-manager";
|
||||||
import AlbumManager from "./pages/album-manager";
|
import AlbumManager from "./pages/album-manager";
|
||||||
import {InstanceSelectorPage} from "./pages/instance-selector-page";
|
import {InstancesPage} from "./pages/instances-page";
|
||||||
import {NoAccessPage} from "./pages/no-access";
|
import {NoAccessPage} from "./pages/no-access";
|
||||||
import {InstancesProvider} from "./services/instances/instances-context";
|
import {InstancesProvider} from "./services/instances/instances-context";
|
||||||
import {AdminLayout} from "./layout";
|
import {AdminLayout} from "./layout";
|
||||||
|
import {Home} from "@mui/icons-material";
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
const router = createBrowserRouter(
|
||||||
|
createRoutesFromElements(
|
||||||
root.render(
|
<Route path="/" element={<AdminLayout/>} handle={{
|
||||||
<React.StrictMode>
|
crumb: () => <Link to="/"><Home /></Link>
|
||||||
<BrowserRouter>
|
}}>
|
||||||
<ScrollToTop>
|
<Route index element={<InstancesPage/>}/>
|
||||||
<Keycloak>
|
<Route path="/no-access" element={<NoAccessPage/>}/>
|
||||||
<InstancesProvider>
|
<Route path="/:instanceId" handle={{
|
||||||
<AdminLayout>
|
crumb: (routeData) => <span>{routeData?.params?.instanceId}</span>
|
||||||
<Routes>
|
}}>
|
||||||
<Route index path="/" element={<InstanceSelectorPage/>}/>
|
<Route index path="" element={<Navigate to="albums"/>} />
|
||||||
<Route path="/no-access" element={<NoAccessPage/>}/>
|
<Route path="albums/*" element={<Outlet />} handle={{
|
||||||
<Route path="/:instanceId">
|
crumb: (routeData) => <Link to={`/${routeData?.params?.instanceId}/albums`}>albums</Link>
|
||||||
{/*<Route index element={<ManageInstancePage/>}/>*/}
|
}}>
|
||||||
<Route index path="" element={<Navigate to="albums" />} />
|
<Route index element={<PortfolioManager/>} />
|
||||||
<Route path="albums" element={<PortfolioManager/>} />
|
<Route path=":albumId" element={<AlbumManager/>} handle={{
|
||||||
<Route path="albums/:albumId" element={<AlbumManager/>}/>
|
crumb: (routeData) => <Link to={`/${routeData?.params?.instanceId}/albums/${routeData.params.albumId}`}>{routeData.params.albumId}</Link>
|
||||||
<Route path="signin-keycloak" element={<Navigate to="/"/>}/>
|
}}/>
|
||||||
<Route path="signin-keycloak" element={<Navigate to="/"/>}/>
|
</Route>
|
||||||
</Route>
|
<Route path="signin-keycloak" element={<Navigate to="/"/>}/>
|
||||||
<Route path="*" element={<Navigate to={"/no-access"}/>}/>
|
</Route>
|
||||||
</Routes>
|
<Route path="*" element={<Navigate to={"/no-access"}/>}/>
|
||||||
</AdminLayout>
|
</Route>
|
||||||
</InstancesProvider>
|
)
|
||||||
</Keycloak>
|
|
||||||
</ScrollToTop>
|
|
||||||
</BrowserRouter>
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
function ScrollToTop({children}) {
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
const location = useLocation();
|
root.render(
|
||||||
useLayoutEffect(() => {
|
<React.StrictMode>
|
||||||
document.documentElement.scrollTo(0, 0);
|
<Keycloak>
|
||||||
}, [location.pathname]);
|
<InstancesProvider>
|
||||||
return children
|
<RouterProvider router={router} />
|
||||||
}
|
</InstancesProvider>
|
||||||
|
</Keycloak>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
+47
-16
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import Header from "./components/header";
|
import Header from "./components/header";
|
||||||
import {useNavigate} from "react-router-dom";
|
import {Outlet, useLocation, useNavigate, useNavigation} from "react-router-dom";
|
||||||
import {useEffect} from "react";
|
import {useEffect, useLayoutEffect} from "react";
|
||||||
|
|
||||||
import "./admin.scss";
|
import "./admin.scss";
|
||||||
import Loader from "./components/Loader";
|
import Loader from "./components/Loader";
|
||||||
@@ -10,6 +10,7 @@ import {createTheme} from '@mui/material/styles';
|
|||||||
import {ThemeProvider} from "@mui/material";
|
import {ThemeProvider} from "@mui/material";
|
||||||
import {useAuth} from "./services/authentication/use-auth";
|
import {useAuth} from "./services/authentication/use-auth";
|
||||||
import {useInstances} from "./services/instances/use-instances";
|
import {useInstances} from "./services/instances/use-instances";
|
||||||
|
import {Breadcrumb} from "./components/breadcrumb";
|
||||||
|
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
@@ -20,10 +21,12 @@ const theme = createTheme({
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export function AdminLayout({children}) {
|
export function AdminLayout() {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
const {pathname} = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {isLoading, userData, userManager, signIn} = useAuth();
|
const {isLoading, userData, userManager, signIn} = useAuth();
|
||||||
const {isLoading: instancesIsLoading} = useInstances();
|
const {isLoading: instancesIsLoading, setSelectedInstance, selectedInstance, instances} = useInstances();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function handleLogin() {
|
async function handleLogin() {
|
||||||
@@ -50,19 +53,47 @@ export function AdminLayout({children}) {
|
|||||||
silentLogin();
|
silentLogin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [userData, isLoading, userManager, navigate, signIn]);
|
|
||||||
|
if (instances && !selectedInstance && setSelectedInstance) {
|
||||||
|
const possibleSelectedInstance = pathname.split('/')[1];
|
||||||
|
if (possibleSelectedInstance) {
|
||||||
|
const selectedInstance = instances?.find(inst => inst.instanceId === possibleSelectedInstance);
|
||||||
|
if (selectedInstance) {
|
||||||
|
console.log("We found an instance", selectedInstance);
|
||||||
|
setSelectedInstance(selectedInstance);
|
||||||
|
} else {
|
||||||
|
console.log("We could not detect an instance", possibleSelectedInstance);
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSelectedInstance(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [userData, isLoading, userManager, navigate, signIn, setSelectedInstance, selectedInstance, pathname, instances]);
|
||||||
|
|
||||||
return <ThemeProvider theme={theme}>
|
return <ThemeProvider theme={theme}>
|
||||||
<Header/>
|
<ScrollToTop>
|
||||||
<div id="admin-page">
|
<Header/>
|
||||||
<main id="page">
|
<div id="admin-page">
|
||||||
{userData ? <>
|
<main id="page">
|
||||||
{(isLoading || instancesIsLoading) && <Loader/>}
|
<Breadcrumb />
|
||||||
{!isLoading && !instancesIsLoading && children}
|
{userData ? <>
|
||||||
</>
|
{(isLoading || instancesIsLoading || navigation.state === 'loading') && <Loader/>}
|
||||||
: <Loader/>
|
{!isLoading && !instancesIsLoading && <Outlet/>}
|
||||||
}
|
</>
|
||||||
</main>
|
: <Loader/>
|
||||||
</div>
|
}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</ScrollToTop>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ScrollToTop({children}) {
|
||||||
|
const location = useLocation();
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
document.documentElement.scrollTo(0, 0);
|
||||||
|
}, [location.pathname]);
|
||||||
|
return children
|
||||||
}
|
}
|
||||||
+27
-18
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {useRef, useState} from "react";
|
import {useEffect, useRef, useState} from "react";
|
||||||
import {NavLink, useNavigate} from "react-router-dom";
|
import {NavLink, useNavigate} from "react-router-dom";
|
||||||
import {Button, IconButton, InputAdornment, TextField} from "@mui/material";
|
import {Button, IconButton, InputAdornment, TextField} from "@mui/material";
|
||||||
import {useAuth} from "../services/authentication/use-auth";
|
import {useAuth} from "../services/authentication/use-auth";
|
||||||
@@ -7,13 +7,13 @@ import {useInstances} from "../services/instances/use-instances";
|
|||||||
import {Instance} from "../models/instance";
|
import {Instance} from "../models/instance";
|
||||||
import {
|
import {
|
||||||
AddAPhoto, AddCircle,
|
AddAPhoto, AddCircle,
|
||||||
ArrowRightAltOutlined, RemoveCircle
|
ArrowRightAltOutlined, Edit, RemoveCircle
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
|
|
||||||
import "./instance-selector-page.scss";
|
import "./instances-page.scss";
|
||||||
import DragAndDrop from "../components/drag-and-drop/drag-and-drop";
|
import DragAndDrop from "../components/drag-and-drop/drag-and-drop";
|
||||||
|
|
||||||
export function InstanceSelectorPage() {
|
export function InstancesPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {
|
const {
|
||||||
instances,
|
instances,
|
||||||
@@ -23,14 +23,14 @@ export function InstanceSelectorPage() {
|
|||||||
uploadCoverPhoto,
|
uploadCoverPhoto,
|
||||||
instanceApiError
|
instanceApiError
|
||||||
} = useInstances();
|
} = useInstances();
|
||||||
const [instancesToUpdate, setInstancesToUpdate] = useState(instances?.map(inst => new Instance(inst)) || []);
|
const [instancesToUpdate, setInstancesToUpdate] = useState(null);
|
||||||
const {userData} = useAuth();
|
const {userData} = useAuth();
|
||||||
|
|
||||||
const [newInstanceTitle, setNewInstanceTitle] = useState("");
|
const [newInstanceTitle, setNewInstanceTitle] = useState("");
|
||||||
|
|
||||||
// useEffect(() => {
|
useEffect(() => {
|
||||||
// setInstancesToUpdate(instances);
|
setInstancesToUpdate(instances?.map(inst => new Instance(inst)) || []);
|
||||||
// }, [instances]);
|
}, [instances]);
|
||||||
|
|
||||||
let onNewInstanceTitleChanged = (e) => {
|
let onNewInstanceTitleChanged = (e) => {
|
||||||
setNewInstanceTitle(e.target.value);
|
setNewInstanceTitle(e.target.value);
|
||||||
@@ -59,14 +59,15 @@ export function InstanceSelectorPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const instanceHasChanges = (idx) => {
|
const instanceHasChanges = (idx) => {
|
||||||
return (instancesToUpdate[idx].title || "") !== (instances[idx].title || "") ||
|
return (instancesToUpdate[idx].title || "") !== (instances[idx]?.title || "") ||
|
||||||
(instancesToUpdate[idx].subtitle || "") !== (instances[idx].subtitle || "") ||
|
(instancesToUpdate[idx].subtitle || "") !== (instances[idx]?.subtitle || "") ||
|
||||||
JSON.stringify(instancesToUpdate[idx].urls) !== JSON.stringify(instances[idx].urls);
|
JSON.stringify(instancesToUpdate[idx].urls) !== JSON.stringify(instances[idx]?.urls);
|
||||||
}
|
}
|
||||||
const handleCreateInstance = async () => {
|
const handleCreateInstance = async () => {
|
||||||
await createInstance(new Instance({
|
await createInstance(new Instance({
|
||||||
title: newInstanceTitle
|
title: newInstanceTitle
|
||||||
}));
|
}));
|
||||||
|
setNewInstanceTitle("");
|
||||||
}
|
}
|
||||||
const handleSaveInstance = async (idx) => {
|
const handleSaveInstance = async (idx) => {
|
||||||
if (instanceHasChanges(idx)) {
|
if (instanceHasChanges(idx)) {
|
||||||
@@ -93,6 +94,11 @@ export function InstanceSelectorPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <div id="instance-selector-page">
|
return <div id="instance-selector-page">
|
||||||
|
<div id="page-title">
|
||||||
|
<div id="left-part">
|
||||||
|
<div className="title">Sites</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{instanceApiError && <div className="error-box">{instanceApiError}</div>}
|
{instanceApiError && <div className="error-box">{instanceApiError}</div>}
|
||||||
<div>
|
<div>
|
||||||
{userData?.profile?.resource_access?.photos?.roles?.indexOf("admin") >= 0 && <>
|
{userData?.profile?.resource_access?.photos?.roles?.indexOf("admin") >= 0 && <>
|
||||||
@@ -122,8 +128,11 @@ export function InstanceSelectorPage() {
|
|||||||
onClick={(e) => fileInputRefs.current[idx].click(e)}
|
onClick={(e) => fileInputRefs.current[idx].click(e)}
|
||||||
isLoading={isUploading[idx]}>
|
isLoading={isUploading[idx]}>
|
||||||
{!isUploading[idx] && instance.coverPhoto
|
{!isUploading[idx] && instance.coverPhoto
|
||||||
? <img src={`${instance.coverPhoto.url}?width=450&height=450&fit=cover`}
|
? <>
|
||||||
|
<img src={`${instance.coverPhoto.url}?width=450&height=450&fit=cover`}
|
||||||
alt={instance.coverPhoto.name}/>
|
alt={instance.coverPhoto.name}/>
|
||||||
|
<div className="btn-edit-photo"><Edit /></div>
|
||||||
|
</>
|
||||||
: <AddAPhoto className="no-photos"/>
|
: <AddAPhoto className="no-photos"/>
|
||||||
}
|
}
|
||||||
</DragAndDrop>
|
</DragAndDrop>
|
||||||
@@ -162,12 +171,12 @@ export function InstanceSelectorPage() {
|
|||||||
)
|
)
|
||||||
}}/>
|
}}/>
|
||||||
<div className="instance-links">
|
<div className="instance-links">
|
||||||
{instance.urls.map(url => <div className="instance-link">
|
{instance.urls.map(url => <div className="instance-link" key={url}>
|
||||||
<IconButton color="error" onClick={(e) => handleDeleteUrl(e, idx, url)}>
|
<IconButton color="error" onClick={(e) => handleDeleteUrl(e, idx, url)}>
|
||||||
<RemoveCircle />
|
<RemoveCircle/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<NavLink to={url}>{url}</NavLink>
|
<NavLink to={url}>{url}</NavLink>
|
||||||
</div>)}
|
</div>)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="input-buttons">
|
<div className="input-buttons">
|
||||||
+28
@@ -9,6 +9,32 @@
|
|||||||
|
|
||||||
> .instance {
|
> .instance {
|
||||||
|
|
||||||
|
.cover-photo {
|
||||||
|
img {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.btn-edit-photo {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
.btn-edit-photo {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
svg {
|
||||||
|
color: white;
|
||||||
|
font-size: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
@@ -28,8 +54,10 @@
|
|||||||
.instance-links {
|
.instance-links {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.instance-link {
|
div.instance-link {
|
||||||
padding: 2px 0;
|
padding: 2px 0;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $accentColor;
|
color: $accentColor;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
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>
|
|
||||||
</>;
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
MoveDown,
|
MoveDown,
|
||||||
MoveUp
|
MoveUp
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import {Link, useNavigate} from "react-router-dom";
|
import {Link, Outlet, useNavigate} from "react-router-dom";
|
||||||
import Loader from "../components/Loader";
|
import Loader from "../components/Loader";
|
||||||
|
|
||||||
import {Button, TextField} from "@mui/material";
|
import {Button, TextField} from "@mui/material";
|
||||||
@@ -204,6 +204,7 @@ function PortfolioManager() {
|
|||||||
</div>}
|
</div>}
|
||||||
{!apiError && albums?.length === 0 && <div><Link to="new" state={{album: new Album()}}>Click here</Link> to create your first album!</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} />
|
<Loader loading={isLoading} />
|
||||||
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import useInstancesApi from "./instances-api";
|
import useInstancesApi from "./instances-api";
|
||||||
import {useAuth} from "../authentication/use-auth";
|
import {useAuth} from "../authentication/use-auth";
|
||||||
import {useLocation, useNavigate} from "react-router-dom";
|
|
||||||
import {Instance} from "../../models/instance";
|
import {Instance} from "../../models/instance";
|
||||||
|
|
||||||
export const InstancesContext = React.createContext(
|
export const InstancesContext = React.createContext(
|
||||||
@@ -9,13 +8,12 @@ export const InstancesContext = React.createContext(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const InstancesProvider = ({children}) => {
|
export const InstancesProvider = ({children}) => {
|
||||||
const navigate = useNavigate();
|
// const {pathname} = useLocation();
|
||||||
const {pathname} = useLocation();
|
|
||||||
const {userData} = useAuth();
|
const {userData} = useAuth();
|
||||||
const {fetchInstances, createInstance, deleteInstance, updateInstance, uploadCoverPhoto, instanceApiError} = useInstancesApi();
|
const {fetchInstances, createInstance, deleteInstance, updateInstance, uploadCoverPhoto, instanceApiError} = useInstancesApi();
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [instances, setInstances] = useState(undefined);
|
const [instances, setInstances] = useState(/** @type {Instance[]} **/undefined);
|
||||||
const [selectedInstance, setSelectedInstance] = useState(null);
|
const [selectedInstance, setSelectedInstance] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -26,22 +24,10 @@ export const InstancesProvider = ({children}) => {
|
|||||||
const instances = await fetchInstances();
|
const instances = await fetchInstances();
|
||||||
setInstances(instances);
|
setInstances(instances);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
} else {
|
|
||||||
const possibleSelectedInstance = pathname.split('/')[1];
|
|
||||||
if(possibleSelectedInstance){
|
|
||||||
const selectedInstance = instances?.find(inst => inst.instanceId === possibleSelectedInstance);
|
|
||||||
if (selectedInstance) {
|
|
||||||
setSelectedInstance(selectedInstance);
|
|
||||||
} else {
|
|
||||||
navigate("/");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setSelectedInstance(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [navigate, pathname, userData, instances, fetchInstances]);
|
}, [userData, instances, fetchInstances]);
|
||||||
|
|
||||||
const handleCreateInstance = async (instance) => {
|
const handleCreateInstance = async (instance) => {
|
||||||
const newInstance = await createInstance(instance);
|
const newInstance = await createInstance(instance);
|
||||||
@@ -54,6 +40,12 @@ export const InstancesProvider = ({children}) => {
|
|||||||
setInstances([...instances]);
|
setInstances([...instances]);
|
||||||
return updatedInstance;
|
return updatedInstance;
|
||||||
}
|
}
|
||||||
|
const handleDeleteInstance = async (instanceId) => {
|
||||||
|
await deleteInstance(instanceId);
|
||||||
|
const idxToRemove = instances.findIndex(i => i.instanceId === instanceId);
|
||||||
|
instances.splice(idxToRemove, 1)
|
||||||
|
setInstances([...instances]);
|
||||||
|
}
|
||||||
const handleUpdateCoverPhoto = async (instanceId, formData) => {
|
const handleUpdateCoverPhoto = async (instanceId, formData) => {
|
||||||
const updatedInstance = await uploadCoverPhoto(instanceId, formData);
|
const updatedInstance = await uploadCoverPhoto(instanceId, formData);
|
||||||
const idx = instances.findIndex(i => i.instanceId === updatedInstance.instanceId);
|
const idx = instances.findIndex(i => i.instanceId === updatedInstance.instanceId);
|
||||||
@@ -68,7 +60,7 @@ export const InstancesProvider = ({children}) => {
|
|||||||
selectedInstance,
|
selectedInstance,
|
||||||
setSelectedInstance,
|
setSelectedInstance,
|
||||||
createInstance: handleCreateInstance,
|
createInstance: handleCreateInstance,
|
||||||
deleteInstance,
|
deleteInstance: handleDeleteInstance,
|
||||||
updateInstance: handleUpdateInstance,
|
updateInstance: handleUpdateInstance,
|
||||||
uploadCoverPhoto: handleUpdateCoverPhoto,
|
uploadCoverPhoto: handleUpdateCoverPhoto,
|
||||||
instanceApiError,
|
instanceApiError,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {ApiRouter, AuthorizeOptions, HttpDelete, HttpGet, HttpPatch, HttpPost, H
|
|||||||
import {BadRequestError, ValidationError} from "../../models/errors/index.js";
|
import {BadRequestError, ValidationError} from "../../models/errors/index.js";
|
||||||
import {albums} from "../../services/albums.js";
|
import {albums} from "../../services/albums.js";
|
||||||
import multer from "@koa/multer";
|
import multer from "@koa/multer";
|
||||||
import {instances} from "../../services/instances.js";
|
|
||||||
|
|
||||||
const upload = multer();
|
const upload = multer();
|
||||||
class AlbumsApi extends ApiRouter {
|
class AlbumsApi extends ApiRouter {
|
||||||
@@ -40,7 +39,8 @@ class AlbumsApi extends ApiRouter {
|
|||||||
/**
|
/**
|
||||||
* @param {KoaRequestContext} ctx
|
* @param {KoaRequestContext} ctx
|
||||||
*/
|
*/
|
||||||
async create({parameters: {instanceId}, body}) {
|
async create(ctx) {
|
||||||
|
const {parameters: {instanceId}, body} = ctx;
|
||||||
return {
|
return {
|
||||||
data: await albums.createAlbum(instanceId, body)
|
data: await albums.createAlbum(instanceId, body)
|
||||||
};
|
};
|
||||||
@@ -49,7 +49,8 @@ class AlbumsApi extends ApiRouter {
|
|||||||
/**
|
/**
|
||||||
* @param {KoaRequestContext} ctx
|
* @param {KoaRequestContext} ctx
|
||||||
*/
|
*/
|
||||||
async update({parameters: {instanceId, albumId}, body: {name, description, sort}}) {
|
async update(ctx) {
|
||||||
|
let {parameters: {instanceId, albumId}, body: {name, description, sort}} = ctx;
|
||||||
sort = sort || "auto";
|
sort = sort || "auto";
|
||||||
if (!["manual", "auto"].includes(sort)) {
|
if (!["manual", "auto"].includes(sort)) {
|
||||||
throw new ValidationError([{
|
throw new ValidationError([{
|
||||||
@@ -65,7 +66,8 @@ class AlbumsApi extends ApiRouter {
|
|||||||
/**
|
/**
|
||||||
* @param {KoaRequestContext} ctx
|
* @param {KoaRequestContext} ctx
|
||||||
*/
|
*/
|
||||||
async patch({parameters: {instanceId, albumId}, body}) {
|
async patch(ctx) {
|
||||||
|
const {parameters: {instanceId, albumId}, body} = ctx;
|
||||||
return {
|
return {
|
||||||
data: await albums.patchAlbum(instanceId, albumId, body)
|
data: await albums.patchAlbum(instanceId, albumId, body)
|
||||||
};
|
};
|
||||||
@@ -122,9 +124,10 @@ class AlbumsApi extends ApiRouter {
|
|||||||
/**
|
/**
|
||||||
* @param {KoaRequestContext} ctx
|
* @param {KoaRequestContext} ctx
|
||||||
*/
|
*/
|
||||||
async patchPhoto({parameters: {instanceId, albumId}, body}) {
|
async patchPhoto(ctx) {
|
||||||
|
const {parameters: {instanceId, albumId, photoId}, body} = ctx;
|
||||||
return {
|
return {
|
||||||
data: await albums.patchPhoto(instanceId, albumId, parameters.photoId, body)
|
data: await albums.patchPhoto(instanceId, albumId, photoId, body)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ApiRouter, AuthorizeOptions, HttpGet, HttpPatch, HttpPost, HttpPut} from "../api-routing/index.js";
|
import {ApiRouter, AuthorizeOptions, HttpDelete, HttpGet, HttpPatch, HttpPost, HttpPut} from "../api-routing/index.js";
|
||||||
import {instances} from "../../services/instances.js";
|
import {instances} from "../../services/instances.js";
|
||||||
import {BadRequestError} from "../../models/errors/index.js";
|
import {BadRequestError} from "../../models/errors/index.js";
|
||||||
import {albums} from "../../services/albums.js";
|
import {albums} from "../../services/albums.js";
|
||||||
@@ -14,6 +14,7 @@ class InstancesApi extends ApiRouter {
|
|||||||
}));
|
}));
|
||||||
this.registerEndpoint(new HttpGet("/:instanceId", this.fetchInstance));
|
this.registerEndpoint(new HttpGet("/:instanceId", this.fetchInstance));
|
||||||
this.registerEndpoint(new HttpPut("/:instanceId", this.updateInstance));
|
this.registerEndpoint(new HttpPut("/:instanceId", this.updateInstance));
|
||||||
|
this.registerEndpoint(new HttpDelete("/:instanceId", this.deleteInstance));
|
||||||
this.registerEndpoint(new HttpPost("/:instanceId/upload-cover-photo", this.uploadPhoto, {
|
this.registerEndpoint(new HttpPost("/:instanceId/upload-cover-photo", this.uploadPhoto, {
|
||||||
middlewares: [upload.array('cover-photo')]
|
middlewares: [upload.array('cover-photo')]
|
||||||
}
|
}
|
||||||
@@ -51,6 +52,14 @@ class InstancesApi extends ApiRouter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {KoaRequestContext} ctx
|
||||||
|
*/
|
||||||
|
async deleteInstance({parameters, body}) {
|
||||||
|
await instances.deleteInstance(parameters.instanceId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {KoaRequestContext} ctx
|
* @param {KoaRequestContext} ctx
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ class AlbumsService {
|
|||||||
album,
|
album,
|
||||||
albumToMove
|
albumToMove
|
||||||
]);
|
]);
|
||||||
return await this.getAlbums();
|
return await this.getAlbums(instanceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ class InstancesService {
|
|||||||
throw new ApiError(err);
|
throw new ApiError(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async deleteInstance(instanceId) {
|
||||||
|
try {
|
||||||
|
await db.instances.deleteOne({ instanceId });
|
||||||
|
console.log("Instance deleted", instanceId)
|
||||||
|
} catch(err) {
|
||||||
|
throw new ApiError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Instance} instance
|
* @param {Instance} instance
|
||||||
|
|||||||
Reference in New Issue
Block a user