User custom url, ign & sens initial

This commit is contained in:
Kalle
2022-09-29 23:26:25 +03:00
parent 5e0eacf91a
commit 34c1b0e71a
7 changed files with 140 additions and 33 deletions

View File

@@ -5,7 +5,12 @@ import type { BuildAbilitiesTupleWithUnknown } from "./modules/in-game-lists";
export const TWEET_LENGTH_MAX_LENGTH = 280;
export const DISCORD_MESSAGE_MAX_LENGTH = 2000;
export const USER_BIO_MAX_LENGTH = DISCORD_MESSAGE_MAX_LENGTH;
export const USER = {
BIO_MAX_LENGTH: DISCORD_MESSAGE_MAX_LENGTH,
CUSTOM_URL_MAX_LENGTH: 32,
IN_GAME_NAME_TEXT_MAX_LENGTH: 10,
IN_GAME_NAME_DISCRIMINATOR_LENGTH: 4,
};
export const PlUS_SUGGESTION_FIRST_COMMENT_MAX_LENGTH = 500;
export const PlUS_SUGGESTION_COMMENT_MAX_LENGTH = TWEET_LENGTH_MAX_LENGTH;

View File

@@ -30,7 +30,18 @@ export function upsert(
}
const updateProfileStm = sql.prepare(updateProfileSql);
export function updateProfile(args: Pick<User, "country" | "id" | "bio">) {
export function updateProfile(
args: Pick<
User,
| "country"
| "id"
| "bio"
| "customUrl"
| "motionSens"
| "stickSens"
| "inGameName"
>
) {
updateProfileStm.run(args);
}

View File

@@ -2,6 +2,10 @@ update
"User"
set
"country" = @country,
"bio" = @bio
"bio" = @bio,
"customUrl" = @customUrl,
"stickSens" = @stickSens,
"motionSens" = @motionSens,
"inGameName" = @inGameName
where
"id" = @id
"id" = @id

View File

@@ -12,6 +12,10 @@ export interface User {
youtubeId: string | null;
bio: string | null;
country: string | null;
customUrl: string | null;
stickSens: number | null;
motionSens: number | null;
inGameName: string | null;
patronTier: number | null;
patronSince: number | null;
}

View File

@@ -17,7 +17,7 @@ import { z } from "zod";
import { Button } from "~/components/Button";
import { Label } from "~/components/Label";
import { Main } from "~/components/Main";
import { USER_BIO_MAX_LENGTH } from "~/constants";
import { USER } from "~/constants";
import { db } from "~/db";
import { type User } from "~/db/types";
import { requireUser } from "~/modules/auth";
@@ -25,7 +25,7 @@ import { i18next } from "~/modules/i18n";
import styles from "~/styles/u-edit.css";
import { translatedCountry } from "~/utils/i18n.server";
import { parseRequestFormData } from "~/utils/remix";
import { falsyToNull } from "~/utils/zod";
import { falsyToNull, undefinedToNull } from "~/utils/zod";
import { type UserPageLoaderData } from "../u.$identifier";
export const links: LinksFunction = () => {
@@ -44,18 +44,64 @@ const userEditActionSchema = z.object({
),
bio: z.preprocess(
falsyToNull,
z.string().max(USER_BIO_MAX_LENGTH).nullable()
z.string().max(USER.BIO_MAX_LENGTH).nullable()
),
customUrl: z.preprocess(
falsyToNull,
z
.string()
.max(USER.CUSTOM_URL_MAX_LENGTH)
.refine(
(val) => val === null || Number.isNaN(Number(val)),
// xxx: translate
"Name in the custom URL can't only contain numbers"
)
.nullable()
),
stickSens: z.preprocess(
undefinedToNull,
z
.number()
.min(-50)
.max(50)
.refine((val) => val % 5 === 0)
.nullable()
),
motionSens: z.preprocess(
undefinedToNull,
z
.number()
.min(-50)
.max(50)
.refine((val) => val % 5 === 0)
.nullable()
),
inGameNameText: z.preprocess(
falsyToNull,
z.string().max(USER.IN_GAME_NAME_TEXT_MAX_LENGTH).nullable()
),
inGameNameDiscriminator: z.preprocess(
falsyToNull,
z.string().length(USER.IN_GAME_NAME_DISCRIMINATOR_LENGTH).nullable()
),
});
export const action: ActionFunction = async ({ request }) => {
const data = await parseRequestFormData({
request,
schema: userEditActionSchema,
});
const { inGameNameText, inGameNameDiscriminator, ...data } =
await parseRequestFormData({
request,
schema: userEditActionSchema,
});
const user = await requireUser(request);
db.users.updateProfile({ ...data, id: user.id });
db.users.updateProfile({
...data,
inGameName:
inGameNameText && inGameNameDiscriminator
? `${inGameNameText}#${inGameNameDiscriminator}`
: null,
id: user.id,
});
return null;
};
@@ -79,8 +125,7 @@ export const loader = async ({ request }: LoaderArgs) => {
};
export default function UserEditPage() {
const data = useLoaderData<typeof loader>();
const { t } = useTranslation(["common", "user"]);
const { t } = useTranslation(["common"]);
const [, parentRoute] = useMatches();
invariant(parentRoute);
const parentRouteData = parentRoute.data as UserPageLoaderData;
@@ -89,23 +134,7 @@ export default function UserEditPage() {
return (
<Main>
<Form className="u-edit__container" method="post">
<div>
<label htmlFor="country">{t("user:country")}</label>
<select
className="u-edit__country-select"
name="country"
id="country"
defaultValue={parentRouteData.country?.code ?? ""}
data-cy="country-select"
>
<option value="" />
{data.countries.map((country) => (
<option key={country.code} value={country.code}>
{`${country.name} ${country.emoji}`}
</option>
))}
</select>
</div>
<CountrySelect parentRouteData={parentRouteData} />
<BioTextarea initialValue={parentRouteData.bio} />
<Button
loadingText={t("common:actions.saving")}
@@ -120,6 +149,35 @@ export default function UserEditPage() {
);
}
function CountrySelect({
parentRouteData,
}: {
parentRouteData: UserPageLoaderData;
}) {
const { t } = useTranslation(["user"]);
const data = useLoaderData<typeof loader>();
return (
<div>
<label htmlFor="country">{t("user:country")}</label>
<select
className="u-edit__country-select"
name="country"
id="country"
defaultValue={parentRouteData.country?.code ?? ""}
data-cy="country-select"
>
<option value="" />
{data.countries.map((country) => (
<option key={country.code} value={country.code}>
{`${country.name} ${country.emoji}`}
</option>
))}
</select>
</div>
);
}
function BioTextarea({ initialValue }: { initialValue: User["bio"] }) {
const { t } = useTranslation("user");
const [value, setValue] = React.useState(initialValue ?? "");
@@ -128,7 +186,7 @@ function BioTextarea({ initialValue }: { initialValue: User["bio"] }) {
<div className="w-full">
<Label
htmlFor="bio"
valueLimits={{ current: value.length, max: USER_BIO_MAX_LENGTH }}
valueLimits={{ current: value.length, max: USER.BIO_MAX_LENGTH }}
>
{t("bio")}
</Label>
@@ -138,7 +196,7 @@ function BioTextarea({ initialValue }: { initialValue: User["bio"] }) {
data-cy="bio-textarea"
value={value}
onChange={(e) => setValue(e.target.value)}
maxLength={USER_BIO_MAX_LENGTH}
maxLength={USER.BIO_MAX_LENGTH}
/>
</div>
);

View File

@@ -54,6 +54,12 @@ export function falsyToNull(value: unknown): unknown {
return null;
}
export function undefinedToNull(value: unknown): unknown {
if (value === undefined) return null;
return value;
}
export function actualNumber(value: unknown) {
const parsed = Number(value);

View File

@@ -0,0 +1,19 @@
module.exports.up = function (db) {
db.prepare(`alter table "User" add "customUrl" text`).run();
db.prepare(
`create unique index user_custom_url_unique on "User"("customUrl")`
).run();
db.prepare(`alter table "User" add "stickSens" integer`).run();
db.prepare(`alter table "User" add "motionSens" integer`).run();
db.prepare(`alter table "User" add "inGameName" text`).run();
};
module.exports.down = function (db) {
db.prepare(`drop index user_custom_url_unique`).run();
db.prepare(`alter table "User" drop column "customUrl"`).run();
db.prepare(`alter table "User" drop column "stickSens"`).run();
db.prepare(`alter table "User" drop column "motionSens"`).run();
db.prepare(`alter table "User" drop column "inGameName"`).run();
};