Skip to content

Commit 742f4e3

Browse files
committed
feat: add loading states and visual feedback for onboarding steps
1 parent 7b75107 commit 742f4e3

File tree

4 files changed

+141
-82
lines changed

4 files changed

+141
-82
lines changed

src-frontend/components/onboarding/configure-step.tsx

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { ArrowLeft, ArrowRight, Folder } from "lucide-react";
1+
import { ArrowLeft, ArrowRight, Folder, Loader2 } from "lucide-react";
2+
import { useState } from "react";
23
import { Button } from "@/components/ui/button";
34
import {
45
FormControl,
@@ -25,6 +26,7 @@ interface ConfigureStepProps {
2526

2627
export function ConfigureStep({ onNext, onBack }: ConfigureStepProps) {
2728
const { control, getValues, setValue, trigger } = useFormContext();
29+
const [isConfiguring, setIsConfiguring] = useState(false);
2830

2931
const handleDirectorySelect = async () => {
3032
if (typeof window === "undefined" || !window.__TAURI__) return;
@@ -41,7 +43,14 @@ export function ConfigureStep({ onNext, onBack }: ConfigureStepProps) {
4143
const handleConfigureClient = async () => {
4244
const isValid = await trigger(["dataDir", "serverUrl"]);
4345
if (isValid) {
44-
onNext();
46+
setIsConfiguring(true);
47+
try {
48+
// Simulate some configuration time or add actual configuration logic here
49+
await new Promise((resolve) => setTimeout(resolve, 500));
50+
onNext();
51+
} finally {
52+
setIsConfiguring(false);
53+
}
4554
}
4655
};
4756

@@ -81,6 +90,7 @@ export function ConfigureStep({ onNext, onBack }: ConfigureStepProps) {
8190
variant="outline"
8291
onClick={handleDirectorySelect}
8392
type="button"
93+
disabled={isConfiguring}
8494
>
8595
<Folder className="mr-2 h-4 w-4" />
8696
Browse
@@ -113,14 +123,32 @@ export function ConfigureStep({ onNext, onBack }: ConfigureStepProps) {
113123
</CardContent>
114124

115125
<CardFooter className="flex justify-between">
116-
<Button variant="outline" className="cursor-pointer" onClick={onBack}>
126+
<Button
127+
variant="outline"
128+
className="cursor-pointer"
129+
onClick={onBack}
130+
disabled={isConfiguring}
131+
>
117132
<ArrowLeft className="mr-2 h-4 w-4" />
118133
Back
119134
</Button>
120135

121-
<Button className="cursor-pointer" onClick={handleConfigureClient}>
122-
Next
123-
<ArrowRight className="ml-2 h-4 w-4" />
136+
<Button
137+
className="cursor-pointer"
138+
onClick={handleConfigureClient}
139+
disabled={isConfiguring}
140+
>
141+
{isConfiguring ? (
142+
<>
143+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
144+
Configuring...
145+
</>
146+
) : (
147+
<>
148+
Next
149+
<ArrowRight className="ml-2 h-4 w-4" />
150+
</>
151+
)}
124152
</Button>
125153
</CardFooter>
126154
</Card>

src-frontend/components/onboarding/connect-step.tsx

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export function ConnectStep({
4141
setIsLoading,
4242
}: ConnectStepProps) {
4343
const [showToken, setShowToken] = useState(false);
44+
const [isConnecting, setIsConnecting] = useState(false);
4445
const { updateSettings, status, datasite, connect, settings } =
4546
useConnectionStore();
4647

@@ -58,34 +59,39 @@ export function ConnectStep({
5859
}, [settings.url, settings.token, form]);
5960

6061
const handleConnectDaemon = async (values: ConnectionFormValues) => {
62+
setIsConnecting(true);
6163
setIsLoading(true);
6264

63-
// Update connection settings from form values
64-
updateSettings({
65-
url: values.url,
66-
token: values.token,
67-
});
65+
try {
66+
// Update connection settings from form values
67+
updateSettings({
68+
url: values.url,
69+
token: values.token,
70+
});
6871

69-
// Attempt connection
70-
const result = await connect();
71-
setIsLoading(false);
72+
// Attempt connection
73+
const result = await connect();
7274

73-
// Error handling
74-
if (result.success) {
75-
if (isConfigValid(datasite?.status)) {
76-
onComplete();
75+
// Error handling
76+
if (result.success) {
77+
if (isConfigValid(datasite?.status)) {
78+
onComplete();
79+
} else {
80+
onNext();
81+
}
7782
} else {
78-
onNext();
83+
Object.entries(result.errors).forEach(([key, value]) => {
84+
if (value && key in values) {
85+
form.setError(key as keyof ConnectionFormValues, {
86+
type: "manual",
87+
message: value,
88+
});
89+
}
90+
});
7991
}
80-
} else {
81-
Object.entries(result.errors).forEach(([key, value]) => {
82-
if (value && key in values) {
83-
form.setError(key as keyof ConnectionFormValues, {
84-
type: "manual",
85-
message: value,
86-
});
87-
}
88-
});
92+
} finally {
93+
setIsConnecting(false);
94+
setIsLoading(false);
8995
}
9096
};
9197

@@ -164,9 +170,9 @@ export function ConnectStep({
164170
<Button
165171
type="submit"
166172
className="w-full cursor-pointer"
167-
disabled={isLoading || status === "connecting"}
173+
disabled={isLoading || status === "connecting" || isConnecting}
168174
>
169-
{isLoading || status === "connecting" ? (
175+
{isLoading || status === "connecting" || isConnecting ? (
170176
<>
171177
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
172178
Connecting...

src-frontend/components/onboarding/email-step.tsx

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useFormContext } from "react-hook-form";
2-
import { ArrowLeft, ArrowRight, Mail } from "lucide-react";
2+
import { ArrowLeft, ArrowRight, Mail, Loader2 } from "lucide-react";
33
import { Button } from "@/components/ui/button";
44
import {
55
FormControl,
@@ -22,6 +22,7 @@ import { toast } from "@/hooks/use-toast";
2222
import { GoogleIcon } from "@/components/logo/google-icon";
2323
import { GithubIcon } from "@/components/logo/github-icon";
2424
import { useConnectionStore } from "@/stores";
25+
import { useState } from "react";
2526

2627
interface EmailStepProps {
2728
onNext: (email: string) => void;
@@ -31,6 +32,7 @@ interface EmailStepProps {
3132

3233
export function EmailStep({ onNext, onBack, isLoading }: EmailStepProps) {
3334
const { control, getValues, trigger, resetField } = useFormContext();
35+
const [isSendingCode, setIsSendingCode] = useState(false);
3436
const {
3537
settings: { url, token },
3638
} = useConnectionStore();
@@ -50,28 +52,34 @@ export function EmailStep({ onNext, onBack, isLoading }: EmailStepProps) {
5052
const isValid = await trigger(["email"]);
5153
if (!isValid) return;
5254

53-
// Clear the token field in the form
54-
resetField("token");
55+
setIsSendingCode(true);
5556

56-
const response = await fetch(
57-
`${url}/v1/init/token?email=${getValues("email")}&server_url=${getValues("serverUrl")}`,
58-
{
59-
headers: {
60-
"Content-Type": "application/json",
61-
Authorization: `Bearer ${token}`,
57+
try {
58+
// Clear the token field in the form
59+
resetField("token");
60+
61+
const response = await fetch(
62+
`${url}/v1/init/token?email=${getValues("email")}&server_url=${getValues("serverUrl")}`,
63+
{
64+
headers: {
65+
"Content-Type": "application/json",
66+
Authorization: `Bearer ${token}`,
67+
},
6268
},
63-
},
64-
);
65-
if (!response.ok) {
66-
const data = await response.json();
67-
toast({
68-
icon: "❌",
69-
title: "Error",
70-
description: `Failed to send verification code. Error: ${data.message}`,
71-
});
72-
return;
69+
);
70+
if (!response.ok) {
71+
const data = await response.json();
72+
toast({
73+
icon: "❌",
74+
title: "Error",
75+
description: `Failed to send verification code. Error: ${data.message}`,
76+
});
77+
return;
78+
}
79+
onNext();
80+
} finally {
81+
setIsSendingCode(false);
7382
}
74-
onNext();
7583
};
7684

7785
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
@@ -103,7 +111,7 @@ export function EmailStep({ onNext, onBack, isLoading }: EmailStepProps) {
103111
variant="outline"
104112
className="w-full cursor-pointer"
105113
onClick={() => handleOAuthLogin("Google")}
106-
disabled={isLoading}
114+
disabled={isLoading || isSendingCode}
107115
type="button"
108116
>
109117
<GoogleIcon className="mr-2 h-4 w-4" />
@@ -113,7 +121,7 @@ export function EmailStep({ onNext, onBack, isLoading }: EmailStepProps) {
113121
variant="outline"
114122
className="w-full cursor-pointer"
115123
onClick={() => handleOAuthLogin("GitHub")}
116-
disabled={isLoading}
124+
disabled={isLoading || isSendingCode}
117125
type="button"
118126
>
119127
<GithubIcon className="mr-2 h-4 w-4" />
@@ -143,7 +151,7 @@ export function EmailStep({ onNext, onBack, isLoading }: EmailStepProps) {
143151
placeholder="you@example.com"
144152
className="h-11 pl-10"
145153
onKeyDown={handleKeyDown}
146-
disabled={isLoading}
154+
disabled={isLoading || isSendingCode}
147155
{...field}
148156
/>
149157
</div>
@@ -162,18 +170,27 @@ export function EmailStep({ onNext, onBack, isLoading }: EmailStepProps) {
162170
variant="outline"
163171
className="cursor-pointer"
164172
onClick={onBack}
165-
disabled={isLoading}
173+
disabled={isLoading || isSendingCode}
166174
>
167175
<ArrowLeft className="mr-2 h-4 w-4" />
168176
Back
169177
</Button>
170178
<Button
171179
className="cursor-pointer"
172180
onClick={handleEmailLogin}
173-
disabled={isLoading}
181+
disabled={isLoading || isSendingCode}
174182
>
175-
Continue with Email
176-
<ArrowRight className="ml-2 h-4 w-4" />
183+
{isLoading || isSendingCode ? (
184+
<>
185+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
186+
Sending Code...
187+
</>
188+
) : (
189+
<>
190+
Continue with Email
191+
<ArrowRight className="ml-2 h-4 w-4" />
192+
</>
193+
)}
177194
</Button>
178195
</CardFooter>
179196
</Card>

src-frontend/components/onboarding/verify-step.tsx

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from "@/components/ui/card";
2121
import { useConnectionStore } from "@/stores";
2222
import { toast } from "@/hooks/use-toast";
23+
import { useState } from "react";
2324

2425
interface VerifyStepProps {
2526
onComplete: () => void;
@@ -29,6 +30,7 @@ interface VerifyStepProps {
2930

3031
export function VerifyStep({ onComplete, onBack, isLoading }: VerifyStepProps) {
3132
const { control, getValues, trigger } = useFormContext();
33+
const [isVerifying, setIsVerifying] = useState(false);
3234
const {
3335
settings: { url, token },
3436
} = useConnectionStore();
@@ -37,31 +39,37 @@ export function VerifyStep({ onComplete, onBack, isLoading }: VerifyStepProps) {
3739
const isValid = await trigger(["token"]);
3840
if (!isValid) return;
3941

40-
const response = await fetch(`${url}/v1/init/datasite`, {
41-
method: "POST",
42-
headers: {
43-
"Content-Type": "application/json",
44-
Authorization: `Bearer ${token}`,
45-
},
46-
body: JSON.stringify({
47-
dataDir: getValues("dataDir"),
48-
email: getValues("email"),
49-
serverUrl: getValues("serverUrl"),
50-
token: getValues("token"),
51-
}),
52-
});
42+
setIsVerifying(true);
5343

54-
if (!response.ok) {
55-
toast({
56-
icon: "❌",
57-
variant: "destructive",
58-
title: "Failed to verify email",
59-
description: "Please try again",
44+
try {
45+
const response = await fetch(`${url}/v1/init/datasite`, {
46+
method: "POST",
47+
headers: {
48+
"Content-Type": "application/json",
49+
Authorization: `Bearer ${token}`,
50+
},
51+
body: JSON.stringify({
52+
dataDir: getValues("dataDir"),
53+
email: getValues("email"),
54+
serverUrl: getValues("serverUrl"),
55+
token: getValues("token"),
56+
}),
6057
});
61-
return;
62-
}
6358

64-
onComplete();
59+
if (!response.ok) {
60+
toast({
61+
icon: "❌",
62+
variant: "destructive",
63+
title: "Failed to verify email",
64+
description: "Please try again",
65+
});
66+
return;
67+
}
68+
69+
onComplete();
70+
} finally {
71+
setIsVerifying(false);
72+
}
6573
};
6674

6775
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
@@ -111,17 +119,17 @@ export function VerifyStep({ onComplete, onBack, isLoading }: VerifyStepProps) {
111119
variant="outline"
112120
className="cursor-pointer"
113121
onClick={onBack}
114-
disabled={isLoading}
122+
disabled={isLoading || isVerifying}
115123
>
116124
<ArrowLeft className="mr-2 h-4 w-4" />
117125
Back
118126
</Button>
119127
<Button
120128
className="cursor-pointer"
121-
disabled={isLoading}
129+
disabled={isLoading || isVerifying}
122130
onClick={handleCompleteSetup}
123131
>
124-
{isLoading ? (
132+
{isLoading || isVerifying ? (
125133
<>
126134
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
127135
Verifying...

0 commit comments

Comments
 (0)