Skip to content

Commit 32227a5

Browse files
authored
fix: smooth dashboard shell and auth reset flow (#207)
1 parent 48e17a1 commit 32227a5

6 files changed

Lines changed: 171 additions & 29 deletions

File tree

apps/web/src/app/ui/chart.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import * as React from "react";
44
import type { TooltipValueType } from "recharts";
55
import * as RechartsPrimitive from "recharts";
6+
import { SIDEBAR_SHELL_EXPAND_DURATION_MS } from "@/app/ui/sidebar";
67
import { cn } from "@/lib/utils";
78

89
const THEMES = { light: "", dark: ".dark" } as const;
@@ -43,6 +44,7 @@ function ChartContainer({
4344
children,
4445
config,
4546
initialDimension = INITIAL_DIMENSION,
47+
responsiveDebounce = SIDEBAR_SHELL_EXPAND_DURATION_MS,
4648
...props
4749
}: React.ComponentProps<"div"> & {
4850
config: ChartConfig;
@@ -53,6 +55,7 @@ function ChartContainer({
5355
width: number;
5456
height: number;
5557
};
58+
responsiveDebounce?: number;
5659
}) {
5760
const uniqueId = React.useId();
5861
const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}`;
@@ -70,6 +73,7 @@ function ChartContainer({
7073
>
7174
<ChartStyle id={chartId} config={config} />
7275
<RechartsPrimitive.ResponsiveContainer
76+
debounce={responsiveDebounce}
7377
initialDimension={initialDimension}
7478
>
7579
{children}

apps/web/src/app/ui/sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const SIDEBAR_KEYBOARD_SHORTCUT = "b";
2929
const SIDEBAR_SHELL_EASING_BASELINE = "cubic-bezier(0.23,1,0.32,1)";
3030
// Shell width motion is intentionally disabled for diagnosis right now.
3131
// Both open and close snap instantly until we restore a tuned transition.
32-
const SIDEBAR_SHELL_EXPAND_DURATION_MS = 500;
32+
export const SIDEBAR_SHELL_EXPAND_DURATION_MS = 500;
3333
export const SIDEBAR_SHELL_COLLAPSE_DURATION_MS = 500;
3434

3535
export type SidebarShellMotionVariant = "baseline" | "geometry-trace";

apps/web/src/features/auth/LoginForm.tsx

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ import { Label } from "@/app/ui/label";
1212
import { Separator } from "@/app/ui/separator";
1313
import { useAnalyticsTracking } from "@/features/analytics/tracking/useAnalyticsTracking";
1414
import { authClient } from "@/lib/auth-client";
15+
import { cn } from "@/lib/utils";
16+
17+
type FeedbackState = {
18+
kind: "error" | "success";
19+
message: string;
20+
} | null;
1521

1622
function getCallbackURL(): string {
1723
const params = new URLSearchParams(window.location.search);
@@ -38,30 +44,75 @@ export function LoginForm({
3844
}) {
3945
const [email, setEmail] = useState("");
4046
const [password, setPassword] = useState("");
41-
const [error, setError] = useState("");
47+
const [feedback, setFeedback] = useState<FeedbackState>(null);
4248
const [loading, setLoading] = useState(false);
49+
const [requestingPasswordReset, setRequestingPasswordReset] = useState(false);
4350
const { trackAuthenticationAction } = useAnalyticsTracking({
4451
pageName: "login",
4552
});
4653

4754
async function handleSubmit(e: React.FormEvent) {
4855
e.preventDefault();
56+
setFeedback(null);
4957
trackAuthenticationAction({
5058
actionName: "sign_in",
5159
sourceComponent: "login_form",
5260
authMethod: "email_password",
5361
});
54-
setError("");
5562
setLoading(true);
5663
const { error } = await authClient.signIn.email({ email, password });
5764
setLoading(false);
5865
if (error) {
59-
setError(error.message ?? "Sign in failed");
66+
setFeedback({
67+
kind: "error",
68+
message: error.message ?? "Sign in failed",
69+
});
70+
}
71+
}
72+
73+
async function handleRequestPasswordReset() {
74+
if (!email.trim()) {
75+
const emailField = document.getElementById("email");
76+
if (emailField instanceof HTMLInputElement) {
77+
emailField.focus();
78+
}
79+
setFeedback({
80+
kind: "error",
81+
message:
82+
"Enter your email first and we will send the reset link there.",
83+
});
84+
return;
6085
}
86+
87+
trackAuthenticationAction({
88+
actionName: "request_password_reset",
89+
sourceComponent: "login_form",
90+
authMethod: "email_password",
91+
});
92+
setFeedback(null);
93+
setRequestingPasswordReset(true);
94+
const { error } = await authClient.requestPasswordReset({
95+
email,
96+
redirectTo: `${window.location.origin}/reset-password`,
97+
});
98+
setRequestingPasswordReset(false);
99+
100+
if (error) {
101+
setFeedback({
102+
kind: "error",
103+
message: error.message ?? "Could not send password reset email",
104+
});
105+
return;
106+
}
107+
108+
setFeedback({
109+
kind: "success",
110+
message: `If an account exists for ${email.trim()}, a reset link is on its way.`,
111+
});
61112
}
62113

63114
async function handleSocialSignIn(provider: "google" | "github") {
64-
setError("");
115+
setFeedback(null);
65116
trackAuthenticationAction({
66117
actionName: "sign_in",
67118
sourceComponent: "login_form",
@@ -72,7 +123,10 @@ export function LoginForm({
72123
callbackURL: getCallbackURL(),
73124
});
74125
if (error) {
75-
setError(error.message ?? `Sign in with ${provider} failed`);
126+
setFeedback({
127+
kind: "error",
128+
message: error.message ?? `Sign in with ${provider} failed`,
129+
});
76130
}
77131
}
78132

@@ -90,25 +144,70 @@ export function LoginForm({
90144
<Label htmlFor="email">Email</Label>
91145
<Input
92146
id="email"
147+
name="email"
93148
type="email"
149+
autoComplete="email"
94150
placeholder="you@example.com"
95151
value={email}
96-
onChange={(e) => setEmail(e.target.value)}
152+
onChange={(e) => {
153+
setEmail(e.target.value);
154+
if (feedback) {
155+
setFeedback(null);
156+
}
157+
}}
97158
required
98159
/>
99160
</div>
100161
<div className="flex flex-col gap-2">
101-
<Label htmlFor="password">Password</Label>
162+
<div className="flex items-center justify-between gap-3">
163+
<Label htmlFor="password">Password</Label>
164+
<Button
165+
type="button"
166+
variant="ghost"
167+
size="xs"
168+
onClick={() => {
169+
void handleRequestPasswordReset();
170+
}}
171+
disabled={requestingPasswordReset}
172+
className="text-muted-foreground hover:text-foreground"
173+
>
174+
{requestingPasswordReset
175+
? "Sending link..."
176+
: feedback?.kind === "success"
177+
? "Resend link"
178+
: "Forgot password?"}
179+
</Button>
180+
</div>
102181
<Input
103182
id="password"
183+
name="password"
104184
type="password"
185+
autoComplete="current-password"
105186
value={password}
106-
onChange={(e) => setPassword(e.target.value)}
187+
onChange={(e) => {
188+
setPassword(e.target.value);
189+
if (feedback?.kind === "error") {
190+
setFeedback(null);
191+
}
192+
}}
107193
required
108194
/>
109195
</div>
110-
{error && <p className="text-sm text-destructive">{error}</p>}
111-
<Button type="submit" disabled={loading}>
196+
{feedback ? (
197+
<div
198+
role={feedback.kind === "error" ? "alert" : "status"}
199+
aria-live="polite"
200+
className={cn(
201+
"rounded-3xl px-3 py-2 text-sm leading-5 ring-1",
202+
feedback.kind === "error"
203+
? "bg-destructive/5 text-destructive ring-destructive/15"
204+
: "bg-muted/35 text-muted-foreground ring-border/60",
205+
)}
206+
>
207+
{feedback.message}
208+
</div>
209+
) : null}
210+
<Button type="submit" disabled={loading || requestingPasswordReset}>
112211
{loading ? "Signing in..." : "Sign in"}
113212
</Button>
114213
</form>

apps/web/src/features/auth/ResetPasswordForm.tsx

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import {
1010
import { Input } from "@/app/ui/input";
1111
import { Label } from "@/app/ui/label";
1212
import { authClient } from "@/lib/auth-client";
13+
import { cn } from "@/lib/utils";
14+
15+
type FeedbackState = {
16+
kind: "error" | "success";
17+
message: string;
18+
} | null;
1319

1420
export function ResetPasswordForm({
1521
onBackToLogin,
@@ -19,22 +25,28 @@ export function ResetPasswordForm({
1925
const token = new URLSearchParams(window.location.search).get("token");
2026
const invalidToken = new URLSearchParams(window.location.search).get("error");
2127
const [password, setPassword] = useState("");
22-
const [error, setError] = useState(
23-
invalidToken ? "This reset link is invalid or expired." : "",
28+
const [feedback, setFeedback] = useState<FeedbackState>(
29+
invalidToken
30+
? {
31+
kind: "error",
32+
message: "This reset link is invalid or expired.",
33+
}
34+
: null,
2435
);
25-
const [successMessage, setSuccessMessage] = useState("");
2636
const [loading, setLoading] = useState(false);
2737

2838
async function handleSubmit(e: React.FormEvent) {
2939
e.preventDefault();
3040

3141
if (!token) {
32-
setError("This reset link is invalid or expired.");
42+
setFeedback({
43+
kind: "error",
44+
message: "This reset link is invalid or expired.",
45+
});
3346
return;
3447
}
3548

36-
setError("");
37-
setSuccessMessage("");
49+
setFeedback(null);
3850
setLoading(true);
3951

4052
const { error } = await authClient.resetPassword({
@@ -45,11 +57,17 @@ export function ResetPasswordForm({
4557
setLoading(false);
4658

4759
if (error) {
48-
setError(error.message ?? "Password reset failed");
60+
setFeedback({
61+
kind: "error",
62+
message: error.message ?? "Password reset failed",
63+
});
4964
return;
5065
}
5166

52-
setSuccessMessage("Your password has been reset. You can now sign in.");
67+
setFeedback({
68+
kind: "success",
69+
message: "Your password has been reset. You can now sign in.",
70+
});
5371
setPassword("");
5472
}
5573

@@ -65,28 +83,49 @@ export function ResetPasswordForm({
6583
<Label htmlFor="new-password">New password</Label>
6684
<Input
6785
id="new-password"
86+
name="newPassword"
6887
type="password"
88+
autoComplete="new-password"
6989
value={password}
70-
onChange={(e) => setPassword(e.target.value)}
90+
onChange={(e) => {
91+
setPassword(e.target.value);
92+
if (feedback?.kind === "error") {
93+
setFeedback(null);
94+
}
95+
}}
7196
required
97+
minLength={8}
98+
disabled={!token || loading}
7299
/>
73100
</div>
74-
{error ? <p className="text-sm text-destructive">{error}</p> : null}
75-
{successMessage ? (
76-
<p className="text-sm text-muted-foreground">{successMessage}</p>
101+
{feedback ? (
102+
<div
103+
role={feedback.kind === "error" ? "alert" : "status"}
104+
aria-live="polite"
105+
className={cn(
106+
"rounded-3xl px-3 py-2 text-sm leading-5 ring-1",
107+
feedback.kind === "error"
108+
? "bg-destructive/5 text-destructive ring-destructive/15"
109+
: "bg-muted/35 text-muted-foreground ring-border/60",
110+
)}
111+
>
112+
{feedback.message}
113+
</div>
77114
) : null}
78115
<Button type="submit" disabled={loading || !token}>
79116
{loading ? "Resetting password..." : "Reset password"}
80117
</Button>
81118
</form>
82119

83-
<button
120+
<Button
84121
type="button"
122+
variant="ghost"
123+
size="sm"
85124
onClick={onBackToLogin}
86-
className="text-left text-sm underline underline-offset-4 hover:text-primary"
125+
className="self-start text-muted-foreground"
87126
>
88127
Back to sign in
89-
</button>
128+
</Button>
90129
</CardContent>
91130
</Card>
92131
);

apps/web/src/features/shell/AppShellLayout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export function AppShellLayout() {
135135
return (
136136
<TooltipProvider>
137137
<div
138-
className="dashboard-01-preview h-dvh overflow-hidden text-foreground"
138+
className="dashboard-01-preview h-dvh overflow-hidden overscroll-none text-foreground"
139139
data-sidebar-news-hide-performance-chart-debug={
140140
sidebarTuning.newsHidePerformanceChartWhileActive ? "true" : "false"
141141
}
@@ -150,7 +150,7 @@ export function AppShellLayout() {
150150
? () => {}
151151
: undefined
152152
}
153-
className="dashboard-01-chrome-frame h-full overflow-hidden"
153+
className="dashboard-01-chrome-frame h-full overflow-hidden overscroll-none"
154154
style={
155155
{
156156
"--sidebar-width": `${sidebarTuning.expandedWidth}rem`,

apps/web/src/features/shell/components/AppSidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ function getSidebarFooterStackClassName(mode: SidebarDisplayMode) {
8787

8888
function getSidebarContentFrameClassName(mode: SidebarDisplayMode) {
8989
return cn(
90-
"relative flex h-full min-h-0 flex-col bg-transparent",
90+
"relative flex h-full min-h-0 flex-col overscroll-none bg-transparent",
9191
mode === "expanded"
9292
? "w-(--sidebar-width) overflow-x-clip overflow-y-auto text-clip whitespace-nowrap"
9393
: "w-(--sidebar-width-icon) pb-1.5",

0 commit comments

Comments
 (0)