Skip to content
Closed
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
5 changes: 5 additions & 0 deletions apps/web/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.output
.nitro
.tanstack
.vite
*.tsbuildinfo
22 changes: 22 additions & 0 deletions apps/web/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import eslintVitNode from "@vitnode/config/eslint";
import eslintVitNodeReact from "@vitnode/config/eslint.react";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));

export default [
...eslintVitNode,
...eslintVitNodeReact,
{
ignores: ["src/routeTree.gen.ts", ".output", ".nitro", ".tanstack"],
},
{
languageOptions: {
parserOptions: {
project: "./tsconfig.json",
tsconfigRootDir: __dirname,
},
},
},
];
37 changes: 37 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "web",
"version": "1.2.0-canary.78",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"start": "node .output/server/index.mjs",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@fontsource-variable/geist": "^5.3.0",
"@tanstack/react-query": "^5.101.4",
"@tanstack/react-router": "1.170.31",
"@tanstack/react-start": "1.168.48",
"@vitnode/i18n": "workspace:*",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"use-intl": "^4.13.7"
},
"devDependencies": {
"@tailwindcss/vite": "^4.3.3",
"@types/node": "^26.1.1",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "6.0.4",
"@vitnode/config": "workspace:*",
"eslint": "^10.7.0",
"nitro": "^3.0.260610-beta",
"tailwindcss": "^4.3.3",
"typescript": "^6.0.3",
"vite": "8.1.5"
}
}
87 changes: 87 additions & 0 deletions apps/web/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* eslint-disable */

// @ts-nocheck

// noinspection JSUnusedGlobalSymbols

// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.

import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as AdminRouteImport } from './routes/admin'

const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const AdminRoute = AdminRouteImport.update({
id: '/admin',
path: '/admin',
getParentRoute: () => rootRouteImport,
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/admin': typeof AdminRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/admin': typeof AdminRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/admin': typeof AdminRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/admin'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/admin'
id: '__root__' | '/' | '/admin'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AdminRoute: typeof AdminRoute
}

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/admin': {
id: '/admin'
path: '/admin'
fullPath: '/admin'
preLoaderRoute: typeof AdminRouteImport
parentRoute: typeof rootRouteImport
}
}
}

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AdminRoute: AdminRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

import type { getRouter } from './router.tsx'
import type { startInstance } from './start.ts'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
config: Awaited<ReturnType<typeof startInstance.getOptions>>
}
}
47 changes: 47 additions & 0 deletions apps/web/src/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRouter } from "@tanstack/react-router";
import { deLocalizeUrl, localizeUrl } from "@vitnode/i18n/client";
import { useTranslations } from "use-intl";

import { routeTree } from "./routeTree.gen";

export interface RouterAppContext {
queryClient: QueryClient;
}

const NotFound = () => {
const t = useTranslations("notFound");

return (
<main className="flex min-h-dvh flex-col items-center justify-center p-6">
<h1 className="text-3xl font-semibold text-balance">{t("title")}</h1>
</main>
);
};

export const getRouter = () => {
const queryClient = new QueryClient();

return createRouter({
routeTree,
context: { queryClient } satisfies RouterAppContext,
scrollRestoration: true,
defaultPreload: "intent",
defaultNotFoundComponent: NotFound,
// The locale lives only in the public URL: the router matches, and app code
// links against, paths with no locale segment at all.
rewrite: {
input: ({ url }) => deLocalizeUrl(url),
output: ({ url }) => localizeUrl(url),
},
Wrap: ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
),
});
};

declare module "@tanstack/react-router" {
interface Register {
router: ReturnType<typeof getRouter>;
}
}
57 changes: 57 additions & 0 deletions apps/web/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { QueryClient } from "@tanstack/react-query";

import {
createRootRouteWithContext,
HeadContent,
Scripts,
} from "@tanstack/react-router";
import {
getCurrentLocale,
I18nProvider,
messagesQueryOptions,
} from "@vitnode/i18n/client";

import appCss from "../styles/globals.css?url";

export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()(
{
// The locale comes from the request, not from a route param, so the loader
// is what resolves it - and returning the messages hands them to the client
// through the router's own SSR payload, with nothing to re-fetch on hydrate.
loader: async ({ context }) => {
const locale = getCurrentLocale();

return {
locale,
messages: await context.queryClient.ensureQueryData(
messagesQueryOptions(locale),
),
};
},
head: () => ({
meta: [
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{ title: "VitNode" },
],
links: [{ rel: "stylesheet", href: appCss }],
}),
shellComponent: RootDocument,
},
);

function RootDocument({ children }: { children: React.ReactNode }) {
const initial = Route.useLoaderData();

return (
<html lang={initial.locale}>
<head>
<HeadContent />
</head>
<body className="antialiased">
<I18nProvider initial={initial}>{children}</I18nProvider>
<Scripts />
</body>
</html>
);
}
23 changes: 23 additions & 0 deletions apps/web/src/routes/admin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { useTranslations } from "use-intl";

export const Route = createFileRoute("/admin")({
component: Admin,
});

function Admin() {
const t = useTranslations("admin");

return (
<main className="flex min-h-dvh flex-col items-center justify-center gap-4 p-6">
<h1 className="text-3xl font-semibold text-balance">{t("title")}</h1>
<p className="text-muted-foreground leading-relaxed text-pretty">
{t("description")}
</p>
{/* Never localized, so a plain Link rather than a LocalizedLink. */}
<Link className="text-primary underline" to="/">
{t("title")}
</Link>
</main>
);
}
31 changes: 31 additions & 0 deletions apps/web/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { LocaleSwitcher, LocalizedLink } from "@vitnode/i18n/client";
import { useTranslations } from "use-intl";

export const Route = createFileRoute("/")({
component: Home,
});

function Home() {
const t = useTranslations("home");

return (
<main className="flex min-h-dvh flex-col items-center justify-center gap-4 p-6">
<h1 className="text-3xl font-semibold text-balance">{t("title")}</h1>
<p className="text-muted-foreground leading-relaxed text-pretty">
{t("description")}
</p>
<LocaleSwitcher />
<div className="flex items-center gap-4">
{/* Localized: picks up the current locale's prefix. */}
<LocalizedLink className="text-primary underline" to="/">
{t("title")}
</LocalizedLink>
{/* Never localized, so a plain Link. */}
<Link className="text-primary underline" to="/admin">
{t("admin")}
</Link>
</div>
</main>
);
}
6 changes: 6 additions & 0 deletions apps/web/src/start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createStart } from "@tanstack/react-start";
import { localeRequestMiddleware } from "@vitnode/i18n/server";

export const startInstance = createStart(() => ({
requestMiddleware: [localeRequestMiddleware],
}));
Loading
Loading