diff --git a/messages/en.json b/messages/en.json index ffb7600..033726b 100644 --- a/messages/en.json +++ b/messages/en.json @@ -103,7 +103,9 @@ "cardFundedFromPool": "Funded from the pool", "cardVerifiedAgo": "verified {ago} ago", "emptyTitle": "No projects found", - "emptySub": "There are no projects matching the \"{filter}\" category in the pool right now." + "emptySub": "There are no projects matching the \"{filter}\" category in the pool right now.", + "loadMore": "Show {count} more", + "showingCount": "Showing {shown} of {total} projects" }, "Deposit": { "stepAmount": "Amount", diff --git a/messages/fr.json b/messages/fr.json index 0ba1855..6aa9d54 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -103,7 +103,9 @@ "cardFundedFromPool": "Financé par le pool", "cardVerifiedAgo": "vérifié il y a {ago}", "emptyTitle": "Aucun projet trouvé", - "emptySub": "Il n'y a aucun projet correspondant à la catégorie \"{filter}\" dans le pool en ce moment." + "emptySub": "Il n'y a aucun projet correspondant à la catégorie \"{filter}\" dans le pool en ce moment.", + "loadMore": "Afficher {count} de plus", + "showingCount": "{shown} projets sur {total} affichés" }, "Deposit": { "stepAmount": "Montant", diff --git a/src/app/connect/page.tsx b/src/app/connect/page.tsx index 53dd55c..cdda198 100644 --- a/src/app/connect/page.tsx +++ b/src/app/connect/page.tsx @@ -1,18 +1,36 @@ 'use client' -import { useEffect } from 'react' -import { useRouter } from 'next/navigation' +import { Suspense, useEffect } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' import { Connect } from '../../screens/Connect' import { useWallet } from '../../wallet/WalletProvider' -export default function ConnectPage() { +/** + * Only same-origin, absolute in-app paths are honoured as a return target. + * `next` arrives in the URL, so treating it as a bare redirect would let a + * crafted link bounce a freshly-connected wallet holder off-site. A leading + * `//` (or `/\`) is rejected because the browser reads it as protocol-relative. + */ +function safeNext(raw: string | null): string | null { + if (!raw) return null + if (!raw.startsWith('/')) return null + if (raw.startsWith('//') || raw.startsWith('/\\')) return null + return raw +} + +function ConnectRoute() { const router = useRouter() + const searchParams = useSearchParams() const { connected, connect, connectDemo } = useWallet() - // Once a wallet is connected (real modal selection or the demo path), move on. + const next = safeNext(searchParams.get('next')) + + // Once a wallet is connected (real modal selection or the demo path), move on + // — back to whatever the visitor was originally reaching for, if a guard sent + // them here, otherwise the default first stop. useEffect(() => { - if (connected) router.push('/deposit') - }, [connected, router]) + if (connected) router.replace(next ?? '/deposit') + }, [connected, router, next]) return ( ) } + +export default function ConnectPage() { + return ( + + + + ) +} diff --git a/src/app/deposit/page.tsx b/src/app/deposit/page.tsx index 6e1c706..0eef45a 100644 --- a/src/app/deposit/page.tsx +++ b/src/app/deposit/page.tsx @@ -1,9 +1,21 @@ 'use client' +import { Suspense } from 'react' import { useRouter } from 'next/navigation' import { Deposit } from '../../screens/Deposit' +import { RequireWallet } from '../../wallet/RequireWallet' -export default function DepositPage() { +function DepositRoute() { const router = useRouter() return router.push('/portfolio')} /> } + +export default function DepositPage() { + return ( + + + + + + ) +} diff --git a/src/app/portfolio/page.tsx b/src/app/portfolio/page.tsx index 58fbf7a..19870c0 100644 --- a/src/app/portfolio/page.tsx +++ b/src/app/portfolio/page.tsx @@ -1,9 +1,11 @@ 'use client' +import { Suspense } from 'react' import { useRouter } from 'next/navigation' import { Portfolio } from '../../screens/Portfolio' +import { RequireWallet } from '../../wallet/RequireWallet' -export default function PortfolioPage() { +function PortfolioRoute() { const router = useRouter() return ( ) } + +export default function PortfolioPage() { + // Suspense wraps the guard because it reads useSearchParams to preserve the + // visitor's intent; without a boundary Next cannot prerender the shell. + return ( + + + + + + ) +} diff --git a/src/app/withdraw/page.tsx b/src/app/withdraw/page.tsx index e833f7f..045f397 100644 --- a/src/app/withdraw/page.tsx +++ b/src/app/withdraw/page.tsx @@ -1,11 +1,23 @@ 'use client' +import { Suspense } from 'react' import { useRouter } from 'next/navigation' import { Withdraw } from '../../screens/Withdraw' +import { RequireWallet } from '../../wallet/RequireWallet' -export default function WithdrawPage() { +function WithdrawRoute() { const router = useRouter() return ( router.push('/portfolio')} onBack={() => router.push('/portfolio')} /> ) } + +export default function WithdrawPage() { + return ( + + + + + + ) +} diff --git a/src/brand/Helio.tsx b/src/brand/Helio.tsx index 8f728b5..ee8a6b7 100644 --- a/src/brand/Helio.tsx +++ b/src/brand/Helio.tsx @@ -1,4 +1,5 @@ import { useId } from 'react' +import { INK, SOLAR_RAMP, solarAlpha } from './palette' /** * Helio — the platform's one spectacle, here in its static, accessible fallback @@ -36,20 +37,19 @@ export function Helio({ size = 360, motes = 14, breathe = true }: HelioProps) { - - - - + {SOLAR_RAMP.map((stop) => ( + + ))} - - - + + + {dots.map((d, i) => ( - + ))} void } -/* --- Brand palette (matches the static Helio exactly) -------------------- */ -const SOLAR = '#FFB400' // the sun — emissive accent -const CORE = '#FFD451' // warm core surface tint -const HALO = '#FFC633' // glow halo (sits between core and solar) -const INK = '#0B2B23' // deep pine — corona motes - const clamp01 = (n: number) => (n < 0 ? 0 : n > 1 ? 1 : n) /* ------------------------------------------------------------------------- * @@ -274,7 +270,7 @@ function Scene({ highlight (echoing the static SVG's specular), plus a core glow light. */} - + diff --git a/src/brand/Mark.tsx b/src/brand/Mark.tsx index 40ec19e..6ad05bf 100644 --- a/src/brand/Mark.tsx +++ b/src/brand/Mark.tsx @@ -3,6 +3,13 @@ * year-long path), drawn in currentColor with the one permitted second colour: * a solar dot. Upper loop subtly smaller, as in the real analemma. */ +/** + * The Heliobond analemma — a single continuous tilted figure-eight (the sun's + * year-long path), drawn in currentColor with the one permitted second colour: + * a solar dot. Upper loop subtly smaller, as in the real analemma. + */ +import { SOLAR } from './palette' + export interface MarkProps { size?: number } @@ -24,7 +31,7 @@ export function Mark({ size = 28 }: MarkProps) { strokeLinejoin="round" strokeLinecap="round" /> - + diff --git a/src/brand/palette.ts b/src/brand/palette.ts new file mode 100644 index 0000000..6b7fc9c --- /dev/null +++ b/src/brand/palette.ts @@ -0,0 +1,64 @@ +/** + * Brand palette — the single source of truth for the literal colour values that + * canvas-style renderers need. + * + * Most of the interface reads colour through the CSS custom properties in + * `src/styles/tokens/colors.css`, and that remains the preferred route. But two + * renderers cannot: the SVG builds gradient stops, and + * hands colours to three.js, which needs a parsed value rather than a + * `var(--solar)` string. Both used to carry their own hard-coded hexes, which + * meant the same brand colour existed in three places and could drift. + * + * These constants mirror the colour tokens exactly: + * · `solar` === `--solar` + * · `ink` === `--ink` (light theme) + * The remaining entries are the solar ramp the orb is built from. They are + * deliberately theme-independent: the Helio is a fixed brand object and renders + * the same sun after sunset as it does at noon. + */ + +/** `--solar` — the sun, the brand accent. */ +export const SOLAR = '#FFB400' + +/** `--ink` (light theme) — deep pine, used for the corona motes. */ +export const INK = '#0B2B23' + +/** Warm highlight at the centre of the orb, and the key light in the WebGL scene. */ +export const SOLAR_HIGHLIGHT = '#FFF4D6' + +/** Warm core surface tint — the second stop of the orb gradient. */ +export const SOLAR_CORE = '#FFD451' + +/** Glow halo — sits between the core tint and solar proper. */ +export const SOLAR_HALO = '#FFC633' + +/** Deep edge of the orb, where the sun falls away into its own limb. */ +export const SOLAR_DEEP = '#F59A00' + +/** + * The solar ramp, centre outwards. Consumed by the SVG orb's radial gradient so + * the stop list is derived rather than restated. + */ +export const SOLAR_RAMP = [ + { offset: '0%', color: SOLAR_HIGHLIGHT }, + { offset: '34%', color: SOLAR_CORE }, + { offset: '72%', color: SOLAR }, + { offset: '100%', color: SOLAR_DEEP }, +] as const + +/** `--solar` as RGB channels, for building the `rgba()` glow stops. */ +export const SOLAR_RGB = '255, 180, 0' + +/** Build an `rgba()` string from `--solar` at a given alpha. */ +export function solarAlpha(alpha: number): string { + return `rgba(${SOLAR_RGB}, ${alpha})` +} + +export const BRAND = { + solar: SOLAR, + ink: INK, + solarHighlight: SOLAR_HIGHLIGHT, + solarCore: SOLAR_CORE, + solarHalo: SOLAR_HALO, + solarDeep: SOLAR_DEEP, +} as const diff --git a/src/components/FormField.tsx b/src/components/FormField.tsx index 115b70d..a2f0f03 100644 --- a/src/components/FormField.tsx +++ b/src/components/FormField.tsx @@ -1,4 +1,10 @@ -import type { CSSProperties, InputHTMLAttributes, ReactNode, SelectHTMLAttributes, TextareaHTMLAttributes } from 'react' +import type { + CSSProperties, + InputHTMLAttributes, + ReactNode, + SelectHTMLAttributes, + TextareaHTMLAttributes, +} from 'react' export interface FormFieldProps { label: string @@ -29,7 +35,12 @@ export interface FormTextareaProps extends TextareaHTMLAttributes + return ( +