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
120 changes: 120 additions & 0 deletions .github/workflows/preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
name: Preview deployment

on:
pull_request:
branches: [develop, main]

concurrency:
group: preview-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
pull-requests: write
contents: read

jobs:
build-preview:
name: Build & deploy preview
runs-on: ubuntu-latest

env:
# Enable the MSW mock layer for this build.
NEXT_PUBLIC_PREVIEW_MODE: "1"
# Signal that this is a preview, not a real production build.
PREVIEW_BUILD: "1"
# Stub values so the build doesn't fail on missing env vars.
NEXT_PUBLIC_API_URL: "http://localhost:8000/api/v1"
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE: ""
NEXT_PUBLIC_STELLAR_RPC_URL: ""
NEXT_PUBLIC_STELLAR_EXPLORER_URL: ""
NEXT_PUBLIC_MARKET_CORE_CONTRACT_ID: ""
NEXT_PUBLIC_TREASURY_CONTRACT_ID: ""
NEXT_PUBLIC_RESOLVER_CONTRACT_ID: ""
NEXT_PUBLIC_REASONING_REGISTRY_CONTRACT_ID: ""
NEXT_PUBLIC_USDC_CONTRACT_ID: ""
NEXT_PUBLIC_ADMIN_ADDRESSES: ""
NEXT_PUBLIC_DAILY_PASS_PRICE_USDC: "0.5"

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Safety check – mock must not ship to production
run: npx tsx scripts/check-preview-not-in-prod.ts

- name: Build (static export)
run: npx next build

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: preview-${{ github.event.pull_request.number }}
path: out/
retention-days: 7

# ── Optional: deploy to a static host ────────────────────────────────
# Uncomment and fill in when a static host (Cloudflare Pages, Netlify,
# Vercel, …) is configured. The deploy step should output a URL, which
# is then posted as a PR comment below.
#
# - name: Deploy to Cloudflare Pages
# id: deploy
# uses: cloudflare/pages-action@v1
# with:
# apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
# accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# projectName: oracledesk-preview
# directory: out
# branch: pr-${{ github.event.pull_request.number }}

- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const prNumber = context.payload.pull_request.number;
const sha = context.payload.pull_request.head.sha.slice(0, 7);

const body = [
'## Preview build',
'',
`Built from ${sha} with mock backend (MSW) and fake Stellar RPC.`,
'',
'The build artifact is attached to this workflow run. ' +
'To explore it locally: download, unzip and serve with `npx serve out/`.',
'',
'> **Note:** All data is mocked. No real transactions occur.',
].join('\n');

// Find an existing comment and update it, or create a new one.
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});

const existing = comments.find(
(c) => c.user?.type === 'Bot' && c.body?.startsWith('## Preview build'),
);

if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { PwaRegistrar } from "@/components/pwa/pwa-registrar";
import { DebugMount } from "@/components/debug/debug-mount";
import { RpcDegradedBanner } from "@/components/debug/RpcPanel";
import { WebVitals } from "@/app/web-vitals";
import { CommandPalette } from "@/components/command-palette/CommandPalette";

const geistSans = Geist({
subsets: ["latin"],
Expand Down Expand Up @@ -66,6 +67,7 @@ export default function RootLayout({
{children}
<Footer />
<DebugMount />
<CommandPalette />
<WebVitals />
</InboxProvider>
</HealthProvider>
Expand Down
19 changes: 13 additions & 6 deletions app/markets/page-client.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";
import { Icon } from "@/components/ui/icon";

import React, { useState, useEffect, useMemo } from "react";
import React, { useCallback, useState, useEffect, useMemo } from "react";
import { motion, AnimatePresence, useReducedMotion } from "framer-motion";
import { useBurstFeed } from "@/lib/realtime/useBurst";
import { formatFeedTime } from "@/lib/realtime/format";
Expand Down Expand Up @@ -348,14 +348,21 @@ export default function MarketsPageClient() {
[marketsData]
);

// Stable callback: avoids a new function identity on every render so the
// React Compiler (and manual memoization on FilterBar) can optimise correctly.
const handleSetStatus = useCallback(
(s: string) => setStatus(s as MarketStatus),
[],
);

return (
<div className="min-h-screen bg-surface">
<main className="pt-24 pb-12 px-gutter max-w-container-max-width mx-auto">
<FilterBar
status={status}
setStatus={(s) => setStatus(s as MarketStatus)}
category={category}
setCategory={setCategory}
<FilterBar
status={status}
setStatus={handleSetStatus}
category={category}
setCategory={setCategory}
/>
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
Expand Down
Loading