From 0604d587cf7d07435c786061d4204b243cde52bd Mon Sep 17 00:00:00 2001
From: Kenneth Yang <82800265+kjy5@users.noreply.github.com>
Date: Sun, 25 Dec 2022 14:36:05 -0800
Subject: [PATCH] 106 feature new artifact page (#118)
* Remove redundant PR type spec
* Implemented routes, added pages
* Comments
* Link to artifact pages, use artifact id
* Artifact page basic components
* Artifact page responsive layout
* Extract props and fixed prop names
* Rich Link basic in place, fine-tuning sass
* Rich links with style
* Tweak margins, fixed back button styling
* Installed photoswipe...
* Added style and is rendering, but opening in new tab
* Fixed gallery not opening
* Set thumbnail width
* Extra comments
---
.eslintrc.cjs | 2 +-
.github/workflows/build.yml | 1 -
.github/workflows/reformat-and-lint.yml | 1 -
package-lock.json | 108 ++++++++++++++++++-
package.json | 7 +-
src/components/ArtifactCard.tsx | 22 ++--
src/components/Gallery.d.ts | 3 +
src/components/Gallery.jsx | 93 ++++++++++++++++
src/components/Quarter.tsx | 13 +--
src/components/RichLink.tsx | 33 ++++++
src/components/Year.tsx | 13 +--
src/main.tsx | 47 +++++----
src/routes/ArtifactPage.tsx | 135 ++++++++++++++++++++++++
src/routes/ErrorPage.tsx | 32 ++++++
src/routes/Root.tsx | 36 +++++++
src/scripts/interfaces.ts | 15 +--
src/scripts/parse-data.ts | 23 ++--
src/styles/ArtifactCard.sass | 2 +
src/styles/ArtifactPage.sass | 32 ++++++
src/styles/Gallery.sass | 11 ++
src/styles/Quarter.sass | 2 +-
src/styles/RichLink.sass | 48 +++++++++
tsconfig.json | 2 +-
23 files changed, 615 insertions(+), 66 deletions(-)
create mode 100644 src/components/Gallery.d.ts
create mode 100644 src/components/Gallery.jsx
create mode 100644 src/components/RichLink.tsx
create mode 100644 src/routes/ArtifactPage.tsx
create mode 100644 src/routes/ErrorPage.tsx
create mode 100644 src/routes/Root.tsx
create mode 100644 src/styles/ArtifactPage.sass
create mode 100644 src/styles/Gallery.sass
create mode 100644 src/styles/RichLink.sass
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 66dabfa..fe55ffb 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -12,7 +12,7 @@ module.exports = {
},
plugins: ["@typescript-eslint"],
root: true,
- ignorePatterns: ["dist", "vite.config.ts"],
+ ignorePatterns: ["dist", "vite.config.ts", "src/components/Gallery.jsx"],
rules: {
"sort-imports": "error",
},
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ba0f9d2..0b152a5 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -3,7 +3,6 @@ name: Build Website
# Controls when the workflow will run
on:
pull_request:
- types: [opened, synchronize, reopened]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
diff --git a/.github/workflows/reformat-and-lint.yml b/.github/workflows/reformat-and-lint.yml
index 8e36eec..de3ad89 100644
--- a/.github/workflows/reformat-and-lint.yml
+++ b/.github/workflows/reformat-and-lint.yml
@@ -2,7 +2,6 @@ name: Reformat and Lint
on:
pull_request:
- types: [opened, synchronize, reopened]
workflow_dispatch:
diff --git a/package-lock.json b/package-lock.json
index 35ea858..3984db8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,12 +8,17 @@
"name": "honors-portfolio",
"version": "0.0.0",
"dependencies": {
+ "photoswipe": "^5.3.4",
+ "photoswipe-dynamic-caption-plugin": "^1.2.7",
+ "prop-types": "^15.8.1",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.6.1"
},
"devDependencies": {
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
+ "@types/react-router-dom": "^5.3.3",
"@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.47.0",
"@vitejs/plugin-legacy": "^3.0.1",
@@ -537,6 +542,14 @@
"node": ">= 8"
}
},
+ "node_modules/@remix-run/router": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.2.1.tgz",
+ "integrity": "sha512-XiY0IsyHR+DXYS5vBxpoBe/8veTeoRpMHP+vDosLZxL5bnpetzI0igkxkLZS235ldLzyfkxF+2divEwWHP3vMQ==",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@swc/core": {
"version": "1.3.23",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.23.tgz",
@@ -726,6 +739,12 @@
"node": ">=10"
}
},
+ "node_modules/@types/history": {
+ "version": "4.7.11",
+ "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
+ "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
+ "dev": true
+ },
"node_modules/@types/json-schema": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
@@ -758,6 +777,27 @@
"@types/react": "*"
}
},
+ "node_modules/@types/react-router": {
+ "version": "5.1.20",
+ "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
+ "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
+ "dev": true,
+ "dependencies": {
+ "@types/history": "^4.7.11",
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-router-dom": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
+ "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
+ "dev": true,
+ "dependencies": {
+ "@types/history": "^4.7.11",
+ "@types/react": "*",
+ "@types/react-router": "*"
+ }
+ },
"node_modules/@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
@@ -2068,6 +2108,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -2178,6 +2226,19 @@
"node": ">=8"
}
},
+ "node_modules/photoswipe": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.3.4.tgz",
+ "integrity": "sha512-SN+RWHqxJvdwzXJsh8KrG+ajjPpdTX5HpKglEd0k9o6o5fW+QHPkW8//Bo11MB+NQwTa/hFw8BDv2EdxiDXjNw==",
+ "engines": {
+ "node": ">= 0.12.0"
+ }
+ },
+ "node_modules/photoswipe-dynamic-caption-plugin": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/photoswipe-dynamic-caption-plugin/-/photoswipe-dynamic-caption-plugin-1.2.7.tgz",
+ "integrity": "sha512-5XXdXLf2381nwe7KqQvcyStiUBi9TitYXppUQTrzPwYAi4lZsmWNnNKMclM7I4QGlX6fXo42v3bgb6rlK9pY1Q=="
+ },
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@@ -2244,6 +2305,16 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
"node_modules/punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -2296,6 +2367,41 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
+ "node_modules/react-router": {
+ "version": "6.6.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.6.1.tgz",
+ "integrity": "sha512-YkvlYRusnI/IN0kDtosUCgxqHeulN5je+ew8W+iA1VvFhf86kA+JEI/X/8NqYcr11hCDDp906S+SGMpBheNeYQ==",
+ "dependencies": {
+ "@remix-run/router": "1.2.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.6.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.6.1.tgz",
+ "integrity": "sha512-u+8BKUtelStKbZD5UcY0NY90WOzktrkJJhyhNg7L0APn9t1qJNLowzrM9CHdpB6+rcPt6qQrlkIXsTvhuXP68g==",
+ "dependencies": {
+ "@remix-run/router": "1.2.1",
+ "react-router": "6.6.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
diff --git a/package.json b/package.json
index 349a668..3bae7f0 100644
--- a/package.json
+++ b/package.json
@@ -10,12 +10,17 @@
"relint": "prettier --write . && eslint --fix ."
},
"dependencies": {
+ "photoswipe": "^5.3.4",
+ "photoswipe-dynamic-caption-plugin": "^1.2.7",
+ "prop-types": "^15.8.1",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.6.1"
},
"devDependencies": {
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
+ "@types/react-router-dom": "^5.3.3",
"@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.47.0",
"@vitejs/plugin-legacy": "^3.0.1",
diff --git a/src/components/ArtifactCard.tsx b/src/components/ArtifactCard.tsx
index b858378..1dd630c 100644
--- a/src/components/ArtifactCard.tsx
+++ b/src/components/ArtifactCard.tsx
@@ -1,20 +1,26 @@
import "../styles/ArtifactCard.sass";
-import { Artifact } from "../scripts/interfaces";
+import { ArtifactData } from "../scripts/interfaces";
+import { NavLink } from "react-router-dom";
/**
- * Artifact card component. Displays high-level information about an artifact (title, subtitle, 3D graphic).
- * @param props {Artifact} artifact - Artifact object
+ * ArtifactPage card component. Displays high-level information about an artifact (title, subtitle, 3D graphic).
+ * @param {{artifact: ArtifactData}} props - Artifact to display
* @constructor
* @return {JSX.Element}
*/
export default function ArtifactCard(props: {
- artifact: Artifact;
+ artifact: ArtifactData;
}): JSX.Element {
+ // Extract props
+ const { id, title, subtitle } = props.artifact;
+
+ // Render
return (
-
+ /* skipcq: JS-0394 */
+
- {props.artifact.title}
- {props.artifact.subtitle}
-
+ {title}
+ {subtitle}
+
);
}
diff --git a/src/components/Gallery.d.ts b/src/components/Gallery.d.ts
new file mode 100644
index 0000000..caf5ba3
--- /dev/null
+++ b/src/components/Gallery.d.ts
@@ -0,0 +1,3 @@
+import { ImageData } from "../scripts/interfaces";
+
+export default function Gallery(props: { images: ImageData[] }): JSX.Element;
diff --git a/src/components/Gallery.jsx b/src/components/Gallery.jsx
new file mode 100644
index 0000000..5428f8d
--- /dev/null
+++ b/src/components/Gallery.jsx
@@ -0,0 +1,93 @@
+// noinspection JSUnusedGlobalSymbols
+
+import "../styles/Gallery.sass";
+import { useEffect } from "react";
+import PhotoSwipeLightbox from "photoswipe/lightbox";
+import "photoswipe/style.css";
+import "photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css";
+import PropTypes from "prop-types";
+import PhotoSwipeDynamicCaption from "photoswipe-dynamic-caption-plugin";
+
+/**
+ * Gallery component, displays a gallery of images
+ * @param {object} props - Images to display
+ * @constructor
+ * @returns {JSX.Element}
+ */
+export default function Gallery(props) {
+ // Extract prop data
+ const { images } = props;
+ const galleryID = images[0].artifact.replaceAll(" ", "-").toLowerCase();
+
+ // Initialize the gallery
+ useEffect(() => {
+ // Create lightbox
+ let lightbox = new PhotoSwipeLightbox({
+ gallery: `#${galleryID}`,
+ children: "a",
+ padding: { top: 30, right: 70, bottom: 30, left: 70 },
+ preloaderDelay: 0,
+ pswpModule: () => import("photoswipe"),
+ });
+
+ // Add dynamic caption plugin
+ // noinspection JSUnusedLocalSymbols
+ const _photoSwipeDynamicCaption = new PhotoSwipeDynamicCaption(lightbox, {
+ type: "auto",
+ captionContent: ".pswp-caption-content",
+ });
+
+ // Start the lightbox
+ lightbox.init();
+
+ // Cleanup
+ return () => {
+ lightbox.destroy();
+ lightbox = null;
+ };
+ }, []);
+
+ // Render
+ return (
+
+ );
+}
+
+Gallery.propTypes = {
+ images: PropTypes.arrayOf(
+ PropTypes.shape({
+ artifact: PropTypes.string.isRequired,
+ width: PropTypes.number.isRequired,
+ height: PropTypes.number.isRequired,
+ name: PropTypes.string.isRequired,
+ description: PropTypes.string,
+ thumbnail: PropTypes.string.isRequired,
+ image: PropTypes.string.isRequired,
+ })
+ ),
+};
diff --git a/src/components/Quarter.tsx b/src/components/Quarter.tsx
index d30e979..512f431 100644
--- a/src/components/Quarter.tsx
+++ b/src/components/Quarter.tsx
@@ -1,19 +1,19 @@
import "../styles/Quarter.sass";
-import { Artifact } from "../scripts/interfaces";
import ArtifactCard from "./ArtifactCard";
+import { ArtifactData } from "../scripts/interfaces";
/**
* Quarter component. Groups a quarter heading with a grid of artifacts.
- * @param props {Artifact[]} filterArtifacts - Artifacts filtered by quarter
+ * @param {{filteredArtifacts: ArtifactData[]}} props - filterArtifacts - Artifacts filtered by quarter
* @constructor
* @return {JSX.Element}
*/
export default function Quarter(props: {
- filterArtifacts: Artifact[];
+ filteredArtifacts: ArtifactData[];
}): JSX.Element {
// Compute quarter string
let quarter = "Fall";
- switch (props.filterArtifacts[0].quarter) {
+ switch (props.filteredArtifacts[0].quarter) {
case 1:
quarter = "Winter";
break;
@@ -27,12 +27,13 @@ export default function Quarter(props: {
break;
}
+ // Render
return (
{quarter}
- {props.filterArtifacts.map((artifact: Artifact) => (
-
+ {props.filteredArtifacts.map((artifact: ArtifactData) => (
+
))}
diff --git a/src/components/RichLink.tsx b/src/components/RichLink.tsx
new file mode 100644
index 0000000..8ee7af2
--- /dev/null
+++ b/src/components/RichLink.tsx
@@ -0,0 +1,33 @@
+import "../styles/RichLink.sass";
+import { LinkData } from "../scripts/interfaces";
+
+/**
+ * RichLink component. Displays a link with a title, description, and thumbnail.
+ * @param {{linkData: LinkData}} props - Information about the link
+ * @constructor
+ * @return {JSX.Element}
+ */
+export default function RichLink(props: { linkData: LinkData }): JSX.Element {
+ // Extract props
+ const { url, image, title, description } = props.linkData;
+
+ // Render
+ return (
+
+
+
+
{title}
+
{description}
+
{url}
+
+
+ );
+}
diff --git a/src/components/Year.tsx b/src/components/Year.tsx
index bfbae39..0167d82 100644
--- a/src/components/Year.tsx
+++ b/src/components/Year.tsx
@@ -1,15 +1,15 @@
import "../styles/Year.sass";
-import { Artifact } from "../scripts/interfaces";
+import { ArtifactData } from "../scripts/interfaces";
import Quarter from "./Quarter";
/**
* Year component. Groups a year heading with a list of quarters.
- * @param props {Artifact[]} filteredArtifacts - Artifacts filtered by year
+ * @param {{filteredArtifacts: ArtifactData[]}} props - Artifacts filtered by year
* @constructor
* @return {JSX.Element}
*/
export default function Year(props: {
- filteredArtifacts: Artifact[];
+ filteredArtifacts: ArtifactData[];
}): JSX.Element {
// Compute year string
let year = "Freshman 2021 - 2022";
@@ -29,18 +29,19 @@ export default function Year(props: {
// Compute quarter set (get unique quarters)
const quarters: Set = new Set();
- props.filteredArtifacts.forEach((artifact: Artifact) => {
+ props.filteredArtifacts.forEach((artifact: ArtifactData) => {
quarters.add(artifact.quarter);
});
+ // Render
return (
{year}
{Array.from(quarters).map((quarter: number) => (
artifact.quarter === quarter
+ filteredArtifacts={props.filteredArtifacts.filter(
+ (artifact: ArtifactData) => artifact.quarter === quarter
)}
/>
))}
diff --git a/src/main.tsx b/src/main.tsx
index 0c9ab67..0fd519c 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,32 +1,37 @@
import "./styles/index.sass";
-import { Artifact } from "./scripts/interfaces";
-import ComingSoon from "./components/ComingSoon";
+import { ArtifactData } from "./scripts/interfaces";
+import ArtifactPage from "./routes/ArtifactPage";
+import ErrorPage from "./routes/ErrorPage";
import ParseData from "./scripts/parse-data";
import React from "react";
import ReactDOM from "react-dom/client";
-import Year from "./components/Year";
+import Root from "./routes/Root";
+// eslint-disable-next-line sort-imports
+import { createBrowserRouter, RouterProvider } from "react-router-dom";
-// Parse data and return as an array of Artifact objects
-const artifacts: Artifact[] = ParseData();
+// Parse data and return as an array of ArtifactPage objects
+const artifacts: ArtifactData[] = ParseData();
-// Compute year set (get unique years)
-const years: Set = new Set();
-artifacts.forEach((artifact: Artifact) => {
- years.add(artifact.year);
-});
+// Create router
+const router = createBrowserRouter([
+ {
+ path: "/honors-portfolio/",
+ element: ,
+ errorElement: ,
+ loader: () => artifacts,
+ },
+ {
+ path: "/honors-portfolio/:id",
+ element: ,
+ errorElement: ,
+ loader: ({ params }) =>
+ artifacts.find((artifact: ArtifactData) => artifact.id === params.id),
+ },
+]);
+// Render
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
-
-
- {/* Artifacts */}
- {Array.from(years).map((year: number) => (
- artifact.year === year
- )}
- />
- ))}
+
);
diff --git a/src/routes/ArtifactPage.tsx b/src/routes/ArtifactPage.tsx
new file mode 100644
index 0000000..baef03d
--- /dev/null
+++ b/src/routes/ArtifactPage.tsx
@@ -0,0 +1,135 @@
+import "../styles/ArtifactPage.sass";
+import { ArtifactData, EmbedData, LinkData } from "../scripts/interfaces";
+import { NavLink, useLoaderData } from "react-router-dom";
+import React, { Fragment } from "react";
+import Gallery from "../components/Gallery";
+import RichLink from "../components/RichLink";
+
+/**
+ * Artifact header, displays artifact title, subtitle, date, and links
+ * @param {{artifact: ArtifactData}} props - Artifact data
+ * @constructor
+ * @returns {JSX.Element}
+ */
+function ArtifactHeader(props: { artifact: ArtifactData }): JSX.Element {
+ const { year, quarter, title, subtitle, links } = props.artifact;
+
+ /**
+ * Convert quarter number encoding to readable string
+ */
+ const quarterToString = (): string => {
+ switch (quarter) {
+ case 0:
+ return "Fall";
+ case 1:
+ return "Winter";
+ case 2:
+ return "Spring";
+ default:
+ return "Summer";
+ }
+ };
+
+ /**
+ * Convert year number encoding to readable string
+ */
+ const yearToString = (): string => {
+ switch (year) {
+ case 0:
+ return "Freshman";
+ case 1:
+ return "Sophomore";
+ case 2:
+ return "Junior";
+ default:
+ return "Senior";
+ }
+ };
+
+ return (
+
+ {/* Title and subtitle */}
+
{title}
+ {subtitle}
+
+ {/* Year and quarter */}
+
+ Quarter: {yearToString()} {quarterToString()}
+
+
+ {/* External Link */}
+ {links &&
+ links.map((link: LinkData) => (
+
+ ))}
+
+ );
+}
+
+/**
+ * Artifact page, displays artifact information including text, images, and embedded media
+ * @constructor
+ * @returns {JSX.Element}
+ */
+export default function ArtifactPage(): JSX.Element {
+ // Get and extract data
+ const artifact = useLoaderData() as ArtifactData;
+
+ // Force scroll to top
+ window.scroll(0, 0);
+
+ // Render
+ return (
+
+ {/* Back button */}
+ {/* skipcq: JS-0394 */}
+
+ ← Return to Kenneth's Honors Portfolio
+
+
+ {/* 3D graphics element */}
+
+
+ {/* Artifact content */}
+
+
+ {/* Text */}
+
{artifact.text}
+
+
+ {/* Images */}
+ {artifact.images &&
}
+
+ {/* Embedded items */}
+ {artifact.embeds && (
+
+ )}
+
+ );
+}
diff --git a/src/routes/ErrorPage.tsx b/src/routes/ErrorPage.tsx
new file mode 100644
index 0000000..0663827
--- /dev/null
+++ b/src/routes/ErrorPage.tsx
@@ -0,0 +1,32 @@
+import { useRouteError } from "react-router-dom";
+
+interface RouteError {
+ statusText?: string;
+ message?: string;
+}
+
+/**
+ * Error / 404 page, displays error information
+ * @param props {fromArtifact: boolean} Whether the error page is being displayed from an artifact page
+ * @constructor
+ * @returns {JSX.Element}
+ */
+export default function ErrorPage(props: {
+ fromArtifact: boolean;
+}): JSX.Element {
+ const error = useRouteError() as RouteError;
+
+ return (
+
+
Oops!
+ {props.fromArtifact ? (
+
It appears this artifact doesn't exist!
+ ) : (
+ <>
+
Sorry, an unexpected error has occurred.
+
{error.statusText || error.message}
+ >
+ )}
+
+ );
+}
diff --git a/src/routes/Root.tsx b/src/routes/Root.tsx
new file mode 100644
index 0000000..c19f44f
--- /dev/null
+++ b/src/routes/Root.tsx
@@ -0,0 +1,36 @@
+import { ArtifactData } from "../scripts/interfaces";
+import ComingSoon from "../components/ComingSoon";
+import Year from "../components/Year";
+import { useLoaderData } from "react-router-dom";
+
+/**
+ * Website root, displays all artifacts and navigation
+ * @constructor
+ * @returns {JSX.Element}
+ */
+export default function Root(): JSX.Element {
+ // Get loader data
+ const artifacts = useLoaderData() as ArtifactData[];
+
+ // Compute year set (get unique years)
+ const years: Set = new Set();
+ artifacts.forEach((artifact: ArtifactData) => {
+ years.add(artifact.year);
+ });
+
+ return (
+ <>
+
+
+ {/* Artifacts */}
+ {Array.from(years).map((year: number) => (
+ artifact.year === year
+ )}
+ />
+ ))}
+ >
+ );
+}
diff --git a/src/scripts/interfaces.ts b/src/scripts/interfaces.ts
index f2669ee..54c2bf4 100644
--- a/src/scripts/interfaces.ts
+++ b/src/scripts/interfaces.ts
@@ -1,15 +1,16 @@
-export interface Artifact {
+export interface ArtifactData {
+ id: string;
year: number;
quarter: number;
title: string;
subtitle: string;
- images?: Image[];
- embeds?: Embed[];
- links?: Link[];
+ images?: ImageData[];
+ embeds?: EmbedData[];
+ links?: LinkData[];
text: string;
}
-export interface Image {
+export interface ImageData {
artifact: string;
width: number;
height: number;
@@ -19,7 +20,7 @@ export interface Image {
image: string;
}
-export interface Link {
+export interface LinkData {
artifact: string;
title: string;
description: string;
@@ -27,7 +28,7 @@ export interface Link {
image: string;
}
-export interface Embed {
+export interface EmbedData {
artifact: string;
url: string;
}
diff --git a/src/scripts/parse-data.ts b/src/scripts/parse-data.ts
index 12cf7e4..7dbe821 100644
--- a/src/scripts/parse-data.ts
+++ b/src/scripts/parse-data.ts
@@ -1,13 +1,13 @@
-import { Artifact, Embed, Image, Link } from "./interfaces";
+import { ArtifactData, EmbedData, ImageData, LinkData } from "./interfaces";
import artifactsString from "../assets/artifacts.tsv?raw";
import embedsString from "../assets/embeds.tsv?raw";
import imagesString from "../assets/images.tsv?raw";
import linksString from "../assets/links.tsv?raw";
// Parsed sub-objects
-const parsedImages: Image[] = [];
-const parsedLinks: Link[] = [];
-const parsedEmbeds: Embed[] = [];
+const parsedImages: ImageData[] = [];
+const parsedLinks: LinkData[] = [];
+const parsedEmbeds: EmbedData[] = [];
/**
* Parse the images.tsv file into `parsedImages`
@@ -86,10 +86,10 @@ const parseEmbeds = (): void => {
/**
* Parse the artifacts.tsv file and integrate sub-objects
- * @returns {Artifact[]} The parsed artifacts
+ * @returns {ArtifactData[]} The parsed artifacts
*/
-export default function ParseData(): Artifact[] {
- const output: Artifact[] = [];
+export default function ParseData(): ArtifactData[] {
+ const output: ArtifactData[] = [];
// Parse sub-objects
parseImages();
@@ -117,7 +117,8 @@ export default function ParseData(): Artifact[] {
] = line.split("\t");
// Start building artifact (fill in required fields)
- const currentArtifact: Artifact = {
+ const currentArtifact: ArtifactData = {
+ id: title.replaceAll(" ", "-").toLowerCase(),
year: parseInt(year),
quarter: parseInt(quarter),
title,
@@ -127,17 +128,17 @@ export default function ParseData(): Artifact[] {
// Add images
currentArtifact.images = hasImages
- ? parsedImages.filter((image: Image) => image.artifact === title)
+ ? parsedImages.filter((image: ImageData) => image.artifact === title)
: undefined;
// Add links
currentArtifact.links = hasLinks
- ? parsedLinks.filter((link: Link) => link.artifact === title)
+ ? parsedLinks.filter((link: LinkData) => link.artifact === title)
: undefined;
// Add embeds
currentArtifact.embeds = hasEmbeds
- ? parsedEmbeds.filter((embed: Embed) => embed.artifact === title)
+ ? parsedEmbeds.filter((embed: EmbedData) => embed.artifact === title)
: undefined;
// Add to output
diff --git a/src/styles/ArtifactCard.sass b/src/styles/ArtifactCard.sass
index 44cf874..9822826 100644
--- a/src/styles/ArtifactCard.sass
+++ b/src/styles/ArtifactCard.sass
@@ -16,6 +16,8 @@ $hover-scale: 1.05
// Text
text-align: center
+ text-decoration: none
+ color: white
// Animation
transition: transform $animation-duration
diff --git a/src/styles/ArtifactPage.sass b/src/styles/ArtifactPage.sass
new file mode 100644
index 0000000..90851de
--- /dev/null
+++ b/src/styles/ArtifactPage.sass
@@ -0,0 +1,32 @@
+.ArtifactPage
+ // Text
+ color: white
+
+.ArtifactPage__return
+ cursor: pointer
+ opacity: 75%
+ color: white
+ text-decoration: none
+
+.ArtifactPage__content
+ display: grid
+ grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr))
+
+.ArtifactPage__content--header
+ margin-right: 1rem
+
+.ArtifactPage__title
+ justify-self: start
+ align-self: end
+
+.ArtifactPage__title h2
+ opacity: 75%
+
+.ArtifactPage__text
+ display: block
+ grid-column: span 2
+
+.ArtifactPage__embeds iframe
+ aspect-ratio: 16 / 9
+ width: 100%
+ border: none
\ No newline at end of file
diff --git a/src/styles/Gallery.sass b/src/styles/Gallery.sass
new file mode 100644
index 0000000..0201816
--- /dev/null
+++ b/src/styles/Gallery.sass
@@ -0,0 +1,11 @@
+.Gallery__thumbnail
+ display: inline-block
+
+ width: 100%
+ max-width: 20rem
+ aspect-ratio: initial
+
+.pswp__dynamic-caption
+ border-radius: 0.5rem
+ background: rgba(0, 0, 0, 0.75)
+ margin: 0.5rem
\ No newline at end of file
diff --git a/src/styles/Quarter.sass b/src/styles/Quarter.sass
index b3fb89e..b093a5d 100644
--- a/src/styles/Quarter.sass
+++ b/src/styles/Quarter.sass
@@ -3,5 +3,5 @@
.quarter__artifacts
display: grid
- grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr))
+ grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr))
grid-gap: 1rem
\ No newline at end of file
diff --git a/src/styles/RichLink.sass b/src/styles/RichLink.sass
new file mode 100644
index 0000000..60503b7
--- /dev/null
+++ b/src/styles/RichLink.sass
@@ -0,0 +1,48 @@
+.RichLink
+ outline: .2rem solid dimgray
+ margin-bottom: 0.5rem
+ border-radius: 0.5rem
+ width: 100%
+ display: flex
+ text-decoration: none
+
+.RichLink__image
+ background-position: center center
+ background-size: cover
+ background-repeat: no-repeat
+ height: 6rem
+ width: 7rem
+ overflow: hidden
+ border-radius: 0.5rem 0 0 0.5rem
+
+.RichLink__text
+ padding: .5rem
+ width: calc(100% - 7rem)
+
+.RichLink__text--title
+ font-size: 1rem
+ color: white
+ margin: 0 0 0.5rem 0
+ white-space: nowrap
+ text-overflow: ellipsis
+ overflow: hidden
+
+
+.RichLink__text--description
+ font-size: 0.9rem
+ color: white
+ opacity: 75%
+ margin: 0
+ display: -webkit-box
+ -webkit-line-clamp: 2
+ -webkit-box-orient: vertical
+ overflow: hidden
+ text-overflow: ellipsis
+
+
+.RichLink__text--href
+ font-size: 14px
+ margin: 0
+ white-space: nowrap
+ text-overflow: ellipsis
+ overflow: hidden
diff --git a/tsconfig.json b/tsconfig.json
index c7b9729..8dc33c6 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,7 +3,7 @@
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
- "allowJs": false,
+ "allowJs": true,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,