/* eslint-disable @next/next/no-img-element */ "use client"; import { UserInfo } from "@/ldap"; import React, { HTMLInputTypeAttribute, InputHTMLAttributes } from "react"; import styles from "./AboutMe.module.css"; const fallbackAvatar = "https://i.clong.biz/i/oc4zjlqr.png"; type UpdateResponse = { ok: boolean; error?: string; }; // TODO skip do your magic type InputProps = { label: string; name: string; type: HTMLInputTypeAttribute; error?: string; displayImage?: string; } & InputHTMLAttributes<HTMLInputElement>; const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => { // get console to shut up const inputProps = { ...props }; delete inputProps.displayImage; return ( <div className={styles.formRow}> <label htmlFor={props.id}>{props.label}</label> {props.displayImage && ( <img src={props.displayImage} className={styles.avatar} alt={"Your avatar"} width="50px" height="50px" /> )} <div className={styles.formVert}> <input {...inputProps} ref={ref} className={styles.fancyInput} /> {props.error != null && <p className={styles.error}>{props.error}</p>} </div> </div> ); }); Input.displayName = "Input"; async function fileAsBase64(f: File) { const reader = new FileReader(); reader.readAsArrayBuffer(f); return new Promise<string>((resolve, reject) => { reader.onload = () => { const result = reader.result as ArrayBuffer; const buffer = Buffer.from(result); resolve(buffer.toString("base64")); }; reader.onerror = () => reject(reader.error); }); } export default function AboutMe({ info }: { info: UserInfo }) { const displayNameRef = React.useRef<HTMLInputElement>(null); const emailRef = React.useRef<HTMLInputElement>(null); const avatarRef = React.useRef<HTMLInputElement>(null); const submitRef = React.useRef<HTMLInputElement>(null); const [avatar, setAvatar] = React.useState<string | null>( info.avatar ?? null ); const currentPasswordRef = React.useRef<HTMLInputElement>(null); const newPasswordRef = React.useRef<HTMLInputElement>(null); const confirmPasswordRef = React.useRef<HTMLInputElement>(null); const submitPasswordRef = React.useRef<HTMLInputElement>(null); const [incorrectPassword, setIncorrectPassword] = React.useState(false); const [passwordMismatch, setPasswordMismatch] = React.useState(false); const [avatarBig, setAvatarBig] = React.useState(false); return ( <div className={styles.content}> <h2 className={styles.header}>User information</h2> <form onSubmit={async (e) => { e.preventDefault(); // turn the data uri into just base64 const avatarChanged = avatar !== null && avatar !== info.avatar; const avatarData = avatarChanged ? avatar?.split(",")[1] : null; submitRef.current!.disabled = true; const req = await fetch("/api/update", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ displayName: displayNameRef.current?.value, email: emailRef.current?.value, avatar: avatarData }) }); submitRef.current!.disabled = false; try { const res: UpdateResponse = await req.json(); if (!res.ok && res.error !== null) { switch (res.error) { case "avatarBig": setAvatarBig(true); break; } } } catch { console.error(req); } }} > <Input type="text" name="username" label="Username" defaultValue={info.username} disabled title="You can't change your username." /> <Input type="text" name="display-name" label="Display name" defaultValue={info.displayName} ref={displayNameRef} /> <Input type="email" name="email" label="Email" defaultValue={info.email} ref={emailRef} /> {/* why, html gods, why? */} <input type="file" name="avatar" accept="image/png, image/jpeg" ref={avatarRef} style={{ display: "none" }} /> <Input type="button" value="Choose file" name="avatar" label="Avatar" accept="image/png, image/jpeg" error={avatarBig ? "Avatar is too big." : undefined} onClick={() => { avatarRef.current?.click(); const eventListener = async () => { avatarRef.current?.removeEventListener("change", eventListener); const file = avatarRef.current?.files?.[0]; if (file == null) return; if (file.size > 1_000_000) { setAvatarBig(true); return; } else { setAvatarBig(false); } const b64 = await fileAsBase64(file); setAvatar(`data:${file.type};base64,${b64}`); }; avatarRef.current?.addEventListener("change", eventListener); }} displayImage={avatar ?? fallbackAvatar} /> <div className={styles.formRow}> <input type="submit" value="Save" ref={submitRef} className={styles.fancyInput} /> </div> </form> <hr className={styles.divider} /> <h2 className={styles.header}>Change password</h2> <form onSubmit={async (e) => { e.preventDefault(); setIncorrectPassword(false); setPasswordMismatch(false); if ( newPasswordRef.current?.value !== confirmPasswordRef.current?.value ) { setPasswordMismatch(true); return; } submitPasswordRef.current!.disabled = true; const req = await fetch("/api/changePassword", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ currentPassword: currentPasswordRef.current?.value, newPassword: newPasswordRef.current?.value }) }); submitPasswordRef.current!.disabled = false; try { const res: UpdateResponse = await req.json(); if (!res.ok && res.error !== null) { switch (res.error) { case "incorrectPassword": setIncorrectPassword(true); break; } } } catch { console.error(req); } }} > <Input type="password" name="current-password" label="Current" minLength={12} required ref={currentPasswordRef} error={incorrectPassword ? "Incorrect password." : undefined} /> <Input type="password" name="new-password" label="New" minLength={12} required ref={newPasswordRef} /> <Input type="password" name="confirm-password" label="Confirm" ref={confirmPasswordRef} minLength={12} required error={passwordMismatch ? "Passwords do not match." : undefined} /> <div className={styles.formRow}> <input type="submit" value="Change password" ref={submitPasswordRef} className={styles.fancyInput} /> </div> </form> </div> ); }