diff options
author | Ashelyn Rose <git@tempest.dev> | 2023-12-02 16:29:03 -0700 |
---|---|---|
committer | Ashelyn Rose <git@tempest.dev> | 2023-12-02 16:29:03 -0700 |
commit | c74fad179379260dcf46edeae22c1f04ee842508 (patch) | |
tree | 6e5cce69c0b01227599933179cc7be2b230486a0 | |
parent | 11aed6e30f3050a1062e37149bba878d485d52a8 (diff) |
Manual static image optimization
-rw-r--r-- | app/about/page.tsx | 8 | ||||
-rw-r--r-- | app/contact/page.tsx | 76 | ||||
-rw-r--r-- | components/ContactForm.tsx | 69 | ||||
-rw-r--r-- | components/Image/index.tsx | 108 | ||||
-rw-r--r-- | components/InfoBar/index.tsx | 8 | ||||
-rw-r--r-- | components/layout/Header.tsx | 8 | ||||
-rw-r--r-- | config/system.json | 12 | ||||
-rw-r--r-- | next-env.d.ts | 1 | ||||
-rw-r--r-- | next.config.js | 5 | ||||
-rw-r--r-- | package-lock.json | 8 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | styles/layout.css | 5 | ||||
-rw-r--r-- | utils/profiles.ts | 8 |
13 files changed, 210 insertions, 108 deletions
diff --git a/app/about/page.tsx b/app/about/page.tsx index 863bea9..255f403 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -1,8 +1,7 @@ -import Image from 'next/image' +import Image from 'components/Image' import system from '~/config/system.json' import styles from '~/styles/about.module.css' -import profilePics from '~/utils/profiles' export interface Member { name: string, @@ -11,6 +10,7 @@ export interface Member { color: string, bioShort: string, readMore: string, + profileImg: string, } export default function About() { @@ -32,7 +32,7 @@ export default function About() { alt="" width={150} height={150} - src={profilePics[member.name.toLowerCase()]} + src={member.profileImg} /> <h2>{member.name}</h2> <p className={styles.pronouns}>{member.mainPronouns}</p> @@ -61,7 +61,7 @@ export default function About() { alt="" width={80} height={80} - src={profilePics[member.name.toLowerCase()]} + src={member.profileImg} /> <h2>{member.name}</h2> <p className={styles.pronouns}>{member.mainPronouns}</p> diff --git a/app/contact/page.tsx b/app/contact/page.tsx index 1c144c2..6af8ad1 100644 --- a/app/contact/page.tsx +++ b/app/contact/page.tsx @@ -1,56 +1,7 @@ -'use client' -import { useState, useRef, FormEvent } from 'react' import InfoBar from "~/components/InfoBar/" - -import styles from '~/styles/form.module.css' - -const submitUrl = 'https://contact.tempest.dev/api/contact/me' +import ContactForm from "~/components/ContactForm" export default function Contact() { - const [submitting, setSubmitting] = useState(false) - const [status, setStatus] = useState('') - - const nameRef = useRef<HTMLInputElement>() - const emailRef = useRef<HTMLInputElement>() - const messageRef = useRef<HTMLTextAreaElement>() - - const submit = async (ev: FormEvent<HTMLFormElement>) => { - ev.preventDefault() - - setStatus('') - - const name = nameRef.current.value - const email = emailRef.current.value - const message = messageRef.current.value - - if (!name) setStatus(s => s + ' Name required.') - if (!email) setStatus(s => s + ' Email required.') - if (!message) setStatus(s => s + ' Message required.') - - if (!name || !email || !message) - return - - setSubmitting(true) - - try { - - await fetch(submitUrl, { - method: 'post', - headers: { - 'content-type': 'application/json' - }, - body: JSON.stringify({ - name, email, message - }) - }) - - setStatus('Message sent successfully') - } catch { - setStatus('Error submitting message, please try again') - } finally { - setSubmitting(false) - } - } return ( <> @@ -61,30 +12,7 @@ export default function Contact() { <InfoBar> Be nice. Please don't make us regret putting this here </InfoBar> - <form className={styles.form} onSubmit={submit}> - <label htmlFor="name">Name:</label> - <input disabled={submitting} name="name" ref={nameRef} /> - - <label htmlFor="email">Email:</label> - <input disabled={submitting} name="email" ref={emailRef} /> - - <label htmlFor="message">Message:</label> - <textarea disabled={submitting} name="message" ref={messageRef} /> - - <button disabled={submitting} type="submit">Submit</button> - {status && <span className={styles.status}>{status}</span>} - {/*<FormController - url="https://kae.tempest.dev/api/contact/tempest" - afterSubmit={() => setStatus('Message sent successfully!')} - onError={() => setStatus('Error submitting message, please try again.')} - > - <Input name="name" validate={v => !!v} /> - <Input name="email" validate={v => !!v && v.includes('@')} /> - <TextArea name="message" validate={v => !!v && v.length < 1000} /> - <Button onClick={() => setStatus(null)} type="submit">submit</Button> - {status && <p>{status}</p>} - </FormController>*/} - </form> + <ContactForm/> </main> </> ) diff --git a/components/ContactForm.tsx b/components/ContactForm.tsx new file mode 100644 index 0000000..a64cb85 --- /dev/null +++ b/components/ContactForm.tsx @@ -0,0 +1,69 @@ +'use client' + +import styles from '~/styles/form.module.css' + +const submitUrl = 'https://contact.tempest.dev/api/contact/me' +import { useState, useRef, FormEvent } from 'react' + +export default function ContactForm() { + const [submitting, setSubmitting] = useState(false) + const [status, setStatus] = useState('') + + const nameRef = useRef<HTMLInputElement>() + const emailRef = useRef<HTMLInputElement>() + const messageRef = useRef<HTMLTextAreaElement>() + + const submit = async (ev: FormEvent<HTMLFormElement>) => { + ev.preventDefault() + + setStatus('') + + const name = nameRef.current.value + const email = emailRef.current.value + const message = messageRef.current.value + + if (!name) setStatus(s => s + ' Name required.') + if (!email) setStatus(s => s + ' Email required.') + if (!message) setStatus(s => s + ' Message required.') + + if (!name || !email || !message) + return + + setSubmitting(true) + + try { + + await fetch(submitUrl, { + method: 'post', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + name, email, message + }) + }) + + setStatus('Message sent successfully') + } catch { + setStatus('Error submitting message, please try again') + } finally { + setSubmitting(false) + } + } + + return ( + <form className={styles.form} onSubmit={submit}> + <label htmlFor="name">Name:</label> + <input disabled={submitting} name="name" ref={nameRef} /> + + <label htmlFor="email">Email:</label> + <input disabled={submitting} name="email" ref={emailRef} /> + + <label htmlFor="message">Message:</label> + <textarea disabled={submitting} name="message" ref={messageRef} /> + + <button disabled={submitting} type="submit">Submit</button> + {status && <span className={styles.status}>{status}</span>} + </form> + ) +} diff --git a/components/Image/index.tsx b/components/Image/index.tsx new file mode 100644 index 0000000..e7a393c --- /dev/null +++ b/components/Image/index.tsx @@ -0,0 +1,108 @@ +import { promises as fs } from 'fs' +import path from 'path' +import { ImgHTMLAttributes } from 'react' +import sharp from 'sharp' + +const outputDir = path.join(process.cwd(), '.next/static/images') +const serverPath = '/_next/static/images/' + +interface ReqProps { + src: string, + alt: string, +} + +type FixedSingleDimension = {width: number} | {height: number} +type FixedBothDimension = {width: number, height: number} +type MultiSize = {width: number[]} | {height: number[]} +type SizeProps = FixedSingleDimension | FixedBothDimension | MultiSize +type ImageAttrs = Omit<ImgHTMLAttributes<HTMLImageElement>, "width" | "height" | "src"> + +type ImageProps = ImageAttrs + & SizeProps + & ReqProps + +interface ProcessedImage { + hostPath: string, + randPrefix: string, + widths: {[width: number] : string} +} + +async function ImageServer({src,...imgAttrs}: ImageProps) { + const srcImg = sharp(src) + const extension = path.extname(src) + const filename = path.basename(src, extension) + const randPrefix = (Math.random() + 1).toString(36).substring(7) + const width = 'width' in imgAttrs ? imgAttrs.width : null + const height = 'height' in imgAttrs ? imgAttrs.height : null + + console.log(`processing ${src} for width ${width} and height ${height}`) + + // Make sure we can write to the dir + await fs.mkdir(outputDir, {recursive: true}) + + // Handle where we have an array of dimensions + if (Array.isArray(width) || Array.isArray(height)) { + const dimension = Array.isArray(width) ? 'width' : 'height' + const sizes = Array.isArray(width) ? width : height as number[] + const suffix = dimension.charAt(0) + + // Copy in original + fs.copyFile(src, path.join(outputDir, `${randPrefix}-${filename}${extension}`)) + + // Create resized copies + const srcSetLines = await Promise.all(sizes + .map(async size => { + const outputName = `${randPrefix}-${filename}-${size}${suffix}${extension}` + const outputPath = path.join(outputDir, outputName) + return srcImg + .resize({[dimension]: size}) + .toFile(outputPath) + .then(result => `${serverPath}${outputName} ${result.width}w`) + })) + + return ( + <img + src={src} + srcSet={srcSetLines.join(',')} + {...imgAttrs} + width={undefined} + height={undefined} + /> + ) + } else { + const dimensions = { + width: typeof width === 'number' ? width : undefined, + height: typeof height === 'number' ? height : undefined + } + + const dimensionString = (() => { + if (dimensions.height && dimensions.height) + return `${dimensions.width}x${dimensions.height}` + if (dimensions.width) + return `${dimensions.width}w` + return `${dimensions.height}h` + })() + + const outputName = `${randPrefix}-${filename}-${dimensionString}${extension}` + const outputPath = path.join(outputDir, outputName) + const outputSrc = serverPath + outputName + await srcImg.resize(dimensions).toFile(outputPath) + + return ( + <img + src={outputSrc} + {...imgAttrs} + {...dimensions} + /> + ) + } +} + +export default function ImageServerWrap(props: ImageProps) { + return ( + <> + {/* @ts-expect-error Async Server Component */} + <ImageServer {...props} /> + </> + ) +} diff --git a/components/InfoBar/index.tsx b/components/InfoBar/index.tsx index 2901883..20f7130 100644 --- a/components/InfoBar/index.tsx +++ b/components/InfoBar/index.tsx @@ -1,8 +1,7 @@ import { ReactNode } from 'react' -import Image from 'next/image' +import Image from 'components/Image' import system from '~/config/system.json' -import profilePics from '~/utils/profiles' import styles from './InfoBar.module.css' interface ArbitraryChildrenProps { @@ -39,10 +38,9 @@ export default function InfoBar(props: InfobarProps) { } if ('authorName' in props) { - const authorKey = props.authorName.toLowerCase() const author = system.members.find((member) => member.name === props.authorName) const style = { '--author-color': author?.color } as React.CSSProperties - const picture = profilePics[authorKey] + const picture = author.profileImg return ( <aside className={`${styles.infobar} ${styles.postMeta}`} style={style}> @@ -59,7 +57,7 @@ export default function InfoBar(props: InfobarProps) { const memberKey = props.memberName.toLowerCase() const member = system.members.find((member) => member.name === props.memberName) const style = { '--member-color': member?.color } as React.CSSProperties - const picture = profilePics[memberKey] + const picture = member.profileImg return ( <aside className={`${styles.infobar} ${styles.memberProfile}`} style={style}> diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx index 5e589c9..f55047c 100644 --- a/components/layout/Header.tsx +++ b/components/layout/Header.tsx @@ -1,11 +1,9 @@ -'use client' - import React from 'react' -import Image from 'next/image' +import Image from 'components/Image' import { usePathname } from 'next/navigation' -import header from '~/images/aurora-1197753.jpg' +const header = 'images/aurora-1197753.jpg' export default function Title() { const pathname = usePathname() @@ -29,7 +27,7 @@ export default function Title() { src={header} alt="" role="presentation" - fill={true} + width={[2560,1920,1280,800,600]} sizes={` (max-width: 2560) 100vw, (max-width: 1920) 100vw, diff --git a/config/system.json b/config/system.json index ad8adc3..a1b2f55 100644 --- a/config/system.json +++ b/config/system.json @@ -12,7 +12,8 @@ {"name": "names:", "value": "rose, rosalyn, occasionally callista"}, {"name": "pronouns:", "value": "they/them. very occasionally she/her but i'm not usually in the mood"}, {"name": "orientation:", "value": "the hell if i know"} - ] + ], + "profileImg": "images/profile/rose.png" },{ "name": "Dawn", "featured": true, @@ -25,7 +26,8 @@ {"name": "Names:", "value": "Dawn. Perhaps \"Ashelyn Dawn\" if you're feeling fancy"}, {"name": "Pronouns:", "value": "She/her, no exceptions"}, {"name": "Orientation:", "value": "Asexual, polyromantic lesbian"} - ] + ], + "profileImg": "images/profile/dawn.png" },{ "name": "echo", "mainPronouns": "it/its", @@ -37,7 +39,8 @@ {"name": "name:", "value": "echo"}, {"name": "pronouns:", "value": "it/its, “that one”, most neopronouns okay"}, {"name": "orientation:", "value": "aroace"} - ] + ], + "profileImg": "images/profile/echo.png" },{ "name": "Corona", "featured": true, @@ -50,6 +53,7 @@ {"name": "Names:", "value": "Corona (previously known as Harrow)"}, {"name": "Pronouns:", "value": "she/her or they/them with equal preference"}, {"name": "Orientation", "value": "Indeterminate"} - ] + ], + "profileImg": "images/profile/corona.png" }] } diff --git a/next-env.d.ts b/next-env.d.ts index 4f11a03..2532e77 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,5 +1,4 @@ /// <reference types="next" /> -/// <reference types="next/image-types/global" /> // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.js b/next.config.js index 152b5d3..de1d2e3 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,7 @@ module.exports = { output: 'export', - trailingSlash: true + trailingSlash: true, + images: { + disableStaticImages: true + }, } diff --git a/package-lock.json b/package-lock.json index 818bbd3..684ba5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ }, "devDependencies": { "@types/node": "20.1.0", - "@types/react": "^18.2.6", + "@types/react": "^18.2.41", "typescript": "5.0.4" } }, @@ -195,9 +195,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.6.tgz", - "integrity": "sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA==", + "version": "18.2.41", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.41.tgz", + "integrity": "sha512-CwOGr/PiLiNBxEBqpJ7fO3kocP/2SSuC9fpH5K7tusrg4xPSRT/193rzolYwQnTN02We/ATXKnb6GqA5w4fRxw==", "dev": true, "dependencies": { "@types/prop-types": "*", diff --git a/package.json b/package.json index f478321..e05f3f6 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "devDependencies": { "@types/node": "20.1.0", - "@types/react": "^18.2.6", + "@types/react": "^18.2.41", "typescript": "5.0.4" } } diff --git a/styles/layout.css b/styles/layout.css index d416712..abc709a 100644 --- a/styles/layout.css +++ b/styles/layout.css @@ -141,9 +141,10 @@ header .headerBackground { } header .headerBackground img { - z-index: -1; object-fit: cover; object-position: center 75%; + width: 100%; + height: 100%; } header .headerBackground::after { @@ -152,6 +153,8 @@ header .headerBackground::after { width: 100%; height: 100%; background: rgba(0,0,0,.35); + position: absolute; + top: 0; } header:not(.homepage) ~ h1.pageTitle { diff --git a/utils/profiles.ts b/utils/profiles.ts deleted file mode 100644 index 06302a2..0000000 --- a/utils/profiles.ts +++ /dev/null @@ -1,8 +0,0 @@ -import rose from '~/images/profile/rose.png' -import dawn from '~/images/profile/dawn.png' -import echo from '~/images/profile/echo.png' -import corona from '~/images/profile/corona.png' - -export default { - rose, dawn, echo, corona -} |