mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-14 09:39:15 +00:00
New homepage (#861)
* Paste button * Logo and animated blobs * Predictable initial blob position * Initial blob shape * Update canvas on resize if not updating every frame * lol * Get styles from CSS * Background blobs * Fade into the center * Get initial focus * Pause time while page is hidden * Footer * Optimise amount of initial CSS * More CSS optimisation * Install button * Home page with demo loading * Tweak size * Replace thumbnails * Responsive demo section * Responsive main section * Remove debug stuff * Fix prerender SVG size * Changes from feedback * Blob nudges (#872) * more smaller blobs * less blobs that are practically invisible * more dynamic speed range and stronger gravity * Reverting resize observer change The content rect is different to getBoundingClientRect Co-authored-by: Adam Argyle <atom@argyleink.com>
This commit is contained in:
372
src/shared/prerendered-app/Intro/index.tsx
Normal file
372
src/shared/prerendered-app/Intro/index.tsx
Normal file
@@ -0,0 +1,372 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { linkRef } from 'shared/prerendered-app/util';
|
||||
import '../../custom-els/loading-spinner';
|
||||
import logo from 'url:./imgs/logo.svg';
|
||||
import githubLogo from 'url:./imgs/github-logo.svg';
|
||||
import largePhoto from 'url:./imgs/demos/demo-large-photo.jpg';
|
||||
import artwork from 'url:./imgs/demos/demo-artwork.jpg';
|
||||
import deviceScreen from 'url:./imgs/demos/demo-device-screen.png';
|
||||
import largePhotoIcon from 'url:./imgs/demos/icon-demo-large-photo.jpg';
|
||||
import artworkIcon from 'url:./imgs/demos/icon-demo-artwork.jpg';
|
||||
import deviceScreenIcon from 'url:./imgs/demos/icon-demo-device-screen.jpg';
|
||||
import logoIcon from 'url:./imgs/demos/icon-demo-logo.png';
|
||||
import logoWithText from 'url:./imgs/logo-with-text.svg';
|
||||
import * as style from './style.css';
|
||||
import type SnackBarElement from 'shared/custom-els/snack-bar';
|
||||
import 'shared/custom-els/snack-bar';
|
||||
import { startBlobs } from './blob-anim/meta';
|
||||
|
||||
const demos = [
|
||||
{
|
||||
description: 'Large photo',
|
||||
size: '2.8mb',
|
||||
filename: 'photo.jpg',
|
||||
url: largePhoto,
|
||||
iconUrl: largePhotoIcon,
|
||||
},
|
||||
{
|
||||
description: 'Artwork',
|
||||
size: '2.9mb',
|
||||
filename: 'art.jpg',
|
||||
url: artwork,
|
||||
iconUrl: artworkIcon,
|
||||
},
|
||||
{
|
||||
description: 'Device screen',
|
||||
size: '1.6mb',
|
||||
filename: 'pixel3.png',
|
||||
url: deviceScreen,
|
||||
iconUrl: deviceScreenIcon,
|
||||
},
|
||||
{
|
||||
description: 'SVG icon',
|
||||
size: '13k',
|
||||
filename: 'squoosh.svg',
|
||||
url: logo,
|
||||
iconUrl: logoIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const blobAnimImport =
|
||||
!__PRERENDER__ && matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
? undefined
|
||||
: import('./blob-anim');
|
||||
const installButtonSource = 'introInstallButton-Purple';
|
||||
const supportsClipboardAPI =
|
||||
!__PRERENDER__ && navigator.clipboard && navigator.clipboard.read;
|
||||
|
||||
async function getImageClipboardItem(
|
||||
items: ClipboardItem[],
|
||||
): Promise<undefined | Blob> {
|
||||
for (const item of items) {
|
||||
const type = item.types.find((type) => type.startsWith('image/'));
|
||||
if (type) return item.getType(type);
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onFile?: (file: File) => void;
|
||||
showSnack?: SnackBarElement['showSnackbar'];
|
||||
}
|
||||
interface State {
|
||||
fetchingDemoIndex?: number;
|
||||
beforeInstallEvent?: BeforeInstallPromptEvent;
|
||||
showBlobSVG: boolean;
|
||||
}
|
||||
|
||||
export default class Intro extends Component<Props, State> {
|
||||
state: State = {
|
||||
showBlobSVG: true,
|
||||
};
|
||||
private fileInput?: HTMLInputElement;
|
||||
private blobCanvas?: HTMLCanvasElement;
|
||||
private installingViaButton = false;
|
||||
|
||||
componentDidMount() {
|
||||
// Listen for beforeinstallprompt events, indicating Squoosh is installable.
|
||||
window.addEventListener(
|
||||
'beforeinstallprompt',
|
||||
this.onBeforeInstallPromptEvent,
|
||||
);
|
||||
|
||||
// Listen for the appinstalled event, indicating Squoosh has been installed.
|
||||
window.addEventListener('appinstalled', this.onAppInstalled);
|
||||
|
||||
if (blobAnimImport) {
|
||||
blobAnimImport.then((module) => {
|
||||
this.setState(
|
||||
{
|
||||
showBlobSVG: false,
|
||||
},
|
||||
() => module.startBlobAnim(this.blobCanvas!),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener(
|
||||
'beforeinstallprompt',
|
||||
this.onBeforeInstallPromptEvent,
|
||||
);
|
||||
window.removeEventListener('appinstalled', this.onAppInstalled);
|
||||
}
|
||||
|
||||
private onFileChange = (event: Event): void => {
|
||||
const fileInput = event.target as HTMLInputElement;
|
||||
const file = fileInput.files && fileInput.files[0];
|
||||
if (!file) return;
|
||||
this.fileInput!.value = '';
|
||||
this.props.onFile!(file);
|
||||
};
|
||||
|
||||
private onOpenClick = () => {
|
||||
this.fileInput!.click();
|
||||
};
|
||||
|
||||
private onDemoClick = async (index: number, event: Event) => {
|
||||
try {
|
||||
this.setState({ fetchingDemoIndex: index });
|
||||
const demo = demos[index];
|
||||
const blob = await fetch(demo.url).then((r) => r.blob());
|
||||
const file = new File([blob], demo.filename, { type: blob.type });
|
||||
this.props.onFile!(file);
|
||||
} catch (err) {
|
||||
this.setState({ fetchingDemoIndex: undefined });
|
||||
this.props.showSnack!("Couldn't fetch demo image");
|
||||
}
|
||||
};
|
||||
|
||||
private onBeforeInstallPromptEvent = (event: BeforeInstallPromptEvent) => {
|
||||
// Don't show the mini-infobar on mobile
|
||||
event.preventDefault();
|
||||
|
||||
// Save the beforeinstallprompt event so it can be called later.
|
||||
this.setState({ beforeInstallEvent: event });
|
||||
|
||||
// Log the event.
|
||||
const gaEventInfo = {
|
||||
eventCategory: 'pwa-install',
|
||||
eventAction: 'promo-shown',
|
||||
nonInteraction: true,
|
||||
};
|
||||
ga('send', 'event', gaEventInfo);
|
||||
};
|
||||
|
||||
private onInstallClick = async (event: Event) => {
|
||||
// Get the deferred beforeinstallprompt event
|
||||
const beforeInstallEvent = this.state.beforeInstallEvent;
|
||||
// If there's no deferred prompt, bail.
|
||||
if (!beforeInstallEvent) return;
|
||||
|
||||
this.installingViaButton = true;
|
||||
|
||||
// Show the browser install prompt
|
||||
beforeInstallEvent.prompt();
|
||||
|
||||
// Wait for the user to accept or dismiss the install prompt
|
||||
const { outcome } = await beforeInstallEvent.userChoice;
|
||||
// Send the analytics data
|
||||
const gaEventInfo = {
|
||||
eventCategory: 'pwa-install',
|
||||
eventAction: 'promo-clicked',
|
||||
eventLabel: installButtonSource,
|
||||
eventValue: outcome === 'accepted' ? 1 : 0,
|
||||
};
|
||||
ga('send', 'event', gaEventInfo);
|
||||
|
||||
// If the prompt was dismissed, we aren't going to install via the button.
|
||||
if (outcome === 'dismissed') {
|
||||
this.installingViaButton = false;
|
||||
}
|
||||
};
|
||||
|
||||
private onAppInstalled = () => {
|
||||
// We don't need the install button, if it's shown
|
||||
this.setState({ beforeInstallEvent: undefined });
|
||||
|
||||
// Don't log analytics if page is not visible
|
||||
if (document.hidden) return;
|
||||
|
||||
// Try to get the install, if it's not set, use 'browser'
|
||||
const source = this.installingViaButton ? installButtonSource : 'browser';
|
||||
ga('send', 'event', 'pwa-install', 'installed', source);
|
||||
|
||||
// Clear the install method property
|
||||
this.installingViaButton = false;
|
||||
};
|
||||
|
||||
private onPasteClick = async () => {
|
||||
let clipboardItems: ClipboardItem[];
|
||||
|
||||
try {
|
||||
clipboardItems = await navigator.clipboard.read();
|
||||
} catch (err) {
|
||||
this.props.showSnack!(`No permission to access clipboard`);
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = await getImageClipboardItem(clipboardItems);
|
||||
|
||||
if (!blob) {
|
||||
this.props.showSnack!(`No image found in the clipboard`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onFile!(new File([blob], 'image.unknown'));
|
||||
};
|
||||
|
||||
render(
|
||||
{}: Props,
|
||||
{ fetchingDemoIndex, beforeInstallEvent, showBlobSVG }: State,
|
||||
) {
|
||||
return (
|
||||
<div class={style.intro}>
|
||||
<input
|
||||
class={style.hide}
|
||||
ref={linkRef(this, 'fileInput')}
|
||||
type="file"
|
||||
onChange={this.onFileChange}
|
||||
/>
|
||||
<div class={style.main}>
|
||||
{!__PRERENDER__ && (
|
||||
<canvas
|
||||
ref={linkRef(this, 'blobCanvas')}
|
||||
class={style.blobCanvas}
|
||||
/>
|
||||
)}
|
||||
<h1 class={style.logoContainer}>
|
||||
<img
|
||||
class={style.logo}
|
||||
src={logoWithText}
|
||||
alt="Squoosh"
|
||||
width="539"
|
||||
height="162"
|
||||
/>
|
||||
</h1>
|
||||
<div class={style.loadImg}>
|
||||
{showBlobSVG && (
|
||||
<svg
|
||||
class={style.blobSvg}
|
||||
viewBox="-1.25 -1.25 2.5 2.5"
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
>
|
||||
{startBlobs.map((points) => (
|
||||
<path
|
||||
d={points
|
||||
.map((point, i) => {
|
||||
const nextI = i === points.length - 1 ? 0 : i + 1;
|
||||
let d = '';
|
||||
if (i === 0) {
|
||||
d += `M${point[2]} ${point[3]}`;
|
||||
}
|
||||
return (
|
||||
d +
|
||||
`C${point[4]} ${point[5]} ${points[nextI][0]} ${points[nextI][1]} ${points[nextI][2]} ${points[nextI][3]}`
|
||||
);
|
||||
})
|
||||
.join('')}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
)}
|
||||
<div
|
||||
class={style.loadImgContent}
|
||||
style={{ visibility: __PRERENDER__ ? 'hidden' : '' }}
|
||||
>
|
||||
<button class={style.loadBtn} onClick={this.onOpenClick}>
|
||||
<svg viewBox="0 0 24 24" class={style.loadIcon}>
|
||||
<path d="M19 7v3h-2V7h-3V5h3V2h2v3h3v2h-3zm-3 4V8h-3V5H5a2 2 0 00-2 2v12c0 1.1.9 2 2 2h12a2 2 0 002-2v-8h-3zM5 19l3-4 2 3 3-4 4 5H5z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div>
|
||||
<span class={style.dropText}>Drop </span>OR{' '}
|
||||
{supportsClipboardAPI ? (
|
||||
<button class={style.pasteBtn} onClick={this.onPasteClick}>
|
||||
Paste
|
||||
</button>
|
||||
) : (
|
||||
'Paste'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class={style.demosContainer}>
|
||||
<svg viewBox="0 0 1920 140" class={style.topWave}>
|
||||
<path
|
||||
d="M1920 0l-107 28c-106 29-320 85-533 93-213 7-427-36-640-50s-427 0-533 7L0 85v171h1920z"
|
||||
class={style.subWave}
|
||||
/>
|
||||
<path
|
||||
d="M0 129l64-26c64-27 192-81 320-75 128 5 256 69 384 64 128-6 256-80 384-91s256 43 384 70c128 26 256 26 320 26h64v96H0z"
|
||||
class={style.mainWave}
|
||||
/>
|
||||
</svg>
|
||||
<div class={style.contentPadding}>
|
||||
<p class={style.demoTitle}>
|
||||
Or <strong>try one</strong> of these:
|
||||
</p>
|
||||
<ul class={style.demos}>
|
||||
{demos.map((demo, i) => (
|
||||
<li>
|
||||
<button
|
||||
class="unbutton"
|
||||
onClick={(event) => this.onDemoClick(i, event)}
|
||||
>
|
||||
<div>
|
||||
<div class={style.demoIconContainer}>
|
||||
<img
|
||||
class={style.demoIcon}
|
||||
src={demo.iconUrl}
|
||||
alt={demo.description}
|
||||
/>
|
||||
{fetchingDemoIndex === i && (
|
||||
<div class={style.demoLoader}>
|
||||
<loading-spinner />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div class={style.demoSize}>{demo.size}</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class={style.footer}>
|
||||
<svg viewBox="0 0 1920 79" class={style.topWave}>
|
||||
<path
|
||||
d="M0 59l64-11c64-11 192-34 320-43s256-5 384 4 256 23 384 34 256 21 384 14 256-30 320-41l64-11v94H0z"
|
||||
class={style.footerWave}
|
||||
/>
|
||||
</svg>
|
||||
<div class={style.contentPadding}>
|
||||
<footer class={style.footerItems}>
|
||||
<a
|
||||
class={style.footerLink}
|
||||
href="https://github.com/GoogleChromeLabs/squoosh/blob/dev/README.md#privacy"
|
||||
>
|
||||
Privacy
|
||||
</a>
|
||||
<a
|
||||
class={style.footerLinkWithLogo}
|
||||
href="https://github.com/GoogleChromeLabs/squoosh"
|
||||
>
|
||||
<img src={githubLogo} alt="" width="10" height="10" />
|
||||
Source on Github
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
{beforeInstallEvent && (
|
||||
<button class={style.installBtn} onClick={this.onInstallClick}>
|
||||
Install
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user