This commit is contained in:
Oleg Maslov
2026-09-02 10:14:22 +02:00
parent 0c3e2ead3b
commit b20b138fe4
771 changed files with 161561 additions and 9027 deletions

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest';
import { config } from '../middleware';
import { config } from '../proxy';
describe('www Clerk middleware boundary', () => {
describe('www Clerk proxy boundary', () => {
it('protects only identity and server-owned flows', () => {
expect(config.matcher).toEqual([
'/account(.*)',

View File

@@ -1,6 +1,6 @@
'use client';
import { useEffect, useState, type CSSProperties, type ReactNode } from 'react';
import { useSyncExternalStore, type CSSProperties, type ReactNode } from 'react';
import { useTranslations } from 'next-intl';
import { detectOSFromUserAgent, type OSId } from '../_lib/os-detection';
import { emit, events } from '../_lib/event-taxonomy';
@@ -15,6 +15,10 @@ interface DownloadCTAProps {
const DOWNLOAD_URL = '/download';
const subscribeToClientEnvironment = () => () => {};
const getClientOS = () => detectOSFromUserAgent(navigator.userAgent);
const getServerOS = (): null => null;
/**
* OS-aware download CTA. Renders a generic "Download" label at SSR + first
* paint, then swaps to "Download for {os}" after hydration via
@@ -33,13 +37,11 @@ export default function DownloadCTA({
style,
}: DownloadCTAProps) {
const t = useTranslations('landing.download_cta');
const [os, setOS] = useState<OSId | null>(null);
useEffect(() => {
if (typeof navigator !== 'undefined') {
setOS(detectOSFromUserAgent(navigator.userAgent));
}
}, []);
const os = useSyncExternalStore<OSId | null>(
subscribeToClientEnvironment,
getClientOS,
getServerOS,
);
const label = children ?? (os ? t('with_os', { os }) : t('default'));

View File

@@ -2,6 +2,7 @@
import { useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { SignInButton, UserButton, Show } from '@clerk/nextjs';
import BrandMark from './BrandMark';
import DownloadCTA from './DownloadCTA';
@@ -47,9 +48,9 @@ export default function Navbar() {
return (
<header className={headerClass}>
<div className={styles.inner}>
<a href="/" className={styles.brand} aria-label={t('aria.home')}>
<Link href="/" className={styles.brand} aria-label={t('aria.home')}>
<BrandMark withWordmark />
</a>
</Link>
<nav className={styles.nav} aria-label={t('aria.primary')}>
{NAV_ITEMS.map((item) => (

View File

@@ -1,6 +1,6 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useState, useSyncExternalStore } from 'react';
import { useTranslations } from 'next-intl';
import DownloadCTA from './DownloadCTA';
import { emit, events } from '../_lib/event-taxonomy';
@@ -59,15 +59,19 @@ const STRIPE_ENDPOINT =
const KVARK_URL = 'https://www.kvark.ai';
const subscribeToLocation = () => () => {};
const getCheckoutCancelled = () =>
new URLSearchParams(window.location.search).get('checkout') === 'cancelled';
const getServerCheckoutCancelled = () => false;
export default function Pricing() {
const t = useTranslations('landing.pricing');
const [billing, setBilling] = useState<BillingPeriod>('monthly');
const [checkoutCancelled, setCheckoutCancelled] = useState(false);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
setCheckoutCancelled(params.get('checkout') === 'cancelled');
}, []);
const checkoutCancelled = useSyncExternalStore(
subscribeToLocation,
getCheckoutCancelled,
getServerCheckoutCancelled,
);
const handleBillingChange = useCallback((mode: BillingPeriod) => {
setBilling(mode);

View File

@@ -41,8 +41,8 @@ export default function Reveal({
const node = nodeRef.current;
if (!node) return;
if (typeof IntersectionObserver === 'undefined') {
setVisible(true);
return;
const fallback = window.setTimeout(() => setVisible(true), 0);
return () => window.clearTimeout(fallback);
}
const observer = new IntersectionObserver(
(entries) => {

View File

@@ -19,6 +19,5 @@ export const events = {
export function emit(event: LandingEvent): void {
if (process.env.NODE_ENV === 'production') return;
// eslint-disable-next-line no-console
console.info('[landing.event]', event.name, event.properties ?? {});
}

View File

@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import type { CSSProperties } from 'react';
import type { Metadata } from 'next';
import Link from 'next/link';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import BrandMark from '../../_components/BrandMark';
@@ -41,11 +42,11 @@ export default function MethodologyPage() {
return (
<main style={pageStyle}>
<header style={headerStyle}>
<a href="/" style={brandLinkStyle}>
<Link href="/" style={brandLinkStyle}>
<BrandMark withWordmark />
<span style={separatorStyle}>·</span>
<span style={crumbStyle}>Methodology</span>
</a>
</Link>
</header>
<article style={articleStyle} className="methodology-prose">
@@ -53,9 +54,9 @@ export default function MethodologyPage() {
</article>
<footer style={footerStyle}>
<a href="/" style={backLinkStyle}>
<Link href="/" style={backLinkStyle}>
Back to Waggle
</a>
</Link>
</footer>
<style>{scopedCss}</style>

View File

@@ -218,7 +218,6 @@ export default function RootLayout({ children }: { children: ReactNode }) {
<meta name="twitter:image" content={META_OG_IMAGE} />
<script
type="application/ld+json"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
// Static compile-time object (no user input); escape `<` per the
// standard JSON-LD embedding guidance to rule out </script> breaks.

View File

@@ -0,0 +1,12 @@
import { defineConfig, globalIgnores } from 'eslint/config';
import nextVitals from 'eslint-config-next/core-web-vitals';
export default defineConfig([
...nextVitals,
globalIgnores([
'.next/**',
'out/**',
'build/**',
'next-env.d.ts',
]),
]);

View File

@@ -1,6 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -7,7 +7,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint": "eslint .",
"test": "node --disable-warning=DEP0040 ../../node_modules/vitest/vitest.mjs run",
"test:watch": "vitest"
},
@@ -15,20 +15,21 @@
"@clerk/nextjs": "^7.3.0",
"@clerk/themes": "^2.4.57",
"lucide-react": "^0.577.0",
"next": "^15.1.0",
"next-intl": "^3.26.0",
"next": "16.3.0",
"next-intl": "^4.13.4",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-markdown": "^9.0.1",
"remark-gfm": "^4.0.0",
"sharp": "0.35.3",
"stripe": "^21.0.1"
},
"devDependencies": {
"@types/node": "^22.10.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^6.0.1",
"eslint-config-next": "^15.1.0",
"@vitejs/plugin-react": "5.1.1",
"eslint-config-next": "^16.3.0",
"typescript": "^5.9.3"
}
}

25
apps/www/proxy.ts Normal file
View File

@@ -0,0 +1,25 @@
// Next.js 16 convention: proxy.ts replaces the deprecated middleware.ts name.
import { clerkMiddleware } from '@clerk/nextjs/server';
/**
* Clerk auth proxy.
*
* Default behavior: all routes are PUBLIC. Per-route protection is enforced
* with `await auth.protect()` inside Server Components / Route Handlers.
* Sign-up gating for Pro/Teams checkout is handled at CTA level via
* <SignUpButton mode="modal"> (see §5.4).
*/
export default clerkMiddleware();
export const config = {
matcher: [
// Keep public acquisition, legal, documentation, and download pages
// independent of Clerk session refreshes. Auth is required only where the
// route reads identity or owns an authenticated flow.
'/account(.*)',
'/sign-in(.*)',
'/sign-up(.*)',
'/(api|trpc)(.*)',
],
};

View File

@@ -4,7 +4,7 @@
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "preserve",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
@@ -25,7 +25,8 @@
"app/**/*.tsx",
"__tests__/**/*.ts",
"__tests__/**/*.tsx",
".next/types/**/*.ts"
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules", ".next"]
}