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;
|
overflow-x: hidden;
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
background-color: var(--bg);
|
background-color: var(--bg);
|
||||||
font-family: lunchtype, system-ui, -apple-system, BlinkMacSystemFont,
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
"Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue",
|
Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||||
sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-stretch: expanded;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
input,
|
input,
|
||||||
button,
|
|
||||||
label {
|
label {
|
||||||
transition: background-color var(--theme-transition),
|
transition: background-color var(--theme-transition),
|
||||||
color var(--theme-transition);
|
color var(--theme-transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input:disabled, button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
button {
|
button {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: 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 {
|
input::placeholder {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import ColorChanger from "@/components/ColorChanger";
|
import ColorChanger from "@/components/ColorChanger";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import "./fonts.css";
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "gluestick",
|
title: "gluestick",
|
||||||
|
|
|
@ -1,36 +1,28 @@
|
||||||
.content {
|
.content {
|
||||||
width: min-content;
|
max-width: 700px;
|
||||||
margin: 2rem auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profileGrid {
|
.profileGrid {
|
||||||
display: grid;
|
/* todo */
|
||||||
grid-template-columns: 300px 1fr;
|
|
||||||
column-gap: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.profileTower *:first-child {
|
.divider {
|
||||||
margin-top: 0 !important;
|
width: 400px;
|
||||||
|
|
||||||
|
background-color: var(--fg-darker);
|
||||||
|
height: 1px;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
margin: 1rem auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connections {
|
.logout {
|
||||||
margin-top: 1rem;
|
background: var(--bg-dark);
|
||||||
}
|
border: 0;
|
||||||
|
border-radius: 0.15rem;
|
||||||
.connections > *:nth-child(2) {
|
cursor: pointer;
|
||||||
margin-top: 0.5rem;
|
padding: 0.5em 1em;
|
||||||
}
|
|
||||||
|
|
||||||
.rightGrid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
column-gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.userName {
|
|
||||||
font-size: 3rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* stolen from prettyform */
|
/* stolen from prettyform */
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { UserInfo } from "@/ldap";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styles from "./AboutMe.module.css";
|
import styles from "./AboutMe.module.css";
|
||||||
import AvatarChanger from "@/components/AvatarChanger";
|
import AvatarChanger from "@/components/AvatarChanger";
|
||||||
import Input, { Label } from "@/components/Input";
|
import Input from "@/components/Input";
|
||||||
import { Form, Formik, FormikHelpers } from "formik";
|
import { Form, Formik, FormikHelpers } from "formik";
|
||||||
import {
|
import {
|
||||||
AboutMeFormValues,
|
AboutMeFormValues,
|
||||||
|
@ -16,22 +16,84 @@ import {
|
||||||
import PrettyForm from "@/components/PrettyForm";
|
import PrettyForm from "@/components/PrettyForm";
|
||||||
import Toast from "@/components/Toast";
|
import Toast from "@/components/Toast";
|
||||||
import { AuthProviderState } from "@/auth/AuthProvider";
|
import { AuthProviderState } from "@/auth/AuthProvider";
|
||||||
import inputStyles from "@/components/Input.module.css";
|
import { exec } from "child_process";
|
||||||
import Connection from "@/components/Connection";
|
|
||||||
|
|
||||||
type UpdateResponse = {
|
type UpdateResponse = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
error?: string;
|
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({
|
export default function AboutMe({
|
||||||
info,
|
info,
|
||||||
providers: [discordState, githubState]
|
providers
|
||||||
}: {
|
}: {
|
||||||
info: UserInfo;
|
info: UserInfo;
|
||||||
providers: AuthProviderState[];
|
providers: AuthProviderState[];
|
||||||
}) {
|
}) {
|
||||||
// TODO: Reimplement password changing.
|
|
||||||
const [globalError, setGlobalError] = React.useState<string | null>(null);
|
const [globalError, setGlobalError] = React.useState<string | null>(null);
|
||||||
const [madeProfileChanges, setMadeChanges] = React.useState(false);
|
const [madeProfileChanges, setMadeChanges] = React.useState(false);
|
||||||
const [madePasswordChanges, setMadePasswordChanges] = React.useState(false);
|
const [madePasswordChanges, setMadePasswordChanges] = React.useState(false);
|
||||||
|
@ -120,6 +182,7 @@ export default function AboutMe({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
|
<h2 className={styles.userName}>{info.username}</h2>
|
||||||
<PrettyForm globalError={globalError}>
|
<PrettyForm globalError={globalError}>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
|
@ -128,92 +191,56 @@ export default function AboutMe({
|
||||||
>
|
>
|
||||||
{({ isSubmitting }) => (
|
{({ isSubmitting }) => (
|
||||||
<Form className={styles.profileGrid}>
|
<Form className={styles.profileGrid}>
|
||||||
<div className={styles.profileTower}>
|
{madeProfileChanges ? <Toast>Saved your changes.</Toast> : null}
|
||||||
<Input
|
<Input
|
||||||
type="file"
|
type="text"
|
||||||
name="avatar"
|
name="username"
|
||||||
customRender={(fieldProps) => (
|
label="Username"
|
||||||
<AvatarChanger
|
defaultValue={info.username}
|
||||||
currentAvatarBlob={fieldProps.field.value}
|
disabled
|
||||||
onChange={(newBlob) =>
|
title="You can't change your username."
|
||||||
fieldProps.form.setFieldValue("avatar", newBlob)
|
/>
|
||||||
}
|
<Input
|
||||||
vertical
|
type="text"
|
||||||
/>
|
name="displayName"
|
||||||
)}
|
label="Display name"
|
||||||
/>
|
defaultValue={info.displayName}
|
||||||
</div>
|
/>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
label="Email"
|
||||||
|
defaultValue={info.email}
|
||||||
|
/>
|
||||||
|
|
||||||
<div>
|
<Input
|
||||||
<h2 className={styles.userName}>{info.username}</h2>
|
type="file"
|
||||||
<div className={styles.rightGrid}>
|
name="avatar"
|
||||||
<div>
|
label="Avatar"
|
||||||
{madeProfileChanges ? (
|
accept="image/png, image/jpeg"
|
||||||
<Toast>Saved your changes.</Toast>
|
customRender={(fieldProps) => (
|
||||||
) : null}
|
<AvatarChanger
|
||||||
<Input
|
currentAvatarBlob={fieldProps.field.value}
|
||||||
type="text"
|
onChange={(newBlob) =>
|
||||||
name="username"
|
fieldProps.form.setFieldValue("avatar", newBlob)
|
||||||
label="Username"
|
}
|
||||||
defaultValue={info.username}
|
/>
|
||||||
disabled
|
)}
|
||||||
hint="This can’t be changed."
|
/>
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
name="displayName"
|
|
||||||
label="Display name"
|
|
||||||
defaultValue={info.displayName}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
<input
|
||||||
type="email"
|
type="submit"
|
||||||
name="email"
|
value="Save"
|
||||||
label="Email"
|
className={styles.fancyInput}
|
||||||
defaultValue={info.email}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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>
|
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
</PrettyForm>
|
</PrettyForm>
|
||||||
{/*<PrettyForm globalError={passwordError}>
|
<hr className={styles.divider} />
|
||||||
|
<h2 className={styles.header}>Change password</h2>
|
||||||
|
<PrettyForm globalError={passwordError}>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialPasswordValues}
|
initialValues={initialPasswordValues}
|
||||||
onSubmit={handlePasswordSubmit}
|
onSubmit={handlePasswordSubmit}
|
||||||
|
@ -259,6 +286,7 @@ export default function AboutMe({
|
||||||
</Formik>
|
</Formik>
|
||||||
</PrettyForm>
|
</PrettyForm>
|
||||||
|
|
||||||
|
<hr className={styles.divider} />
|
||||||
<h2 className={styles.header}>Connections</h2>
|
<h2 className={styles.header}>Connections</h2>
|
||||||
<div className={styles.authProviderList}>
|
<div className={styles.authProviderList}>
|
||||||
{providers.map((provider) => (
|
{providers.map((provider) => (
|
||||||
|
@ -266,6 +294,7 @@ export default function AboutMe({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr className={styles.divider} />
|
||||||
<input
|
<input
|
||||||
type="button"
|
type="button"
|
||||||
value="Log out"
|
value="Log out"
|
||||||
|
@ -275,7 +304,7 @@ export default function AboutMe({
|
||||||
"ticket=; expires=" + new Date().toUTCString() + "; path=/";
|
"ticket=; expires=" + new Date().toUTCString() + "; path=/";
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
}}
|
}}
|
||||||
/>*/}
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,44 +2,32 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical {
|
.avatarChanger :is(img, svg) {
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatarChanger .current-avatar,
|
|
||||||
.avatarChanger svg {
|
|
||||||
width: 3em;
|
width: 3em;
|
||||||
height: 3em;
|
height: 3em;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical.vertical .current-avatar {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: inherit;
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatarChanger button svg {
|
.avatarChanger button svg {
|
||||||
width: 1.2em;
|
width: 1.2em;
|
||||||
height: 1.2em;
|
height: 1.2em;
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatarChanger input[type="file"] {
|
.avatarChanger input[type=file] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploadButton {
|
.uploadButton {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
background: var(--bg-dark);
|
background: var(--bg-dark);
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0.15rem;
|
border-radius: 0.15rem;
|
||||||
padding: 0.5em 1em;
|
padding: 0.25em 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
|
@ -8,12 +8,10 @@ import UserIcon from "./icons/UserIcon";
|
||||||
|
|
||||||
export default function AvatarChanger({
|
export default function AvatarChanger({
|
||||||
currentAvatarBlob,
|
currentAvatarBlob,
|
||||||
onChange,
|
onChange
|
||||||
vertical = false
|
|
||||||
}: {
|
}: {
|
||||||
currentAvatarBlob: string | null;
|
currentAvatarBlob: string | null;
|
||||||
onChange: (newAvatar: string) => void;
|
onChange: (newAvatar: string) => void;
|
||||||
vertical?: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
const input = React.useRef<HTMLInputElement>(null);
|
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
|
// I give you the most support and well wishes while you work on this project -Ari
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={classnames(styles.avatarChanger, "avatar-changer")}>
|
||||||
className={classnames(
|
|
||||||
styles.avatarChanger,
|
|
||||||
"avatar-changer",
|
|
||||||
vertical ? styles.vertical : null
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{currentAvatarBlob != null ? (
|
{currentAvatarBlob != null ? (
|
||||||
<img
|
<img src={currentAvatarBlob!} alt="Your avatar" />
|
||||||
className="current-avatar"
|
|
||||||
src={currentAvatarBlob!}
|
|
||||||
alt="Your avatar"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<UserIcon />
|
<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;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.formRow label {
|
||||||
display: block;
|
display: block;
|
||||||
font-variant: all-small-caps;
|
font-variant: all-small-caps;
|
||||||
font-size: 105%;
|
font-size: 105%;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Field, FieldProps, FieldAttributes, FormikProps } from "formik";
|
import { Field, FieldProps, FieldAttributes, FormikProps } from "formik";
|
||||||
import React, { LabelHTMLAttributes } from "react";
|
import React from "react";
|
||||||
import styles from "./Input.module.css";
|
import styles from "./Input.module.css";
|
||||||
import classnames from "classnames";
|
|
||||||
|
|
||||||
type CustomInputProps<T> = {
|
type CustomInputProps<T> = {
|
||||||
customRender?: (fieldProps: FieldProps) => React.ReactNode;
|
customRender?: (fieldProps: FieldProps) => React.ReactNode;
|
||||||
|
@ -12,26 +11,15 @@ type CustomInputProps<T> = {
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Label({
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: LabelHTMLAttributes<HTMLLabelElement>) {
|
|
||||||
return (
|
|
||||||
<label className={classnames(styles.label, props.className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Input<T>(
|
export default function Input<T>(
|
||||||
props: CustomInputProps<T> &
|
props: CustomInputProps<T> &
|
||||||
FieldAttributes<{ hint?: string; label?: string; disabled?: boolean }>
|
FieldAttributes<{ hint?: string; label: string; disabled?: boolean }>
|
||||||
) {
|
) {
|
||||||
const generatedId = React.useId();
|
const generatedId = React.useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classnames("form-row", styles.formRow)}>
|
<div className={styles.formRow}>
|
||||||
{props.label ? <Label htmlFor={generatedId}>{props.label}</Label> : null}
|
<label htmlFor={generatedId}>{props.label}</label>
|
||||||
<Field id={generatedId} {...props}>
|
<Field id={generatedId} {...props}>
|
||||||
{(fieldProps: FieldProps) => {
|
{(fieldProps: FieldProps) => {
|
||||||
let { field, meta, form } = fieldProps;
|
let { field, meta, form } = fieldProps;
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
.form {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
.form :is(button, input)[type="submit"] {
|
.form :is(button, input)[type="submit"] {
|
||||||
background: var(--bg-dark);
|
background: var(--bg-dark);
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|
Loading…
Reference in New Issue