mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-24 15:08:44 -05:00
Replace useIsMounted with useHydrated
Strict improvement because we avoid the flash on clientside navigation. One practical bug was scroll restoration between tournament teams list and user page. When user pressed back they ended up at the bottom of the page because the placeholder (smaller height than actual content) rendered. With useHydrated this placeholder is no longer rendered for client side navigations.
This commit is contained in:
parent
2a4d6ad80e
commit
3925b73d32
|
|
@ -1,7 +1,7 @@
|
|||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { BLANK_IMAGE_URL, discordAvatarUrl } from "~/utils/urls";
|
||||
import styles from "./Avatar.module.css";
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ export function Avatar({
|
|||
} & React.ButtonHTMLAttributes<HTMLImageElement>) {
|
||||
const [isErrored, setIsErrored] = React.useState(false);
|
||||
const [loaded, setLoaded] = React.useState(false);
|
||||
const isClient = useIsMounted();
|
||||
const isClient = useHydrated();
|
||||
|
||||
const isIdenticon =
|
||||
!url && (!user?.discordAvatar || isErrored || identiconInput);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { Link } from "react-router";
|
|||
import type { GearType, Tables, UserWithPlusTier } from "~/db/tables";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import type { BuildWeaponWithTop500Info } from "~/features/builds/builds-types";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import type {
|
||||
Ability as AbilityType,
|
||||
|
|
@ -56,7 +56,7 @@ export function BuildCard({ build, owner, canEdit = false }: BuildProps) {
|
|||
const user = useUser();
|
||||
const { t } = useTranslation(["weapons", "builds", "common", "game-misc"]);
|
||||
const { formatDate } = useTimeFormat();
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
const {
|
||||
id,
|
||||
|
|
@ -123,9 +123,9 @@ export function BuildCard({ build, owner, canEdit = false }: BuildProps) {
|
|||
</div>
|
||||
) : null}
|
||||
<time
|
||||
className={clsx("whitespace-nowrap", { invisible: !isMounted })}
|
||||
className={clsx("whitespace-nowrap", { invisible: !isHydrated })}
|
||||
>
|
||||
{isMounted
|
||||
{isHydrated
|
||||
? formatDate(databaseTimestampToDate(updatedAt), {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { type AxisOptions, Chart as ReactChart } from "react-charts";
|
|||
import type { TooltipRendererProps } from "react-charts/types/components/TooltipRenderer";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Theme, useTheme } from "~/features/theme/core/provider";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import styles from "./Chart.module.css";
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ export default function Chart({
|
|||
}) {
|
||||
const { i18n } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
const primaryAxis = React.useMemo<
|
||||
AxisOptions<(typeof options)[number]["data"][number]>
|
||||
|
|
@ -62,7 +62,7 @@ export default function Chart({
|
|||
[],
|
||||
);
|
||||
|
||||
if (!isMounted) {
|
||||
if (!isHydrated) {
|
||||
return <div className={clsx(styles.container, containerClassName)} />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { dateToYearMonthDayHourMinuteString, isValidDate } from "~/utils/dates";
|
||||
import { logger } from "~/utils/logger";
|
||||
|
||||
|
|
@ -38,20 +38,20 @@ export function DateInput({
|
|||
}
|
||||
return [null, ""];
|
||||
});
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
return (
|
||||
<>
|
||||
{parsedDate && isMounted && (
|
||||
{parsedDate && isHydrated && (
|
||||
<input name={name} type="hidden" value={parsedDate.getTime() ?? ""} />
|
||||
)}
|
||||
<input
|
||||
{...inputProps}
|
||||
type="datetime-local"
|
||||
disabled={!isMounted || inputProps.disabled}
|
||||
disabled={!isHydrated || inputProps.disabled}
|
||||
// This is important, because SSR will likely have a date in the wrong
|
||||
// timezone. We can only fill in a value once hydration is over.
|
||||
value={isMounted ? valueString : ""}
|
||||
value={isHydrated ? valueString : ""}
|
||||
min={min ? dateToYearMonthDayHourMinuteString(min) : undefined}
|
||||
max={max ? dateToYearMonthDayHourMinuteString(max) : undefined}
|
||||
onChange={(e) => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { type FetcherWithComponents, useFetcher } from "react-router";
|
||||
import type { SendouButtonProps } from "~/components/elements/Button";
|
||||
import { SendouDialog } from "~/components/elements/Dialog";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { SubmitButton } from "./SubmitButton";
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ export function FormWithConfirm({
|
|||
const componentsFetcher = useFetcher();
|
||||
const fetcher = _fetcher ?? componentsFetcher;
|
||||
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const { t } = useTranslation(["common"]);
|
||||
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||
const formRef = React.useRef<HTMLFormElement>(null);
|
||||
|
|
@ -57,7 +57,7 @@ export function FormWithConfirm({
|
|||
|
||||
return (
|
||||
<>
|
||||
{isMounted
|
||||
{isHydrated
|
||||
? // using portal here makes nesting this component in another form work
|
||||
createPortal(
|
||||
<fetcher.Form
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type * as React from "react";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
|
||||
export function RelativeTime({
|
||||
|
|
@ -9,13 +9,13 @@ export function RelativeTime({
|
|||
children: React.ReactNode;
|
||||
timestamp: number;
|
||||
}) {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const { formatDateTime } = useTimeFormat();
|
||||
|
||||
return (
|
||||
<abbr
|
||||
title={
|
||||
isMounted
|
||||
isHydrated
|
||||
? formatDateTime(new Date(timestamp), {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as React from "react";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { useFetcher } from "react-router";
|
||||
import type { SidebarStream } from "~/features/core/streams/streams.server";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import type { LanguageCode } from "~/modules/i18n/config";
|
||||
import { databaseTimestampToDate, formatDistanceToNow } from "~/utils/dates";
|
||||
import { navIconUrl, tournamentRegisterPage } from "~/utils/urls";
|
||||
|
|
@ -25,7 +25,7 @@ export function StreamListItems({
|
|||
savedTournamentIds?: number[];
|
||||
}) {
|
||||
const { t, i18n } = useTranslation(["front"]);
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
const formatRelativeDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp * 1000);
|
||||
|
|
@ -94,7 +94,7 @@ export function StreamListItems({
|
|||
</span>
|
||||
) : stream.subtitle ? (
|
||||
stream.subtitle
|
||||
) : !isMounted ? (
|
||||
) : !isHydrated ? (
|
||||
<span className="invisible">Placeholder</span>
|
||||
) : isUpcoming ? (
|
||||
formatRelativeDate(stream.startsAt)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from "react-aria-components";
|
||||
import { SendouBottomTexts } from "~/components/elements/BottomTexts";
|
||||
import { SendouCalendar } from "~/components/elements/Calendar";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import styles from "./DatePicker.module.css";
|
||||
import { SendouLabel } from "./Label";
|
||||
|
||||
|
|
@ -32,9 +32,9 @@ export function SendouDatePicker<T extends DateValue>({
|
|||
isRequired,
|
||||
...rest
|
||||
}: SendouDatePickerProps<T>) {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
if (!isMounted) {
|
||||
if (!isHydrated) {
|
||||
return (
|
||||
<div>
|
||||
<SendouLabel required={isRequired}>{label}</SendouLabel>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { createPortal } from "react-dom";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { SendouDialog } from "~/components/elements/Dialog";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { LOG_IN_URL, SENDOU_INK_DISCORD_URL } from "~/utils/urls";
|
||||
import styles from "./UserItem.module.css";
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ export function LogInButtonContainer({
|
|||
children: React.ReactNode;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const [searchParams] = useSearchParams();
|
||||
const authError = searchParams.get("authError");
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ export function LogInButtonContainer({
|
|||
{children}
|
||||
</form>
|
||||
{authError != null &&
|
||||
isMounted &&
|
||||
isHydrated &&
|
||||
createPortal(
|
||||
<SendouDialog
|
||||
isDismissable
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { Link, useFetcher, useLocation, useMatches } from "react-router";
|
|||
import { useUser } from "~/features/auth/core/user";
|
||||
import { useChatContext } from "~/features/chat/useChatContext";
|
||||
import { FriendMenu } from "~/features/friends/components/FriendMenu";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import type { RootLoaderData } from "~/root";
|
||||
import type { Breadcrumb, SendouRouteHandle } from "~/utils/remix.server";
|
||||
import {
|
||||
|
|
@ -215,7 +215,7 @@ export function Layout({
|
|||
|
||||
const { t } = useTranslation(["front", "common"]);
|
||||
const { formatRelativeDate } = useTimeFormat();
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const location = useLocation();
|
||||
const headerRef = React.useRef<HTMLElement>(null);
|
||||
const navOffset = useNavOffset(headerRef);
|
||||
|
|
@ -280,7 +280,7 @@ export function Layout({
|
|||
to={event.url}
|
||||
imageUrl={event.logoUrl ?? undefined}
|
||||
subtitle={
|
||||
isMounted ? (
|
||||
isHydrated ? (
|
||||
formatRelativeDate(event.startTime)
|
||||
) : (
|
||||
<span className="invisible">Placeholder</span>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { LinkButton, SendouButton } from "~/components/elements/Button";
|
|||
import { SendouDialog } from "~/components/elements/Dialog";
|
||||
import { FormWithConfirm } from "~/components/FormWithConfirm";
|
||||
import { Pagination } from "~/components/Pagination";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { usePagination } from "~/hooks/usePagination";
|
||||
import { useSearchParamState } from "~/hooks/useSearchParamState";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
|
|
@ -49,9 +49,9 @@ export function ArtGrid({
|
|||
revive: (value) =>
|
||||
itemsToDisplay.find((art) => art.id === Number(value))?.id,
|
||||
});
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
if (!isMounted) return null;
|
||||
if (!isHydrated) return null;
|
||||
|
||||
const bigArt = itemsToDisplay.find((art) => art.id === bigArtId);
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { Placeholder } from "~/components/Placeholder";
|
|||
import { Table } from "~/components/Table";
|
||||
import { WeaponSelect } from "~/components/WeaponSelect";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { abilitiesShort } from "~/modules/in-game-lists/abilities";
|
||||
import type {
|
||||
Ability as AbilityType,
|
||||
|
|
@ -114,9 +114,9 @@ export const handle: SendouRouteHandle = {
|
|||
export const shouldRevalidate: ShouldRevalidateFunction = () => false;
|
||||
|
||||
export default function BuildAnalyzerShell() {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
if (!isMounted) {
|
||||
if (!isHydrated) {
|
||||
return <Placeholder />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { Flag } from "~/components/Flag";
|
|||
import { Image, ModeImage } from "~/components/Image";
|
||||
import { TierPill } from "~/components/TierPill";
|
||||
import { BadgeDisplay } from "~/features/badges/components/BadgeDisplay";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { navIconUrl } from "~/utils/urls";
|
||||
|
|
@ -25,7 +25,7 @@ export function TournamentCard({
|
|||
className?: string;
|
||||
withRelativeTime?: boolean;
|
||||
}) {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const { formatDateTimeSmartMinutes, formatDistanceToNow } = useTimeFormat();
|
||||
|
||||
const isShowcase = tournament.type === "showcase";
|
||||
|
|
@ -34,7 +34,7 @@ export function TournamentCard({
|
|||
|
||||
const time = () => {
|
||||
if (!isShowcase) return null;
|
||||
if (!isMounted) return "Placeholder";
|
||||
if (!isHydrated) return "Placeholder";
|
||||
|
||||
const date = databaseTimestampToDate(tournament.startTime);
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ export function TournamentCard({
|
|||
{isShowcase ? (
|
||||
<time
|
||||
className={clsx(styles.time, {
|
||||
invisible: !isMounted,
|
||||
invisible: !isHydrated,
|
||||
})}
|
||||
dateTime={databaseTimestampToDate(
|
||||
tournament.startTime,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { Section } from "~/components/Section";
|
|||
import { Table } from "~/components/Table";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { MapPool } from "~/features/map-list-generator/core/map-pool";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
|
|
@ -81,7 +81,7 @@ export default function CalendarEventPage() {
|
|||
const user = useUser();
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const { t } = useTranslation(["common", "calendar"]);
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const { formatDateTime } = useTimeFormat();
|
||||
|
||||
return (
|
||||
|
|
@ -100,7 +100,7 @@ export default function CalendarEventPage() {
|
|||
})}
|
||||
</span>
|
||||
<time dateTime={databaseTimestampToDate(startTime).toISOString()}>
|
||||
{isMounted
|
||||
{isHydrated
|
||||
? formatDateTime(databaseTimestampToDate(startTime), {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { SubmitButton } from "~/components/SubmitButton";
|
|||
import type { CalendarEventTag, Tables } from "~/db/tables";
|
||||
import { MapPool } from "~/features/map-list-generator/core/map-pool";
|
||||
import * as Progression from "~/features/tournament-bracket/core/Progression";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import type { RankedModeShort } from "~/modules/in-game-lists/types";
|
||||
import { useHasRole } from "~/modules/permissions/hooks";
|
||||
|
|
@ -460,8 +460,8 @@ function DatesInput({ allowMultiDate }: { allowMultiDate?: boolean }) {
|
|||
|
||||
const datesCount = datesInputState.length;
|
||||
|
||||
const isMounted = useIsMounted();
|
||||
const usersTimeZone = isMounted
|
||||
const isHydrated = useHydrated();
|
||||
const usersTimeZone = isHydrated
|
||||
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
: "";
|
||||
const NEW_CALENDAR_EVENT_HOURS_OFFSET = 24;
|
||||
|
|
@ -541,7 +541,7 @@ function DatesInput({ allowMultiDate }: { allowMultiDate?: boolean }) {
|
|||
</div>
|
||||
{datesCount < CALENDAR_EVENT.MAX_AMOUNT_OF_DATES &&
|
||||
allowMultiDate && <AddButton onAdd={addDate} />}
|
||||
<FormMessage type="info" className={clsx({ invisible: !isMounted })}>
|
||||
<FormMessage type="info" className={clsx({ invisible: !isHydrated })}>
|
||||
{t("calendar:inYourTimeZone")} {usersTimeZone}
|
||||
</FormMessage>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useState } from "react";
|
|||
import type { MetaFunction, ShouldRevalidateFunction } from "react-router";
|
||||
import { Main } from "~/components/Main";
|
||||
import { Placeholder } from "~/components/Placeholder";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists/types";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { COMP_ANALYZER_URL, navIconUrl } from "~/utils/urls";
|
||||
|
|
@ -37,9 +37,9 @@ export const handle: SendouRouteHandle = {
|
|||
export const shouldRevalidate: ShouldRevalidateFunction = () => false;
|
||||
|
||||
export default function CompAnalyzerShell() {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
if (!isMounted) {
|
||||
if (!isHydrated) {
|
||||
return <Placeholder />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { TournamentCard } from "~/features/calendar/components/TournamentCard";
|
|||
import { SplatoonRotations } from "~/features/front-page/components/SplatoonRotations";
|
||||
import type * as Changelog from "~/features/front-page/core/Changelog.server";
|
||||
import * as Seasons from "~/features/mmr/core/Seasons";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import styles from "~/styles/front.module.css";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
|
|
@ -66,10 +66,10 @@ function SeasonDates({
|
|||
season: ReturnType<typeof useSeasonData>["season"];
|
||||
className: string;
|
||||
}) {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const { formatDate } = useTimeFormat();
|
||||
|
||||
return isMounted ? (
|
||||
return isHydrated ? (
|
||||
<div className={className}>
|
||||
{formatDate(season.starts, { month: "long", day: "numeric" })} -{" "}
|
||||
{formatDate(season.ends, { month: "long", day: "numeric" })}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { Image, TierImage, WeaponImage } from "~/components/Image";
|
|||
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 { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import { useHasRole } from "~/modules/permissions/hooks";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
|
|
@ -92,7 +92,7 @@ function TeamLFGPost({
|
|||
post: Post & { team: NonNullable<Post["team"]> };
|
||||
tiersMap: TiersMap;
|
||||
}) {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const user = useUser();
|
||||
const isAdmin = useHasRole("ADMIN");
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
|
|
@ -104,7 +104,7 @@ function TeamLFGPost({
|
|||
<div className="stack horizontal items-center justify-between">
|
||||
<PostTeamLogoHeader team={post.team} />
|
||||
<div className="stack horizontal items-center sm">
|
||||
{isMounted && <PostTimezonePill timezone={post.timezone} />}
|
||||
{isHydrated && <PostTimezonePill timezone={post.timezone} />}
|
||||
{post.languages && (
|
||||
<PostLanguagePill languages={post.languages} />
|
||||
)}
|
||||
|
|
@ -305,18 +305,18 @@ function PostPills({
|
|||
canEdit?: boolean;
|
||||
postId: number;
|
||||
}) {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("stack sm xs-row horizontal flex-wrap", {
|
||||
invisible: !isMounted,
|
||||
invisible: !isHydrated,
|
||||
})}
|
||||
>
|
||||
{typeof timezone === "string" && isMounted && (
|
||||
{typeof timezone === "string" && isHydrated && (
|
||||
<PostTimezonePill timezone={timezone} />
|
||||
)}
|
||||
{!isMounted && <PostTimezonePillPlaceholder />}
|
||||
{!isHydrated && <PostTimezonePillPlaceholder />}
|
||||
{typeof plusTier === "number" && (
|
||||
<PostPlusServerPill plusTier={plusTier} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { lazy } from "react";
|
||||
import type { MetaFunction } from "react-router";
|
||||
import { Placeholder } from "~/components/Placeholder";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { navIconUrl, PLANNER_URL } from "~/utils/urls";
|
||||
|
|
@ -30,9 +30,9 @@ export const handle: SendouRouteHandle = {
|
|||
const Planner = lazy(() => import("~/features/map-planner/components/Planner"));
|
||||
|
||||
export default function MapPlannerPage() {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
if (!isMounted) return <Placeholder />;
|
||||
if (!isHydrated) return <Placeholder />;
|
||||
|
||||
return <Planner />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import * as R from "remeda";
|
|||
import type { z } from "zod";
|
||||
import { LinkButton, SendouButton } from "~/components/elements/Button";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
|
|
@ -59,9 +59,9 @@ export default function ScrimsPage() {
|
|||
const user = useUser();
|
||||
const { t } = useTranslation(["calendar", "scrims"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
if (!isMounted)
|
||||
if (!isHydrated)
|
||||
return (
|
||||
<Main>
|
||||
<div className={styles.placeholder} />
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import { useRecentlyReportedWeapons } from "~/features/sendouq/q-hooks";
|
|||
import { AddPrivateNoteDialog } from "~/features/sendouq-match/components/AddPrivateNoteDialog";
|
||||
import type { ReportedWeaponForMerging } from "~/features/sendouq-match/core/reported-weapons.server";
|
||||
import { resolveRoomPass } from "~/features/tournament-bracket/tournament-bracket-utils";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useMainContentWidth } from "~/hooks/useMainContentWidth";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists/types";
|
||||
|
|
@ -93,9 +93,9 @@ export const handle: SendouRouteHandle = {
|
|||
};
|
||||
|
||||
export default function QMatchShell() {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
if (!isMounted)
|
||||
if (!isHydrated)
|
||||
return (
|
||||
<Main>
|
||||
<Placeholder />
|
||||
|
|
@ -108,7 +108,7 @@ export default function QMatchShell() {
|
|||
function QMatchPage() {
|
||||
const user = useUser();
|
||||
const isStaff = useHasRole("STAFF");
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const { t } = useTranslation(["q"]);
|
||||
const { formatDateTime } = useTimeFormat();
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
|
@ -161,10 +161,10 @@ function QMatchPage() {
|
|||
<h2>{t("q:match.header", { number: data.match.id })}</h2>
|
||||
<div
|
||||
className={clsx("text-xs text-lighter", {
|
||||
invisible: !isMounted,
|
||||
invisible: !isHydrated,
|
||||
})}
|
||||
>
|
||||
{isMounted
|
||||
{isHydrated
|
||||
? formatDateTime(databaseTimestampToDate(data.match.createdAt), {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
|
|
@ -249,7 +249,7 @@ function Score({
|
|||
reportedAt: number;
|
||||
ownTeamReported: boolean;
|
||||
}) {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const { t } = useTranslation(["q"]);
|
||||
const { formatDateTime } = useTimeFormat();
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
|
@ -300,10 +300,10 @@ function Score({
|
|||
<div className="text-lg font-bold">{score.join(" - ")}</div>
|
||||
{data.match.isLocked ? (
|
||||
<div
|
||||
className={clsx("text-xs text-lighter", { invisible: !isMounted })}
|
||||
className={clsx("text-xs text-lighter", { invisible: !isHydrated })}
|
||||
>
|
||||
{t("q:match.reportedBy", { name: reporter?.username ?? "admin" })}{" "}
|
||||
{isMounted
|
||||
{isHydrated
|
||||
? formatDateTime(databaseTimestampToDate(reportedAt), {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
|
|
@ -653,7 +653,7 @@ function BottomSection({
|
|||
const { t } = useTranslation(["q", "common"]);
|
||||
const width = useMainContentWidth();
|
||||
const isMobile = width < 650;
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const user = useUser();
|
||||
const isStaff = useHasRole("STAFF");
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
|
@ -669,7 +669,7 @@ function BottomSection({
|
|||
return `SQ${lastDigit}`;
|
||||
};
|
||||
|
||||
if (!isMounted) return null;
|
||||
if (!isHydrated) return null;
|
||||
|
||||
const mapListElement = (
|
||||
<MapList
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
} from "~/features/chat/chat-utils";
|
||||
import { updateNoScreenSchema } from "~/features/settings/settings-schemas";
|
||||
import { SendouForm } from "~/form/SendouForm";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { modesShort } from "~/modules/in-game-lists/modes";
|
||||
import type { ModeShort } from "~/modules/in-game-lists/types";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
|
|
@ -304,7 +304,7 @@ function WeaponPool() {
|
|||
|
||||
function Sounds() {
|
||||
const { t } = useTranslation(["q"]);
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
return (
|
||||
<details>
|
||||
|
|
@ -314,8 +314,8 @@ function Sounds() {
|
|||
</div>
|
||||
</summary>
|
||||
<div className="mb-4">
|
||||
{isMounted && <SoundCheckboxes />}
|
||||
{isMounted && <SoundSlider />}
|
||||
{isHydrated && <SoundCheckboxes />}
|
||||
{isHydrated && <SoundSlider />}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Avatar } from "~/components/Avatar";
|
|||
import { TierImage, WeaponImage } from "~/components/Image";
|
||||
import { Main } from "~/components/Main";
|
||||
import { useAutoRerender } from "~/hooks/useAutoRerender";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { twitchThumbnailUrlToSrc } from "~/modules/twitch/utils";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
|
|
@ -125,10 +125,10 @@ export default function SendouQStreamsPage() {
|
|||
|
||||
function RelativeStartTime({ startedAt }: { startedAt: Date }) {
|
||||
const { i18n } = useTranslation();
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
useAutoRerender();
|
||||
|
||||
if (!isMounted) return null;
|
||||
if (!isHydrated) return null;
|
||||
|
||||
const minutesAgo = Math.floor((startedAt.getTime() - Date.now()) / 1000 / 60);
|
||||
const formatter = new Intl.RelativeTimeFormat(i18n.language, {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { Placeholder } from "~/components/Placeholder";
|
|||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { useAutoRefresh } from "~/hooks/useAutoRefresh";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useMainContentWidth } from "~/hooks/useMainContentWidth";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
|
|
@ -62,9 +62,9 @@ export const meta: MetaFunction = (args) => {
|
|||
};
|
||||
|
||||
export default function QLookingShell() {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
if (!isMounted)
|
||||
if (!isHydrated)
|
||||
return (
|
||||
<Main>
|
||||
<Placeholder />
|
||||
|
|
@ -115,7 +115,7 @@ function QLookingPage() {
|
|||
|
||||
function InfoText() {
|
||||
const { t } = useTranslation(["q"]);
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const fetcher = useFetcher();
|
||||
const { formatTime } = useTimeFormat();
|
||||
|
|
@ -165,7 +165,7 @@ function InfoText() {
|
|||
return (
|
||||
<div
|
||||
className={clsx("text-xs text-lighter stack horizontal justify-between", {
|
||||
invisible: !isMounted,
|
||||
invisible: !isHydrated,
|
||||
})}
|
||||
>
|
||||
<div className="stack sm horizontal">
|
||||
|
|
@ -181,7 +181,7 @@ function InfoText() {
|
|||
<StreamsLinkButton />
|
||||
</div>
|
||||
<span className="text-xxs">
|
||||
{isMounted
|
||||
{isHydrated
|
||||
? t("q:looking.lastUpdatedAt", {
|
||||
time: formatTime(new Date(data.lastUpdated)),
|
||||
})
|
||||
|
|
@ -211,11 +211,11 @@ function StreamsLinkButton() {
|
|||
function Groups() {
|
||||
const { t } = useTranslation(["q"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
const width = useMainContentWidth();
|
||||
|
||||
if (!isMounted) return null;
|
||||
if (!isHydrated) return null;
|
||||
|
||||
const isMobile = width < IS_Q_LOOKING_MOBILE_BREAKPOINT;
|
||||
const isFullGroup =
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import type { Tables } from "~/db/tables";
|
|||
import { useUser } from "~/features/auth/core/user";
|
||||
import type * as Seasons from "~/features/mmr/core/Seasons";
|
||||
import { useAutoRerender } from "~/hooks/useAutoRerender";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useHasRole } from "~/modules/permissions/hooks";
|
||||
import { metaTags, type SerializeFrom } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
|
|
@ -208,7 +208,7 @@ const clockFormatter = ({
|
|||
minute: "numeric",
|
||||
});
|
||||
function Clocks() {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const { t, i18n } = useTranslation(["q"]);
|
||||
useAutoRerender();
|
||||
|
||||
|
|
@ -221,8 +221,8 @@ function Clocks() {
|
|||
{t(`q:front.cities.${country.city}`)}
|
||||
</div>
|
||||
<Flag countryCode={country.countryCode} />
|
||||
<div className={clsx({ invisible: !isMounted })}>
|
||||
{isMounted
|
||||
<div className={clsx({ invisible: !isHydrated })}>
|
||||
{isHydrated
|
||||
? weekdayFormatter({
|
||||
timeZone: country.timeZone,
|
||||
locale: i18n.language,
|
||||
|
|
@ -230,8 +230,8 @@ function Clocks() {
|
|||
: // take space
|
||||
"Monday"}
|
||||
</div>
|
||||
<div className={clsx({ invisible: !isMounted })}>
|
||||
{isMounted
|
||||
<div className={clsx({ invisible: !isHydrated })}>
|
||||
{isHydrated
|
||||
? clockFormatter({
|
||||
timeZone: country.timeZone,
|
||||
locale: i18n.language,
|
||||
|
|
@ -294,7 +294,7 @@ function ActiveSeasonInfo({
|
|||
season: SerializeFrom<Seasons.ListItem>;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation(["q"]);
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
const starts = new Date(season.starts);
|
||||
const ends = new Date(season.ends);
|
||||
|
|
@ -310,11 +310,11 @@ function ActiveSeasonInfo({
|
|||
return (
|
||||
<div
|
||||
className={clsx("text-lighter text-xs text-center", {
|
||||
invisible: !isMounted,
|
||||
invisible: !isHydrated,
|
||||
})}
|
||||
>
|
||||
{t("q:front.seasonOpen", { nth: season.nth })}{" "}
|
||||
{isMounted ? (
|
||||
{isHydrated ? (
|
||||
<b>
|
||||
{dateToString(starts)} - {dateToString(ends)}
|
||||
</b>
|
||||
|
|
@ -401,8 +401,8 @@ function UpcomingSeasonInfo({
|
|||
season: SerializeFrom<Seasons.ListItem>;
|
||||
}) {
|
||||
const { t } = useTranslation(["q"]);
|
||||
const isMounted = useIsMounted();
|
||||
if (!isMounted) return null;
|
||||
const isHydrated = useHydrated();
|
||||
if (!isHydrated) return null;
|
||||
|
||||
const starts = new Date(season.starts);
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import { ModeImage } from "~/components/Image";
|
|||
import { Main } from "~/components/Main";
|
||||
import { Placeholder } from "~/components/Placeholder";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { modesShort } from "~/modules/in-game-lists/modes";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
|
|
@ -65,9 +65,9 @@ export const handle: SendouRouteHandle = {
|
|||
};
|
||||
|
||||
export default function TierListMakerPage() {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
if (!isMounted)
|
||||
if (!isHydrated)
|
||||
return (
|
||||
<Main bigger>
|
||||
<Placeholder />
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import {
|
|||
isLeagueRoundLocked,
|
||||
resolveLeagueRoundStartDate,
|
||||
} from "~/features/tournament/tournament-utils";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useSearchParamState } from "~/hooks/useSearchParamState";
|
||||
import type { StageId } from "~/modules/in-game-lists/types";
|
||||
import { SPLATTERCOLOR_SCREEN_ID } from "~/modules/in-game-lists/weapon-ids";
|
||||
|
|
@ -78,7 +78,7 @@ export function StartedMatch({
|
|||
type: "EDIT" | "OTHER";
|
||||
}) {
|
||||
const { t } = useTranslation(["tournament"]);
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const user = useUser();
|
||||
const tournament = useTournament();
|
||||
const data = useLoaderData<TournamentMatchLoaderData>();
|
||||
|
|
@ -252,11 +252,11 @@ export function StartedMatch({
|
|||
{result ? (
|
||||
<div
|
||||
className={clsx("text-center text-xs text-lighter", {
|
||||
invisible: !isMounted,
|
||||
invisible: !isHydrated,
|
||||
})}
|
||||
data-testid="report-timestamp"
|
||||
>
|
||||
{isMounted
|
||||
{isHydrated
|
||||
? databaseTimestampToDate(result.createdAt).toLocaleString()
|
||||
: "t"}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from "~/components/elements/Tabs";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useSearchParamState } from "~/hooks/useSearchParamState";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import { useVisibilityChange } from "~/hooks/useVisibilityChange";
|
||||
|
|
@ -45,7 +45,7 @@ export default function TournamentBracketsPage() {
|
|||
const { revalidate } = useRevalidator();
|
||||
const user = useUser();
|
||||
const tournament = useTournament();
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const ctx = useOutletContext();
|
||||
|
||||
const defaultBracketIdx = () => {
|
||||
|
|
@ -88,7 +88,7 @@ export default function TournamentBracketsPage() {
|
|||
tournament.isOrganizer(user) &&
|
||||
!bracket.canBeStarted &&
|
||||
bracket.preview &&
|
||||
isMounted;
|
||||
isHydrated;
|
||||
|
||||
const waitingForTeamsText = () => {
|
||||
if (bracketIdx > 0 || tournament.regularCheckInStartInThePast) {
|
||||
|
|
@ -252,7 +252,7 @@ function BracketStarter({
|
|||
bracketIdx: number;
|
||||
}) {
|
||||
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
const close = React.useCallback(() => {
|
||||
setDialogOpen(false);
|
||||
|
|
@ -260,7 +260,7 @@ function BracketStarter({
|
|||
|
||||
return (
|
||||
<>
|
||||
{isMounted && dialogOpen ? (
|
||||
{isHydrated && dialogOpen ? (
|
||||
<BracketMapListDialog
|
||||
close={close}
|
||||
bracket={bracket}
|
||||
|
|
@ -308,7 +308,7 @@ function MapPreparer({
|
|||
bracketIdx: number;
|
||||
}) {
|
||||
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const prepared = useTournamentPreparedMaps();
|
||||
const tournament = useTournament();
|
||||
|
||||
|
|
@ -326,7 +326,7 @@ function MapPreparer({
|
|||
|
||||
return (
|
||||
<>
|
||||
{isMounted && dialogOpen ? (
|
||||
{isHydrated && dialogOpen ? (
|
||||
<BracketMapListDialog
|
||||
close={close}
|
||||
bracket={bracket}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { useUser } from "~/features/auth/core/user";
|
|||
import { IS_Q_LOOKING_MOBILE_BREAKPOINT } from "~/features/sendouq/q-constants";
|
||||
import { useTournament } from "~/features/tournament/routes/to.$id";
|
||||
import { SendouForm } from "~/form/SendouForm";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useMainContentWidth } from "~/hooks/useMainContentWidth";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { userPage } from "~/utils/urls";
|
||||
|
|
@ -47,9 +47,9 @@ export const handle: SendouRouteHandle = {
|
|||
};
|
||||
|
||||
export default function TournamentLFGShell() {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
if (!isMounted) return <Placeholder />;
|
||||
if (!isHydrated) return <Placeholder />;
|
||||
|
||||
return <TournamentLFGPage />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import clsx from "clsx";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { LinkButton } from "~/components/elements/Button";
|
||||
import type { MonthYear } from "~/features/plus-voting/core";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import { databaseTimestampToDate, nullPaddedDatesOfMonth } from "~/utils/dates";
|
||||
import type { SerializeFrom } from "~/utils/remix";
|
||||
|
|
@ -23,7 +23,7 @@ export function EventCalendar({
|
|||
fallbackLogoUrl,
|
||||
}: EventCalendarProps) {
|
||||
const dates = nullPaddedDatesOfMonth({ month, year });
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const dayHeaders = Array.from({ length: 7 }, (_, i) => {
|
||||
|
|
@ -47,7 +47,7 @@ export function EventCalendar({
|
|||
const startTimeDate = databaseTimestampToDate(event.startTime);
|
||||
|
||||
return (
|
||||
isMounted &&
|
||||
isHydrated &&
|
||||
startTimeDate.getDate() === date?.getUTCDate() &&
|
||||
startTimeDate.getMonth() === date.getUTCMonth()
|
||||
);
|
||||
|
|
@ -76,14 +76,14 @@ function EventCalendarCell({
|
|||
events: SerializeFrom<typeof loader>["events"];
|
||||
fallbackLogoUrl: string;
|
||||
}) {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.calendarDay, {
|
||||
[styles.calendarDayPrevious]: !date,
|
||||
[styles.calendarDayToday]:
|
||||
isMounted &&
|
||||
isHydrated &&
|
||||
date?.getDate() === new Date().getDate() &&
|
||||
date?.getMonth() === new Date().getMonth() &&
|
||||
date?.getFullYear() === new Date().getFullYear(),
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { useUser } from "~/features/auth/core/user";
|
|||
import { BadgeDisplay } from "~/features/badges/components/BadgeDisplay";
|
||||
import { BannedUsersList } from "~/features/tournament-organization/components/BannedPlayersList";
|
||||
import { SendouForm } from "~/form/SendouForm";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import { useHasPermission, useHasRole } from "~/modules/permissions/hooks";
|
||||
import { databaseTimestampNow, databaseTimestampToDate } from "~/utils/dates";
|
||||
|
|
@ -503,7 +503,7 @@ function EventInfo({
|
|||
showYear?: boolean;
|
||||
}) {
|
||||
const { formatDateTime } = useTimeFormat();
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
return (
|
||||
<div className="stack sm">
|
||||
|
|
@ -521,9 +521,9 @@ function EventInfo({
|
|||
<div>
|
||||
<div>{event.name}</div>
|
||||
<time
|
||||
className={clsx(styles.eventInfoTime, { invisible: !isMounted })}
|
||||
className={clsx(styles.eventInfoTime, { invisible: !isHydrated })}
|
||||
>
|
||||
{isMounted
|
||||
{isHydrated
|
||||
? formatDateTime(databaseTimestampToDate(event.startTime), {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ import { MapPool } from "~/features/map-list-generator/core/map-pool";
|
|||
import { ModeMapPoolPicker } from "~/features/sendouq-settings/components/ModeMapPoolPicker";
|
||||
import type { TournamentDataTeam } from "~/features/tournament-bracket/core/Tournament.server";
|
||||
import { useAutoRerender } from "~/hooks/useAutoRerender";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useSearchParamState } from "~/hooks/useSearchParamState";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import { modesShort, rankedModesShort } from "~/modules/in-game-lists/modes";
|
||||
|
|
@ -78,7 +78,7 @@ import { useTournament } from "./to.$id";
|
|||
export { action, loader };
|
||||
|
||||
export default function TournamentRegisterPage() {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const tournament = useTournament();
|
||||
|
||||
return (
|
||||
|
|
@ -122,7 +122,7 @@ export default function TournamentRegisterPage() {
|
|||
<div className={clsx(styles.by, "mt-2")}>
|
||||
<div className="stack horizontal xs items-center">
|
||||
<Clock className={styles.infoIcon} />{" "}
|
||||
{isMounted ? (
|
||||
{isHydrated ? (
|
||||
<TimePopover
|
||||
time={tournament.ctx.startTime}
|
||||
options={{
|
||||
|
|
@ -393,7 +393,7 @@ function RegistrationProgress({
|
|||
}) {
|
||||
const { t } = useTranslation(["tournament"]);
|
||||
const tournament = useTournament();
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const { formatDate } = useTimeFormat();
|
||||
|
||||
const completedIfTruthy = (condition: unknown) =>
|
||||
|
|
@ -434,7 +434,7 @@ function RegistrationProgress({
|
|||
tournament.registrationClosesAt.getTime() !==
|
||||
tournament.ctx.startTime.getTime();
|
||||
|
||||
const registrationClosesAtString = isMounted
|
||||
const registrationClosesAtString = isHydrated
|
||||
? formatDate(
|
||||
tournament.isLeagueSignup
|
||||
? tournament.ctx.startTime
|
||||
|
|
@ -523,13 +523,13 @@ function CheckIn({
|
|||
checkedIn?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation(["tournament"]);
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const fetcher = useFetcher();
|
||||
const { formatTime } = useTimeFormat();
|
||||
|
||||
useAutoRerender();
|
||||
|
||||
const checkInStartsString = isMounted
|
||||
const checkInStartsString = isHydrated
|
||||
? formatTime(startDate, {
|
||||
minute: "numeric",
|
||||
hour: "numeric",
|
||||
|
|
@ -538,7 +538,7 @@ function CheckIn({
|
|||
})
|
||||
: "";
|
||||
|
||||
const checkInEndsString = isMounted
|
||||
const checkInEndsString = isHydrated
|
||||
? formatTime(endDate, {
|
||||
minute: "numeric",
|
||||
hour: "numeric",
|
||||
|
|
@ -549,7 +549,7 @@ function CheckIn({
|
|||
|
||||
if (status === "UPCOMING") {
|
||||
return (
|
||||
<div className={clsx("text-center text-xs", { invisible: !isMounted })}>
|
||||
<div className={clsx("text-center text-xs", { invisible: !isHydrated })}>
|
||||
{t("tournament:pre.checkIn.range", {
|
||||
start: checkInStartsString,
|
||||
finish: checkInEndsString,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { DANGEROUS_CAN_ACCESS_DEV_CONTROLS } from "~/features/admin/core/dev-con
|
|||
import { useUser } from "~/features/auth/core/user";
|
||||
import { useChatContext } from "~/features/chat/useChatContext";
|
||||
import { Tournament } from "~/features/tournament-bracket/core/Tournament";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { removeMarkdown } from "~/utils/strings";
|
||||
import {
|
||||
|
|
@ -83,12 +83,12 @@ export const handle: SendouRouteHandle = {
|
|||
const TournamentContext = React.createContext<Tournament>(null!);
|
||||
|
||||
export default function TournamentLayoutShell() {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
// tournaments are something that people like to refresh a lot
|
||||
// which can cause spikes that are hard for the server to handle
|
||||
// this is just making sure the SSR for this page is as fast as possible in prod
|
||||
if (!isMounted)
|
||||
if (!isHydrated)
|
||||
return (
|
||||
<Main bigger>
|
||||
<Placeholder />
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import {
|
|||
findWidgetById,
|
||||
} from "~/features/user-page/core/widgets/portfolio";
|
||||
import { USER } from "~/features/user-page/user-page-constants";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { action } from "../actions/u.$identifier.edit-widgets.server";
|
||||
import { WidgetSettingsForm } from "../components/WidgetSettingsForm";
|
||||
import { loader } from "../loaders/u.$identifier.edit-widgets.server";
|
||||
|
|
@ -41,7 +41,7 @@ export { action, loader };
|
|||
export default function EditWidgetsPage() {
|
||||
const { t } = useTranslation(["user", "common"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const fetcher = useFetcher();
|
||||
|
||||
const [selectedWidgets, setSelectedWidgets] = useState<
|
||||
|
|
@ -134,7 +134,7 @@ export default function EditWidgetsPage() {
|
|||
setExpandedWidgetId(expandedWidgetId === widgetId ? null : widgetId);
|
||||
};
|
||||
|
||||
if (!isMounted) {
|
||||
if (!isHydrated) {
|
||||
return <Placeholder />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { FormMessage } from "~/components/FormMessage";
|
|||
import { FriendCodePopover } from "~/components/FriendCodePopover";
|
||||
import { BADGE } from "~/features/badges/badges-constants";
|
||||
import { SendouForm } from "~/form/SendouForm";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useHasRole } from "~/modules/permissions/hooks";
|
||||
import { countryCodeToTranslatedName } from "~/utils/i18n";
|
||||
import invariant from "~/utils/invariant";
|
||||
|
|
@ -114,11 +114,11 @@ export default function UserEditPage() {
|
|||
|
||||
function useCountryOptions() {
|
||||
const { i18n } = useTranslation();
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
return COUNTRY_CODES.map((countryCode) => ({
|
||||
value: countryCode,
|
||||
label: isMounted
|
||||
label: isHydrated
|
||||
? countryCodeToTranslatedName({
|
||||
countryCode,
|
||||
language: i18n.language,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import type {
|
|||
SeasonTournamentResult,
|
||||
} from "~/features/sendouq-match/SQMatchRepository.server";
|
||||
import { useWeaponUsage } from "~/hooks/swr";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import { modesShort } from "~/modules/in-game-lists/modes";
|
||||
import { stageIds } from "~/modules/in-game-lists/stage-ids";
|
||||
|
|
@ -206,7 +206,7 @@ function SeasonHeader({
|
|||
}) {
|
||||
const { t } = useTranslation(["user"]);
|
||||
const { formatDate } = useTimeFormat();
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const { starts, ends } = Seasons.nthToDateRange(seasonViewed);
|
||||
const navigate = useNavigate();
|
||||
const options = useSeasonSelectOptions();
|
||||
|
|
@ -237,9 +237,11 @@ function SeasonHeader({
|
|||
)}
|
||||
</SendouSelect>
|
||||
<div
|
||||
className={clsx("text-sm text-lighter mt-2", { invisible: !isMounted })}
|
||||
className={clsx("text-sm text-lighter mt-2", {
|
||||
invisible: !isHydrated,
|
||||
})}
|
||||
>
|
||||
{isMounted ? (
|
||||
{isHydrated ? (
|
||||
<>
|
||||
{formatDate(new Date(starts), {
|
||||
day: "numeric",
|
||||
|
|
@ -735,7 +737,7 @@ function Results({
|
|||
seasonViewed: number;
|
||||
results: UserSeasonsPageLoaderData["results"];
|
||||
}) {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const { formatDate } = useTimeFormat();
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
|
@ -768,11 +770,11 @@ function Results({
|
|||
className={clsx(
|
||||
"text-xs font-semi-bold text-theme-secondary",
|
||||
{
|
||||
invisible: !isMounted || !shouldRenderDateHeader,
|
||||
invisible: !isHydrated || !shouldRenderDateHeader,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{isMounted
|
||||
{isHydrated
|
||||
? formatDate(databaseTimestampToDate(result.createdAt), {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { Image, WeaponImage } from "~/components/Image";
|
|||
import { Main } from "~/components/Main";
|
||||
import { YouTubeEmbed } from "~/components/YouTubeEmbed";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useSearchParamState } from "~/hooks/useSearchParamState";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import { shortStageName } from "~/modules/in-game-lists/stage-ids";
|
||||
|
|
@ -80,7 +80,7 @@ export default function VodPage() {
|
|||
defaultValue: 0,
|
||||
revive: Number,
|
||||
});
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
const [autoplay, setAutoplay] = React.useState(false);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const { t } = useTranslation(["common", "vods"]);
|
||||
|
|
@ -102,10 +102,10 @@ export default function VodPage() {
|
|||
<PovUser pov={data.vod.pov} />
|
||||
<time
|
||||
className={clsx("text-lighter text-xs", {
|
||||
invisible: !isMounted,
|
||||
invisible: !isHydrated,
|
||||
})}
|
||||
>
|
||||
{isMounted
|
||||
{isHydrated
|
||||
? formatDate(databaseTimestampToDate(data.vod.youtubeDate), {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
|
|
|
|||
32
app/hooks/useHydrated.ts
Normal file
32
app/hooks/useHydrated.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import * as React from "react";
|
||||
|
||||
// credits: https://github.com/sergiodxa/remix-utils/blob/main/src/react/use-hydrated.ts
|
||||
|
||||
function subscribe() {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a boolean indicating if the JS has been hydrated already.
|
||||
* When doing Server-Side Rendering, the result will always be false.
|
||||
* When doing Client-Side Rendering, the result will always be false on the
|
||||
* first render and true from then on. Even if a new component renders it will
|
||||
* always start with true.
|
||||
*
|
||||
* Example: Disable a button that needs JS to work.
|
||||
* ```tsx
|
||||
* let hydrated = useHydrated();
|
||||
* return (
|
||||
* <button type="button" disabled={!hydrated} onClick={doSomethingCustom}>
|
||||
* Click me
|
||||
* </button>
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function useHydrated() {
|
||||
return React.useSyncExternalStore(
|
||||
subscribe,
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import * as React from "react";
|
||||
|
||||
export function useIsMounted() {
|
||||
const [isMounted, setIsMounted] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
return isMounted;
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ import {
|
|||
useTheme,
|
||||
} from "./features/theme/core/provider";
|
||||
import { getThemeSession } from "./features/theme/core/theme-session.server";
|
||||
import { useIsMounted } from "./hooks/useIsMounted";
|
||||
import { useHydrated } from "./hooks/useHydrated";
|
||||
import { DEFAULT_LANGUAGE } from "./modules/i18n/config";
|
||||
import { i18nCookie, i18next } from "./modules/i18n/i18next.server";
|
||||
import { IS_E2E_TEST_RUN } from "./utils/e2e";
|
||||
|
|
@ -416,9 +416,9 @@ export const ErrorBoundary = () => {
|
|||
};
|
||||
|
||||
function HydrationTestIndicator() {
|
||||
const isMounted = useIsMounted();
|
||||
const isHydrated = useHydrated();
|
||||
|
||||
if (!isMounted) return null;
|
||||
if (!isHydrated) return null;
|
||||
|
||||
return <div style={{ display: "none" }} data-testid="hydrated" />;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user