1import type { RelayClientDeviceRecord } from "@t3tools/contracts/relay";
2import { SmartphoneIcon } from "lucide-react";
3
4import { useManagedRelayDevices } from "../../cloud/managedRelayState";
5import { Badge } from "../ui/badge";
6import { Button } from "../ui/button";
7import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty";
8import { Skeleton } from "../ui/skeleton";
9import {
10 mobileClientNotificationDetail,
11 mobileClientPlatformLabel,
12 mobileClientUpdatedAtLabel,
13} from "./MobileClientsUserProfilePage.logic";
14import {
15 ClerkUserProfilePage,
16 ClerkUserProfileRefreshButton,
17 ClerkUserProfileRow,
18} from "./ClerkUserProfilePage";
19
20const MOBILE_CLIENT_SKELETON_ROWS = ["primary", "secondary"] as const;
21
22function MobileClientStatusBadge({
23 enabled,
24 label,
25}: {
26 readonly enabled: boolean;
27 readonly label: string;
28}) {
29 return (
30 <Badge variant={enabled ? "success" : "outline"}>
31 {label}: {enabled ? "On" : "Off"}
32 </Badge>
33 );
34}
35
36function MobileClientRow({ device }: { readonly device: RelayClientDeviceRecord }) {
37 return (
38 <ClerkUserProfileRow icon={<SmartphoneIcon className="size-4" />}>
39 <div className="flex flex-col gap-0.5 sm:flex-row sm:items-start sm:justify-between sm:gap-3">
40 <div className="min-w-0">
41 <h3 className="truncate text-sm leading-4.5 font-medium text-foreground">
42 {device.label}
43 </h3>
44 <p className="text-xs leading-4.5 text-muted-foreground">
45 {mobileClientPlatformLabel(device)}
46 </p>
47 </div>
48 <p className="shrink-0 text-2xs leading-4 text-muted-foreground/75">
49 {mobileClientUpdatedAtLabel(device.updatedAt)}
50 </p>
51 </div>
52 <div className="mt-2 flex flex-wrap gap-1.5">
53 <MobileClientStatusBadge
54 enabled={device.notifications.enabled}
55 label="Push notifications"
56 />
57 <MobileClientStatusBadge enabled={device.liveActivities.enabled} label="Live Activities" />
58 </div>
59 <p className="mt-1.5 text-xs leading-4.5 text-muted-foreground/80">
60 {mobileClientNotificationDetail(device)}
61 </p>
62 </ClerkUserProfileRow>
63 );
64}
65
66function MobileClientsSkeleton() {
67 return (
68 <div aria-label="Loading mobile clients" className="divide-y border-t" role="status">
69 {MOBILE_CLIENT_SKELETON_ROWS.map((row) => (
70 <div key={row} className="py-4">
71 <div className="flex gap-3">
72 <Skeleton shape="card" className="size-8 shrink-0" />
73 <div className="flex-1 space-y-2">
74 <Skeleton className="h-3.5 w-36" />
75 <Skeleton className="h-3 w-28" />
76 <div className="flex gap-2">
77 <Skeleton className="h-4.5 w-28" />
78 <Skeleton className="h-4.5 w-24" />
79 </div>
80 </div>
81 </div>
82 </div>
83 ))}
84 </div>
85 );
86}
87
88function EmptyMobileClients() {
89 return (
90 <Empty size="compact">
91 <EmptyMedia variant="icon">
92 <SmartphoneIcon />
93 </EmptyMedia>
94 <EmptyHeader>
95 <EmptyTitle>No mobile clients</EmptyTitle>
96 <EmptyDescription>
97 Install The Knife on your phone and sign in to T3 Connect to get push notifications and Live
98 Activities.
99 </EmptyDescription>
100 </EmptyHeader>
101 </Empty>
102 );
103}
104
105export function MobileClientsUserProfilePage() {
106 const devicesState = useManagedRelayDevices();
107 const devices = devicesState.data ?? [];
108 const isInitialLoad =
109 !devicesState.accountId || (devicesState.data === null && !devicesState.error);
110 const hasErrorWithoutData = devicesState.error !== null && devicesState.data === null;
111
112 return (
113 <ClerkUserProfilePage
114 title="Mobile clients"
115 description="Mobile devices that get notifications from your environments."
116 action={
117 <ClerkUserProfileRefreshButton
118 isPending={devicesState.isPending}
119 onClick={devicesState.refresh}
120 />
121 }
122 >
123 <div>
124 {devicesState.error ? (
125 <div
126 className="mb-4 flex flex-col gap-3 border-t border-destructive/35 py-3 text-xs sm:flex-row sm:items-center sm:justify-between"
127 role="alert"
128 >
129 <div>
130 <p className="font-medium text-destructive-foreground">
131 Could not load mobile clients
132 </p>
133 <p className="mt-0.5 text-xs text-muted-foreground">{devicesState.error}</p>
134 </div>
135 <Button size="xs" variant="outline" onClick={devicesState.refresh}>
136 Try again
137 </Button>
138 </div>
139 ) : null}
140
141 {isInitialLoad ? (
142 <MobileClientsSkeleton />
143 ) : hasErrorWithoutData ? null : devices.length > 0 ? (
144 <ul className="border-t">
145 {devices.map((device) => (
146 <MobileClientRow key={device.deviceId} device={device} />
147 ))}
148 </ul>
149 ) : (
150 <div className="border-t">
151 <EmptyMobileClients />
152 </div>
153 )}
154 </div>
155 </ClerkUserProfilePage>
156 );
157}