284 lines
7.8 KiB
TypeScript
284 lines
7.8 KiB
TypeScript
|
/* 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>
|
||
|
);
|
||
|
}
|