diff --git a/package-lock.json b/package-lock.json
index 888b8d5..cacab2c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index a837096..36be349 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index e728ffa..a908d4a 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -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({
+ {/* todo: lmfao */}
-
+
{children}
diff --git a/src/app/login/LoginForm.tsx b/src/app/login/LoginForm.tsx
index dd04a61..f12c8f8 100644
--- a/src/app/login/LoginForm.tsx
+++ b/src/app/login/LoginForm.tsx
@@ -54,7 +54,7 @@ export default function LoginForm() {
onSubmit={handleFormSubmit}
validationSchema={loginSchema}
>
- {() => (
+ {({ isSubmitting }) => (
)}
diff --git a/src/app/me/AboutMe.module.css b/src/app/me/AboutMe.module.css
index d07e1ff..71ebbf7 100644
--- a/src/app/me/AboutMe.module.css
+++ b/src/app/me/AboutMe.module.css
@@ -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;
+}
\ No newline at end of file
diff --git a/src/app/me/AboutMe.tsx b/src/app/me/AboutMe.tsx
index df3a668..4fce5b7 100644
--- a/src/app/me/AboutMe.tsx
+++ b/src/app/me/AboutMe.tsx
@@ -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;
-const Input = React.forwardRef((props, ref) => {
- // get console to shut up
- const inputProps = { ...props };
- delete inputProps.displayImage;
-
- return (
-
-
-
- {props.displayImage && (
-
- )}
-
-
-
-
- {props.error != null &&
{props.error}
}
-
-
- );
-});
-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(null);
- const emailRef = React.useRef(null);
- const avatarRef = React.useRef(null);
- const submitRef = React.useRef(null);
+ const [globalError, setGlobalError] = React.useState(null);
+ const initialValues: AboutMeFormValues = {
+ username: info.username,
+ displayName: info.displayName,
+ email: info.email,
+ avatar: info.avatar
+ };
- const [avatar, setAvatar] = React.useState(
- info.avatar ?? null
- );
+ async function handleFormSubmit(
+ { displayName, email, avatar }: AboutMeFormValues,
+ { setSubmitting }: FormikHelpers
+ ) {
+ 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(null);
- const newPasswordRef = React.useRef(null);
- const confirmPasswordRef = React.useRef(null);
- const submitPasswordRef = React.useRef(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(null);
+ const initialPasswordValues: PasswordUpdateFormValues = {
+ password: "",
+ newPassword: "",
+ confirmPassword: ""
+ };
+
+ async function handlePasswordSubmit(
+ { password, newPassword }: PasswordUpdateFormValues,
+ { setFieldError, setSubmitting }: FormikHelpers
+ ) {
+ 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 (
);
}
diff --git a/src/app/me/page.tsx b/src/app/me/page.tsx
index 70f611b..1b6731d 100644
--- a/src/app/me/page.tsx
+++ b/src/app/me/page.tsx
@@ -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 ;
}
diff --git a/src/app/register/RegisterForm.tsx b/src/app/register/RegisterForm.tsx
index 0367110..bef3e61 100644
--- a/src/app/register/RegisterForm.tsx
+++ b/src/app/register/RegisterForm.tsx
@@ -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(null);
const router = useRouter();
- const [initialValues, setInitialValues] = React.useState({
+ 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({
{
- const file = event.currentTarget.files?.[0];
- if (file != null) {
- form.setFieldValue("avatar", file);
- }
- }}
+ customRender={(fieldProps) => (
+
+ fieldProps.form.setFieldValue("avatar", newBlob)
+ }
+ />
+ )}
/>
diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx
index b7a6b8c..d609a98 100644
--- a/src/app/register/page.tsx
+++ b/src/app/register/page.tsx
@@ -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
fuck off
;
}
@@ -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);
+ }
}
}
diff --git a/src/components/AvatarChanger.module.css b/src/components/AvatarChanger.module.css
new file mode 100644
index 0000000..9f95934
--- /dev/null
+++ b/src/components/AvatarChanger.module.css
@@ -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;
+}
\ No newline at end of file
diff --git a/src/components/AvatarChanger.tsx b/src/components/AvatarChanger.tsx
new file mode 100644
index 0000000..d8b57ad
--- /dev/null
+++ b/src/components/AvatarChanger.tsx
@@ -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
(null);
+
+ async function handleFileChange(event: ChangeEvent) {
+ 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 (
+
+ {currentAvatarBlob != null ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/components/ColorChanger.tsx b/src/components/ColorChanger.tsx
index 2db31b4..6165c59 100644
--- a/src/components/ColorChanger.tsx
+++ b/src/components/ColorChanger.tsx
@@ -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!
};
diff --git a/src/components/Input.tsx b/src/components/Input.tsx
index c5c79c5..3ca4191 100644
--- a/src/components/Input.tsx
+++ b/src/components/Input.tsx
@@ -3,6 +3,8 @@ import React from "react";
import styles from "./Input.module.css";
type CustomInputProps = {
+ customRender?: (fieldProps: FieldProps) => React.ReactNode;
+
customOnChange?: (
event: React.ChangeEvent,
form: FormikProps
@@ -10,7 +12,8 @@ type CustomInputProps = {
};
export default function Input(
- props: CustomInputProps & FieldAttributes<{ hint?: string; label: string }>
+ props: CustomInputProps &
+ FieldAttributes<{ hint?: string; label: string; disabled?: boolean }>
) {
const generatedId = React.useId();
@@ -18,7 +21,8 @@ export default function Input(
- {({ field, meta, form }: FieldProps) => {
+ {(fieldProps: FieldProps) => {
+ let { field, meta, form } = fieldProps;
let textAfterField =
meta.touched && meta.error ? (
{meta.error}
@@ -39,20 +43,26 @@ export default function Input(
return (
<>
- {
- 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 ? (
+ {
+ 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}
>
);
diff --git a/src/components/PrettyForm.module.css b/src/components/PrettyForm.module.css
index 763080b..1e47caf 100644
--- a/src/components/PrettyForm.module.css
+++ b/src/components/PrettyForm.module.css
@@ -14,4 +14,4 @@
color: var(--error);
font-size: 80%;
transition: color var(--theme-transition);
-}
\ No newline at end of file
+}
diff --git a/src/components/icons/UploadIcon.tsx b/src/components/icons/UploadIcon.tsx
new file mode 100644
index 0000000..8b6044b
--- /dev/null
+++ b/src/components/icons/UploadIcon.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+
+export default function UploadIcon() {
+ return (
+
+ );
+}
diff --git a/src/components/icons/UserIcon.tsx b/src/components/icons/UserIcon.tsx
new file mode 100644
index 0000000..44f56ce
--- /dev/null
+++ b/src/components/icons/UserIcon.tsx
@@ -0,0 +1,20 @@
+import React from "react";
+
+export default function UserIcon() {
+ return (
+
+ );
+}
diff --git a/src/schemas.ts b/src/schemas.ts
index 0867682..16278fc 100644
--- a/src/schemas.ts
+++ b/src/schemas.ts
@@ -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 =
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()
- .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 = 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 =
+ Yup.object().shape({
+ password: PASSWORD,
+ newPassword: PASSWORD,
+ confirmPassword: CONFIRM_PASSWORD("newPassword")
+ });
+
+export interface PasswordUpdateFormValues {
+ password: string;
+ newPassword: string;
+ confirmPassword: string;
}