Skip to content

Commit f2e12cb

Browse files
authored
Merge pull request #354 from Smartdevs17/fix/issues-240-241-242-243-v2
2 parents 3f9e496 + 99bef3a commit f2e12cb

9 files changed

Lines changed: 452 additions & 53 deletions

File tree

frontend/src/app/settings/page.tsx

Lines changed: 137 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
"use client";
22

33
import { useState, useEffect } from "react";
4-
import { Copy, Check, LogOut, Moon, Sun, Bell } from "lucide-react";
4+
import { Copy, Check, LogOut, Moon, Sun, Bell, Globe } from "lucide-react";
55
import { useWallet } from "@/context/wallet-context";
66
import { useRouter } from "next/navigation";
77
import { shortenPublicKey, formatNetwork } from "@/lib/wallet";
8+
import toast from "react-hot-toast";
9+
10+
type DisplayCurrency = "USD" | "XLM" | "USDC";
11+
type AmountFormat = "full" | "compact";
812

913
export default function SettingsPage() {
1014
const router = useRouter();
1115
const { session, disconnect, isHydrated } = useWallet();
12-
const [emailNotifications, setEmailNotifications] = useState(true);
13-
const [theme, setTheme] = useState<"light" | "dark">(() => {
16+
17+
const [browserPush, setBrowserPush] = useState(false);
18+
const [theme, setTheme] = useState<"light" | "dark" | "system">(() => {
1419
if (typeof window !== "undefined") {
1520
const saved = localStorage.getItem("flowfi-theme") as
1621
| "light"
1722
| "dark"
23+
| "system"
1824
| null;
1925
if (saved) {
2026
document.documentElement.classList.toggle("dark", saved === "dark");
@@ -23,31 +29,66 @@ export default function SettingsPage() {
2329
}
2430
return "dark";
2531
});
32+
33+
const [displayCurrency, setDisplayCurrency] = useState<DisplayCurrency>(() => {
34+
if (typeof window !== "undefined") {
35+
return (localStorage.getItem("flowfi-currency") as DisplayCurrency) || "USD";
36+
}
37+
return "USD";
38+
});
39+
40+
const [amountFormat, setAmountFormat] = useState<AmountFormat>(() => {
41+
if (typeof window !== "undefined") {
42+
return (localStorage.getItem("flowfi-amount-format") as AmountFormat) || "full";
43+
}
44+
return "full";
45+
});
46+
2647
const [copied, setCopied] = useState(false);
2748

28-
const toggleTheme = () => {
29-
const next = theme === "dark" ? "light" : "dark";
30-
setTheme(next);
31-
localStorage.setItem("flowfi-theme", next);
32-
document.documentElement.classList.toggle(
33-
"dark",
34-
next === "dark"
35-
);
49+
const toggleTheme = (newTheme: "light" | "dark" | "system") => {
50+
setTheme(newTheme);
51+
localStorage.setItem("flowfi-theme", newTheme);
52+
if (newTheme === "system") {
53+
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
54+
document.documentElement.classList.toggle("dark", prefersDark);
55+
} else {
56+
document.documentElement.classList.toggle("dark", newTheme === "dark");
57+
}
3658
};
3759

3860
const copyAddress = async () => {
3961
if (session?.publicKey) {
4062
await navigator.clipboard.writeText(session.publicKey);
4163
setCopied(true);
64+
toast.success("Address copied to clipboard");
4265
setTimeout(() => setCopied(false), 1500);
4366
}
4467
};
4568

4669
const handleDisconnect = () => {
4770
disconnect();
71+
toast.success("Wallet disconnected");
4872
router.push("/");
4973
};
5074

75+
const handleBrowserPushToggle = async () => {
76+
if (!browserPush) {
77+
try {
78+
await Notification.requestPermission();
79+
setBrowserPush(Notification.permission === "granted");
80+
if (Notification.permission === "granted") {
81+
toast.success("Browser notifications enabled");
82+
}
83+
} catch {
84+
toast.error("Failed to enable notifications");
85+
}
86+
} else {
87+
setBrowserPush(false);
88+
toast("Browser notifications disabled");
89+
}
90+
};
91+
5192
if (!isHydrated) {
5293
return (
5394
<div className="relative min-h-screen overflow-hidden bg-gradient-to-br from-zinc-950 via-zinc-900 to-black dark:from-white dark:via-gray-100 dark:to-gray-200 transition-colors flex items-center justify-center">
@@ -68,42 +109,40 @@ export default function SettingsPage() {
68109

69110
<div>
70111
<h1 className="text-3xl font-semibold tracking-tight text-white dark:text-black">
71-
Profile Settings
112+
Settings
72113
</h1>
73114
<p className="text-sm opacity-60 mt-1">
74-
Manage your FlowFi experience
115+
Manage your FlowFi preferences
75116
</p>
76117
</div>
77118

78-
{/* Email Notifications */}
119+
{/* Browser Push Notifications */}
79120
<div className="flex items-center justify-between group">
80121
<div className="flex items-center gap-3">
81122
<div className="p-2 rounded-lg bg-purple-500/20 text-purple-400">
82123
<Bell size={18} />
83124
</div>
84125
<div>
85126
<p className="font-medium text-white dark:text-black">
86-
Email Notifications
127+
Browser Notifications
87128
</p>
88129
<p className="text-sm opacity-60">
89-
Get notified about activity
130+
Get notified about stream activity
90131
</p>
91132
</div>
92133
</div>
93134

94135
<button
95-
onClick={() =>
96-
setEmailNotifications(!emailNotifications)
97-
}
136+
onClick={handleBrowserPushToggle}
98137
className={`relative w-14 h-7 rounded-full transition-all duration-300 ${
99-
emailNotifications
138+
browserPush
100139
? "bg-gradient-to-r from-purple-500 to-blue-500"
101140
: "bg-zinc-600"
102141
}`}
103142
>
104143
<span
105144
className={`absolute top-1 left-1 w-5 h-5 bg-white rounded-full shadow-md transform transition duration-300 ${
106-
emailNotifications
145+
browserPush
107146
? "translate-x-7"
108147
: "translate-x-0"
109148
}`}
@@ -112,27 +151,94 @@ export default function SettingsPage() {
112151
</div>
113152

114153
{/* Theme Toggle */}
115-
<div className="flex items-center justify-between">
154+
<div className="space-y-4">
116155
<div className="flex items-center gap-3">
117156
<div className="p-2 rounded-lg bg-blue-500/20 text-blue-400">
118-
{theme === "dark" ? <Moon size={18} /> : <Sun size={18} />}
157+
{theme === "dark" ? <Moon size={18} /> : theme === "light" ? <Sun size={18} /> : <Globe size={18} />}
119158
</div>
120159
<div>
121160
<p className="font-medium text-white dark:text-black">
122161
Appearance
123162
</p>
124163
<p className="text-sm opacity-60">
125-
Toggle dark & light mode
164+
Choose your theme preference
126165
</p>
127166
</div>
128167
</div>
129168

130-
<button
131-
onClick={toggleTheme}
132-
className="px-4 py-2 text-sm rounded-xl border border-white/10 dark:border-black/10 hover:scale-105 transition-transform text-white dark:text-black"
133-
>
134-
{theme === "dark" ? "Light Mode" : "Dark Mode"}
135-
</button>
169+
<div className="flex gap-2">
170+
{(["light", "dark", "system"] as const).map((t) => (
171+
<button
172+
key={t}
173+
onClick={() => toggleTheme(t)}
174+
className={`px-4 py-2 text-sm rounded-xl border transition-all ${
175+
theme === t
176+
? "border-purple-500 bg-purple-500/20 text-white"
177+
: "border-white/10 dark:border-black/10 text-white/60 dark:text-black/60 hover:border-white/20"
178+
}`}
179+
>
180+
{t.charAt(0).toUpperCase() + t.slice(1)}
181+
</button>
182+
))}
183+
</div>
184+
</div>
185+
186+
{/* Display Preferences */}
187+
<div className="space-y-4">
188+
<div className="flex items-center gap-3">
189+
<div className="p-2 rounded-lg bg-green-500/20 text-green-400">
190+
<Globe size={18} />
191+
</div>
192+
<div>
193+
<p className="font-medium text-white dark:text-black">
194+
Display Preferences
195+
</p>
196+
<p className="text-sm opacity-60">
197+
Customize how amounts are displayed
198+
</p>
199+
</div>
200+
</div>
201+
202+
<div className="space-y-3 pl-12">
203+
<div>
204+
<label className="text-sm text-white/60 dark:text-black/60">Default Token</label>
205+
<select
206+
value={displayCurrency}
207+
onChange={(e) => {
208+
const val = e.target.value as DisplayCurrency;
209+
setDisplayCurrency(val);
210+
localStorage.setItem("flowfi-currency", val);
211+
}}
212+
className="mt-1 block w-full px-3 py-2 rounded-lg bg-black/40 dark:bg-white/40 border border-white/10 dark:border-black/10 text-white dark:text-black text-sm"
213+
>
214+
<option value="USD">USD</option>
215+
<option value="XLM">XLM</option>
216+
<option value="USDC">USDC</option>
217+
</select>
218+
</div>
219+
220+
<div>
221+
<label className="text-sm text-white/60 dark:text-black/60">Amount Format</label>
222+
<div className="flex gap-2 mt-1">
223+
{(["full", "compact"] as const).map((fmt) => (
224+
<button
225+
key={fmt}
226+
onClick={() => {
227+
setAmountFormat(fmt);
228+
localStorage.setItem("flowfi-amount-format", fmt);
229+
}}
230+
className={`px-3 py-1.5 text-xs rounded-lg border transition-all ${
231+
amountFormat === fmt
232+
? "border-blue-500 bg-blue-500/20 text-white"
233+
: "border-white/10 text-white/60 hover:border-white/20"
234+
}`}
235+
>
236+
{fmt === "full" ? "Full (1.0000000)" : "Compact (1.0)"}
237+
</button>
238+
))}
239+
</div>
240+
</div>
241+
</div>
136242
</div>
137243

138244
{/* Wallet Section */}
@@ -202,4 +308,4 @@ export default function SettingsPage() {
202308
</div>
203309
</div>
204310
);
205-
}
311+
}

frontend/src/components/Dashboard.tsx

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { fetchUserEvents } from '@/lib/dashboard';
44
import { useWallet } from '@/context/wallet-context';
55
import { BackendStreamEvent } from '@/lib/api-types';
66
import { downloadCSV } from '@/utils/csvExport';
7+
import toast from 'react-hot-toast';
8+
import { fromStroops } from '@/utils/amount';
79

810
interface StreamData extends Record<string, unknown> {
911
id: string;
@@ -44,21 +46,23 @@ const Dashboard: React.FC = () => {
4446
setEvents(data);
4547
} catch (error) {
4648
console.error(error);
49+
toast.error('Failed to load activity events');
4750
} finally {
4851
setIsLoadingEvents(false);
4952
}
5053
};
5154

5255
const handleExport = () => {
5356
downloadCSV(mockStreams, 'flowfi-stream-history.csv');
57+
toast.success('CSV exported successfully!');
5458
};
5559

5660
const handleTopUp = (streamId: string) => {
5761
const amount = prompt(`Enter amount to add to stream ${streamId}:`);
5862
if (amount && parseFloat(amount) > 0) {
5963
console.log(`Adding ${amount} funds to stream ${streamId}`);
6064
// TODO: Integrate with Soroban contract's top_up_stream function
61-
alert(`Successfully added ${amount} to stream ${streamId}`);
65+
toast.success(`Successfully added ${amount} to stream ${streamId}`);
6266
}
6367
};
6468

@@ -108,14 +112,14 @@ const Dashboard: React.FC = () => {
108112
<tr key={stream.id} className="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
109113
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{stream.date}</td>
110114
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 font-mono">{stream.recipient}</td>
111-
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100 font-semibold">{stream.deposited} {stream.token}</td>
112-
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{stream.withdrawn} {stream.token}</td>
115+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100 font-semibold">{fromStroops(BigInt(stream.deposited), 7)} {stream.token}</td>
116+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{fromStroops(BigInt(stream.withdrawn), 7)} {stream.token}</td>
113117
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{stream.token}</td>
114118
<td className="px-6 py-4 whitespace-nowrap text-sm">
115119
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full
116-
${stream.status === 'Active' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' :
117-
stream.status === 'Completed' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' :
118-
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'}`}>
120+
${stream.status === 'Active' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' :
121+
stream.status === 'Completed' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' :
122+
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'}`}>
119123
{stream.status}
120124
</span>
121125
</td>
@@ -135,7 +139,21 @@ const Dashboard: React.FC = () => {
135139
</table>
136140
</div>
137141
) : (
138-
<ActivityHistory events={events} isLoading={isLoadingEvents} />
142+
<>
143+
{isLoadingEvents ? (
144+
<div className="space-y-4">
145+
{[1, 2, 3].map((i) => (
146+
<div key={i} className="animate-pulse bg-white dark:bg-gray-800 shadow rounded-lg p-6">
147+
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/4 mb-4"></div>
148+
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-full mb-2"></div>
149+
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
150+
</div>
151+
))}
152+
</div>
153+
) : (
154+
<ActivityHistory events={events} isLoading={isLoadingEvents} />
155+
)}
156+
</>
139157
)}
140158
</div>
141159
);

frontend/src/components/IncomingStreams.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,18 @@
33
import React, { useState } from 'react';
44
import type { Stream } from '@/lib/dashboard';
55
import { useStreamingAmount } from '@/hooks/useStreamingAmount';
6+
import toast from 'react-hot-toast';
7+
import { fromStroops } from '@/utils/amount';
68

79
interface IncomingStreamsProps {
810
streams: Stream[];
911
onWithdraw: (stream: Stream) => Promise<void>;
1012
withdrawingStreamId?: string | null;
1113
}
1214

13-
function formatTokenAmount(value: number): string {
14-
if (!Number.isFinite(value)) return '0.0000';
15-
16-
return new Intl.NumberFormat('en-US', {
17-
minimumFractionDigits: 4,
18-
maximumFractionDigits: 4,
19-
}).format(value);
15+
function formatTokenAmount(value: number, decimals: number = 7): string {
16+
if (!Number.isFinite(value)) return '0.0000000';
17+
return fromStroops(BigInt(Math.floor(value)), decimals);
2018
}
2119

2220
const ClaimableAmount: React.FC<{ stream: Stream }> = ({ stream }) => {
@@ -62,6 +60,15 @@ const IncomingStreams: React.FC<IncomingStreamsProps> = ({
6260
setFilter(e.target.value as 'All' | 'Active' | 'Completed' | 'Paused');
6361
};
6462

63+
const handleWithdraw = async (stream: Stream) => {
64+
try {
65+
await onWithdraw(stream);
66+
toast.success(`Successfully withdrew from stream #${stream.id}`);
67+
} catch (error) {
68+
toast.error(`Failed to withdraw from stream #${stream.id}`);
69+
}
70+
};
71+
6572
return (
6673
<div className="bg-white/40 dark:bg-slate-900/40 backdrop-blur-md rounded-2xl border border-white/20 dark:border-white/10 shadow-xl overflow-hidden">
6774
<div className="p-6 border-b border-white/20 dark:border-white/10 flex flex-col md:flex-row md:items-center justify-between gap-4">
@@ -128,7 +135,7 @@ const IncomingStreams: React.FC<IncomingStreamsProps> = ({
128135
<button
129136
disabled={stream.status !== 'Active' || withdrawingStreamId === stream.id}
130137
onClick={() => {
131-
void onWithdraw(stream);
138+
void handleWithdraw(stream);
132139
}}
133140
className={`px-4 py-2 rounded-lg transition-all ${stream.status === 'Active'
134141
? 'bg-accent text-white hover:bg-accent-hover shadow-lg'

0 commit comments

Comments
 (0)