chom
This commit is contained in:
parent
c0b8ee2427
commit
2acdeb8c95
|
@ -14,6 +14,7 @@
|
||||||
"@types/react": "18.0.38",
|
"@types/react": "18.0.38",
|
||||||
"@types/react-dom": "18.0.11",
|
"@types/react-dom": "18.0.11",
|
||||||
"asn1": "^0.2.6",
|
"asn1": "^0.2.6",
|
||||||
|
"classnames": "^2.3.2",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"eslint": "8.39.0",
|
"eslint": "8.39.0",
|
||||||
"eslint-config-next": "13.3.1",
|
"eslint-config-next": "13.3.1",
|
||||||
|
@ -3138,6 +3139,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
|
"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": {
|
"node_modules/clean-stack": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
|
"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": {
|
"clean-stack": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
"@types/react": "18.0.38",
|
"@types/react": "18.0.38",
|
||||||
"@types/react-dom": "18.0.11",
|
"@types/react-dom": "18.0.11",
|
||||||
"asn1": "^0.2.6",
|
"asn1": "^0.2.6",
|
||||||
|
"classnames": "^2.3.2",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"eslint": "8.39.0",
|
"eslint": "8.39.0",
|
||||||
"eslint-config-next": "13.3.1",
|
"eslint-config-next": "13.3.1",
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
import ColorChanger from "@/components/ColorChanger";
|
import ColorChanger from "@/components/ColorChanger";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { Inter } from "next/font/google";
|
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "gluestick",
|
title: "gluestick",
|
||||||
|
@ -19,10 +16,11 @@ export default function RootLayout({
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<link rel="icon" href="/icon.svg" />
|
<link rel="icon" href="/icon.svg" />
|
||||||
|
{/* todo: lmfao */}
|
||||||
<meta property="og:image" content="/icon.svg" />
|
<meta property="og:image" content="/icon.svg" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body className={inter.className}>
|
<body>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
<ColorChanger />
|
<ColorChanger />
|
||||||
|
|
|
@ -54,7 +54,7 @@ export default function LoginForm() {
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
validationSchema={loginSchema}
|
validationSchema={loginSchema}
|
||||||
>
|
>
|
||||||
{() => (
|
{({ isSubmitting }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -68,7 +68,7 @@ export default function LoginForm() {
|
||||||
name="password"
|
name="password"
|
||||||
label="Password"
|
label="Password"
|
||||||
/>
|
/>
|
||||||
<input type="submit" value="Login" />
|
<input type="submit" value="Login" disabled={isSubmitting} />
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
|
|
|
@ -1,103 +1,26 @@
|
||||||
.content {
|
.content {
|
||||||
max-width: 500px;
|
max-width: 700px;
|
||||||
margin: auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.profileGrid {
|
||||||
padding: 1rem;
|
/* todo */
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
background-color: var(--fg-darker);
|
background-color: var(--fg-darker);
|
||||||
height: 1px;
|
height: 1px;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
||||||
|
margin: 1rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout {
|
||||||
|
background: var(--bg-dark);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0.15rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5em 1em;
|
||||||
}
|
}
|
|
@ -4,8 +4,16 @@
|
||||||
import { UserInfo } from "@/ldap";
|
import { UserInfo } from "@/ldap";
|
||||||
import React, { HTMLInputTypeAttribute, InputHTMLAttributes } from "react";
|
import React, { HTMLInputTypeAttribute, InputHTMLAttributes } from "react";
|
||||||
import styles from "./AboutMe.module.css";
|
import styles from "./AboutMe.module.css";
|
||||||
|
import AvatarChanger from "@/components/AvatarChanger";
|
||||||
const fallbackAvatar = "https://i.clong.biz/i/oc4zjlqr.png";
|
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 = {
|
type UpdateResponse = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
|
@ -21,35 +29,6 @@ type InputProps = {
|
||||||
displayImage?: string;
|
displayImage?: string;
|
||||||
} & InputHTMLAttributes<HTMLInputElement>;
|
} & 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) {
|
async function fileAsBase64(f: File) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.readAsArrayBuffer(f);
|
reader.readAsArrayBuffer(f);
|
||||||
|
@ -64,48 +43,31 @@ async function fileAsBase64(f: File) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AboutMe({ info }: { info: UserInfo }) {
|
export default function AboutMe({ info }: { info: UserInfo }) {
|
||||||
const displayNameRef = React.useRef<HTMLInputElement>(null);
|
const [globalError, setGlobalError] = React.useState<string | null>(null);
|
||||||
const emailRef = React.useRef<HTMLInputElement>(null);
|
const initialValues: AboutMeFormValues = {
|
||||||
const avatarRef = React.useRef<HTMLInputElement>(null);
|
username: info.username,
|
||||||
const submitRef = React.useRef<HTMLInputElement>(null);
|
displayName: info.displayName,
|
||||||
|
email: info.email,
|
||||||
|
avatar: info.avatar
|
||||||
|
};
|
||||||
|
|
||||||
const [avatar, setAvatar] = React.useState<string | null>(
|
async function handleFormSubmit(
|
||||||
info.avatar ?? null
|
{ displayName, email, avatar }: AboutMeFormValues,
|
||||||
);
|
{ setSubmitting }: FormikHelpers<AboutMeFormValues>
|
||||||
|
) {
|
||||||
const currentPasswordRef = React.useRef<HTMLInputElement>(null);
|
setSubmitting(true);
|
||||||
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", {
|
const req = await fetch("/api/update", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
displayName: displayNameRef.current?.value,
|
displayName,
|
||||||
email: emailRef.current?.value,
|
email,
|
||||||
avatar: avatarData
|
avatar: avatar != null ? avatar.split(",")[1] : null
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
submitRef.current!.disabled = false;
|
setSubmitting(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res: UpdateResponse = await req.json();
|
const res: UpdateResponse = await req.json();
|
||||||
|
@ -113,15 +75,65 @@ export default function AboutMe({ info }: { info: UserInfo }) {
|
||||||
if (!res.ok && res.error !== null) {
|
if (!res.ok && res.error !== null) {
|
||||||
switch (res.error) {
|
switch (res.error) {
|
||||||
case "avatarBig":
|
case "avatarBig":
|
||||||
setAvatarBig(true);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
console.error(req);
|
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.userName}>{info.username}</h2>
|
||||||
|
<PrettyForm globalError={globalError}>
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
validationSchema={aboutMeSchema}
|
||||||
>
|
>
|
||||||
|
{({ isSubmitting }) => (
|
||||||
|
<Form className={styles.profileGrid}>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
name="username"
|
name="username"
|
||||||
|
@ -132,160 +144,93 @@ export default function AboutMe({ info }: { info: UserInfo }) {
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
name="display-name"
|
name="displayName"
|
||||||
label="Display name"
|
label="Display name"
|
||||||
defaultValue={info.displayName}
|
defaultValue={info.displayName}
|
||||||
ref={displayNameRef}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
label="Email"
|
label="Email"
|
||||||
defaultValue={info.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
|
<Input
|
||||||
type="button"
|
type="file"
|
||||||
value="Choose file"
|
|
||||||
name="avatar"
|
name="avatar"
|
||||||
label="Avatar"
|
label="Avatar"
|
||||||
accept="image/png, image/jpeg"
|
accept="image/png, image/jpeg"
|
||||||
error={avatarBig ? "Avatar is too big." : undefined}
|
customRender={(fieldProps) => (
|
||||||
onClick={() => {
|
<AvatarChanger
|
||||||
avatarRef.current?.click();
|
currentAvatarBlob={fieldProps.field.value}
|
||||||
|
onChange={(newBlob) =>
|
||||||
const eventListener = async () => {
|
fieldProps.form.setFieldValue("avatar", newBlob)
|
||||||
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
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
value="Save"
|
value="Save"
|
||||||
ref={submitRef}
|
|
||||||
className={styles.fancyInput}
|
className={styles.fancyInput}
|
||||||
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Form>
|
||||||
</form>
|
)}
|
||||||
|
</Formik>
|
||||||
|
</PrettyForm>
|
||||||
|
|
||||||
<hr className={styles.divider} />
|
<hr className={styles.divider} />
|
||||||
|
|
||||||
<h2 className={styles.header}>Change password</h2>
|
<h2 className={styles.header}>Change password</h2>
|
||||||
<form
|
<PrettyForm globalError={passwordError}>
|
||||||
onSubmit={async (e) => {
|
<Formik
|
||||||
e.preventDefault();
|
initialValues={initialPasswordValues}
|
||||||
setIncorrectPassword(false);
|
onSubmit={handlePasswordSubmit}
|
||||||
setPasswordMismatch(false);
|
validationSchema={passwordUpdateSchema}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
{({ isSubmitting }) => (
|
||||||
|
<Form className={styles.profileGrid}>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
name="current-password"
|
name="password"
|
||||||
label="Current"
|
label="Current"
|
||||||
minLength={12}
|
minLength={12}
|
||||||
required
|
required
|
||||||
ref={currentPasswordRef}
|
|
||||||
error={incorrectPassword ? "Incorrect password." : undefined}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
name="new-password"
|
name="newPassword"
|
||||||
label="New"
|
label="New"
|
||||||
minLength={12}
|
minLength={12}
|
||||||
required
|
required
|
||||||
ref={newPasswordRef}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
name="confirm-password"
|
name="confirmPassword"
|
||||||
label="Confirm"
|
label="Confirm"
|
||||||
ref={confirmPasswordRef}
|
|
||||||
minLength={12}
|
minLength={12}
|
||||||
required
|
required
|
||||||
error={passwordMismatch ? "Passwords do not match." : undefined}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.formRow}>
|
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
value="Change password"
|
value="Save"
|
||||||
ref={submitPasswordRef}
|
|
||||||
className={styles.fancyInput}
|
className={styles.fancyInput}
|
||||||
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Form>
|
||||||
</form>
|
)}
|
||||||
|
</Formik>
|
||||||
|
</PrettyForm>
|
||||||
|
|
||||||
<hr className={styles.divider} />
|
<hr className={styles.divider} />
|
||||||
|
|
||||||
<div className={styles.formRow}>
|
|
||||||
<input
|
<input
|
||||||
type="button"
|
type="button"
|
||||||
value="Log out"
|
value="Log out"
|
||||||
className={styles.fancyInput}
|
className={styles.logout}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
document.cookie =
|
document.cookie =
|
||||||
"ticket=; expires=" + new Date().toUTCString() + "; path=/";
|
"ticket=; expires=" + new Date().toUTCString() + "; path=/";
|
||||||
|
@ -293,6 +238,5 @@ export default function AboutMe({ info }: { info: UserInfo }) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ export default async function Page() {
|
||||||
if (!user) redirect("/login");
|
if (!user) redirect("/login");
|
||||||
|
|
||||||
const info = await getUserInfo(user);
|
const info = await getUserInfo(user);
|
||||||
if (info === null) redirect("/login");
|
if (info === null) redirect("/register");
|
||||||
|
|
||||||
return <AboutMe info={info} />;
|
return <AboutMe info={info} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { fileAsBase64 } from "@/forms";
|
||||||
import Input from "@/components/Input";
|
import Input from "@/components/Input";
|
||||||
import PrettyForm from "@/components/PrettyForm";
|
import PrettyForm from "@/components/PrettyForm";
|
||||||
import HugeSubmit from "@/components/HugeSubmit";
|
import HugeSubmit from "@/components/HugeSubmit";
|
||||||
|
import AvatarChanger from "@/components/AvatarChanger";
|
||||||
|
|
||||||
type RegisterResponse = {
|
type RegisterResponse = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
|
@ -27,14 +28,14 @@ export default function RegisterForm({
|
||||||
const [globalError, setGlobalError] = React.useState<string | null>(null);
|
const [globalError, setGlobalError] = React.useState<string | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [initialValues, setInitialValues] = React.useState<RegisterFormValues>({
|
const initialValues: RegisterFormValues = {
|
||||||
username: "",
|
username: "",
|
||||||
displayName: initialDisplayName ?? "",
|
displayName: initialDisplayName ?? "",
|
||||||
email: initialEmail ?? "",
|
email: initialEmail ?? "",
|
||||||
password: "",
|
password: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
avatar: undefined
|
avatar: initialAvatarBase64
|
||||||
});
|
};
|
||||||
|
|
||||||
async function handleFormSubmit(
|
async function handleFormSubmit(
|
||||||
{ avatar, username, displayName, email, password }: RegisterFormValues,
|
{ avatar, username, displayName, email, password }: RegisterFormValues,
|
||||||
|
@ -42,11 +43,6 @@ export default function RegisterForm({
|
||||||
) {
|
) {
|
||||||
setSubmitting(true);
|
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`, {
|
const resp = await fetch(`/api/register`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -57,7 +53,7 @@ export default function RegisterForm({
|
||||||
displayName,
|
displayName,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
avatarBase64
|
avatarBase64: avatar != null ? avatar.split(",")[1] : undefined
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -144,19 +140,20 @@ export default function RegisterForm({
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
hint={
|
hint={
|
||||||
"This image will automatically be used as your avatar with supported services - maximum 1 MB. " +
|
"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."
|
|
||||||
}
|
}
|
||||||
type="file"
|
type="file"
|
||||||
name="avatar"
|
name="avatar"
|
||||||
label="Avatar"
|
label="Avatar"
|
||||||
accept="image/png, image/jpeg"
|
accept="image/png, image/jpeg"
|
||||||
customOnChange={(event, form) => {
|
customRender={(fieldProps) => (
|
||||||
const file = event.currentTarget.files?.[0];
|
<AvatarChanger
|
||||||
if (file != null) {
|
currentAvatarBlob={fieldProps.field.value}
|
||||||
form.setFieldValue("avatar", file);
|
onChange={(newBlob) =>
|
||||||
|
fieldProps.form.setFieldValue("avatar", newBlob)
|
||||||
}
|
}
|
||||||
}}
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
|
|
|
@ -32,7 +32,7 @@ export default async function Page({
|
||||||
if (searchParams.avatar != null && searchParams.avatar !== "") {
|
if (searchParams.avatar != null && searchParams.avatar !== "") {
|
||||||
const url = new URL(searchParams.avatar);
|
const url = new URL(searchParams.avatar);
|
||||||
|
|
||||||
if (avatarUrlAllowed(url)) {
|
if (!avatarUrlAllowed(url)) {
|
||||||
return <p>fuck off</p>;
|
return <p>fuck off</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,14 +40,16 @@ export default async function Page({
|
||||||
const blob = await req.blob();
|
const blob = await req.blob();
|
||||||
const arrayBuffer = await blob.arrayBuffer();
|
const arrayBuffer = await blob.arrayBuffer();
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
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 {
|
try {
|
||||||
initialAvatarBase64 =
|
const jpg = await ensureJpg(buffer);
|
||||||
"data:image/jpeg;base64," + (await ensureJpg(buffer));
|
initialAvatarBase64 = "data:image/jpeg;base64," + jpg;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -9,14 +9,14 @@ type ColorScheme = {
|
||||||
|
|
||||||
bg: string;
|
bg: string;
|
||||||
bgDark: string;
|
bgDark: string;
|
||||||
bgDarker?: string;
|
bgDarker: string;
|
||||||
|
|
||||||
fg: string;
|
fg: string;
|
||||||
fgDark: string;
|
fgDark: string;
|
||||||
fgDarker?: string;
|
fgDarker: string;
|
||||||
|
|
||||||
error?: string;
|
error: string;
|
||||||
warning?: string;
|
warning: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const colors: ColorScheme[] = [
|
const colors: ColorScheme[] = [
|
||||||
|
@ -153,8 +153,10 @@ function set(colorScheme: ColorScheme) {
|
||||||
const fixedColors = {
|
const fixedColors = {
|
||||||
"--bg": colorScheme.bg,
|
"--bg": colorScheme.bg,
|
||||||
"--bg-dark": colorScheme.bgDark,
|
"--bg-dark": colorScheme.bgDark,
|
||||||
|
"--bg-darker": colorScheme.bgDarker,
|
||||||
"--fg": colorScheme.fg,
|
"--fg": colorScheme.fg,
|
||||||
"--fg-dark": colorScheme.fgDark,
|
"--fg-dark": colorScheme.fgDark,
|
||||||
|
"--fg-darker": colorScheme.fgDarker,
|
||||||
"--error": colorScheme.error ?? fallback.error!,
|
"--error": colorScheme.error ?? fallback.error!,
|
||||||
"--warning": colorScheme.warning ?? fallback.warning!
|
"--warning": colorScheme.warning ?? fallback.warning!
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,8 @@ import React from "react";
|
||||||
import styles from "./Input.module.css";
|
import styles from "./Input.module.css";
|
||||||
|
|
||||||
type CustomInputProps<T> = {
|
type CustomInputProps<T> = {
|
||||||
|
customRender?: (fieldProps: FieldProps) => React.ReactNode;
|
||||||
|
|
||||||
customOnChange?: (
|
customOnChange?: (
|
||||||
event: React.ChangeEvent<HTMLInputElement>,
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
form: FormikProps<T>
|
form: FormikProps<T>
|
||||||
|
@ -10,7 +12,8 @@ type CustomInputProps<T> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Input<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();
|
const generatedId = React.useId();
|
||||||
|
|
||||||
|
@ -18,7 +21,8 @@ export default function Input<T>(
|
||||||
<div className={styles.formRow}>
|
<div className={styles.formRow}>
|
||||||
<label htmlFor={generatedId}>{props.label}</label>
|
<label htmlFor={generatedId}>{props.label}</label>
|
||||||
<Field id={generatedId} {...props}>
|
<Field id={generatedId} {...props}>
|
||||||
{({ field, meta, form }: FieldProps) => {
|
{(fieldProps: FieldProps) => {
|
||||||
|
let { field, meta, form } = fieldProps;
|
||||||
let textAfterField =
|
let textAfterField =
|
||||||
meta.touched && meta.error ? (
|
meta.touched && meta.error ? (
|
||||||
<p className={styles.error}>{meta.error}</p>
|
<p className={styles.error}>{meta.error}</p>
|
||||||
|
@ -39,9 +43,12 @@ export default function Input<T>(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{props.customRender == null ? (
|
||||||
<input
|
<input
|
||||||
type={props.type}
|
type={props.type}
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
|
disabled={props.disabled}
|
||||||
|
title={props.title}
|
||||||
{...inputFields}
|
{...inputFields}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
console.log(event);
|
console.log(event);
|
||||||
|
@ -53,6 +60,9 @@ export default function Input<T>(
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
props.customRender(fieldProps)
|
||||||
|
)}
|
||||||
{textAfterField}
|
{textAfterField}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,9 +4,33 @@ const REQUIRED = "Required.";
|
||||||
const USERNAME = Yup.string()
|
const USERNAME = Yup.string()
|
||||||
.required(REQUIRED)
|
.required(REQUIRED)
|
||||||
.min(1, "Username is too short.");
|
.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()
|
const PASSWORD = Yup.string()
|
||||||
.required(REQUIRED)
|
.required(REQUIRED)
|
||||||
.min(12, "Password must be at least 12 characters long.");
|
.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({
|
export const loginSchema = Yup.object().shape({
|
||||||
username: USERNAME,
|
username: USERNAME,
|
||||||
|
@ -21,20 +45,11 @@ export type LoginFormValues = {
|
||||||
export const registerSchema: Yup.Schema<RegisterFormValues> =
|
export const registerSchema: Yup.Schema<RegisterFormValues> =
|
||||||
Yup.object().shape({
|
Yup.object().shape({
|
||||||
username: USERNAME,
|
username: USERNAME,
|
||||||
displayName: Yup.string()
|
displayName: DISPLAY_NAME,
|
||||||
.required(REQUIRED)
|
email: EMAIL,
|
||||||
.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."),
|
|
||||||
password: PASSWORD,
|
password: PASSWORD,
|
||||||
avatar: Yup.mixed<File>()
|
confirmPassword: CONFIRM_PASSWORD("password"),
|
||||||
.test("fileSize", "File is larger than 1 MB.", (value) => {
|
avatar: AVATAR
|
||||||
if (value == null) return true;
|
|
||||||
return value.size <= 1_000_000;
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface RegisterFormValues {
|
export interface RegisterFormValues {
|
||||||
|
@ -43,5 +58,32 @@ export interface RegisterFormValues {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
confirmPassword: 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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue