diff options
Diffstat (limited to 'components')
-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 |
4 files changed, 183 insertions, 10 deletions
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, |