import { Link, useFetcher } from "@remix-run/react"; import clsx from "clsx"; import { formatDistanceToNow } from "date-fns"; import React from "react"; import { useTranslation } from "react-i18next"; import { Avatar } from "~/components/Avatar"; import { Divider } from "~/components/Divider"; import { SendouButton } from "~/components/elements/Button"; import { Flag } from "~/components/Flag"; import { FormWithConfirm } from "~/components/FormWithConfirm"; import { Image, TierImage, WeaponImage } from "~/components/Image"; import { EditIcon } from "~/components/icons/Edit"; import { TrashIcon } from "~/components/icons/Trash"; import { useUser } from "~/features/auth/core/user"; import * as Seasons from "~/features/mmr/core/Seasons"; import type { TieredSkill } from "~/features/mmr/tiered.server"; import { useIsMounted } from "~/hooks/useIsMounted"; import { useHasRole } from "~/modules/permissions/hooks"; import { databaseTimestampToDate } from "~/utils/dates"; import { lfgNewPostPage, navIconUrl, userPage } from "~/utils/urls"; import { hourDifferenceBetweenTimezones } from "../core/timezone"; import type { LFGLoaderData, TiersMap } from "../routes/lfg"; import styles from "./LFGPost.module.css"; type Post = LFGLoaderData["posts"][number]; export function LFGPost({ post, tiersMap, }: { post: Post; tiersMap: TiersMap; }) { if (post.team) { return ( ); } return ; } const USER_POST_EXPANDABLE_CRITERIA = 300; function UserLFGPost({ post, tiersMap }: { post: Post; tiersMap: TiersMap }) { const user = useUser(); const isAdmin = useHasRole("ADMIN"); const [isExpanded, setIsExpanded] = React.useState(false); return (
{post.author.id === user?.id || isAdmin ? ( ) : null}
); } function TeamLFGPost({ post, tiersMap, }: { post: Post & { team: NonNullable }; tiersMap: TiersMap; }) { const isMounted = useIsMounted(); const user = useUser(); const isAdmin = useHasRole("ADMIN"); const [isExpanded, setIsExpanded] = React.useState(false); return (
{isMounted && }
{post.author.id === user?.id ? ( ) : null}
{isExpanded ? ( ) : ( )}
{post.author.id === user?.id || isAdmin ? ( ) : null}
); } function PostTeamLogoHeader({ team }: { team: NonNullable }) { return (
{team.avatarUrl ? : null} {team.name}
); } function PostTeamMembersPeek({ team, tiersMap, }: { team: NonNullable; tiersMap: TiersMap; }) { return (
{team.members.map((member) => ( ))}
); } function PostTeamMembersFull({ team, tiersMap, postId, }: { team: NonNullable; tiersMap: TiersMap; postId: number; }) { return (
{team.members.map((member) => (
))}
); } function PostTeamMember({ member, tiersMap, }: { member: NonNullable["members"][number]; tiersMap: TiersMap; }) { const tiers = tiersMap.get(member.id); const tier = tiers?.latest ?? tiers?.previous; return (
{member.username} {tier ? : null}
); } function PostUserHeader({ author, includeWeapons, }: { author: Post["author"]; includeWeapons: boolean; }) { return (
{author.username} {" "} {author.country ? : null}
{includeWeapons ? (
{author.weaponPool.map(({ weaponSplId, isFavorite }) => ( ))}
) : null}
); } function PostTime({ createdAt, updatedAt, }: { createdAt: number; updatedAt: number; }) { const { t, i18n } = useTranslation(["lfg"]); const createdAtDate = databaseTimestampToDate(createdAt); const updatedAtDate = databaseTimestampToDate(updatedAt); const overDayDifferenceBetween = updatedAtDate.getTime() - createdAtDate.getTime() > 1000 * 60 * 60 * 24; return (
{createdAtDate.toLocaleString(i18n.language, { month: "long", day: "numeric", })}{" "} {overDayDifferenceBetween ? (
({t("lfg:post.lastActive")}{" "} {formatDistanceToNow(updatedAtDate, { addSuffix: true, })} )
) : null}
); } function PostPills({ timezone, plusTier, languages, tiers, canEdit, postId, }: { timezone?: string | null; plusTier?: number | null; languages?: string | null; tiers?: NonNullable>; canEdit?: boolean; postId: number; }) { const isMounted = useIsMounted(); return (
{typeof timezone === "string" && isMounted && ( )} {!isMounted && } {typeof plusTier === "number" && ( )} {tiers && } {typeof languages === "string" && ( )} {canEdit && }
); } function PostTimezonePillPlaceholder() { return
; } const currentSeasonNth = Seasons.currentOrPrevious()!.nth; function PostSkillPills({ tiers, }: { tiers: NonNullable>; }) { const hasBoth = tiers.latest && tiers.previous; return (
{tiers.latest ? ( ) : null} {tiers.previous ? ( ) : null}
); } function PostSkillPill({ seasonNth, tier, cut, }: { seasonNth: number; tier: TieredSkill["tier"]; cut?: "START" | "END"; }) { return (
S{seasonNth}
); } function PostPlusServerPill({ plusTier }: { plusTier: number }) { return (
{plusTier}
); } function PostTimezonePill({ timezone }: { timezone: string }) { const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const diff = hourDifferenceBetweenTimezones(userTimezone, timezone); const textColorClass = () => { const absDiff = Math.abs(diff); if (absDiff <= 3) { return "text-success"; } if (absDiff <= 6) { return "text-warning"; } return "text-error"; }; return (
{diff === 0 ? "±" : ""} {diff > 0 ? "+" : ""} {diff}h
); } function PostLanguagePill({ languages }: { languages: string }) { return (
{languages.replace(/,/g, " / ").toUpperCase()}
); } function PostTextTypeHeader({ type }: { type: Post["type"] }) { const { t } = useTranslation(["lfg"]); return (
{t(`lfg:types.${type}`)}
); } function PostEditButton({ id }: { id: number }) { const { t } = useTranslation(["common"]); return ( {t("common:actions.edit")} ); } function PostDeleteButton({ id, type }: { id: number; type: Post["type"] }) { const fetcher = useFetcher(); const { t } = useTranslation(["common", "lfg"]); return ( } > {t("common:actions.delete")} ); } function PostExpandableText({ text, isExpanded: _isExpanded, setIsExpanded, expandableCriteria, }: { text: string; isExpanded: boolean; setIsExpanded: (isExpanded: boolean) => void; expandableCriteria?: number; }) { const { t } = useTranslation(["common"]); const isExpandable = !expandableCriteria || text.length > expandableCriteria; const isExpanded = !isExpandable ? true : _isExpanded; return (
{text}
{isExpandable ? ( setIsExpanded(!isExpanded)} className={clsx([styles.showAllButton], { [styles.showAllButtonExpanded]: isExpanded, })} variant="outlined" size="small" > {isExpanded ? t("common:actions.showLess") : t("common:actions.showMore")} ) : null} {!isExpanded ?
: null}
); }