| @@ -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}> | ||||
| @@ -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( | |||||
| <Route path="/" element={<AdminLayout/>} handle={{ | |||||
| crumb: () => <Link to="/"><Home /></Link> | |||||
| }}> | |||||
| <Route index element={<InstancesPage/>}/> | |||||
| <Route path="/no-access" element={<NoAccessPage/>}/> | |||||
| <Route path="/:instanceId" handle={{ | |||||
| crumb: (routeData) => <span>{routeData?.params?.instanceId}</span> | |||||
| }}> | |||||
| <Route index path="" element={<Navigate to="albums"/>} /> | |||||
| <Route path="albums/*" element={<Outlet />} handle={{ | |||||
| crumb: (routeData) => <Link to={`/${routeData?.params?.instanceId}/albums`}>albums</Link> | |||||
| }}> | |||||
| <Route index element={<PortfolioManager/>} /> | |||||
| <Route path=":albumId" element={<AlbumManager/>} handle={{ | |||||
| crumb: (routeData) => <Link to={`/${routeData?.params?.instanceId}/albums/${routeData.params.albumId}`}>{routeData.params.albumId}</Link> | |||||
| }}/> | |||||
| </Route> | |||||
| <Route path="signin-keycloak" element={<Navigate to="/"/>}/> | |||||
| </Route> | |||||
| <Route path="*" element={<Navigate to={"/no-access"}/>}/> | |||||
| </Route> | |||||
| ) | |||||
| ); | |||||
| const root = ReactDOM.createRoot(document.getElementById('root')); | |||||
| root.render( | root.render( | ||||
| <React.StrictMode> | <React.StrictMode> | ||||
| <BrowserRouter> | |||||
| <ScrollToTop> | |||||
| <Keycloak> | |||||
| <InstancesProvider> | |||||
| <AdminLayout> | |||||
| <Routes> | |||||
| <Route index path="/" element={<InstanceSelectorPage/>}/> | |||||
| <Route path="/no-access" element={<NoAccessPage/>}/> | |||||
| <Route path="/:instanceId"> | |||||
| {/*<Route index element={<ManageInstancePage/>}/>*/} | |||||
| <Route index path="" element={<Navigate to="albums" />} /> | |||||
| <Route path="albums" element={<PortfolioManager/>} /> | |||||
| <Route path="albums/:albumId" element={<AlbumManager/>}/> | |||||
| <Route path="signin-keycloak" element={<Navigate to="/"/>}/> | |||||
| <Route path="signin-keycloak" element={<Navigate to="/"/>}/> | |||||
| </Route> | |||||
| <Route path="*" element={<Navigate to={"/no-access"}/>}/> | |||||
| </Routes> | |||||
| </AdminLayout> | |||||
| </InstancesProvider> | |||||
| </Keycloak> | |||||
| </ScrollToTop> | |||||
| </BrowserRouter> | |||||
| <Keycloak> | |||||
| <InstancesProvider> | |||||
| <RouterProvider router={router} /> | |||||
| </InstancesProvider> | |||||
| </Keycloak> | |||||
| </React.StrictMode> | </React.StrictMode> | ||||
| ); | |||||
| function ScrollToTop({children}) { | |||||
| const location = useLocation(); | |||||
| useLayoutEffect(() => { | |||||
| document.documentElement.scrollTo(0, 0); | |||||
| }, [location.pathname]); | |||||
| return children | |||||
| } | |||||
| ); | |||||
| @@ -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 {useEffect} from "react"; | |||||
| import {Outlet, useLocation, useNavigate, useNavigation} from "react-router-dom"; | |||||
| 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]); | |||||
| return <ThemeProvider theme={theme}> | |||||
| <Header/> | |||||
| <div id="admin-page"> | |||||
| <main id="page"> | |||||
| {userData ? <> | |||||
| {(isLoading || instancesIsLoading) && <Loader/>} | |||||
| {!isLoading && !instancesIsLoading && children} | |||||
| </> | |||||
| : <Loader/> | |||||
| 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("/"); | |||||
| } | } | ||||
| </main> | |||||
| </div> | |||||
| } else { | |||||
| setSelectedInstance(null); | |||||
| } | |||||
| } | |||||
| }, [userData, isLoading, userManager, navigate, signIn, setSelectedInstance, selectedInstance, pathname, instances]); | |||||
| return <ThemeProvider theme={theme}> | |||||
| <ScrollToTop> | |||||
| <Header/> | |||||
| <div id="admin-page"> | |||||
| <main id="page"> | |||||
| <Breadcrumb /> | |||||
| {userData ? <> | |||||
| {(isLoading || instancesIsLoading || navigation.state === 'loading') && <Loader/>} | |||||
| {!isLoading && !instancesIsLoading && <Outlet/>} | |||||
| </> | |||||
| : <Loader/> | |||||
| } | |||||
| </main> | |||||
| </div> | |||||
| </ScrollToTop> | |||||
| </ThemeProvider> | </ThemeProvider> | ||||
| } | |||||
| function ScrollToTop({children}) { | |||||
| const location = useLocation(); | |||||
| useLayoutEffect(() => { | |||||
| document.documentElement.scrollTo(0, 0); | |||||
| }, [location.pathname]); | |||||
| return children | |||||
| } | } | ||||
| @@ -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(() => { | |||||
| // setInstancesToUpdate(instances); | |||||
| // }, [instances]); | |||||
| useEffect(() => { | |||||
| setInstancesToUpdate(instances?.map(inst => new Instance(inst)) || []); | |||||
| }, [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 || "") || | |||||
| (instancesToUpdate[idx].subtitle || "") !== (instances[idx].subtitle || "") || | |||||
| JSON.stringify(instancesToUpdate[idx].urls) !== JSON.stringify(instances[idx].urls); | |||||
| 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 () => { | 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"> | |||||
| <IconButton color="error" onClick={(e) => handleDeleteUrl(e, idx, url)}> | |||||
| <RemoveCircle /> | |||||
| </IconButton> | |||||
| <NavLink to={url}>{url}</NavLink> | |||||
| </div>)} | |||||
| {instance.urls.map(url => <div className="instance-link" key={url}> | |||||
| <IconButton color="error" onClick={(e) => handleDeleteUrl(e, idx, url)}> | |||||
| <RemoveCircle/> | |||||
| </IconButton> | |||||
| <NavLink to={url}>{url}</NavLink> | |||||
| </div>)} | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div className="input-buttons"> | <div className="input-buttons"> | ||||
| @@ -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 | ||||