Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/docs/src/app/[locale]/(docs)/docs/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"use client";

import {
RouteErrorView,
type RouteErrorViewProps,
} from "@vitnode/core/views/error/route-error-view";

export default function ErrorBoundary({ error, retry }: RouteErrorViewProps) {
return <RouteErrorView error={error} retry={retry} />;
}
3 changes: 2 additions & 1 deletion apps/docs/src/app/[locale]/(main)/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ChevronRight } from "lucide-react";
import { Suspense } from "react";

import { AnimatedBeamHome } from "../../../../components/animated-beam/animated-beam-home";
import { AnimatedBeamHomeSkeleton } from "../../../../components/animated-beam/animated-beam-home-skeleton";
import { AdminSection } from "./sections/admin/admin";
import { CallToActionSection } from "./sections/call-to-action";
import { PoweringBySection } from "./sections/powering-by/powering-by";
Expand Down Expand Up @@ -78,7 +79,7 @@ export default function HomePage() {

<PoweringBySection />
<AdminSection />
<Suspense fallback={null}>
<Suspense fallback={<AnimatedBeamHomeSkeleton />}>
<AnimatedBeamHome />
</Suspense>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Loader } from "@vitnode/core/components/ui/loader";

export default function Loading() {
return (
<div className="container mx-auto flex items-center justify-center p-4">
<Loader />
<span className="sr-only">Loading</span>
</div>
);
}
10 changes: 10 additions & 0 deletions apps/docs/src/app/[locale]/(main)/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"use client";

import {
RouteErrorView,
type RouteErrorViewProps,
} from "@vitnode/core/views/error/route-error-view";

export default function ErrorBoundary({ error, retry }: RouteErrorViewProps) {
return <RouteErrorView error={error} retry={retry} />;
}
10 changes: 10 additions & 0 deletions apps/docs/src/app/[locale]/admin/(auth)/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"use client";

import {
RouteErrorView,
type RouteErrorViewProps,
} from "@vitnode/core/views/error/route-error-view";

export default function ErrorBoundary({ error, retry }: RouteErrorViewProps) {
return <RouteErrorView error={error} retry={retry} />;
}
10 changes: 10 additions & 0 deletions apps/docs/src/app/[locale]/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"use client";

import {
RouteErrorView,
type RouteErrorViewProps,
} from "@vitnode/core/views/error/route-error-view";

export default function ErrorBoundary({ error, retry }: RouteErrorViewProps) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the reset callback supplied by Next.js

When any new route boundary, modified global boundary, or generated-app boundary renders, Next.js supplies a reset callback rather than retry; destructuring retry therefore passes undefined into the view, and clicking “Try again” throws instead of rerendering the failed segment. Accept reset and pass it through, optionally renaming it during destructuring.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Declare the new entrypoints as arrow components

This and the other newly added ErrorBoundary, GlobalError, and Loading entrypoints use function declarations, contrary to the repository-wide requirement that React components use arrow functions. Convert these entrypoint components to arrow declarations and export them as defaults.

AGENTS.md reference: AGENTS.md:L7-L7

Useful? React with 👍 / 👎.

return <RouteErrorView error={error} retry={retry} />;
}
16 changes: 14 additions & 2 deletions apps/docs/src/app/global-error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ const geist = Geist({
subsets: ["latin"],
});

export default function GlobalError() {
return <GlobalErrorView className={`${geist.className} antialiased`} />;
export default function GlobalError({
error,
retry,
}: {
error: Error & { digest?: string };
retry: () => void;
}) {
return (
<GlobalErrorView
className={`${geist.className} antialiased`}
error={error}
retry={retry}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Skeleton } from "@vitnode/core/components/ui/skeleton";

const Row = ({ center }: { center?: boolean }) => (
<div className="flex flex-row items-center justify-between">
<Skeleton className="size-12" />
<Skeleton className={center ? "size-16" : "size-12"} />
<Skeleton className="size-12" />
</div>
);

export const AnimatedBeamHomeSkeleton = () => (
<div
aria-busy="true"
className="relative flex w-full items-center justify-center overflow-hidden p-4 sm:max-w-md"
>
<div className="flex size-full max-w-lg flex-col items-stretch justify-between gap-10">
<Row />
<Row center />
<Row />
</div>

<span className="sr-only">Loading</span>
</div>
);
6 changes: 2 additions & 4 deletions apps/docs/src/locales/@vitnode/core/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@
"desc": "Przepraszamy, występują problemy techniczne po stronie serwera."
},
"title": "Ups! Coś poszło nie tak.",
"try_again": "Spróbuj ponownie",
"reference": "Identyfikator błędu: {digest}",
"internal_server_error": "Wewnętrzny błąd serwera.",
"field_required": "To pole jest wymagane.",
"field_min_length": "To pole musi mieć co najmniej {min} znaków.",
Expand Down Expand Up @@ -409,8 +411,6 @@
"drop_here": "Upuść tutaj widżet",
"empty_title": "Twój pulpit jest pusty",
"empty_desc": "Dodaj widżety, aby mieć ważne dane zawsze pod ręką.",
"saved_title": "Pulpit zapisany",
"saved_desc": "Ten układ należy tylko do Ciebie - inni administratorzy mają swój własny.",
"error_title": "Nie udało się zapisać pulpitu",
"error_desc": "Coś poszło nie tak po drodze do serwera. Spróbuj ponownie.",
"refresh_error": "Nie udało się odświeżyć tego widżetu. Odśwież stronę, aby zobaczyć zmiany.",
Expand All @@ -433,8 +433,6 @@
"open": "Skonfiguruj {title}",
"title": "Skonfiguruj {title}",
"desc": "Te ustawienia dotyczą tylko tej karty - inni administratorzy mają swoje własne.",
"saved_title": "Ustawienia zapisane",
"saved_desc": "Karta użyje ich, gdy skończysz układać pulpit.",
"error_title": "Nie udało się zapisać ustawień",
"error_desc": "Coś poszło nie tak po drodze do serwera. Spróbuj ponownie.",
"load_error": "Nie udało się wczytać tych ustawień. Zamknij i spróbuj ponownie."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ const geist = Geist({
subsets: ["latin"],
});

export default function GlobalError() {
return <GlobalErrorView className={`${geist.className} antialiased`} />;
export default function GlobalError({
error,
retry,
}: {
error: Error & { digest?: string };
retry: () => void;
}) {
return (
<GlobalErrorView
className={`${geist.className} antialiased`}
error={error}
retry={retry}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ const geist = Geist({
subsets: ["latin"],
});

export default function GlobalError() {
return <GlobalErrorView className={`${geist.className} antialiased`} />;
export default function GlobalError({
error,
retry,
}: {
error: Error & { digest?: string };
retry: () => void;
}) {
return (
<GlobalErrorView
className={`${geist.className} antialiased`}
error={error}
retry={retry}
/>
);
}
6 changes: 2 additions & 4 deletions packages/vitnode/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@
"desc": "Sorry, we're experiencing technical difficulties on our server."
},
"title": "Oops! Something went wrong.",
"try_again": "Try again",
"reference": "Error reference: {digest}",
"internal_server_error": "Internal server error.",
"field_required": "This field is required.",
"field_min_length": "This field must be at least {min} characters.",
Expand Down Expand Up @@ -398,8 +400,6 @@
"drop_here": "Drop a widget here",
"empty_title": "Your dashboard is empty",
"empty_desc": "Add widgets to keep the numbers you care about one glance away.",
"saved_title": "Dashboard saved",
"saved_desc": "Your layout is yours alone - other admins keep theirs.",
"error_title": "Could not save your dashboard",
"error_desc": "Something went wrong on the way to the server. Please try again.",
"refresh_error": "Could not reload this widget. Reload the page to see your changes.",
Expand All @@ -422,8 +422,6 @@
"open": "Configure {title}",
"title": "Configure {title}",
"desc": "These settings apply to this card only - other admins keep their own.",
"saved_title": "Settings saved",
"saved_desc": "The card picks them up once you are done arranging the board.",
"error_title": "Could not save the settings",
"error_desc": "Something went wrong on the way to the server. Please try again.",
"load_error": "Could not load these settings. Close this and try again."
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Loader } from "@/components/ui/loader";

export default function Loading() {
return (
<div className="container mx-auto flex items-center justify-center p-4">
<Loader />
<span className="sr-only">Loading</span>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,6 @@ export const DashboardBoardProvider = ({
}

setIsEditing(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore confirmation after saving the dashboard

After a successful layout mutation, this path now exits editing and refreshes without confirming that the update was saved. Restore a sonner success toast with a description so the completed edit provides the repository-required feedback.

AGENTS.md reference: AGENTS.md:L11-L11

Useful? React with 👍 / 👎.

toast.success(t("saved_title"), { description: t("saved_desc") });
router.refresh();
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,14 +121,6 @@ export const WidgetSettingsDialog = ({
}

setOpen(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore confirmation after saving widget settings

When widget settings save successfully, the dialog now closes and schedules the card refresh without showing any confirmation; users cannot distinguish a completed save from a dismissal until they inspect the widget. Restore the removed sonner success toast and its description.

AGENTS.md reference: AGENTS.md:L11-L11

Useful? React with 👍 / 👎.

toast.success(t("settings.saved_title"), {
description: t("settings.saved_desc"),
});

// Left until the dialog has finished closing. The card suspends
// while it is re-rendered, and a suspended render mid-animation
// strands the overlay on screen - batched into this transition it
// would also hold the close back until the new card was ready.
setTimeout(onSaved, 300);
} finally {
resolve();
Expand Down
63 changes: 57 additions & 6 deletions packages/vitnode/src/views/error/global-error-view.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
"use client";

import type { Metadata } from "next/dist/types";

import { HomeIcon, RefreshCwIcon } from "lucide-react";
// eslint-disable-next-line no-restricted-imports
import Link from "next/link";
import { useTransition } from "react";

import { LogoVitNode } from "@/components/logo-vitnode";
import { ThemeProvider } from "@/components/theme-provider";
import { Button, buttonVariants } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";

export const metadata: Metadata = {
title: "Error 500!",
};

export const GlobalErrorView = ({ className }: { className?: string }) => {
export interface GlobalErrorViewProps {
className?: string;
error: Error & { digest?: string };
retry: () => void;
}

export const GlobalErrorView = ({
className,
error,
retry,
}: GlobalErrorViewProps) => {
const [isRetrying, startRetry] = useTransition();

return (
<html lang="en" suppressHydrationWarning>
<body className={className}>
Expand All @@ -22,16 +40,49 @@ export const GlobalErrorView = ({ className }: { className?: string }) => {
<LogoVitNode className="mb-4 h-12 w-auto" />
</Link>

<h1 className="text-3xl font-semibold tracking-tight">
<h1 className="text-3xl font-semibold tracking-tight text-balance">
Oops! Something went wrong.
</h1>

<Card className="w-full">
<CardContent>
<p className="text-muted-foreground">
An unexpected error occurred. Please try refreshing the page
or come back later.
<CardContent className="flex flex-col gap-4">
<p className="text-muted-foreground leading-relaxed text-pretty">
An unexpected error occurred. Try again, and if it keeps
happening come back a little later.
</p>

{error.digest ? (
<p className="text-muted-foreground/70 font-mono text-xs">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep error reference text at least 14px

The error digest is rendered with Tailwind's text-xs size, which is normally 12px; the same class is used by the route error view. These references are user-facing text that may need to be read or relayed to support, so use at least text-sm to satisfy the repository's 14px minimum.

AGENTS.md reference: AGENTS.md:L50-L50

Useful? React with 👍 / 👎.

Error reference: {error.digest}
</p>
) : null}

<div className="flex flex-col items-center justify-center gap-3 sm:flex-row">
<Button
isLoading={isRetrying}
onClick={() => {
// `retry` runs its own transition; wrapping it keeps
// the button in a pending state until it settles.
startRetry(() => {
retry();
});
}}
size="lg"
>
<RefreshCwIcon />
Try again
</Button>

<Link
className={cn(
buttonVariants({ size: "lg", variant: "ghost" }),
)}
href="/"
>
<HomeIcon />
Back to home
</Link>
</div>
</CardContent>
</Card>
</div>
Expand Down
Loading
Loading