Repository: daneden/photos.daneden.me Branch: main Commit: fc206dc7968c Files: 22 Total size: 18.8 KB Directory structure: gitextract_sr7bdbz4/ ├── .eslintrc ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── automerge.yml │ └── main.yml ├── .gitignore ├── .husky/ │ └── .gitignore ├── .prettierrc.js ├── README.md ├── components/ │ ├── App.tsx │ ├── GlobalStyles.tsx │ ├── Header.tsx │ ├── Image.tsx │ └── types.d.ts ├── data/ │ ├── altDescriptions.json │ └── meta.tsx ├── hooks/ │ └── useMatchMedia.ts ├── next-env.d.ts ├── package.json ├── pages/ │ ├── _document.tsx │ └── index.tsx ├── scripts/ │ └── exif.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc ================================================ { "extends": "next" } ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: npm directory: "/" schedule: interval: daily ================================================ FILE: .github/workflows/automerge.yml ================================================ name: Enable automerge for dependabot PRs on: pull_request_target: jobs: merge-me: name: Enable automerge for dependabot PRs runs-on: ubuntu-latest steps: - name: Enable automerge for dependabot PRs uses: daneden/enable-automerge-action@v1 with: github-token: ${{ secrets.PAT }} ================================================ FILE: .github/workflows/main.yml ================================================ name: Create issues from todos on: push: branches: - master jobs: todos: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: todo-actions uses: dtinth/todo-actions@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TODO_ACTIONS_MONGO_URL: ${{ secrets.TODO_ACTIONS_MONGO_URL }} ================================================ FILE: .gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # dependencies node_modules npm-shrinkwrap.json package-lock.json # production build .next # misc .DS_Store npm-debug.log yarn-error.log # ignore generated data package-lock.json data/manifest.ts .now ================================================ FILE: .husky/.gitignore ================================================ _ ================================================ FILE: .prettierrc.js ================================================ module.exports = { semi: false, trailingComma: "es5", } ================================================ FILE: README.md ================================================ # photos.daneden.me A place for my photos to shine (in more glory than Instagram can deliver). ## How does this work? I used the amazing [create-react-app](https://github.com/facebookincubator/create-react-app) as a starting point for this site, and added a few things to make it my own. - Images are uploaded to the `public/images` folder - Next, there's the pre-start script [`exif.js`](https://github.com/daneden/photos.daneden.me/blob/master/scripts/exif.js). This Node script uses `node-exiftool` to loop over each image in the folder and extract exif data. The particular data I wanted to display was the aperture, shutter speed, ISO, and focal length. This data is dropped into `manifest.ts`, which is ignored by git to avoid too many sources of truth (in this case, the images remain the sources of truth). - [`index.tsx`](https://github.com/daneden/photos.daneden.me/blob/master/src/index.tsx) is what imports the data from `manifest.ts` and passes it as props to the images, which are rendered as React components. ================================================ FILE: components/App.tsx ================================================ import { ReactElement, ReactNode } from "react" import altDescriptions from "../data/altDescriptions.json" import GlobalStyles from "./GlobalStyles" import Header from "./Header" import Image, { Props as ImageProps } from "./Image" type Props = { preface?: ReactElement images: Array } function Preface({ children }: { children: ReactNode }): ReactElement { return ( <>
{children}
) } function App(props: Props): ReactElement { return ( <>
{props.preface}
{props.images.map((img, i) => ( {altDescriptions[img.name]} ))}
) } export default App ================================================ FILE: components/GlobalStyles.tsx ================================================ export default function GlobalStyles() { return ( ) } ================================================ FILE: components/Header.tsx ================================================ import { ReactElement } from "react" const Header = (): ReactElement => { return (

photos.daneden.me

) } export default Header ================================================ FILE: components/Image.tsx ================================================ import NextImage from "next/image" import * as React from "react" const { useEffect, useState } = React export type Props = { aspectRatio: number camera: string alt: string focalLength: string fStop: number iso: number name: string speed: string width: number height: number } function Image(props: Props) { const [imageLoaded, setImageLoaded] = useState(false) const { aspectRatio, camera, alt: description, fStop, focalLength, iso, name, width, height, } = props const url = `/images/${name}` const image = ( setImageLoaded(true)} src={url} width={width} /> ) const speed = // If the shutter speed is a fraction, we want to style it appropriately. props.speed.includes("/") ? ( {props.speed} ) : ( props.speed ) return ( <>
{image}

{camera}, {`\u0192${fStop}, `} {speed} sec, {focalLength}, ISO {iso}

) } export default Image ================================================ FILE: components/types.d.ts ================================================ declare module "*.woff2" declare module "*.woff" ================================================ FILE: data/altDescriptions.json ================================================ { "00013.jpg": "A man standing in a warmly-lit room and taking a photograph of something out of frame. Behind him is a long, daylight-drenched and spacious corridor. Close by, a woman stares at something else out of frame, and in the very far distance, green trees can be seen through a frosted window.", "00015.jpg": "A woman crouches near a body of water at sunrise to take a photograph of a city in the distance.", "00016.jpg": "The interior of Chicago’s Natural History Museum, looking down on tourists in the atrium. A group of people stand around a Tyrannosaurus Rex skeleton, looking at a map.", "00021.jpg": "A brown horse eating grass on a sunny day. In the distant background, an earthy hill is peppered with small buildings, just beyond a shallow body of water.", "00028.jpg": "", "00030.jpg": "", "00031.jpg": "", "00034.jpg": "", "00035.jpg": "", "00036.jpg": "", "00037.jpg": "", "00039.jpg": "", "00041.jpg": "", "00043.jpg": "", "00045.jpg": "", "00046.jpg": "", "00047.jpg": "", "00048.jpg": "", "00049.jpg": "", "00050.jpg": "", "00051.jpg": "", "00052.jpg": "", "00053.jpg": "", "00054.jpg": "The exterior of a modern residential building in Barcelona, with concrete balconies overflowing with vibrant green plants.", "00055.jpg": "The interior of London’s Natural History Museum’s atrium. Light cascades in through windows in the ceiling; a blue whale’s skeleton is in the center of the frame, with dozens of people walking around the atrium and looking at the exhibits.", "00056.jpg": "A field of wildflowers; vibrant spots of red poppies, yellow, lilac, and purple flowers pepper a green field." } ================================================ FILE: data/meta.tsx ================================================ const siteInfo = { title: "Dan Eden \u2014 Photos", description: "Photography by Daniel Eden, a designer living in Manchester, UK.", fullDescription: ( <>

Dan Eden is a Designer from Manchester, England. He prefers to talk in the first person.

Amongst thousands of photos, it’s easy to lose track of my favorites. This little website serves as a home for the photos I’m most proud of.

You can follow me on Twitter and{" "} Instagram.

), image: "https://photos.daneden.me/images/00013.jpg", } export default siteInfo ================================================ FILE: hooks/useMatchMedia.ts ================================================ import { useEffect, useState } from "react" export default function useMatchMedia(query) { const mq = window.matchMedia !== undefined ? window.matchMedia(query) : null const [matches, setMatches] = useState(mq !== null ? mq.matches : false) useEffect(() => { function updateMqMatches(mediaQueryList) { setMatches(mediaQueryList.matches) } if (mq !== null) { mq.addListener(updateMqMatches) return () => { mq.removeListener(updateMqMatches) } } }, [mq]) return matches } ================================================ FILE: next-env.d.ts ================================================ /// /// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. ================================================ FILE: package.json ================================================ { "name": "photos", "version": "0.0.1", "private": true, "devDependencies": { "@types/node": "^18.7.14", "@types/react": "^18.0.18", "@types/react-dom": "^18.0.6", "eslint": "^8.52.0", "eslint-config-next": "^14.0.0", "husky": "8.0.1", "lint-staged": "13.0.3", "prettier": "2.7.1", "typescript": "^5.2.2" }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "*.{js,json,css,md}": [ "prettier --write" ] }, "dependencies": { "dist-exiftool": "10.53.0", "next": "^14.0.0", "node-exiftool": "2.3.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "scripts": { "prestart": "node ./scripts/exif.js", "prebuild": "node ./scripts/exif.js", "dev": "next", "build": "next build", "start": "next start" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } ================================================ FILE: pages/_document.tsx ================================================ import Document, { Head, Html, Main, NextScript } from "next/document" class MyDocument extends Document { render() { return (
) } } export default MyDocument ================================================ FILE: pages/index.tsx ================================================ import Head from "next/head" import React, { useEffect } from "react" import App from "../components/App" import imageData from "../data/manifest" import siteInfo from "../data/meta" const images = imageData.slice().reverse() function HomePage() { useEffect(() => { let content: HTMLElement = document.body window.addEventListener("mousewheel", scrollHandler) function scrollHandler(e) { if (content === undefined) { content = document.body } else { content.scrollLeft += e.deltaY content.setAttribute("style", `--scroll-delta: ${content.scrollLeft}`) } } return () => { window.removeEventListener("mousewheel", scrollHandler) } }) return ( <> {siteInfo.title} ) } export default HomePage ================================================ FILE: scripts/exif.js ================================================ "use strict" const fs = require("fs") const exiftool = require("node-exiftool") const exiftoolBin = require("dist-exiftool") const ep = new exiftool.ExiftoolProcess(exiftoolBin) const CAMERAS = { SONY: { "ILCE-6000": "Sony a6000", }, "LEICA CAMERA AG": { "LEICA Q (Typ 116)": "Leica Q", "LEICA SL2-S": "Leica SL2-S", }, } async function createManifestFromExifData(exifData) { let fileInfo = [] // Transform the data to remove all but the info we care about exifData.data.forEach(async (datum) => { // The aspect ratio here is actually in terms of // height:width (instead of typical width:height) // since they all have a fixed height relative to the // viewport const [width, height] = datum.ImageSize.split("x").map((n) => parseInt(n)) const aspectRatio = [width, height].reduce((w, h) => w / h) const info = { aspectRatio, camera: CAMERAS[datum.Make][datum.Model] ?? datum.Make, fStop: datum.FNumber || 16, name: datum.FileName, // I only have one manual lens, but this ternary is a hacky workaround. focalLength: datum.FocalLength ? datum.FocalLength.replace(" ", "") : "12mm", iso: datum.ISO, speed: String(datum.ShutterSpeed), alt: datum.Description || "", width, height, } fileInfo.push(info) }) // Sort the image data by filename fileInfo.sort((a, b) => { let keyA = parseInt(a.name.split(".")[0]), keyB = parseInt(b.name.split(".")[0]) if (keyA < keyB) return -1 if (keyA > keyB) return 1 return 0 }) // Write data to file for the app to consume let writeString = `import { Props as ImageData } from "../components/Image" const imageData: Array = ${JSON.stringify(fileInfo, null, " ")} export default imageData` fs.writeFile("./data/manifest.ts", writeString, (err) => { if (err) return console.log(err) }) } ep.open() .then((pid) => { console.log(`🏁 Started exiftool process (PID: ${pid})`) console.log("📸 Extracting photo metadata...") return ep .readMetadata("./public/images/") .then(async (res) => { await createManifestFromExifData(res) }) .catch((error) => { console.log("Error: ", error) }) }) .then(() => { return ep.close().then(() => { console.log("✅ Metadata extracted! Closing exiftool.") }) }) .catch((error) => { console.error("🚨 Error extracting photo metadata!", error) }) ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": false, "forceConsistentCasingInFileNames": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "preserve", "incremental": true }, "exclude": [ "node_modules" ], "include": [ "next-env.d.ts", "**/*.ts", "**/*.tsx" ] }