diff --git a/packages/docs/package.json b/packages/docs/package.json index d2c2bb699..33bae40b6 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -24,6 +24,7 @@ "@headlessui/react": "2.2.0", "@headlessui/tailwindcss": "^0.2.1", "@icons-pack/react-simple-icons": "^11.2.0", + "@number-flow/react": "^0.5.5", "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-select": "^2.1.5", diff --git a/packages/docs/src/app/(pages)/stats/page.tsx b/packages/docs/src/app/(pages)/stats/page.tsx index f0bae498a..a9e3d8007 100644 --- a/packages/docs/src/app/(pages)/stats/page.tsx +++ b/packages/docs/src/app/(pages)/stats/page.tsx @@ -1,5 +1,4 @@ import { Card } from '@tremor/react' -import Image from 'next/image' import { SearchParams } from 'nuqs/server' import { Suspense } from 'react' import { NPMDownloads, NPMStats } from './_components/downloads' @@ -33,7 +32,7 @@ export default async function StatsPage({ searchParams }: StatsPageProps) { - Project analytics and stats + 🎉 + + Announcing nuqs version 2 + + 🎉 + + ) +} + +export function NuqsV2AnnouncementSidebarBanner() { + return ( +
+ 🎉 + + Announcing nuqs v2 ! + + 🎉 +
+ ) +} + +export function ReactParis2025SideBanner() { + return ( +
+

🗣️ nuqs will be featured at

+
+ +

+ + React + {' '} + + Paris + {' '} + '25 +
+ + Get your ticket now! + +

+
+ + + +

+ Use the code Francois_Paris for a 20% discount on your + ticket. +

+
+ ) +} diff --git a/packages/docs/src/app/docs/layout.tsx b/packages/docs/src/app/docs/layout.tsx index 77be63d04..66650f723 100644 --- a/packages/docs/src/app/docs/layout.tsx +++ b/packages/docs/src/app/docs/layout.tsx @@ -1,8 +1,8 @@ import { source } from '@/src/app/source' import { getSharedLayoutProps } from '@/src/components/shared-layout' import { DocsLayout } from 'fumadocs-ui/layouts/docs' -import Link from 'next/link' import { Suspense, type ReactNode } from 'react' +import { ReactParis2025SideBanner } from '../banners' export default function RootDocsLayout({ children }: { children: ReactNode }) { return ( @@ -11,19 +11,7 @@ export default function RootDocsLayout({ children }: { children: ReactNode }) { {...getSharedLayoutProps()} sidebar={{ collapsible: false, - banner: ( -
- 🎉 - - Announcing nuqs v2 ! - - 🎉 -
- ), + banner: , footer: ( diff --git a/packages/docs/src/app/globals.css b/packages/docs/src/app/globals.css index 6d5d4e6ff..a1bce33a3 100644 --- a/packages/docs/src/app/globals.css +++ b/packages/docs/src/app/globals.css @@ -129,3 +129,9 @@ fill: currentColor; } } + +@layer components { + number-flow-react::part(suffix) { + @apply ml-0.5 text-sm font-medium text-muted-foreground; + } +} diff --git a/packages/docs/src/app/layout.tsx b/packages/docs/src/app/layout.tsx index ae3b2a627..4265e2c6d 100644 --- a/packages/docs/src/app/layout.tsx +++ b/packages/docs/src/app/layout.tsx @@ -1,13 +1,12 @@ -import { Banner } from 'fumadocs-ui/components/banner' import { RootProvider } from 'fumadocs-ui/provider' import type { Metadata } from 'next' import { Inter } from 'next/font/google' -import Link from 'next/link' import Script from 'next/script' import { NuqsAdapter } from 'nuqs/adapters/next' import type { ReactNode } from 'react' import { ResponsiveHelper } from '../components/responsive-helpers' import { cn } from '../lib/utils' +import { NuqsV2AnnouncementTopBanner } from './banners' import './globals.css' const inter = Inter({ @@ -40,21 +39,7 @@ export default function Layout({ children }: { children: ReactNode }) { suppressHydrationWarning > - - 🎉 - - Announcing nuqs version 2 - - 🎉 - + {children} diff --git a/packages/docs/src/app/playground/layout.tsx b/packages/docs/src/app/playground/layout.tsx index 9fe0fcbdf..c259524d7 100644 --- a/packages/docs/src/app/playground/layout.tsx +++ b/packages/docs/src/app/playground/layout.tsx @@ -1,8 +1,8 @@ import { getSharedLayoutProps } from '@/src/components/shared-layout' import { DocsLayout } from 'fumadocs-ui/layouts/docs' import { DocsBody, DocsPage } from 'fumadocs-ui/page' -import Link from 'next/link' import React, { Suspense } from 'react' +import { ReactParis2025SideBanner } from '../banners' import { getPlaygroundTree } from './(demos)/demos' import { DebugControl } from './debug-control' @@ -25,19 +25,7 @@ export default function PlaygroundLayout({ {...getSharedLayoutProps()} sidebar={{ collapsible: false, - banner: ( -
- 🎉 - - Announcing nuqs v2 ! - - 🎉 -
- ), + banner: , footer: ( }> diff --git a/packages/docs/src/components/countdown.tsx b/packages/docs/src/components/countdown.tsx new file mode 100644 index 000000000..d82e255aa --- /dev/null +++ b/packages/docs/src/components/countdown.tsx @@ -0,0 +1,99 @@ +'use client' + +import NumberFlow, { NumberFlowGroup } from '@number-flow/react' +import { ComponentProps, ReactNode, useEffect, useState } from 'react' +import { cn } from '../lib/utils' + +type CountdownProps = ComponentProps<'div'> & { + targetDate: Date + expiredMessage?: ReactNode +} + +export function Countdown({ + targetDate, + expiredMessage = null, + className, + ...props +}: CountdownProps) { + const remaining = targetDate.getTime() - Date.now() + const [days, setDays] = useState(() => + Math.floor(remaining / 1000 / 60 / 60 / 24) + ) + const [hours, setHours] = useState( + () => Math.floor(remaining / 1000 / 60 / 60) % 24 + ) + const [minutes, setMinutes] = useState( + () => Math.floor(remaining / 1000 / 60) % 60 + ) + const [seconds, setSeconds] = useState(() => + Math.floor((remaining / 1000) % 60) + ) + + useEffect(() => { + const timer = setInterval(() => { + const remaining = targetDate.getTime() - Date.now() + setDays(Math.floor(remaining / 1000 / 60 / 60 / 24)) + setHours(Math.floor(remaining / 1000 / 60 / 60) % 24) + setMinutes(Math.floor(remaining / 1000 / 60) % 60) + setSeconds(Math.floor((remaining / 1000) % 60)) + if (remaining <= 0) { + clearInterval(timer) + } + }, 500) + + return () => clearInterval(timer) + }, [targetDate]) + + if (days <= 0 && hours <= 0 && minutes <= 0 && seconds <= 0) { + return expiredMessage + } + + return ( +
0 ? 'gap-1 text-xl' : 'text-3xl', + className + )} + title={targetDate.toISOString()} + {...props} + > + + {days > 0 && ( + + )} + 0 ? 'h' : undefined} + digits={{ 1: { max: 2 } }} + format={{ minimumIntegerDigits: 2 }} + /> + 0 ? undefined : ':'} + suffix={days > 0 ? 'm' : undefined} + digits={{ 1: { max: 5 } }} + format={{ minimumIntegerDigits: 2 }} + /> + 0 ? undefined : ':'} + suffix={days > 0 ? 's' : undefined} + digits={{ 1: { max: 5 } }} + format={{ minimumIntegerDigits: 2 }} + /> + +
+ ) +} diff --git a/packages/docs/src/components/react-paris.tsx b/packages/docs/src/components/react-paris.tsx new file mode 100644 index 000000000..7fd0ccd53 --- /dev/null +++ b/packages/docs/src/components/react-paris.tsx @@ -0,0 +1,45 @@ +import { ComponentProps } from 'react' + +export function ReactParisLogo(props: ComponentProps<'svg'>) { + return ( + + + + + + + + + + + ) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ce20bb7c..d48ea7eea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,9 @@ importers: '@icons-pack/react-simple-icons': specifier: ^11.2.0 version: 11.2.0(react@19.0.0) + '@number-flow/react': + specifier: ^0.5.5 + version: 0.5.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-checkbox': specifier: ^1.1.3 version: 1.1.3(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -554,7 +557,7 @@ importers: version: 26.0.0 next: specifier: 15.1.5 - version: 15.1.5(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-201e55d-20241215)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: catalog:react19 version: 19.0.0 @@ -1614,6 +1617,12 @@ packages: resolution: {integrity: sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + '@number-flow/react@0.5.5': + resolution: {integrity: sha512-Zdju5n0osxrb+7jbcpUJ9L2VJ2+9ptwjz5+A+2wq9Q32hs3PW/noPJjHtLTrtGINM9mEw76DcDg0ac/dx6j1aA==} + peerDependencies: + react: ^18 || ^19 + react-dom: ^18 || ^19 + '@octokit/auth-token@5.1.2': resolution: {integrity: sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==} engines: {node: '>= 18'} @@ -4213,6 +4222,9 @@ packages: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -5997,6 +6009,9 @@ packages: - which - write-file-atomic + number-flow@0.5.3: + resolution: {integrity: sha512-iLKyssImNWQmJ41rza9K7P5lHRZTyishi/9FarWPLQHYY2Ydtl6eiXINEjZ1fa8dHeY0O7+YOD+Py3ZsJddYkg==} + nwsapi@2.2.16: resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==} @@ -9049,6 +9064,13 @@ snapshots: dependencies: which: 3.0.1 + '@number-flow/react@0.5.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + esm-env: 1.2.2 + number-flow: 0.5.3 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@octokit/auth-token@5.1.2': {} '@octokit/core@6.1.3': @@ -12124,6 +12146,8 @@ snapshots: esrecurse: 4.3.0 estraverse: 4.3.0 + esm-env@1.2.2: {} + esprima@4.0.1: {} esrecurse@4.3.0: @@ -14294,6 +14318,32 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + next@15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@next/env': 15.1.5 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.15 + busboy: 1.6.0 + caniuse-lite: 1.0.30001697 + postcss: 8.4.31 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + styled-jsx: 5.1.6(@babel/core@7.26.7)(react@19.0.0) + optionalDependencies: + '@next/swc-darwin-arm64': 15.1.5 + '@next/swc-darwin-x64': 15.1.5 + '@next/swc-linux-arm64-gnu': 15.1.5 + '@next/swc-linux-arm64-musl': 15.1.5 + '@next/swc-linux-x64-gnu': 15.1.5 + '@next/swc-linux-x64-musl': 15.1.5 + '@next/swc-win32-arm64-msvc': 15.1.5 + '@next/swc-win32-x64-msvc': 15.1.5 + '@opentelemetry/api': 1.9.0 + sharp: 0.33.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + next@15.1.5(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-201e55d-20241215)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@next/env': 15.1.5 @@ -14304,7 +14354,7 @@ snapshots: postcss: 8.4.31 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - styled-jsx: 5.1.6(react@19.0.0) + styled-jsx: 5.1.6(@babel/core@7.26.7)(react@19.0.0) optionalDependencies: '@next/swc-darwin-arm64': 15.1.5 '@next/swc-darwin-x64': 15.1.5 @@ -14392,6 +14442,10 @@ snapshots: npm@10.9.2: {} + number-flow@0.5.3: + dependencies: + esm-env: 1.2.2 + nwsapi@2.2.16: {} object-assign@4.1.1: {} @@ -15734,10 +15788,12 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-jsx@5.1.6(react@19.0.0): + styled-jsx@5.1.6(@babel/core@7.26.7)(react@19.0.0): dependencies: client-only: 0.0.1 react: 19.0.0 + optionalDependencies: + '@babel/core': 7.26.7 sucrase@3.35.0: dependencies: