This commit is contained in:
Skip R. 2023-04-26 19:59:17 -07:00
parent c0b8ee2427
commit 2acdeb8c95
17 changed files with 476 additions and 401 deletions

11
package-lock.json generated
View File

@ -14,6 +14,7 @@
"@types/react": "18.0.38",
"@types/react-dom": "18.0.11",
"asn1": "^0.2.6",
"classnames": "^2.3.2",
"dotenv": "^16.0.3",
"eslint": "8.39.0",
"eslint-config-next": "13.3.1",
@ -3138,6 +3139,11 @@
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"node_modules/classnames": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
},
"node_modules/clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@ -10555,6 +10561,11 @@
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"classnames": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
},
"clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",

View File

@ -18,6 +18,7 @@
"@types/react": "18.0.38",
"@types/react-dom": "18.0.11",
"asn1": "^0.2.6",
"classnames": "^2.3.2",
"dotenv": "^16.0.3",
"eslint": "8.39.0",
"eslint-config-next": "13.3.1",

View File

@ -1,8 +1,5 @@
import ColorChanger from "@/components/ColorChanger";
import "./globals.css";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "gluestick",
@ -19,10 +16,11 @@ export default function RootLayout({
<html lang="en">
<head>
<link rel="icon" href="/icon.svg" />
{/* todo: lmfao */}
<meta property="og:image" content="/icon.svg" />
</head>
<body className={inter.className}>
<body>
{children}
<ColorChanger />

View File

@ -54,7 +54,7 @@ export default function LoginForm() {
onSubmit={handleFormSubmit}
validationSchema={loginSchema}
>
{() => (
{({ isSubmitting }) => (
<Form>
<Input
type="text"
@ -68,7 +68,7 @@ export default function LoginForm() {
name="password"
label="Password"
/>
<input type="submit" value="Login" />
<input type="submit" value="Login" disabled={isSubmitting} />
</Form>
)}
</Formik>

View File

@ -1,103 +1,26 @@
.content {
max-width: 500px;
margin: auto;
max-width: 700px;
margin: 0 auto;
}
.header {
padding: 1rem;
padding-bottom: 0;
}
.form {
max-width: 500px;
}
.form input[type="submit"] {
padding: 1rem 1.5rem;
font-size: 140%;
background: var(--bg-dark);
border: 0;
border-radius: 0.15rem;
cursor: pointer;
font-weight: 600;
}
.buttonContainer {
display: flex;
justify-content: center;
margin: 2rem 0;
}
.buttonContainer input:disabled {
cursor: not-allowed;
color: var(--fg-dark);
}
.formRow {
margin: 1rem 0;
display: flex;
flex-direction: row;
justify-content: center;
}
.formRow label {
font-variant: all-small-caps;
font-size: 105%;
width: 100px;
height: 50px;
/* center */
display: flex;
align-items: center;
}
.formVert {
flex-direction: column;
align-items: center;
}
.fancyInput {
padding: 0.5em 1em;
border: none;
border-radius: 0.15rem;
margin: 0.5rem 0;
width: 250px;
display: block;
background: var(--bg-dark);
}
.formRow input[name="avatar"] {
width: 190px;
}
.formRow .avatar {
margin-right: 10px;
border-radius: 10%;
}
.formRow input:disabled {
cursor: not-allowed;
background: var(--bg-darker);
color: var(--fg-darker);
}
.hint {
color: var(--fg-dark);
font-size: 80%;
transition: color var(--theme-transition);
}
.error {
color: var(--error);
font-size: 80%;
transition: color var(--theme-transition);
.profileGrid {
/* todo */
}
.divider {
width: 400px;
margin: auto;
background-color: var(--fg-darker);
height: 1px;
border: none;
margin: 1rem auto;
}
.logout {
background: var(--bg-dark);
border: 0;
border-radius: 0.15rem;
cursor: pointer;
padding: 0.5em 1em;
}

View File

@ -4,8 +4,16 @@
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";
import AvatarChanger from "@/components/AvatarChanger";
import Input from "@/components/Input";
import { Form, Formik, FormikHelpers } from "formik";
import {
AboutMeFormValues,
PasswordUpdateFormValues,
aboutMeSchema,
passwordUpdateSchema
} from "@/schemas";
import PrettyForm from "@/components/PrettyForm";
type UpdateResponse = {
ok: boolean;
@ -21,35 +29,6 @@ type InputProps = {
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);
@ -64,235 +43,200 @@ async function fileAsBase64(f: File) {
}
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 [globalError, setGlobalError] = React.useState<string | null>(null);
const initialValues: AboutMeFormValues = {
username: info.username,
displayName: info.displayName,
email: info.email,
avatar: info.avatar
};
const [avatar, setAvatar] = React.useState<string | null>(
info.avatar ?? null
);
async function handleFormSubmit(
{ displayName, email, avatar }: AboutMeFormValues,
{ setSubmitting }: FormikHelpers<AboutMeFormValues>
) {
setSubmitting(true);
const req = await fetch("/api/update", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
displayName,
email,
avatar: avatar != null ? avatar.split(",")[1] : null
})
});
setSubmitting(false);
const currentPasswordRef = React.useRef<HTMLInputElement>(null);
const newPasswordRef = React.useRef<HTMLInputElement>(null);
const confirmPasswordRef = React.useRef<HTMLInputElement>(null);
const submitPasswordRef = React.useRef<HTMLInputElement>(null);
try {
const res: UpdateResponse = await req.json();
const [incorrectPassword, setIncorrectPassword] = React.useState(false);
const [passwordMismatch, setPasswordMismatch] = React.useState(false);
const [avatarBig, setAvatarBig] = React.useState(false);
if (!res.ok && res.error !== null) {
switch (res.error) {
case "avatarBig":
break;
}
}
} catch {
console.error(req);
}
}
const [passwordError, setPasswordError] = React.useState<string | null>(null);
const initialPasswordValues: PasswordUpdateFormValues = {
password: "",
newPassword: "",
confirmPassword: ""
};
async function handlePasswordSubmit(
{ password, newPassword }: PasswordUpdateFormValues,
{ setFieldError, setSubmitting }: FormikHelpers<PasswordUpdateFormValues>
) {
console.log(password, newPassword);
setSubmitting(true);
const req = await fetch("/api/changePassword", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
currentPassword: password,
newPassword: newPassword
})
});
setSubmitting(false);
try {
const res: UpdateResponse = await req.json();
if (!res.ok && res.error !== null) {
switch (res.error) {
case "incorrectPassword":
setFieldError("password", "Incorrect password.");
break;
}
}
} catch {
console.error(req);
}
}
return (
<div className={styles.content}>
<h2 className={styles.header}>User information</h2>
<form
onSubmit={async (e) => {
e.preventDefault();
<h2 className={styles.userName}>{info.username}</h2>
<PrettyForm globalError={globalError}>
<Formik
initialValues={initialValues}
onSubmit={handleFormSubmit}
validationSchema={aboutMeSchema}
>
{({ isSubmitting }) => (
<Form className={styles.profileGrid}>
<Input
type="text"
name="username"
label="Username"
defaultValue={info.username}
disabled
title="You can't change your username."
/>
<Input
type="text"
name="displayName"
label="Display name"
defaultValue={info.displayName}
/>
<Input
type="email"
name="email"
label="Email"
defaultValue={info.email}
/>
// turn the data uri into just base64
const avatarChanged = avatar !== null && avatar !== info.avatar;
const avatarData = avatarChanged ? avatar?.split(",")[1] : null;
<Input
type="file"
name="avatar"
label="Avatar"
accept="image/png, image/jpeg"
customRender={(fieldProps) => (
<AvatarChanger
currentAvatarBlob={fieldProps.field.value}
onChange={(newBlob) =>
fieldProps.form.setFieldValue("avatar", newBlob)
}
/>
)}
/>
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>
<input
type="submit"
value="Save"
className={styles.fancyInput}
disabled={isSubmitting}
/>
</Form>
)}
</Formik>
</PrettyForm>
<hr className={styles.divider} />
<h2 className={styles.header}>Change password</h2>
<form
onSubmit={async (e) => {
e.preventDefault();
setIncorrectPassword(false);
setPasswordMismatch(false);
<PrettyForm globalError={passwordError}>
<Formik
initialValues={initialPasswordValues}
onSubmit={handlePasswordSubmit}
validationSchema={passwordUpdateSchema}
>
{({ isSubmitting }) => (
<Form className={styles.profileGrid}>
<Input
type="password"
name="password"
label="Current"
minLength={12}
required
/>
if (
newPasswordRef.current?.value !== confirmPasswordRef.current?.value
) {
setPasswordMismatch(true);
return;
}
<Input
type="password"
name="newPassword"
label="New"
minLength={12}
required
/>
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;
<Input
type="password"
name="confirmPassword"
label="Confirm"
minLength={12}
required
/>
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>
<input
type="submit"
value="Save"
className={styles.fancyInput}
disabled={isSubmitting}
/>
</Form>
)}
</Formik>
</PrettyForm>
<hr className={styles.divider} />
<div className={styles.formRow}>
<input
type="button"
value="Log out"
className={styles.fancyInput}
onClick={async () => {
document.cookie =
"ticket=; expires=" + new Date().toUTCString() + "; path=/";
window.location.href = "/";
}}
/>
</div>
<input
type="button"
value="Log out"
className={styles.logout}
onClick={async () => {
document.cookie =
"ticket=; expires=" + new Date().toUTCString() + "; path=/";
window.location.href = "/";
}}
/>
</div>
);
}

View File

@ -8,7 +8,7 @@ export default async function Page() {
if (!user) redirect("/login");
const info = await getUserInfo(user);
if (info === null) redirect("/login");
if (info === null) redirect("/register");
return <AboutMe info={info} />;
}

View File

@ -9,6 +9,7 @@ import { fileAsBase64 } from "@/forms";
import Input from "@/components/Input";
import PrettyForm from "@/components/PrettyForm";
import HugeSubmit from "@/components/HugeSubmit";
import AvatarChanger from "@/components/AvatarChanger";
type RegisterResponse = {
ok: boolean;
@ -27,14 +28,14 @@ export default function RegisterForm({
const [globalError, setGlobalError] = React.useState<string | null>(null);
const router = useRouter();
const [initialValues, setInitialValues] = React.useState<RegisterFormValues>({
const initialValues: RegisterFormValues = {
username: "",
displayName: initialDisplayName ?? "",
email: initialEmail ?? "",
password: "",
confirmPassword: "",
avatar: undefined
});
avatar: initialAvatarBase64
};
async function handleFormSubmit(
{ avatar, username, displayName, email, password }: RegisterFormValues,
@ -42,11 +43,6 @@ export default function RegisterForm({
) {
setSubmitting(true);
let avatarBase64 = avatar != null ? await fileAsBase64(avatar) : null;
if (avatarBase64 == null && initialAvatarBase64 != null) {
avatarBase64 = initialAvatarBase64.split(",")[1];
}
const resp = await fetch(`/api/register`, {
method: "POST",
headers: {
@ -57,7 +53,7 @@ export default function RegisterForm({
displayName,
email,
password,
avatarBase64
avatarBase64: avatar != null ? avatar.split(",")[1] : undefined
})
});
@ -144,19 +140,20 @@ export default function RegisterForm({
<Input
hint={
"This image will automatically be used as your avatar with supported services - maximum 1 MB. " +
"Will use the avatar of the service you signed up with if not provided."
"This image will automatically be used as your avatar with supported services - maximum 1 MB. "
}
type="file"
name="avatar"
label="Avatar"
accept="image/png, image/jpeg"
customOnChange={(event, form) => {
const file = event.currentTarget.files?.[0];
if (file != null) {
form.setFieldValue("avatar", file);
}
}}
customRender={(fieldProps) => (
<AvatarChanger
currentAvatarBlob={fieldProps.field.value}
onChange={(newBlob) =>
fieldProps.form.setFieldValue("avatar", newBlob)
}
/>
)}
/>
<div className={styles.buttonContainer}>

View File

@ -32,7 +32,7 @@ export default async function Page({
if (searchParams.avatar != null && searchParams.avatar !== "") {
const url = new URL(searchParams.avatar);
if (avatarUrlAllowed(url)) {
if (!avatarUrlAllowed(url)) {
return <p>fuck off</p>;
}
@ -40,12 +40,14 @@ export default async function Page({
const blob = await req.blob();
const arrayBuffer = await blob.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
try {
initialAvatarBase64 =
"data:image/jpeg;base64," + (await ensureJpg(buffer));
} catch (e) {
console.error(e);
if (buffer.length <= 1_000_000) {
// I hope you are doing well, you deserve the best of luck while working on this project -Ari
try {
const jpg = await ensureJpg(buffer);
initialAvatarBase64 = "data:image/jpeg;base64," + jpg;
} catch (e) {
console.error(e);
}
}
}

View File

@ -0,0 +1,33 @@
.avatarChanger {
display: flex;
flex-flow: row nowrap;
gap: 1rem;
margin: 0.5rem 0;
}
.avatarChanger :is(img, svg) {
width: 3em;
height: 3em;
border-radius: 0.25rem;
}
.avatarChanger button svg {
width: 1.5em;
height: 1.5em;
margin-right: 0.5em;
}
.avatarChanger input[type=file] {
display: none;
}
.uploadButton {
display: flex;
align-items: center;
background: var(--bg-dark);
border: 0;
border-radius: 0.15rem;
padding: 0.25em 1em;
cursor: pointer;
}

View File

@ -0,0 +1,53 @@
import React, { ChangeEvent } from "react";
import classnames from "classnames";
import styles from "./AvatarChanger.module.css";
import { fileAsBase64 } from "@/forms";
import UploadIcon from "./icons/UploadIcon";
import UserIcon from "./icons/UserIcon";
export default function AvatarChanger({
currentAvatarBlob,
onChange
}: {
currentAvatarBlob: string | null;
onChange: (newAvatar: string) => void;
}) {
const input = React.useRef<HTMLInputElement>(null);
async function handleFileChange(event: ChangeEvent<HTMLInputElement>) {
const file = event.currentTarget.files?.[0];
if (file == null) return;
const base64 = await fileAsBase64(file);
onChange(`data:${file.type};base64,${base64}`);
}
// I give you the most support and well wishes while you work on this project -Ari
return (
<div className={classnames(styles.avatarChanger, "avatar-changer")}>
{currentAvatarBlob != null ? (
<img src={currentAvatarBlob!} alt="Your avatar" />
) : (
<UserIcon />
)}
<button
type="button"
className={styles.uploadButton}
onClick={() => {
input.current?.click();
}}
>
<UploadIcon />
Upload a new avatar
</button>
<input
type="file"
accept="image/png, image/jpeg"
ref={input}
onChange={handleFileChange}
/>
</div>
);
}

View File

@ -9,14 +9,14 @@ type ColorScheme = {
bg: string;
bgDark: string;
bgDarker?: string;
bgDarker: string;
fg: string;
fgDark: string;
fgDarker?: string;
fgDarker: string;
error?: string;
warning?: string;
error: string;
warning: string;
};
const colors: ColorScheme[] = [
@ -153,8 +153,10 @@ function set(colorScheme: ColorScheme) {
const fixedColors = {
"--bg": colorScheme.bg,
"--bg-dark": colorScheme.bgDark,
"--bg-darker": colorScheme.bgDarker,
"--fg": colorScheme.fg,
"--fg-dark": colorScheme.fgDark,
"--fg-darker": colorScheme.fgDarker,
"--error": colorScheme.error ?? fallback.error!,
"--warning": colorScheme.warning ?? fallback.warning!
};

View File

@ -3,6 +3,8 @@ import React from "react";
import styles from "./Input.module.css";
type CustomInputProps<T> = {
customRender?: (fieldProps: FieldProps) => React.ReactNode;
customOnChange?: (
event: React.ChangeEvent<HTMLInputElement>,
form: FormikProps<T>
@ -10,7 +12,8 @@ type CustomInputProps<T> = {
};
export default function Input<T>(
props: CustomInputProps<T> & FieldAttributes<{ hint?: string; label: string }>
props: CustomInputProps<T> &
FieldAttributes<{ hint?: string; label: string; disabled?: boolean }>
) {
const generatedId = React.useId();
@ -18,7 +21,8 @@ export default function Input<T>(
<div className={styles.formRow}>
<label htmlFor={generatedId}>{props.label}</label>
<Field id={generatedId} {...props}>
{({ field, meta, form }: FieldProps) => {
{(fieldProps: FieldProps) => {
let { field, meta, form } = fieldProps;
let textAfterField =
meta.touched && meta.error ? (
<p className={styles.error}>{meta.error}</p>
@ -39,20 +43,26 @@ export default function Input<T>(
return (
<>
<input
type={props.type}
placeholder={props.placeholder}
{...inputFields}
onChange={(event) => {
console.log(event);
if (props.customOnChange) {
console.log("using custom on change");
props.customOnChange(event, form);
} else {
form.setFieldValue(field.name, event.currentTarget.value);
}
}}
/>
{props.customRender == null ? (
<input
type={props.type}
placeholder={props.placeholder}
disabled={props.disabled}
title={props.title}
{...inputFields}
onChange={(event) => {
console.log(event);
if (props.customOnChange) {
console.log("using custom on change");
props.customOnChange(event, form);
} else {
form.setFieldValue(field.name, event.currentTarget.value);
}
}}
/>
) : (
props.customRender(fieldProps)
)}
{textAfterField}
</>
);

View File

@ -14,4 +14,4 @@
color: var(--error);
font-size: 80%;
transition: color var(--theme-transition);
}
}

View File

@ -0,0 +1,39 @@
import React from "react";
export default function UploadIcon() {
return (
<svg
viewBox="0 0 128 128"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<g
stroke="none"
strokeWidth="1"
fill="none"
fillRule="evenodd"
strokeLinecap="round"
>
<path
d="M16,64 L16,92 C16,103.045695 24.954305,112 36,112 L92,112 C103.045695,112 112,103.045695 112,92 L112,64 L112,64"
stroke="currentColor"
strokeWidth="13"
></path>
<line
x1="64"
y1="80"
x2="64"
y2="16"
stroke="currentColor"
strokeWidth="13"
></line>
<polyline
stroke="currentColor"
strokeWidth="13"
points="32 48 64 16 96 48"
></polyline>
</g>
</svg>
);
}

View File

@ -0,0 +1,20 @@
import React from "react";
export default function UserIcon() {
return (
<svg
viewBox="0 0 128 128"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<circle fill="currentColor" cx="64" cy="48" r="32"></circle>
<path
d="M112,128 C112,101.490332 90.509668,80 64,80 C37.490332,80 16,101.490332 16,128 C16,128 112,128 112,128 Z"
fill="currentColor"
></path>
</g>
</svg>
);
}

View File

@ -4,9 +4,33 @@ const REQUIRED = "Required.";
const USERNAME = Yup.string()
.required(REQUIRED)
.min(1, "Username is too short.");
const DISPLAY_NAME = Yup.string()
.required(REQUIRED)
.min(1, "Display name is too short.");
const EMAIL = Yup.string().required(REQUIRED).email("Not an email.");
const PASSWORD = Yup.string()
.required(REQUIRED)
.min(12, "Password must be at least 12 characters long.");
const CONFIRM_PASSWORD = (name: string) =>
Yup.string()
.required(REQUIRED)
.oneOf([Yup.ref(name, {})], "Passwords must match.");
const AVATAR = Yup.string().test(
"file-size",
"File is bigger than 1 MB.",
(value) => {
if (value == null) return true;
try {
const buf = Buffer.from(value, "base64");
return buf.length <= 1_000_000;
} catch (e) {
return false;
}
}
);
export const loginSchema = Yup.object().shape({
username: USERNAME,
@ -21,20 +45,11 @@ export type LoginFormValues = {
export const registerSchema: Yup.Schema<RegisterFormValues> =
Yup.object().shape({
username: USERNAME,
displayName: Yup.string()
.required(REQUIRED)
.min(1, "Display name is too short."),
email: Yup.string().required(REQUIRED).email("Not an email."),
confirmPassword: Yup.string()
.required(REQUIRED)
.oneOf([Yup.ref("password", {})], "Passwords must match."),
displayName: DISPLAY_NAME,
email: EMAIL,
password: PASSWORD,
avatar: Yup.mixed<File>()
.test("fileSize", "File is larger than 1 MB.", (value) => {
if (value == null) return true;
return value.size <= 1_000_000;
})
.optional()
confirmPassword: CONFIRM_PASSWORD("password"),
avatar: AVATAR
});
export interface RegisterFormValues {
@ -43,5 +58,32 @@ export interface RegisterFormValues {
email: string;
password: string;
confirmPassword: string;
avatar?: File;
avatar?: string;
}
export const aboutMeSchema: Yup.Schema<AboutMeFormValues> = Yup.object().shape({
username: USERNAME,
displayName: DISPLAY_NAME,
email: EMAIL,
avatar: AVATAR
});
export interface AboutMeFormValues {
username: string;
displayName: string;
email: string;
avatar?: string;
}
export const passwordUpdateSchema: Yup.Schema<PasswordUpdateFormValues> =
Yup.object().shape({
password: PASSWORD,
newPassword: PASSWORD,
confirmPassword: CONFIRM_PASSWORD("newPassword")
});
export interface PasswordUpdateFormValues {
password: string;
newPassword: string;
confirmPassword: string;
}