| @@ -79,126 +79,141 @@ | |||||
| border: 2px solid $borderColor; | border: 2px solid $borderColor; | ||||
| border-radius: $borderRadius; | border-radius: $borderRadius; | ||||
| position: relative; | position: relative; | ||||
| } | |||||
| .cover-photo { | |||||
| position: relative; | |||||
| height: 350px; | |||||
| flex: 0 0 350px; | |||||
| margin-bottom: 3px; | |||||
| cursor: pointer; | |||||
| width: 100%; | |||||
| max-width: 100%; | |||||
| border-radius: $borderRadius; | |||||
| box-shadow: #333 5px 5px 5px; | |||||
| overflow: hidden; | |||||
| .cover-photo { | |||||
| position: relative; | |||||
| height: 350px; | |||||
| flex: 0 0 350px; | |||||
| margin-bottom: 3px; | |||||
| cursor: pointer; | |||||
| img { | |||||
| width: 100%; | width: 100%; | ||||
| max-width: 100%; | |||||
| border-radius: $borderRadius; | |||||
| box-shadow: #333 5px 5px 5px; | |||||
| overflow: hidden; | |||||
| img { | |||||
| width: 100%; | |||||
| transform: scale(1); | |||||
| transform: scale(1); | |||||
| } | |||||
| &:hover { | |||||
| img, | |||||
| svg { | |||||
| transform: scale(1.05); | |||||
| } | } | ||||
| } | |||||
| &:hover { | |||||
| img, | |||||
| svg { | |||||
| transform: scale(1.05); | |||||
| } | |||||
| .file-name { | |||||
| position: absolute; | |||||
| bottom: 0; | |||||
| background: white; | |||||
| width: 100%; | |||||
| text-align: center; | |||||
| font-size: 0.8em; | |||||
| font-style: italic; | |||||
| line-height: 1.2em; | |||||
| padding: 0 20px; | |||||
| } | |||||
| &.cover-photo--no-img { | |||||
| //padding-left: calc(350px / 2 - 37.5px); | |||||
| display: flex; | |||||
| align-items: center; | |||||
| flex-direction: column; | |||||
| justify-content: center; | |||||
| #page-loader { | |||||
| padding: 9px; | |||||
| } | } | ||||
| .file-name { | |||||
| position: absolute; | |||||
| bottom: 0; | |||||
| background: white; | |||||
| width: 100%; | |||||
| text-align: center; | |||||
| font-size: 0.8em; | |||||
| font-style: italic; | |||||
| line-height: 1.2em; | |||||
| padding: 0 20px; | |||||
| svg.no-photos { | |||||
| width: 75px; | |||||
| height: 75px; | |||||
| color: $accentColor; | |||||
| } | } | ||||
| } | |||||
| } | |||||
| &.cover-photo--no-img { | |||||
| //padding-left: calc(350px / 2 - 37.5px); | |||||
| display: flex; | |||||
| align-items: center; | |||||
| flex-direction: column; | |||||
| justify-content: center; | |||||
| .card-details { | |||||
| flex: 1 1 auto; | |||||
| margin-left: 25px; | |||||
| margin-top: 25px; | |||||
| #page-loader { | |||||
| padding: 9px; | |||||
| } | |||||
| @media(max-width: 968px) { | |||||
| margin-left: 0; | |||||
| width: 100%; | |||||
| max-width: 450px; | |||||
| } | |||||
| svg.no-photos { | |||||
| width: 75px; | |||||
| height: 75px; | |||||
| color: $accentColor; | |||||
| } | |||||
| } | |||||
| &.card-details--flex { | |||||
| flex: 0 0 100%; | |||||
| display: flex; | |||||
| align-items: flex-start; | |||||
| justify-content: center; | |||||
| } | } | ||||
| .card-details { | |||||
| flex: 1 1 auto; | |||||
| margin-left: 25px; | |||||
| margin-top: 25px; | |||||
| .card-details-left { | |||||
| flex: 0 0 50%; | |||||
| } | |||||
| @media(max-width: 968px) { | |||||
| margin-left: 0; | |||||
| width: 100%; | |||||
| max-width: 450px; | |||||
| } | |||||
| .card-details-right { | |||||
| flex: 0 0 50%; | |||||
| } | |||||
| .input-element { | |||||
| margin-bottom: 25px; | |||||
| .input-error { | |||||
| visibility: hidden; | |||||
| position: absolute; | |||||
| bottom: -1.4em; | |||||
| left: 25px; | |||||
| font-size: 11pt; | |||||
| color: red; | |||||
| font-style: italic; | |||||
| } | |||||
| .input-element { | |||||
| margin-bottom: 25px; | |||||
| .input-error { | |||||
| visibility: hidden; | |||||
| position: absolute; | |||||
| bottom: -1.4em; | |||||
| left: 25px; | |||||
| font-size: 11pt; | |||||
| color: red; | |||||
| font-style: italic; | |||||
| } | } | ||||
| } | } | ||||
| } | |||||
| .input-buttons { | |||||
| display: flex; | |||||
| justify-content: space-between; | |||||
| @media(max-width: 968px) { | |||||
| flex-direction: column; | |||||
| } | |||||
| .input-buttons { | |||||
| .left-buttons { | |||||
| display: flex; | display: flex; | ||||
| justify-content: space-between; | |||||
| @media(max-width: 968px) { | |||||
| flex-direction: column; | |||||
| button { | |||||
| margin-right: 15px; | |||||
| } | } | ||||
| .left-buttons { | |||||
| display: flex; | |||||
| @media(max-width: 968px) { | |||||
| justify-content: space-between; | |||||
| margin-bottom: 15px; | |||||
| button { | button { | ||||
| margin-right: 15px; | |||||
| } | |||||
| @media(max-width: 968px) { | |||||
| justify-content: space-between; | |||||
| margin-bottom: 15px; | |||||
| button { | |||||
| margin-right: 0; | |||||
| } | |||||
| margin-right: 0; | |||||
| } | } | ||||
| } | } | ||||
| } | |||||
| .right-buttons { | |||||
| display: flex; | |||||
| .right-buttons { | |||||
| display: flex; | |||||
| button { | |||||
| margin-left: 15px; | |||||
| } | |||||
| button { | |||||
| margin-left: 15px; | |||||
| } | |||||
| @media(max-width: 968px) { | |||||
| justify-content: space-between; | |||||
| margin-bottom: 15px; | |||||
| @media(max-width: 968px) { | |||||
| justify-content: space-between; | |||||
| margin-bottom: 15px; | |||||
| button { | |||||
| margin-left: 0; | |||||
| } | |||||
| button { | |||||
| margin-left: 0; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -239,4 +254,57 @@ | |||||
| font-size: 1rem; | font-size: 1rem; | ||||
| margin-bottom: 15px; | margin-bottom: 15px; | ||||
| } | } | ||||
| } | |||||
| .pos--rel { | |||||
| position: relative; | |||||
| } | |||||
| .pointer { | |||||
| cursor: pointer; | |||||
| } | |||||
| .pad { | |||||
| padding: 15px; | |||||
| } | |||||
| .bold { | |||||
| font-weight: bold; | |||||
| } | |||||
| .border { | |||||
| border: 2px solid $borderColor; | |||||
| } | |||||
| .border--round { | |||||
| border-radius: $borderRadius; | |||||
| } | |||||
| .inline-block { | |||||
| display: inline-block; | |||||
| } | |||||
| .flex { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| justify-content: center; | |||||
| } | |||||
| .flex--vert { | |||||
| flex-direction: column; | |||||
| } | |||||
| .flex--vert-sm { | |||||
| @media(max-width: 768px) { | |||||
| flex-direction: column; | |||||
| } | |||||
| } | |||||
| .flex--align-center-sm { | |||||
| @media(max-width: 768px) { | |||||
| align-items: center; | |||||
| } | |||||
| } | |||||
| .flex--stretch { | |||||
| justify-content: space-between; | |||||
| } | |||||
| .flex--align-start { | |||||
| align-items: flex-start; | |||||
| } | } | ||||
| @@ -15,10 +15,10 @@ import '@fontsource/roboto/500.css'; | |||||
| import '@fontsource/roboto/700.css'; | import '@fontsource/roboto/700.css'; | ||||
| import Keycloak from "./services/authentication/keycloak"; | import Keycloak from "./services/authentication/keycloak"; | ||||
| import PortfolioManager from "./pages/portfolio-manager"; | |||||
| import AlbumManager from "./pages/album-manager"; | |||||
| import InstanceAlbumsPage from "./pages/instance-albums-page"; | |||||
| import AlbumPage from "./pages/album-page"; | |||||
| import {InstancesPage} from "./pages/instances-page"; | import {InstancesPage} from "./pages/instances-page"; | ||||
| import {NoAccessPage} from "./pages/no-access"; | |||||
| import {NoAccessPage} from "./pages/no-access-page"; | |||||
| 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"; | import {Home} from "@mui/icons-material"; | ||||
| @@ -37,8 +37,8 @@ const router = createBrowserRouter( | |||||
| <Route path="albums/*" element={<Outlet />} handle={{ | <Route path="albums/*" element={<Outlet />} handle={{ | ||||
| crumb: (routeData) => <Link to={`/${routeData?.params?.instanceId}/albums`}>albums</Link> | crumb: (routeData) => <Link to={`/${routeData?.params?.instanceId}/albums`}>albums</Link> | ||||
| }}> | }}> | ||||
| <Route index element={<PortfolioManager/>} /> | |||||
| <Route path=":albumId" element={<AlbumManager/>} handle={{ | |||||
| <Route index element={<InstanceAlbumsPage/>} /> | |||||
| <Route path=":albumId" element={<AlbumPage />} handle={{ | |||||
| crumb: (routeData) => <Link to={`/${routeData?.params?.instanceId}/albums/${routeData.params.albumId}`}>{routeData.params.albumId}</Link> | crumb: (routeData) => <Link to={`/${routeData?.params?.instanceId}/albums/${routeData.params.albumId}`}>{routeData.params.albumId}</Link> | ||||
| }}/> | }}/> | ||||
| </Route> | </Route> | ||||
| @@ -7,6 +7,7 @@ export class Instance { | |||||
| this.title = instance?.title; | this.title = instance?.title; | ||||
| this.subtitle = instance?.subtitle; | this.subtitle = instance?.subtitle; | ||||
| this.coverPhoto = instance?.coverPhoto ? new Photo(instance.coverPhoto) : null; | this.coverPhoto = instance?.coverPhoto ? new Photo(instance.coverPhoto) : null; | ||||
| this.homeLayout = instance?.homeLayout; | |||||
| this.urls = instance?.urls?.map(u => u) || []; | this.urls = instance?.urls?.map(u => u) || []; | ||||
| } | } | ||||
| } | } | ||||
| @@ -9,10 +9,10 @@ import DragAndDrop from "../components/drag-and-drop/drag-and-drop"; | |||||
| import {Album} from "../models/album"; | import {Album} from "../models/album"; | ||||
| import Loader from "../components/Loader"; | import Loader from "../components/Loader"; | ||||
| import "./album-manager.scss"; | |||||
| import "./album-page.scss"; | |||||
| import {useInstances} from "../services/instances/use-instances"; | import {useInstances} from "../services/instances/use-instances"; | ||||
| function AlbumManager() { | |||||
| function AlbumPage() { | |||||
| const {state} = useLocation(); | const {state} = useLocation(); | ||||
| const {album} = state; // Read values passed on state | const {album} = state; // Read values passed on state | ||||
| const {selectedInstance} = useInstances(); | const {selectedInstance} = useInstances(); | ||||
| @@ -299,4 +299,4 @@ function AlbumManager() { | |||||
| </div>; | </div>; | ||||
| } | } | ||||
| export default AlbumManager; | |||||
| export default AlbumPage; | |||||
| @@ -13,10 +13,10 @@ import Loader from "../components/Loader"; | |||||
| import {Button, TextField} from "@mui/material"; | import {Button, TextField} from "@mui/material"; | ||||
| import {Album} from "../models/album"; | import {Album} from "../models/album"; | ||||
| import "./portfolio-manager.scss" | |||||
| import "./instance-albums-page.scss" | |||||
| import {useInstances} from "../services/instances/use-instances"; | import {useInstances} from "../services/instances/use-instances"; | ||||
| function PortfolioManager() { | |||||
| function InstanceAlbumsPage() { | |||||
| const navigate = useNavigate(); | const navigate = useNavigate(); | ||||
| const {selectedInstance} = useInstances(); | const {selectedInstance} = useInstances(); | ||||
| @@ -208,4 +208,4 @@ function PortfolioManager() { | |||||
| </div> | </div> | ||||
| } | } | ||||
| export default PortfolioManager; | |||||
| export default InstanceAlbumsPage; | |||||
| @@ -6,8 +6,7 @@ import {useAuth} from "../services/authentication/use-auth"; | |||||
| import {useInstances} from "../services/instances/use-instances"; | import {useInstances} from "../services/instances/use-instances"; | ||||
| import {Instance} from "../models/instance"; | import {Instance} from "../models/instance"; | ||||
| import { | import { | ||||
| AddAPhoto, AddCircle, | |||||
| ArrowRightAltOutlined, Edit, RemoveCircle | |||||
| AddAPhoto, AddCircle, ArrowRightAltOutlined, Edit, RadioButtonChecked, RadioButtonUnchecked, RemoveCircle | |||||
| } from "@mui/icons-material"; | } from "@mui/icons-material"; | ||||
| import "./instances-page.scss"; | import "./instances-page.scss"; | ||||
| @@ -16,12 +15,7 @@ import DragAndDrop from "../components/drag-and-drop/drag-and-drop"; | |||||
| export function InstancesPage() { | export function InstancesPage() { | ||||
| const navigate = useNavigate(); | const navigate = useNavigate(); | ||||
| const { | const { | ||||
| instances, | |||||
| createInstance, | |||||
| deleteInstance, | |||||
| updateInstance, | |||||
| uploadCoverPhoto, | |||||
| instanceApiError | |||||
| instances, createInstance, deleteInstance, updateInstance, uploadCoverPhoto, instanceApiError | |||||
| } = useInstances(); | } = useInstances(); | ||||
| const [instancesToUpdate, setInstancesToUpdate] = useState(null); | const [instancesToUpdate, setInstancesToUpdate] = useState(null); | ||||
| const {userData} = useAuth(); | const {userData} = useAuth(); | ||||
| @@ -59,9 +53,10 @@ export function InstancesPage() { | |||||
| } | } | ||||
| 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) | |||||
| || instancesToUpdate[idx].homeLayout !== instances[idx]?.homeLayout; | |||||
| } | } | ||||
| const handleCreateInstance = async () => { | const handleCreateInstance = async () => { | ||||
| await createInstance(new Instance({ | await createInstance(new Instance({ | ||||
| @@ -92,7 +87,20 @@ export function InstancesPage() { | |||||
| const handleDrop = async (e, idx) => { | const handleDrop = async (e, idx) => { | ||||
| await handleFiles(e, idx) | await handleFiles(e, idx) | ||||
| } | } | ||||
| const handleSelectLayout = (idx, layout) => { | |||||
| instancesToUpdate[idx].homeLayout = layout; | |||||
| setInstancesToUpdate([...instancesToUpdate]); | |||||
| } | |||||
| const homeLayouts = [{ | |||||
| id: 1, | |||||
| name: "Layout 1" | |||||
| }, { | |||||
| id: 2, | |||||
| name: "Layout 2" | |||||
| }, { | |||||
| id: 3, | |||||
| name: "Layout 3" | |||||
| }] | |||||
| return <div id="instance-selector-page"> | return <div id="instance-selector-page"> | ||||
| <div id="page-title"> | <div id="page-title"> | ||||
| <div id="left-part"> | <div id="left-part"> | ||||
| @@ -120,87 +128,101 @@ export function InstancesPage() { | |||||
| </>} | </>} | ||||
| </div> | </div> | ||||
| <div id="instance-list"> | <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}/> | |||||
| <div className="btn-edit-photo"><Edit /></div> | |||||
| </> | |||||
| : <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"> | |||||
| {instancesToUpdate?.map((instance, idx) => <div | |||||
| className="pad flex flex--stretch flex--align-start border border--round 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}/> | |||||
| <div className="btn-edit-photo"><Edit/></div> | |||||
| </> : <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="flex flex--align-start flex--vert-sm flex--align-center-sm"> | |||||
| <div className="card-details-left"> | |||||
| <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" | <IconButton color="warning" | ||||
| onClick={(e) => handleAddNewInstanceUrl(e, idx)}> | onClick={(e) => handleAddNewInstanceUrl(e, idx)}> | ||||
| <AddCircle/> | <AddCircle/> | ||||
| </IconButton> | </IconButton> | ||||
| </InputAdornment> | |||||
| ) | |||||
| }}/> | |||||
| <div className="instance-links"> | |||||
| {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>)} | |||||
| </InputAdornment>) | |||||
| }}/> | |||||
| <div className="instance-links"> | |||||
| {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> | ||||
| <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 className="card-details-right"> | |||||
| <div className="flex flex--vert flex--align-start pad"> | |||||
| <span className="inline-block bold">Home layout:</span> | |||||
| <div className="flex flex--vert pad"> | |||||
| {homeLayouts.map(layout => | |||||
| <div key={layout.id} className="flex pointer" onClick={() => handleSelectLayout(idx, layout.id)}> | |||||
| <span>{instance.homeLayout === layout.id ? <RadioButtonChecked/> : | |||||
| <RadioButtonUnchecked/>} | |||||
| </span> | |||||
| <span> {layout.name}</span> | |||||
| </div> | |||||
| )} | |||||
| </div> | |||||
| </div> | </div> | ||||
| </div> | </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}`)}> | |||||
| Manage <ArrowRightAltOutlined/> | |||||
| </Button> | |||||
| </div> | |||||
| </div> | |||||
| </div> | </div> | ||||
| )} | |||||
| </div>)} | |||||
| </div> | </div> | ||||
| </div>; | </div>; | ||||
| } | } | ||||
| @@ -13,9 +13,11 @@ | |||||
| img { | img { | ||||
| position: relative; | position: relative; | ||||
| } | } | ||||
| .btn-edit-photo { | .btn-edit-photo { | ||||
| display: none; | display: none; | ||||
| } | } | ||||
| &:hover { | &:hover { | ||||
| .btn-edit-photo { | .btn-edit-photo { | ||||
| position: absolute; | position: absolute; | ||||
| @@ -27,6 +29,7 @@ | |||||
| width: 100%; | width: 100%; | ||||
| height: 100%; | height: 100%; | ||||
| background-color: rgba(255, 255, 255, 0.2); | background-color: rgba(255, 255, 255, 0.2); | ||||
| svg { | svg { | ||||
| color: white; | color: white; | ||||
| font-size: 48px; | font-size: 48px; | ||||
| @@ -70,5 +73,4 @@ | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,20 @@ | |||||
| import {NavLink} from "react-router-dom"; | |||||
| import {ArrowRightAlt} from "@mui/icons-material"; | |||||
| import "./home-layout1.scss"; | |||||
| export function HomeLayout1({instance}) { | |||||
| return <div id="homeLayout1"> | |||||
| <div className="cover-photo"> | |||||
| <img alt={instance.coverPhoto?.name} src={instance.coverPhoto?.url}/> | |||||
| </div> | |||||
| <div className="instance-details"> | |||||
| <div className="box"> | |||||
| <h1>{instance.title}</h1> | |||||
| <h2>{instance.subtitle}</h2> | |||||
| <NavLink to="/portfolio" className="goto-website-link"> | |||||
| <span>ENTER</span><ArrowRightAlt/> | |||||
| </NavLink> | |||||
| </div> | |||||
| </div> | |||||
| </div>; | |||||
| } | |||||
| @@ -1,4 +1,4 @@ | |||||
| #home { | |||||
| #homeLayout1 { | |||||
| height: 100vh; | height: 100vh; | ||||
| width: 100vw; | width: 100vw; | ||||
| display: grid; | display: grid; | ||||
| @@ -36,15 +36,17 @@ | |||||
| padding: 60px; | padding: 60px; | ||||
| max-width: 620px; | max-width: 620px; | ||||
| text-align: center; | text-align: center; | ||||
| @media(max-width: 600px) { | |||||
| padding: 0; | |||||
| max-width: 90vw; | |||||
| } | |||||
| h1 { | h1 { | ||||
| text-align: center; | text-align: center; | ||||
| display: inline-block; | display: inline-block; | ||||
| font-size: 37px; | font-size: 37px; | ||||
| line-height: 1em; | line-height: 1em; | ||||
| margin: 0; | |||||
| margin-bottom: 22px; | |||||
| //font-family: proxima-nova; | |||||
| margin: 0 0 22px; | |||||
| font-weight: 700; | font-weight: 700; | ||||
| font-style: normal; | font-style: normal; | ||||
| text-transform: uppercase; | text-transform: uppercase; | ||||
| @@ -65,29 +67,38 @@ | |||||
| letter-spacing: .28em; | letter-spacing: .28em; | ||||
| line-height: 1.7em; | line-height: 1.7em; | ||||
| color: #333; | color: #333; | ||||
| margin: 0; | |||||
| margin-bottom: 36px; | |||||
| margin: 0 0 36px; | |||||
| } | } | ||||
| .goto-website-link { | .goto-website-link { | ||||
| text-rendering: optimizeLegibility; | text-rendering: optimizeLegibility; | ||||
| white-space: nowrap; | white-space: nowrap; | ||||
| line-height: 1em; | |||||
| //font-family: proxima-nova; | //font-family: proxima-nova; | ||||
| font-weight: 500; | |||||
| font-style: normal; | font-style: normal; | ||||
| font-size: 14px; | |||||
| text-transform: uppercase; | text-transform: uppercase; | ||||
| letter-spacing: 0; | letter-spacing: 0; | ||||
| text-align: center; | text-align: center; | ||||
| padding: 1em calc(1.44em - 0em) 1em 1.44em; | |||||
| display: inline-block; | |||||
| padding: 1em 1.44em; | |||||
| text-decoration: none; | text-decoration: none; | ||||
| background-color: transparent; | background-color: transparent; | ||||
| color: #333; | color: #333; | ||||
| border: 2px solid #333; | border: 2px solid #333; | ||||
| display: inline-block; | |||||
| height: calc(22px + 2em); | |||||
| transition: background-color 170ms ease-in-out, color 170ms ease-in-out; | transition: background-color 170ms ease-in-out, color 170ms ease-in-out; | ||||
| //transition: color 170ms ease-in-out, border-color 170ms ease-in-out; | |||||
| span { | |||||
| display: inline-block; | |||||
| vertical-align: top; | |||||
| line-height: 22px; | |||||
| font-size: 14px; | |||||
| font-weight: 500; | |||||
| } | |||||
| svg { | |||||
| margin-left: 5px; | |||||
| font-size: 22px; | |||||
| } | |||||
| &:hover { | &:hover { | ||||
| background-color: black; | background-color: black; | ||||
| @@ -0,0 +1,20 @@ | |||||
| import {NavLink} from "react-router-dom"; | |||||
| import {ArrowRightAlt} from "@mui/icons-material"; | |||||
| import "./home-layout2.scss"; | |||||
| export function HomeLayout2({instance}) { | |||||
| return <div id="homeLayout2"> | |||||
| <div className="instance-details"> | |||||
| <div className="box"> | |||||
| <h1>{instance.title}</h1> | |||||
| <h2>{instance.subtitle}</h2> | |||||
| <NavLink to="/portfolio" className="goto-website-link"> | |||||
| <span>ENTER</span><ArrowRightAlt/> | |||||
| </NavLink> | |||||
| </div> | |||||
| </div> | |||||
| <div className="cover-photo"> | |||||
| <img alt={instance.coverPhoto?.name} src={instance.coverPhoto?.url}/> | |||||
| </div> | |||||
| </div>; | |||||
| } | |||||
| @@ -0,0 +1,110 @@ | |||||
| #homeLayout2 { | |||||
| height: 100vh; | |||||
| width: 100vw; | |||||
| display: grid; | |||||
| grid-template-columns: 1fr 1fr; | |||||
| @media(max-width: 1200px) { | |||||
| grid-template-columns: 1fr; | |||||
| grid-template-rows: 1fr 1fr; | |||||
| } | |||||
| .cover-photo { | |||||
| overflow: hidden; | |||||
| display: flex; | |||||
| justify-content: center; | |||||
| align-items: center; | |||||
| img { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| object-position: 50% 50%; | |||||
| object-fit: cover; | |||||
| } | |||||
| } | |||||
| .instance-details { | |||||
| display: flex; | |||||
| justify-content: center; | |||||
| flex-direction: column; | |||||
| .box { | |||||
| position: static !important; | |||||
| transform: none; | |||||
| margin-left: auto; | |||||
| margin-right: auto; | |||||
| padding: 60px; | |||||
| max-width: 620px; | |||||
| text-align: center; | |||||
| @media(max-width: 600px) { | |||||
| padding: 0; | |||||
| max-width: 90vw; | |||||
| } | |||||
| h1 { | |||||
| text-align: center; | |||||
| display: inline-block; | |||||
| font-size: 37px; | |||||
| line-height: 1em; | |||||
| margin: 0 0 22px; | |||||
| font-weight: 700; | |||||
| font-style: normal; | |||||
| text-transform: uppercase; | |||||
| letter-spacing: .1em; | |||||
| color: #333; | |||||
| @media(max-width: 600px) { | |||||
| font-size: 24px; | |||||
| } | |||||
| } | |||||
| h2 { | |||||
| //font-family: minion-pro; | |||||
| font-size: 19px; | |||||
| font-weight: 400; | |||||
| font-style: normal; | |||||
| text-transform: uppercase; | |||||
| letter-spacing: .28em; | |||||
| line-height: 1.7em; | |||||
| color: #333; | |||||
| margin: 0 0 36px; | |||||
| } | |||||
| .goto-website-link { | |||||
| text-rendering: optimizeLegibility; | |||||
| white-space: nowrap; | |||||
| //font-family: proxima-nova; | |||||
| font-style: normal; | |||||
| text-transform: uppercase; | |||||
| letter-spacing: 0; | |||||
| text-align: center; | |||||
| padding: 1em 1.44em; | |||||
| text-decoration: none; | |||||
| background-color: transparent; | |||||
| color: #333; | |||||
| border: 2px solid #333; | |||||
| display: inline-block; | |||||
| height: calc(22px + 2em); | |||||
| transition: background-color 170ms ease-in-out, color 170ms ease-in-out; | |||||
| span { | |||||
| display: inline-block; | |||||
| vertical-align: top; | |||||
| line-height: 22px; | |||||
| font-size: 14px; | |||||
| font-weight: 500; | |||||
| } | |||||
| svg { | |||||
| margin-left: 5px; | |||||
| font-size: 22px; | |||||
| } | |||||
| &:hover { | |||||
| background-color: black; | |||||
| color: white; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,20 @@ | |||||
| import {NavLink} from "react-router-dom"; | |||||
| import {ArrowRightAlt} from "@mui/icons-material"; | |||||
| import "./home-layout3.scss"; | |||||
| export function HomeLayout3({instance}) { | |||||
| return <div id="homeLayout3"> | |||||
| <div className="cover-photo"> | |||||
| <img alt={instance.coverPhoto?.name} src={instance.coverPhoto?.url}/> | |||||
| <div className="instance-details"> | |||||
| <div className="box"> | |||||
| <h1>{instance.title}</h1> | |||||
| <h2>{instance.subtitle}</h2> | |||||
| <NavLink to="/portfolio" className="goto-website-link"> | |||||
| <span>ENTER</span><ArrowRightAlt/> | |||||
| </NavLink> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div>; | |||||
| } | |||||
| @@ -0,0 +1,110 @@ | |||||
| #homeLayout3 { | |||||
| height: 100vh; | |||||
| width: 100vw; | |||||
| .cover-photo { | |||||
| overflow: hidden; | |||||
| display: flex; | |||||
| justify-content: center; | |||||
| align-items: center; | |||||
| img { | |||||
| position: absolute; | |||||
| top: 0; | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| object-position: 50% 50%; | |||||
| object-fit: cover; | |||||
| } | |||||
| } | |||||
| .instance-details { | |||||
| margin-top: calc(50vh - 125px); | |||||
| height: 250px; | |||||
| width: 100vw; | |||||
| background-color: white; | |||||
| display: flex; | |||||
| justify-content: center; | |||||
| flex-direction: column; | |||||
| position: relative; | |||||
| .box { | |||||
| position: static !important; | |||||
| transform: none; | |||||
| margin-left: auto; | |||||
| margin-right: auto; | |||||
| padding: 60px; | |||||
| max-width: 620px; | |||||
| text-align: center; | |||||
| @media(max-width: 600px) { | |||||
| padding: 0; | |||||
| max-width: 90vw; | |||||
| } | |||||
| h1 { | |||||
| text-align: center; | |||||
| display: inline-block; | |||||
| font-size: 37px; | |||||
| line-height: 1em; | |||||
| margin: 0 0 22px; | |||||
| font-weight: 700; | |||||
| font-style: normal; | |||||
| text-transform: uppercase; | |||||
| letter-spacing: .1em; | |||||
| color: #333; | |||||
| @media(max-width: 600px) { | |||||
| font-size: 24px; | |||||
| } | |||||
| } | |||||
| h2 { | |||||
| //font-family: minion-pro; | |||||
| font-size: 19px; | |||||
| font-weight: 400; | |||||
| font-style: normal; | |||||
| text-transform: uppercase; | |||||
| letter-spacing: .28em; | |||||
| line-height: 1.7em; | |||||
| color: #333; | |||||
| margin: 0 0 36px; | |||||
| } | |||||
| .goto-website-link { | |||||
| text-rendering: optimizeLegibility; | |||||
| white-space: nowrap; | |||||
| //font-family: proxima-nova; | |||||
| font-style: normal; | |||||
| text-transform: uppercase; | |||||
| letter-spacing: 0; | |||||
| text-align: center; | |||||
| padding: 1em 1.44em; | |||||
| text-decoration: none; | |||||
| background-color: transparent; | |||||
| color: #333; | |||||
| border: 2px solid #333; | |||||
| display: inline-block; | |||||
| height: calc(22px + 2em); | |||||
| transition: background-color 170ms ease-in-out, color 170ms ease-in-out; | |||||
| span { | |||||
| display: inline-block; | |||||
| vertical-align: top; | |||||
| line-height: 22px; | |||||
| font-size: 14px; | |||||
| font-weight: 500; | |||||
| } | |||||
| svg { | |||||
| margin-left: 5px; | |||||
| font-size: 22px; | |||||
| } | |||||
| &:hover { | |||||
| background-color: black; | |||||
| color: white; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -7,5 +7,6 @@ export class Instance { | |||||
| this.title = instance?.title; | this.title = instance?.title; | ||||
| this.subtitle = instance?.subtitle; | this.subtitle = instance?.subtitle; | ||||
| this.coverPhoto = instance?.coverPhoto ? new Photo(instance.coverPhoto) : null; | this.coverPhoto = instance?.coverPhoto ? new Photo(instance.coverPhoto) : null; | ||||
| this.homeLayout = instance?.homeLayout; | |||||
| } | } | ||||
| } | } | ||||
| @@ -30,7 +30,7 @@ export default function AlbumPage() { | |||||
| dangerouslySetInnerHTML={{__html: selectedAlbum.description.replace(/\n/g, "<br />")}}> | dangerouslySetInnerHTML={{__html: selectedAlbum.description.replace(/\n/g, "<br />")}}> | ||||
| </div>} | </div>} | ||||
| </div> | </div> | ||||
| {selectedAlbum.photos && <div id="photo-list" className="photo-grid-layout"> | |||||
| {selectedAlbum.photos && <div id="photo-list" className="photo-grid-layout photo-grid-layout--no-details"> | |||||
| {selectedAlbum.photos.map((photo, idx) => | {selectedAlbum.photos.map((photo, idx) => | ||||
| <div className="photo grid-cell" key={photo.photoId} | <div className="photo grid-cell" key={photo.photoId} | ||||
| onClick={() => setSelectedPhotoIndex(idx)}> | onClick={() => setSelectedPhotoIndex(idx)}> | ||||
| @@ -41,11 +41,11 @@ export default function AlbumPage() { | |||||
| <ZoomOutMapSharp/> | <ZoomOutMapSharp/> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div className="details"> | |||||
| <div className="name"> | |||||
| {photo.name} | |||||
| </div> | |||||
| </div> | |||||
| {/*<div className="details">*/} | |||||
| {/* <div className="name">*/} | |||||
| {/* {photo.name}*/} | |||||
| {/* </div>*/} | |||||
| {/*</div>*/} | |||||
| </div> | </div> | ||||
| )} | )} | ||||
| </div>} | </div>} | ||||
| @@ -1,23 +1,15 @@ | |||||
| import {useCurrentInstance} from "../../services/current-instance/use-current-instance"; | import {useCurrentInstance} from "../../services/current-instance/use-current-instance"; | ||||
| import "./home-page.scss"; | |||||
| import {NavLink} from "react-router-dom"; | |||||
| import {HomeLayout1} from "../../components/home-layouts/home-layout1"; | |||||
| import {HomeLayout2} from "../../components/home-layouts/home-layout2"; | |||||
| import {HomeLayout3} from "../../components/home-layouts/home-layout3"; | |||||
| export function HomePage() { | export function HomePage() { | ||||
| const {instance} = useCurrentInstance(); | const {instance} = useCurrentInstance(); | ||||
| return instance && <div id="home"> | |||||
| <div className="cover-photo"> | |||||
| <img alt={instance.coverPhoto?.name} src={instance.coverPhoto?.url}/> | |||||
| </div> | |||||
| <div className="instance-details"> | |||||
| <div className="box"> | |||||
| <h1>{instance.title}</h1> | |||||
| <h2>{instance.subtitle}</h2> | |||||
| <NavLink to="/portfolio" className="goto-website-link"> | |||||
| GO TO WEBSITE | |||||
| </NavLink> | |||||
| </div> | |||||
| </div> | |||||
| </div>; | |||||
| return instance && instance.homeLayout === 3 | |||||
| ? <HomeLayout3 instance={instance}/> | |||||
| : instance.homeLayout === 2 | |||||
| ? <HomeLayout2 instance={instance}/> | |||||
| : <HomeLayout1 instance={instance}/> | |||||
| ; | |||||
| } | } | ||||
| @@ -191,6 +191,10 @@ html:has(#photoViewer) { | |||||
| column-gap: 30px; | column-gap: 30px; | ||||
| margin-bottom: 4em; | margin-bottom: 4em; | ||||
| &.photo-grid-layout--no-details { | |||||
| row-gap: 30px; | |||||
| } | |||||
| //@media (max-width: 1920px) { | //@media (max-width: 1920px) { | ||||
| // grid-template-columns: repeat(4, 1fr); | // grid-template-columns: repeat(4, 1fr); | ||||
| //} | //} | ||||
| @@ -10,6 +10,7 @@ export class Instance { | |||||
| this.title = instance?.title; | this.title = instance?.title; | ||||
| this.subtitle = instance?.subtitle; | this.subtitle = instance?.subtitle; | ||||
| this.coverPhoto = instance?.coverPhoto ? new Photo(instance?.coverPhoto) : null; | this.coverPhoto = instance?.coverPhoto ? new Photo(instance?.coverPhoto) : null; | ||||
| this.homeLayout = instance?.homeLayout; | |||||
| this.urls = instance?.urls || []; | this.urls = instance?.urls || []; | ||||
| } | } | ||||
| @@ -23,6 +24,8 @@ export class Instance { | |||||
| subtitle; | subtitle; | ||||
| /** @type {Photo} **/ | /** @type {Photo} **/ | ||||
| coverPhoto; | coverPhoto; | ||||
| /** @type {Number} **/ | |||||
| homeLayout; | |||||
| /** @type {string[]} **/ | /** @type {string[]} **/ | ||||
| urls; | urls; | ||||
| } | } | ||||
| @@ -38,6 +38,7 @@ class InstancesService { | |||||
| instance.title = instanceToUpdate.title; | instance.title = instanceToUpdate.title; | ||||
| instance.subtitle = instanceToUpdate.subtitle; | instance.subtitle = instanceToUpdate.subtitle; | ||||
| instance.urls = instanceToUpdate.urls; | instance.urls = instanceToUpdate.urls; | ||||
| instance.homeLayout = instanceToUpdate.homeLayout; | |||||
| try { | try { | ||||
| await db.instances.update(instance); | await db.instances.update(instance); | ||||