mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-11 05:36:10 -05:00
User custom url, ign & sens initial
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
19
migrations/008-user-new-fields.js
Normal file
19
migrations/008-user-new-fields.js
Normal 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();
|
||||
};
|
||||
Reference in New Issue
Block a user