diff options
Diffstat (limited to 'src/views')
| -rw-r--r-- | src/views/App.js | 42 | ||||
| -rw-r--r-- | src/views/Detail.js | 65 | ||||
| -rw-r--r-- | src/views/Gallery.js | 75 |
3 files changed, 163 insertions, 19 deletions
diff --git a/src/views/App.js b/src/views/App.js new file mode 100644 index 0000000..c5a7f83 --- /dev/null +++ b/src/views/App.js @@ -0,0 +1,42 @@ +/** + * Main React app logic + */ + +import React, { useState, useEffect, useCallback } from "react"; + +import Detail from "./Detail.js"; +import buildGraph from "../graph.js"; + +export default function App() { + const [db, setDb] = useState(null); + const [node, setNode] = useState(null); + const [detailVisible, setDetailVisible] = useState(null); + + useEffect(async () => { + const newDb = await loadDB(); + setDb(newDb); + buildGraph(newDb, { + click: handleClick, + }); + }, []); + + const handleClick = useCallback((node) => { + setNode(node); + setDetailVisible(true); + }); + + const handleClose = useCallback((node) => { + setDetailVisible(false); + }); + + return ( + <div> + <Detail node={node} visible={detailVisible} onClose={handleClose} /> + </div> + ); +} + +async function loadDB() { + const request = await fetch("/assets/db.json"); + return await request.json(); +} diff --git a/src/views/Detail.js b/src/views/Detail.js index 51598c1..0a756c9 100644 --- a/src/views/Detail.js +++ b/src/views/Detail.js @@ -2,30 +2,57 @@ * Detail view, displaying text plus media */ -export default function Detail({ node }) { - const index = id + 1; +import React from "react"; + +import Gallery from "./Gallery.js"; + +export default function Detail({ node, visible, onClose }) { + if (!node) { + return <div className="detail" />; + } + const { id, data } = node; + const index = id + 1; return ( - <div className="detail"> + <div className={visible ? "detail visible" : "detail"}> <div className="content"> - <div className="title"> - {index} - <br /> - {data.author} - <br /> - {data.title} - <br /> + <div> + <div className="title"> + <div className="index">{pad(index)}</div> + {data.author && ( + <div dangerouslySetInnerHTML={{ __html: data.author }} /> + )} + <div dangerouslySetInnerHTML={{ __html: data.title }} /> + </div> + <div + className="citation" + dangerouslySetInnerHTML={{ __html: data.citation }} + /> + <div + className="description" + dangerouslySetInnerHTML={{ __html: data.description }} + /> + </div> + </div> + <div className="media"> + <div className="buttons close"> + <img src="/assets/img/close.svg" onClick={onClose} /> </div> - <div - className="caption" - dangerouslySetInnerHTML={{ __html: data.caption }} - /> - <div - className="description" - dangerouslySetInnerHTML={{ __html: data.description }} - /> + {node.type === "video" ? ( + "video" + ) : ( + <Gallery images={data.images} visible={visible} /> + )} </div> - <div className="media"></div> </div> ); } + +const pad = (value) => (value < 10 ? "0" + value : value); +const capitalizeWord = (text = "") => + text ? text.charAt(0).toUpperCase() + text.slice(1) : ""; +const capitalize = (text = "") => + String(text || "") + .split(" ") + .map(capitalizeWord) + .join(" "); diff --git a/src/views/Gallery.js b/src/views/Gallery.js new file mode 100644 index 0000000..7bdf18b --- /dev/null +++ b/src/views/Gallery.js @@ -0,0 +1,75 @@ +/** + * Detail view, displaying text plus media + */ + +import React, { useState, useEffect } from "react"; + +export default function Gallery({ images, visible }) { + const hasItems = !!images?.length; + const oneItem = images?.length === 1; + + const [index, setIndex] = useState(0); + const [opacity, setOpacity] = useState(0); + useEffect(() => { + setIndex(0); + setOpacity(0); + setTimeout(() => setOpacity(1), 500); + }, [images]); + + function previous() { + setOpacity(0); + setTimeout(() => setIndex(Math.max(0, index - 1)), 100); + // setTimeout(() => setOpacity(1), 500); + } + function next() { + setOpacity(0); + setTimeout(() => setIndex(Math.min(images.length - 1, index + 1)), 100); + // setTimeout(() => setOpacity(1), 500); + } + function nextOrWrap() { + if (oneItem) return; + setOpacity(0); + setTimeout(() => setIndex(mod(index + 1, images.length)), 100); + // setTimeout(() => setOpacity(1), 500); + } + function appear() { + setOpacity(1); + } + + if (!hasItems) { + return <div className="gallery" style={{ opacity: 0 }} />; + } + + return ( + <div className="gallery" style={{ opacity: visible ? 1 : 0 }}> + <div className="image"> + {visible && !!images[index] && ( + <img + src={images[index].uri} + onClick={nextOrWrap} + onLoad={appear} + style={{ opacity }} + /> + )} + </div> + <div className="buttons"> + {!oneItem && ( + <img + src="/assets/img/arrow-back.svg" + onClick={previous} + style={{ opacity: index > 0 ? 1 : 0 }} + /> + )} + {!oneItem && ( + <img + src="/assets/img/arrow-forward.svg" + onClick={next} + style={{ opacity: index < images.length - 1 ? 1 : 0 }} + /> + )} + </div> + </div> + ); +} + +const mod = (n, m) => n - m * Math.floor(n / m); |
