Skip to content

Commit c42df9e

Browse files
committed
improve information displayed
1 parent 81a835a commit c42df9e

File tree

11 files changed

+220
-44
lines changed

11 files changed

+220
-44
lines changed

app/components/ui/popover.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as PopoverPrimitive from '@radix-ui/react-popover'
2+
import * as React from 'react'
3+
4+
import { cn } from '#app/utils/misc.tsx'
5+
6+
const Popover = PopoverPrimitive.Root
7+
8+
const PopoverTrigger = PopoverPrimitive.Trigger
9+
10+
const PopoverContent = React.forwardRef<
11+
React.ElementRef<typeof PopoverPrimitive.Content>,
12+
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
13+
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
14+
<PopoverPrimitive.Portal>
15+
<PopoverPrimitive.Content
16+
ref={ref}
17+
align={align}
18+
sideOffset={sideOffset}
19+
className={cn(
20+
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
21+
className,
22+
)}
23+
{...props}
24+
/>
25+
</PopoverPrimitive.Portal>
26+
))
27+
PopoverContent.displayName = PopoverPrimitive.Content.displayName
28+
29+
export { Popover, PopoverTrigger, PopoverContent }

app/components/ui/tooltip.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,19 @@ const TooltipContent = React.forwardRef<
2626
TooltipContent.displayName = TooltipPrimitive.Content.displayName
2727

2828
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
29+
30+
export function SimpleTooltip({
31+
content,
32+
children,
33+
}: {
34+
content: string | null
35+
children: React.ReactNode
36+
}) {
37+
if (!content) return children
38+
return (
39+
<Tooltip>
40+
<TooltipTrigger asChild>{children}</TooltipTrigger>
41+
<TooltipContent>{content}</TooltipContent>
42+
</Tooltip>
43+
)
44+
}

app/root.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
} from './components/ui/dropdown-menu.tsx'
3535
import { Icon, href as iconsHref } from './components/ui/icon.tsx'
3636
import { EpicToaster } from './components/ui/sonner.tsx'
37+
import { TooltipProvider } from './components/ui/tooltip.tsx'
3738
import { ThemeSwitch, useTheme } from './routes/resources+/theme-switch.tsx'
3839
import tailwindStyleSheetUrl from './styles/tailwind.css?url'
3940
import { getUserId, logout } from './utils/auth.server.ts'
@@ -248,9 +249,11 @@ function Logo() {
248249
function AppWithProviders() {
249250
const data = useLoaderData<typeof loader>()
250251
return (
251-
<HoneypotProvider {...data.honeyProps}>
252-
<App />
253-
</HoneypotProvider>
252+
<TooltipProvider>
253+
<HoneypotProvider {...data.honeyProps}>
254+
<App />
255+
</HoneypotProvider>
256+
</TooltipProvider>
254257
)
255258
}
256259

app/routes/recipients+/$recipientId.index.tsx

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,21 @@ import {
77
type LoaderFunctionArgs,
88
type SerializeFrom,
99
} from '@remix-run/node'
10-
import { useFetcher, useLoaderData } from '@remix-run/react'
11-
import cronParser from 'cron-parser'
10+
import { Link, useFetcher, useLoaderData } from '@remix-run/react'
1211
import { z } from 'zod'
1312
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
1413
import { ErrorList, TextareaField } from '#app/components/forms.js'
1514
import { Icon } from '#app/components/ui/icon.tsx'
1615
import { StatusButton } from '#app/components/ui/status-button.js'
16+
import {
17+
SimpleTooltip,
18+
Tooltip,
19+
TooltipContent,
20+
TooltipProvider,
21+
TooltipTrigger,
22+
} from '#app/components/ui/tooltip.js'
1723
import { requireUserId } from '#app/utils/auth.server.ts'
24+
import { formatSendTime, getSendTime } from '#app/utils/cron.server.js'
1825
import { prisma } from '#app/utils/db.server.ts'
1926
import { useDoubleCheck } from '#app/utils/misc.js'
2027
import { sendTextToRecipient } from '#app/utils/text.server.js'
@@ -39,8 +46,6 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
3946

4047
const { messages, ...recipientProps } = recipient
4148

42-
const interval = cronParser.parseExpression(recipientProps.scheduleCron)
43-
4449
return json({
4550
recipient: recipientProps,
4651
futureMessages: messages
@@ -55,17 +60,9 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
5560
const isLast = i === arr.length - 1
5661
const earlierOrder = isFirst ? null : (oneBefore + twoBefore) / 2
5762
const laterOrder = isLast ? null : (oneAfter + twoAfter) / 2
58-
const sendAtDisplay = interval
59-
.next()
60-
.toDate()
61-
.toLocaleDateString('en-US', {
62-
weekday: 'short',
63-
year: 'numeric',
64-
month: 'short',
65-
day: 'numeric',
66-
hour: 'numeric',
67-
minute: 'numeric',
68-
})
63+
const sendAtDisplay = formatSendTime(
64+
getSendTime(recipient.scheduleCron, i),
65+
)
6966
return {
7067
id: m.id,
7168
content: m.content,
@@ -215,11 +212,17 @@ export default function RecipientRoute() {
215212

216213
return (
217214
<ul className="flex flex-col gap-12">
218-
{data.futureMessages.map(m => (
219-
<li key={m.id} className="flex justify-start gap-2 align-top">
220-
<MessageForms message={m} />
221-
</li>
222-
))}
215+
{data.futureMessages.length ? (
216+
data.futureMessages.map(m => (
217+
<li key={m.id} className="flex justify-start gap-2 align-top">
218+
<MessageForms message={m} />
219+
</li>
220+
))
221+
) : (
222+
<Link to="new" className="underline">
223+
Create a new message
224+
</Link>
225+
)}
223226
</ul>
224227
)
225228
}
@@ -316,10 +319,15 @@ function UpdateOrderForm({
316319
name="intent"
317320
value={updateMessageActionIntent}
318321
>
322+
{/* TODO: the tooltip doesn't seem to be working... */}
319323
{direction === 'later' ? (
320-
<Icon size="lg" name="chevron-down" title="Move later" />
324+
<SimpleTooltip content="Move later">
325+
<Icon size="lg" name="chevron-down" />
326+
</SimpleTooltip>
321327
) : (
322-
<Icon size="lg" name="chevron-up" title="Move earlier" />
328+
<SimpleTooltip content="Move earlier">
329+
<Icon size="lg" name="chevron-up" />
330+
</SimpleTooltip>
323331
)}
324332
</StatusButton>
325333
<ErrorList id={fields.order.errorId} errors={fields.order.errors} />

app/routes/recipients+/$recipientId.past.tsx

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
3030

3131
return json({
3232
recipient: recipientProps,
33+
messageCountDisplay: messages.length.toLocaleString(),
3334
pastMessages: messages
3435
.filter(m => m.sentAt)
3536
.sort((m1, m2) => m2.sentAt!.getTime() - m1.sentAt!.getTime())
@@ -60,19 +61,26 @@ export default function RecipientRoute() {
6061
const data = useLoaderData<typeof loader>()
6162

6263
return (
63-
<ul className="flex flex-col gap-2">
64-
{data.pastMessages.map(m => (
65-
<li
66-
key={m.id}
67-
className="flex flex-col justify-start gap-2 align-top lg:flex-row"
68-
>
69-
<span className="text-muted-secondary-foreground min-w-36">
70-
{m.sentAtDisplay}
71-
</span>
72-
<span>{m.content}</span>
73-
</li>
74-
))}
75-
</ul>
64+
<div>
65+
<p className="mb-8">
66+
You have sent <strong>{data.messageCountDisplay}</strong>{' '}
67+
{data.pastMessages.length === 1 ? 'message' : 'messages'} to{' '}
68+
{data.recipient.name}.
69+
</p>
70+
<ul className="flex flex-col gap-2">
71+
{data.pastMessages.map(m => (
72+
<li
73+
key={m.id}
74+
className="flex flex-col justify-start gap-2 align-top lg:flex-row"
75+
>
76+
<span className="min-w-36 text-muted-secondary-foreground">
77+
{m.sentAtDisplay}
78+
</span>
79+
<span>{m.content}</span>
80+
</li>
81+
))}
82+
</ul>
83+
</div>
7684
)
7785
}
7886

app/routes/recipients+/$recipientId.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,24 @@ import { GeneralErrorBoundary } from '#app/components/error-boundary.js'
1313
import { floatingToolbarClassName } from '#app/components/floating-toolbar.js'
1414
import { Button } from '#app/components/ui/button.js'
1515
import { Icon } from '#app/components/ui/icon.js'
16+
import {
17+
Popover,
18+
PopoverContent,
19+
PopoverTrigger,
20+
} from '#app/components/ui/popover.tsx'
1621
import {
1722
Tabs,
1823
TabsContent,
1924
TabsList,
2025
TabsTrigger,
2126
} from '#app/components/ui/tabs.js'
27+
import {
28+
Tooltip,
29+
TooltipContent,
30+
TooltipTrigger,
31+
} from '#app/components/ui/tooltip.js'
2232
import { requireUserId } from '#app/utils/auth.server.js'
33+
import { formatSendTime, getSendTime } from '#app/utils/cron.server.js'
2334
import { prisma } from '#app/utils/db.server.js'
2435

2536
export async function loader({ params, request }: LoaderFunctionArgs) {
@@ -30,13 +41,19 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
3041
id: true,
3142
name: true,
3243
phoneNumber: true,
44+
scheduleCron: true,
3345
verified: true,
3446
},
3547
})
3648

3749
invariantResponse(recipient, 'Not found', { status: 404 })
3850

39-
return json({ recipient })
51+
return json({
52+
recipient,
53+
formattedNextSendTime: formatSendTime(
54+
getSendTime(recipient.scheduleCron, 0),
55+
),
56+
})
4057
}
4158

4259
export const meta: MetaFunction<typeof loader> = ({ data }) => {
@@ -84,7 +101,7 @@ export default function RecipientRoute() {
84101
<div className="absolute inset-0 flex flex-col px-10">
85102
<h2 className="mb-2 h-36 pt-12 text-h2 lg:mb-6">
86103
{data.recipient.name}
87-
<small className="block text-sm text-secondary-foreground">
104+
<small className="block text-sm font-normal text-secondary-foreground">
88105
{data.recipient.phoneNumber}{' '}
89106
{data.recipient.verified ? (
90107
''
@@ -96,6 +113,12 @@ export default function RecipientRoute() {
96113
(unverified)
97114
</Link>
98115
)}
116+
<Tooltip>
117+
<TooltipTrigger className="cursor-default">
118+
{data.formattedNextSendTime}
119+
</TooltipTrigger>
120+
<TooltipContent>Next send time</TooltipContent>
121+
</Tooltip>
99122
</small>
100123
</h2>
101124
<div className="absolute left-3 right-3 top-[8.7rem] rounded-lg bg-muted/80 px-2 py-2 shadow-xl shadow-accent backdrop-blur-sm">

app/routes/recipients+/_layout.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,23 @@ import { json, type LoaderFunctionArgs } from '@remix-run/node'
22
import { NavLink, Outlet, useLoaderData } from '@remix-run/react'
33
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
44
import { Icon } from '#app/components/ui/icon.tsx'
5+
import {
6+
Tooltip,
7+
TooltipContent,
8+
TooltipTrigger,
9+
} from '#app/components/ui/tooltip.js'
510
import { requireUserId } from '#app/utils/auth.server.js'
611
import { prisma } from '#app/utils/db.server.ts'
712
import { cn } from '#app/utils/misc.tsx'
813

914
export async function loader({ request }: LoaderFunctionArgs) {
1015
const userId = await requireUserId(request)
1116
const recipients = await prisma.recipient.findMany({
12-
select: { id: true, name: true },
17+
select: {
18+
id: true,
19+
name: true,
20+
_count: { select: { messages: { where: { sentAt: null } } } },
21+
},
1322
where: { userId },
1423
})
1524

@@ -48,10 +57,26 @@ export default function RecipientsRoute() {
4857
preventScrollReset
4958
prefetch="intent"
5059
className={({ isActive }) =>
51-
cn(navLinkDefaultClassName, isActive && 'bg-accent')
60+
cn(
61+
navLinkDefaultClassName,
62+
isActive && 'bg-accent',
63+
'flex gap-1',
64+
)
5265
}
5366
>
54-
{recipient.name}
67+
<span>{recipient.name}</span>
68+
{recipient._count.messages <= 0 ? (
69+
<Tooltip>
70+
<TooltipTrigger>
71+
<Icon
72+
size="xs"
73+
name="exclamation-circle-outline"
74+
className="self-start"
75+
/>
76+
</TooltipTrigger>
77+
<TooltipContent>No new messages</TooltipContent>
78+
</Tooltip>
79+
) : null}
5580
</NavLink>
5681
</li>
5782
))}

app/utils/cron.server.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,21 @@ export async function sendNextTexts() {
115115
if (reminderSentCount) console.log(`Sent ${reminderSentCount} reminders`)
116116
if (dueSentCount) console.log(`Sent ${dueSentCount} due texts`)
117117
}
118+
119+
export function getSendTime(scheduleCron: string, number: number) {
120+
const interval = cronParser.parseExpression(scheduleCron)
121+
let next = interval.next().toDate()
122+
while (number-- > 0) next = interval.next().toDate()
123+
return next
124+
}
125+
126+
export function formatSendTime(date: Date) {
127+
return date.toLocaleDateString('en-US', {
128+
weekday: 'short',
129+
year: 'numeric',
130+
month: 'short',
131+
day: 'numeric',
132+
hour: 'numeric',
133+
minute: 'numeric',
134+
})
135+
}
Lines changed: 8 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)