summary refs log tree commit diff
path: root/components
diff options
context:
space:
mode:
Diffstat (limited to 'components')
-rw-r--r--components/ContactForm.tsx69
-rw-r--r--components/Image/index.tsx108
-rw-r--r--components/InfoBar/index.tsx8
-rw-r--r--components/layout/Header.tsx8
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,