groups
This commit is contained in:
47
site.css
47
site.css
@@ -4,16 +4,24 @@ body {
|
|||||||
color: #335;
|
color: #335;
|
||||||
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Helvetica,Arial,sans-serif;
|
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Helvetica,Arial,sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow-x: hidden;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.no-scroll {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
color: #69c;
|
color: #69c;
|
||||||
|
cursor: pointer;
|
||||||
font-size: 45px;
|
font-size: 45px;
|
||||||
font-weight: lighter;
|
font-weight: lighter;
|
||||||
line-height: 60px;
|
line-height: 60px;
|
||||||
@@ -32,15 +40,35 @@ h2 {
|
|||||||
margin-left: 30px;
|
margin-left: 30px;
|
||||||
margin-right: 30px;
|
margin-right: 30px;
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ImageSet h2 {
|
||||||
border-bottom: 1px solid #eef;
|
border-bottom: 1px solid #eef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Root-setCovers {
|
||||||
|
display: grid;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SetCover {
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: end;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.ImageSet-location,
|
.ImageSet-location,
|
||||||
.ImageSet-description {
|
.ImageSet-description,
|
||||||
|
.SetCover-location,
|
||||||
|
.SetCover-description {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ImageSet-location:after {
|
.ImageSet-location:after,
|
||||||
|
.SetCover-location:after {
|
||||||
content: " · ";
|
content: " · ";
|
||||||
white-space: break-spaces;
|
white-space: break-spaces;
|
||||||
}
|
}
|
||||||
@@ -100,9 +128,20 @@ h2 {
|
|||||||
color: #ccf;
|
color: #ccf;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
.ImageSet h2 {
|
||||||
border-color: #335;
|
border-color: #335;
|
||||||
color: #bbb;
|
color: #bbb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
.Root-setCovers {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1400px) {
|
||||||
|
.Root-setCovers {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ export class BigPicture extends React.PureComponent<Props, {}> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
window.addEventListener("keyup", this._onEscape as any);
|
window.addEventListener("keyup", this._onEscape as any);
|
||||||
|
document.body.classList.add("no-scroll");
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener("keyup", this._onEscape as any);
|
window.removeEventListener("keyup", this._onEscape as any);
|
||||||
|
document.body.classList.remove("no-scroll");
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ interface Row {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Grid extends React.PureComponent<Props, {}> {
|
export class Grid extends React.PureComponent<Props, {}> {
|
||||||
static displayName: string = "Grid";
|
static displayName: string = "Grid";
|
||||||
|
|
||||||
private gridHeight = 0;
|
private gridHeight = 0;
|
||||||
|
|
||||||
@@ -31,14 +31,14 @@ export class Grid extends React.PureComponent<Props, {}> {
|
|||||||
const rows: Row[] = [];
|
const rows: Row[] = [];
|
||||||
let rowWidth = 0;
|
let rowWidth = 0;
|
||||||
|
|
||||||
this.props.images.forEach(image => {
|
this.props.images.forEach((image) => {
|
||||||
const newWidth = rowWidth + image.width / image.height;
|
const newWidth = rowWidth + image.width / image.height;
|
||||||
const height = this.props.width / newWidth;
|
const height = this.props.width / newWidth;
|
||||||
|
|
||||||
if (height < this._rowHeight()) {
|
if (height < this._rowHeight()) {
|
||||||
rows.push({
|
rows.push({
|
||||||
images: row,
|
images: row,
|
||||||
width: rowWidth
|
width: rowWidth,
|
||||||
});
|
});
|
||||||
|
|
||||||
row = [];
|
row = [];
|
||||||
@@ -50,13 +50,13 @@ export class Grid extends React.PureComponent<Props, {}> {
|
|||||||
});
|
});
|
||||||
rows.push({
|
rows.push({
|
||||||
images: row,
|
images: row,
|
||||||
width: rowWidth
|
width: rowWidth,
|
||||||
});
|
});
|
||||||
|
|
||||||
const images = rows.map(row => {
|
const images = rows.map((row) => {
|
||||||
const height = Math.min(this.props.height, this.props.width / row.width);
|
const height = Math.min(this.props.height, this.props.width / row.width);
|
||||||
|
|
||||||
const pics = row.images.map(image => {
|
const pics = row.images.map((image) => {
|
||||||
return (
|
return (
|
||||||
<Picture
|
<Picture
|
||||||
image={image}
|
image={image}
|
||||||
@@ -75,7 +75,7 @@ export class Grid extends React.PureComponent<Props, {}> {
|
|||||||
<div
|
<div
|
||||||
className="Grid-row"
|
className="Grid-row"
|
||||||
style={{ height: height + "px" }}
|
style={{ height: height + "px" }}
|
||||||
key={row.images.map(image => image.src).join(",")}
|
key={row.images.map((image) => image.src).join(",")}
|
||||||
>
|
>
|
||||||
{pics}
|
{pics}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,8 +21,12 @@ export class ImageSet extends React.PureComponent<Props, {}> {
|
|||||||
return (
|
return (
|
||||||
<div className="ImageSet" ref={this.divRef}>
|
<div className="ImageSet" ref={this.divRef}>
|
||||||
<h2>
|
<h2>
|
||||||
<span className="ImageSet-location">{this.props.imageSet.location}</span>
|
<span className="ImageSet-location">
|
||||||
<span className="ImageSet-description">{this.props.imageSet.description}</span>
|
{this.props.imageSet.location}
|
||||||
|
</span>
|
||||||
|
<span className="ImageSet-description">
|
||||||
|
{this.props.imageSet.description}
|
||||||
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
<Grid
|
<Grid
|
||||||
images={this.props.imageSet.images}
|
images={this.props.imageSet.images}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export class Picture extends React.PureComponent<Props, State> {
|
|||||||
static displayName = "Picture";
|
static displayName = "Picture";
|
||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
isMounted: false
|
isMounted: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -52,7 +52,7 @@ export class Picture extends React.PureComponent<Props, State> {
|
|||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}
|
||||||
src={srcSet.bestSrc}
|
src={srcSet.bestSrc}
|
||||||
height={this.props.height + "px"}
|
height={this.props.height + "px"}
|
||||||
width={this.props.width + "px"}
|
width={Math.floor(this.props.width) + "px"}
|
||||||
/>
|
/>
|
||||||
</picture>
|
</picture>
|
||||||
);
|
);
|
||||||
@@ -64,7 +64,7 @@ export class Picture extends React.PureComponent<Props, State> {
|
|||||||
let bestSize = 800;
|
let bestSize = 800;
|
||||||
let bestScale = Infinity;
|
let bestScale = Infinity;
|
||||||
|
|
||||||
Model.SIZES.forEach(size => {
|
Model.SIZES.forEach((size) => {
|
||||||
const width =
|
const width =
|
||||||
this.props.image.width > this.props.image.height
|
this.props.image.width > this.props.image.height
|
||||||
? size
|
? size
|
||||||
@@ -87,7 +87,7 @@ export class Picture extends React.PureComponent<Props, State> {
|
|||||||
return {
|
return {
|
||||||
jpeg: jpegSrcSet.join(","),
|
jpeg: jpegSrcSet.join(","),
|
||||||
webp: webpSrcSet.join(","),
|
webp: webpSrcSet.join(","),
|
||||||
bestSrc: `img/${bestSize}/${this.props.image.src}`
|
bestSrc: `img/${bestSize}/${this.props.image.src}`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BigPicture } from "./big_picture";
|
import { BigPicture } from "./big_picture";
|
||||||
import { ImageSet } from "./image_set";
|
import { ImageSet } from "./image_set";
|
||||||
|
import { SetCover } from "./set_cover";
|
||||||
import * as Model from "../model";
|
import * as Model from "../model";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@@ -9,6 +10,7 @@ export interface Props {}
|
|||||||
export interface State {
|
export interface State {
|
||||||
data?: Model.Data | null;
|
data?: Model.Data | null;
|
||||||
selectedImage?: Model.Image | null;
|
selectedImage?: Model.Image | null;
|
||||||
|
selectedSet?: Model.ImageSet | null;
|
||||||
gridHeights: number[];
|
gridHeights: number[];
|
||||||
pageBottom: number;
|
pageBottom: number;
|
||||||
width: number;
|
width: number;
|
||||||
@@ -39,53 +41,75 @@ export class Root extends React.PureComponent<Props, State> {
|
|||||||
gridHeights: [],
|
gridHeights: [],
|
||||||
pageBottom: this._viewHeight() + window.pageYOffset,
|
pageBottom: this._viewHeight() + window.pageYOffset,
|
||||||
width: this._viewWidth(),
|
width: this._viewWidth(),
|
||||||
height: this._viewHeight()
|
height: this._viewHeight(),
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
window
|
window
|
||||||
.fetch(Model.dataUrl)
|
.fetch(Model.dataUrl)
|
||||||
.then(data => data.json())
|
.then((data) => data.json())
|
||||||
.then(json => this.setState({ data: json }))
|
.then((json) => this.setState({ data: json }))
|
||||||
.then(this._loadHash)
|
.then(this._loadHash)
|
||||||
.then(this._onViewChange)
|
.then(this._onViewChange)
|
||||||
.catch(e => console.error("Error fetching data", e));
|
.catch((e) => console.error("Error fetching data", e));
|
||||||
|
|
||||||
window.onresize = this._onViewChange;
|
window.onresize = this._onViewChange;
|
||||||
window.onscroll = this._onViewChange;
|
window.onscroll = this._onViewChange;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
screen.orientation.onchange = this._onViewChange;
|
screen.orientation.onchange = this._onViewChange;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
window.onorientationchange = this._onViewChange;
|
window.onorientationchange = this._onViewChange;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
window.onpopstate = this._loadHash;
|
window.onpopstate = this._loadHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
private _renderSet(set: Model.ImageSet) {
|
||||||
const imageSets = this.state.data
|
return (
|
||||||
? this.state.data.sets.map((set, idx) => (
|
<ImageSet
|
||||||
<ImageSet
|
key={set.location + set.description}
|
||||||
|
imageSet={set}
|
||||||
|
pageBottom={this.state.pageBottom}
|
||||||
|
setGridHeight={this._setGridHeight(0)}
|
||||||
|
onImageSelected={this._onImageSelected}
|
||||||
|
width={this.state.width}
|
||||||
|
height={this.state.height}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderSetCovers(sets: Model.ImageSet[]) {
|
||||||
|
return (
|
||||||
|
<div className="Root-setCovers">
|
||||||
|
{sets.map((set) => (
|
||||||
|
<SetCover
|
||||||
key={set.location + set.description}
|
key={set.location + set.description}
|
||||||
imageSet={set}
|
imageSet={set}
|
||||||
pageBottom={
|
onClick={() => {
|
||||||
this.state.pageBottom - this._getPreviousGridHeights(idx)
|
this._onSetSelected(set);
|
||||||
}
|
scrollTo(0, 0);
|
||||||
setGridHeight={this._setGridHeight(idx)}
|
}}
|
||||||
onImageSelected={this._onImageSelected}
|
width={Math.min(this.state.width, 400)}
|
||||||
width={this.state.width}
|
|
||||||
height={this.state.height}
|
|
||||||
/>
|
/>
|
||||||
))
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const imageSets = this.state.data
|
||||||
|
? this.state.selectedSet
|
||||||
|
? this._renderSet(this.state.selectedSet)
|
||||||
|
: this._renderSetCovers(this.state.data.sets)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Root">
|
<div className="Root">
|
||||||
{this._bigPicture()}
|
{this._bigPicture()}
|
||||||
<h1>Aaron's Ski Pictures</h1>
|
<h1 onClick={this._onHomeSelected}>Aaron's Ski Pictures</h1>
|
||||||
{imageSets}
|
{imageSets}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -102,19 +126,26 @@ export class Root extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
private _loadHash = () => {
|
private _loadHash = () => {
|
||||||
if (window.location.hash.length > 0 && this.state.data) {
|
if (window.location.hash.length > 0 && this.state.data) {
|
||||||
const src = window.location.hash.slice(1);
|
const hash = window.location.hash.slice(1);
|
||||||
let selectedImage: Model.Image | null = null;
|
|
||||||
|
|
||||||
this.state.data.sets.forEach(set => {
|
let selectedImage: Model.Image | null = null;
|
||||||
const image = set.images.find(image => image.src === src);
|
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) {
|
if (image) {
|
||||||
selectedImage = image;
|
selectedImage = image;
|
||||||
|
selectedSet = set;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({ selectedImage: selectedImage });
|
this.setState({ selectedImage, selectedSet });
|
||||||
} else {
|
} else {
|
||||||
this.setState({ selectedImage: null });
|
this.setState({ selectedImage: null, selectedSet: null });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,16 +162,37 @@ export class Root extends React.PureComponent<Props, State> {
|
|||||||
window.history.pushState(null, "", `#${img.src}`);
|
window.history.pushState(null, "", `#${img.src}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 = () => {
|
private _showGrid = () => {
|
||||||
this.setState({ selectedImage: null });
|
this.setState({ selectedImage: null });
|
||||||
window.history.pushState(null, "", "#");
|
this._onSetSelected(this.state.selectedSet as Model.ImageSet);
|
||||||
};
|
};
|
||||||
|
|
||||||
private _setGridHeight = (grid: number) => (height: number) => {
|
private _setGridHeight = (grid: number) => (height: number) => {
|
||||||
if (this.state.gridHeights[grid] === height) {
|
if (this.state.gridHeights[grid] === height) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState(state => {
|
this.setState((state) => {
|
||||||
const newGridHeights = [...state.gridHeights];
|
const newGridHeights = [...state.gridHeights];
|
||||||
newGridHeights[grid] = height;
|
newGridHeights[grid] = height;
|
||||||
|
|
||||||
|
|||||||
48
src/components/set_cover.tsx
Normal file
48
src/components/set_cover.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Picture } from "./picture";
|
||||||
|
import * as Model from "../model";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
imageSet: Model.ImageSet;
|
||||||
|
onClick: () => void;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface State {}
|
||||||
|
|
||||||
|
export class SetCover extends React.PureComponent<Props, State> {
|
||||||
|
static displayName = "SetCover";
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const image = this.props.imageSet.images[0];
|
||||||
|
const isTall = image.height > image.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 (
|
||||||
|
<div className="SetCover" onClick={this.props.onClick}>
|
||||||
|
<Picture
|
||||||
|
image={image}
|
||||||
|
onClick={() => {}}
|
||||||
|
height={height}
|
||||||
|
width={width}
|
||||||
|
/>
|
||||||
|
<h2>
|
||||||
|
<span className="SetCover-location">
|
||||||
|
{this.props.imageSet.location}
|
||||||
|
</span>
|
||||||
|
<span className="SetCover-description">
|
||||||
|
{this.props.imageSet.description}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user