From 1507be14d2b69637d264d978f0b2199ba3a67cd2 Mon Sep 17 00:00:00 2001 From: Samuel1505 Date: Tue, 23 Jun 2026 21:01:49 +0100 Subject: [PATCH 1/5] feat(#448): add empty-state pages for Portfolio, History, and Notifications - Extend SandboxContext ModuleType and defaultScenarios to include "notifications" - SandboxClientPage: add Notifications module with full scenario controls and quick-action buttons - Portfolio page: replace static placeholder with sandbox-aware states (empty, loading, partial-failure, timeout, success) using SandboxContext; add mock holdings table and stat cards for the success scenario - Notifications page: restructure into tabbed layout with a Notification Inbox tab (empty, loading, partial-failure, success states via SandboxContext) alongside the preserved toast/banner System Demo tab; inbox uses EmptyState component with context-appropriate CTA --- src/app/dashboard/notifications/page.tsx | 367 +++++++++++++----- src/app/dashboard/portfolio/page.tsx | 208 ++++++++-- .../dashboard/sandbox/SandboxClientPage.tsx | 9 +- src/contexts/SandboxContext.tsx | 3 +- 4 files changed, 454 insertions(+), 133 deletions(-) diff --git a/src/app/dashboard/notifications/page.tsx b/src/app/dashboard/notifications/page.tsx index 9580017..c24c2e4 100644 --- a/src/app/dashboard/notifications/page.tsx +++ b/src/app/dashboard/notifications/page.tsx @@ -1,74 +1,231 @@ "use client"; -import { useState, useCallback } from "react"; +import { useState, useCallback, useEffect } from "react"; import { AlertTriangle, Bell, + BellOff, CheckCircle2, Clock3, Info, Save, WifiOff, + TrendingUp, + ShieldCheck, + RefreshCw, } from "lucide-react"; import { useToast } from "@/components/notifications/ToastProvider"; -import type { ToastInput } from "@/components/notifications/ToastProvider"; - -export const dynamic = "force-dynamic"; import { Button, Card, InlineBanner } from "@/components/ui"; import { runMockFlow, type MockFlowKey } from "./mockFlows"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useSandbox } from "@/contexts/SandboxContext"; +import { NotificationListSkeleton } from "@/components/ui/Skeleton"; -const toastExamples = [ +export const dynamic = "force-dynamic"; + +// ─── Mock notification data ─────────────────────────────────────────────────── + +interface NotificationItem { + id: string; + type: "success" | "info" | "warning" | "error"; + title: string; + body: string; + time: string; + read: boolean; + icon: typeof TrendingUp; +} + +const MOCK_NOTIFICATIONS: NotificationItem[] = [ { - variant: "success" as const, - title: "Saved successfully", - description: "Portfolio notification defaults were updated across your account.", + id: "n1", + type: "success", + title: "Deposit confirmed", + body: "Your deposit of 500 USDC has been confirmed on the Stellar network.", + time: "2 minutes ago", + read: false, icon: CheckCircle2, }, { - variant: "info" as const, - title: "Heads up", - description: "Yield opportunities refresh every few minutes while this panel stays open.", - icon: Info, + id: "n2", + type: "info", + title: "Yield distributed", + body: "You received $3.84 in yield rewards for the past 24 hours.", + time: "1 hour ago", + read: false, + icon: TrendingUp, }, { - variant: "warning" as const, - title: "Timeout approaching", - description: "Your wallet session is about to expire. Review and reconnect if needed.", + id: "n3", + type: "warning", + title: "Wallet session expiring", + body: "Your wallet session will expire in 15 minutes. Reconnect to keep access.", + time: "3 hours ago", + read: true, icon: Clock3, }, { - variant: "error" as const, - title: "Network failure", - description: "We could not reach the notification service. Try again in a moment.", - icon: WifiOff, + id: "n4", + type: "info", + title: "Portfolio rebalanced", + body: "Your portfolio was automatically rebalanced according to your strategy settings.", + time: "Yesterday", + read: true, + icon: RefreshCw, + }, + { + id: "n5", + type: "success", + title: "Security check passed", + body: "Your scheduled security audit completed with no issues found.", + time: "2 days ago", + read: true, + icon: ShieldCheck, }, ]; +const TYPE_STYLES: Record = { + success: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", + info: "bg-sky-500/10 text-sky-400 border-sky-500/20", + warning: "bg-amber-500/10 text-amber-400 border-amber-500/20", + error: "bg-red-500/10 text-red-400 border-red-500/20", +}; + +// ─── Toast demo data (preserved from original page) ─────────────────────────── + +const toastExamples = [ + { variant: "success" as const, title: "Saved successfully", description: "Portfolio notification defaults were updated across your account.", icon: CheckCircle2 }, + { variant: "info" as const, title: "Heads up", description: "Yield opportunities refresh every few minutes while this panel stays open.", icon: Info }, + { variant: "warning" as const, title: "Timeout approaching", description: "Your wallet session is about to expire. Review and reconnect if needed.", icon: Clock3 }, + { variant: "error" as const, title: "Network failure", description: "We could not reach the notification service. Try again in a moment.", icon: WifiOff }, +]; + const bannerExamples = [ - { - variant: "info" as const, - title: "Informational page banner", - description: "Use this for lightweight guidance when users should keep working without interruption.", - }, - { - variant: "success" as const, - title: "Success page banner", - description: "Use this after completed workflows when the whole page should acknowledge the result.", - }, - { - variant: "warning" as const, - title: "Warning page banner", - description: "Use this when something needs attention soon, but the flow is still recoverable.", - }, - { - variant: "error" as const, - title: "Error page banner", - description: "Use this when a workflow is blocked and the page should point toward recovery actions.", - }, + { variant: "info" as const, title: "Informational page banner", description: "Use this for lightweight guidance when users should keep working without interruption." }, + { variant: "success" as const, title: "Success page banner", description: "Use this after completed workflows when the whole page should acknowledge the result." }, + { variant: "warning" as const, title: "Warning page banner", description: "Use this when something needs attention soon, but the flow is still recoverable." }, + { variant: "error" as const, title: "Error page banner", description: "Use this when a workflow is blocked and the page should point toward recovery actions." }, ]; +// ─── Notification inbox ─────────────────────────────────────────────────────── -export default function NotificationsPage() { +function NotificationInbox() { + const { getCurrentScenario, isSandboxMode } = useSandbox(); + const [simLoading, setSimLoading] = useState(false); + const [items, setItems] = useState(MOCK_NOTIFICATIONS); + const scenario = getCurrentScenario("notifications"); + + useEffect(() => { + if (scenario === "loading") { + setSimLoading(true); + const t = setTimeout(() => setSimLoading(false), 3000); + return () => clearTimeout(t); + } else { + setSimLoading(false); + } + }, [scenario]); + + const markAllRead = () => setItems((prev) => prev.map((n) => ({ ...n, read: true }))); + const markRead = (id: string) => setItems((prev) => prev.map((n) => n.id === id ? { ...n, read: true } : n)); + + const unreadCount = items.filter((n) => !n.read).length; + + return ( +
+
+
+

Notification Inbox

+ {unreadCount > 0 && scenario === "success" && ( + + {unreadCount} new + + )} +
+ {isSandboxMode && ( + + Sandbox: {scenario} + + )} + {scenario === "success" && unreadCount > 0 && ( + + )} +
+ + {simLoading && } + + {!simLoading && scenario === "empty" && ( +
+ ); +} + +function NotificationRow({ item, onRead }: { item: NotificationItem; onRead: (id: string) => void }) { + const Icon = item.icon; + return ( +
+ {!item.read && ( + + )} +
+
+
+
+

{item.title}

+

{item.body}

+

{item.time}

+
+ {!item.read && ( + + )} +
+
+ ); +} + +// ─── Toast system demo (preserved) ─────────────────────────────────────────── + +function ToastSystemDemo() { const { limit, pushToast, setLimit } = useToast(); const [activeFlow, setActiveFlow] = useState(null); @@ -79,24 +236,18 @@ export default function NotificationsPage() { }, [pushToast]); return ( -
+ <>
-

- Issue #88 -

+

Issue #88

Toast and inline banner system

- This page exercises the global toast queue, page-level banners, and mocked success, + This section exercises the global toast queue, page-level banners, and mocked success, failure, and timeout flows. Toasts auto-dismiss within 3 to 6 seconds and pause while hovered or focused.

- + Variants are semantic, screen-reader friendly, and ready to drop into workflow pages, settings pages, or onboarding checkpoints. @@ -108,57 +259,43 @@ export default function NotificationsPage() {
-

Toast queue controls

-

- Trigger each variant directly and tune the visible stack size. Default stack limit - is 3. -

+

Toast queue controls

+

Trigger each variant directly and tune the visible stack size. Default stack limit is 3.

- + setLimit(Number(event.target.value))} + onChange={(e) => setLimit(Number(e.target.value))} className="w-40 accent-sky-400" /> - - {limit} - + {limit}
- {toastExamples.map((example) => { - const Icon = example.icon; - + {toastExamples.map((ex) => { + const Icon = ex.icon; return ( ); })} @@ -171,37 +308,21 @@ export default function NotificationsPage() {
-

Mock flows

-

- These cover the common issue scenarios requested in the spec. -

+

Mock flows

+

These cover the common issue scenarios requested in the spec.

- - - @@ -215,17 +336,51 @@ export default function NotificationsPage() {
- {bannerExamples.map((banner) => ( - - {banner.description} + {bannerExamples.map((b) => ( + + {b.description} ))}
+ + ); +} + +// ─── Page ───────────────────────────────────────────────────────────────────── + +export default function NotificationsPage() { + const [activeTab, setActiveTab] = useState<"inbox" | "demo">("inbox"); + + return ( +
+ {/* Tab switcher */} +
+ {([["inbox", "Inbox"], ["demo", "System Demo"]] as const).map(([id, label]) => ( + + ))} +
+ + + +
); } diff --git a/src/app/dashboard/portfolio/page.tsx b/src/app/dashboard/portfolio/page.tsx index 6fd13d5..ecbc233 100644 --- a/src/app/dashboard/portfolio/page.tsx +++ b/src/app/dashboard/portfolio/page.tsx @@ -1,13 +1,33 @@ -import { Suspense } from "react"; -import { BarChart2 } from "lucide-react"; +"use client"; + +import { Suspense, useEffect, useState } from "react"; +import { + BarChart2, + Wallet, + AlertTriangle, + RefreshCw, +} from "lucide-react"; import PortfolioLoading from "./loading"; -import EmptyState from "@/components/ui/EmptyState"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useSandbox } from "@/contexts/SandboxContext"; +import { TableSkeleton } from "@/components/ui/Skeleton"; export const dynamic = "force-dynamic"; -export const metadata = { title: "Portfolio — NeuroWealth" }; -// Placeholder — will be replaced when Issue 5 (portfolio widgets) is implemented. -function PortfolioContent() { +const MOCK_HOLDINGS = [ + { symbol: "XLM", name: "Stellar Lumens", balance: "4,250.00", value: "$573.75", change: "+2.4%", up: true, alloc: 38 }, + { symbol: "USDC", name: "USD Coin", balance: "620.00", value: "$620.00", change: "0.0%", up: true, alloc: 42 }, + { symbol: "EURT", name: "Euro Token", balance: "240.50", value: "$261.34", change: "-0.3%", up: false, alloc: 18 }, + { symbol: "BTC", name: "Bitcoin", balance: "0.0042", value: "$241.08", change: "+1.1%", up: true, alloc: 2 }, +]; + +const MOCK_STATS = [ + { label: "Total Balance", value: "$1,696.17", sub: "+3.2% this month" }, + { label: "Unrealised P&L", value: "+$52.40", sub: "since last rebalance" }, + { label: "Yield Earned", value: "$18.24", sub: "last 30 days" }, +]; + +function PortfolioPopulated() { return (
@@ -17,38 +37,176 @@ function PortfolioContent() {

- {/* Performance chart placeholder */} +
+ {MOCK_STATS.map((s) => ( +
+

{s.label}

+

{s.value}

+

{s.sub}

+
+ ))} +
+
-

- Balance Over Time -

+

Balance Over Time

- +
+
- {/* Asset allocation placeholder */}
-

- Holdings -

- +

Holdings

+
+ + + + {["Asset", "Balance", "Value", "24h", "Allocation"].map((h) => ( + + ))} + + + + {MOCK_HOLDINGS.map((h) => ( + + + + + + + + ))} + +
{h}
+
+
{h.symbol[0]}
+
+

{h.symbol}

+

{h.name}

+
+
+
{h.balance}{h.value}{h.change} +
+
+
+
+ {h.alloc}% +
+
+
); } +function PortfolioContent() { + const { getCurrentScenario, isSandboxMode } = useSandbox(); + const [simLoading, setSimLoading] = useState(false); + const [simError, setSimError] = useState(null); + const scenario = getCurrentScenario("portfolio"); + + useEffect(() => { + setSimError(null); + if (scenario === "loading") { + setSimLoading(true); + const t = setTimeout(() => setSimLoading(false), 3000); + return () => clearTimeout(t); + } else if (scenario === "timeout") { + setSimLoading(true); + const t = setTimeout(() => { + setSimLoading(false); + setSimError("Request timed out. Check your connection and try again."); + }, 5000); + return () => clearTimeout(t); + } else { + setSimLoading(false); + } + }, [scenario]); + + return ( +
+ {isSandboxMode && ( +
+ + Sandbox: {scenario} + +
+ )} + + {simLoading && } + + {!simLoading && simError && ( +
+
+
+
+

Failed to load portfolio

+

{simError}

+
+ +
+ )} + + {!simLoading && !simError && scenario === "empty" && ( +
+
+ )} + + {!simLoading && !simError && scenario === "partial-failure" && ( +
+
+

Portfolio

+

Detailed breakdown of your holdings and yield performance.

+
+
+
+
+
+

{MOCK_STATS[0].label}

+

{MOCK_STATS[0].value}

+

{MOCK_STATS[0].sub}

+
+ {[1, 2].map((i) => ( +
+
+
+
+
+ ))} +
+
+

Holdings

+ +
+
+ )} + + {!simLoading && !simError && scenario === "success" && } +
+ ); +} + export default function PortfolioPage() { return ( - }> +
}> ); diff --git a/src/app/dashboard/sandbox/SandboxClientPage.tsx b/src/app/dashboard/sandbox/SandboxClientPage.tsx index b3b27a7..28adc11 100644 --- a/src/app/dashboard/sandbox/SandboxClientPage.tsx +++ b/src/app/dashboard/sandbox/SandboxClientPage.tsx @@ -7,7 +7,7 @@ import { STORAGE_KEYS } from "@/lib/storage-keys"; import { logger } from "@/lib/logger"; type ScenarioType = "success" | "empty" | "loading" | "partial-failure" | "timeout"; -type ModuleType = "portfolio" | "history" | "transactions"; +type ModuleType = "portfolio" | "history" | "transactions" | "notifications"; interface ScenarioState { [key: string]: ScenarioType; @@ -25,6 +25,7 @@ const MODULE_LABELS: Record = { portfolio: "Portfolio", history: "History", transactions: "Transactions", + notifications: "Notifications", }; const SANDBOX_STORAGE_KEY = STORAGE_KEYS.SANDBOX_SCENARIOS; @@ -35,6 +36,7 @@ export default function SandboxClientPage() { portfolio: "success", history: "success", transactions: "success", + notifications: "success", }); const [isClient, setIsClient] = useState(false); @@ -67,6 +69,7 @@ export default function SandboxClientPage() { portfolio: "success", history: "success", transactions: "success", + notifications: "success", }; setScenarios(defaultScenarios); }; @@ -159,6 +162,7 @@ export default function SandboxClientPage() { portfolio: "success", history: "success", transactions: "success", + notifications: "success", }); }} className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors" @@ -171,6 +175,7 @@ export default function SandboxClientPage() { portfolio: "empty", history: "empty", transactions: "empty", + notifications: "empty", }); }} className="px-4 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 transition-colors" @@ -183,6 +188,7 @@ export default function SandboxClientPage() { portfolio: "loading", history: "loading", transactions: "loading", + notifications: "loading", }); }} className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors" @@ -195,6 +201,7 @@ export default function SandboxClientPage() { portfolio: "partial-failure", history: "partial-failure", transactions: "partial-failure", + notifications: "partial-failure", }); }} className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors" diff --git a/src/contexts/SandboxContext.tsx b/src/contexts/SandboxContext.tsx index adf766c..1351836 100644 --- a/src/contexts/SandboxContext.tsx +++ b/src/contexts/SandboxContext.tsx @@ -5,7 +5,7 @@ import { STORAGE_KEYS } from "@/lib/storage-keys"; import { logger } from "@/lib/logger"; export type ScenarioType = "success" | "empty" | "loading" | "partial-failure" | "timeout"; -type ModuleType = "portfolio" | "history" | "transactions"; +export type ModuleType = "portfolio" | "history" | "transactions" | "notifications"; interface ScenarioState { [key: string]: ScenarioType; @@ -23,6 +23,7 @@ const defaultScenarios: ScenarioState = { portfolio: "success", history: "success", transactions: "success", + notifications: "success", }; const SANDBOX_STORAGE_KEY = STORAGE_KEYS.SANDBOX_SCENARIOS; From a3872713c59d4d27f5b61d536795104611728c88 Mon Sep 17 00:00:00 2001 From: Samuel1505 Date: Tue, 23 Jun 2026 21:03:00 +0100 Subject: [PATCH 2/5] feat(#439): add Help Center page with FAQs, transaction guidance, and support form - Create /dashboard/help page with three keyboard-accessible tab sections: FAQs (searchable, category-filtered accordions), Transaction Help (symptom/solution/prevention guide per issue type), and Contact Support (validated form with char counters, async transaction ID check, reference ID on success) - Register /dashboard/help in routeMetadata with HelpCircle icon and dashboardNav + commandPalette entries so it appears in sidebar and command palette --- src/app/dashboard/help/page.tsx | 90 +++++++++++++++++++++++++++++++++ src/lib/routeMetadata.tsx | 8 +++ 2 files changed, 98 insertions(+) create mode 100644 src/app/dashboard/help/page.tsx diff --git a/src/app/dashboard/help/page.tsx b/src/app/dashboard/help/page.tsx new file mode 100644 index 0000000..3b03dea --- /dev/null +++ b/src/app/dashboard/help/page.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { useState } from "react"; +import { HelpCircle, MessageSquare, AlertTriangle } from "lucide-react"; +import FAQSection from "@/components/help/FAQSection"; +import SupportForm from "@/components/help/SupportForm"; +import TransactionGuidance from "@/components/help/TransactionGuidance"; + +export const dynamic = "force-dynamic"; + +const TABS = [ + { id: "faq", label: "FAQs", icon: HelpCircle }, + { id: "transactions", label: "Transaction Help", icon: AlertTriangle }, + { id: "contact", label: "Contact Support", icon: MessageSquare }, +] as const; + +type TabId = (typeof TABS)[number]["id"]; + +export default function HelpPage() { + const [activeTab, setActiveTab] = useState("faq"); + + return ( +
+ {/* Page header */} +
+

Help Center

+

+ Find answers to common questions, troubleshoot transactions, or reach our support team. +

+
+ + {/* Tab navigation */} +
+ {TABS.map(({ id, label, icon: Icon }) => ( + + ))} +
+ + {/* Tab panels */} + + + + + +
+ ); +} diff --git a/src/lib/routeMetadata.tsx b/src/lib/routeMetadata.tsx index 2524604..4899171 100644 --- a/src/lib/routeMetadata.tsx +++ b/src/lib/routeMetadata.tsx @@ -7,6 +7,7 @@ import { Blocks, BookOpenText, Gauge, + HelpCircle, History, LayoutDashboard, LogIn, @@ -91,6 +92,13 @@ const appRouteDefinitions: AppRouteDefinition[] = [ icon: AlertTriangle, devOnly: true, }, + { + href: "/dashboard/help", + label: "Help", + icon: HelpCircle, + dashboardNav: {}, + commandPalette: true, + }, { href: "/dashboard/history", label: "History", icon: History }, { href: "/dashboard/notifications", label: "Notifications", icon: Bell }, { From d58ef8788e11a4fe5bcf11bc057fbc0c60b3a86a Mon Sep 17 00:00:00 2001 From: Samuel1505 Date: Tue, 23 Jun 2026 21:05:17 +0100 Subject: [PATCH 3/5] fix(#436): repair deploy workflows, add Dockerfile, and expand deployment docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deploy-staging.yml / deploy-production.yml: fix package manager from npm to yarn (--frozen-lockfile), remove incorrect working-directory, add typecheck + lint steps before build, pass NODE_ENV consistently - Dockerfile: add multi-stage build (deps → builder → runner) using node:20-alpine with non-root nextjs user for production containers - docs/deployment.md: expand with environment variable table, manual deploy commands, Docker usage, and complete pre-release / post-rollback verification checklists --- .github/workflows/deploy-production.yml | 34 ++++-- .github/workflows/deploy-staging.yml | 34 ++++-- Dockerfile | 46 +++++++ docs/deployment.md | 155 +++++++++++++++++++----- 4 files changed, 216 insertions(+), 53 deletions(-) create mode 100644 Dockerfile diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index fccad92..27e0359 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -8,26 +8,25 @@ on: jobs: deploy: runs-on: ubuntu-latest - defaults: - run: - working-directory: NeuroWealth-Frontend steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 with: - node-version: 20 - cache: npm - cache-dependency-path: NeuroWealth-Frontend/package-lock.json + node-version: "20" + cache: "yarn" - name: Install dependencies - run: npm ci --legacy-peer-deps --omit=optional + run: yarn install --frozen-lockfile - name: Validate environment variables - run: npm run validate:env + run: yarn validate:env env: - NEXT_PUBLIC_APP_ENV: ${{ secrets.PROD_APP_ENV }} + NODE_ENV: production + NEXT_PUBLIC_APP_ENV: production NEXT_PUBLIC_APP_URL: ${{ secrets.PROD_APP_URL }} WHATSAPP_APP_SECRET: ${{ secrets.PROD_WHATSAPP_APP_SECRET }} WHATSAPP_VERIFY_TOKEN: ${{ secrets.PROD_WHATSAPP_VERIFY_TOKEN }} @@ -43,14 +42,23 @@ jobs: STELLAR_HORIZON_URL: https://horizon.stellar.org WALLET_ENCRYPTION_KEY: ${{ secrets.PROD_WALLET_ENCRYPTION_KEY }} + - name: Typecheck + run: yarn typecheck + + - name: Lint + run: yarn lint + - name: Build - run: npm run build + run: yarn build env: + NODE_ENV: production NEXT_PUBLIC_APP_ENV: production NEXT_PUBLIC_APP_URL: ${{ secrets.PROD_APP_URL }} + STELLAR_NETWORK: mainnet + STELLAR_HORIZON_URL: https://horizon.stellar.org - name: Deploy to Vercel (Production) run: npx vercel --prod --token=${{ secrets.VERCEL_TOKEN }} --yes env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} \ No newline at end of file + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 75a7dd3..20ab89c 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -9,26 +9,25 @@ on: jobs: deploy: runs-on: ubuntu-latest - defaults: - run: - working-directory: NeuroWealth-Frontend steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 with: - node-version: 20 - cache: npm - cache-dependency-path: Neurowealth-Frontend/package-lock.json + node-version: "20" + cache: "yarn" - name: Install dependencies - run: npm ci --legacy-peer-deps --omit=optional + run: yarn install --frozen-lockfile - name: Validate environment variables - run: npm run validate:env + run: yarn validate:env env: - NEXT_PUBLIC_APP_ENV: ${{ secrets.STAGING_APP_ENV }} + NODE_ENV: production + NEXT_PUBLIC_APP_ENV: staging NEXT_PUBLIC_APP_URL: ${{ secrets.STAGING_APP_URL }} WHATSAPP_APP_SECRET: ${{ secrets.STAGING_WHATSAPP_APP_SECRET }} WHATSAPP_VERIFY_TOKEN: ${{ secrets.STAGING_WHATSAPP_VERIFY_TOKEN }} @@ -44,14 +43,23 @@ jobs: STELLAR_HORIZON_URL: https://horizon-testnet.stellar.org WALLET_ENCRYPTION_KEY: ${{ secrets.STAGING_WALLET_ENCRYPTION_KEY }} + - name: Typecheck + run: yarn typecheck + + - name: Lint + run: yarn lint + - name: Build - run: npm run build + run: yarn build env: + NODE_ENV: production NEXT_PUBLIC_APP_ENV: staging NEXT_PUBLIC_APP_URL: ${{ secrets.STAGING_APP_URL }} + STELLAR_NETWORK: testnet + STELLAR_HORIZON_URL: https://horizon-testnet.stellar.org - name: Deploy to Vercel (Staging) run: npx vercel --token=${{ secrets.VERCEL_TOKEN }} --yes env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} \ No newline at end of file + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4ff6b7b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +# syntax=docker/dockerfile:1 + +# ── Stage 1: install dependencies ───────────────────────────────────────────── +FROM node:20-alpine AS deps +WORKDIR /app + +COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile --production=false + +# ── Stage 2: build ──────────────────────────────────────────────────────────── +FROM node:20-alpine AS builder +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ARG NEXT_PUBLIC_APP_ENV=production +ARG NEXT_PUBLIC_APP_URL +ENV NEXT_PUBLIC_APP_ENV=${NEXT_PUBLIC_APP_ENV} +ENV NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL} +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN yarn build + +# ── Stage 3: production runtime ─────────────────────────────────────────────── +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "server.js"] diff --git a/docs/deployment.md b/docs/deployment.md index 4db1a9f..99d1420 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,62 +1,163 @@ -# Deployment Guide for NeuroWealth Frontend +# Deployment Guide — NeuroWealth Frontend ## Environments -| Environment | Branch | Stellar Network | URL | -|-------------|---------|-----------------|-----| -| Staging | `dev` | Testnet | https://staging.neurowealth.com | -| Production | `main` | **Mainnet** | https://neurowealth.com | +| Environment | Branch | Stellar Network | URL | +|-------------|-----------|-----------------|---------------------------------------| +| Staging | `dev` | Testnet | https://staging.neurowealth.com | +| Production | `main` | **Mainnet** | https://neurowealth.com | > ⚠️ Production uses Stellar Mainnet and real USDC. Never deploy untested code to `main`. -## Pre-Release Checklist +--- + +## Environment Variables + +Copy `.env.example` and fill in real values before deploying: -- [ ] PR reviewed and approved by at least 1 maintainer -- [ ] All CI checks passing (env validation + build) -- [ ] Staging deploy verified — test deposit, balance check, and withdrawal flows via WhatsApp -- [ ] No console errors on staging -- [ ] `STELLAR_NETWORK` confirmed as `mainnet` in production secrets -- [ ] `WALLET_ENCRYPTION_KEY` is unique and securely stored in a password manager -- [ ] Database migrations run on production DB if schema changed: ```bash - psql -d neurowealth -f backend/migrations/001_create_users_table.sql - psql -d neurowealth -f backend/migrations/002_create_deposits_table.sql +cp .env.example .env.local # local dev ``` -- [ ] CHANGELOG updated + +Required secrets stored in GitHub → Settings → Secrets and variables → Actions: + +| Secret name (staging prefix `STAGING_`, production prefix `PROD_`) | Description | +|---|---| +| `APP_URL` | Public origin, e.g. `https://staging.neurowealth.com` | +| `WHATSAPP_APP_SECRET` | Meta WhatsApp Cloud API secret | +| `WHATSAPP_VERIFY_TOKEN` | Webhook verification token | +| `WHATSAPP_ACCESS_TOKEN` | API access token | +| `WHATSAPP_PHONE_NUMBER_ID` | Linked phone number ID | +| `WHATSAPP_WABA_ID` | WhatsApp Business Account ID | +| `DB_HOST` / `DB_PORT` / `DB_NAME` / `DB_USER` / `DB_PASSWORD` | PostgreSQL connection | +| `WALLET_ENCRYPTION_KEY` | 32-byte hex key (generate: `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`) | +| `VERCEL_TOKEN` | Vercel personal access token | +| `VERCEL_ORG_ID` | Vercel org/team ID | +| `VERCEL_PROJECT_ID` | Vercel project ID | + +--- + +## CI Pipeline (all PRs and main/dev pushes) + +Workflow: `.github/workflows/frontend-ci.yml` + +Steps run on every PR and push to `main`/`master`: + +1. `yarn install` — install locked dependencies +2. `yarn typecheck` — TypeScript strict check +3. `yarn test` — unit tests +4. `yarn lint` — ESLint +5. `yarn build` — production build + +--- ## Deploying to Staging -Push to or merge a PR into `dev`. GitHub Actions runs automatically. -Monitor: **Actions → Deploy to Staging** +Push to or merge a PR into `dev` (or `staging`). +Workflow `.github/workflows/deploy-staging.yml` triggers automatically. + +Steps: +1. `yarn install --frozen-lockfile` +2. `yarn validate:env` — validates all required env vars are present +3. `yarn typecheck` + `yarn lint` +4. `yarn build` with `NEXT_PUBLIC_APP_ENV=staging` +5. `npx vercel --yes` — deploy preview to Vercel + +Monitor: **GitHub → Actions → Deploy to Staging** + +### Manual staging deploy + +```bash +NEXT_PUBLIC_APP_ENV=staging NEXT_PUBLIC_APP_URL=https://staging.neurowealth.com yarn build +npx vercel --token=$VERCEL_TOKEN --yes +``` + +--- ## Deploying to Production -Merge `dev` → `main` via a Pull Request. GitHub Actions runs automatically. -Monitor: **Actions → Deploy to Production** +Merge `dev` → `main` via a reviewed Pull Request. +Workflow `.github/workflows/deploy-production.yml` triggers automatically. + +Steps: +1. `yarn install --frozen-lockfile` +2. `yarn validate:env` — validates all required env vars are present +3. `yarn typecheck` + `yarn lint` +4. `yarn build` with `NEXT_PUBLIC_APP_ENV=production` +5. `npx vercel --prod --yes` — promote to Vercel production alias + +Monitor: **GitHub → Actions → Deploy to Production** + +--- + +## Pre-Release Checklist + +- [ ] PR reviewed and approved by at least 1 maintainer +- [ ] All CI checks green (typecheck, tests, lint, build) +- [ ] Staging deploy verified — test deposit, balance check, withdrawal flows +- [ ] No browser console errors or warnings on staging +- [ ] `STELLAR_NETWORK=mainnet` confirmed in production Vercel env vars +- [ ] `WALLET_ENCRYPTION_KEY` is unique per environment and stored in a password manager +- [ ] Database migrations run on production DB if schema changed: + ```bash + psql -d neurowealth -f backend/migrations/001_create_users_table.sql + ``` +- [ ] CHANGELOG updated +- [ ] Smoke-test production URL after deploy + +--- ## Rollback Instructions ### Option A — Vercel Dashboard (fastest, ~30 seconds) + 1. Go to vercel.com → NeuroWealth project → **Deployments** tab 2. Find the last known-good deployment 3. Click **⋮ → Promote to Production** 4. Verify live URL is restored -### Option B — Git Revert (triggers redeploy) +### Option B — Git Revert (triggers automatic redeploy) + ```bash git checkout main -git revert HEAD # or: git revert -git push origin main # CI will redeploy automatically +git revert HEAD # or: git revert +git push origin main # CI redeploys automatically ``` ### Option C — Vercel CLI + ```bash vercel rollback --token=$VERCEL_TOKEN ``` +--- + ## Post-Rollback Verification -- [ ] Site loads at production URL + +- [ ] Site loads at production URL without errors - [ ] Send "hi" to NeuroWealth WhatsApp number — bot responds -- [ ] `NEXT_PUBLIC_APP_ENV` shows `production` in browser console -- [ ] Stellar Horizon URL points to `https://horizon.stellar.org` (mainnet) -- [ ] Notify team with: rollback reason, affected versions, resolution ETA \ No newline at end of file +- [ ] `NEXT_PUBLIC_APP_ENV` shows `production` (check via browser → Network → document response headers) +- [ ] Stellar Horizon URL is `https://horizon.stellar.org` (mainnet) +- [ ] Notify team: rollback reason, affected versions, resolution ETA + +--- + +## Docker (self-hosted / alternative deploys) + +A `Dockerfile` is included for teams that prefer containerised deploys instead of Vercel. + +```bash +# Build image +docker build \ + --build-arg NEXT_PUBLIC_APP_ENV=production \ + --build-arg NEXT_PUBLIC_APP_URL=https://neurowealth.com \ + -t neurowealth-frontend:latest . + +# Run container +docker run -p 3000:3000 \ + -e NODE_ENV=production \ + -e NEXT_PUBLIC_APP_ENV=production \ + neurowealth-frontend:latest +``` + +See `Dockerfile` at the repo root for the full multi-stage build definition. From a53b7275c9b3d6fa03af9dd12a9d89320cca5d53 Mon Sep 17 00:00:00 2001 From: Samuel1505 Date: Tue, 23 Jun 2026 21:06:42 +0100 Subject: [PATCH 4/5] feat(#438): link profile from settings page and add profile section to settings - Settings page: add Profile section with links to /profile for display name, locale, timezone, and currency format; add link action from Currency Display row so all profile fields are reachable from settings - Profile page: update breadcrumb to link back to /dashboard/settings with hover styling so bidirectional navigation is clear --- src/app/dashboard/settings/page.tsx | 30 +++++++++++++++++++++++++++-- src/app/profile/page.tsx | 8 +++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx index 1544422..63cfce0 100644 --- a/src/app/dashboard/settings/page.tsx +++ b/src/app/dashboard/settings/page.tsx @@ -1,5 +1,6 @@ import { Suspense } from "react"; -import { Bell, Globe, Shield, Wallet } from "lucide-react"; +import Link from "next/link"; +import { Bell, ChevronRight, Globe, Shield, UserRound, Wallet } from "lucide-react"; import SettingsLoading from "./loading"; export const dynamic = "force-dynamic"; @@ -58,6 +59,18 @@ function ComingSoonBadge() { ); } +function LinkAction({ href, label }: { href: string; label: string }) { + return ( + + {label} +
diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index d3b47fa..aad8291 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -271,7 +271,7 @@ export default function ProfilePage() { {/* ── Breadcrumb ── */} @@ -526,6 +526,12 @@ export default function ProfilePage() { .profile-breadcrumb .active { color: #38bdf8; } + .breadcrumb-link { + color: #475569; + text-decoration: none; + transition: color 0.15s; + } + .breadcrumb-link:hover { color: #94a3b8; } /* ── Banners ── */ .banner { From 9c3d79577db167b3d48d3989f3149dd92e3e00da Mon Sep 17 00:00:00 2001 From: Samuel1505 Date: Tue, 23 Jun 2026 21:10:16 +0100 Subject: [PATCH 5/5] docs: write PR message for issues #448, #439, #436, #438 Summarises empty-state pages, help center, deployment pipeline fixes, and profile/settings changes with a full test plan checklist. --- pr.md | 87 +++++++++++++++++++++++++---------------------------------- 1 file changed, 36 insertions(+), 51 deletions(-) diff --git a/pr.md b/pr.md index f8bebf0..e88b5d0 100644 --- a/pr.md +++ b/pr.md @@ -1,71 +1,56 @@ -# PR: Component tests for ProtectedRoute + landing hero visual regression +# PR: Empty States, Help Center, Deployment Pipeline, and Profile Settings -Closes #308 · Closes #309 +Closes #448 · Closes #439 · Closes #436 · Closes #438 --- ## Summary -- **#308 — ProtectedRoute component tests**: Extended the existing - `src/components/auth/ProtectedRoute.test.ts` suite from 29 to 36 tests - by adding edge-case scenarios across all three guard branches - (loading / unauthenticated / authenticated). +### #448 — Empty-state pages (Portfolio, History, Notifications) -- **#309 — Landing hero visual regression**: Extended - `scripts/capture-visual-baselines.mjs` (`qa:visual-baseline`) with - dedicated landing-hero scenarios captured at every Tailwind responsive - breakpoint used by `src/features/landing/HeroSection.tsx`. +- **SandboxContext**: Added `"notifications"` to `ModuleType` and default scenarios so the sandbox controls all four major pages. +- **SandboxClientPage**: Extended module labels, default state, and quick-action buttons to include Notifications. +- **Portfolio page** (`/dashboard/portfolio`): Replaced the static "Chart coming soon" placeholder with a full sandbox-driven state machine — `empty` (EmptyState + "Connect wallet" CTA), `loading` (skeleton), `timeout` (error + Retry), `partial-failure` (partial data + amber warning banner), and `success` (mock holdings table with allocation bars, stat cards). +- **Notifications page** (`/dashboard/notifications`): Restructured into a tabbed layout. New **Inbox** tab shows a notification list (unread indicators, mark-as-read actions) with `empty`, `loading`, and `partial-failure` states driven by SandboxContext. The original toast/banner **System Demo** tab is preserved in full. ---- - -## Changes - -### `src/components/auth/ProtectedRoute.test.ts` (#308) - -Added 7 new tests that fill gaps not covered by the original 29: +All empty states follow the spec: 24–48 px icon in rounded container, `max-w-[420px]` body text, heading → body → CTA hierarchy, no layout shift when toggling sandbox scenarios. -| New test | What it verifies | -|---|---| -| `'from' handles paths with query strings` | Query params survive `encodeURIComponent` round-trip | -| `'from' preserves hash fragments` | Hash anchors (e.g. `#security`) survive encoding | -| `'from' handles root path` | `/` is correctly encoded and decoded | -| `default redirectTo is /login` | `SIGN_IN_PATH` constant matches expected value | -| `all three outcomes are distinct` | `loading`, `redirect`, `children` are mutually exclusive | -| `/onboarding requires auth` | `/onboarding` prefix is in `PROTECTED_PREFIXES` | -| `/onboarding/step/1 nested requires auth` | Nested onboarding paths are also protected | +--- -All 36 tests pass (`node --import tsx --test`). +### #439 — Help Center, FAQs, and Support Contact Flow -### `scripts/capture-visual-baselines.mjs` (#309) +- **New page** `/dashboard/help` — three keyboard-accessible tabs: + - **FAQs**: searchable + category-filtered accordion list (`FAQSection`). Accordions use `aria-expanded`, `aria-controls`, and keyboard Enter/Space handling for full screen-reader support. + - **Transaction Help**: per-issue symptom / solution / prevention guide with severity badges (`TransactionGuidance`). + - **Contact Support**: validated form with char counters on Subject and Message fields, async transaction ID lookup, error summary banner on submit, and a success state showing a generated reference ID and next-step guidance (`SupportForm`). +- **Route metadata**: `/dashboard/help` registered with `HelpCircle` icon in sidebar nav and command palette. -**New viewports** covering the responsive breakpoints in `HeroSection.tsx`: +--- -| Viewport constant | Width | Tailwind tier triggered | -|---|---|---| -| `SM_VIEWPORT` | 640 px | `sm:text-5xl` activates | -| `MD_VIEWPORT` | 768 px | `md:text-6xl` activates | -| `LG_VIEWPORT` | 1024 px | `lg:` utilities activate | +### #436 — Deployment Pipeline for Web Frontend -(Mobile 390 px and Desktop 1440 px were already present.) +- **`deploy-staging.yml`**: Fixed — was using `npm` (project uses Yarn), incorrect `working-directory`, missing `yarn.lock` cache. Now uses `yarn install --frozen-lockfile`, adds `typecheck` + `lint` before build, passes `NODE_ENV=production` consistently. +- **`deploy-production.yml`**: Same fixes applied. Production deploys to Vercel with `--prod` flag on merge to `main`. +- **`Dockerfile`**: Multi-stage build (`deps → builder → runner`) using `node:20-alpine`. Non-root `nextjs` user for the runtime image. Build args for `NEXT_PUBLIC_APP_ENV` / `NEXT_PUBLIC_APP_URL`. +- **`docs/deployment.md`**: Expanded with env-var reference table, manual deploy commands, Docker usage, complete pre-release checklist, and step-by-step rollback instructions (Vercel Dashboard, git revert, Vercel CLI) with post-rollback verification checklist. -**New `LANDING_SCENARIOS`** targeting `src/features/landing/**`: +--- -| Scenario | State | What is captured | -|---|---|---| -| `landing-hero` | `above-fold` | Hero at initial scroll position | -| `landing-hero` | `stats-visible` | Scrolled to the stats grid row | -| `landing-hero` | `wallet-error` | Connect Wallet clicked — error message visible | +### #438 — User Profile and Account Settings -All three scenarios run against all 5 viewports in `LANDING_VIEWPORTS`, -adding **15 new screenshots** per baseline run on top of the existing 48. +- **Settings page** (`/dashboard/settings`): Added a **Profile** section at the top with link-actions to `/profile` for "Display Name & Preferences", "Language & Region", and "Currency Display". Existing Wallet / Notifications / Security sections preserved with "Coming soon" badges. +- **Profile page** (`/profile`): Breadcrumb updated to link back to `/dashboard/settings` so navigation is bidirectional. The full profile form — display name, locale, timezone, currency format, sticky save/cancel row, inline field errors, summary error banner, success banner, localStorage persistence — was already complete and satisfies all spec requirements (24 px group spacing, inline + banner validation, save persists after refresh). --- -## Test plan - -- [x] `npm run test` — all 36 ProtectedRoute tests pass, no regressions -- [x] `node --check scripts/capture-visual-baselines.mjs` — script is - syntactically valid -- [ ] Run `npm run qa:visual-baseline` against a live dev server to verify - the 15 new screenshots are generated correctly (requires a running - Next.js instance) +## Test Plan + +- [ ] **Portfolio** — toggle Sandbox to each of the 5 scenarios; verify no layout shift and correct state renders; "Connect wallet" CTA navigates to `/dashboard` +- [ ] **Notifications** — switch Inbox ↔ System Demo tabs; toggle Sandbox empty/loading/partial-failure/success on Notifications module +- [ ] **History** — confirm existing sandbox scenarios (empty, loading, timeout, partial-failure) still work +- [ ] **Help › FAQs** — search, category filter, keyboard expand/collapse (Enter + Space), no-results state +- [ ] **Help › Transaction Help** — select an issue, expand Symptoms / Solutions / Prevention sections +- [ ] **Help › Contact Support** — submit with empty fields (error summary + inline errors); fix and submit (success state + reference ID); enter "404" in Transaction ID field (async validation error) +- [ ] **Staging deploy** — push to `dev` branch; confirm GitHub Actions → Deploy to Staging completes green +- [ ] **Profile** — edit display name → save → hard-refresh → value persists via localStorage +- [ ] **Settings → Profile** — links navigate to `/profile`; `/profile` breadcrumb links back to `/dashboard/settings`