Skip to content

Commit 861941c

Browse files
committed
make it better
1 parent 56c79c8 commit 861941c

File tree

3 files changed

+76
-34
lines changed

3 files changed

+76
-34
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export default function RecipientRoute() {
7979
''
8080
) : (
8181
<Link
82+
preventScrollReset
8283
to="edit"
8384
className="text-body-2xs text-destructive underline"
8485
>
@@ -95,6 +96,7 @@ export default function RecipientRoute() {
9596
<nav>
9697
<Link
9798
to="."
99+
preventScrollReset
98100
className={`flex items-center gap-2 rounded-md px-3 py-2 transition-colors ${
99101
currentPath === '.'
100102
? 'bg-accent text-accent-foreground'
@@ -106,6 +108,7 @@ export default function RecipientRoute() {
106108
</Link>
107109
<Link
108110
to="new"
111+
preventScrollReset
109112
className={`flex items-center gap-2 rounded-md px-3 py-2 transition-colors ${
110113
currentPath === 'new'
111114
? 'bg-accent text-accent-foreground'
@@ -116,6 +119,7 @@ export default function RecipientRoute() {
116119
</Link>
117120
<Link
118121
to="past"
122+
preventScrollReset
119123
className={`flex items-center gap-2 rounded-md px-3 py-2 transition-colors ${
120124
currentPath === 'past'
121125
? 'bg-accent text-accent-foreground'
@@ -127,6 +131,7 @@ export default function RecipientRoute() {
127131

128132
<Link
129133
to="edit"
134+
preventScrollReset
130135
className={`flex items-center gap-2 rounded-md px-3 py-2 transition-colors ${
131136
currentPath === 'edit'
132137
? 'bg-accent text-accent-foreground'

app/routes/_app+/recipients+/_layout.tsx

Lines changed: 66 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { json, type LoaderFunctionArgs } from '@remix-run/node'
22
import { Link, NavLink, Outlet, useLoaderData } from '@remix-run/react'
3+
import { useState } from 'react'
34
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
45
import { ButtonLink } from '#app/components/ui/button.tsx'
56
import {
67
DropdownMenu,
78
DropdownMenuContent,
9+
DropdownMenuItem,
810
DropdownMenuTrigger,
911
} from '#app/components/ui/dropdown-menu.tsx'
1012
import { Icon } from '#app/components/ui/icon.tsx'
1113
import { SimpleTooltip } from '#app/components/ui/tooltip.js'
1214
import { requireUserId } from '#app/utils/auth.server.js'
15+
import { getNextScheduledTime } from '#app/utils/cron.server.ts'
1316
import { prisma } from '#app/utils/db.server.ts'
1417
import { cn } from '#app/utils/misc.tsx'
1518

@@ -19,16 +22,43 @@ export async function loader({ request }: LoaderFunctionArgs) {
1922
select: {
2023
id: true,
2124
name: true,
25+
scheduleCron: true,
26+
timeZone: true,
2227
_count: { select: { messages: { where: { sentAt: null } } } },
28+
messages: {
29+
where: { sentAt: null },
30+
orderBy: { order: 'asc' },
31+
take: 1,
32+
},
2333
},
2434
where: { userId },
2535
})
2636

27-
return json({ recipients })
37+
// Calculate next scheduled time for each recipient and sort
38+
const sortedRecipients = recipients
39+
.map((recipient) => ({
40+
...recipient,
41+
nextScheduledAt: getNextScheduledTime(
42+
recipient.scheduleCron,
43+
recipient.timeZone,
44+
),
45+
}))
46+
.sort((a, b) => {
47+
// If either recipient has no messages, they should be last
48+
if (a._count.messages === 0 && b._count.messages === 0) return 0
49+
if (a._count.messages === 0) return 1
50+
if (b._count.messages === 0) return -1
51+
52+
// Sort by next scheduled time
53+
return a.nextScheduledAt.getTime() - b.nextScheduledAt.getTime()
54+
})
55+
56+
return json({ recipients: sortedRecipients })
2857
}
2958

3059
export default function RecipientsLayout() {
3160
const { recipients } = useLoaderData<typeof loader>()
61+
const [isOpen, setIsOpen] = useState(false)
3262

3363
return (
3464
<div className="container mx-auto flex min-h-0 flex-grow flex-col px-4 pt-4 md:px-8 md:pt-8">
@@ -51,45 +81,47 @@ export default function RecipientsLayout() {
5181

5282
<div className="bg-background-alt flex min-h-0 flex-1 flex-col">
5383
<div className="flex flex-col gap-4 overflow-visible border-b-2 py-4 pl-1 pr-4">
54-
<DropdownMenu>
84+
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
5585
<DropdownMenuTrigger className="hover:bg-background-alt-hover cursor-pointer px-2 py-1">
5686
<Icon name="chevron-down">Select recipient</Icon>
5787
</DropdownMenuTrigger>
5888
<DropdownMenuContent className="min-w-64">
5989
{recipients.map((recipient) => (
60-
<NavLink
61-
key={recipient.id}
62-
to={recipient.id}
63-
className={({ isActive }) =>
64-
cn(
65-
'flex items-center gap-2 overflow-x-auto text-xl hover:bg-background',
66-
isActive ? 'underline' : '',
67-
)
68-
}
69-
>
70-
{({ isActive }) => (
71-
<div className="flex items-center gap-1">
72-
<Icon
73-
name="arrow-right"
74-
size="sm"
75-
className={cn(
76-
isActive ? 'opacity-100' : 'opacity-0',
77-
'transition-opacity',
90+
<DropdownMenuItem asChild key={recipient.id}>
91+
<NavLink
92+
to={recipient.id}
93+
preventScrollReset
94+
onClick={() => setIsOpen(false)}
95+
className={cn(
96+
'flex w-full items-center gap-2 overflow-x-auto rounded-sm px-2 py-1.5 text-xl transition-colors',
97+
'hover:bg-accent hover:text-accent-foreground',
98+
'focus:bg-accent focus:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
99+
)}
100+
>
101+
{({ isActive }) => (
102+
<div className="flex items-center gap-1">
103+
<Icon
104+
name="arrow-right"
105+
size="sm"
106+
className={cn(
107+
isActive ? 'opacity-100' : 'opacity-0',
108+
'transition-opacity',
109+
)}
110+
/>
111+
{recipient.name}
112+
{recipient._count.messages > 0 ? null : (
113+
<SimpleTooltip content="No messages scheduled">
114+
<Icon
115+
name="exclamation-circle-outline"
116+
className="text-danger-foreground"
117+
title="no messages scheduled"
118+
/>
119+
</SimpleTooltip>
78120
)}
79-
/>
80-
{recipient.name}
81-
{recipient._count.messages > 0 ? null : (
82-
<SimpleTooltip content="No messages scheduled">
83-
<Icon
84-
name="exclamation-circle-outline"
85-
className="text-danger-foreground"
86-
title="no messages scheduled"
87-
/>
88-
</SimpleTooltip>
89-
)}
90-
</div>
91-
)}
92-
</NavLink>
121+
</div>
122+
)}
123+
</NavLink>
124+
</DropdownMenuItem>
93125
))}
94126
{recipients.length === 0 && (
95127
<div className="bg-warning-background text-warning-foreground px-4 py-2 text-sm">

app/utils/cron.server.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ export function getSendTime(
122122
return next
123123
}
124124

125+
export function getNextScheduledTime(scheduleCron: string, timeZone: string) {
126+
const interval = cronParser.parseExpression(scheduleCron, { tz: timeZone })
127+
return interval.next().toDate()
128+
}
129+
125130
export function formatSendTime(date: Date, timezone: string): string {
126131
const options: Intl.DateTimeFormatOptions = {
127132
weekday: 'short',

0 commit comments

Comments
 (0)