Skip to content

Commit 9a1d759

Browse files
ervwalterclaude
andcommitted
💄 style: improve UI components and fix navigation guard
- Replace confirm dialogs with proper ConfirmDialog components - Add toast notifications for user feedback on actions - Fix navigation guard to properly handle form state - Make provider buttons responsive on mobile screens - Fix HTML nesting issues in dialog descriptions - Add CSS animations for toast notifications - Reorganize toast components to fix React fast refresh warnings 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 29d16e6 commit 9a1d759

12 files changed

Lines changed: 347 additions & 92 deletions

File tree

‎apps/web/package-lock.json‎

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎apps/web/package.json‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
{
23
"name": "web",
34
"private": true,
@@ -22,8 +23,9 @@
2223
"@js-joda/timezone": "2.22.0",
2324
"@radix-ui/react-alert-dialog": "1.1.14",
2425
"@radix-ui/react-icons": "1.3.2",
25-
"@radix-ui/react-radio-group": "1.3.7",
26-
"@radix-ui/react-switch": "1.2.5",
26+
"@radix-ui/react-radio-group": "^1.3.7",
27+
"@radix-ui/react-switch": "^1.2.5",
28+
"@radix-ui/react-toast": "^1.2.14",
2729
"@radix-ui/react-toggle-group": "1.1.10",
2830
"@radix-ui/themes": "3.2.1",
2931
"@supabase/supabase-js": "2.50.3",

‎apps/web/src/App.tsx‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,24 @@ import { queryClient } from "./lib/queryClient";
88
import { BackgroundQueryProgress } from "./lib/progress/BackgroundQueryProgress";
99
import { ProgressManager } from "./lib/progress/ProgressManager";
1010
import { ErrorBoundary } from "./components/ErrorBoundary";
11+
import { ToastContextProvider } from "./components/ui/ToastProvider";
1112
import "@bprogress/core/css";
1213
import "./lib/progress/progress.css";
1314

1415
function App() {
1516
return (
1617
<ErrorBoundary>
1718
<QueryClientProvider client={queryClient}>
18-
<ProgressProvider color="#eef5ff" height="3px" delay={250} spinnerPosition="top-right">
19-
<ProgressManager />
20-
<AuthProvider>
21-
<BackgroundQueryProgress />
22-
<RouterProvider router={router} />
23-
</AuthProvider>
24-
</ProgressProvider>
25-
<ReactQueryDevtools initialIsOpen={false} />
19+
<ToastContextProvider>
20+
<ProgressProvider color="#eef5ff" height="3px" delay={250} spinnerPosition="top-right">
21+
<ProgressManager />
22+
<AuthProvider>
23+
<BackgroundQueryProgress />
24+
<RouterProvider router={router} />
25+
</AuthProvider>
26+
</ProgressProvider>
27+
<ReactQueryDevtools initialIsOpen={false} />
28+
</ToastContextProvider>
2629
</QueryClientProvider>
2730
</ErrorBoundary>
2831
);
Lines changed: 111 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,16 @@
1-
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2-
import { apiRequest } from "../../lib/api/client";
3-
4-
interface ProviderLink {
5-
provider: string;
6-
connectedAt: string;
7-
updateReason?: string;
8-
hasToken: boolean;
9-
}
1+
import { useState } from "react";
2+
import { useProviderLinks } from "../../lib/api/queries";
3+
import { useDisconnectProvider, useResyncProvider } from "../../lib/api/mutations";
4+
import { useToast } from "../../lib/hooks/useToast";
5+
import { ConfirmDialog } from "../ui/ConfirmDialog";
106

117
export function ConnectedAccountsSection() {
12-
const queryClient = useQueryClient();
13-
14-
const { data: providerLinks, isLoading } = useQuery({
15-
queryKey: ["providerLinks"],
16-
queryFn: () => apiRequest<ProviderLink[]>("/providers/links"),
17-
});
8+
const { data: providerLinks, isLoading } = useProviderLinks();
9+
const { showToast } = useToast();
10+
const [disconnectProvider, setDisconnectProvider] = useState<{ id: string; name: string } | null>(null);
1811

19-
const disconnectMutation = useMutation({
20-
mutationFn: (provider: string) => apiRequest(`/providers/${provider}`, { method: "DELETE" }),
21-
onSuccess: () => {
22-
queryClient.invalidateQueries({ queryKey: ["providerLinks"] });
23-
},
24-
});
25-
26-
const resyncMutation = useMutation({
27-
mutationFn: (provider: string) => apiRequest(`/providers/${provider}/resync`, { method: "POST" }),
28-
});
12+
const disconnectMutation = useDisconnectProvider();
13+
const resyncMutation = useResyncProvider();
2914

3015
const providers = [
3116
{
@@ -54,61 +39,112 @@ export function ConnectedAccountsSection() {
5439
const connectedProviders = new Set(providerLinks?.map((link) => link.provider) || []);
5540

5641
return (
57-
<div className="p-6">
58-
<h2 className="text-xl font-semibold mb-4">Connected Accounts</h2>
42+
<>
43+
<div className="p-6">
44+
<h2 className="text-xl font-semibold mb-4">Connected Accounts</h2>
5945

60-
<div className="space-y-4">
61-
{providers.map((provider) => {
62-
const isConnected = connectedProviders.has(provider.id);
63-
const providerLink = providerLinks?.find((link) => link.provider === provider.id);
46+
<div className="space-y-4">
47+
{providers.map((provider) => {
48+
const isConnected = connectedProviders.has(provider.id);
49+
const providerLink = providerLinks?.find((link) => link.provider === provider.id);
6450

65-
return (
66-
<div key={provider.id} className="flex items-center justify-between p-4 border border-gray-200 rounded-lg">
67-
<div className="flex items-center space-x-4">
68-
<div className="text-2xl">{provider.icon}</div>
69-
<div>
70-
<h3 className="font-medium text-gray-900">{provider.name}</h3>
71-
<p className="text-sm text-gray-600">{provider.description}</p>
72-
{isConnected && providerLink && (
73-
<p className="text-xs text-gray-500 mt-1">Connected {new Date(providerLink.connectedAt).toLocaleDateString()}</p>
74-
)}
51+
return (
52+
<div key={provider.id} className="flex flex-col sm:flex-row sm:items-center sm:justify-between p-4 border border-gray-200 rounded-lg space-y-3 sm:space-y-0">
53+
<div className="flex items-center space-x-4">
54+
<div className="text-2xl">{provider.icon}</div>
55+
<div>
56+
<h3 className="font-medium text-gray-900">{provider.name}</h3>
57+
<p className="text-sm text-gray-600">{provider.description}</p>
58+
{isConnected && providerLink && (
59+
<p className="text-xs text-gray-500 mt-1">Connected {new Date(providerLink.connectedAt).toLocaleDateString()}</p>
60+
)}
61+
</div>
7562
</div>
76-
</div>
7763

78-
<div className="flex items-center space-x-2">
79-
{isConnected ? (
80-
<>
81-
<button
82-
type="button"
83-
onClick={() => resyncMutation.mutate(provider.id)}
84-
disabled={resyncMutation.isPending}
85-
className="px-3 py-1 text-sm font-medium text-blue-600 hover:text-blue-700 disabled:text-gray-400"
86-
>
87-
{resyncMutation.isPending ? "Syncing..." : "Resync"}
88-
</button>
89-
<button
90-
type="button"
91-
onClick={() => {
92-
if (confirm(`Are you sure you want to disconnect ${provider.name}? This will remove all weight data from this provider.`)) {
93-
disconnectMutation.mutate(provider.id);
94-
}
95-
}}
96-
disabled={disconnectMutation.isPending}
97-
className="px-3 py-1 text-sm font-medium text-red-600 hover:text-red-700 disabled:text-gray-400"
98-
>
99-
{disconnectMutation.isPending ? "Disconnecting..." : "Disconnect"}
100-
</button>
101-
</>
102-
) : (
103-
<a href={`/link?provider=${provider.id}`} className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700">
104-
Connect
105-
</a>
106-
)}
64+
<div className="flex items-center space-x-2 self-end sm:self-auto">
65+
{isConnected ? (
66+
<>
67+
<button
68+
type="button"
69+
onClick={() => {
70+
resyncMutation.mutate(provider.id, {
71+
onSuccess: () => {
72+
showToast({
73+
title: "Resync Complete",
74+
description: `${provider.name} data has been resynced successfully.`,
75+
variant: "success",
76+
});
77+
},
78+
onError: () => {
79+
showToast({
80+
title: "Resync Failed",
81+
description: `Failed to resync ${provider.name} data. Please try again.`,
82+
variant: "error",
83+
});
84+
},
85+
});
86+
}}
87+
disabled={resyncMutation.isPending}
88+
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed transition-colors"
89+
>
90+
{resyncMutation.isPending ? "Syncing..." : "Resync"}
91+
</button>
92+
<button
93+
type="button"
94+
onClick={() => setDisconnectProvider({ id: provider.id, name: provider.name })}
95+
disabled={disconnectMutation.isPending}
96+
className="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed transition-colors"
97+
>
98+
{disconnectMutation.isPending ? "Disconnecting..." : "Disconnect"}
99+
</button>
100+
</>
101+
) : (
102+
<a href={`/link?provider=${provider.id}`} className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700">
103+
Connect
104+
</a>
105+
)}
106+
</div>
107107
</div>
108-
</div>
109-
);
110-
})}
108+
);
109+
})}
110+
</div>
111111
</div>
112-
</div>
112+
113+
<ConfirmDialog
114+
open={!!disconnectProvider}
115+
onOpenChange={(open) => !open && setDisconnectProvider(null)}
116+
title={`Disconnect ${disconnectProvider?.name}?`}
117+
description={
118+
<div className="space-y-2">
119+
<p>Are you sure you want to disconnect {disconnectProvider?.name}?</p>
120+
<p>This will remove all weight data from this provider.</p>
121+
</div>
122+
}
123+
confirmText="Disconnect"
124+
destructive
125+
onConfirm={() => {
126+
if (disconnectProvider) {
127+
disconnectMutation.mutate(disconnectProvider.id, {
128+
onSuccess: () => {
129+
showToast({
130+
title: "Disconnected",
131+
description: `${disconnectProvider.name} has been disconnected successfully.`,
132+
variant: "success",
133+
});
134+
setDisconnectProvider(null);
135+
},
136+
onError: () => {
137+
showToast({
138+
title: "Disconnect Failed",
139+
description: `Failed to disconnect ${disconnectProvider.name}. Please try again.`,
140+
variant: "error",
141+
});
142+
setDisconnectProvider(null);
143+
},
144+
});
145+
}
146+
}}
147+
/>
148+
</>
113149
);
114150
}

‎apps/web/src/components/ui/ConfirmDialog.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ export function ConfirmDialog({
2828
<AlertDialog.Overlay className="fixed inset-0 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
2929
<AlertDialog.Content className="fixed left-[50%] top-[50%] max-h-[85vh] w-[90vw] max-w-[500px] translate-x-[-50%] translate-y-[-50%] rounded-lg bg-white p-6 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]">
3030
<AlertDialog.Title className="text-lg font-semibold">{title}</AlertDialog.Title>
31-
<AlertDialog.Description className="mt-2 text-sm text-gray-600">{description}</AlertDialog.Description>
31+
<AlertDialog.Description asChild>
32+
<div className="mt-2 text-sm text-gray-600">{description}</div>
33+
</AlertDialog.Description>
3234
<div className="mt-6 flex justify-end space-x-3">
3335
<AlertDialog.Cancel asChild>
3436
<button className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import * as React from "react";
2+
import * as ToastPrimitives from "@radix-ui/react-toast";
3+
import { clsx } from "clsx";
4+
5+
const ToastProvider = ToastPrimitives.Provider;
6+
const ToastViewport = React.forwardRef<
7+
React.ElementRef<typeof ToastPrimitives.Viewport>,
8+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
9+
>(({ className, ...props }, ref) => (
10+
<ToastPrimitives.Viewport
11+
ref={ref}
12+
className={clsx(
13+
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
14+
className
15+
)}
16+
{...props}
17+
/>
18+
));
19+
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
20+
21+
const Toast = React.forwardRef<
22+
React.ElementRef<typeof ToastPrimitives.Root>,
23+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & {
24+
variant?: "default" | "success" | "error";
25+
}
26+
>(({ className, variant = "default", ...props }, ref) => {
27+
return (
28+
<ToastPrimitives.Root
29+
ref={ref}
30+
className={clsx(
31+
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all",
32+
"data-[state=open]:animate-[slide-in-from-right_0.2s_ease-out]",
33+
"data-[state=closed]:animate-[slide-out-to-right_0.2s_ease-in]",
34+
"data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none",
35+
{
36+
"border-gray-200 bg-white text-gray-950": variant === "default",
37+
"border-green-200 bg-green-50 text-green-900": variant === "success",
38+
"border-red-200 bg-red-50 text-red-900": variant === "error",
39+
},
40+
className
41+
)}
42+
{...props}
43+
/>
44+
);
45+
});
46+
Toast.displayName = ToastPrimitives.Root.displayName;
47+
48+
const ToastClose = React.forwardRef<
49+
React.ElementRef<typeof ToastPrimitives.Close>,
50+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
51+
>(({ className, ...props }, ref) => (
52+
<ToastPrimitives.Close
53+
ref={ref}
54+
className={clsx(
55+
"absolute right-2 top-2 rounded-md p-1 text-gray-950/50 opacity-0 transition-opacity hover:text-gray-950 focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100",
56+
className
57+
)}
58+
toast-close=""
59+
{...props}
60+
>
61+
<svg
62+
width="15"
63+
height="15"
64+
viewBox="0 0 15 15"
65+
fill="none"
66+
xmlns="http://www.w3.org/2000/svg"
67+
className="h-4 w-4"
68+
>
69+
<path
70+
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
71+
fill="currentColor"
72+
fillRule="evenodd"
73+
clipRule="evenodd"
74+
/>
75+
</svg>
76+
</ToastPrimitives.Close>
77+
));
78+
ToastClose.displayName = ToastPrimitives.Close.displayName;
79+
80+
const ToastTitle = React.forwardRef<
81+
React.ElementRef<typeof ToastPrimitives.Title>,
82+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
83+
>(({ className, ...props }, ref) => (
84+
<ToastPrimitives.Title
85+
ref={ref}
86+
className={clsx("text-sm font-semibold", className)}
87+
{...props}
88+
/>
89+
));
90+
ToastTitle.displayName = ToastPrimitives.Title.displayName;
91+
92+
const ToastDescription = React.forwardRef<
93+
React.ElementRef<typeof ToastPrimitives.Description>,
94+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
95+
>(({ className, ...props }, ref) => (
96+
<ToastPrimitives.Description
97+
ref={ref}
98+
className={clsx("text-sm opacity-90", className)}
99+
{...props}
100+
/>
101+
));
102+
ToastDescription.displayName = ToastPrimitives.Description.displayName;
103+
104+
export {
105+
ToastProvider,
106+
ToastViewport,
107+
Toast,
108+
ToastTitle,
109+
ToastDescription,
110+
ToastClose,
111+
};
112+

0 commit comments

Comments
 (0)