Manual static image optimization

main
Ashelyn Dawn 5 months ago
parent 11aed6e30f
commit c74fad1793
No known key found for this signature in database
GPG Key ID: D1980B8C6F349BC1

@ -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>

@ -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>
</>
)

@ -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>
)
}

@ -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} />
</>
)
}

@ -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}>

@ -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,

@ -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"
}]
}

1
next-env.d.ts vendored

@ -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.

@ -1,4 +1,7 @@
module.exports = {
output: 'export',
trailingSlash: true
trailingSlash: true,
images: {
disableStaticImages: true
},
}

8
package-lock.json generated

@ -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": "*",

@ -20,7 +20,7 @@
},
"devDependencies": {
"@types/node": "20.1.0",
"@types/react": "^18.2.6",
"@types/react": "^18.2.41",
"typescript": "5.0.4"
}
}

@ -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 {

@ -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
}
Loading…
Cancel
Save