fix: react hydration issues by using the same locale during ssr and hydration
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
This commit is contained in:
parent
ca38f7ca69
commit
63e12868b1
12 changed files with 65 additions and 18 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { Card, CardContent } from "~/client/components/ui/card";
|
||||
import { ByteSize } from "~/client/components/bytes-size";
|
||||
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
|
||||
import type { ResticSnapshotSummaryDto } from "@zerobyte/core/restic";
|
||||
import { formatDuration } from "~/utils/utils";
|
||||
|
||||
|
|
@ -7,8 +8,6 @@ type Props = {
|
|||
summary?: ResticSnapshotSummaryDto | null;
|
||||
};
|
||||
|
||||
const formatCount = (value: number) => value.toLocaleString();
|
||||
|
||||
const getDurationLabel = (start: string, end: string) => {
|
||||
const startMs = new Date(start).getTime();
|
||||
const endMs = new Date(end).getTime();
|
||||
|
|
@ -17,8 +16,11 @@ const getDurationLabel = (start: string, end: string) => {
|
|||
};
|
||||
|
||||
export const BackupSummaryCard = ({ summary }: Props) => {
|
||||
const { locale } = useRootLoaderData();
|
||||
|
||||
if (!summary) return null;
|
||||
|
||||
const formatCount = (value: number) => value.toLocaleString(locale);
|
||||
const durationLabel = getDurationLabel(summary.backup_start, summary.backup_end);
|
||||
|
||||
const topStats = [
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type React from "react";
|
||||
import { formatBytes } from "~/utils/format-bytes";
|
||||
import { useFormatBytes } from "~/client/hooks/use-format-bytes";
|
||||
|
||||
type ByteSizeProps = {
|
||||
bytes: number;
|
||||
|
|
@ -14,6 +14,7 @@ type ByteSizeProps = {
|
|||
};
|
||||
|
||||
export function ByteSize(props: ByteSizeProps) {
|
||||
const formatBytes = useFormatBytes();
|
||||
const {
|
||||
bytes,
|
||||
base = 1000,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { ByteSize } from "~/client/components/bytes-size";
|
||||
import { useFormatBytes } from "~/client/hooks/use-format-bytes";
|
||||
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
|
||||
import { Card } from "~/client/components/ui/card";
|
||||
import { Progress } from "~/client/components/ui/progress";
|
||||
import { type RestoreProgressEvent, useServerEvents } from "~/client/hooks/use-server-events";
|
||||
import { formatBytes } from "~/utils/format-bytes";
|
||||
import { formatDuration } from "~/utils/utils";
|
||||
|
||||
type Props = {
|
||||
|
|
@ -12,6 +13,8 @@ type Props = {
|
|||
};
|
||||
|
||||
export const RestoreProgress = ({ repositoryId, snapshotId }: Props) => {
|
||||
const formatBytes = useFormatBytes();
|
||||
const { locale } = useRootLoaderData();
|
||||
const { addEventListener } = useServerEvents();
|
||||
const [progress, setProgress] = useState<RestoreProgressEvent | null>(null);
|
||||
|
||||
|
|
@ -71,9 +74,9 @@ export const RestoreProgress = ({ repositoryId, snapshotId }: Props) => {
|
|||
<div>
|
||||
<p className="text-xs uppercase text-muted-foreground">Files</p>
|
||||
<p className="font-medium">
|
||||
{progress.files_restored.toLocaleString()}
|
||||
{progress.files_restored.toLocaleString(locale)}
|
||||
/
|
||||
{progress.total_files.toLocaleString()}
|
||||
{progress.total_files.toLocaleString(locale)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
|
||||
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
|
||||
import { cn } from "~/client/lib/utils";
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
|
|
@ -116,6 +117,7 @@ function ChartTooltipContent({
|
|||
labelKey?: string;
|
||||
}) {
|
||||
const { config } = useChart();
|
||||
const { locale } = useRootLoaderData();
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
|
|
@ -205,7 +207,7 @@ function ChartTooltipContent({
|
|||
</div>
|
||||
{item.value && (
|
||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||
{item.value.toLocaleString()}
|
||||
{item.value.toLocaleString(locale)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -565,10 +565,16 @@ function SidebarMenuSkeleton({
|
|||
}: React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean;
|
||||
}) {
|
||||
// Random width between 50 to 90%.
|
||||
const skeletonId = React.useId();
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, []);
|
||||
let hash = 0;
|
||||
|
||||
for (const char of skeletonId) {
|
||||
hash = (hash * 31 + char.charCodeAt(0)) % 40;
|
||||
}
|
||||
|
||||
return `${hash + 50}%`;
|
||||
}, [skeletonId]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
12
app/client/hooks/use-format-bytes.ts
Normal file
12
app/client/hooks/use-format-bytes.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
|
||||
import { formatBytes, type FormatBytesOptions } from "~/utils/format-bytes";
|
||||
|
||||
export const useFormatBytes = () => {
|
||||
const { locale } = useRootLoaderData();
|
||||
|
||||
return (bytes: number, options?: FormatBytesOptions) =>
|
||||
formatBytes(bytes, {
|
||||
...options,
|
||||
locale: options?.locale ?? locale,
|
||||
});
|
||||
};
|
||||
3
app/client/hooks/use-root-loader-data.ts
Normal file
3
app/client/hooks/use-root-loader-data.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { Route as RootRoute } from "~/routes/__root";
|
||||
|
||||
export const useRootLoaderData = () => RootRoute.useLoaderData();
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { formatDistanceToNow, isValid } from "date-fns";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Route as RootRoute } from "~/routes/__root";
|
||||
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
|
||||
|
||||
export type DateInput = Date | string | number | null | undefined;
|
||||
|
||||
|
|
@ -201,7 +201,7 @@ export function formatTimeAgo(date: DateInput, now = Date.now()): string {
|
|||
}
|
||||
|
||||
export function useTimeFormat() {
|
||||
const { locale, timeZone, dateFormat, timeFormat, now } = RootRoute.useLoaderData();
|
||||
const { locale, timeZone, dateFormat, timeFormat, now } = useRootLoaderData();
|
||||
const [currentNow, setCurrentNow] = useState(now);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ByteSize } from "~/client/components/bytes-size";
|
||||
import { useFormatBytes } from "~/client/hooks/use-format-bytes";
|
||||
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
|
||||
import { Card } from "~/client/components/ui/card";
|
||||
import { Progress } from "~/client/components/ui/progress";
|
||||
import { getBackupProgressOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import type { GetBackupProgressResponse } from "~/client/api-client/types.gen";
|
||||
import { formatDuration } from "~/utils/utils";
|
||||
import { formatBytes } from "~/utils/format-bytes";
|
||||
|
||||
type Props = {
|
||||
scheduleShortId: string;
|
||||
|
|
@ -13,6 +14,8 @@ type Props = {
|
|||
};
|
||||
|
||||
export const BackupProgressCard = ({ scheduleShortId, initialProgress }: Props) => {
|
||||
const formatBytes = useFormatBytes();
|
||||
const { locale } = useRootLoaderData();
|
||||
const { data: progress } = useQuery({
|
||||
...getBackupProgressOptions({ path: { shortId: scheduleShortId } }),
|
||||
initialData: initialProgress,
|
||||
|
|
@ -52,7 +55,7 @@ export const BackupProgressCard = ({ scheduleShortId, initialProgress }: Props)
|
|||
<p className="font-medium">
|
||||
{progress ? (
|
||||
<>
|
||||
{files_done.toLocaleString()} / {total_files.toLocaleString()}
|
||||
{files_done.toLocaleString(locale)} / {total_files.toLocaleString(locale)}
|
||||
</>
|
||||
) : (
|
||||
"—"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import type { GetRepositoryStatsResponse } from "~/client/api-client/types.gen";
|
||||
import { ByteSize } from "~/client/components/bytes-size";
|
||||
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
import { Card, CardContent, CardTitle } from "~/client/components/ui/card";
|
||||
import { parseError } from "~/client/lib/errors";
|
||||
|
|
@ -26,6 +27,7 @@ const toSafeNumber = (value: number | undefined) => {
|
|||
};
|
||||
|
||||
export function CompressionStatsChart({ repositoryShortId, initialStats }: Props) {
|
||||
const { locale } = useRootLoaderData();
|
||||
const refreshStats = useMutation({
|
||||
...refreshRepositoryStatsMutation(),
|
||||
onSuccess: () => {
|
||||
|
|
@ -133,7 +135,9 @@ export function CompressionStatsChart({ repositoryShortId, initialStats }: Props
|
|||
<div className="flex flex-col gap-1.5 lg:col-span-2">
|
||||
<div className="text-sm font-medium text-muted-foreground">Snapshots</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-xl font-semibold text-foreground font-mono">{snapshotsCount.toLocaleString()}</span>
|
||||
<span className="text-xl font-semibold text-foreground font-mono">
|
||||
{snapshotsCount.toLocaleString(locale)}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{compressionProgressPercent.toFixed(1)}% compressed
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { Input } from "~/client/components/ui/input";
|
|||
import { Label } from "~/client/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
|
||||
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
|
||||
import { authClient } from "~/client/lib/auth-client";
|
||||
import {
|
||||
DATE_FORMATS,
|
||||
|
|
@ -29,7 +30,6 @@ import {
|
|||
} from "~/client/lib/datetime";
|
||||
import { logger } from "~/client/lib/logger";
|
||||
import { type AppContext } from "~/context";
|
||||
import { Route as RootRoute } from "~/routes/__root";
|
||||
import { TwoFactorSection } from "../components/two-factor-section";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { SsoSettingsSection } from "~/client/modules/sso/components/sso-settings-section";
|
||||
|
|
@ -50,7 +50,7 @@ export function SettingsPage({ appContext, initialMembers, initialSsoSettings, i
|
|||
const [downloadDialogOpen, setDownloadDialogOpen] = useState(false);
|
||||
const [downloadPassword, setDownloadPassword] = useState("");
|
||||
const [isChangingPassword, setIsChangingPassword] = useState(false);
|
||||
const { locale, dateFormat, timeFormat } = RootRoute.useLoaderData();
|
||||
const { locale, dateFormat, timeFormat } = useRootLoaderData();
|
||||
|
||||
const { tab } = useSearch({ from: "/(dashboard)/settings/" });
|
||||
const activeTab = tab || "account";
|
||||
|
|
|
|||
|
|
@ -1,9 +1,20 @@
|
|||
import "./setup.ts";
|
||||
import { GlobalRegistrator } from "@happy-dom/global-registrator";
|
||||
import { afterAll, afterEach, beforeAll } from "bun:test";
|
||||
import { afterAll, afterEach, beforeAll, mock } from "bun:test";
|
||||
import { client } from "~/client/api-client/client.gen";
|
||||
import { server } from "~/test/msw/server";
|
||||
|
||||
void mock.module("~/client/hooks/use-root-loader-data", () => ({
|
||||
useRootLoaderData: () => ({
|
||||
theme: "dark",
|
||||
locale: "en-US",
|
||||
timeZone: "UTC",
|
||||
dateFormat: "MM/DD/YYYY",
|
||||
timeFormat: "12h",
|
||||
now: Date.now(),
|
||||
}),
|
||||
}));
|
||||
|
||||
GlobalRegistrator.register({ url: "http://localhost:3000" });
|
||||
|
||||
client.setConfig({
|
||||
|
|
|
|||
Loading…
Reference in a new issue