Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
38 changes: 32 additions & 6 deletions src/app/connect/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Connect
Expand All @@ -22,3 +40,11 @@ export default function ConnectPage() {
/>
)
}

export default function ConnectPage() {
return (
<Suspense fallback={null}>
<ConnectRoute />
</Suspense>
)
}
14 changes: 13 additions & 1 deletion src/app/deposit/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <Deposit onDone={() => router.push('/portfolio')} />
}

export default function DepositPage() {
return (
<Suspense fallback={null}>
<RequireWallet>
<DepositRoute />
</RequireWallet>
</Suspense>
)
}
16 changes: 15 additions & 1 deletion src/app/portfolio/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Portfolio
Expand All @@ -12,3 +14,15 @@ export default function PortfolioPage() {
/>
)
}

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 (
<Suspense fallback={null}>
<RequireWallet>
<PortfolioRoute />
</RequireWallet>
</Suspense>
)
}
14 changes: 13 additions & 1 deletion src/app/withdraw/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Withdraw onDone={() => router.push('/portfolio')} onBack={() => router.push('/portfolio')} />
)
}

export default function WithdrawPage() {
return (
<Suspense fallback={null}>
<RequireWallet>
<WithdrawRoute />
</RequireWallet>
</Suspense>
)
}
16 changes: 8 additions & 8 deletions src/brand/Helio.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -36,20 +37,19 @@ export function Helio({ size = 360, motes = 14, breathe = true }: HelioProps) {
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
<defs>
<radialGradient id={coreId} cx="42%" cy="38%" r="68%">
<stop offset="0%" stopColor="#FFF4D6" />
<stop offset="34%" stopColor="#FFD451" />
<stop offset="72%" stopColor="#FFB400" />
<stop offset="100%" stopColor="#F59A00" />
{SOLAR_RAMP.map((stop) => (
<stop key={stop.offset} offset={stop.offset} stopColor={stop.color} />
))}
</radialGradient>
<radialGradient id={glowId} cx="50%" cy="50%" r="50%">
<stop offset="0%" stopColor="rgba(255,180,0,0.42)" />
<stop offset="55%" stopColor="rgba(255,180,0,0.12)" />
<stop offset="100%" stopColor="rgba(255,180,0,0)" />
<stop offset="0%" stopColor={solarAlpha(0.42)} />
<stop offset="55%" stopColor={solarAlpha(0.12)} />
<stop offset="100%" stopColor={solarAlpha(0)} />
</radialGradient>
</defs>
<circle cx={cx} cy={cx} r={size * 0.48} fill={`url(#${glowId})`} />
{dots.map((d, i) => (
<circle key={i} cx={d.x} cy={d.y} r={d.s} fill="#0B2B23" opacity="0.55" />
<circle key={i} cx={d.x} cy={d.y} r={d.s} fill={INK} opacity="0.55" />
))}
<g
className="hb-orb"
Expand Down
10 changes: 3 additions & 7 deletions src/brand/HelioWebGL.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import type { ComponentRef } from 'react'
import { Canvas, useFrame } from '@react-three/fiber'
import { MeshDistortMaterial } from '@react-three/drei'
import * as THREE from 'three'
/* --- Brand palette (shared with the static Helio and the CSS tokens) ------ */
import { SOLAR, SOLAR_CORE as CORE, SOLAR_HALO as HALO, INK, SOLAR_HIGHLIGHT } from './palette'

export interface HelioWebGLProps {
/** Rendered size in px (square). */
Expand All @@ -36,12 +38,6 @@ export interface HelioWebGLProps {
onReady?: () => 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)

/* ------------------------------------------------------------------------- *
Expand Down Expand Up @@ -274,7 +270,7 @@ function Scene({
highlight (echoing the static SVG's specular), plus a core glow light. */}
<ambientLight intensity={0.28} />
<pointLight position={[0, 0, 0]} intensity={0.9} color={SOLAR} distance={8} />
<directionalLight position={[-2.2, 2.4, 3]} intensity={1.05} color={'#FFF4D6'} />
<directionalLight position={[-2.2, 2.4, 3]} intensity={1.05} color={SOLAR_HIGHLIGHT} />
<Halo animate={animate} intensity={intensity} />
<Sun animate={animate} intensity={intensity} />
<Corona specs={specs} animate={animate} />
Expand Down
9 changes: 8 additions & 1 deletion src/brand/Mark.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -24,7 +31,7 @@ export function Mark({ size = 28 }: MarkProps) {
strokeLinejoin="round"
strokeLinecap="round"
/>
<circle cx="64" cy="20" r="7" fill="#FFB400" />
<circle cx="64" cy="20" r="7" fill={SOLAR} />
</g>
</svg>
</span>
Expand Down
64 changes: 64 additions & 0 deletions src/brand/palette.ts
Original file line number Diff line number Diff line change
@@ -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 <Helio> builds gradient stops, and <HelioWebGL>
* 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
15 changes: 13 additions & 2 deletions src/components/FormField.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -29,7 +35,12 @@ export interface FormTextareaProps extends TextareaHTMLAttributes<HTMLTextAreaEl
}

export function FormTextarea(props: FormTextareaProps) {
return <textarea {...props} style={{ ...inputBaseStyle, ...((props.style as CSSProperties | undefined) ?? {}) }} />
return (
<textarea
{...props}
style={{ ...inputBaseStyle, ...((props.style as CSSProperties | undefined) ?? {}) }}
/>
)
}

export interface FormSelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
Expand Down
7 changes: 6 additions & 1 deletion src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ export type { ToastProps, ToastTone } from './Toast'
export { Card } from './Card'
export type { CardProps } from './Card'
export { FormField, FormInput, FormTextarea, FormSelect } from './FormField'
export type { FormFieldProps, FormInputProps, FormTextareaProps, FormSelectProps } from './FormField'
export type {
FormFieldProps,
FormInputProps,
FormTextareaProps,
FormSelectProps,
} from './FormField'
export { Sparkline } from './Sparkline'
export type { SparklineProps } from './Sparkline'
export * from './icons'
Loading
Loading