moving
This commit is contained in:
@@ -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(.*)',
|
||||
|
||||
@@ -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'));
|
||||
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 ?? {});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
12
apps/www/eslint.config.mjs
Normal file
12
apps/www/eslint.config.mjs
Normal 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',
|
||||
]),
|
||||
]);
|
||||
3
apps/www/next-env.d.ts
vendored
3
apps/www/next-env.d.ts
vendored
@@ -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.
|
||||
|
||||
@@ -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
25
apps/www/proxy.ts
Normal 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)(.*)',
|
||||
],
|
||||
};
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user