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
35 changes: 19 additions & 16 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { QueryProvider } from "@/providers/QueryProvider";
import { DevToolsSwitcher } from "@/components/ui/DevToolsSwitcher";
import { OnboardingProvider } from "@/providers/OnboardingProvider";
import OnboardingTour from "@/components/layout/OnboardingTour";
import { CurrencyProvider } from "@/hooks/useCurrency";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand Down Expand Up @@ -136,22 +137,24 @@ export default function RootLayout({
disableTransitionOnChange
>
<AuthProvider>
<QueryProvider>
<WalletProvider>
<ModalProvider>
<OnboardingProvider>
<AppLayout
headerSlot={<NavBar />}
footerSlot={<Footer />}
>
{children}
<OnboardingTour />
</AppLayout>
<DevToolsSwitcher />
</OnboardingProvider>
</ModalProvider>
</WalletProvider>
</QueryProvider>
<CurrencyProvider>
<QueryProvider>
<WalletProvider>
<ModalProvider>
<OnboardingProvider>
<AppLayout
headerSlot={<NavBar />}
footerSlot={<Footer />}
>
{children}
<OnboardingTour />
</AppLayout>
<DevToolsSwitcher />
</OnboardingProvider>
</ModalProvider>
</WalletProvider>
</QueryProvider>
</CurrencyProvider>
</AuthProvider>
</ThemeProvider>
{/* Register service worker after page is interactive */}
Expand Down
31 changes: 30 additions & 1 deletion src/components/home/layout/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { usePathname } from "next/navigation";
import { ThemeToggle } from "@/components/ui/theme-toggle";
import CreateWalletModal from "@/components/CreateWalletModal";
import { useWallet } from "@/providers/WalletProvider";
import { useCurrency, type DisplayCurrency } from "@/hooks/useCurrency";

const DISPLAY_CURRENCIES: DisplayCurrency[] = ["XLM", "USD", "EUR", "GBP", "JPY"];

function shortenAddress(address: string): string {
return `${address.slice(0, 4)}…${address.slice(-4)}`;
Expand All @@ -18,6 +21,7 @@ export default function NavBar() {
const [copied, setCopied] = useState(false);
const pathname = usePathname() ?? "";
const { address, balance, network, isLoading, isWrongNetwork, disconnect } = useWallet();
const { selectedCurrency, setSelectedCurrency, convert } = useCurrency();

const authRoutes = ["/login", "/sign-in", "/sign-up"];
if (authRoutes.includes(pathname)) return null;
Expand Down Expand Up @@ -115,6 +119,17 @@ export default function NavBar() {
<div className="hidden md:flex items-center space-x-4 ml-6">
<ThemeToggle />

<select
value={selectedCurrency}
onChange={(event) => setSelectedCurrency(event.target.value as DisplayCurrency)}
className="rounded-lg border border-border bg-card/40 px-2 py-1.5 text-xs font-medium text-foreground transition-colors hover:border-purple-500 focus:outline-none focus:ring-1 focus:ring-purple-500 cursor-pointer"
aria-label="Select display currency"
>
{DISPLAY_CURRENCIES.map((currency) => (
<option key={currency} value={currency}>{currency}</option>
))}
</select>

{isLandingPage ? (
<>
<Link href="/login">
Expand All @@ -138,7 +153,7 @@ export default function NavBar() {
</span>
{balance && (
<span className="text-xs font-mono text-muted-foreground border-l border-border/40 pl-2">
{balance} XLM
{convert(Number(balance))}
</span>
)}
<button
Expand Down Expand Up @@ -208,6 +223,20 @@ export default function NavBar() {
</Link>

<div className="pt-2 border-t border-border/40">
<label className="flex items-center justify-between text-sm">
<span>Currency</span>
<select
value={selectedCurrency}
onChange={(event) => setSelectedCurrency(event.target.value as DisplayCurrency)}
className="rounded-lg border border-border bg-card px-2 py-1 text-xs"
aria-label="Select display currency"
>
{DISPLAY_CURRENCIES.map((currency) => (
<option key={currency} value={currency}>{currency}</option>
))}
</select>
</label>

{address ? (
<button
onClick={disconnect}
Expand Down
13 changes: 11 additions & 2 deletions src/components/marketplace/BookSessionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ChevronRight, CheckCircle, CalendarClock } from "lucide-react";
import { Modal } from "@/components/ui/Modal";
import { Button } from "@/components/ui/Button";
import { Badge } from "@/components/ui/Badge";
import { useCurrency } from "@/hooks/useCurrency";

interface BookSessionModalProps {
expertName: string;
Expand All @@ -26,6 +27,9 @@ export default function BookSessionModal({
const [currentStep, setCurrentStep] = useState<Step>("deposit");
const [isProcessing, setIsProcessing] = useState(false);
const [reservationId, setReservationId] = useState<string | null>(null);
const { convert, selectedCurrency } = useCurrency();
const slotFeeXlm = Number.parseFloat(slotFee);
const displaySlotFee = Number.isFinite(slotFeeXlm) ? convert(slotFeeXlm) : `${slotFee} XLM`;

const handleDeposit = async () => {
setIsProcessing(true);
Expand Down Expand Up @@ -61,7 +65,12 @@ export default function BookSessionModal({
</div>
<div className="border-t border-white/10 pt-4 flex items-center justify-between">
<span className="text-sm text-white/60">Slot Reservation Fee</span>
<span className="text-lg font-bold text-purple-400">{slotFee} XLM</span>
<div className="text-right">
<span className="text-lg font-bold text-purple-400">{displaySlotFee}</span>
{selectedCurrency !== "XLM" && (
<p className="text-xs text-white/50">{slotFee} XLM</p>
)}
</div>
</div>
</div>

Expand Down Expand Up @@ -122,7 +131,7 @@ export default function BookSessionModal({
<h3 className="text-xl font-bold">Session Reserved!</h3>
<p className="text-sm text-white/60 max-w-sm">
Your appointment with <span className="text-white font-semibold">{expertName}</span> has been secured.
The slot fee of <span className="text-purple-400 font-semibold">{slotFee} XLM</span> will be applied to your final session invoice.
The slot fee of <span className="text-purple-400 font-semibold">{displaySlotFee}</span>{selectedCurrency !== "XLM" ? ` (${slotFee} XLM)` : ""} will be applied to your final session invoice.
</p>
</div>
</div>
Expand Down
13 changes: 6 additions & 7 deletions src/components/marketplace/ExpertCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface ExpertCardProps {
export default function ExpertCard({ expert }: ExpertCardProps) {
const queryClient = useQueryClient();
const { multiplier, isSurgeActive } = useSurgeMultiplier();
const { rates } = useCurrency();
const { convert, selectedCurrency } = useCurrency();

const handleMouseEnter = () => {
prefetchExpert(queryClient, expert.id);
Expand All @@ -25,9 +25,8 @@ export default function ExpertCard({ expert }: ExpertCardProps) {
// Calculate surge percentage for display
const surgePercent = Math.round((multiplier - 1) * 100);

// Calculate hourly and per-minute USD rates
const hourlyRateUSD = rates ? expert.hourlyRate * rates.usd : null;
const minuteRateUSD = hourlyRateUSD ? hourlyRateUSD / 60 : null;
const displayHourlyRate = convert(expert.hourlyRate);
const displayMinuteRate = convert(expert.hourlyRate / 60);

return (
<Link href={`/explore-experts/${expert.id}`}>
Expand Down Expand Up @@ -74,11 +73,11 @@ export default function ExpertCard({ expert }: ExpertCardProps) {
className="font-bold text-lg text-purple-400 cursor-help"
title="Rate may increase during high demand periods"
>
{expert.hourlyRate} XLM/hr
{displayHourlyRate} / hr
</span>
{minuteRateUSD && (
{selectedCurrency !== 'XLM' && (
<span className="text-sm text-gray-400">
(${minuteRateUSD.toFixed(2)} / min)
{expert.hourlyRate.toFixed(4)} XLM/hr ({displayMinuteRate} / min)
</span>
)}
{isSurgeActive && (
Expand Down
13 changes: 5 additions & 8 deletions src/components/profile/ExpertDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,7 @@ interface ExpertDetailsProps {
}

export default function ExpertDetails({ expert, onBookClick }: ExpertDetailsProps) {
const { rates } = useCurrency();

// Calculate USD rates
const hourlyRateUSD = rates ? expert.hourlyRate * rates.usd : null;
const { convert, selectedCurrency } = useCurrency();


return (
Expand Down Expand Up @@ -209,10 +206,10 @@ export default function ExpertDetails({ expert, onBookClick }: ExpertDetailsProp
<div className="text-sm text-gray-400">Total Sessions</div>
</div>
<div className="bg-black/30 rounded-lg p-4">
<div className="text-2xl font-bold text-purple-400">{expert.hourlyRate} XLM/hr</div>
<div className="text-sm text-gray-400">
{hourlyRateUSD ? `($${hourlyRateUSD.toFixed(2)} / hr)` : 'Hourly Rate'}
</div>
<div className="text-2xl font-bold text-purple-400">{convert(expert.hourlyRate)} / hr</div>
{selectedCurrency !== "XLM" && (
<div className="text-sm text-gray-400">{expert.hourlyRate.toFixed(4)} XLM/hr</div>
)}
</div>
<div className="bg-black/30 rounded-lg p-4">
<div className="text-2xl font-bold text-purple-400">{expert.reviews}</div>
Expand Down
22 changes: 20 additions & 2 deletions src/hooks/useCurrency.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, useEffect, useCallback } from "react";
import { createContext, createElement, useState, useEffect, useCallback, useContext } from "react";

// ─── Types ────────────────────────────────────────────────────────────────────

Expand All @@ -21,6 +21,8 @@ export interface UseCurrencyReturn {
isLoading: boolean;
}

const CurrencyContext = createContext<UseCurrencyReturn | null>(null);

// ─── Constants ────────────────────────────────────────────────────────────────

const COINGECKO_URL =
Expand All @@ -46,7 +48,7 @@ const CURRENCY_DECIMALS: Record<DisplayCurrency, number> = {

// ─── Hook ─────────────────────────────────────────────────────────────────────

export function useCurrency(): UseCurrencyReturn {
function useCurrencyState(): UseCurrencyReturn {
const [selectedCurrency, setSelectedCurrency] =
useState<DisplayCurrency>("USD");
const [rates, setRates] = useState<CurrencyRates | null>(null);
Expand Down Expand Up @@ -103,3 +105,19 @@ export function useCurrency(): UseCurrencyReturn {

return { selectedCurrency, setSelectedCurrency, rates, convert, isLoading };
}

export function CurrencyProvider({ children }: { children: React.ReactNode }) {
const value = useCurrencyState();

return createElement(CurrencyContext.Provider, { value }, children);
}

export function useCurrency(): UseCurrencyReturn {
const context = useContext(CurrencyContext);

if (!context) {
throw new Error("useCurrency must be used within a CurrencyProvider");
}

return context;
}
Loading