summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/graph.js2
-rw-r--r--src/index.js21
-rw-r--r--src/views/App.js42
-rw-r--r--src/views/Detail.js65
-rw-r--r--src/views/Gallery.js75
5 files changed, 169 insertions, 36 deletions
diff --git a/src/graph.js b/src/graph.js
index fdac979..4ed953a 100644
--- a/src/graph.js
+++ b/src/graph.js
@@ -105,7 +105,7 @@ export default function buildGraph(db, handlers) {
// graph.d3Force("charge").strength(-150);
// camera orbit
- const distance = 250;
+ const distance = 415;
let angle = 0;
graph.cameraPosition({
x: distance * Math.sin(angle),
diff --git a/src/index.js b/src/index.js
index c51ecd1..c084565 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,23 +2,12 @@
* No.6092 site for Charles Stankievech
*/
-import buildGraph from "./graph.js";
+import React from "react";
+import ReactDOM from "react-dom";
+import App from "./views/App.js";
-async function main() {
- const db = await loadDB();
-
- function handleClick(node) {
- console.log(node);
- }
-
- buildGraph(db, {
- click: handleClick,
- });
-}
-
-async function loadDB() {
- const request = await fetch("/assets/db.json");
- return await request.json();
+function main() {
+ ReactDOM.render(<App />, document.querySelector("#app"));
}
main();
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);