mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
163 lines
4.9 KiB
TypeScript
163 lines
4.9 KiB
TypeScript
import clsx from "clsx";
|
|
import { add, sub } from "date-fns";
|
|
import React from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import type { MetaFunction } from "react-router";
|
|
import { useFetcher, useLoaderData } from "react-router";
|
|
import { AddNewButton } from "~/components/AddNewButton";
|
|
import { Alert } from "~/components/Alert";
|
|
import { Main } from "~/components/Main";
|
|
import { SubmitButton } from "~/components/SubmitButton";
|
|
import { useUser } from "~/features/auth/core/user";
|
|
import { useSearchParamStateEncoder } from "~/hooks/useSearchParamState";
|
|
import { databaseTimestampToDate } from "~/utils/dates";
|
|
import { metaTags, type SerializeFrom } from "~/utils/remix";
|
|
import type { SendouRouteHandle } from "~/utils/remix.server";
|
|
import type { Unpacked } from "~/utils/types";
|
|
import { LFG_PAGE, lfgNewPostPage, navIconUrl } from "~/utils/urls";
|
|
import { action } from "../actions/lfg.server";
|
|
import { LFGAddFilterButton } from "../components/LFGAddFilterButton";
|
|
import { LFGFilters } from "../components/LFGFilters";
|
|
import { LFGPost } from "../components/LFGPost";
|
|
import { filterPosts } from "../core/filtering";
|
|
import { LFG } from "../lfg-constants";
|
|
import {
|
|
filterToSmallStr,
|
|
type LFGFilter,
|
|
smallStrToFilter,
|
|
} from "../lfg-types";
|
|
import { loader } from "../loaders/lfg.server";
|
|
import styles from "./lfg.module.css";
|
|
export { loader, action };
|
|
|
|
export const handle: SendouRouteHandle = {
|
|
i18n: ["lfg"],
|
|
breadcrumb: () => ({
|
|
imgPath: navIconUrl("lfg"),
|
|
href: LFG_PAGE,
|
|
type: "IMAGE",
|
|
}),
|
|
};
|
|
|
|
export const meta: MetaFunction = (args) => {
|
|
return metaTags({
|
|
title: "LFG",
|
|
ogTitle: "Splatoon LFG (looking for players, teams & coaches)",
|
|
description:
|
|
"Find people to play Splatoon with. Create a post or browse existing ones. For looking players, teams, scrim partners and coaches alike.",
|
|
location: args.location,
|
|
});
|
|
};
|
|
|
|
export type LFGLoaderData = SerializeFrom<typeof loader>;
|
|
export type LFGLoaderPost = Unpacked<LFGLoaderData["posts"]>;
|
|
export type TiersMap = ReturnType<typeof unserializeTiers>;
|
|
|
|
const unserializeTiers = (data: SerializeFrom<typeof loader>) =>
|
|
new Map(data.tiersMap);
|
|
|
|
function decodeURLQuery(queryString: string): LFGFilter[] {
|
|
if (queryString === "") {
|
|
return [];
|
|
}
|
|
return queryString
|
|
.split("-")
|
|
.map(smallStrToFilter)
|
|
.filter((x) => x !== null);
|
|
}
|
|
|
|
function encodeURLQuery(filters: LFGFilter[]): string {
|
|
return filters.map(filterToSmallStr).join("-");
|
|
}
|
|
|
|
export default function LFGPage() {
|
|
const { t } = useTranslation(["common", "lfg"]);
|
|
const user = useUser();
|
|
const data = useLoaderData<typeof loader>();
|
|
const [filterFromSearch, setTilterFromSearch] = useSearchParamStateEncoder({
|
|
defaultValue: [],
|
|
name: "q",
|
|
revive: decodeURLQuery,
|
|
encode: encodeURLQuery,
|
|
});
|
|
const [filters, _setFilters] = React.useState<LFGFilter[]>(filterFromSearch);
|
|
const setFilters = (x: LFGFilter[]) => {
|
|
setTilterFromSearch(x);
|
|
_setFilters(x);
|
|
};
|
|
|
|
const tiersMap = React.useMemo(() => unserializeTiers(data), [data]);
|
|
|
|
const filteredPosts = filterPosts(data.posts, filters, tiersMap);
|
|
|
|
const showExpiryAlert = (post: Unpacked<LFGLoaderData["posts"]>) => {
|
|
if (post.author.id !== user?.id) return false;
|
|
|
|
const expiryDate = add(databaseTimestampToDate(post.updatedAt), {
|
|
days: LFG.POST_FRESHNESS_DAYS,
|
|
});
|
|
const expiryCloseDate = sub(expiryDate, { days: 7 });
|
|
|
|
if (new Date() < expiryCloseDate) return false;
|
|
|
|
return true;
|
|
};
|
|
|
|
return (
|
|
<Main className="stack xl">
|
|
<div className="stack sm horizontal justify-end">
|
|
<LFGAddFilterButton
|
|
addFilter={(newFilter) => setFilters([...filters, newFilter])}
|
|
filters={filters}
|
|
/>
|
|
<AddNewButton navIcon="lfg" to={lfgNewPostPage()} />
|
|
</div>
|
|
<LFGFilters
|
|
filters={filters}
|
|
changeFilter={(newFilter) =>
|
|
setFilters(
|
|
filters.map((filter) =>
|
|
filter._tag === newFilter._tag ? newFilter : filter,
|
|
),
|
|
)
|
|
}
|
|
removeFilterByTag={(tag) =>
|
|
setFilters(filters.filter((filter) => filter._tag !== tag))
|
|
}
|
|
/>
|
|
{filteredPosts.map((post) => (
|
|
<div
|
|
key={post.id}
|
|
id={String(post.id)}
|
|
className={clsx("stack sm", styles.post)}
|
|
>
|
|
{showExpiryAlert(post) ? <PostExpiryAlert postId={post.id} /> : null}
|
|
<LFGPost post={post} tiersMap={tiersMap} />
|
|
</div>
|
|
))}
|
|
{filteredPosts.length === 0 ? (
|
|
<div className="text-lighter text-lg font-semi-bold text-center mt-6">
|
|
{t("lfg:noPosts")}
|
|
</div>
|
|
) : null}
|
|
</Main>
|
|
);
|
|
}
|
|
|
|
function PostExpiryAlert({ postId }: { postId: number }) {
|
|
const { t } = useTranslation(["common", "lfg"]);
|
|
const fetcher = useFetcher();
|
|
|
|
return (
|
|
<Alert variation="WARNING">
|
|
<fetcher.Form method="post" className="stack md horizontal items-center">
|
|
<input type="hidden" name="id" value={postId} />
|
|
{t("lfg:expiring")}{" "}
|
|
<SubmitButton _action="BUMP_POST" variant="outlined" size="small">
|
|
{t("common:actions.clickHere")}
|
|
</SubmitButton>
|
|
</fetcher.Form>
|
|
</Alert>
|
|
);
|
|
}
|