Skip to content

Commit 9dbe613

Browse files
committed
Update2
1 parent 2c39ad4 commit 9dbe613

2 files changed

Lines changed: 2 additions & 264 deletions

File tree

frontend/src/app/streams/create/page.tsx

Lines changed: 0 additions & 259 deletions
Original file line numberDiff line numberDiff line change
@@ -8,263 +8,4 @@ export const metadata: Metadata = {
88

99
export default function CreateStreamPage() {
1010
return <CreateStreamContent />;
11-
import React, { useState } from "react";
12-
import {
13-
createStream,
14-
toBaseUnits,
15-
toDurationSeconds,
16-
getTokenAddress,
17-
toSorobanErrorMessage,
18-
TOKEN_ADDRESSES
19-
} from "@/lib/soroban";
20-
import { hasValidPrecision, validateAmountInput } from "@/utils/amount";
21-
import { isValidStellarPublicKey } from "@/lib/stellar";
22-
import { toast } from "react-hot-toast";
23-
import { useRouter } from "next/navigation";
24-
import Link from "next/link";
25-
import { ArrowLeft } from "lucide-react";
26-
import { useWallet } from "@/context/wallet-context";
27-
28-
const TOKEN_DECIMALS = 7;
29-
30-
export default function CreateStreamPage() {
31-
const { status, session } = useWallet();
32-
const router = useRouter();
33-
const [nowTimestamp] = useState(() => Date.now());
34-
const [loading, setLoading] = useState(false);
35-
const [txState, setTxState] = useState<"idle" | "signing" | "submitted" | "confirming">("idle");
36-
const [formData, setFormData] = useState({
37-
recipient: "",
38-
token: "XLM",
39-
amount: "",
40-
duration: "30", // days
41-
});
42-
43-
const handleSubmit = async (e: React.FormEvent) => {
44-
e.preventDefault();
45-
if (status !== "connected" || !session) {
46-
toast.error("Please connect your wallet first.");
47-
return;
48-
}
49-
50-
// Validate recipient
51-
if (!formData.recipient.trim()) {
52-
toast.error("Recipient address is required");
53-
return;
54-
}
55-
if (!isValidStellarPublicKey(formData.recipient)) {
56-
toast.error("Invalid Stellar public key format");
57-
return;
58-
}
59-
60-
// Validate amount
61-
const validationError = validateAmountInput(formData.amount, TOKEN_DECIMALS);
62-
if (validationError) {
63-
toast.error(validationError);
64-
return;
65-
}
66-
67-
// Validate duration
68-
const durationNum = parseFloat(formData.duration);
69-
if (isNaN(durationNum) || durationNum <= 0) {
70-
toast.error("Duration must be a positive number");
71-
return;
72-
}
73-
74-
setLoading(true);
75-
setTxState("signing");
76-
77-
try {
78-
const amountBigInt = toBaseUnits(formData.amount);
79-
const durationBigInt = toDurationSeconds(formData.duration, "days");
80-
const tokenAddress = getTokenAddress(formData.token);
81-
82-
const result = await createStream(session, {
83-
recipient: formData.recipient,
84-
tokenAddress,
85-
amount: amountBigInt,
86-
durationSeconds: durationBigInt,
87-
});
88-
89-
if (result.success) {
90-
setTxState("confirming");
91-
toast.success("Stream created successfully!");
92-
// Small delay to allow indexer to catch up
93-
setTimeout(() => {
94-
router.push("/dashboard");
95-
}, 2000);
96-
}
97-
} catch (error) {
98-
console.error("Stream creation failed:", error);
99-
toast.error(toSorobanErrorMessage(error));
100-
} finally {
101-
setLoading(false);
102-
setTxState("idle");
103-
}
104-
};
105-
106-
const getButtonText = () => {
107-
if (!loading) return "Start Streaming";
108-
switch (txState) {
109-
case "signing": return "Confirm in Wallet...";
110-
case "submitted": return "Submitting to Network...";
111-
case "confirming": return "Finalizing Stream...";
112-
default: return "Processing...";
113-
}
114-
};
115-
116-
// Inline validation feedback for the amount field. validateAmountInput
117-
// returns an error message when invalid and null when valid. Only show it
118-
// once the user has typed something — the empty case is handled on submit.
119-
const amountError = formData.amount
120-
? validateAmountInput(formData.amount, TOKEN_DECIMALS)
121-
: null;
122-
123-
const recipientError = formData.recipient
124-
? (!isValidStellarPublicKey(formData.recipient) ? "Invalid Stellar public key format" : null)
125-
: null;
126-
127-
const durationError = formData.duration
128-
? (isNaN(Number(formData.duration)) || Number(formData.duration) <= 0
129-
? "Duration must be a positive number"
130-
: null)
131-
: null;
132-
133-
return (
134-
<div className="container mx-auto max-w-2xl px-4 py-12">
135-
<Link
136-
href="/dashboard"
137-
className="mb-8 inline-flex items-center text-sm font-medium text-slate-400 hover:text-white transition-colors"
138-
>
139-
<ArrowLeft className="mr-2 h-4 w-4" />
140-
Back to Dashboard
141-
</Link>
142-
143-
<div className="glass-card rounded-3xl border-slate-800 p-8">
144-
<h1 className="mb-2 text-3xl font-bold">Create New Stream</h1>
145-
<p className="mb-8 text-slate-400">
146-
Set up a real-time payment stream to any Stellar address.
147-
</p>
148-
149-
<form onSubmit={handleSubmit} className="space-y-6">
150-
<div className="space-y-2">
151-
<label className="text-sm font-medium text-slate-300">
152-
Recipient Address
153-
</label>
154-
<input
155-
type="text"
156-
placeholder="G..."
157-
className={`w-full rounded-xl border ${
158-
recipientError ? "border-red-500 focus:border-red-500" : "border-slate-800 focus:border-accent"
159-
} bg-slate-900/50 p-4 outline-none transition-colors`}
160-
value={formData.recipient}
161-
onChange={(e) => setFormData({ ...formData, recipient: e.target.value })}
162-
required
163-
/>
164-
{recipientError && (
165-
<p className="text-xs text-red-400 mt-1">{recipientError}</p>
166-
)}
167-
</div>
168-
169-
<div className="grid grid-cols-2 gap-4">
170-
<div className="space-y-2">
171-
<label className="text-sm font-medium text-slate-300">
172-
Token
173-
</label>
174-
<select
175-
className="w-full rounded-xl border border-slate-800 bg-slate-900/50 p-4 outline-none focus:border-accent transition-colors appearance-none"
176-
value={formData.token}
177-
onChange={(e) => setFormData({ ...formData, token: e.target.value })}
178-
>
179-
{Object.keys(TOKEN_ADDRESSES).map((symbol) => (
180-
<option key={symbol} value={symbol}>
181-
{symbol}
182-
</option>
183-
))}
184-
</select>
185-
</div>
186-
<div className="space-y-2">
187-
<label className="text-sm font-medium text-slate-300">
188-
Total Amount
189-
</label>
190-
<input
191-
type="text"
192-
inputMode="decimal"
193-
placeholder="0.00"
194-
className={`w-full rounded-xl border ${
195-
amountError ? "border-red-500 focus:border-red-500" : "border-slate-800 focus:border-accent"
196-
} bg-slate-900/50 p-4 outline-none transition-colors`}
197-
value={formData.amount}
198-
onChange={(e) => {
199-
const newValue = e.target.value;
200-
// Only allow valid number characters and check precision
201-
if (newValue === '' || /^\d*\.?\d*$/.test(newValue)) {
202-
if (hasValidPrecision(newValue, TOKEN_DECIMALS)) {
203-
setFormData({ ...formData, amount: newValue });
204-
}
205-
}
206-
}}
207-
required
208-
/>
209-
{amountError && (
210-
<p className="text-xs text-red-400 mt-1">{amountError}</p>
211-
)}
212-
</div>
213-
</div>
214-
215-
<div className="space-y-2">
216-
<label className="text-sm font-medium text-slate-300">
217-
Duration (Days)
218-
</label>
219-
<input
220-
type="number"
221-
placeholder="30"
222-
className={`w-full rounded-xl border ${
223-
durationError ? "border-red-500 focus:border-red-500" : "border-slate-800 focus:border-accent"
224-
} bg-slate-900/50 p-4 outline-none transition-colors`}
225-
value={formData.duration}
226-
onChange={(e) => setFormData({ ...formData, duration: e.target.value })}
227-
required
228-
/>
229-
{durationError && (
230-
<p className="text-xs text-red-400 mt-1">{durationError}</p>
231-
)}
232-
</div>
233-
234-
<div className="rounded-2xl bg-accent/5 p-6 space-y-4">
235-
<div className="flex justify-between items-center text-sm">
236-
<span className="text-slate-400">Streaming Rate</span>
237-
<span className="font-mono font-medium text-accent">
238-
{formData.amount && formData.duration && Number(formData.duration) > 0
239-
? (Number(formData.amount) / (Number(formData.duration) * 86400)).toFixed(8)
240-
: "0.00000000"} {formData.token}/sec
241-
</span>
242-
</div>
243-
<div className="flex justify-between items-center text-sm">
244-
<span className="text-slate-400">Estimated End Date</span>
245-
<span className="font-medium">
246-
{formData.duration && Number(formData.duration) > 0
247-
? new Date(nowTimestamp + Number(formData.duration) * 86400000).toLocaleDateString()
248-
: "—"}
249-
</span>
250-
</div>
251-
</div>
252-
253-
<button
254-
type="submit"
255-
disabled={loading || status !== "connected" || !!amountError || !!recipientError || !!durationError}
256-
className="w-full rounded-xl bg-accent py-4 text-lg font-bold text-background transition-all hover:opacity-90 disabled:opacity-50 active:scale-[0.98]"
257-
>
258-
{getButtonText()}
259-
</button>
260-
261-
{status !== "connected" && (
262-
<p className="text-center text-sm text-red-400">
263-
Please connect your wallet to create a stream.
264-
</p>
265-
)}
266-
</form>
267-
</div>
268-
</div>
269-
);
27011
}

frontend/src/components/dashboard/dashboard-view.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,8 @@ const SIDEBAR_ITEMS: SidebarItem[] = [
108108
/** Shimmer card used as a placeholder while data loads */
109109
function SkeletonCard({ className = "" }: { className?: string }) {
110110
return (
111-
<Skeleton
112-
className={`rounded-2xl ${className}`}
113-
aria-hidden="true"
114-
/>
115-
>
111+
<div className={`relative overflow-hidden rounded-2xl ${className}`} aria-hidden="true">
112+
<Skeleton className="w-full h-full" />
116113
{/* shimmer sweep */}
117114
<div className="absolute inset-0 -translate-x-full animate-shimmer bg-gradient-to-r from-transparent via-white/10 to-transparent" />
118115
</div>

0 commit comments

Comments
 (0)