Skip to content

Commit f1b9c24

Browse files
authored
Merge pull request #161 from 0xVida/issue137
feat: Frontend: Stream Creation Wizard
2 parents ae6a9ba + 79d86cc commit f1b9c24

4 files changed

Lines changed: 97 additions & 8 deletions

File tree

frontend/components/stream-creation/AmountStep.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"use client";
2-
import React from "react";
2+
import React, { useRef, useEffect } from "react";
33

44
interface AmountStepProps {
55
value: string;
@@ -14,6 +14,13 @@ export const AmountStep: React.FC<AmountStepProps> = ({
1414
error,
1515
token,
1616
}) => {
17+
const inputRef = useRef<HTMLInputElement>(null);
18+
19+
// Auto-focus on mount
20+
useEffect(() => {
21+
inputRef.current?.focus();
22+
}, []);
23+
1724
return (
1825
<div className="space-y-4">
1926
<div>
@@ -32,6 +39,7 @@ export const AmountStep: React.FC<AmountStepProps> = ({
3239
</label>
3340
<div className="relative">
3441
<input
42+
ref={inputRef}
3543
id="amount"
3644
type="number"
3745
step="any"

frontend/components/stream-creation/RecipientStep.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"use client";
2-
import React from "react";
2+
import React, { useRef, useEffect } from "react";
33

44
interface RecipientStepProps {
55
value: string;
@@ -12,6 +12,13 @@ export const RecipientStep: React.FC<RecipientStepProps> = ({
1212
onChange,
1313
error,
1414
}) => {
15+
const inputRef = useRef<HTMLInputElement>(null);
16+
17+
// Auto-focus on mount
18+
useEffect(() => {
19+
inputRef.current?.focus();
20+
}, []);
21+
1522
return (
1623
<div className="space-y-4">
1724
<div>
@@ -30,6 +37,7 @@ export const RecipientStep: React.FC<RecipientStepProps> = ({
3037
Stellar Public Key
3138
</label>
3239
<input
40+
ref={inputRef}
3341
id="recipient"
3442
type="text"
3543
value={value}

frontend/components/stream-creation/ScheduleStep.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"use client";
2-
import React, { useMemo } from "react";
2+
import React, { useMemo, useRef, useEffect } from "react";
33

44
interface ScheduleStepProps {
55
duration: string;
@@ -29,6 +29,13 @@ export const ScheduleStep: React.FC<ScheduleStepProps> = ({
2929
amount,
3030
token,
3131
}) => {
32+
const durationInputRef = useRef<HTMLInputElement>(null);
33+
34+
// Auto-focus on mount
35+
useEffect(() => {
36+
durationInputRef.current?.focus();
37+
}, []);
38+
3239
const ratePerSecond = useMemo(() => {
3340
if (!amount || !duration || parseFloat(amount) <= 0 || parseFloat(duration) <= 0) {
3441
return null;
@@ -91,6 +98,7 @@ export const ScheduleStep: React.FC<ScheduleStepProps> = ({
9198
Duration
9299
</label>
93100
<input
101+
ref={durationInputRef}
94102
id="duration"
95103
type="number"
96104
step="any"

frontend/components/stream-creation/StreamCreationWizard.tsx

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,26 @@ export const StreamCreationWizard: React.FC<StreamCreationWizardProps> = ({
9595
if (validateStep(currentStep)) {
9696
if (currentStep < STEPS.length) {
9797
setCurrentStep(currentStep + 1);
98+
// Scroll to top when moving to next step
99+
const modal = document.querySelector('.glass-card');
100+
if (modal) {
101+
modal.scrollTo({ top: 0, behavior: 'smooth' });
102+
}
103+
}
104+
} else {
105+
// Scroll to first error if validation fails
106+
const firstError = document.querySelector('[role="alert"]');
107+
if (firstError) {
108+
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
98109
}
99110
}
100111
};
101112

102113
const handleBack = () => {
103114
if (currentStep > 1) {
104115
setCurrentStep(currentStep - 1);
116+
// Scroll to top when going back
117+
window.scrollTo({ top: 0, behavior: 'smooth' });
105118
}
106119
};
107120

@@ -113,9 +126,14 @@ export const StreamCreationWizard: React.FC<StreamCreationWizardProps> = ({
113126
} catch (error) {
114127
console.error("Failed to create stream:", error);
115128
// Error handling can be added here (e.g., toast notification)
116-
} finally {
117129
setIsSubmitting(false);
118130
}
131+
} else {
132+
// Scroll to first error if validation fails
133+
const firstError = document.querySelector('[role="alert"]');
134+
if (firstError) {
135+
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
136+
}
119137
}
120138
};
121139

@@ -163,8 +181,27 @@ export const StreamCreationWizard: React.FC<StreamCreationWizardProps> = ({
163181
}
164182
};
165183

184+
// Handle Escape key to close
185+
React.useEffect(() => {
186+
const handleEscape = (e: KeyboardEvent) => {
187+
if (e.key === 'Escape') {
188+
onClose();
189+
}
190+
};
191+
window.addEventListener('keydown', handleEscape);
192+
return () => window.removeEventListener('keydown', handleEscape);
193+
}, [onClose]);
194+
166195
return (
167-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
196+
<div
197+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
198+
onClick={(e) => {
199+
// Close on backdrop click
200+
if (e.target === e.currentTarget) {
201+
onClose();
202+
}
203+
}}
204+
>
168205
<div className="glass-card relative z-10 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto rounded-2xl border border-glass-border p-8">
169206
<div className="flex items-center justify-between mb-6">
170207
<h2 className="text-2xl font-bold">Create Payment Stream</h2>
@@ -192,12 +229,25 @@ export const StreamCreationWizard: React.FC<StreamCreationWizardProps> = ({
192229

193230
<Stepper steps={STEPS} currentStep={currentStep} />
194231

195-
<div className="my-8 min-h-[300px]">{renderStepContent()}</div>
232+
<div className="my-8 min-h-[300px]">
233+
<div className="mb-6 flex items-center justify-between">
234+
<div className="text-sm text-slate-400">
235+
Step {currentStep} of {STEPS.length}
236+
</div>
237+
<div className="text-xs text-slate-500">
238+
{Math.round((currentStep / STEPS.length) * 100)}% complete
239+
</div>
240+
</div>
241+
{renderStepContent()}
242+
</div>
196243

197244
<div className="flex justify-between gap-4 pt-6 border-t border-glass-border">
198245
<div>
199246
{currentStep > 1 && (
200247
<Button variant="outline" onClick={handleBack}>
248+
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
249+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
250+
</svg>
201251
Back
202252
</Button>
203253
)}
@@ -207,10 +257,25 @@ export const StreamCreationWizard: React.FC<StreamCreationWizardProps> = ({
207257
Cancel
208258
</Button>
209259
{currentStep < STEPS.length ? (
210-
<Button onClick={handleNext}>Next</Button>
260+
<Button onClick={handleNext}>
261+
Next
262+
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
263+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
264+
</svg>
265+
</Button>
211266
) : (
212267
<Button onClick={handleSubmit} disabled={isSubmitting}>
213-
{isSubmitting ? "Creating..." : "Create Stream"}
268+
{isSubmitting ? (
269+
<>
270+
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
271+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
272+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
273+
</svg>
274+
Creating...
275+
</>
276+
) : (
277+
"Create Stream"
278+
)}
214279
</Button>
215280
)}
216281
</div>

0 commit comments

Comments
 (0)