mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
LFG post language select (#2660)
Co-authored-by: Kalle <38327916+Sendouc@users.noreply.github.com>
This commit is contained in:
parent
04d6ef51c9
commit
d7faf2c4e3
|
|
@ -321,6 +321,7 @@ export interface LFGPost {
|
|||
authorId: number;
|
||||
teamId: number | null;
|
||||
plusTierVisibility: number | null;
|
||||
languages: string | null;
|
||||
updatedAt: Generated<number>;
|
||||
createdAt: GeneratedAlways<number>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export async function posts(user?: { id: number; plusTier: number | null }) {
|
|||
"LFGPost.createdAt",
|
||||
"LFGPost.updatedAt",
|
||||
"LFGPost.plusTierVisibility",
|
||||
"LFGPost.languages",
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom("User")
|
||||
|
|
@ -128,6 +129,7 @@ export function updatePost(
|
|||
timezone: args.timezone,
|
||||
type: args.type,
|
||||
plusTierVisibility: args.plusTierVisibility,
|
||||
languages: args.languages,
|
||||
updatedAt: dateToDatabaseTimestamp(new Date()),
|
||||
})
|
||||
.where("id", "=", postId)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import { requireUser } from "~/features/auth/core/user.server";
|
|||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import { errorToastIfFalsy, parseRequestPayload } from "~/utils/remix.server";
|
||||
import { LFG_PAGE } from "~/utils/urls";
|
||||
import { falsyToNull, id } from "~/utils/zod";
|
||||
import { falsyToNull, id, noDuplicates, safeJSONParse } from "~/utils/zod";
|
||||
import { languagesUnified } from "../../../modules/i18n/config";
|
||||
import * as LFGRepository from "../LFGRepository.server";
|
||||
import { LFG, TEAM_POST_TYPES, TIMEZONES } from "../lfg-constants";
|
||||
|
||||
|
|
@ -39,6 +40,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||
type: data.type,
|
||||
teamId: shouldIncludeTeam ? team?.id : null,
|
||||
plusTierVisibility: data.plusTierVisibility,
|
||||
languages: data.languages.length > 0 ? data.languages.join(",") : null,
|
||||
});
|
||||
} else {
|
||||
await LFGRepository.insertPost({
|
||||
|
|
@ -48,6 +50,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||
teamId: shouldIncludeTeam ? team?.id : null,
|
||||
authorId: user.id,
|
||||
plusTierVisibility: data.plusTierVisibility,
|
||||
languages: data.languages.length > 0 ? data.languages.join(",") : null,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +66,15 @@ const schema = z.object({
|
|||
falsyToNull,
|
||||
z.coerce.number().int().min(1).max(3).nullish(),
|
||||
),
|
||||
languages: z.preprocess(
|
||||
safeJSONParse,
|
||||
z
|
||||
.array(z.string())
|
||||
.refine(noDuplicates)
|
||||
.refine((val) =>
|
||||
val.every((lang) => languagesUnified.some((l) => l.code === lang)),
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
const validateCanUpdatePost = async ({
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ function Filter({
|
|||
return (
|
||||
<div>
|
||||
<div className="stack horizontal justify-between">
|
||||
<Label>
|
||||
<Label htmlFor={`${filter._tag.toLowerCase()}-filter`}>
|
||||
{t(`lfg:filters.${filter._tag}`)} {t("lfg:filters.suffix")}
|
||||
</Label>
|
||||
<SendouButton
|
||||
|
|
@ -166,6 +166,7 @@ function TypeFilterFields({
|
|||
return (
|
||||
<div>
|
||||
<select
|
||||
id="type-filter"
|
||||
className="w-max"
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
|
|
@ -195,6 +196,7 @@ function TimezoneFilterFields({
|
|||
return (
|
||||
<div>
|
||||
<input
|
||||
id="timezone-filter"
|
||||
type="number"
|
||||
value={value}
|
||||
min={0}
|
||||
|
|
@ -220,6 +222,7 @@ function LanguageFilterFields({
|
|||
return (
|
||||
<div>
|
||||
<select
|
||||
id="language-filter"
|
||||
className="w-max"
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
|
|
@ -251,6 +254,7 @@ function PlusTierFilterFields({
|
|||
return (
|
||||
<div>
|
||||
<select
|
||||
id="plustier-filter"
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
changeFilter({ _tag: "PlusTier", tier: Number(e.target.value) })
|
||||
|
|
@ -277,6 +281,7 @@ function TierFilterFields({
|
|||
return (
|
||||
<div>
|
||||
<select
|
||||
id={`${_tag.toLowerCase()}-filter`}
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
changeFilter({ _tag, tier: e.target.value as TierName })
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ function UserLFGPost({ post, tiersMap }: { post: Post; tiersMap: TiersMap }) {
|
|||
/>
|
||||
<PostTime createdAt={post.createdAt} updatedAt={post.updatedAt} />
|
||||
<PostPills
|
||||
languages={post.author.languages}
|
||||
languages={post.languages}
|
||||
plusTier={post.author.plusTier}
|
||||
timezone={post.timezone}
|
||||
tiers={
|
||||
|
|
@ -104,7 +104,12 @@ function TeamLFGPost({
|
|||
<div className="stack xs">
|
||||
<div className="stack horizontal items-center justify-between">
|
||||
<PostTeamLogoHeader team={post.team} />
|
||||
{isMounted && <PostTimezonePill timezone={post.timezone} />}
|
||||
<div className="stack horizontal items-center sm">
|
||||
{isMounted && <PostTimezonePill timezone={post.timezone} />}
|
||||
{post.languages && (
|
||||
<PostLanguagePill languages={post.languages} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="stack horizontal justify-between">
|
||||
|
|
@ -181,7 +186,6 @@ function PostTeamMembersFull({
|
|||
<div key={member.id} className="stack sm">
|
||||
<PostUserHeader author={member} includeWeapons />
|
||||
<PostPills
|
||||
languages={member.languages}
|
||||
plusTier={member.plusTier}
|
||||
tiers={tiersMap.get(member.id)}
|
||||
postId={postId}
|
||||
|
|
|
|||
|
|
@ -63,9 +63,7 @@ function filterMatchesPost(
|
|||
);
|
||||
}
|
||||
case "Language":
|
||||
return checkMatchesSomeUserInPost(post, (user) =>
|
||||
user.languages?.includes(filter.language),
|
||||
);
|
||||
return !!post.languages?.includes(filter.language);
|
||||
case "PlusTier":
|
||||
return checkMatchesSomeUserInPost(
|
||||
post,
|
||||
|
|
|
|||
|
|
@ -16,12 +16,13 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
);
|
||||
const userQSettingsData = await QSettingsRepository.settingsByUserId(user.id);
|
||||
const allPosts = await LFGRepository.posts(user);
|
||||
const postToEdit = searchParamsToBuildToEdit(request, user.id, allPosts);
|
||||
|
||||
return {
|
||||
team: userProfileData?.team,
|
||||
weaponPool: userProfileData?.weapons,
|
||||
languages: userQSettingsData.languages,
|
||||
postToEdit: searchParamsToBuildToEdit(request, user.id, allPosts),
|
||||
languages: postToEdit?.languages?.split(",") ?? userQSettingsData.languages,
|
||||
postToEdit,
|
||||
userPostTypes: userPostTypes(allPosts, user.id),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,23 +1,20 @@
|
|||
import { Link, useFetcher, useLoaderData } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LinkButton } from "~/components/elements/Button";
|
||||
import { LinkButton, SendouButton } from "~/components/elements/Button";
|
||||
import { FormMessage } from "~/components/FormMessage";
|
||||
import { WeaponImage } from "~/components/Image";
|
||||
import { ArrowLeftIcon } from "~/components/icons/ArrowLeft";
|
||||
import { CrossIcon } from "~/components/icons/Cross";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Main } from "~/components/Main";
|
||||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { languagesUnified } from "~/modules/i18n/config";
|
||||
import { useHasRole } from "~/modules/permissions/hooks";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import {
|
||||
LFG_PAGE,
|
||||
navIconUrl,
|
||||
SENDOUQ_SETTINGS_PAGE,
|
||||
userEditProfilePage,
|
||||
} from "~/utils/urls";
|
||||
import { LFG_PAGE, navIconUrl, userEditProfilePage } from "~/utils/urls";
|
||||
import { action } from "../actions/lfg.new.server";
|
||||
import { LFG, TEAM_POST_TYPES, TIMEZONES } from "../lfg-constants";
|
||||
import { loader } from "../loaders/lfg.new.server";
|
||||
|
|
@ -225,19 +222,54 @@ function PlusVisibilitySelect() {
|
|||
function Languages() {
|
||||
const { t } = useTranslation(["lfg"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const [value, setValue] = React.useState(data.languages ?? []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Label>{t("lfg:new.languages.header")}</Label>
|
||||
<div className="stack horizontal sm">
|
||||
{data.languages?.join(" / ").toUpperCase()}
|
||||
<input type="hidden" name="languages" value={JSON.stringify(value)} />
|
||||
<Label htmlFor="postLanguage">{t("lfg:new.languages.header")}</Label>
|
||||
<select
|
||||
id="postLanguage"
|
||||
className="w-max"
|
||||
onChange={(e) => {
|
||||
const newLanguages = [...value, e.target.value].sort((a, b) =>
|
||||
a.localeCompare(b),
|
||||
);
|
||||
setValue(newLanguages);
|
||||
}}
|
||||
>
|
||||
<option value="">{t("lfg:new.languages.placeholder")}</option>
|
||||
{languagesUnified
|
||||
.filter((lang) => !value.includes(lang.code))
|
||||
.map((option) => {
|
||||
return (
|
||||
<option key={option.code} value={option.code}>
|
||||
{option.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<div className="mt-2">
|
||||
{value.map((code) => {
|
||||
const name = languagesUnified.find((l) => l.code === code)?.name;
|
||||
|
||||
return (
|
||||
<div key={code} className="stack horizontal items-center sm">
|
||||
<span>{name}</span>
|
||||
<SendouButton
|
||||
icon={<CrossIcon />}
|
||||
variant="minimal-destructive"
|
||||
onPress={() => {
|
||||
const newLanguages = value.filter(
|
||||
(codeInArr) => codeInArr !== code,
|
||||
);
|
||||
setValue(newLanguages);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<FormMessage type="info">
|
||||
{t("lfg:new.editOn")}{" "}
|
||||
<Link to={SENDOUQ_SETTINGS_PAGE}>
|
||||
{t("lfg:new.languages.sqSettingsPage")}
|
||||
</Link>
|
||||
</FormMessage>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
105
e2e/lfg.spec.ts
105
e2e/lfg.spec.ts
|
|
@ -22,4 +22,109 @@ test.describe("LFG", () => {
|
|||
await expect(page.getByTestId("add-filter-button")).toBeVisible();
|
||||
await expect(page.getByText("looking for a cool team")).toBeVisible();
|
||||
});
|
||||
|
||||
test("creates post with custom languages", async ({ page }) => {
|
||||
await seed(page);
|
||||
await impersonate(page);
|
||||
await navigate({
|
||||
page,
|
||||
url: LFG_PAGE,
|
||||
});
|
||||
|
||||
// create post with Japanese and Korean
|
||||
await page.getByTestId("anything-adder-menu-button").click();
|
||||
await page.getByTestId("menu-item-lfgPost").click();
|
||||
|
||||
await page.getByLabel("Text").fill("looking for Japanese/Korean team");
|
||||
|
||||
const languageSelect = page.getByLabel("Languages");
|
||||
await languageSelect.selectOption("ja");
|
||||
await languageSelect.selectOption("ko");
|
||||
|
||||
await submit(page);
|
||||
|
||||
// verify languages are displayed
|
||||
await expect(page.getByText("JA / KO")).toBeVisible();
|
||||
});
|
||||
|
||||
test("edits post languages", async ({ page }) => {
|
||||
await seed(page);
|
||||
await impersonate(page);
|
||||
await navigate({
|
||||
page,
|
||||
url: LFG_PAGE,
|
||||
});
|
||||
|
||||
// create post with Dansk
|
||||
await page.getByTestId("anything-adder-menu-button").click();
|
||||
await page.getByTestId("menu-item-lfgPost").click();
|
||||
|
||||
await page.getByLabel("Text").fill("test post for language editing");
|
||||
|
||||
const languageSelect = page.getByLabel("Languages");
|
||||
await languageSelect.selectOption("da");
|
||||
|
||||
await submit(page);
|
||||
|
||||
// wait for redirect to LFG page
|
||||
await expect(page.getByTestId("add-filter-button")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("test post for language editing"),
|
||||
).toBeVisible();
|
||||
|
||||
// remove Dansk and add Spanish
|
||||
await page.getByRole("link", { name: "Edit" }).first().click();
|
||||
await page.getByText("Dansk").locator("..").getByRole("button").click();
|
||||
await languageSelect.selectOption("es");
|
||||
|
||||
await submit(page);
|
||||
|
||||
// wait for redirect to LFG page
|
||||
await expect(page.getByTestId("add-filter-button")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("test post for language editing"),
|
||||
).toBeVisible();
|
||||
|
||||
// verify updated language is displayed
|
||||
await expect(page.getByText(/ES/)).toBeVisible();
|
||||
await expect(page.getByText(/DA/)).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("filters posts by language", async ({ page }) => {
|
||||
await seed(page);
|
||||
await impersonate(page);
|
||||
await navigate({
|
||||
page,
|
||||
url: LFG_PAGE,
|
||||
});
|
||||
|
||||
// create post with Japanese
|
||||
await page.getByTestId("anything-adder-menu-button").click();
|
||||
await page.getByTestId("menu-item-lfgPost").click();
|
||||
|
||||
await page.getByLabel("Text").fill("Japanese speaking team");
|
||||
|
||||
const languageSelect = page.getByLabel("Languages");
|
||||
await languageSelect.selectOption("ja");
|
||||
|
||||
await submit(page);
|
||||
|
||||
// wait for redirect to LFG page
|
||||
await expect(page.getByTestId("add-filter-button")).toBeVisible();
|
||||
await expect(page.getByText("Japanese speaking team")).toBeVisible();
|
||||
|
||||
// filter by Japanese
|
||||
await page.getByTestId("add-filter-button").click();
|
||||
await page.getByText("Spoken language").click();
|
||||
await page.getByLabel("Spoken language").selectOption("ja");
|
||||
|
||||
// verify Japanese post is visible
|
||||
await expect(page.getByText("Japanese speaking team")).toBeVisible();
|
||||
|
||||
// change filter to Spanish
|
||||
await page.getByLabel("Spoken language").selectOption("es");
|
||||
|
||||
// verify Japanese post is not visible
|
||||
await expect(page.getByText("Japanese speaking team")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
"new.weaponPool.header": "Våbenpulje",
|
||||
"new.weaponPool.userProfile": "Brugerprofil",
|
||||
"new.languages.header": "Sprog",
|
||||
"new.languages.placeholder": "",
|
||||
"new.languages.sqSettingsPage": "SendouQ opsætnings-side"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
"new.weaponPool.header": "",
|
||||
"new.weaponPool.userProfile": "",
|
||||
"new.languages.header": "",
|
||||
"new.languages.placeholder": "",
|
||||
"new.languages.sqSettingsPage": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
"new.weaponPool.header": "Weapon pool",
|
||||
"new.weaponPool.userProfile": "user profile",
|
||||
"new.languages.header": "Languages",
|
||||
"new.languages.placeholder": "Select all that apply",
|
||||
"new.languages.sqSettingsPage": "SendouQ settings page"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
"new.weaponPool.header": "Grupo de armas",
|
||||
"new.weaponPool.userProfile": "perfil de usuario",
|
||||
"new.languages.header": "Idiomas",
|
||||
"new.languages.placeholder": "Elegir los que correspondan",
|
||||
"new.languages.sqSettingsPage": "página de ajustes de SendouQ"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
"new.weaponPool.header": "Grupo de armas",
|
||||
"new.weaponPool.userProfile": "perfil de usuario",
|
||||
"new.languages.header": "Idiomas",
|
||||
"new.languages.placeholder": "Elegir los que correspondan",
|
||||
"new.languages.sqSettingsPage": "página de ajustes de SendouQ"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
"new.weaponPool.header": "",
|
||||
"new.weaponPool.userProfile": "",
|
||||
"new.languages.header": "",
|
||||
"new.languages.placeholder": "",
|
||||
"new.languages.sqSettingsPage": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
"new.weaponPool.header": "Arme utilisé",
|
||||
"new.weaponPool.userProfile": "profil",
|
||||
"new.languages.header": "Langues",
|
||||
"new.languages.placeholder": "Selectionner",
|
||||
"new.languages.sqSettingsPage": "page de paramètres SendouQ"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
"new.weaponPool.header": "",
|
||||
"new.weaponPool.userProfile": "",
|
||||
"new.languages.header": "",
|
||||
"new.languages.placeholder": "",
|
||||
"new.languages.sqSettingsPage": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
"new.weaponPool.header": "Pool di armi",
|
||||
"new.weaponPool.userProfile": "profilo utente",
|
||||
"new.languages.header": "Lingue",
|
||||
"new.languages.placeholder": "Seleziona quelle che preferisci",
|
||||
"new.languages.sqSettingsPage": "Pagina impostazioni SendouQ"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
"new.weaponPool.header": "武器プール",
|
||||
"new.weaponPool.userProfile": "ユーザープロファイル",
|
||||
"new.languages.header": "言語",
|
||||
"new.languages.placeholder": "該当するものを全て選んでください。",
|
||||
"new.languages.sqSettingsPage": "SendouQ 設定"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
"new.weaponPool.header": "",
|
||||
"new.weaponPool.userProfile": "",
|
||||
"new.languages.header": "",
|
||||
"new.languages.placeholder": "",
|
||||
"new.languages.sqSettingsPage": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
"new.weaponPool.header": "",
|
||||
"new.weaponPool.userProfile": "",
|
||||
"new.languages.header": "",
|
||||
"new.languages.placeholder": "",
|
||||
"new.languages.sqSettingsPage": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
"new.weaponPool.header": "",
|
||||
"new.weaponPool.userProfile": "",
|
||||
"new.languages.header": "",
|
||||
"new.languages.placeholder": "",
|
||||
"new.languages.sqSettingsPage": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
"new.weaponPool.header": "Pool de armas",
|
||||
"new.weaponPool.userProfile": "perfil de usuário",
|
||||
"new.languages.header": "Línguas",
|
||||
"new.languages.placeholder": "Escolha todos que se aplicam",
|
||||
"new.languages.sqSettingsPage": "página de configurações do SendouQ"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
"new.weaponPool.header": "Пул оружия",
|
||||
"new.weaponPool.userProfile": "профиль пользователя",
|
||||
"new.languages.header": "Языки",
|
||||
"new.languages.placeholder": "Выбрать языки",
|
||||
"new.languages.sqSettingsPage": "Страница настроек SendouQ"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
"new.weaponPool.header": "武器池",
|
||||
"new.weaponPool.userProfile": "用户主页",
|
||||
"new.languages.header": "语言",
|
||||
"new.languages.placeholder": "选择所有符合的选项",
|
||||
"new.languages.sqSettingsPage": "SendouQ设定页面"
|
||||
}
|
||||
|
|
|
|||
8
migrations/109-lfg-post-languages.js
Normal file
8
migrations/109-lfg-post-languages.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export function up(db) {
|
||||
db.transaction(() => {
|
||||
db.prepare(/* sql */ `alter table "LFGPost" add "languages" text`).run();
|
||||
db.prepare(
|
||||
/* sql */ `update "LFGPost" set "languages" = (select "languages" from "User" where "User"."id" = "LFGPost"."authorId")`,
|
||||
).run();
|
||||
})();
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user