diff --git a/src/components/big_picture.tsx b/src/components/big_picture.tsx index 1c546a2..959a4ce 100644 --- a/src/components/big_picture.tsx +++ b/src/components/big_picture.tsx @@ -1,7 +1,6 @@ -import * as Model from "../model"; -import { Picture } from "./picture"; - import * as React from "react"; +import * as Model from "model"; +import { Picture } from "components/picture"; export interface Props { image: Model.Image; @@ -16,93 +15,99 @@ interface TouchStart { y: number; } -export interface State { - touchStart?: TouchStart | null; -} +export const BigPicture: React.FC = ({ + image, + onClose, + showNext, + showPrevious, + width, +}) => { + const [touchStart, setTouchStart] = React.useState(null); -export class BigPicture extends React.PureComponent { - static displayName = "BigPicture"; - - componentDidMount() { - window.addEventListener("keyup", this._onEscape as any); - window.addEventListener("touchstart", this._onTouchStart as any); - window.addEventListener("touchend", this._onTouchEnd as any); - document.body.classList.add("no-scroll"); - } - - componentWillUnmount() { - window.removeEventListener("keyup", this._onEscape as any); - window.removeEventListener("touchstart", this._onTouchStart as any); - window.removeEventListener("touchend", this._onTouchEnd as any); - document.body.classList.remove("no-scroll"); - } - - render() { - const scaleWidth = this.props.image.width / this.props.width; - const scaleHeight = this.props.image.height / (window.innerHeight - 80); - const scale = Math.max(scaleWidth, scaleHeight); - - return ( -
- {}} - height={this.props.image.height / scale} - width={this.props.image.width / scale} - /> -
- - ⬇ Download - - - Close - -
-
- ); - } - - private _keyPress = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - this.props.onClose(); - } - }; - - private _onEscape = (e: React.KeyboardEvent) => { - if (e.key === "Escape") { - this.props.onClose(); - } - }; - - private _onTouchStart = (e: React.TouchEvent) => { - const touch = e.touches[0]; - this.setState({ touchStart: { x: touch.screenX, y: touch.screenY } }); - }; - - private _onTouchEnd = (e: React.TouchEvent) => { - const touch = e.changedTouches[0]; - const touchStart = this.state.touchStart as TouchStart; - - const dx = touch.screenX - touchStart.x; - - if (Math.abs(dx) / window.innerWidth > 0.05) { - if (dx < 0) { - this.props.showNext(); - } else { - this.props.showPrevious(); + const onEscape = React.useCallback( + (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); } - } + }, + [onClose] + ); - this.setState({ touchStart: null }); - }; -} + const onTouchStart = React.useCallback((e: TouchEvent) => { + const touch = e.touches[0]; + setTouchStart({ x: touch.screenX, y: touch.screenY }); + }, []); + + const onTouchEnd = React.useCallback( + (e: TouchEvent) => { + if (!touchStart) { + return; + } + const touch = e.changedTouches[0]; + const dx = touch.screenX - touchStart.x; + + if (Math.abs(dx) / window.innerWidth > 0.05) { + if (dx < 0) { + showNext(); + } else { + showPrevious(); + } + } + + setTouchStart(null); + }, + [showNext, showPrevious, touchStart] + ); + + React.useEffect(() => { + window.addEventListener("keyup", onEscape); + window.addEventListener("touchstart", onTouchStart); + window.addEventListener("touchend", onTouchEnd); + document.body.classList.add("no-scroll"); + + return () => { + window.removeEventListener("keyup", onEscape); + window.removeEventListener("touchstart", onTouchStart); + window.removeEventListener("touchend", onTouchEnd); + document.body.classList.remove("no-scroll"); + }; + }, [onEscape, onTouchEnd, onTouchStart]); + + const scaleWidth = image.width / width; + const scaleHeight = image.height / (window.innerHeight - 80); + const scale = Math.max(scaleWidth, scaleHeight); + + return ( +
+ {}} + height={image.height / scale} + width={image.width / scale} + /> +
+ + ⬇ Download + + { + if (e.key === "Enter") { + onClose(); + } + }} + tabIndex={0} + > + Close + +
+
+ ); +}; diff --git a/src/components/grid.tsx b/src/components/grid.tsx index 40476a3..bfe816f 100644 --- a/src/components/grid.tsx +++ b/src/components/grid.tsx @@ -1,7 +1,6 @@ -import { Picture } from "./picture"; -import * as Model from "../model"; - import * as React from "react"; +import * as Model from "model"; +import { Picture } from "components/picture"; export interface Props { images: Model.Image[]; @@ -24,72 +23,60 @@ interface BadList { badness: number; } -export class Grid extends React.PureComponent { - static displayName: string = "Grid"; +const badness = (row: Model.Image[], width: number, height: number): number => { + const rowWidth = row.reduce((w, img) => w + img.width / img.height, 0); + const rowHeight = width / rowWidth; - private gridHeight = 0; + return (rowHeight - height) * (rowHeight - height); +}; - static badness = ( - row: Model.Image[], - width: number, - height: number - ): number => { - const rowWidth = row.reduce((w, img) => w + img.width / img.height, 0); - const rowHeight = width / rowWidth; +export const Grid: React.FC = ({ + images, + onImageSelected, + pageBottom, + width, + height, +}) => { + const rowsMemo = React.useRef>>(new Map()); + const targetRowHeight = width > 900 ? ROW_HEIGHT : MOBILE_ROW_HEIGHT; - return (rowHeight - height) * (rowHeight - height); - }; - - // [width][idx] -> badness - private rowsMemo: Map> = new Map(); - - componentDidUpdate(prevProps: Props) { + React.useEffect(() => { // The memoized rows depend on the image list; clear when the set changes - if (prevProps.images !== this.props.images) { - this.rowsMemo.clear(); - } - } + rowsMemo.current.clear(); + }, [images]); - rows(idx: number): BadList { - const targetHeight = this._rowHeight(); - - const memo = this.rowsMemo.get(this.props.width) ?? new Map(); + const rowsFor = (idx: number): BadList => { + const memo = rowsMemo.current.get(width) ?? new Map(); const maybeMemo = memo.get(idx); if (maybeMemo) { return maybeMemo; } - if (idx === this.props.images.length) { + if (idx === images.length) { return { splits: [], badness: 0, }; } - if (idx === this.props.images.length - 1) { - const img = this.props.images[idx]; - const h = (img.height * this.props.width) / img.width; + if (idx === images.length - 1) { + const img = images[idx]; + const h = (img.height * width) / img.width; return { splits: [], - badness: (targetHeight - h) * (targetHeight - h), + badness: (targetRowHeight - h) * (targetRowHeight - h), }; } - let bestIdx = -1; let leastBad = 1e50; let bestSplits: number[] = []; - for (let i = idx + 1; i <= this.props.images.length; i++) { - const rowBadness = Grid.badness( - this.props.images.slice(idx, i), - this.props.width, - targetHeight - ); - const rest = this.rows(i); - const badness = rest.badness + rowBadness; - if (badness < leastBad) { - leastBad = badness; - bestIdx = i; + for (let i = idx + 1; i <= images.length; i++) { + const rowBadness = badness(images.slice(idx, i), width, targetRowHeight); + const rest = rowsFor(i); + const totalBadness = rest.badness + rowBadness; + if (totalBadness < leastBad) { + leastBad = totalBadness; bestSplits = [i, ...rest.splits]; } } @@ -101,60 +88,57 @@ export class Grid extends React.PureComponent { memo.set(idx, badList); - if (!this.rowsMemo.has(this.props.width)) { - this.rowsMemo.set(this.props.width, memo); + if (!rowsMemo.current.has(width)) { + rowsMemo.current.set(width, memo); } return badList; - } + }; - render() { - this.gridHeight = 0; + let gridHeight = 0; - const badList = this.rows(0); - let lastBreak = 0; - const rows: Row[] = badList.splits.map((split) => { - const images = this.props.images.slice(lastBreak, split); - lastBreak = split; + const badList = rowsFor(0); + let lastBreak = 0; + const rows: Row[] = badList.splits.map((split) => { + const slice = images.slice(lastBreak, split); + lastBreak = split; - return { - images, - width: images.reduce((acc, img) => acc + img.width / img.height, 0), - }; - }); + return { + images: slice, + width: slice.reduce((acc, img) => acc + img.width / img.height, 0), + }; + }); - const images = rows.map((row) => { - const height = Math.min(this.props.height, this.props.width / row.width); - - const pics = row.images.map((image) => { - return ( - this.props.onImageSelected(image)} - key={image.src} - height={height} - width={(image.width / image.height) * height} - defer={this.gridHeight > this.props.pageBottom} - /> - ); - }); - - this.gridHeight += height; + const pictures = rows.map((row) => { + const rowHeight = Math.min(height, width / row.width); + const pics = row.images.map((image) => { + const scaledWidth = (image.width / image.height) * rowHeight; + const defer = gridHeight > pageBottom; return ( -
image.src).join(",")} - > - {pics} -
+ onImageSelected(image)} + key={image.src} + height={rowHeight} + width={scaledWidth} + defer={defer} + /> ); }); - return
{images}
; - } + gridHeight += rowHeight; - private _rowHeight = (): number => - this.props.width > 900 ? ROW_HEIGHT : MOBILE_ROW_HEIGHT; -} + return ( +
image.src).join(",")} + > + {pics} +
+ ); + }); + + return
{pictures}
; +}; diff --git a/src/components/image_set.tsx b/src/components/image_set.tsx index e784ab7..cd314ff 100644 --- a/src/components/image_set.tsx +++ b/src/components/image_set.tsx @@ -1,61 +1,48 @@ -import { Grid } from "./grid"; -import * as Model from "../model"; - import * as React from "react"; +import * as Model from "model"; +import { Grid } from "components/grid"; export interface Props { imageSet: Model.ImageSet; onImageSelected: (img: Model.Image) => void; onShowHome: () => void; - setGridHeight: (height: number) => void; pageBottom: number; width: number; height: number; } -export class ImageSet extends React.PureComponent { - static displayName = "ImageSet"; - - private divRef = React.createRef(); - - render() { - return ( -
-

- - {this.props.imageSet.location} - - - {this.props.imageSet.description} - -

- - +export const ImageSet: React.FC = ({ + imageSet, + onImageSelected, + onShowHome, + pageBottom, + width, + height, +}) => { + return ( +
+

+ {imageSet.location} + {imageSet.description} +

+ + - ); - } - - componentDidMount() { - this._setGridHeight(); - } - - componentDidUpdate() { - this._setGridHeight(); - } - - private _setGridHeight = () => { - if (this.divRef.current) { - this.props.setGridHeight(this.divRef.current.clientHeight); - } - }; -} +
+ ); +}; diff --git a/src/components/picture.tsx b/src/components/picture.tsx index 18b308f..3d08b75 100644 --- a/src/components/picture.tsx +++ b/src/components/picture.tsx @@ -1,6 +1,5 @@ -import * as Model from "../model"; - import * as React from "react"; +import * as Model from "model"; export interface Props { image: Model.Image; @@ -10,10 +9,6 @@ export interface Props { defer?: boolean; } -export interface State { - isMounted: boolean; -} - interface SrcSetInfo { jpeg: string; webp: string; @@ -21,46 +16,20 @@ interface SrcSetInfo { bestSrc: string; } -export class Picture extends React.PureComponent { - static displayName = "Picture"; +export const Picture: React.FC = ({ + image, + onClick, + height, + width, + defer, +}) => { + const [isMounted, setIsMounted] = React.useState(false); - state: State = { - isMounted: false, - }; + React.useEffect(() => { + setIsMounted(true); + }, []); - componentDidMount() { - this.setState({ isMounted: true }); - } - - render() { - if (this.props.defer || !this.state.isMounted) { - return ( -
- ); - } - - const srcSet = this._srcset(); - - return ( - - - - - - - ); - } - - private _srcset = (): SrcSetInfo => { + const srcSet = React.useMemo(() => { const jpegSrcSet: string[] = []; const webpSrcSet: string[] = []; const avifSrcSet: string[] = []; @@ -68,15 +37,13 @@ export class Picture extends React.PureComponent { let bestScale = Infinity; Model.SIZES.forEach((size) => { - const width = - this.props.image.width > this.props.image.height - ? size - : (this.props.image.width / this.props.image.height) * size; + const derivedWidth = + image.width > image.height ? size : (image.width / image.height) * size; - const scale = width / this.props.width; + const scale = derivedWidth / width; if (scale >= 1 || size === 2400) { - const jpeg = `img/${size}/${this.props.image.src}`; + const jpeg = `img/${size}/${image.src}`; const webp = jpeg.replace("jpg", "webp"); const avif = jpeg.replace("jpg", "avif"); jpegSrcSet.push(`${jpeg} ${scale}x`); @@ -93,7 +60,27 @@ export class Picture extends React.PureComponent { jpeg: jpegSrcSet.join(","), webp: webpSrcSet.join(","), avif: avifSrcSet.join(","), - bestSrc: `img/${bestSize}/${this.props.image.src}`, + bestSrc: `img/${bestSize}/${image.src}`, }; - }; -} + }, [image, width]); + + if (defer || !isMounted) { + return
; + } + + return ( + + + + + + + ); +}; diff --git a/src/components/root.tsx b/src/components/root.tsx index 2526ba0..21de8fe 100644 --- a/src/components/root.tsx +++ b/src/components/root.tsx @@ -1,235 +1,255 @@ -import { BigPicture } from "./big_picture"; -import { ImageSet } from "./image_set"; -import { SetCover } from "./set_cover"; -import * as Model from "../model"; - import * as React from "react"; +import * as Model from "model"; +import { BigPicture } from "components/big_picture"; +import { ImageSet } from "components/image_set"; +import { SetCover } from "components/set_cover"; export interface Props {} -export interface State { - data?: Model.Data | null; - selectedImage?: Model.Image | null; - selectedSet?: Model.ImageSet | null; - gridHeights: number[]; - pageBottom: number; - width: number; - height: number; -} +const viewWidth = (): number => { + const widths = [ + window.innerWidth, + window.outerWidth, + document.documentElement?.clientWidth, + document.body?.clientWidth, + ].filter((w): w is number => typeof w === "number" && w > 0 && isFinite(w)); -export class Root extends React.PureComponent { - static displayName = "Root"; + return widths.length > 0 ? Math.max(...widths) : 0; +}; - // innerWidth gets messed up when rotating phones from landscape -> portrait, - // and chrome seems to not report innerWidth correctly when scrollbars are present - private _viewWidth = (): number => { - const widths = [ - window.innerWidth, - window.outerWidth, - document.documentElement?.clientWidth, - document.body?.clientWidth, - ].filter((w): w is number => typeof w === "number" && w > 0 && isFinite(w)); +const viewHeight = (): number => { + const heights = [ + window.innerHeight, + window.outerHeight, + document.documentElement?.clientHeight, + document.body?.clientHeight, + ].filter((h): h is number => typeof h === "number" && h > 0 && isFinite(h)); - // Use the largest reasonable value to avoid shrinking the grid when a single - // measurement source temporarily reports something tiny. - return widths.length > 0 ? Math.max(...widths) : 0; - }; + return heights.length > 0 ? Math.max(...heights) : 0; +}; - private _viewHeight = (): number => { - const heights = [ - window.innerHeight, - window.outerHeight, - document.documentElement?.clientHeight, - document.body?.clientHeight, - ].filter((h): h is number => typeof h === "number" && h > 0 && isFinite(h)); +const formatHash = (set: Model.ImageSet) => + set.location.replace(/[^a-zA-Z0-9-_]/g, "-") + + "-" + + set.description.replace(/[^a-zA-Z0-9-_]/g, "-"); - return heights.length > 0 ? Math.max(...heights) : 0; - }; +export const Root: React.FC = () => { + const [data, setData] = React.useState(null); + const [selectedImage, setSelectedImage] = React.useState( + null + ); + const [selectedSet, setSelectedSet] = React.useState( + null + ); + const [dimensions, setDimensions] = React.useState(() => { + const height = viewHeight(); + return { + pageBottom: height + window.pageYOffset, + width: viewWidth(), + height, + }; + }); - state: State = { - gridHeights: [], - pageBottom: this._viewHeight() + window.pageYOffset, - width: this._viewWidth(), - height: this._viewHeight(), - }; + const updateView = React.useCallback(() => { + const height = viewHeight(); + setDimensions({ + pageBottom: height + window.pageYOffset, + width: viewWidth(), + height, + }); + }, []); - componentDidMount() { + const loadHash = React.useCallback(() => { + if (window.location.hash.length === 0) { + setSelectedImage(null); + setSelectedSet(null); + return; + } + + if (!data) { + return; + } + + const hash = window.location.hash.slice(1); + let nextImage: Model.Image | null = null; + let nextSet: Model.ImageSet | null = null; + + data.sets.forEach((set) => { + if (formatHash(set) === hash) { + nextSet = set; + } + + const image = set.images.find((img) => img.src === hash); + if (image) { + nextImage = image; + nextSet = set; + } + }); + + setSelectedImage(nextImage); + setSelectedSet(nextSet); + }, [data]); + + React.useEffect(() => { + let isMounted = true; window .fetch(Model.dataUrl) - .then((data) => data.json()) - .then((json) => this.setState({ data: json })) - .then(this._loadHash) - .then(this._onViewChange) + .then((response) => response.json()) + .then((json) => { + if (isMounted) { + setData(json); + } + }) .catch((e) => console.error("Error fetching data", e)); - window.onresize = this._onViewChange; - window.onscroll = this._onViewChange; + return () => { + isMounted = false; + }; + }, []); - try { - screen.orientation.onchange = this._onViewChange; - } catch (e) {} + React.useEffect(() => { + if (data) { + loadHash(); + } + }, [data, loadHash]); - try { - window.onorientationchange = this._onViewChange; - } catch (e) {} + React.useEffect(() => { + const handlePopState = () => loadHash(); - window.onpopstate = this._loadHash; - } + window.addEventListener("resize", updateView); + window.addEventListener("scroll", updateView, { passive: true }); + window.addEventListener("orientationchange", updateView); + window.addEventListener("popstate", handlePopState); - private _renderSet(set: Model.ImageSet) { - return ( - - ); - } + const orientation = (screen as any).orientation; + if (orientation?.addEventListener) { + orientation.addEventListener("change", updateView); + } + + updateView(); + + return () => { + window.removeEventListener("resize", updateView); + window.removeEventListener("scroll", updateView); + window.removeEventListener("orientationchange", updateView); + window.removeEventListener("popstate", handlePopState); + if (orientation?.removeEventListener) { + orientation.removeEventListener("change", updateView); + } + }; + }, [loadHash, updateView]); + + React.useEffect(() => { + if (selectedSet) { + document.title = + selectedSet.location + + " – " + + selectedSet.description + + " – Skiing - Aaron Gutierrez"; + } else { + document.title = "Skiing - Aaron Gutierrez"; + } + }, [selectedSet]); + + const onImageSelected = React.useCallback( + (img: Model.Image) => { + if (selectedImage) { + window.history.replaceState(null, "", `#${img.src}`); + } else { + window.history.pushState(null, "", `#${img.src}`); + } + setSelectedImage(img); + }, + [selectedImage] + ); + + const onSetSelected = React.useCallback((set: Model.ImageSet) => { + setSelectedSet(set); + window.history.pushState(null, "", `#${formatHash(set)}`); + }, []); + + const onHomeSelected = React.useCallback(() => { + setSelectedSet(null); + setSelectedImage(null); + window.history.pushState(null, "", "#"); + }, []); + + const showGrid = React.useCallback(() => { + setSelectedImage(null); + window.history.go(-1); + if (selectedSet) { + onSetSelected(selectedSet); + } + }, [onSetSelected, selectedSet]); + + const showNextBigPicture = React.useCallback(() => { + if (!selectedSet || !selectedImage) { + return; + } + const images: Model.Image[] = selectedSet.images; + const current = images.indexOf(selectedImage); + const next = current + 1 >= images.length ? 0 : current + 1; + onImageSelected(images[next]); + }, [onImageSelected, selectedImage, selectedSet]); + + const showPreviousBigPicture = React.useCallback(() => { + if (!selectedSet || !selectedImage) { + return; + } + const images: Model.Image[] = selectedSet.images; + const current = images.indexOf(selectedImage); + const previous = current - 1 < 0 ? images.length - 1 : current - 1; + onImageSelected(images[previous]); + }, [onImageSelected, selectedImage, selectedSet]); + + const renderSets = () => { + if (!data) { + return null; + } + if (selectedSet) { + return ( + + ); + } - private _renderSetCovers(sets: Model.ImageSet[]) { return (
- {sets.map((set) => ( + {data.sets.map((set) => ( { - this._onSetSelected(set); + onSetSelected(set); scrollTo(0, 0); }} - width={Math.min(this.state.width, 400)} + width={Math.min(dimensions.width, 400)} /> ))}
); - } - - render() { - const imageSets = this.state.data - ? this.state.selectedSet - ? this._renderSet(this.state.selectedSet) - : this._renderSetCovers(this.state.data.sets) - : null; - - return ( -
- {this._bigPicture()} -

Aaron's Ski Pictures

- {imageSets} -
- ); - } - - private _bigPicture = () => - this.state.selectedImage ? ( - - ) : null; - - private _loadHash = () => { - if (window.location.hash.length > 0 && this.state.data) { - const hash = window.location.hash.slice(1); - - let selectedImage: Model.Image | null = null; - let selectedSet: Model.ImageSet | null = null; - - this.state.data.sets.forEach((set) => { - if (this._setToHash(set) === hash) { - selectedSet = set; - } - - const image = set.images.find((image) => image.src === hash); - if (image) { - selectedImage = image; - selectedSet = set; - } - }); - - this.setState({ selectedImage, selectedSet }); - } else { - this.setState({ selectedImage: null, selectedSet: null }); - } }; - private _onViewChange = () => { - this.setState({ - pageBottom: this._viewHeight() + window.pageYOffset, - width: this._viewWidth(), - height: this._viewHeight(), - }); - }; - - private _onImageSelected = (img: Model.Image) => { - if (this.state.selectedImage) { - window.history.replaceState(null, "", `#${img.src}`); - } else { - window.history.pushState(null, "", `#${img.src}`); - } - this.setState({ selectedImage: img }); - }; - - private _onSetSelected = (set: Model.ImageSet) => { - this.setState({ selectedSet: set }); - document.title = - set.location + " – " + set.description + " – Skiing - Aaron Gutierrez"; - window.history.pushState(null, "", `#${this._setToHash(set)}`); - }; - - private _onHomeSelected = () => { - this.setState({ - selectedSet: null, - selectedImage: null, - }); - window.history.pushState(null, "", "#"); - document.title = "Skiing - Aaron Gutierrez"; - }; - - private _setToHash = (set: Model.ImageSet) => - set.location.replace(/[^a-zA-Z0-9-_]/g, "-") + - "-" + - set.description.replace(/[^a-zA-Z0-9-_]/g, "-"); - - private _showGrid = () => { - this.setState({ selectedImage: null }); - window.history.go(-1); - this._onSetSelected(this.state.selectedSet as Model.ImageSet); - }; - - private _showNextBigPicture = () => { - const images: Model.Image[] = this.state.selectedSet - ?.images as Model.Image[]; - const current = images.indexOf(this.state.selectedImage as Model.Image); - const next = current + 1 >= images.length ? 0 : current + 1; - this._onImageSelected(images[next]); - }; - - private _showPreviousBigPicture = () => { - const images: Model.Image[] = this.state.selectedSet - ?.images as Model.Image[]; - const current = images.indexOf(this.state.selectedImage as Model.Image); - const previous = current - 1 < 0 ? images.length - 1 : current - 1; - this._onImageSelected(images[previous]); - }; - - private _setGridHeight = (grid: number) => (height: number) => { - if (this.state.gridHeights[grid] === height) { - return; - } - this.setState((state) => { - const newGridHeights = [...state.gridHeights]; - newGridHeights[grid] = height; - - return { gridHeights: newGridHeights }; - }); - }; -} + return ( +
+ {selectedImage ? ( + + ) : null} +

Aaron's Ski Pictures

+ {renderSets()} +
+ ); +}; diff --git a/src/components/set_cover.tsx b/src/components/set_cover.tsx index 2d444a1..6687711 100644 --- a/src/components/set_cover.tsx +++ b/src/components/set_cover.tsx @@ -1,7 +1,6 @@ -import { Picture } from "./picture"; -import * as Model from "../model"; - import * as React from "react"; +import * as Model from "model"; +import { Picture } from "components/picture"; export interface Props { imageSet: Model.ImageSet; @@ -9,40 +8,30 @@ export interface Props { width: number; } -export interface State {} +export const SetCover: React.FC = ({ imageSet, onClick, width }) => { + const coverImage = imageSet.images[0]; + const isTall = coverImage.height > coverImage.width; -export class SetCover extends React.PureComponent { - static displayName = "SetCover"; + const height = isTall + ? width + : (coverImage.height / coverImage.width) * width; - render() { - const image = this.props.imageSet.images[0]; - const isTall = image.height > image.width; + const normalizedWidth = isTall + ? (coverImage.width / coverImage.height) * width + : width; - const height = isTall - ? this.props.width - : (image.height / image.width) * this.props.width; - - const width = isTall - ? (image.width / image.height) * this.props.width - : this.props.width; - - return ( -
- {}} - height={height} - width={width} - /> -

- - {this.props.imageSet.location} - - - {this.props.imageSet.description} - -

-
- ); - } -} + return ( +
+ {}} + height={height} + width={normalizedWidth} + /> +

+ {imageSet.location} + {imageSet.description} +

+
+ ); +}; diff --git a/src/index.tsx b/src/index.tsx index ee3c12a..35b3f1e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,4 @@ -import { Root } from "./components/root"; +import { Root } from "components/root"; import { createRoot } from "react-dom/client"; diff --git a/tsconfig.json b/tsconfig.json index c91c739..2097a84 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "outDir": "./dist/", - "baseUrl": ".", + "baseUrl": "./src", "sourceMap": true, "noImplicitAny": true, "strictNullChecks": true, diff --git a/webpack.config.js b/webpack.config.js index ae36061..eb337b6 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -26,7 +26,8 @@ module.exports = (env) => { resolve: { // Add '.ts' and '.tsx' as resolvable extensions. - extensions: [".ts", ".tsx", ".js", ".json"] + extensions: [".ts", ".tsx", ".js", ".json"], + modules: [path.resolve(__dirname, "src"), "node_modules"], }, module: {