User page bio

This commit is contained in:
Kalle
2022-05-17 21:40:55 +03:00
parent 7d73a0478c
commit 2e7bb5706d
10 changed files with 120 additions and 32 deletions

View File

@@ -1,3 +1,5 @@
export const USER_BIO_MAX_LENGTH = 2000;
export const navItemsGrouped: {
title: string;
items: {

View File

@@ -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);
}

View File

@@ -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
? {

View File

@@ -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";
}

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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);
});
});