Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pnpm-debug.log*
lerna-debug.log*
.git_disabled


.next/
node_modules
dist
dist-ssr
Expand Down
6 changes: 4 additions & 2 deletions app/(dashboard)/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { DisconnectButton } from '@/components/wallet/DisconnectButton';
export default function AdminDashboardPage() {
return (
<div>
<h1>Admin Dashboard</h1>
<h1 data-tour="dashboard-title">Admin Dashboard</h1>
{/* Wallet session controls — see hooks/useWallet.ts for disconnect logic */}
<DisconnectButton />
<div data-tour="disconnect-wallet">
<DisconnectButton />
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion app/(dashboard)/deliveries/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default function DeliveryDetailsPage({
}) {
return (
<div>
<h1>Delivery Details: {params.id}</h1>
<h1 data-tour="delivery-details-title">Delivery Details: {params.id}</h1>
{/* Delivery status and tracking placeholder */}
</div>
);
Expand Down
6 changes: 4 additions & 2 deletions app/(dashboard)/deliveries/create/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { CurrencyConverter } from '@/features/deliveries/components/CurrencyConverter';

export default function CreateDeliveryPage() {
return (
<div>
<h1>Create New Delivery</h1>
{/* Delivery creation form placeholder */}
<h1 data-tour="create-delivery-title">Create New Delivery</h1>
<CurrencyConverter />
</div>
);
}
14 changes: 14 additions & 0 deletions app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Tour } from '@/components/onboarding/Tour';

export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<Tour />
{children}
</>
);
}
6 changes: 6 additions & 0 deletions app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
--background-end-rgb: 255, 255, 255;
}

.dark {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 15, 23, 42;
--background-end-rgb: 30, 41, 59;
}

body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
Expand Down
13 changes: 11 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import './global.css';
import { ThemeToggle } from '@/components/ui/ThemeToggle';
import { Providers } from './providers';

export const metadata = {
title: 'SwiftChain',
Expand All @@ -11,8 +13,15 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
<html lang="en" suppressHydrationWarning>
<body>
<Providers>
<div className="fixed right-4 top-4 z-50">
<ThemeToggle />
</div>
{children}
</Providers>
</body>
</html>
);
}
27 changes: 27 additions & 0 deletions app/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import { useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from 'next-themes';

export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
retry: 1,
refetchOnWindowFocus: true,
},
},
}),
);

return (
<QueryClientProvider client={queryClient}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
</QueryClientProvider>
);
}
1,486 changes: 1,486 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

98 changes: 98 additions & 0 deletions components/onboarding/Tour.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use client';

import Joyride from 'react-joyride';
import { useOnboardingTour } from '@/hooks/useOnboardingTour';

export function Tour() {
const { run, stepIndex, steps, onJoyrideEvent, skipTour } = useOnboardingTour();

return (
<Joyride
callback={onJoyrideEvent}
continuous
disableOverlayClose
hideCloseButton={false}
run={run}
scrollToFirstStep
showProgress
showSkipButton
stepIndex={stepIndex}
steps={steps}
styles={{
options: {
zIndex: 2000,
},
}}
locale={{
back: 'Back',
close: 'Close',
last: 'Finish',
next: 'Next',
skip: 'Skip Tour',
}}
spotlightClicks={false}
floaterProps={{
disableAnimation: true,
}}
tooltipComponent={(props) => {
const {
backProps,
closeProps,
index,
isLastStep,
primaryProps,
skipProps,
step,
tooltipProps,
} = props;

return (
<div
{...tooltipProps}
className="max-w-sm rounded-lg border border-secondary/30 bg-white p-4 text-black shadow-xl dark:bg-slate-900 dark:text-white"
>
<div className="mb-3 text-sm leading-relaxed">{step.content}</div>
<div className="flex items-center justify-between gap-2">
<button
type="button"
{...skipProps}
onClick={() => {
skipTour();
skipProps?.onClick?.({} as never);
}}
className="text-xs text-secondary underline"
>
Skip Tour
</button>
<div className="flex items-center gap-2">
{index > 0 && (
<button
type="button"
{...backProps}
className="rounded border border-secondary/40 px-2 py-1 text-xs"
>
Back
</button>
)}
<button
type="button"
{...primaryProps}
className="rounded bg-primary px-3 py-1 text-xs text-white"
>
{isLastStep ? 'Finish' : 'Next'}
</button>
<button
type="button"
{...closeProps}
className="rounded border border-secondary/40 px-2 py-1 text-xs"
>
Close
</button>
</div>
</div>
</div>
);
}}
/>
);
}
23 changes: 23 additions & 0 deletions components/ui/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';

import { Moon, Sun } from 'lucide-react';
import { useTheme } from '@/hooks/useTheme';

export function ThemeToggle() {
const { resolvedTheme, toggleTheme } = useTheme();
const isDark = resolvedTheme === 'dark';

const iconClassName = 'h-5 w-5';

return (
<button
type="button"
onClick={() => void toggleTheme()}
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
data-tour="theme-toggle"
className="inline-flex items-center justify-center rounded-md border border-secondary/40 p-2 transition-colors hover:bg-secondary/20"
>
{isDark ? <Sun className={iconClassName} /> : <Moon className={iconClassName} />}
</button>
);
}
94 changes: 94 additions & 0 deletions features/deliveries/components/CurrencyConverter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use client';

import { useCurrencyConverter } from '@/hooks/useCurrencyConverter';

const SUPPORTED_FIAT_CODES = ['USD', 'EUR', 'NGN', 'KES', 'GHS'] as const;

export function CurrencyConverter() {
const {
fiatCode,
setFiatCode,
fiatAmount,
setFiatAmount,
rate,
lastUpdated,
xlmEstimate,
isLoading,
isError,
refetch,
} = useCurrencyConverter();

return (
<section className="mt-6 rounded-lg border border-secondary/30 p-4">
<h2 className="text-lg font-semibold">Cross-Border Fiat-to-XLM Preview</h2>
<p className="mt-1 text-sm text-secondary">
Enter your local amount to estimate the equivalent XLM in real time.
</p>

<div className="mt-4 grid gap-3 md:grid-cols-2">
<label className="flex flex-col gap-1 text-sm">
<span>Local currency</span>
<select
value={fiatCode}
onChange={(event) => setFiatCode(event.target.value)}
className="rounded-md border border-secondary/40 bg-transparent px-3 py-2"
>
{SUPPORTED_FIAT_CODES.map((code) => (
<option key={code} value={code} className="text-black">
{code}
</option>
))}
</select>
</label>

<label className="flex flex-col gap-1 text-sm">
<span>Amount ({fiatCode})</span>
<input
type="number"
min="0"
step="0.01"
value={fiatAmount}
onChange={(event) => setFiatAmount(event.target.value)}
className="rounded-md border border-secondary/40 bg-transparent px-3 py-2"
placeholder={`Enter amount in ${fiatCode}`}
/>
</label>
</div>

<div className="mt-4 rounded-md bg-secondary/10 p-3 text-sm">
{isLoading && <p>Loading current exchange rate...</p>}

{!isLoading && isError && (
<div className="space-y-2">
<p className="text-red-600 dark:text-red-400">
Rate unavailable, try again.
</p>
<button
type="button"
onClick={() => void refetch()}
className="rounded border border-secondary/40 px-2 py-1 text-xs"
>
Retry rate lookup
</button>
</div>
)}

{!isLoading && !isError && xlmEstimate !== null && (
<div className="space-y-1">
<p>
Estimated total: <strong>{xlmEstimate.toFixed(6)} XLM</strong>
</p>
<p className="text-xs text-secondary">
Live rate: 1 XLM = {rate?.toFixed(6)} {fiatCode}
</p>
{lastUpdated && (
<p className="text-xs text-secondary">
Last updated: {new Date(lastUpdated).toLocaleString()}
</p>
)}
</div>
)}
</div>
</section>
);
}
Loading
Loading