Compare commits
No commits in common. "967bb2a2d20b9061358d77b3bbeff3fc3b4edf44" and "cbcb8268b0baf843a88bbd0a52fad238d31c95ec" have entirely different histories.
967bb2a2d2
...
cbcb8268b0
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,53 +0,0 @@
|
|||
@font-face {
|
||||
font-family: lunchtype;
|
||||
font-weight: 400;
|
||||
src: url("/fonts/lunchtype22-regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: lunchtype;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
src: url("/fonts/lunchtype22-regular-italic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: lunchtype;
|
||||
font-weight: 500;
|
||||
src: url("/fonts/lunchtype22-medium.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: lunchtype;
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
src: url("/fonts/lunchtype22-medium.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: lunchtype;
|
||||
font-weight: 400;
|
||||
font-stretch: condensed;
|
||||
src: url("/fonts/lunchtype25-regular-condensed.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: lunchtype;
|
||||
font-weight: 500;
|
||||
font-stretch: condensed;
|
||||
src: url("/fonts/lunchtype25-medium-condensed.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: lunchtype;
|
||||
font-weight: 400;
|
||||
font-stretch: expanded;
|
||||
src: url("/fonts/lunchtype24-regular-expanded.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: lunchtype;
|
||||
font-weight: 500;
|
||||
font-stretch: expanded;
|
||||
src: url("/fonts/lunchtype24-medium-expanded.woff2") format("woff2");
|
||||
}
|
|
@ -62,46 +62,27 @@ body {
|
|||
overflow-x: hidden;
|
||||
color: var(--fg);
|
||||
background-color: var(--bg);
|
||||
font-family: lunchtype, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
font-stretch: expanded;
|
||||
font-weight: 500;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import ColorChanger from "@/components/ColorChanger";
|
||||
import "./globals.css";
|
||||
import "./fonts.css";
|
||||
|
||||
export const metadata = {
|
||||
title: "gluestick",
|
||||
|
|
|
@ -1,36 +1,28 @@
|
|||
.content {
|
||||
width: min-content;
|
||||
margin: 2rem auto;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.profileGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
column-gap: 2rem;
|
||||
/* todo */
|
||||
}
|
||||
|
||||
.profileTower *:first-child {
|
||||
margin-top: 0 !important;
|
||||
.divider {
|
||||
width: 400px;
|
||||
|
||||
background-color: var(--fg-darker);
|
||||
height: 1px;
|
||||
border: none;
|
||||
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.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;
|
||||
.logout {
|
||||
background: var(--bg-dark);
|
||||
border: 0;
|
||||
border-radius: 0.15rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
/* stolen from prettyform */
|
||||
|
|
|
@ -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, { Label } from "@/components/Input";
|
||||
import Input from "@/components/Input";
|
||||
import { Form, Formik, FormikHelpers } from "formik";
|
||||
import {
|
||||
AboutMeFormValues,
|
||||
|
@ -16,22 +16,84 @@ import {
|
|||
import PrettyForm from "@/components/PrettyForm";
|
||||
import Toast from "@/components/Toast";
|
||||
import { AuthProviderState } from "@/auth/AuthProvider";
|
||||
import inputStyles from "@/components/Input.module.css";
|
||||
import Connection from "@/components/Connection";
|
||||
import { exec } from "child_process";
|
||||
|
||||
type UpdateResponse = {
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
async function fileAsBase64(f: File) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsArrayBuffer(f);
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
reader.onload = () => {
|
||||
const result = reader.result as ArrayBuffer;
|
||||
const buffer = Buffer.from(result);
|
||||
resolve(buffer.toString("base64"));
|
||||
};
|
||||
reader.onerror = () => reject(reader.error);
|
||||
});
|
||||
}
|
||||
|
||||
function AuthProviderButton({ provider }: { provider: AuthProviderState }) {
|
||||
// bullshit hack
|
||||
const holdTime = provider.connected ? 3000 : 0;
|
||||
const interval = React.useRef<NodeJS.Timeout | null>();
|
||||
const inputRef = React.useRef<HTMLInputElement>(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 (
|
||||
<input
|
||||
type="submit"
|
||||
className={
|
||||
styles.fancyInput + " " + (provider.connected ? styles.progress : "")
|
||||
}
|
||||
onMouseDown={mouseDown}
|
||||
onMouseUp={mouseUp}
|
||||
value={provider.connected ? "Disconnect" : "Connect"}
|
||||
ref={inputRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthProviderEntry({ provider }: { provider: AuthProviderState }) {
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
{provider.name}:{" "}
|
||||
{provider.connected ? provider.username : "Not connected"}
|
||||
</p>
|
||||
|
||||
<AuthProviderButton provider={provider} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AboutMe({
|
||||
info,
|
||||
providers: [discordState, githubState]
|
||||
providers
|
||||
}: {
|
||||
info: UserInfo;
|
||||
providers: AuthProviderState[];
|
||||
}) {
|
||||
// TODO: Reimplement password changing.
|
||||
const [globalError, setGlobalError] = React.useState<string | null>(null);
|
||||
const [madeProfileChanges, setMadeChanges] = React.useState(false);
|
||||
const [madePasswordChanges, setMadePasswordChanges] = React.useState(false);
|
||||
|
@ -120,6 +182,7 @@ export default function AboutMe({
|
|||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<h2 className={styles.userName}>{info.username}</h2>
|
||||
<PrettyForm globalError={globalError}>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
|
@ -128,92 +191,56 @@ export default function AboutMe({
|
|||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form className={styles.profileGrid}>
|
||||
<div className={styles.profileTower}>
|
||||
<Input
|
||||
type="file"
|
||||
name="avatar"
|
||||
customRender={(fieldProps) => (
|
||||
<AvatarChanger
|
||||
currentAvatarBlob={fieldProps.field.value}
|
||||
onChange={(newBlob) =>
|
||||
fieldProps.form.setFieldValue("avatar", newBlob)
|
||||
}
|
||||
vertical
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{madeProfileChanges ? <Toast>Saved your changes.</Toast> : null}
|
||||
<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}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h2 className={styles.userName}>{info.username}</h2>
|
||||
<div className={styles.rightGrid}>
|
||||
<div>
|
||||
{madeProfileChanges ? (
|
||||
<Toast>Saved your changes.</Toast>
|
||||
) : null}
|
||||
<Input
|
||||
type="text"
|
||||
name="username"
|
||||
label="Username"
|
||||
defaultValue={info.username}
|
||||
disabled
|
||||
hint="This can’t be changed."
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
name="displayName"
|
||||
label="Display name"
|
||||
defaultValue={info.displayName}
|
||||
/>
|
||||
<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)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
name="email"
|
||||
label="Email"
|
||||
defaultValue={info.email}
|
||||
/>
|
||||
|
||||
<div className={inputStyles.formRow}>
|
||||
<button type="button">Change Password</button>
|
||||
</div>
|
||||
|
||||
<div className={inputStyles.formRow}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
document.cookie =
|
||||
"ticket=; expires=" +
|
||||
new Date().toUTCString() +
|
||||
"; path=/";
|
||||
window.location.href = "/";
|
||||
}}
|
||||
>
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
className={styles.fancyInput}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.connections}>
|
||||
<Label>Connections</Label>
|
||||
<Connection service="Discord" authState={discordState} />
|
||||
<Connection service="GitHub" authState={githubState} />
|
||||
<Connection service="Tailscale" unavailable />
|
||||
<Connection service="Migadu" unavailable />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
className={styles.fancyInput}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</PrettyForm>
|
||||
{/*<PrettyForm globalError={passwordError}>
|
||||
<hr className={styles.divider} />
|
||||
<h2 className={styles.header}>Change password</h2>
|
||||
<PrettyForm globalError={passwordError}>
|
||||
<Formik
|
||||
initialValues={initialPasswordValues}
|
||||
onSubmit={handlePasswordSubmit}
|
||||
|
@ -259,6 +286,7 @@ export default function AboutMe({
|
|||
</Formik>
|
||||
</PrettyForm>
|
||||
|
||||
<hr className={styles.divider} />
|
||||
<h2 className={styles.header}>Connections</h2>
|
||||
<div className={styles.authProviderList}>
|
||||
{providers.map((provider) => (
|
||||
|
@ -266,6 +294,7 @@ export default function AboutMe({
|
|||
))}
|
||||
</div>
|
||||
|
||||
<hr className={styles.divider} />
|
||||
<input
|
||||
type="button"
|
||||
value="Log out"
|
||||
|
@ -275,7 +304,7 @@ export default function AboutMe({
|
|||
"ticket=; expires=" + new Date().toUTCString() + "; path=/";
|
||||
window.location.href = "/";
|
||||
}}
|
||||
/>*/}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,44 +2,32 @@
|
|||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
gap: 1rem;
|
||||
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.avatarChanger .current-avatar,
|
||||
.avatarChanger svg {
|
||||
.avatarChanger :is(img, 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.5em 1em;
|
||||
padding: 0.25em 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
|
@ -8,12 +8,10 @@ import UserIcon from "./icons/UserIcon";
|
|||
|
||||
export default function AvatarChanger({
|
||||
currentAvatarBlob,
|
||||
onChange,
|
||||
vertical = false
|
||||
onChange
|
||||
}: {
|
||||
currentAvatarBlob: string | null;
|
||||
onChange: (newAvatar: string) => void;
|
||||
vertical?: boolean;
|
||||
}) {
|
||||
const input = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
|
@ -27,19 +25,9 @@ export default function AvatarChanger({
|
|||
|
||||
// I give you the most support and well wishes while you work on this project -Ari
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
styles.avatarChanger,
|
||||
"avatar-changer",
|
||||
vertical ? styles.vertical : null
|
||||
)}
|
||||
>
|
||||
<div className={classnames(styles.avatarChanger, "avatar-changer")}>
|
||||
{currentAvatarBlob != null ? (
|
||||
<img
|
||||
className="current-avatar"
|
||||
src={currentAvatarBlob!}
|
||||
alt="Your avatar"
|
||||
/>
|
||||
<img src={currentAvatarBlob!} alt="Your avatar" />
|
||||
) : (
|
||||
<UserIcon />
|
||||
)}
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
.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;
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
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<HTMLButtonElement>) {
|
||||
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 (
|
||||
<button
|
||||
className={classnames(
|
||||
styles.connection,
|
||||
unavailable ? styles.unavailable : null,
|
||||
!authState?.connected ? styles.disconnected : null
|
||||
)}
|
||||
onClick={handleClick}
|
||||
disabled={changing}
|
||||
>
|
||||
<div className={styles.iconContainer}>●</div>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.serviceName}>{service}</div>
|
||||
{authState?.connected !== false ? (
|
||||
<div
|
||||
className={styles.linkedIdentity}
|
||||
title={
|
||||
authState?.id ?? "This integration is currently unavailable."
|
||||
}
|
||||
>
|
||||
{unavailable ? "Unavailable" : authState!.username}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{authState?.connected && !unavailable ? <CheckIcon /> : null}
|
||||
</button>
|
||||
);
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
.formRow label {
|
||||
display: block;
|
||||
font-variant: all-small-caps;
|
||||
font-size: 105%;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Field, FieldProps, FieldAttributes, FormikProps } from "formik";
|
||||
import React, { LabelHTMLAttributes } from "react";
|
||||
import React from "react";
|
||||
import styles from "./Input.module.css";
|
||||
import classnames from "classnames";
|
||||
|
||||
type CustomInputProps<T> = {
|
||||
customRender?: (fieldProps: FieldProps) => React.ReactNode;
|
||||
|
@ -12,26 +11,15 @@ type CustomInputProps<T> = {
|
|||
) => void;
|
||||
};
|
||||
|
||||
export function Label({
|
||||
children,
|
||||
...props
|
||||
}: LabelHTMLAttributes<HTMLLabelElement>) {
|
||||
return (
|
||||
<label className={classnames(styles.label, props.className)} {...props}>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Input<T>(
|
||||
props: CustomInputProps<T> &
|
||||
FieldAttributes<{ hint?: string; label?: string; disabled?: boolean }>
|
||||
FieldAttributes<{ hint?: string; label: string; disabled?: boolean }>
|
||||
) {
|
||||
const generatedId = React.useId();
|
||||
|
||||
return (
|
||||
<div className={classnames("form-row", styles.formRow)}>
|
||||
{props.label ? <Label htmlFor={generatedId}>{props.label}</Label> : null}
|
||||
<div className={styles.formRow}>
|
||||
<label htmlFor={generatedId}>{props.label}</label>
|
||||
<Field id={generatedId} {...props}>
|
||||
{(fieldProps: FieldProps) => {
|
||||
let { field, meta, form } = fieldProps;
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
.form {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.form :is(button, input)[type="submit"] {
|
||||
background: var(--bg-dark);
|
||||
border: 0;
|
||||
|
|
Loading…
Reference in New Issue