From 967bb2a2d20b9061358d77b3bbeff3fc3b4edf44 Mon Sep 17 00:00:00 2001 From: slice Date: Fri, 28 Apr 2023 08:53:58 -0700 Subject: [PATCH] first proto of nice about me page :3 --- src/app/globals.css | 29 +++- src/app/me/AboutMe.module.css | 42 +++-- src/app/me/AboutMe.tsx | 201 ++++++++++-------------- src/components/AvatarChanger.module.css | 22 ++- src/components/AvatarChanger.tsx | 18 ++- src/components/Connection.module.css | 45 ++++++ src/components/Connection.tsx | 70 +++++++++ src/components/Input.module.css | 2 +- src/components/Input.tsx | 20 ++- src/components/PrettyForm.module.css | 4 - 10 files changed, 298 insertions(+), 155 deletions(-) create mode 100644 src/components/Connection.module.css create mode 100644 src/components/Connection.tsx diff --git a/src/app/globals.css b/src/app/globals.css index 1b46c0b..b82dfe6 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -67,24 +67,41 @@ body { sans-serif; } +h2 { + font-size: 2rem; + font-stretch: expanded; + font-weight: 500; +} + html, body, input, +button, label { transition: background-color var(--theme-transition), color var(--theme-transition); } -input:disabled, -button:disabled { - opacity: 0.5; - cursor: not-allowed !important; -} - input, button { font: inherit; color: inherit; + background-color: inherit; +} + +button, +input[type="submit"] { + padding: 0.5em 1em; + background-color: var(--bg-dark); + border-radius: 0.25rem; + border: none; + cursor: pointer; +} + +input:disabled, +button:disabled { + opacity: 0.5; + cursor: not-allowed !important; } input::placeholder { diff --git a/src/app/me/AboutMe.module.css b/src/app/me/AboutMe.module.css index 2b2130c..bdfa0b0 100644 --- a/src/app/me/AboutMe.module.css +++ b/src/app/me/AboutMe.module.css @@ -1,28 +1,36 @@ .content { - max-width: 700px; - margin: 0 auto; + width: min-content; + margin: 2rem auto; } .profileGrid { - /* todo */ + display: grid; + grid-template-columns: 300px 1fr; + column-gap: 2rem; } -.divider { - width: 400px; - - background-color: var(--fg-darker); - height: 1px; - border: none; - - margin: 1rem auto; +.profileTower *:first-child { + margin-top: 0 !important; } -.logout { - background: var(--bg-dark); - border: 0; - border-radius: 0.15rem; - cursor: pointer; - padding: 0.5em 1em; +.connections { + margin-top: 1rem; +} + +.connections > *:nth-child(2) { + margin-top: 0.5rem; +} + +.rightGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + column-gap: 2rem; +} + +.userName { + font-size: 3rem; + text-transform: uppercase; + margin: 0; } /* stolen from prettyform */ diff --git a/src/app/me/AboutMe.tsx b/src/app/me/AboutMe.tsx index 206009c..b347c43 100644 --- a/src/app/me/AboutMe.tsx +++ b/src/app/me/AboutMe.tsx @@ -5,7 +5,7 @@ import { UserInfo } from "@/ldap"; import React from "react"; import styles from "./AboutMe.module.css"; import AvatarChanger from "@/components/AvatarChanger"; -import Input from "@/components/Input"; +import Input, { Label } from "@/components/Input"; import { Form, Formik, FormikHelpers } from "formik"; import { AboutMeFormValues, @@ -16,84 +16,22 @@ import { import PrettyForm from "@/components/PrettyForm"; import Toast from "@/components/Toast"; import { AuthProviderState } from "@/auth/AuthProvider"; -import { exec } from "child_process"; +import inputStyles from "@/components/Input.module.css"; +import Connection from "@/components/Connection"; type UpdateResponse = { ok: boolean; error?: string; }; -async function fileAsBase64(f: File) { - const reader = new FileReader(); - reader.readAsArrayBuffer(f); - return new Promise((resolve, reject) => { - reader.onload = () => { - const result = reader.result as ArrayBuffer; - const buffer = Buffer.from(result); - resolve(buffer.toString("base64")); - }; - reader.onerror = () => reject(reader.error); - }); -} - -function AuthProviderButton({ provider }: { provider: AuthProviderState }) { - // bullshit hack - const holdTime = provider.connected ? 3000 : 0; - const interval = React.useRef(); - const inputRef = React.useRef(null); - - const execute = async () => { - const name = provider.name.toLowerCase(); - if (!provider.connected) { - window.location.href = `/oauth/${name}/login`; - } else { - await fetch(`/api/unlink?provider=${name}`, { method: "POST" }); - window.location.reload(); - } - }; - - const mouseDown = () => { - interval.current = setTimeout(execute, holdTime); - }; - - const mouseUp = () => { - if (interval.current) clearTimeout(interval.current); - }; - - return ( - - ); -} - -function AuthProviderEntry({ provider }: { provider: AuthProviderState }) { - return ( - <> -

- {provider.name}:{" "} - {provider.connected ? provider.username : "Not connected"} -

- - - - ); -} - export default function AboutMe({ info, - providers + providers: [discordState, githubState] }: { info: UserInfo; providers: AuthProviderState[]; }) { + // TODO: Reimplement password changing. const [globalError, setGlobalError] = React.useState(null); const [madeProfileChanges, setMadeChanges] = React.useState(false); const [madePasswordChanges, setMadePasswordChanges] = React.useState(false); @@ -182,7 +120,6 @@ export default function AboutMe({ return (
-

{info.username}

{({ isSubmitting }) => (
- {madeProfileChanges ? Saved your changes. : null} - - - +
+ ( + + fieldProps.form.setFieldValue("avatar", newBlob) + } + vertical + /> + )} + /> +
- ( - - fieldProps.form.setFieldValue("avatar", newBlob) - } - /> - )} - /> +
+

{info.username}

+
+
+ {madeProfileChanges ? ( + Saved your changes. + ) : null} + + - + + +
+ +
+ +
+ +
+ + +
+ +
+ + + + + +
+
+
)}
-
-

Change password

- + {/* -

Connections

{providers.map((provider) => ( @@ -294,7 +266,6 @@ export default function AboutMe({ ))}
-
+ />*/}
); } diff --git a/src/components/AvatarChanger.module.css b/src/components/AvatarChanger.module.css index 7be6bd7..7d7a6b6 100644 --- a/src/components/AvatarChanger.module.css +++ b/src/components/AvatarChanger.module.css @@ -2,32 +2,44 @@ display: flex; flex-flow: row nowrap; gap: 1rem; - margin: 0.5rem 0; } -.avatarChanger :is(img, svg) { +.vertical { + flex-direction: column; +} + +.avatarChanger .current-avatar, +.avatarChanger svg { width: 3em; height: 3em; border-radius: 0.25rem; } +.vertical.vertical .current-avatar { + display: block; + width: 100%; + height: inherit; + aspect-ratio: 1/1; +} + .avatarChanger button svg { width: 1.2em; height: 1.2em; margin-right: 0.5em; } -.avatarChanger input[type=file] { +.avatarChanger input[type="file"] { display: none; } .uploadButton { display: flex; align-items: center; + justify-content: center; background: var(--bg-dark); border: 0; border-radius: 0.15rem; - padding: 0.25em 1em; + padding: 0.5em 1em; cursor: pointer; -} \ No newline at end of file +} diff --git a/src/components/AvatarChanger.tsx b/src/components/AvatarChanger.tsx index d8b57ad..b935233 100644 --- a/src/components/AvatarChanger.tsx +++ b/src/components/AvatarChanger.tsx @@ -8,10 +8,12 @@ import UserIcon from "./icons/UserIcon"; export default function AvatarChanger({ currentAvatarBlob, - onChange + onChange, + vertical = false }: { currentAvatarBlob: string | null; onChange: (newAvatar: string) => void; + vertical?: boolean; }) { const input = React.useRef(null); @@ -25,9 +27,19 @@ export default function AvatarChanger({ // I give you the most support and well wishes while you work on this project -Ari return ( -
+
{currentAvatarBlob != null ? ( - Your avatar + Your avatar ) : ( )} diff --git a/src/components/Connection.module.css b/src/components/Connection.module.css new file mode 100644 index 0000000..923d8cc --- /dev/null +++ b/src/components/Connection.module.css @@ -0,0 +1,45 @@ +.connection { + padding: 1rem 2rem; + background-color: var(--bg-dark); + margin: 1rem 0; + border-radius: 0.25rem; + width: 300px; + display: flex; + flex-flow: row nowrap; + column-gap: 1rem; + align-items: center; + text-align: left; +} + +.connection svg { + height: 1.5rem; + margin-left: auto; +} + +.iconContainer { + font-size: 2.5em; + margin-right: 0.75rem; +} + +.info { + display: flex; + flex-flow: column nowrap; + row-gap: 0.25rem; +} + +.serviceName { + font-weight: 500; + font-stretch: expanded; + font-size: 1.3em; +} + +.linkedIdentity { + opacity: 0.7; +} + +.unavailable.unavailable { + background-color: var(--bg-darker); + color: var(--fg-dark); + opacity: 0.5; + cursor: not-allowed; +} diff --git a/src/components/Connection.tsx b/src/components/Connection.tsx new file mode 100644 index 0000000..9443ead --- /dev/null +++ b/src/components/Connection.tsx @@ -0,0 +1,70 @@ +import React from "react"; + +import styles from "./Connection.module.css"; +import classnames from "classnames"; +import CheckIcon from "./icons/CheckIcon"; +import { type AuthProviderState } from "@/auth/AuthProvider"; +import { useRouter } from "next/navigation"; + +export default function Connection({ + service, + unavailable = false, + authState +}: { + service: string; + unavailable?: boolean; + authState?: AuthProviderState; +}) { + const router = useRouter(); + const [changing, setChanging] = React.useState(false); + + // TODO: Reimplement hold-to-unlink. + + async function handleClick(event: React.MouseEvent) { + event.preventDefault(); + + if (unavailable) return; + + const provider = service.toLowerCase(); + if (authState?.connected === false) { + setChanging(true); + router.push(`/oauth/${provider}/login`); + } else { + setChanging(true); + if (confirm(`Unlink your ${service} account?`)) { + await fetch(`/api/unlink?provider=${provider}`, { method: "POST" }); + window.location.reload(); + } else { + setChanging(false); + } + } + } + + return ( + + ); +} diff --git a/src/components/Input.module.css b/src/components/Input.module.css index 4bf0f8b..6108e52 100644 --- a/src/components/Input.module.css +++ b/src/components/Input.module.css @@ -13,7 +13,7 @@ margin: 1rem 0; } -.formRow label { +.label { display: block; font-variant: all-small-caps; font-size: 105%; diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 3ca4191..1eb8add 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -1,6 +1,7 @@ import { Field, FieldProps, FieldAttributes, FormikProps } from "formik"; -import React from "react"; +import React, { LabelHTMLAttributes } from "react"; import styles from "./Input.module.css"; +import classnames from "classnames"; type CustomInputProps = { customRender?: (fieldProps: FieldProps) => React.ReactNode; @@ -11,15 +12,26 @@ type CustomInputProps = { ) => void; }; +export function Label({ + children, + ...props +}: LabelHTMLAttributes) { + return ( + + ); +} + export default function Input( props: CustomInputProps & - FieldAttributes<{ hint?: string; label: string; disabled?: boolean }> + FieldAttributes<{ hint?: string; label?: string; disabled?: boolean }> ) { const generatedId = React.useId(); return ( -
- +
+ {props.label ? : null} {(fieldProps: FieldProps) => { let { field, meta, form } = fieldProps; diff --git a/src/components/PrettyForm.module.css b/src/components/PrettyForm.module.css index 1e47caf..6cfe7be 100644 --- a/src/components/PrettyForm.module.css +++ b/src/components/PrettyForm.module.css @@ -1,7 +1,3 @@ -.form { - max-width: 500px; -} - .form :is(button, input)[type="submit"] { background: var(--bg-dark); border: 0;