mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-26 21:26:01 -05:00
User page bio
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
export const USER_BIO_MAX_LENGTH = 2000;
|
||||
|
||||
export const navItemsGrouped: {
|
||||
title: string;
|
||||
items: {
|
||||
|
||||
@@ -48,11 +48,12 @@ export function upsert(
|
||||
|
||||
const updateProfileStm = sql.prepare(`
|
||||
UPDATE "User"
|
||||
SET "country" = $country
|
||||
SET "country" = $country,
|
||||
"bio" = $bio
|
||||
WHERE "id" = $id
|
||||
`);
|
||||
|
||||
export function updateProfile(params: Pick<User, "country" | "id">) {
|
||||
export function updateProfile(params: Pick<User, "country" | "id" | "bio">) {
|
||||
updateProfileStm.run(params);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ export type UserPageLoaderData = Pick<
|
||||
| "youtubeId"
|
||||
| "twitch"
|
||||
| "twitter"
|
||||
| "bio"
|
||||
> & { country?: { name: string; emoji: string; code: string } };
|
||||
|
||||
export const loader: LoaderFunction = ({ params }) => {
|
||||
@@ -41,6 +42,7 @@ export const loader: LoaderFunction = ({ params }) => {
|
||||
twitch: user.twitch,
|
||||
twitter: user.twitter,
|
||||
youtubeId: user.youtubeId,
|
||||
bio: user.bio,
|
||||
country:
|
||||
countryObj && user.country
|
||||
? {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import type { ActionFunction, LinksFunction } from "@remix-run/node";
|
||||
import { Form, useMatches, useTransition } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import { countries } from "countries-list";
|
||||
import * as React from "react";
|
||||
import { z } from "zod";
|
||||
import { Button } from "~/components/Button";
|
||||
import { USER_BIO_MAX_LENGTH } from "~/constants";
|
||||
import { db } from "~/db";
|
||||
import type { User } from "~/db/types";
|
||||
import styles from "~/styles/u-edit.css";
|
||||
import { parseRequestFormData, requireUser } from "~/utils/remix";
|
||||
import { falsyToNull } from "~/utils/zod";
|
||||
@@ -23,6 +27,7 @@ const userEditActionSchema = z.object({
|
||||
)
|
||||
.nullable()
|
||||
),
|
||||
bio: z.preprocess(falsyToNull, z.string().max(USER_BIO_MAX_LENGTH)),
|
||||
});
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
@@ -61,6 +66,7 @@ export default function UserEditPage() {
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<BioTextarea initialValue={data.bio} />
|
||||
<Button
|
||||
loadingText="Saving..."
|
||||
type="submit"
|
||||
@@ -72,3 +78,34 @@ export default function UserEditPage() {
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
function BioTextarea({ initialValue }: { initialValue: User["bio"] }) {
|
||||
const [value, setValue] = React.useState(initialValue ?? "");
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="u-edit__bio-header">
|
||||
<label htmlFor="bio">Bio</label>
|
||||
<div
|
||||
className={clsx("u-edit__length-limit", lengthWarning(value.length))}
|
||||
>
|
||||
{value.length}/{USER_BIO_MAX_LENGTH}
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
id="bio"
|
||||
name="bio"
|
||||
className="u-edit__bio-textarea"
|
||||
data-cy="bio-textarea"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
maxLength={USER_BIO_MAX_LENGTH}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function lengthWarning(length: number) {
|
||||
if (length >= USER_BIO_MAX_LENGTH) return "error";
|
||||
if (length + 100 >= USER_BIO_MAX_LENGTH) return "warning";
|
||||
}
|
||||
|
||||
@@ -14,34 +14,37 @@ export default function UserInfoPage() {
|
||||
const data = parentRoute.data as UserPageLoaderData;
|
||||
|
||||
return (
|
||||
<div className="u__avatar-container">
|
||||
<Avatar
|
||||
discordAvatar={data.discordAvatar}
|
||||
discordId={data.discordId}
|
||||
size="lg"
|
||||
className="u__avatar"
|
||||
/>
|
||||
<h2 className="u__name">
|
||||
{data.discordName}
|
||||
<span className="u__discriminator">#{data.discordDiscriminator}</span>
|
||||
</h2>
|
||||
{data.country ? (
|
||||
<div className="u__country">
|
||||
<span className="u__country-emoji">{data.country.emoji}</span>{" "}
|
||||
<span className="u__country-name">{data.country.name}</span>
|
||||
<div className="u__container">
|
||||
<div className="u__avatar-container">
|
||||
<Avatar
|
||||
discordAvatar={data.discordAvatar}
|
||||
discordId={data.discordId}
|
||||
size="lg"
|
||||
className="u__avatar"
|
||||
/>
|
||||
<h2 className="u__name">
|
||||
{data.discordName}
|
||||
<span className="u__discriminator">#{data.discordDiscriminator}</span>
|
||||
</h2>
|
||||
{data.country ? (
|
||||
<div className="u__country">
|
||||
<span className="u__country-emoji">{data.country.emoji}</span>{" "}
|
||||
<span className="u__country-name">{data.country.name}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="u__socials">
|
||||
{data.twitch ? (
|
||||
<SocialLink type="twitch" identifier={data.twitch} />
|
||||
) : null}
|
||||
{data.twitter ? (
|
||||
<SocialLink type="twitter" identifier={data.twitter} />
|
||||
) : null}
|
||||
{data.youtubeId ? (
|
||||
<SocialLink type="youtube" identifier={data.youtubeId} />
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="u__socials">
|
||||
{data.twitch ? (
|
||||
<SocialLink type="twitch" identifier={data.twitch} />
|
||||
) : null}
|
||||
{data.twitter ? (
|
||||
<SocialLink type="twitter" identifier={data.twitter} />
|
||||
) : null}
|
||||
{data.youtubeId ? (
|
||||
<SocialLink type="youtube" identifier={data.youtubeId} />
|
||||
) : null}
|
||||
</div>
|
||||
{data.bio ? <article className="u__bio">{data.bio}</article> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ button.tiny > .button-icon {
|
||||
}
|
||||
|
||||
textarea:not(.plain) {
|
||||
padding: var(--s-2) var(--s-3);
|
||||
padding: var(--s-2-5) var(--s-3);
|
||||
border: 1px solid var(--border);
|
||||
accent-color: var(--theme-secondary);
|
||||
background-color: transparent;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
padding-block-end: var(--s-32);
|
||||
padding-inline: var(--s-2);
|
||||
padding-inline: var(--s-2-5);
|
||||
}
|
||||
|
||||
.layout__burger {
|
||||
|
||||
@@ -8,3 +8,29 @@
|
||||
.u-edit__country-select {
|
||||
max-width: 10rem;
|
||||
}
|
||||
|
||||
.u-edit__bio-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.u-edit__bio-textarea {
|
||||
width: 18rem;
|
||||
max-width: 100%;
|
||||
height: 8rem;
|
||||
}
|
||||
|
||||
.u-edit__length-limit {
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-xs);
|
||||
margin-block-start: -5px;
|
||||
}
|
||||
|
||||
.u-edit__length-limit.warning {
|
||||
color: var(--theme-warning);
|
||||
}
|
||||
|
||||
.u-edit__length-limit.error {
|
||||
color: var(--theme-error);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
.u__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-8);
|
||||
}
|
||||
|
||||
.u__avatar-container {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
@@ -78,3 +84,7 @@
|
||||
.u__social-link.twitch > svg {
|
||||
fill: #9146ff;
|
||||
}
|
||||
|
||||
.u__bio {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@@ -11,24 +11,31 @@ describe("User page", () => {
|
||||
cy.getCy("edit-page-link").should("not.exist");
|
||||
});
|
||||
|
||||
it("edits own profile", function () {
|
||||
it.only("edits own profile", function () {
|
||||
cy.auth();
|
||||
cy.visit("/");
|
||||
cy.getCy("user-avatar").click();
|
||||
cy.getCy("profile-button").click();
|
||||
cy.getCy("edit-page-link").click();
|
||||
cy.getCy("country-select").select("FI");
|
||||
|
||||
const bio = "my cool bio :)";
|
||||
cy.getCy("bio-textarea").type(bio);
|
||||
|
||||
cy.getCy("submit-button").click();
|
||||
|
||||
cy.getCy("profile-page-link").click();
|
||||
cy.contains("Sendou");
|
||||
cy.contains("Finland");
|
||||
cy.contains(bio);
|
||||
|
||||
// let's also check clearing select is possible
|
||||
cy.getCy("edit-page-link").click();
|
||||
cy.getCy("country-select").select(0);
|
||||
cy.getCy("country-select").select(0, { force: true });
|
||||
cy.getCy("submit-button").click();
|
||||
cy.getCy("profile-page-link").click();
|
||||
cy.contains("Sendou");
|
||||
cy.contains("Finland").should("not.exist");
|
||||
cy.contains(bio);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user