From fc621eb2f71fe01530c2d816283f517a169054ea Mon Sep 17 00:00:00 2001 From: aXenDeveloper Date: Wed, 30 Sep 2026 18:47:51 +0200 Subject: [PATCH 1/4] refactor: Home page for create-vitnode-app --- apps/web/content/docs/dev/configuration.mdx | 45 +- apps/web/src/components/admin-shell.tsx | 21 - apps/web/src/components/main-header.tsx | 6 - apps/web/src/routes/__root.tsx | 4 +- apps/web/src/routes/_admin.tsx | 51 +- apps/web/src/routes/_main.tsx | 2 +- apps/web/src/site/home/home-content.tsx | 2 - .../src/site/home/sections/integrations.tsx | 469 ++++++++---------- .../home/sections/logos/integration-marks.ts | 2 + .../src/site/home/sections/powering-by.tsx | 48 -- apps/web/src/site/marketing/marketing.css | 48 +- apps/web/src/vitnode.config.ts | 2 + .../root/src/components/admin-shell.tsx | 21 - .../root/src/components/main-footer.tsx | 87 ++-- .../root/src/components/main-header.tsx | 4 - .../root/src/locales/app.ts | 7 +- .../root/src/locales/app/en.json | 42 ++ .../root/src/routes/__root.tsx | 4 +- .../root/src/routes/_admin.tsx | 69 +-- .../root/src/routes/_main.tsx | 2 +- .../root/src/routes/_main/index.tsx | 87 ++-- .../copy-of-vitnode-app/root/src/styles.css | 2 + .../vitnode/src/components/logo-context.tsx | 11 + packages/vitnode/src/pages/admin/sign-in.tsx | 6 +- packages/vitnode/src/pages/login/index.tsx | 3 +- packages/vitnode/src/pages/register.tsx | 3 +- .../vitnode/src/tanstack/admin/actions.ts | 14 +- packages/vitnode/src/tanstack/admin/index.ts | 2 + .../src/tanstack/admin/layout-route.tsx | 61 +++ .../src/tanstack/admin/queries.test.ts | 4 +- packages/vitnode/src/tanstack/admin/shell.tsx | 23 +- .../src/tanstack/admin/sign-in-screen.tsx | 8 +- .../vitnode/src/tanstack/admin/user-bar.tsx | 9 +- packages/vitnode/src/tanstack/auth/actions.ts | 18 +- packages/vitnode/src/tanstack/auth/index.ts | 3 +- .../src/tanstack/auth/login-screen.tsx | 7 +- .../vitnode/src/tanstack/auth/navigation.ts | 11 - .../vitnode/src/tanstack/auth/queries.test.ts | 2 +- .../vitnode/src/tanstack/auth/redirects.ts | 22 +- .../src/tanstack/auth/register-screen.tsx | 9 +- .../vitnode/src/tanstack/layout/header.tsx | 11 +- .../src/tanstack/layout/main-header.tsx | 4 +- .../vitnode/src/views/layouts/providers.tsx | 12 +- packages/vitnode/src/vitnode.config.ts | 2 + 44 files changed, 568 insertions(+), 702 deletions(-) delete mode 100644 apps/web/src/components/admin-shell.tsx delete mode 100644 apps/web/src/components/main-header.tsx delete mode 100644 apps/web/src/site/home/sections/powering-by.tsx delete mode 100644 packages/create-vitnode-app/copy-of-vitnode-app/root/src/components/admin-shell.tsx delete mode 100644 packages/create-vitnode-app/copy-of-vitnode-app/root/src/components/main-header.tsx create mode 100644 packages/create-vitnode-app/copy-of-vitnode-app/root/src/locales/app/en.json create mode 100644 packages/vitnode/src/components/logo-context.tsx create mode 100644 packages/vitnode/src/tanstack/admin/layout-route.tsx diff --git a/apps/web/content/docs/dev/configuration.mdx b/apps/web/content/docs/dev/configuration.mdx index ff648e3d9..b93fdcf88 100644 --- a/apps/web/content/docs/dev/configuration.mdx +++ b/apps/web/content/docs/dev/configuration.mdx @@ -9,10 +9,10 @@ import { Callout } from 'fumadocs-ui/components/callout' A VitNode app has two configuration files, and the line between them is one question: **may a browser hold this?** -| File | Holds | Reached by | -| :----------------------------- | :---------------------------------------------------- | :----------------------------------------------- | -| `src/vitnode.config.ts` | locales, metadata, theme, `debug`, enabled plugin ids | the browser, the server, **and your Vite build** | -| `src/vitnode.server.config.ts` | message loaders | the server only | +| File | Holds | Reached by | +| :----------------------------- | :---------------------------------------------------------- | :----------------------------------------------- | +| `src/vitnode.config.ts` | locales, metadata, logo, theme, `debug`, enabled plugin ids | the browser, the server, **and your Vite build** | +| `src/vitnode.server.config.ts` | message loaders | the server only | --- @@ -20,6 +20,7 @@ question: **may a browser hold this?** ```ts title="apps/web/src/vitnode.config.ts" import { blogPlugin } from '@vitnode/blog/config' +import { LogoVitNodeBrand } from '@vitnode/core/components/logo-vitnode' import { buildConfig } from '@vitnode/core/vitnode.config' export const vitNodeConfig = buildConfig({ @@ -32,6 +33,7 @@ export const vitNodeConfig = buildConfig({ ], timeZone: 'UTC', }, + logo: LogoVitNodeBrand, metadata: { shortTitle: 'VitNode', title: 'VitNode', @@ -45,7 +47,7 @@ export const vitNodeConfig = buildConfig({ Three very different readers depend on this file: -1. **The document shell** renders `metadata`, `theme` and `debug` - in the +1. **The document shell** renders `metadata`, `logo`, `theme` and `debug` - in the browser as well as on the server. 2. **The locale runtime** derives routing from `i18n`. 3. **Your Vite build** loads it with `jiti` while Vite is still resolving its @@ -56,7 +58,7 @@ Three very different readers depend on this file: Because of reader 3, this file is executed at build time; because of reader 1 it is bundled for a browser. So it holds plain data and plugin *identity* - never a `() => import(...)` message loader, and never a module that reaches a - database. + database. The one exception is `logo`: a small, self-contained component. `buildConfig` keeps `locales` a tuple of literal types, so a `Locale` derived @@ -67,6 +69,37 @@ against the list right beside it: export type Locale = (typeof vitNodeConfig.i18n.locales)[number]['code'] ``` +### Logo + +`logo` is the component the site header shows in its home link. Leave it out +and you get the VitNode logo - which is lovely, but it is probably not your +brand. + +```tsx title="src/components/logo.tsx" +export const Logo = () => ( + Acme +) +``` + +```ts title="src/vitnode.config.ts" +import { Logo } from '@/components/logo' + +export const vitNodeConfig = buildConfig({ + // ... + logo: Logo, +}) +``` + +Pass the component itself, not ``. Keep it lean: your Vite build +executes this file too, so a logo that imports half your UI makes every +regeneration pass slower. + ### Enabled plugins Register a plugin with its own factory: diff --git a/apps/web/src/components/admin-shell.tsx b/apps/web/src/components/admin-shell.tsx deleted file mode 100644 index 390358cf1..000000000 --- a/apps/web/src/components/admin-shell.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { AdminShellContent } from '@vitnode/core/tanstack/admin' -import { useAppNavigate } from '@vitnode/core/tanstack/auth' -import { LanguageSwitcher } from '@vitnode/core/tanstack/layout' - -import { adminNav } from '@/admin-nav.gen' - -export const AdminShell = ({ children }: { children: React.ReactNode }) => { - const navigate = useAppNavigate() - - return ( - } - nav={adminNav} - onNavigate={(href) => { - void navigate(href) - }} - > - {children} - - ) -} diff --git a/apps/web/src/components/main-header.tsx b/apps/web/src/components/main-header.tsx deleted file mode 100644 index cd5c91a22..000000000 --- a/apps/web/src/components/main-header.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { LogoVitNodeBrand } from '@vitnode/core/components/logo-vitnode' -import { MainHeader as MainHeaderContent } from '@vitnode/core/tanstack/layout' - -export const MainHeader = () => ( - } /> -) diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 429b6eea2..0c2de90dc 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -24,7 +24,7 @@ import { vitNodeConfig } from '@/vitnode.config' import appCss from '../styles.css?url' -const { debug, editor, i18n, metadata, theme } = vitNodeConfig +const { debug, editor, i18n, logo, metadata, theme } = vitNodeConfig export interface RootRouterContext { queryClient: QueryClient @@ -67,7 +67,7 @@ export const Route = createRootRouteWithContext()({ function RootComponent() { return ( diff --git a/apps/web/src/routes/_admin.tsx b/apps/web/src/routes/_admin.tsx index c02cdfe9d..7545d9d4e 100644 --- a/apps/web/src/routes/_admin.tsx +++ b/apps/web/src/routes/_admin.tsx @@ -1,47 +1,6 @@ -import { createFileRoute, Outlet, redirect } from '@tanstack/react-router' -import { - ADMIN_ENTRY_PATH, - adminReturnToFor, - canEnterAdmin, - ensureAdminAccess, - loadAdminMessages, - preloadAdminAccess, -} from '@vitnode/core/tanstack/admin' -import { pageHead } from '@vitnode/core/tanstack/metadata' +import { createFileRoute } from '@tanstack/react-router' +import { adminLayoutRoute } from '@vitnode/core/tanstack/admin' -import { AdminShell } from '@/components/admin-shell' - -export const Route = createFileRoute('/_admin')({ - beforeLoad: async ({ context, location, preload }) => { - const access = preload - ? await preloadAdminAccess(context.queryClient) - : await ensureAdminAccess(context.queryClient) - - if (!canEnterAdmin(access)) { - // eslint-disable-next-line @typescript-eslint/only-throw-error - throw redirect({ - search: { returnTo: adminReturnToFor(location) }, - to: ADMIN_ENTRY_PATH, - } as unknown as Parameters[0]) - } - - return { adminAccess: access } - }, - - loader: async ({ context }) => { - const { adminNav } = await import('@/admin-nav.gen') - - await loadAdminMessages({ ...context, namespaces: adminNav.namespaces }) - }, - - head: () => pageHead({ robots: 'noindex, nofollow' }), - component: AdminLayout, -}) - -function AdminLayout() { - return ( - - - - ) -} +export const Route = createFileRoute('/_admin')( + adminLayoutRoute(async () => await import('@/admin-nav.gen')), +) diff --git a/apps/web/src/routes/_main.tsx b/apps/web/src/routes/_main.tsx index 228529049..1ac04b9c7 100644 --- a/apps/web/src/routes/_main.tsx +++ b/apps/web/src/routes/_main.tsx @@ -2,10 +2,10 @@ import { createFileRoute, Outlet } from '@tanstack/react-router' import { MainBreadcrumb } from '@vitnode/core/tanstack/breadcrumb' import { loadMainShell, + MainHeader, ThemeLayoutContent, } from '@vitnode/core/tanstack/layout' -import { MainHeader } from '@/components/main-header' import { SiteFooter } from '@/site/marketing/footer' const MainLayout = () => ( diff --git a/apps/web/src/site/home/home-content.tsx b/apps/web/src/site/home/home-content.tsx index c97f3bb4d..547ddddf4 100644 --- a/apps/web/src/site/home/home-content.tsx +++ b/apps/web/src/site/home/home-content.tsx @@ -11,7 +11,6 @@ import { HeroSection } from '@/site/home/sections/hero' import { IntegrationsSection } from '@/site/home/sections/integrations' import { OutcomesSection } from '@/site/home/sections/outcomes' import { PluginsSection } from '@/site/home/sections/plugins' -import { PoweringBySection } from '@/site/home/sections/powering-by' import { PricingSection } from '@/site/home/sections/pricing' import { SecuritySection } from '@/site/home/sections/security' import { ShowcaseSection } from '@/site/home/sections/showcase' @@ -22,7 +21,6 @@ export const HomeRouteContent = () => ( -
diff --git a/apps/web/src/site/home/sections/integrations.tsx b/apps/web/src/site/home/sections/integrations.tsx index 4f3f8edc6..b9d9f4224 100644 --- a/apps/web/src/site/home/sections/integrations.tsx +++ b/apps/web/src/site/home/sections/integrations.tsx @@ -1,144 +1,166 @@ import type { LucideIcon } from 'lucide-react' import { Link } from '@tanstack/react-router' -import { cn } from 'cn' import { - ArrowUpRight, - HardDrive, - Mail, - ShieldCheck, - Sparkles, -} from 'lucide-react' + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@vitnode/core/components/ui/tooltip' +import { HardDrive, Mail, ShieldCheck, Sparkles } from 'lucide-react' import { INTEGRATION_MARKS } from '@/site/home/sections/logos/integration-marks' -import { MarketingSection, SectionHeading } from '@/site/marketing/shared' +import { MarketingSection, TextLink } from '@/site/marketing/shared' type Destination = { docs: string } | { href: string } -type Integration = Destination & { - blurb?: string - category?: string - color: string - featured?: boolean - glyph?: LucideIcon - highlighted?: boolean - mark?: string - name: string - short?: string -} +type Logo = { glyph: LucideIcon } | { mark: string } + +type Integration = Destination & + Logo & { + color: string + name: string + role: string + } const FOREGROUND = 'var(--foreground)' -const INTEGRATIONS: Integration[] = [ - { - blurb: 'Full-stack React with type-safe routing and SSR.', - category: 'Front end', - color: FOREGROUND, - highlighted: true, - href: 'https://tanstack.com/start', - mark: INTEGRATION_MARKS.tanstack, - name: 'TanStack Start', - }, - { - blurb: 'A fast, typed API with OpenAPI docs served at /api/swagger.', - category: 'API', - color: '#e36002', - highlighted: true, - href: 'https://hono.dev/', - mark: INTEGRATION_MARKS.hono, - name: 'Hono', - }, - { - color: '#0e9f6e', - docs: 'dev/email/nodemailer', - glyph: Mail, - name: 'Nodemailer', - }, - { - color: '#ec7211', - docs: 'dev/storage/s3-r2', - glyph: HardDrive, - name: 'S3-compatible storage', - short: 'S3 / R2', - }, - { - color: '#00bfb3', - docs: 'dev/search-elasticsearch', - mark: INTEGRATION_MARKS.elasticsearch, - name: 'Elasticsearch', - }, - { - blurb: 'Full-text search out of the box, tuned per language.', - category: 'Search', - color: '#4169e1', - docs: 'dev/search', - featured: true, - mark: INTEGRATION_MARKS.postgresql, - name: 'PostgreSQL', - }, - { - color: '#06b6d4', - href: 'https://tailwindcss.com/', - mark: INTEGRATION_MARKS.tailwindcss, - name: 'Tailwind CSS', - }, - { - color: '#84a816', - href: 'https://orm.drizzle.team/', - mark: INTEGRATION_MARKS.drizzle, - name: 'Drizzle ORM', - short: 'Drizzle', - }, - { - color: '#f38020', - docs: 'dev/captcha/cloudflare', - mark: INTEGRATION_MARKS.cloudflare, - name: 'Cloudflare Turnstile', - short: 'Turnstile', - }, - { - color: '#4285f4', - docs: 'dev/captcha/recaptcha', - glyph: ShieldCheck, - name: 'Google reCAPTCHA', - short: 'reCAPTCHA', - }, - { - color: FOREGROUND, - docs: 'dev/ai', - glyph: Sparkles, - name: 'AI SDK', - }, - { - color: '#2496ed', - docs: 'dev/deployments/self-hosted', - mark: INTEGRATION_MARKS.docker, - name: 'Docker', - }, - { - color: '#5fa04e', - docs: 'dev/cron/node-cron', - mark: INTEGRATION_MARKS.nodejs, - name: 'node-cron', - }, +const TANSTACK_START: Integration = { + color: FOREGROUND, + href: 'https://tanstack.com/start', + mark: INTEGRATION_MARKS.tanstack, + name: 'TanStack Start', + role: 'Front end', +} + +const HONO: Integration = { + color: '#e36002', + href: 'https://hono.dev/', + mark: INTEGRATION_MARKS.hono, + name: 'Hono', + role: 'API', +} + +const REACT: Integration = { + color: '#149eca', + href: 'https://react.dev/', + mark: INTEGRATION_MARKS.react, + name: 'React', + role: 'UI', +} + +const DRIZZLE: Integration = { + color: '#84a816', + href: 'https://orm.drizzle.team/', + mark: INTEGRATION_MARKS.drizzle, + name: 'Drizzle', + role: 'ORM', +} + +const POSTGRESQL: Integration = { + color: '#4169e1', + docs: 'dev/search', + mark: INTEGRATION_MARKS.postgresql, + name: 'PostgreSQL', + role: 'Database and search', +} + +const TAILWIND: Integration = { + color: '#06b6d4', + href: 'https://tailwindcss.com/', + mark: INTEGRATION_MARKS.tailwindcss, + name: 'Tailwind CSS', + role: 'Styling', +} + +const NODEMAILER: Integration = { + color: '#0e9f6e', + docs: 'dev/email/nodemailer', + glyph: Mail, + name: 'Nodemailer', + role: 'Email', +} + +const S3: Integration = { + color: '#ec7211', + docs: 'dev/storage/s3-r2', + glyph: HardDrive, + name: 'S3 / R2', + role: 'Storage', +} + +const ELASTICSEARCH: Integration = { + color: '#00bfb3', + docs: 'dev/search-elasticsearch', + mark: INTEGRATION_MARKS.elasticsearch, + name: 'Elasticsearch', + role: 'Search', +} + +const AI_SDK: Integration = { + color: FOREGROUND, + docs: 'dev/ai', + glyph: Sparkles, + name: 'AI SDK', + role: 'AI models', +} + +const TURNSTILE: Integration = { + color: '#f38020', + docs: 'dev/captcha/cloudflare', + mark: INTEGRATION_MARKS.cloudflare, + name: 'Cloudflare Turnstile', + role: 'Captcha', +} + +const RECAPTCHA: Integration = { + color: '#4285f4', + docs: 'dev/captcha/recaptcha', + glyph: ShieldCheck, + name: 'Google reCAPTCHA', + role: 'Captcha', +} + +const NODE_CRON: Integration = { + color: '#5fa04e', + docs: 'dev/cron/node-cron', + mark: INTEGRATION_MARKS.nodejs, + name: 'node-cron', + role: 'Cron', +} + +const DOCKER: Integration = { + color: '#2496ed', + docs: 'dev/deployments/self-hosted', + mark: INTEGRATION_MARKS.docker, + name: 'Docker', + role: 'Deploy', +} + +const COLUMNS: Integration[][] = [ + [DOCKER], + [NODEMAILER, TAILWIND], + [DRIZZLE, TANSTACK_START, TURNSTILE], + [ELASTICSEARCH, HONO], + [POSTGRESQL, REACT, S3], + [AI_SDK, RECAPTCHA], + [NODE_CRON], ] -const HIGHLIGHTED = INTEGRATIONS.filter(({ highlighted }) => highlighted) -const MOSAIC = INTEGRATIONS.filter(({ highlighted }) => !highlighted) +const TILE = + 'mk-integration-tile focus-visible:outline-ring block size-11 touch-manipulation rounded-xl focus-visible:outline-2 focus-visible:outline-offset-2 sm:size-18 sm:rounded-2xl lg:size-24 lg:rounded-3xl' -const brandStyle = (color: string) => - ({ '--brand': color }) as React.CSSProperties +const TileFace = ({ integration }: { integration: Integration }) => ( + + + +) -const IntegrationMark = ({ - className, - integration, -}: { - className?: string - integration: Integration -}) => { - const Glyph = integration.glyph +const IntegrationMark = ({ integration }: { integration: Integration }) => { + const className = 'mk-integration-mark size-5 sm:size-8 lg:size-11' - if (Glyph) return + if ('glyph' in integration) + return return ( , 'href'> & { integration: Integration }) => { + const label = `${integration.name}, ${integration.role}` + const style = { '--brand': integration.color } as React.CSSProperties + if ('href' in integration) return ( - {children} + ) return ( - {children} + ) } -const CARD = - 'group focus-visible:outline-ring flex w-full rounded-3xl bg-(--brand)/8 transition-[background-color,scale] duration-200 ease-out hover:bg-(--brand)/12 focus-visible:outline-2 focus-visible:outline-offset-2 active:scale-[0.98]' - -const CardArrow = ({ className }: { className?: string }) => ( - -) - -const CardMark = ({ integration }: { integration: Integration }) => ( - - - -) - -const HighlightCard = ({ integration }: { integration: Integration }) => ( - - - - - - {integration.name} - - - {integration.category} - - - - {integration.blurb} - - - - -) - -const FeaturedCard = ({ integration }: { integration: Integration }) => ( - - - - - - - - {integration.category} - - - {integration.name} - - - {integration.blurb} - - - -) - const IntegrationTile = ({ integration }: { integration: Integration }) => ( - - - - {integration.short ?? integration.name} - - + + } /> + + {integration.name} + {integration.role} + + ) export const IntegrationsSection = () => ( - - - Built on TanStack Start, Hono, Drizzle and Tailwind CSS, with adapters for - email, storage, search, captcha and AI. Every tile links to its guide or - homepage. - - -
-
    - {HIGHLIGHTED.map((integration) => ( -
  • - -
  • - ))} -
- -
    - {MOSAIC.map((integration) => ( -
  • +
    +

    + + Integrations +

    +

    + Made of parts you already know. +

    +
    + + +
    + {COLUMNS.map((column) => ( +
    - {integration.featured ? ( - - ) : ( - - )} -
  • + {column.map((integration) => ( +
    + +
    + ))} +
))} - + + + +
+

+ TanStack Start renders the pages, Hono serves the API and Drizzle talks + to PostgreSQL. Email, storage, search, captcha, AI and cron are adapters + you pick in config. +

+ + Explore the docs +
) diff --git a/apps/web/src/site/home/sections/logos/integration-marks.ts b/apps/web/src/site/home/sections/logos/integration-marks.ts index e80ff5797..5d018062d 100644 --- a/apps/web/src/site/home/sections/logos/integration-marks.ts +++ b/apps/web/src/site/home/sections/logos/integration-marks.ts @@ -12,6 +12,8 @@ export const INTEGRATION_MARKS = { 'M11.998,24c-0.321,0-0.641-0.084-0.922-0.247l-2.936-1.737c-0.438-0.245-0.224-0.332-0.08-0.383 c0.585-0.203,0.703-0.25,1.328-0.604c0.065-0.037,0.151-0.023,0.218,0.017l2.256,1.339c0.082,0.045,0.197,0.045,0.272,0l8.795-5.076 c0.082-0.047,0.134-0.141,0.134-0.238V6.921c0-0.099-0.053-0.192-0.137-0.242l-8.791-5.072c-0.081-0.047-0.189-0.047-0.271,0 L3.075,6.68C2.99,6.729,2.936,6.825,2.936,6.921v10.15c0,0.097,0.054,0.189,0.139,0.235l2.409,1.392 c1.307,0.654,2.108-0.116,2.108-0.89V7.787c0-0.142,0.114-0.253,0.256-0.253h1.115c0.139,0,0.255,0.112,0.255,0.253v10.021 c0,1.745-0.95,2.745-2.604,2.745c-0.508,0-0.909,0-2.026-0.551L2.28,18.675c-0.57-0.329-0.922-0.945-0.922-1.604V6.921 c0-0.659,0.353-1.275,0.922-1.603l8.795-5.082c0.557-0.315,1.296-0.315,1.848,0l8.794,5.082c0.57,0.329,0.924,0.944,0.924,1.603 v10.15c0,0.659-0.354,1.273-0.924,1.604l-8.794,5.078C12.643,23.916,12.324,24,11.998,24z M19.099,13.993 c0-1.9-1.284-2.406-3.987-2.763c-2.731-0.361-3.009-0.548-3.009-1.187c0-0.528,0.235-1.233,2.258-1.233 c1.807,0,2.473,0.389,2.747,1.607c0.024,0.115,0.129,0.199,0.247,0.199h1.141c0.071,0,0.138-0.031,0.186-0.081 c0.048-0.054,0.074-0.123,0.067-0.196c-0.177-2.098-1.571-3.076-4.388-3.076c-2.508,0-4.004,1.058-4.004,2.833 c0,1.925,1.488,2.457,3.895,2.695c2.88,0.282,3.103,0.703,3.103,1.269c0,0.983-0.789,1.402-2.642,1.402 c-2.327,0-2.839-0.584-3.011-1.742c-0.02-0.124-0.126-0.215-0.253-0.215h-1.137c-0.141,0-0.254,0.112-0.254,0.253 c0,1.482,0.806,3.248,4.655,3.248C17.501,17.007,19.099,15.91,19.099,13.993z', postgresql: 'M23.5594 14.7228a.5269.5269 0 0 0-.0563-.1191c-.139-.2632-.4768-.3418-1.0074-.2321-1.6533.3411-2.2935.1312-2.5256-.0191 1.342-2.0482 2.445-4.522 3.0411-6.8297.2714-1.0507.7982-3.5237.1222-4.7316a1.5641 1.5641 0 0 0-.1509-.235C21.6931.9086 19.8007.0248 17.5099.0005c-1.4947-.0158-2.7705.3461-3.1161.4794a9.449 9.449 0 0 0-.5159-.0816 8.044 8.044 0 0 0-1.3114-.1278c-1.1822-.0184-2.2038.2642-3.0498.8406-.8573-.3211-4.7888-1.645-7.2219.0788C.9359 2.1526.3086 3.8733.4302 6.3043c.0409.818.5069 3.334 1.2423 5.7436.4598 1.5065.9387 2.7019 1.4334 3.582.553.9942 1.1259 1.5933 1.7143 1.7895.4474.1491 1.1327.1441 1.8581-.7279.8012-.9635 1.5903-1.8258 1.9446-2.2069.4351.2355.9064.3625 1.39.3772a.0569.0569 0 0 0 .0004.0041 11.0312 11.0312 0 0 0-.2472.3054c-.3389.4302-.4094.5197-1.5002.7443-.3102.064-1.1344.2339-1.1464.8115-.0025.1224.0329.2309.0919.3268.2269.4231.9216.6097 1.015.6331 1.3345.3335 2.5044.092 3.3714-.6787-.017 2.231.0775 4.4174.3454 5.0874.2212.5529.7618 1.9045 2.4692 1.9043.2505 0 .5263-.0291.8296-.0941 1.7819-.3821 2.5557-1.1696 2.855-2.9059.1503-.8707.4016-2.8753.5388-4.1012.0169-.0703.0357-.1207.057-.1362.0007-.0005.0697-.0471.4272.0307a.3673.3673 0 0 0 .0443.0068l.2539.0223.0149.001c.8468.0384 1.9114-.1426 2.5312-.4308.6438-.2988 1.8057-1.0323 1.5951-1.6698zM2.371 11.8765c-.7435-2.4358-1.1779-4.8851-1.2123-5.5719-.1086-2.1714.4171-3.6829 1.5623-4.4927 1.8367-1.2986 4.8398-.5408 6.108-.13-.0032.0032-.0066.0061-.0098.0094-2.0238 2.044-1.9758 5.536-1.9708 5.7495-.0002.0823.0066.1989.0162.3593.0348.5873.0996 1.6804-.0735 2.9184-.1609 1.1504.1937 2.2764.9728 3.0892.0806.0841.1648.1631.2518.2374-.3468.3714-1.1004 1.1926-1.9025 2.1576-.5677.6825-.9597.5517-1.0886.5087-.3919-.1307-.813-.5871-1.2381-1.3223-.4796-.839-.9635-2.0317-1.4155-3.5126zm6.0072 5.0871c-.1711-.0428-.3271-.1132-.4322-.1772.0889-.0394.2374-.0902.4833-.1409 1.2833-.2641 1.4815-.4506 1.9143-1.0002.0992-.126.2116-.2687.3673-.4426a.3549.3549 0 0 0 .0737-.1298c.1708-.1513.2724-.1099.4369-.0417.156.0646.3078.26.3695.4752.0291.1016.0619.2945-.0452.4444-.9043 1.2658-2.2216 1.2494-3.1676 1.0128zm2.094-3.988-.0525.141c-.133.3566-.2567.6881-.3334 1.003-.6674-.0021-1.3168-.2872-1.8105-.8024-.6279-.6551-.9131-1.5664-.7825-2.5004.1828-1.3079.1153-2.4468.079-3.0586-.005-.0857-.0095-.1607-.0122-.2199.2957-.2621 1.6659-.9962 2.6429-.7724.4459.1022.7176.4057.8305.928.5846 2.7038.0774 3.8307-.3302 4.7363-.084.1866-.1633.3629-.2311.5454zm7.3637 4.5725c-.0169.1768-.0358.376-.0618.5959l-.146.4383a.3547.3547 0 0 0-.0182.1077c-.0059.4747-.054.6489-.115.8693-.0634.2292-.1353.4891-.1794 1.0575-.11 1.4143-.8782 2.2267-2.4172 2.5565-1.5155.3251-1.7843-.4968-2.0212-1.2217a6.5824 6.5824 0 0 0-.0769-.2266c-.2154-.5858-.1911-1.4119-.1574-2.5551.0165-.5612-.0249-1.9013-.3302-2.6462.0044-.2932.0106-.5909.019-.8918a.3529.3529 0 0 0-.0153-.1126 1.4927 1.4927 0 0 0-.0439-.208c-.1226-.4283-.4213-.7866-.7797-.9351-.1424-.059-.4038-.1672-.7178-.0869.067-.276.1831-.5875.309-.9249l.0529-.142c.0595-.16.134-.3257.213-.5012.4265-.9476 1.0106-2.2453.3766-5.1772-.2374-1.0981-1.0304-1.6343-2.2324-1.5098-.7207.0746-1.3799.3654-1.7088.5321a5.6716 5.6716 0 0 0-.1958.1041c.0918-1.1064.4386-3.1741 1.7357-4.4823a4.0306 4.0306 0 0 1 .3033-.276.3532.3532 0 0 0 .1447-.0644c.7524-.5706 1.6945-.8506 2.802-.8325.4091.0067.8017.0339 1.1742.081 1.939.3544 3.2439 1.4468 4.0359 2.3827.8143.9623 1.2552 1.9315 1.4312 2.4543-1.3232-.1346-2.2234.1268-2.6797.779-.9926 1.4189.543 4.1729 1.2811 5.4964.1353.2426.2522.4522.2889.5413.2403.5825.5515.9713.7787 1.2552.0696.087.1372.1714.1885.245-.4008.1155-1.1208.3825-1.0552 1.717-.0123.1563-.0423.4469-.0834.8148-.0461.2077-.0702.4603-.0994.7662zm.8905-1.6211c-.0405-.8316.2691-.9185.5967-1.0105a2.8566 2.8566 0 0 0 .135-.0406 1.202 1.202 0 0 0 .1342.103c.5703.3765 1.5823.4213 3.0068.1344-.2016.1769-.5189.3994-.9533.6011-.4098.1903-1.0957.333-1.7473.3636-.7197.0336-1.0859-.0807-1.1721-.151zm.5695-9.2712c-.0059.3508-.0542.6692-.1054 1.0017-.055.3576-.112.7274-.1264 1.1762-.0142.4368.0404.8909.0932 1.3301.1066.887.216 1.8003-.2075 2.7014a3.5272 3.5272 0 0 1-.1876-.3856c-.0527-.1276-.1669-.3326-.3251-.6162-.6156-1.1041-2.0574-3.6896-1.3193-4.7446.3795-.5427 1.3408-.5661 2.1781-.463zm.2284 7.0137a12.3762 12.3762 0 0 0-.0853-.1074l-.0355-.0444c.7262-1.1995.5842-2.3862.4578-3.4385-.0519-.4318-.1009-.8396-.0885-1.2226.0129-.4061.0666-.7543.1185-1.0911.0639-.415.1288-.8443.1109-1.3505.0134-.0531.0188-.1158.0118-.1902-.0457-.4855-.5999-1.938-1.7294-3.253-.6076-.7073-1.4896-1.4972-2.6889-2.0395.5251-.1066 1.2328-.2035 2.0244-.1859 2.0515.0456 3.6746.8135 4.8242 2.2824a.908.908 0 0 1 .0667.1002c.7231 1.3556-.2762 6.2751-2.9867 10.5405zm-8.8166-6.1162c-.025.1794-.3089.4225-.6211.4225a.5821.5821 0 0 1-.0809-.0056c-.1873-.026-.3765-.144-.5059-.3156-.0458-.0605-.1203-.178-.1055-.2844.0055-.0401.0261-.0985.0925-.1488.1182-.0894.3518-.1226.6096-.0867.3163.0441.6426.1938.6113.4186zm7.9305-.4114c.0111.0792-.049.201-.1531.3102-.0683.0717-.212.1961-.4079.2232a.5456.5456 0 0 1-.075.0052c-.2935 0-.5414-.2344-.5607-.3717-.024-.1765.2641-.3106.5611-.352.297-.0414.6111.0088.6356.1851z', + react: + 'M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z', tailwindcss: 'M12.001,4.8c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 C13.666,10.618,15.027,12,18.001,12c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C16.337,6.182,14.976,4.8,12.001,4.8z M6.001,12c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 c1.177,1.194,2.538,2.576,5.512,2.576c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C10.337,13.382,8.976,12,6.001,12z', tanstack: diff --git a/apps/web/src/site/home/sections/powering-by.tsx b/apps/web/src/site/home/sections/powering-by.tsx deleted file mode 100644 index a359d7169..000000000 --- a/apps/web/src/site/home/sections/powering-by.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { InfiniteSlider } from '@/site/home/infinite-slider' -import { DrizzleORMLogo } from '@/site/home/sections/logos/drizzleorm' -import { HonoJSLogo } from '@/site/home/sections/logos/honojs' -import { PostgreSQLLogo } from '@/site/home/sections/logos/postgresql' -import { TailwindCSSLogo } from '@/site/home/sections/logos/tailwindcss' -import { TanStackLogo } from '@/site/home/sections/logos/tanstack' -import { TurboRepoLogo } from '@/site/home/sections/logos/turborepo' - -const TOOLS = [ - { href: 'https://tailwindcss.com/', logo: }, - { href: 'https://tanstack.com/', logo: }, - { href: 'https://hono.dev/', logo: }, - { href: 'https://turborepo.com/', logo: }, - { href: 'https://orm.drizzle.team/', logo: }, - { href: 'https://www.postgresql.org/', logo: }, -] - -export const PoweringBySection = () => ( -
-
-

- Built on tools your team already trusts -

- -
- - {TOOLS.map(({ href, logo }) => ( - - {logo} - - ))} - - -
-
-
-
-
-) diff --git a/apps/web/src/site/marketing/marketing.css b/apps/web/src/site/marketing/marketing.css index 40d9e5551..d7ccf3299 100644 --- a/apps/web/src/site/marketing/marketing.css +++ b/apps/web/src/site/marketing/marketing.css @@ -868,59 +868,47 @@ } } -.mk-integration-tile { +.mk-integration-face { + border: 1px solid var(--border); background-color: var(--card); - color: var(--brand); - box-shadow: - 0 0 0 1px rgb(0 0 0 / 0.06), - 0 1px 2px rgb(0 0 0 / 0.04), - 0 8px 20px -10px rgb(0 0 0 / 0.1); transition: - background-color 200ms ease, - box-shadow 200ms ease, - scale 160ms ease-out; - touch-action: manipulation; -} - -.dark .mk-integration-tile { - box-shadow: 0 0 0 1px rgb(255 255 255 / 0.09); + translate 220ms cubic-bezier(0.23, 1, 0.32, 1), + scale 150ms ease-out, + border-color 150ms ease-out, + box-shadow 220ms cubic-bezier(0.23, 1, 0.32, 1); } .mk-integration-mark { - transition: scale 240ms cubic-bezier(0.23, 1, 0.32, 1); + transition: scale 220ms cubic-bezier(0.23, 1, 0.32, 1); } @media (hover: hover) and (pointer: fine) { - .mk-integration-tile:hover { - background-color: color-mix(in oklch, var(--brand) 8%, var(--card)); - box-shadow: - 0 0 0 1px color-mix(in oklch, var(--brand) 40%, transparent), - 0 2px 4px rgb(0 0 0 / 0.04), - 0 14px 30px -12px color-mix(in oklch, var(--brand) 50%, transparent); + .mk-integration-tile:hover .mk-integration-face { + translate: 0 -4px; + border-color: color-mix(in oklch, var(--brand) 45%, transparent); + box-shadow: 0 14px 28px -14px + color-mix(in oklch, var(--brand) 55%, transparent); } .mk-integration-tile:hover .mk-integration-mark { - scale: 1.06; + scale: 1.08; } } -.mk-integration-tile:active { +.mk-integration-tile:active .mk-integration-face { scale: 0.96; } -.mk-integration-tile:focus-visible { - outline: 2px solid var(--ring); - outline-offset: 3px; -} - @media (prefers-reduced-motion: reduce) { - .mk-integration-tile, + .mk-integration-face, .mk-integration-mark { transition: none; } - .mk-integration-tile:active, + .mk-integration-tile:hover .mk-integration-face, + .mk-integration-tile:active .mk-integration-face, .mk-integration-tile:hover .mk-integration-mark { + translate: none; scale: none; } } diff --git a/apps/web/src/vitnode.config.ts b/apps/web/src/vitnode.config.ts index 1873a5aa4..34374619d 100644 --- a/apps/web/src/vitnode.config.ts +++ b/apps/web/src/vitnode.config.ts @@ -1,4 +1,5 @@ import { blogPlugin } from '@vitnode/blog/config' +import { LogoVitNodeBrand } from '@vitnode/core/components/logo-vitnode' import { buildConfig } from '@vitnode/core/vitnode.config' import { examplePlugin } from '@vitnode/example/config' @@ -34,6 +35,7 @@ export const vitNodeConfig = buildConfig({ }, timeZone: 'UTC', }, + logo: LogoVitNodeBrand, metadata: { shortTitle: 'VitNode', title: 'VitNode', diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/components/admin-shell.tsx b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/components/admin-shell.tsx deleted file mode 100644 index 88ef79db5..000000000 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/components/admin-shell.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { AdminShellContent } from "@vitnode/core/tanstack/admin"; -import { useAppNavigate } from "@vitnode/core/tanstack/auth"; -import { LanguageSwitcher } from "@vitnode/core/tanstack/layout"; - -import { adminNav } from "@/admin-nav.gen"; - -export const AdminShell = ({ children }: { children: React.ReactNode }) => { - const navigate = useAppNavigate(); - - return ( - } - nav={adminNav} - onNavigate={href => { - void navigate(href); - }} - > - {children} - - ); -}; diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/components/main-footer.tsx b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/components/main-footer.tsx index 0a132b8e3..b51e5fe1a 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/components/main-footer.tsx +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/components/main-footer.tsx @@ -3,42 +3,59 @@ import { VITNODE_SPONSOR_URL, VITNODE_WEBSITE_URL, } from "@vitnode/core/lib/docs-links"; +import { GLOBAL_NAMESPACE, RouteMessages } from "@vitnode/core/tanstack/i18n"; import { Heart } from "lucide-react"; +import { useTranslations } from "use-intl"; -export const MainFooter = () => ( - +export const MainFooter = () => ( + + + ); + +const MainFooterContent = () => { + const t = useTranslations("app.footer"); + + return ( + + ); +}; diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/components/main-header.tsx b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/components/main-header.tsx deleted file mode 100644 index 46dfcad3a..000000000 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/components/main-header.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { MainHeader as MainHeaderContent } from "@vitnode/core/tanstack/layout"; - - -export const MainHeader = () => ; diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/locales/app.ts b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/locales/app.ts index da24eea03..f9ee40131 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/locales/app.ts +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/locales/app.ts @@ -1,4 +1,7 @@ import type { AppMessagesMap } from "@vitnode/core/lib/i18n/types"; - -export const appMessages: AppMessagesMap = {}; +export const appMessages: AppMessagesMap = { + en: { + app: async () => await import("./app/en.json"), + }, +}; diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/locales/app/en.json b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/locales/app/en.json new file mode 100644 index 000000000..69df08c5c --- /dev/null +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/locales/app/en.json @@ -0,0 +1,42 @@ +{ + "app": { + "home": { + "meta": { + "title": "Welcome", + "desc": "Your VitNode app is up and running." + }, + "title": "Your VitNode app is running", + "desc": { + "signed_in": "You are signed in as {name}. Open the AdminCP to configure the site.", + "guest": "Create an account or sign in, then open the AdminCP to configure the site. On a fresh install the first registered member becomes the administrator." + }, + "create_account": "Create an account", + "open_admin": "Open AdminCP", + "next_steps": "Next steps", + "steps": { + "register": { + "title": "Register", + "desc": "Create a member account. On a fresh install the first one becomes the administrator." + }, + "admin": { + "title": "AdminCP", + "desc": "Settings, members, roles, plugins and content live here." + }, + "docs": { + "title": "Documentation", + "desc": "Configuration, routing, plugins and deployment guides." + }, + "website": { + "title": "vitnode.com", + "desc": "Project website, release notes and community links." + } + }, + "replace_hint": "This page lives in src/routes/_main/index.tsx. Replace it whenever you are ready." + }, + "footer": { + "powered_by": "Powered by VitNode", + "docs": "Docs", + "sponsor": "Sponsor" + } + } +} diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/__root.tsx b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/__root.tsx index 36910ad75..8f0524dcf 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/__root.tsx +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/__root.tsx @@ -24,7 +24,7 @@ import { vitNodeConfig } from "@/vitnode.config"; import appCss from "../styles.css?url"; -const { debug, editor, i18n, metadata, theme } = vitNodeConfig; +const { debug, editor, i18n, logo, metadata, theme } = vitNodeConfig; export interface RootRouterContext { queryClient: QueryClient; @@ -64,7 +64,7 @@ export const Route = createRootRouteWithContext()({ function RootComponent() { return ( diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/_admin.tsx b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/_admin.tsx index d9a988704..7317e779c 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/_admin.tsx +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/_admin.tsx @@ -1,65 +1,6 @@ -import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"; -import { - ADMIN_ENTRY_PATH, - adminReturnToFor, - canEnterAdmin, - ensureAdminAccess, - loadAdminMessages, - preloadAdminAccess, -} from "@vitnode/core/tanstack/admin"; -import { pageHead } from "@vitnode/core/tanstack/metadata"; +import { createFileRoute } from "@tanstack/react-router"; +import { adminLayoutRoute } from "@vitnode/core/tanstack/admin"; -import { AdminShell } from "@/components/admin-shell"; - -export const Route = createFileRoute("/_admin")({ - beforeLoad: async ({ context, location, preload }) => { - const access = preload - ? await preloadAdminAccess(context.queryClient) - : await ensureAdminAccess(context.queryClient); - - if (!canEnterAdmin(access)) { - // TanStack Router's own control-flow signal: `redirect()` returns a typed - // redirect object that the router catches and turns into a navigation - // (or, during SSR, a 302). Throwing it is what stops the guard. - // - // `/admin` is a sibling leaf, not a child of this route, so this cannot - // loop: the sign-in page's own guard only redirects *away* on a granted - // session, and `sanitizeAdminReturnTo` rejects `/admin` as a target. - // - // Cast because `/admin` is not in this router's type table: it is - // `@vitnode/core`'s own declared route now, mounted by `withVitNodeRoutes`, - // and code-based routes are outside the generated tree's types. The - // *runtime* is unaffected, and `ADMIN_ENTRY_PATH` is the package's own - // constant - so the path and the sign-in route that serves it are still one - // fact in one place. - // eslint-disable-next-line @typescript-eslint/only-throw-error - throw redirect({ - search: { returnTo: adminReturnToFor(location) }, - to: ADMIN_ENTRY_PATH, - } as unknown as Parameters[0]); - } - - // Merged into the context of everything below, so a child route reads - // `context.adminAccess` already narrowed to the granted half of the union. - // It is the same object the guard decided on, from the same cache entry, so - // a page cannot disagree with the guard that let it render. - return { adminAccess: access }; - }, - - loader: async ({ context }) => { - const { adminNav } = await import("@/admin-nav.gen"); - - await loadAdminMessages({ ...context, namespaces: adminNav.namespaces }); - }, - - head: () => pageHead({ robots: "noindex, nofollow" }), - component: AdminLayout, -}); - -function AdminLayout() { - return ( - - - - ); -} +export const Route = createFileRoute("/_admin")( + adminLayoutRoute(async () => await import("@/admin-nav.gen")), +); diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/_main.tsx b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/_main.tsx index 2718d5b5f..a32b9cc5f 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/_main.tsx +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/_main.tsx @@ -2,11 +2,11 @@ import { createFileRoute, Outlet } from "@tanstack/react-router"; import { MainBreadcrumb } from "@vitnode/core/tanstack/breadcrumb"; import { loadMainShell, + MainHeader, ThemeLayoutContent, } from "@vitnode/core/tanstack/layout"; import { MainFooter } from "@/components/main-footer"; -import { MainHeader } from "@/components/main-header"; export const Route = createFileRoute("/_main")({ loader: async ({ context }) => await loadMainShell(context), diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/_main/index.tsx b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/_main/index.tsx index 32b9e6556..0c935cf43 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/_main/index.tsx +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/_main/index.tsx @@ -1,6 +1,6 @@ -import type { LucideIcon } from "lucide-react"; - import type { RegisteredRouter } from "@tanstack/react-router"; +import type { LucideIcon } from "lucide-react"; +import type { Messages } from "use-intl"; import { createFileRoute, Link } from "@tanstack/react-router"; import { LogoVitNode } from "@vitnode/core/components/logo-vitnode"; @@ -10,6 +10,11 @@ import { VITNODE_WEBSITE_URL, } from "@vitnode/core/lib/docs-links"; import { useSessionQuery } from "@vitnode/core/tanstack/auth"; +import { + GLOBAL_NAMESPACE, + intlQueryOptions, + RouteMessages, +} from "@vitnode/core/tanstack/i18n"; import { pageHead } from "@vitnode/core/tanstack/metadata"; import { cn } from "cn"; import { @@ -19,60 +24,80 @@ import { ShieldCheck, UserRoundPlus, } from "lucide-react"; +import { createTranslator, useTranslations } from "use-intl"; const REGISTER_HREF = "/register"; const ADMIN_HREF = "/admin"; +const HOME_NAMESPACES = [GLOBAL_NAMESPACE, "app.home"] as const; export const Route = createFileRoute("/_main/")({ - head: () => + loader: async ({ context: { locale, queryClient } }) => { + const { messages } = await queryClient.query({ + ...intlQueryOptions({ locale, namespaces: HOME_NAMESPACES }), + staleTime: "static", + }); + const t = createTranslator({ + locale, + messages: messages as Messages, + namespace: "app.home.meta", + }); + + return { description: t("desc"), title: t("title") }; + }, + head: ({ loaderData }) => pageHead({ - description: "Your VitNode app is up and running.", + description: loaderData?.description, robots: "index, follow", - title: "Welcome", + title: loaderData?.title, }), component: HomeRoute, }); +type StepId = "admin" | "docs" | "register" | "website"; + interface NextStep { - description: string; external?: boolean; href: string; icon: LucideIcon; - title: string; + id: StepId; } const registerStep: NextStep = { - description: - "Create a member account. On a fresh install the first one becomes the administrator.", href: REGISTER_HREF, icon: UserRoundPlus, - title: "Register", + id: "register", }; const sharedSteps: NextStep[] = [ { - description: "Settings, members, roles, plugins and content live here.", href: ADMIN_HREF, icon: ShieldCheck, - title: "AdminCP", + id: "admin", }, { - description: "Configuration, routing, plugins and deployment guides.", external: true, href: VITNODE_DOCS_URL, icon: BookOpen, - title: "Documentation", + id: "docs", }, { - description: "Project website, release notes and community links.", external: true, href: VITNODE_WEBSITE_URL, icon: Globe, - title: "vitnode.com", + id: "website", }, ]; function HomeRoute() { + return ( + + + + ); +} + +function HomeContent() { + const t = useTranslations("app.home"); const { data } = useSessionQuery(); const user = data?.user ?? null; const steps = user ? sharedSteps : [registerStep, ...sharedSteps]; @@ -84,13 +109,11 @@ function HomeRoute() {

- Your VitNode app is running + {t("title")}

- {user - ? `You are signed in as ${user.name}. Open the AdminCP to configure the site.` - : "Create an account or sign in, then open the AdminCP to configure the site. On a fresh install the first registered member becomes the administrator."} + {user ? t("desc.signed_in", { name: user.name }) : t("desc.guest")}

@@ -101,7 +124,7 @@ function HomeRoute() { to={REGISTER_HREF} > - Create an account + {t("create_account")} )} @@ -116,14 +139,14 @@ function HomeRoute() { to={ADMIN_HREF} > - Open AdminCP + {t("open_admin")}

- Next steps + {t("next_steps")}

- This page lives in src/routes/_main/index.tsx. Replace it - whenever you are ready. + {t.rich("replace_hint", { + code: chunks => {chunks}, + })}

); } -function NextStepCard({ - description, - external = false, - href, - icon: Icon, - title, -}: NextStep) { +function NextStepCard({ external = false, href, icon: Icon, id }: NextStep) { + const t = useTranslations("app.home.steps"); const className = "group bg-card hover:bg-muted/50 focus-visible:border-ring focus-visible:ring-ring/50 flex flex-col gap-3 rounded-xl border p-5 transition-colors outline-none focus-visible:ring-3"; @@ -167,9 +186,9 @@ function NextStepCard({
- {title} + {t(`${id}.title`)} - {description} + {t(`${id}.desc`)}
diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/styles.css b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/styles.css index 69513b185..526224bb1 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/styles.css +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/styles.css @@ -3,6 +3,8 @@ @import "tw-animate-css"; @import "@vitnode/core/styles/tiptap.css"; +@custom-variant dark (&:where(.dark, .dark *)); + :root:not(.dark) { color-scheme: light; --background: oklch(0.96 0.007 264); diff --git a/packages/vitnode/src/components/logo-context.tsx b/packages/vitnode/src/components/logo-context.tsx new file mode 100644 index 000000000..207df7dab --- /dev/null +++ b/packages/vitnode/src/components/logo-context.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +import { LogoVitNodeBrand } from "./logo-vitnode"; + +export type VitNodeLogo = React.ComponentType; + +export const LogoContext = React.createContext( + , +); + +export const useLogo = (): React.ReactNode => React.use(LogoContext); diff --git a/packages/vitnode/src/pages/admin/sign-in.tsx b/packages/vitnode/src/pages/admin/sign-in.tsx index 811467f66..80a17e67e 100644 --- a/packages/vitnode/src/pages/admin/sign-in.tsx +++ b/packages/vitnode/src/pages/admin/sign-in.tsx @@ -3,16 +3,12 @@ import type { AdminSignInSearch } from "@/tanstack/admin/sign-in-search"; import { AdminSignInRouteContent } from "@/tanstack/admin/sign-in-screen"; import { loadAuthCard } from "@/tanstack/auth/login-route"; -import { useAppNavigate } from "@/tanstack/auth/navigation"; import { defineRoute } from "@/tanstack/plugin-routes"; const AdminSignInPage = ({ search, }: PluginRoutePageProps) => ( - + ); export const route = defineRoute({ diff --git a/packages/vitnode/src/pages/login/index.tsx b/packages/vitnode/src/pages/login/index.tsx index 06f846810..a657277c5 100644 --- a/packages/vitnode/src/pages/login/index.tsx +++ b/packages/vitnode/src/pages/login/index.tsx @@ -3,13 +3,12 @@ import type { LoginSearch } from "@/tanstack/auth/route-search"; import { loadLoginRoute } from "@/tanstack/auth/login-route"; import { LoginRouteContent } from "@/tanstack/auth/login-screen"; -import { useAppNavigate } from "@/tanstack/auth/navigation"; import { defineRoute } from "@/tanstack/plugin-routes"; const LoginPage = ({ search, }: PluginRoutePageProps) => ( - + ); export const route = defineRoute({ diff --git a/packages/vitnode/src/pages/register.tsx b/packages/vitnode/src/pages/register.tsx index 1617164db..bd187a67c 100644 --- a/packages/vitnode/src/pages/register.tsx +++ b/packages/vitnode/src/pages/register.tsx @@ -1,9 +1,8 @@ -import { useAppNavigate } from "../tanstack/auth/navigation"; import { loadRegisterRoute } from "../tanstack/auth/register-route"; import { RegisterRouteContent } from "../tanstack/auth/register-screen"; import { defineRoute } from "../tanstack/plugin-routes"; -const RegisterPage = () => ; +const RegisterPage = () => ; export const route = defineRoute({ load: async ({ context }) => { diff --git a/packages/vitnode/src/tanstack/admin/actions.ts b/packages/vitnode/src/tanstack/admin/actions.ts index a2052767b..3ad60254c 100644 --- a/packages/vitnode/src/tanstack/admin/actions.ts +++ b/packages/vitnode/src/tanstack/admin/actions.ts @@ -1,12 +1,12 @@ import { useQueryClient } from "@tanstack/react-query"; +import { useRouter } from "@tanstack/react-router"; import type { SignInSubmit } from "@/views/auth/sign-in/form/sign-in-form-content"; import type { PasskeySignInSubmit } from "@/views/auth/sign-in/passkey/passkey-sign-in-button"; import { getPasskeyInBrowser } from "@/views/auth/passkeys/webauthn"; -import type { AuthNavigate } from "../auth/actions"; - +import { internalDestination } from "../auth/navigation"; import { signInFormResult } from "../auth/screens"; import { authTransport } from "../auth/transport"; import { removeAdminIdentityQueries } from "./queries"; @@ -14,12 +14,11 @@ import { markAdminTabAlive } from "./tab-presence"; export const useAdminSignInAction = ({ destination, - navigate, }: { destination: () => string; - navigate: AuthNavigate; }): SignInSubmit => { const queryClient = useQueryClient(); + const router = useRouter(); return async values => { const result = await authTransport().signIn({ ...values, isAdmin: true }); @@ -28,7 +27,7 @@ export const useAdminSignInAction = ({ removeAdminIdentityQueries(queryClient); markAdminTabAlive(); - await navigate(destination()); + await router.navigate(internalDestination(destination())); return undefined; }; @@ -36,12 +35,11 @@ export const useAdminSignInAction = ({ export const useAdminPasskeySignInAction = ({ destination, - navigate, }: { destination: () => string; - navigate: AuthNavigate; }): PasskeySignInSubmit => { const queryClient = useQueryClient(); + const router = useRouter(); return async () => { const start = await authTransport().startAdminPasskeySignIn(); @@ -64,7 +62,7 @@ export const useAdminPasskeySignInAction = ({ removeAdminIdentityQueries(queryClient); markAdminTabAlive(); - await navigate(destination()); + await router.navigate(internalDestination(destination())); return undefined; }; diff --git a/packages/vitnode/src/tanstack/admin/index.ts b/packages/vitnode/src/tanstack/admin/index.ts index b71942f9f..2a1ca0ec5 100644 --- a/packages/vitnode/src/tanstack/admin/index.ts +++ b/packages/vitnode/src/tanstack/admin/index.ts @@ -7,6 +7,8 @@ export { } from "./breadcrumb"; export { defaultAdminTransport } from "./default-transport"; export * from "./intl"; +export type { AdminNavModule } from "./layout-route"; +export { adminLayoutRoute } from "./layout-route"; export { AdminNavProvider, useAdminNav, useAdminSearchNavItems } from "./nav"; export * from "./permissions"; export { removeAdminIdentityQueries, removeAdminShellQueries } from "./queries"; diff --git a/packages/vitnode/src/tanstack/admin/layout-route.tsx b/packages/vitnode/src/tanstack/admin/layout-route.tsx new file mode 100644 index 000000000..adbd63cd6 --- /dev/null +++ b/packages/vitnode/src/tanstack/admin/layout-route.tsx @@ -0,0 +1,61 @@ +import type { QueryClient } from "@tanstack/react-query"; + +import { lazyRouteComponent, redirect } from "@tanstack/react-router"; + +import type { AdminNavBundle } from "@/views/admin/layouts/sidebar/nav/nav-model"; + +import type { AdminLoaderContext } from "./intl"; + +import { pageHead } from "../metadata"; +import { loadAdminMessages } from "./intl"; +import { adminReturnToFor } from "./return-to"; +import { ensureAdminAccess, preloadAdminAccess } from "./session-query"; +import { ADMIN_ENTRY_PATH, canEnterAdmin } from "./state"; + +export type AdminNavModule = () => Promise<{ adminNav: AdminNavBundle }>; + +export const adminLayoutRoute = (navModule: AdminNavModule) => ({ + beforeLoad: async ({ + context, + location, + preload, + }: { + context: { queryClient: QueryClient }; + location: Parameters[0]; + preload: boolean; + }) => { + const access = preload + ? await preloadAdminAccess(context.queryClient) + : await ensureAdminAccess(context.queryClient); + + if (!canEnterAdmin(access)) { + // eslint-disable-next-line @typescript-eslint/only-throw-error + throw redirect({ + search: { returnTo: adminReturnToFor(location) }, + to: ADMIN_ENTRY_PATH, + }); + } + + return { adminAccess: access }; + }, + + loader: async ({ context }: { context: AdminLoaderContext }) => { + const { adminNav } = await navModule(); + + await loadAdminMessages({ ...context, namespaces: adminNav.namespaces }); + }, + + head: () => pageHead({ robots: "noindex, nofollow" }), + component: lazyRouteComponent(async () => { + const [{ AdminShellContent }, { adminNav }] = await Promise.all([ + import("./shell"), + navModule(), + ]); + + return { + default: function AdminLayout() { + return ; + }, + }; + }), +}); diff --git a/packages/vitnode/src/tanstack/admin/queries.test.ts b/packages/vitnode/src/tanstack/admin/queries.test.ts index 34ed827f0..ebb789099 100644 --- a/packages/vitnode/src/tanstack/admin/queries.test.ts +++ b/packages/vitnode/src/tanstack/admin/queries.test.ts @@ -210,7 +210,7 @@ describe("every identity boundary drops the privileged cache", () => { expect(code).toContain(CLEANUP); expect(code.indexOf(CLEANUP)).toBeLessThan( - code.indexOf("await navigate(destination())"), + code.indexOf("await router.navigate(internalDestination(destination()))"), ); }); @@ -245,7 +245,7 @@ describe("every identity boundary drops the privileged cache", () => { const code = sourceOf("../auth/actions.ts"); expect(code.indexOf(CLEANUP)).toBeLessThan( - code.indexOf("await navigate(destination())"), + code.indexOf("await router.navigate(internalDestination(destination()))"), ); }); diff --git a/packages/vitnode/src/tanstack/admin/shell.tsx b/packages/vitnode/src/tanstack/admin/shell.tsx index ed1eede2e..c9770221e 100644 --- a/packages/vitnode/src/tanstack/admin/shell.tsx +++ b/packages/vitnode/src/tanstack/admin/shell.tsx @@ -1,4 +1,4 @@ -import { useRouterState } from "@tanstack/react-router"; +import { Outlet, useRouterState } from "@tanstack/react-router"; import React from "react"; import type { AdminUserSearch } from "@/views/admin/layouts/search/search-users"; @@ -24,15 +24,10 @@ import { AdminSessionGuard } from "./session-guard"; import { AdminUserBar } from "./user-bar"; export const AdminShellContent = ({ - children, - languageSwitcher, nav, onNavigate, searchUsers, }: { - children: React.ReactNode; - /** The host's language switcher, or nothing on a single-language install. */ - languageSwitcher?: React.ReactNode; nav?: AdminNavBundle; onNavigate?: (href: string) => void; searchUsers?: AdminUserSearch; @@ -50,12 +45,10 @@ export const AdminShellContent = ({ - - {children} + + + + @@ -65,12 +58,10 @@ export const AdminShellContent = ({ const AdminShellFrame = ({ children, - languageSwitcher, onNavigate, searchUsers, }: { children: React.ReactNode; - languageSwitcher?: React.ReactNode; onNavigate?: (href: string) => void; searchUsers?: AdminUserSearch; }) => { @@ -81,9 +72,7 @@ const AdminShellFrame = ({ return ( - } - > + }> diff --git a/packages/vitnode/src/tanstack/admin/sign-in-screen.tsx b/packages/vitnode/src/tanstack/admin/sign-in-screen.tsx index 7f7e2640f..d04c0bf64 100644 --- a/packages/vitnode/src/tanstack/admin/sign-in-screen.tsx +++ b/packages/vitnode/src/tanstack/admin/sign-in-screen.tsx @@ -2,8 +2,6 @@ import { SignInAdminContent } from "@/views/admin/sign-in/sign-in-admin-content" import { SignInFormContent } from "@/views/auth/sign-in/form/sign-in-form-content"; import { PasskeySignInButton } from "@/views/auth/sign-in/passkey/passkey-sign-in-button"; -import type { AuthNavigate } from "../auth/actions"; - import { useMiddlewareConfigQuery } from "../auth/middleware-config"; import { RouteMessages } from "../i18n/route-messages"; import { useAdminPasskeySignInAction, useAdminSignInAction } from "./actions"; @@ -11,19 +9,17 @@ import { sanitizeAdminReturnTo } from "./return-to"; import { ADMIN_SIGN_IN_NAMESPACES } from "./sign-in-route"; export interface AdminSignInRouteProps { - navigate: AuthNavigate; /** Where the administrator was heading before the guard sent them here. */ returnTo?: string; } export const AdminSignInRouteContent = ({ - navigate, returnTo, }: AdminSignInRouteProps) => { const { data: config } = useMiddlewareConfigQuery(); const destination = () => sanitizeAdminReturnTo(returnTo); - const signIn = useAdminSignInAction({ destination, navigate }); - const passkeySignIn = useAdminPasskeySignInAction({ destination, navigate }); + const signIn = useAdminSignInAction({ destination }); + const passkeySignIn = useAdminPasskeySignInAction({ destination }); return ( diff --git a/packages/vitnode/src/tanstack/admin/user-bar.tsx b/packages/vitnode/src/tanstack/admin/user-bar.tsx index ec248d8f1..d9f130618 100644 --- a/packages/vitnode/src/tanstack/admin/user-bar.tsx +++ b/packages/vitnode/src/tanstack/admin/user-bar.tsx @@ -5,14 +5,11 @@ import { useTranslations } from "use-intl"; import { UserBarAdminContent } from "@/views/admin/layouts/user-bar/user-bar-content"; import { useSignOutAction } from "../auth/actions"; +import { LanguageSwitcher } from "../layout/language-switcher"; import { useAdminUser } from "./permissions"; import { removeAdminIdentityQueries } from "./queries"; -export const AdminUserBar = ({ - languageSwitcher, -}: { - languageSwitcher?: React.ReactNode; -}) => { +export const AdminUserBar = () => { const user = useAdminUser(); const signOut = useSignOutAction(); const queryClient = useQueryClient(); @@ -36,7 +33,7 @@ export const AdminUserBar = ({ return ( } onSignOut={handleSignOut} user={user} /> diff --git a/packages/vitnode/src/tanstack/auth/actions.ts b/packages/vitnode/src/tanstack/auth/actions.ts index a72f8c9ab..19ab4b00a 100644 --- a/packages/vitnode/src/tanstack/auth/actions.ts +++ b/packages/vitnode/src/tanstack/auth/actions.ts @@ -15,6 +15,7 @@ import { getPasskeyInBrowser } from "@/views/auth/passkeys/webauthn"; import type { SsoCallbackInput } from "./contract"; import { removeAdminIdentityQueries } from "../admin/queries"; +import { internalDestination } from "./navigation"; import { removeUserIdentityQueries } from "./queries"; import { anonymousSession, @@ -34,16 +35,13 @@ import { import { shouldRefreshSessionAfterSignUp } from "./sign-up-session"; import { authTransport } from "./transport"; -export type AuthNavigate = (href: string) => Promise; - export const useSignInAction = ({ destination, - navigate, }: { destination: () => string; - navigate: AuthNavigate; }): SignInSubmit => { const queryClient = useQueryClient(); + const router = useRouter(); return async values => { const result = await authTransport().signIn(values); @@ -60,7 +58,7 @@ export const useSignInAction = ({ removeUserIdentityQueries(queryClient); await invalidateSession(queryClient); - await navigate(destination()); + await router.navigate(internalDestination(destination())); return undefined; }; @@ -68,12 +66,11 @@ export const useSignInAction = ({ export const usePasskeySignInAction = ({ destination, - navigate, }: { destination: () => string; - navigate: AuthNavigate; }): PasskeySignInSubmit => { const queryClient = useQueryClient(); + const router = useRouter(); return async () => { const start = await authTransport().startPasskeySignIn(); @@ -98,7 +95,7 @@ export const usePasskeySignInAction = ({ removeUserIdentityQueries(queryClient); await invalidateSession(queryClient); - await navigate(destination()); + await router.navigate(internalDestination(destination())); return undefined; }; @@ -188,12 +185,11 @@ export const useSignOutAction = () => { export const useSignUpAction = ({ destination, - navigate, }: { destination: () => string; - navigate: AuthNavigate; }): SignUpSubmit => { const queryClient = useQueryClient(); + const router = useRouter(); return async values => { const result = await authTransport().signUp(values); @@ -207,7 +203,7 @@ export const useSignUpAction = ({ removeAdminIdentityQueries(queryClient); removeUserIdentityQueries(queryClient); await invalidateSession(queryClient); - await navigate(destination()); + await router.navigate(internalDestination(destination())); } return signUpFormResult(result); diff --git a/packages/vitnode/src/tanstack/auth/index.ts b/packages/vitnode/src/tanstack/auth/index.ts index c6da58461..49871685b 100644 --- a/packages/vitnode/src/tanstack/auth/index.ts +++ b/packages/vitnode/src/tanstack/auth/index.ts @@ -6,7 +6,7 @@ export { loadLoginRoute, LOGIN_NAMESPACES } from "./login-route"; export type { LoginRouteProps } from "./login-screen"; export { LoginRouteContent } from "./login-screen"; export * from "./middleware-config"; -export { internalDestination, useAppNavigate } from "./navigation"; +export { internalDestination } from "./navigation"; export { removeUserIdentityQueries } from "./queries"; export * from "./recovery"; export type { PasswordResetRouteData } from "./recovery-route"; @@ -18,7 +18,6 @@ export { } from "./recovery-screen"; export * from "./redirects"; export { loadRegisterRoute, REGISTER_NAMESPACES } from "./register-route"; -export type { RegisterRouteProps } from "./register-screen"; export { RegisterRouteContent } from "./register-screen"; export * from "./return-to"; export * from "./route-search"; diff --git a/packages/vitnode/src/tanstack/auth/login-screen.tsx b/packages/vitnode/src/tanstack/auth/login-screen.tsx index 046777442..175bb4605 100644 --- a/packages/vitnode/src/tanstack/auth/login-screen.tsx +++ b/packages/vitnode/src/tanstack/auth/login-screen.tsx @@ -3,8 +3,6 @@ import { PasskeySignInButton } from "@/views/auth/sign-in/passkey/passkey-sign-i import { SignInContent } from "@/views/auth/sign-in/sign-in-content"; import { SSOButtonsContent } from "@/views/auth/sso/buttons/sso-buttons-content"; -import type { AuthNavigate } from "./actions"; - import { RouteMessages } from "../i18n/route-messages"; import { startSsoAction, @@ -20,21 +18,18 @@ import { import { postAuthDestination } from "./redirects"; export interface LoginRouteProps { - navigate: AuthNavigate; /** Where the visitor was heading before a guard sent them here. */ returnTo?: string; } -export const LoginRouteContent = ({ navigate, returnTo }: LoginRouteProps) => { +export const LoginRouteContent = ({ returnTo }: LoginRouteProps) => { const { data: config } = useMiddlewareConfigQuery(); const methods = authMethodsOf(config); const signIn = useSignInAction({ destination: () => postAuthDestination(returnTo), - navigate, }); const passkeySignIn = usePasskeySignInAction({ destination: () => postAuthDestination(returnTo), - navigate, }); return ( diff --git a/packages/vitnode/src/tanstack/auth/navigation.ts b/packages/vitnode/src/tanstack/auth/navigation.ts index c46f0894a..2b1732845 100644 --- a/packages/vitnode/src/tanstack/auth/navigation.ts +++ b/packages/vitnode/src/tanstack/auth/navigation.ts @@ -1,6 +1,3 @@ -import { useRouter } from "@tanstack/react-router"; - -import type { AuthNavigate } from "./actions"; import type { InternalDestination } from "./redirects"; import { readRequestHost } from "../i18n/locale"; @@ -12,11 +9,3 @@ export const internalDestination = (href: string): InternalDestination => localeRouting: getIntlRuntime().localeRouting, readHost: readRequestHost, }).internalDestination(href); - -export const useAppNavigate = (): AuthNavigate => { - const router = useRouter(); - - return async (href: string): Promise => { - await router.navigate(internalDestination(href)); - }; -}; diff --git a/packages/vitnode/src/tanstack/auth/queries.test.ts b/packages/vitnode/src/tanstack/auth/queries.test.ts index 7b95b7bdf..70dcf66de 100644 --- a/packages/vitnode/src/tanstack/auth/queries.test.ts +++ b/packages/vitnode/src/tanstack/auth/queries.test.ts @@ -247,7 +247,7 @@ describe("every public identity boundary drops the private cache", () => { const code = actionsSource(); expect(code.indexOf(CLEANUP)).toBeLessThan( - code.indexOf("await navigate(destination())"), + code.indexOf("await router.navigate(internalDestination(destination()))"), ); }); diff --git a/packages/vitnode/src/tanstack/auth/redirects.ts b/packages/vitnode/src/tanstack/auth/redirects.ts index 3631cdf36..091eba72f 100644 --- a/packages/vitnode/src/tanstack/auth/redirects.ts +++ b/packages/vitnode/src/tanstack/auth/redirects.ts @@ -1,9 +1,5 @@ -import { useRouter } from "@tanstack/react-router"; - import type { LocaleRouting } from "@/lib/i18n/locale-routing"; -import type { AuthNavigate } from "./actions"; - import { sanitizeReturnTo } from "./return-to"; export const LOGIN_PATH = "/login"; @@ -173,21 +169,5 @@ export const createAuthNavigation = ({ const internalDestination = (href: string): InternalDestination => parseInternalDestination(internalHref(href)); - return { - internalDestination, - /** - * Navigate to a validated internal path. - * - * The browser half. `router.navigate` performs it, so the router's own - * blockers and dangerous-protocol checks run and nothing here reaches around - * the framework with `location.assign`. - */ - useAppNavigate: (): AuthNavigate => { - const router = useRouter(); - - return async (href: string): Promise => { - await router.navigate(internalDestination(href)); - }; - }, - }; + return { internalDestination }; }; diff --git a/packages/vitnode/src/tanstack/auth/register-screen.tsx b/packages/vitnode/src/tanstack/auth/register-screen.tsx index 949df3aad..d2e810d5d 100644 --- a/packages/vitnode/src/tanstack/auth/register-screen.tsx +++ b/packages/vitnode/src/tanstack/auth/register-screen.tsx @@ -2,24 +2,17 @@ import { SignUpFormContent } from "@/views/auth/sign-up/form/sign-up-form-conten import { SignUpContent } from "@/views/auth/sign-up/sign-up-content"; import { SSOButtonsContent } from "@/views/auth/sso/buttons/sso-buttons-content"; -import type { AuthNavigate } from "./actions"; - import { RouteMessages } from "../i18n/route-messages"; import { startSsoAction, useSignUpAction } from "./actions"; import { authMethodsOf, useMiddlewareConfigQuery } from "./middleware-config"; import { postAuthDestination } from "./redirects"; import { REGISTER_NAMESPACES } from "./register-route"; -export interface RegisterRouteProps { - navigate: AuthNavigate; -} - -export const RegisterRouteContent = ({ navigate }: RegisterRouteProps) => { +export const RegisterRouteContent = () => { const { data: config } = useMiddlewareConfigQuery(); const methods = authMethodsOf(config); const signUp = useSignUpAction({ destination: () => postAuthDestination(undefined), - navigate, }); return ( diff --git a/packages/vitnode/src/tanstack/layout/header.tsx b/packages/vitnode/src/tanstack/layout/header.tsx index f8746e6b4..c61d41e11 100644 --- a/packages/vitnode/src/tanstack/layout/header.tsx +++ b/packages/vitnode/src/tanstack/layout/header.tsx @@ -11,7 +11,7 @@ import type { } from "@/lib/navigation"; import type { HeaderNavTranslate } from "@/views/layouts/theme/header/header-nav"; -import { LogoVitNodeBrand } from "@/components/logo-vitnode"; +import { useLogo } from "@/components/logo-context"; import { HeaderLayoutContent } from "@/views/layouts/theme/header/header-content"; import { headerNavItemsFrom, @@ -49,13 +49,8 @@ const headerNavTranslator = ( }; }; -export const Header = ({ - logo = , - user, -}: { - logo?: React.ReactNode; - user?: React.ReactNode; -}) => { +export const Header = ({ user }: { user?: React.ReactNode }) => { + const logo = useLogo(); const locale = useLocale(); const t = useTranslations("core.global"); const { data: config } = useMiddlewareConfigQuery(); diff --git a/packages/vitnode/src/tanstack/layout/main-header.tsx b/packages/vitnode/src/tanstack/layout/main-header.tsx index 3dd48ac53..867058b56 100644 --- a/packages/vitnode/src/tanstack/layout/main-header.tsx +++ b/packages/vitnode/src/tanstack/layout/main-header.tsx @@ -1,6 +1,4 @@ import { Header } from "./header"; import { UserHeader } from "./user-header"; -export const MainHeader = ({ logo }: { logo?: React.ReactNode }) => ( -
} /> -); +export const MainHeader = () =>
} />; diff --git a/packages/vitnode/src/views/layouts/providers.tsx b/packages/vitnode/src/views/layouts/providers.tsx index 6a607d2c6..3eac33675 100644 --- a/packages/vitnode/src/views/layouts/providers.tsx +++ b/packages/vitnode/src/views/layouts/providers.tsx @@ -6,6 +6,8 @@ import type { VitNodeConfig } from "@/vitnode.config"; import { EditWidgetsHost } from "@/blocks/edit-widgets-host"; import { EditorConfigProvider } from "@/components/editor-provider"; import { LanguagesProvider } from "@/components/languages-provider"; +import { LogoContext } from "@/components/logo-context"; +import { LogoVitNodeBrand } from "@/components/logo-vitnode"; import { ThemeProvider } from "@/components/theme-provider"; import { Toaster } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; @@ -15,7 +17,7 @@ import { RateLimitListener } from "./rate-limit-listener"; export interface VitNodeProvidersConfig extends Pick< VitNodeConfig, - "debug" | "editor" | "theme" + "debug" | "editor" | "logo" | "theme" > { locales: LocaleConfig[]; } @@ -23,12 +25,14 @@ export interface VitNodeProvidersConfig extends Pick< export const VitNodeProviders = ({ children, toaster, - config: { debug, editor, locales, theme }, + config: { debug, editor, locales, logo: Logo = LogoVitNodeBrand, theme }, }: { children: React.ReactNode; config: VitNodeProvidersConfig; toaster?: React.ComponentProps; }) => { + const logo = React.useMemo(() => , [Logo]); + React.useEffect(() => { // eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler if (!(debug && CONFIG.node_development)) return; @@ -52,7 +56,9 @@ export const VitNodeProviders = ({ - {children} + + {children} + diff --git a/packages/vitnode/src/vitnode.config.ts b/packages/vitnode/src/vitnode.config.ts index 408e86dad..a39624f92 100644 --- a/packages/vitnode/src/vitnode.config.ts +++ b/packages/vitnode/src/vitnode.config.ts @@ -12,6 +12,7 @@ import type { SearchProviderApiPlugin } from "./api/models/search"; import type { SSOApiPlugin } from "./api/models/sso"; import type { StorageApiPlugin } from "./api/models/storage"; import type { VitNodeEditorConfig } from "./components/editor-provider"; +import type { VitNodeLogo } from "./components/logo-context"; import type { ThemeProviderProps } from "./components/theme-provider"; import type { DefaultTemplateEmailProps } from "./emails/default-template"; import type { @@ -34,6 +35,7 @@ export interface VitNodeConfig< /** Editor settings shared by every `Editor` in the app. */ editor?: VitNodeEditorConfig; i18n: VitNodeI18nConfig; + logo?: VitNodeLogo; metadata: VitNodeMetadata; plugins: BuildPluginReturn[]; theme?: Omit< From 124fd2c8f2ff30d432df1fbef26a97c826bb591b Mon Sep 17 00:00:00 2001 From: aXenDeveloper Date: Wed, 30 Sep 2026 18:55:44 +0200 Subject: [PATCH 2/4] =?UTF-8?q?refactor(i18n):=20=F0=9F=94=A7=20streamline?= =?UTF-8?q?=20i18n=20scripts=20and=20commands=20for=20better=20organizatio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 10 ++--- .../copy-of-vitnode-app/monorepo/turbo.json | 15 ------- .../src/create/create-package-json.ts | 44 +++++++++++++------ turbo.json | 15 ------- 4 files changed, 36 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index ea52eb7fd..14f8cfe40 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,11 @@ "lint:fix": "turbo lint:fix", "test": "turbo test", "test:types": "turbo test:types", - "i18n:create": "turbo i18n:create", - "i18n:check": "turbo i18n:check", - "i18n:delete": "turbo i18n:delete", - "i18n:update": "turbo i18n:update", - "i18n:update:ai": "turbo i18n:update:ai" + "i18n:create": "cd apps/api && pnpm run i18n:create", + "i18n:check": "cd apps/api && pnpm run i18n:check", + "i18n:delete": "cd apps/api && pnpm run i18n:delete", + "i18n:update": "cd apps/api && pnpm run i18n:update", + "i18n:update:ai": "cd apps/api && pnpm run i18n:update:ai" }, "devDependencies": { "@types/node": "^26.5.1", diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/turbo.json b/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/turbo.json index 37e74b7a1..aea6585f0 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/turbo.json +++ b/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/turbo.json @@ -38,21 +38,6 @@ }, "lint:fix": { "dependsOn": ["^lint:fix"] - }, - "i18n:create": { - "dependsOn": ["^i18n:create"] - }, - "i18n:check": { - "dependsOn": ["^i18n:check"] - }, - "i18n:delete": { - "dependsOn": ["^i18n:delete"] - }, - "i18n:update": { - "dependsOn": ["^i18n:update"] - }, - "i18n:update:ai": { - "dependsOn": ["^i18n:update:ai"] } } } diff --git a/packages/create-vitnode-app/src/create/create-package-json.ts b/packages/create-vitnode-app/src/create/create-package-json.ts index 687944123..bd6627bdf 100644 --- a/packages/create-vitnode-app/src/create/create-package-json.ts +++ b/packages/create-vitnode-app/src/create/create-package-json.ts @@ -27,13 +27,27 @@ const eslintScripts = { lint: "eslint .", "lint:fix": "eslint . --fix", }; -const i18nScripts = { - "i18n:create": "vitnode i18n:create", - "i18n:check": "vitnode i18n:check", - "i18n:delete": "vitnode i18n:delete", - "i18n:update": "vitnode i18n:update", - "i18n:update:ai": "vitnode i18n:update:ai", -}; +const i18nCommands = [ + "i18n:create", + "i18n:check", + "i18n:delete", + "i18n:update", + "i18n:update:ai", +] as const; +const i18nScripts = Object.fromEntries( + i18nCommands.map(command => [command, `vitnode ${command}`]), +); + +const runScript = (pm: string, script: string) => + pm === "npm" ? `npm run ${script} --` : `${pm} run ${script}`; + +const i18nRootScripts = (pm: string, appDir: string) => + Object.fromEntries( + i18nCommands.map(command => [ + command, + `cd ${appDir} && ${runScript(pm, command)}`, + ]), + ); const dockerDevScript = (appName: string) => `docker compose -f ./docker-compose.yml -p ${appName}-vitnode-dev up -d`; @@ -57,6 +71,8 @@ export const rootScripts = ( enableEslint: boolean, enableDocker: boolean, appName: string, + pm: string, + i18nAppDir: string, ) => ({ "db:migrate": "turbo db:migrate", "db:prepare": "turbo db:prepare", @@ -64,11 +80,7 @@ export const rootScripts = ( dev: "turbo build:plugins && turbo db:prepare && turbo dev", build: "turbo build", start: "turbo start", - "i18n:create": "turbo i18n:create", - "i18n:check": "turbo i18n:check", - "i18n:delete": "turbo i18n:delete", - "i18n:update": "turbo i18n:update", - "i18n:update:ai": "turbo i18n:update:ai", + ...i18nRootScripts(pm, i18nAppDir), ...withIf(enableEslint, { lint: "turbo lint", "lint:fix": "turbo lint:fix", @@ -328,7 +340,13 @@ export const createPackageJSON = async ({ const rootPkg: PackageJSON = { name: appName, private: true, - scripts: rootScripts(eslint, !!docker, appName), + scripts: rootScripts( + eslint, + !!docker, + appName, + packageManager, + isSingleApp ? "apps/web" : "apps/api", + ), devDependencies: { ...rootDevDeps(eslint), "@vitnode/config": vitnodeVersionRange, diff --git a/turbo.json b/turbo.json index 40f38b8c8..9a0f82827 100644 --- a/turbo.json +++ b/turbo.json @@ -66,21 +66,6 @@ }, "start": { "dependsOn": ["^start"] - }, - "i18n:create": { - "dependsOn": ["^i18n:create"] - }, - "i18n:check": { - "dependsOn": ["^i18n:check"] - }, - "i18n:delete": { - "dependsOn": ["^i18n:delete"] - }, - "i18n:update": { - "dependsOn": ["^i18n:update"] - }, - "i18n:update:ai": { - "dependsOn": ["^i18n:update:ai"] } } } From c5ae26e67b921eb7736fbaeac214f1a232426be8 Mon Sep 17 00:00:00 2001 From: aXenDeveloper Date: Wed, 30 Sep 2026 19:18:18 +0200 Subject: [PATCH 3/4] =?UTF-8?q?refactor(form):=20=E2=9C=A8=20enhance=20for?= =?UTF-8?q?m=20components=20with=20validation=20and=20input=20groups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/form/auto-form.test.tsx | 81 ++++++++++++++++++- .../vitnode/src/components/form/auto-form.tsx | 3 + .../vitnode/src/components/ui/input-group.tsx | 4 +- .../form/password-reset-form-content.test.tsx | 36 +++++++++ .../form/password-reset-form-content.tsx | 14 +++- .../views/auth/password-reset/form/schema.ts | 9 ++- .../form/use-password-reset-form.ts | 8 +- .../src/views/auth/remembered-email.ts | 7 ++ .../sign-in/form/sign-in-form-content.tsx | 46 +++++++++-- .../sign-up/components/password-input.tsx | 65 ++++++++------- .../sign-up/form/sign-up-form-content.tsx | 21 ++++- 11 files changed, 244 insertions(+), 50 deletions(-) create mode 100644 packages/vitnode/src/views/auth/password-reset/form/password-reset-form-content.test.tsx create mode 100644 packages/vitnode/src/views/auth/remembered-email.ts diff --git a/packages/vitnode/src/components/form/auto-form.test.tsx b/packages/vitnode/src/components/form/auto-form.test.tsx index 5b3e42477..7311970cc 100644 --- a/packages/vitnode/src/components/form/auto-form.test.tsx +++ b/packages/vitnode/src/components/form/auto-form.test.tsx @@ -1,10 +1,11 @@ -import { fireEvent, render, screen } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import React from "react"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import z from "zod"; import type { ItemAutoFormComponentProps } from "./auto-form"; +import { FormMessage } from "../ui/form"; import { AutoForm } from "./auto-form"; const LabelContext = React.createContext("first label"); @@ -62,3 +63,79 @@ describe("a field component that reads context and keeps its own state", () => { expect(renamed.id).toBe(input.id); }); }); + +const requiredSchema = z.object({ + title: z.string().min(1, { message: "title is required" }).default(""), +}); + +const RequiredTitleField = ({ field }: ItemAutoFormComponentProps) => ( + <> + + + +); + +describe("a form that can be submitted while invalid", () => { + it("keeps the submit button disabled by default", () => { + render( + , + ); + + expect( + screen.getByRole("button", { name: "core.global.submit" }), + ).toHaveProperty("disabled", true); + }); + + it("reveals the errors on click without calling onSubmit", async () => { + const onSubmit = vi.fn(); + render( + , + ); + const submit = screen.getByRole("button", { name: "core.global.submit" }); + expect(screen.queryByText("title is required")).toBeNull(); + + expect(submit).toHaveProperty("disabled", false); + fireEvent.click(submit); + + expect(await screen.findByText("title is required")).toBeDefined(); + expect(onSubmit).not.toHaveBeenCalled(); + }); + + it("submits once the values are valid", async () => { + const onSubmit = vi.fn(); + render( + , + ); + + fireEvent.change(screen.getByLabelText("title"), { + target: { value: "Hello" }, + }); + fireEvent.click(screen.getByRole("button", { name: "core.global.submit" })); + + await waitFor(() => { + expect(onSubmit).toHaveBeenCalledWith( + { title: "Hello" }, + expect.anything(), + expect.anything(), + ); + }); + }); +}); diff --git a/packages/vitnode/src/components/form/auto-form.tsx b/packages/vitnode/src/components/form/auto-form.tsx index 8336ca310..aca679cfe 100644 --- a/packages/vitnode/src/components/form/auto-form.tsx +++ b/packages/vitnode/src/components/form/auto-form.tsx @@ -184,6 +184,7 @@ const emptySubmitMeta: FormSubmitMeta = {}; export function AutoForm>({ formSchema, mode, + canSubmitWhenInvalid, onSubmit: onSubmitProp, captcha, fields, @@ -193,6 +194,7 @@ export function AutoForm>({ children, ...props }: Omit, "onSubmit"> & { + canSubmitWhenInvalid?: boolean; captcha?: z.infer["captcha"]; fields: ItemAutoFormProps[]; formSchema: T; @@ -216,6 +218,7 @@ export function AutoForm>({ const inputParams = getZodInputParams(jsonSchema); const validator: AutoFormValidator = formSchema; const form = useForm({ + canSubmitWhenInvalid, defaultValues: getDefaults(jsonSchema), onSubmit: async ({ formApi, meta, value }) => { const parsedValues = formSchema.safeParse(value); diff --git a/packages/vitnode/src/components/ui/input-group.tsx b/packages/vitnode/src/components/ui/input-group.tsx index 1148a42e7..0fc1b6228 100644 --- a/packages/vitnode/src/components/ui/input-group.tsx +++ b/packages/vitnode/src/components/ui/input-group.tsx @@ -10,7 +10,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) { return (
[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pe-1.5 has-[>[data-align=inline-start]]:[&>input]:ps-1.5", + "group/input-group border-input has-[[data-slot=input-group-control]:autofill]:bg-primary/5 dark:has-[[data-slot=input-group-control]:autofill]:bg-primary/10 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 dark:bg-input/30 bg-card relative flex h-9 w-full min-w-0 items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-3 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pe-1.5 has-[>[data-align=inline-start]]:[&>input]:ps-1.5", className, )} data-slot="input-group" @@ -121,7 +121,7 @@ function InputGroupInput({ return ( + render( + , + ); + +describe("the reset-request form", () => { + afterEach(() => { + rememberEmail(""); + }); + + it("starts empty when the visitor typed no email on the sign-in form", () => { + renderForm(); + + expect( + screen.getByLabelText("core.auth.sign_up.email.label") + .value, + ).toBe(""); + }); + + it("is filled with the email the visitor typed on the sign-in form", () => { + rememberEmail(" test@test.com "); + + renderForm(); + + expect( + screen.getByLabelText("core.auth.sign_up.email.label") + .value, + ).toBe("test@test.com"); + }); +}); diff --git a/packages/vitnode/src/views/auth/password-reset/form/password-reset-form-content.tsx b/packages/vitnode/src/views/auth/password-reset/form/password-reset-form-content.tsx index fc9935cdb..99ec2232e 100644 --- a/packages/vitnode/src/views/auth/password-reset/form/password-reset-form-content.tsx +++ b/packages/vitnode/src/views/auth/password-reset/form/password-reset-form-content.tsx @@ -1,6 +1,6 @@ import type z from "zod"; -import { MailCheckIcon } from "lucide-react"; +import { MailCheckIcon, MailIcon } from "lucide-react"; import { useTranslations } from "use-intl"; import type { routeMiddlewareSchema } from "@/api/modules/middleware/route"; @@ -15,6 +15,7 @@ import { CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; +import { InputGroupAddon } from "@/components/ui/input-group"; import { Label } from "@/components/ui/label"; import { @@ -87,7 +88,16 @@ export const PasswordResetFormContent = ({ { id: "email", component: props => ( - + + + + + ), }, ]} diff --git a/packages/vitnode/src/views/auth/password-reset/form/schema.ts b/packages/vitnode/src/views/auth/password-reset/form/schema.ts index 5bdf9244b..73cc2ab55 100644 --- a/packages/vitnode/src/views/auth/password-reset/form/schema.ts +++ b/packages/vitnode/src/views/auth/password-reset/form/schema.ts @@ -5,11 +5,12 @@ export interface PasswordResetFormMessages { invalidEmail: string; } -export const createPasswordResetFormSchema = ({ - invalidEmail, -}: PasswordResetFormMessages) => +export const createPasswordResetFormSchema = ( + { invalidEmail }: PasswordResetFormMessages, + { defaultEmail = "" }: { defaultEmail?: string } = {}, +) => z.object({ - email: z.email({ message: invalidEmail }).default(""), + email: z.email({ message: invalidEmail }).default(defaultEmail), }); export type PasswordResetFormSchema = ReturnType< diff --git a/packages/vitnode/src/views/auth/password-reset/form/use-password-reset-form.ts b/packages/vitnode/src/views/auth/password-reset/form/use-password-reset-form.ts index 159d28078..bf893120a 100644 --- a/packages/vitnode/src/views/auth/password-reset/form/use-password-reset-form.ts +++ b/packages/vitnode/src/views/auth/password-reset/form/use-password-reset-form.ts @@ -10,6 +10,7 @@ import type { PasswordResetSubmitValues, } from "./schema"; +import { readRememberedEmail } from "../../remembered-email"; import { createPasswordResetFormSchema, passwordResetFormOutcome, @@ -30,9 +31,10 @@ export const usePasswordResetForm = ({ const tErrors = useTranslations("core.global.errors"); const [sentEmail, setSentEmail] = React.useState(""); - const formSchema = createPasswordResetFormSchema({ - invalidEmail: t("email.invalid"), - }); + const formSchema = createPasswordResetFormSchema( + { invalidEmail: t("email.invalid") }, + { defaultEmail: readRememberedEmail() }, + ); const onSubmit: AutoFormOnSubmit = async ( { email }, diff --git a/packages/vitnode/src/views/auth/remembered-email.ts b/packages/vitnode/src/views/auth/remembered-email.ts new file mode 100644 index 000000000..4a43db2c9 --- /dev/null +++ b/packages/vitnode/src/views/auth/remembered-email.ts @@ -0,0 +1,7 @@ +let rememberedEmail = ""; + +export const rememberEmail = (email: unknown) => { + rememberedEmail = typeof email === "string" ? email.trim() : ""; +}; + +export const readRememberedEmail = () => rememberedEmail; diff --git a/packages/vitnode/src/views/auth/sign-in/form/sign-in-form-content.tsx b/packages/vitnode/src/views/auth/sign-in/form/sign-in-form-content.tsx index abe19af99..e891cc04b 100644 --- a/packages/vitnode/src/views/auth/sign-in/form/sign-in-form-content.tsx +++ b/packages/vitnode/src/views/auth/sign-in/form/sign-in-form-content.tsx @@ -1,10 +1,13 @@ import { Link } from "@tanstack/react-router"; +import { LockIcon, MailIcon } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { useTranslations } from "use-intl"; import { AutoForm } from "@/components/form/auto-form"; import { AutoFormInput } from "@/components/form/fields/input"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { useFormApi } from "@/components/ui/form"; +import { InputGroupAddon } from "@/components/ui/input-group"; import { Skeleton } from "@/components/ui/skeleton"; import { REVEAL_EXIT_TRANSITION, @@ -16,10 +19,28 @@ import { } from "@/lib/motion"; import { AUTH_HREF } from "../../auth-link"; +import { rememberEmail } from "../../remembered-email"; import { type SignInSubmit, useSignInForm } from "./use-sign-in-form"; export type { SignInSubmit }; +const ResetPasswordLink = ({ href }: { href: string }) => { + const t = useTranslations("core.auth.sign_in"); + const { form } = useFormApi(); + + return ( + { + rememberEmail(form.getFieldValue("email")); + }} + to={href} + > + {t("password.reset")} + + ); +}; + export const SignInFormContent = ({ onSignIn, resetPasswordHref = AUTH_HREF.resetPassword, @@ -73,27 +94,36 @@ export const SignInFormContent = ({ { id: "email", component: props => ( - + + + + + ), }, { id: "password", component: props => ( - {t("password.reset")} - + ) : undefined } type="password" {...props} - /> + > + + + + ), }, ]} diff --git a/packages/vitnode/src/views/auth/sign-up/components/password-input.tsx b/packages/vitnode/src/views/auth/sign-up/components/password-input.tsx index 56c427082..752e15e25 100644 --- a/packages/vitnode/src/views/auth/sign-up/components/password-input.tsx +++ b/packages/vitnode/src/views/auth/sign-up/components/password-input.tsx @@ -1,12 +1,17 @@ -import { CheckIcon, XIcon } from "lucide-react"; +import { CheckIcon, LockIcon, XIcon } from "lucide-react"; import React from "react"; import { useTranslations } from "use-intl"; import type { ItemAutoFormComponentProps } from "@/components/form/auto-form"; +import type { Input } from "@/components/ui/input"; import { AutoFormLabel } from "@/components/form/common/label"; import { FormControl, FormMessage } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; +import { + InputGroup, + InputGroupAddon, + InputGroupInput, +} from "@/components/ui/input-group"; import { Tooltip, TooltipContent, @@ -54,31 +59,37 @@ export const PasswordInput = ({ - - { - setOpenTooltip(false); - field.onBlur(); - props.onBlur?.(e); - }} - onChange={e => { - setOpenTooltip(true); - field.onChange(e); - props.onChange?.(e); - }} - pattern={pattern ?? props.pattern} - value={field.value ?? ""} - {...props} - /> - - } - /> + + + { + setOpenTooltip(false); + field.onBlur(); + props.onBlur?.(e); + }} + onChange={e => { + setOpenTooltip(true); + field.onChange(e); + props.onChange?.(e); + }} + pattern={pattern ?? props.pattern} + value={field.value ?? ""} + {...props} + /> + + } + /> + + + + + > + + + + {value.length >= 3 && (
{t.rich("username.your_user_code", { @@ -68,7 +76,16 @@ export const SignUpFormContent = ({ { id: "email", component: props => ( - + + + + + ), }, { From 147cfce9d2143f02a9974cd54d7c37f272b77a19 Mon Sep 17 00:00:00 2001 From: aXenDeveloper Date: Wed, 30 Sep 2026 20:53:09 +0200 Subject: [PATCH 4/4] =?UTF-8?q?refactor(nav):=20=E2=9C=A8=20enhance=20navi?= =?UTF-8?q?gation=20components=20with=20drawer=20support=20and=20improved?= =?UTF-8?q?=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vitnode/src/tanstack/settings/layout.tsx | 1 - .../views/auth/settings/nav-content.test.tsx | 59 ++++++++ .../src/views/auth/settings/nav-content.tsx | 132 ++++++++++++++---- .../src/views/auth/settings/settings-nav.ts | 3 - .../src/views/auth/settings/shell-content.tsx | 38 +---- 5 files changed, 166 insertions(+), 67 deletions(-) create mode 100644 packages/vitnode/src/views/auth/settings/nav-content.test.tsx diff --git a/packages/vitnode/src/tanstack/settings/layout.tsx b/packages/vitnode/src/tanstack/settings/layout.tsx index b408ceb19..80a56d268 100644 --- a/packages/vitnode/src/tanstack/settings/layout.tsx +++ b/packages/vitnode/src/tanstack/settings/layout.tsx @@ -33,7 +33,6 @@ export const SettingsLayoutContent = ({ pathname={pathname} /> } - pathname={pathname} > {children} diff --git a/packages/vitnode/src/views/auth/settings/nav-content.test.tsx b/packages/vitnode/src/views/auth/settings/nav-content.test.tsx new file mode 100644 index 000000000..a363013dc --- /dev/null +++ b/packages/vitnode/src/views/auth/settings/nav-content.test.tsx @@ -0,0 +1,59 @@ +import { + createMemoryHistory, + createRootRoute, + createRoute, + createRouter, + Outlet, + RouterProvider, +} from "@tanstack/react-router"; +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { SettingsNavContent } from "./nav-content"; + +const mount = async (pathname: string) => { + const rootRoute = createRootRoute({ + component: () => ( + <> + + + + ), + }); + const settingsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/settings", + }); + const router = createRouter({ + history: createMemoryHistory({ initialEntries: [pathname] }), + routeTree: rootRoute.addChildren([ + settingsRoute.addChildren([ + createRoute({ getParentRoute: () => settingsRoute, path: "/devices" }), + createRoute({ getParentRoute: () => settingsRoute, path: "/security" }), + ]), + ]), + }); + + render(); + await screen.findByRole("navigation"); +}; + +const currentLinks = () => + screen + .getAllByRole("link") + .filter(link => link.getAttribute("aria-current") === "page") + .map(link => link.textContent); + +describe("SettingsNavContent", () => { + it("marks only the sub-panel as the current page, not the settings root", async () => { + await mount("/settings/devices"); + + expect(currentLinks()).toEqual(["core.auth.settings.nav.devices"]); + }); + + it("marks the overview as the current page on the settings root", async () => { + await mount("/settings"); + + expect(currentLinks()).toEqual(["core.auth.settings.nav.overview"]); + }); +}); diff --git a/packages/vitnode/src/views/auth/settings/nav-content.tsx b/packages/vitnode/src/views/auth/settings/nav-content.tsx index bab294433..5e2d10fe8 100644 --- a/packages/vitnode/src/views/auth/settings/nav-content.tsx +++ b/packages/vitnode/src/views/auth/settings/nav-content.tsx @@ -1,21 +1,27 @@ import { Link } from "@tanstack/react-router"; import { cn } from "cn"; import { - ChevronRightIcon, KeyRoundIcon, + MenuIcon, MonitorSmartphoneIcon, UserRoundIcon, + XIcon, } from "lucide-react"; +import React from "react"; import { useTranslations } from "use-intl"; +import { Button } from "@/components/ui/button"; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer"; + import type { SettingsNavItem, SettingsNavKey } from "./settings-nav"; -import { SETTINGS_INTERACTIVE_ROW } from "./settings-group"; -import { - isSettingsNavItemActive, - SETTINGS_NAV_ITEMS, - SETTINGS_ROOT_HREF, -} from "./settings-nav"; +import { isSettingsNavItemActive, SETTINGS_NAV_ITEMS } from "./settings-nav"; const ICONS: Record< SettingsNavKey, @@ -26,48 +32,53 @@ const ICONS: Record< security: KeyRoundIcon, }; -export const SettingsNavContent = ({ - items = SETTINGS_NAV_ITEMS, +const SettingsNavList = ({ + items, + onNavigate, pathname, }: { - items?: readonly SettingsNavItem[]; + items: readonly SettingsNavItem[]; + onNavigate?: () => void; pathname: string; }) => { const t = useTranslations("core.auth.settings"); const tNav = useTranslations("core.auth.settings.nav"); + const activeIndex = items.findIndex(item => + isSettingsNavItemActive(item, pathname), + ); return ( ); }; + +const SettingsNavDrawer = ({ + items, + pathname, +}: { + items: readonly SettingsNavItem[]; + pathname: string; +}) => { + const t = useTranslations("core.auth.settings"); + const tGlobal = useTranslations("core.global"); + const [isOpen, setIsOpen] = React.useState(false); + + return ( + + + + + + +
+ + {t("title")} + + + + +
+ +
+ { + setIsOpen(false); + }} + pathname={pathname} + /> +
+
+
+ ); +}; + +export const SettingsNavContent = ({ + items = SETTINGS_NAV_ITEMS, + pathname, +}: { + items?: readonly SettingsNavItem[]; + pathname: string; +}) => ( + <> +
+ +
+
+ +
+ +); diff --git a/packages/vitnode/src/views/auth/settings/settings-nav.ts b/packages/vitnode/src/views/auth/settings/settings-nav.ts index 4138abf81..b315c7f9a 100644 --- a/packages/vitnode/src/views/auth/settings/settings-nav.ts +++ b/packages/vitnode/src/views/auth/settings/settings-nav.ts @@ -30,9 +30,6 @@ export const isSettingsNavItemActive = ( pathname: string, ): boolean => normalizeUrl(item.href) === normalizeUrl(pathname); -export const isSettingsRootPath = (pathname: string): boolean => - normalizeUrl(pathname) === normalizeUrl(SETTINGS_ROOT_HREF); - export const activeSettingsNavKey = ( pathname: string, ): SettingsNavKey | undefined => diff --git a/packages/vitnode/src/views/auth/settings/shell-content.tsx b/packages/vitnode/src/views/auth/settings/shell-content.tsx index 445b2ac64..0b12236b0 100644 --- a/packages/vitnode/src/views/auth/settings/shell-content.tsx +++ b/packages/vitnode/src/views/auth/settings/shell-content.tsx @@ -1,10 +1,5 @@ -import { Link, useRouter } from "@tanstack/react-router"; -import { cn } from "cn"; -import { ChevronLeftIcon } from "lucide-react"; +import { useRouter } from "@tanstack/react-router"; import React from "react"; -import { useTranslations } from "use-intl"; - -import { isSettingsRootPath, SETTINGS_ROOT_HREF } from "./settings-nav"; const isFocusVisible = (element: Element | null): boolean => element instanceof HTMLElement && @@ -16,16 +11,12 @@ export const SettingsShellContent = ({ footer, header, nav, - pathname, }: { children: React.ReactNode; footer?: React.ReactNode; header?: React.ReactNode; nav: React.ReactNode; - pathname: string; }) => { - const t = useTranslations("core.auth.settings"); - const isRoot = isSettingsRootPath(pathname); const frameRef = React.useRef(null); const router = useRouter(); @@ -39,7 +30,7 @@ export const SettingsShellContent = ({ ); return ( -
+
{header}
- + -
- {isRoot ? null : ( - -
+
{children}
{footer}