Skip to content

Commit 0de5b18

Browse files
committed
better data
1 parent 6e543cf commit 0de5b18

File tree

10 files changed

+205
-150
lines changed

10 files changed

+205
-150
lines changed

exercises/99.final/01.solution.final/src/components/button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { type ButtonHTMLAttributes } from 'react'
33
import { Link, type LinkProps } from 'react-router'
44

55
const buttonStyles = cva(
6-
'rounded-full font-medium no-underline transition-colors hover:no-underline focus:no-underline',
6+
'flex items-center justify-center rounded-full font-medium no-underline transition-colors hover:no-underline focus:no-underline',
77
{
88
variants: {
99
variant: {

exercises/99.final/01.solution.final/src/components/icon.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ export function Icon({
4040
name,
4141
size = 'font',
4242
className,
43-
title,
4443
children,
4544
...props
4645
}: icons.IconProps & {
@@ -50,13 +49,7 @@ export function Icon({
5049
if (children) {
5150
return (
5251
<span className={`inline-flex ${childrenSizeClassName[size]}`}>
53-
<Icon
54-
name={name}
55-
size={size}
56-
className={className}
57-
title={title}
58-
{...props}
59-
/>
52+
<Icon name={name} size={size} className={className} {...props} />
6053
{children}
6154
</span>
6255
)

exercises/99.final/01.solution.final/src/components/icons.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,17 @@ export function PencilIcon({ title, ...props }: IconProps) {
161161
</svg>
162162
)
163163
}
164+
165+
export function ExclamationCircleIcon({ title, ...props }: IconProps) {
166+
return (
167+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" {...props}>
168+
{title ? <title>{title}</title> : null}
169+
<path
170+
strokeLinecap="round"
171+
strokeLinejoin="round"
172+
strokeWidth={2}
173+
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
174+
/>
175+
</svg>
176+
)
177+
}

exercises/99.final/01.solution.final/src/data.json

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,22 @@
1111
{
1212
"id": "1",
1313
"text": "Thank you for being my partner in every adventure and my comfort in every storm.\n\nI love you more than you know.",
14-
"sentAt": "2044-06-06T10:00:00Z",
15-
"status": "sent"
14+
"sentAt": "2044-06-06T10:00:00Z"
1615
},
1716
{
1817
"id": "2",
1918
"text": "You are my best friend, my confidant, and my greatest support. Thank you for being you.",
20-
"sentAt": "2044-06-13T10:00:00Z",
21-
"status": "sent"
19+
"sentAt": "2044-06-13T10:00:00Z"
2220
},
2321
{
2422
"id": "3",
2523
"text": "Thank you for all the sacrifices you make for our family. Your hard work and dedication mean the world to me.",
26-
"scheduledFor": "2044-06-20T10:00:00Z",
27-
"status": "scheduled"
24+
"sentAt": null
2825
},
2926
{
3027
"id": "4",
3128
"text": "Thank you for being my rock and always supporting me. I am grateful for your unwavering love.",
32-
"scheduledFor": "2044-06-27T10:00:00Z",
33-
"status": "scheduled"
29+
"sentAt": null
3430
}
3531
]
3632
},
@@ -45,14 +41,12 @@
4541
{
4642
"id": "1",
4743
"text": "Happy Mother's Day! Thank you for all the love, guidance, and support you've given me throughout my life.",
48-
"sentAt": "2044-05-12T10:00:00Z",
49-
"status": "sent"
44+
"sentAt": "2044-05-12T10:00:00Z"
5045
},
5146
{
5247
"id": "2",
5348
"text": "Just wanted to let you know I'm thinking of you and I love you. Your strength and wisdom inspire me every day.",
54-
"scheduledFor": "2044-06-19T10:00:00Z",
55-
"status": "scheduled"
49+
"sentAt": null
5650
}
5751
]
5852
},
@@ -67,20 +61,12 @@
6761
{
6862
"id": "1",
6963
"text": "Happy Father's Day! Thank you for being the best role model and for always believing in me.",
70-
"sentAt": "2044-06-16T10:00:00Z",
71-
"status": "sent"
64+
"sentAt": "2044-06-16T10:00:00Z"
7265
},
7366
{
7467
"id": "2",
7568
"text": "I really appreciate all the life lessons you've taught me. You've shaped who I am today.",
76-
"sentAt": "2044-06-17T10:00:00Z",
77-
"status": "sent"
78-
},
79-
{
80-
"id": "3",
81-
"text": "Looking forward to our fishing trip next month. Those moments mean the world to me.",
82-
"scheduledFor": "2044-07-01T10:00:00Z",
83-
"status": "scheduled"
69+
"sentAt": "2044-06-17T10:00:00Z"
8470
}
8571
]
8672
},
@@ -95,14 +81,12 @@
9581
{
9682
"id": "1",
9783
"text": "Happy birthday to my amazing sister! Wishing you all the joy and success in the world.",
98-
"sentAt": "2044-05-20T09:00:00Z",
99-
"status": "sent"
84+
"sentAt": "2044-05-20T09:00:00Z"
10085
},
10186
{
10287
"id": "2",
10388
"text": "Can't wait for our weekend getaway! It's going to be so much fun catching up.",
104-
"scheduledFor": "2044-06-21T09:00:00Z",
105-
"status": "scheduled"
89+
"sentAt": null
10690
}
10791
]
10892
},
@@ -117,14 +101,12 @@
117101
{
118102
"id": "1",
119103
"text": "Thank you for all your wonderful stories and the delicious recipes you've shared with me. They keep our family traditions alive.",
120-
"sentAt": "2044-06-02T11:00:00Z",
121-
"status": "sent"
104+
"sentAt": "2044-06-02T11:00:00Z"
122105
},
123106
{
124107
"id": "2",
125108
"text": "Looking forward to Sunday lunch together. Your cooking always makes everything better.",
126-
"scheduledFor": "2044-06-23T11:00:00Z",
127-
"status": "scheduled"
109+
"sentAt": null
128110
}
129111
]
130112
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import CronExpressionParser from 'cron-parser'
2+
import * as rawData from '#src/data.json'
3+
4+
export const recipients = rawData.recipients.map((recipient) => {
5+
const cronInstancePast = recipient.schedule?.cron
6+
? CronExpressionParser.parse(recipient.schedule.cron, {
7+
tz: recipient.timeZone,
8+
})
9+
: null
10+
const cronInstanceFuture = recipient.schedule?.cron
11+
? CronExpressionParser.parse(recipient.schedule.cron, {
12+
tz: recipient.timeZone,
13+
})
14+
: null
15+
let next = cronInstanceFuture?.next()
16+
17+
// make the mocked messages more realistic timing wise
18+
const processedMessages = recipient.messages.map((message) => {
19+
// If message doesn't have a sentAt, keep it as null
20+
if (!message.sentAt || !cronInstancePast) {
21+
const scheduledAt = next?.toDate()
22+
next = cronInstanceFuture?.next()
23+
return {
24+
...message,
25+
status: 'scheduled',
26+
sentAt: null,
27+
scheduledAt,
28+
}
29+
}
30+
const sentAt = cronInstancePast.prev().toDate()
31+
32+
return {
33+
...message,
34+
sentAt,
35+
status: 'sent',
36+
scheduledAt: sentAt,
37+
}
38+
})
39+
40+
// Sort messages: null values last, otherwise by sentAt date (oldest first)
41+
const sortedMessages = processedMessages.sort((a, b) => {
42+
if (a.sentAt === null && b.sentAt === null) return 0
43+
if (a.sentAt === null) return 1
44+
if (b.sentAt === null) return -1
45+
return a.sentAt.getTime() - b.sentAt.getTime()
46+
})
47+
48+
return {
49+
...recipient,
50+
messages: sortedMessages,
51+
nextScheduledAt: next?.toDate(),
52+
}
53+
})

exercises/99.final/01.solution.final/src/routes/app/recipients/$id.edit.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useParams } from 'react-router'
2-
import { recipients } from '#src/data.json'
2+
import { recipients } from '#src/data.ts'
33
import { RecipientEditor } from './recipient-editor.tsx'
44

55
export function RecipientEditRoute() {

exercises/99.final/01.solution.final/src/routes/app/recipients/$id.tsx

Lines changed: 70 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import CronExpressionParser from 'cron-parser'
21
import { useParams, useRouteError } from 'react-router'
32
import { Button, ButtonLink } from '#src/components/button.tsx'
43
import { Icon } from '#src/components/icon.tsx'
5-
import { recipients } from '#src/data.json'
4+
import { recipients } from '#src/data.ts'
65

76
const DAY_NAMES = [
87
'Sunday',
@@ -20,14 +19,6 @@ export function RecipientRoute() {
2019

2120
if (!recipient) throw new Error(`Recipient with ID of "${id}" not found`)
2221

23-
const schedule = recipient?.schedule
24-
? CronExpressionParser.parse(recipient.schedule.cron, {
25-
tz: recipient.timeZone,
26-
})
27-
: null
28-
const next = schedule?.next()
29-
const dayOfWeek = next?.getDay() ?? 0
30-
3122
return (
3223
<div className="flex min-h-0 flex-grow flex-col">
3324
{/* Left sidebar (header on mobile) */}
@@ -53,7 +44,7 @@ export function RecipientRoute() {
5344
<Icon name="Clock">
5445
<span>
5546
{recipient.schedule ? (
56-
`Every ${DAY_NAMES[dayOfWeek]} at ${next?.getHours()}:${next?.getMinutes()?.toString().padStart(2, '0')}`
47+
`Every ${DAY_NAMES[recipient.nextScheduledAt?.getDay() ?? 0]} at ${recipient.nextScheduledAt?.getHours()}:${recipient.nextScheduledAt?.getMinutes()?.toString().padStart(2, '0')}`
5748
) : (
5849
<span className="text-foreground-alt">Schedule paused</span>
5950
)}
@@ -64,72 +55,80 @@ export function RecipientRoute() {
6455
</div>
6556

6657
<div className="flex min-h-0 flex-1 flex-col">
67-
<div className="flex flex-grow flex-col gap-4 overflow-y-auto p-4 md:p-6">
68-
{recipient.messages.map((message) => (
69-
<div
70-
key={message.id}
71-
className={`rounded-lg p-4 ${
72-
message.status === 'sent'
73-
? 'bg-success-background text-success-foreground'
74-
: 'bg-info-background text-info-foreground'
75-
}`}
76-
>
77-
<div className="mb-2 flex items-center justify-between">
78-
<div className="flex flex-1 items-center gap-2">
79-
{message.status === 'sent' ? (
80-
<Icon name="Check">
81-
<span className="text-sm opacity-80 md:text-base">
82-
Sent on{' '}
83-
{message.sentAt
84-
? new Date(message.sentAt).toLocaleDateString(
85-
'en-US',
86-
{
87-
weekday: 'short',
88-
month: 'short',
89-
day: 'numeric',
90-
year: 'numeric',
91-
},
92-
)
93-
: 'Unknown date'}
94-
</span>
95-
</Icon>
96-
) : (
97-
<Icon name="Clock">
98-
<span className="text-sm opacity-80 md:text-base">
99-
Scheduled for{' '}
100-
{message.scheduledFor
101-
? new Date(message.scheduledFor).toLocaleDateString(
102-
'en-US',
103-
{
58+
<div
59+
className="flex flex-grow flex-col gap-4 overflow-y-auto p-4 md:p-6"
60+
// auto-scroll to the bottom on mount
61+
ref={(node) => {
62+
node?.scrollTo({ top: node.scrollHeight, behavior: 'auto' })
63+
}}
64+
>
65+
{recipient.messages.map((message) => {
66+
const status = message.sentAt ? 'sent' : 'scheduled'
67+
const nextScheduledTime =
68+
status === 'scheduled' ? message.scheduledAt : null
69+
return (
70+
<div
71+
key={message.id}
72+
className={`rounded-lg p-4 ${
73+
status === 'sent'
74+
? 'bg-success-background text-success-foreground'
75+
: 'bg-info-background text-info-foreground'
76+
}`}
77+
>
78+
<div className="mb-2 flex items-center justify-between">
79+
<div className="flex flex-1 items-center gap-2">
80+
{status === 'sent' ? (
81+
<Icon name="Check">
82+
<span className="text-sm opacity-80 md:text-base">
83+
Sent on{' '}
84+
{message.sentAt
85+
? new Date(message.sentAt).toLocaleDateString(
86+
'en-US',
87+
{
88+
weekday: 'short',
89+
month: 'short',
90+
day: 'numeric',
91+
year: 'numeric',
92+
},
93+
)
94+
: 'Unknown date'}
95+
</span>
96+
</Icon>
97+
) : (
98+
<Icon name="Clock">
99+
<span className="text-sm opacity-80 md:text-base">
100+
Scheduled for{' '}
101+
{nextScheduledTime
102+
? nextScheduledTime.toLocaleDateString('en-US', {
104103
weekday: 'short',
105104
month: 'short',
106105
day: 'numeric',
107106
year: 'numeric',
108-
},
109-
)
110-
: 'Unknown date'}
111-
</span>
112-
</Icon>
113-
)}
107+
})
108+
: 'Unknown date'}
109+
</span>
110+
</Icon>
111+
)}
112+
</div>
113+
<Button
114+
icon
115+
variant="borderless"
116+
className={
117+
status === 'sent'
118+
? 'text-info-foreground'
119+
: 'text-success-foreground'
120+
}
121+
>
122+
<Icon name="DotsVertical" />
123+
</Button>
114124
</div>
115-
<Button
116-
icon
117-
variant="borderless"
118-
className={
119-
message.status === 'sent'
120-
? 'text-info-foreground'
121-
: 'text-success-foreground'
122-
}
123-
>
124-
<Icon name="DotsVertical" />
125-
</Button>
125+
{/* break-words does not work for long strings of unbroken text */}
126+
<p className="text-sm [word-break:break-word] whitespace-pre-line md:text-base">
127+
{message.text}
128+
</p>
126129
</div>
127-
{/* break-words does not work for long strings of unbroken text */}
128-
<p className="text-sm [word-break:break-word] whitespace-pre-line md:text-base">
129-
{message.text}
130-
</p>
131-
</div>
132-
))}
130+
)
131+
})}
133132
</div>
134133

135134
<div className="border-border flex-shrink-0 border-t p-4">

exercises/99.final/01.solution.final/src/routes/app/recipients/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ButtonLink } from '#src/components/button.tsx'
22
import { Icon } from '#src/components/icon.tsx'
3-
import { recipients } from '#src/data.json'
3+
import { recipients } from '#src/data.ts'
44

55
export function RecipientIndexRoute() {
66
return (

0 commit comments

Comments
 (0)