1
1
import { json , type LoaderFunctionArgs } from '@remix-run/node'
2
2
import { Link , NavLink , Outlet , useLoaderData } from '@remix-run/react'
3
+ import { useState } from 'react'
3
4
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
4
5
import { ButtonLink } from '#app/components/ui/button.tsx'
5
6
import {
6
7
DropdownMenu ,
7
8
DropdownMenuContent ,
9
+ DropdownMenuItem ,
8
10
DropdownMenuTrigger ,
9
11
} from '#app/components/ui/dropdown-menu.tsx'
10
12
import { Icon } from '#app/components/ui/icon.tsx'
11
13
import { SimpleTooltip } from '#app/components/ui/tooltip.js'
12
14
import { requireUserId } from '#app/utils/auth.server.js'
15
+ import { getNextScheduledTime } from '#app/utils/cron.server.ts'
13
16
import { prisma } from '#app/utils/db.server.ts'
14
17
import { cn } from '#app/utils/misc.tsx'
15
18
@@ -19,16 +22,43 @@ export async function loader({ request }: LoaderFunctionArgs) {
19
22
select : {
20
23
id : true ,
21
24
name : true ,
25
+ scheduleCron : true ,
26
+ timeZone : true ,
22
27
_count : { select : { messages : { where : { sentAt : null } } } } ,
28
+ messages : {
29
+ where : { sentAt : null } ,
30
+ orderBy : { order : 'asc' } ,
31
+ take : 1 ,
32
+ } ,
23
33
} ,
24
34
where : { userId } ,
25
35
} )
26
36
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 } )
28
57
}
29
58
30
59
export default function RecipientsLayout ( ) {
31
60
const { recipients } = useLoaderData < typeof loader > ( )
61
+ const [ isOpen , setIsOpen ] = useState ( false )
32
62
33
63
return (
34
64
< 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() {
51
81
52
82
< div className = "bg-background-alt flex min-h-0 flex-1 flex-col" >
53
83
< 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 } >
55
85
< DropdownMenuTrigger className = "hover:bg-background-alt-hover cursor-pointer px-2 py-1" >
56
86
< Icon name = "chevron-down" > Select recipient</ Icon >
57
87
</ DropdownMenuTrigger >
58
88
< DropdownMenuContent className = "min-w-64" >
59
89
{ 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 >
78
120
) }
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 >
93
125
) ) }
94
126
{ recipients . length === 0 && (
95
127
< div className = "bg-warning-background text-warning-foreground px-4 py-2 text-sm" >
0 commit comments