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
7 changes: 4 additions & 3 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ToastProvider } from "@/components/ui/toast-provider";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand All @@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
});

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Lance - Decentralized Freelance Marketplace",
description: "Stellar-native freelance marketplace with AI-powered dispute resolution",
};

export default function RootLayout({
Expand All @@ -27,7 +28,7 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<ToastProvider>{children}</ToastProvider>
</body>
</html>
);
Expand Down
172 changes: 172 additions & 0 deletions apps/web/components/transaction-example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
"use client";

import { useTransactionToast } from "@/hooks/use-transaction-toast";
import { toast } from "@/lib/toast";

export function TransactionExample() {
const { executeTransaction, showLoading, updateToSuccess, updateToError } =
useTransactionToast();

const handleSimpleToast = () => {
toast.info({
title: "Information",
description: "This is a simple info toast",
});
};

const handleSuccessToast = () => {
toast.success({
title: "Success!",
description: "Your action was completed successfully",
txHash: "a1b2c3d4e5f6g7h8i9j0",
});
};

const handleErrorToast = () => {
toast.error({
title: "Error Occurred",
description: "Something went wrong with your request",
});
};

const handleWarningToast = () => {
toast.warning({
title: "Warning",
description: "Please review your inputs before proceeding",
});
};

const handleSimulatedTransaction = async () => {
await executeTransaction(
async () => {
await new Promise((resolve) => setTimeout(resolve, 3000));
return { txHash: "simulated_tx_hash_12345" };
},
{
loadingMessage: "Creating escrow deposit...",
successMessage: "Escrow deposit confirmed!",
errorMessage: "Failed to create escrow deposit",
},
{
onSuccess: (txHash) => {
console.log("Transaction succeeded:", txHash);
},
onError: (error) => {
console.error("Transaction failed:", error);
},
}
);
};

const handleSimulatedError = async () => {
await executeTransaction(
async () => {
await new Promise((_, reject) =>
setTimeout(() => reject(new Error("tx_insufficient_balance")), 2000)
);
return { txHash: "" };
},
{
loadingMessage: "Processing payment...",
successMessage: "Payment completed!",
errorMessage: "Payment failed",
}
);
};

const handleManualFlow = async () => {
const loadingToast = showLoading(
"Submitting job...",
"Please wait while we process your job posting"
);

try {
await new Promise((resolve) => setTimeout(resolve, 2000));

updateToSuccess(
loadingToast,
"Job Posted!",
"Your job has been successfully posted to the marketplace",
"job_tx_abc123"
);
} catch {
updateToError(
loadingToast,
"Failed to Post Job",
"There was an error posting your job. Please try again."
);
}
};

return (
<div className="p-6 space-y-4">
<h2 className="text-xl font-bold">Toast Notification Examples</h2>

<div className="flex flex-wrap gap-2">
<button
onClick={handleSimpleToast}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Info Toast
</button>

<button
onClick={handleSuccessToast}
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
>
Success + Tx Hash
</button>

<button
onClick={handleErrorToast}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
Error Toast
</button>

<button
onClick={handleWarningToast}
className="px-4 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600"
>
Warning Toast
</button>
</div>

<h3 className="text-lg font-semibold mt-6">Transaction Flows</h3>

<div className="flex flex-wrap gap-2">
<button
onClick={handleSimulatedTransaction}
className="px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600"
>
Simulate Escrow Deposit (Success)
</button>

<button
onClick={handleSimulatedError}
className="px-4 py-2 bg-orange-500 text-white rounded hover:bg-orange-600"
>
Simulate Transaction (Error)
</button>

<button
onClick={handleManualFlow}
className="px-4 py-2 bg-teal-500 text-white rounded hover:bg-teal-600"
>
Manual Toast Flow
</button>
</div>

<div className="mt-6 p-4 bg-gray-100 rounded text-sm">
<p className="font-semibold">Features demonstrated:</p>
<ul className="list-disc ml-5 mt-2 space-y-1">
<li>Five toast types: info, success, error, warning, loading</li>
<li>Automatic transaction hash linking to Stellar Explorer</li>
<li>Error code mapping from Stellar SDK to human-readable messages</li>
<li>Async toast updates (loading → success/error)</li>
<li>Auto-timeout with appropriate durations per toast type</li>
</ul>
</div>
</div>
);
}
33 changes: 33 additions & 0 deletions apps/web/components/ui/toast-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use client";

import { Toaster } from "sonner";
import type { ReactNode } from "react";

interface ToastProviderProps {
children: ReactNode;
}

export function ToastProvider({ children }: ToastProviderProps) {
return (
<>
{children}
<Toaster
position="bottom-right"
richColors
closeButton
duration={5000}
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
/>
</>
);
}
102 changes: 102 additions & 0 deletions apps/web/hooks/use-transaction-toast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"use client";

import { useCallback } from "react";
import { toast, type ToastState } from "@/lib/toast";
import { mapErrorToToast } from "@/lib/error-mapper";

export interface TransactionCallbacks {
onSuccess?: (txHash: string) => void;
onError?: (error: Error) => void;
}

export interface TransactionConfig {
loadingMessage?: string;
successMessage?: string;
errorMessage?: string;
}

export function useTransactionToast() {
const executeTransaction = useCallback(
async <T extends { txHash?: string }>(
operation: () => Promise<T>,
config: TransactionConfig = {},
callbacks?: TransactionCallbacks
): Promise<T | null> => {
const {
loadingMessage = "Processing transaction...",
successMessage = "Transaction completed successfully",
errorMessage = "Transaction failed",
} = config;

const loadingToast = toast.loading({
title: loadingMessage,
description: "Please wait while we confirm your transaction on the Stellar network",
});

try {
const result = await operation();

toast.update(loadingToast, "success", {
title: successMessage,
description: "Your transaction has been confirmed on the blockchain",
txHash: result.txHash,
});

if (result.txHash && callbacks?.onSuccess) {
callbacks.onSuccess(result.txHash);
}

return result;
} catch (error) {
const errorToast = mapErrorToToast(error);

toast.update(loadingToast, "error", {
title: errorToast.title || errorMessage,
description: errorToast.description,
});

if (callbacks?.onError && error instanceof Error) {
callbacks.onError(error);
}

return null;
}
},
[]
);

const showLoading = useCallback((title: string, description?: string): ToastState => {
return toast.loading({ title, description });
}, []);

const updateToSuccess = useCallback(
(
state: ToastState,
title: string,
description?: string,
txHash?: string
): void => {
toast.update(state, "success", { title, description, txHash });
},
[]
);

const updateToError = useCallback(
(state: ToastState, title: string, description?: string): void => {
toast.update(state, "error", { title, description });
},
[]
);

const dismiss = useCallback((state: ToastState): void => {
toast.dismiss(state);
}, []);

return {
executeTransaction,
showLoading,
updateToSuccess,
updateToError,
dismiss,
};
}
Loading
Loading