Skip to content

Commit 0ee0cd9

Browse files
committed
improve more things
1 parent 8d943bd commit 0ee0cd9

File tree

8 files changed

+611
-93
lines changed

8 files changed

+611
-93
lines changed

exercises/99.final/01.solution.final/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@tailwindcss/vite": "^4.0.9",
1717
"class-variance-authority": "^0.7.1",
1818
"clsx": "^2.1.1",
19+
"cron-parser": "^5.0.4",
1920
"react": "^19.0.0",
2021
"react-dom": "^19.0.0",
2122
"react-router": "^7.2.0",

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,17 @@ export function InfoIcon({ title, ...props }: IconProps) {
147147
</svg>
148148
)
149149
}
150+
151+
export function PencilIcon({ title, ...props }: IconProps) {
152+
return (
153+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" {...props}>
154+
{title ? <title>{title}</title> : null}
155+
<path
156+
strokeLinecap="round"
157+
strokeLinejoin="round"
158+
strokeWidth={2}
159+
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
160+
/>
161+
</svg>
162+
)
163+
}

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

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
{
44
"id": "1",
55
"name": "Bethany",
6-
"phone": "(555) 123-4567",
7-
"schedule": { "day": "Thu", "time": "10 AM GMT+2" },
8-
"messageCount": 2,
6+
"countryCode": "+1",
7+
"phone": "5551234567",
8+
"timeZone": "America/Denver",
9+
"schedule": { "cron": "10 10 * * 4" },
910
"messages": [
1011
{
1112
"id": "1",
@@ -36,9 +37,10 @@
3637
{
3738
"id": "2",
3839
"name": "Mom",
39-
"phone": "(555) 234-5678",
40-
"schedule": { "day": "Wed", "time": "10 AM GMT+2" },
41-
"messageCount": 0,
40+
"countryCode": "+1",
41+
"phone": "5552345678",
42+
"timeZone": "America/Denver",
43+
"schedule": { "cron": "0 10 * * 3" },
4244
"messages": [
4345
{
4446
"id": "1",
@@ -57,9 +59,10 @@
5759
{
5860
"id": "3",
5961
"name": "Dad",
60-
"phone": "(555) 345-6789",
62+
"countryCode": "+1",
63+
"phone": "5553456789",
64+
"timeZone": "America/Denver",
6165
"schedule": null,
62-
"messageCount": 3,
6366
"messages": [
6467
{
6568
"id": "1",
@@ -84,9 +87,10 @@
8487
{
8588
"id": "4",
8689
"name": "Emily",
87-
"phone": "(555) 456-7890",
88-
"schedule": { "day": "Mon", "time": "9 AM GMT+2" },
89-
"messageCount": 2,
90+
"countryCode": "+1",
91+
"phone": "5554567890",
92+
"timeZone": "America/Denver",
93+
"schedule": { "cron": "0 9 * * 1" },
9094
"messages": [
9195
{
9296
"id": "1",
@@ -105,9 +109,10 @@
105109
{
106110
"id": "5",
107111
"name": "Grandpa",
108-
"phone": "(555) 567-8901",
109-
"schedule": { "day": "Sun", "time": "11 AM GMT+2" },
110-
"messageCount": 2,
112+
"countryCode": "+1",
113+
"phone": "5555678901",
114+
"timeZone": "America/Denver",
115+
"schedule": { "cron": "0 11 * * 0" },
111116
"messages": [
112117
{
113118
"id": "1",

exercises/99.final/01.solution.final/src/routes/app/layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ButtonLink } from '#src/components/button.tsx'
33

44
export function AppLayout() {
55
return (
6-
<div className="bg-background flex h-screen flex-col">
6+
<div className="bg-background flex h-screen min-h-[700px] flex-col">
77
<header className="bg-background-alt px-4 py-3">
88
<div className="container mx-auto flex max-w-6xl items-center justify-between">
99
<Link
@@ -18,7 +18,7 @@ export function AppLayout() {
1818
</div>
1919
</header>
2020

21-
<div className="flex min-h-0 flex-1 flex-grow flex-col">
21+
<div className="flex min-h-0 flex-1 flex-col">
2222
<Outlet />
2323
</div>
2424
</div>

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

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,59 @@
1+
import CronExpressionParser from 'cron-parser'
12
import { useParams, useRouteError } from 'react-router'
23
import { Button, ButtonLink } from '#src/components/button.tsx'
34
import { Icon } from '#src/components/icon.tsx'
45
import { recipients } from '#src/data.json'
56

7+
const DAY_NAMES = [
8+
'Sunday',
9+
'Monday',
10+
'Tuesday',
11+
'Wednesday',
12+
'Thursday',
13+
'Friday',
14+
'Saturday',
15+
]
16+
617
export function RecipientRoute() {
718
const { id } = useParams()
819
const recipient = recipients.find((r) => r.id === id)
920

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

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+
1231
return (
1332
<div className="flex min-h-0 flex-grow flex-col">
1433
{/* Left sidebar (header on mobile) */}
1534
<div className="border-border flex-shrink-0 border-b p-4">
1635
<div className="flex items-center justify-between md:mb-3">
17-
<h1 className="text-2xl font-bold md:text-3xl">{recipient.name}</h1>
36+
<h2 className="text-2xl font-bold md:text-3xl">{recipient.name}</h2>
1837

1938
<ButtonLink to={`/recipients/${id}/edit`} icon variant="borderless">
20-
<Icon name="Settings" size="lg" />
39+
<Icon name="Pencil" size="lg" />
2140
</ButtonLink>
2241
</div>
2342

2443
<div className="mt-4 flex justify-between gap-4">
2544
<div className="flex items-center gap-2">
2645
<Icon name="Phone">
27-
<span className="font-mono">{recipient.phone}</span>
46+
<span className="font-mono">
47+
{recipient.countryCode} {recipient.phone}
48+
</span>
2849
</Icon>
2950
</div>
3051

3152
<div className="flex items-center gap-2">
3253
<Icon name="Clock">
3354
<span>
3455
{recipient.schedule ? (
35-
`Every ${recipient.schedule.day} at ${recipient.schedule.time}`
56+
`Every ${DAY_NAMES[dayOfWeek]} at ${next?.getHours()}:${next?.getMinutes()?.toString().padStart(2, '0')}`
3657
) : (
3758
<span className="text-foreground-alt">Schedule paused</span>
3859
)}
@@ -42,7 +63,7 @@ export function RecipientRoute() {
4263
</div>
4364
</div>
4465

45-
<div className="flex min-h-0 flex-1 flex-grow flex-col">
66+
<div className="flex min-h-0 flex-1 flex-col">
4667
<div className="flex flex-grow flex-col gap-4 overflow-y-auto p-4 md:p-6">
4768
{recipient.messages.map((message) => (
4869
<div

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export function RecipientsLayout() {
4646
</NavLink>
4747
))}
4848
</div>
49-
<div className="flex flex-1">
49+
<div className="flex flex-1 overflow-auto">
5050
<Outlet />
5151
</div>
5252
</div>
Lines changed: 54 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { CronExpressionParser } from 'cron-parser'
12
import { Button } from '#src/components/button.tsx'
23
import { Icon } from '#src/components/icon.tsx'
34
import { type recipients } from '#src/data.json'
@@ -7,20 +8,17 @@ type Recipient = (typeof recipients)[number]
78
export function RecipientEditor({
89
recipient,
910
}: {
10-
recipient?: Pick<Recipient, 'name' | 'phone' | 'schedule'>
11+
recipient?: Pick<
12+
Recipient,
13+
'name' | 'id' | 'phone' | 'schedule' | 'countryCode' | 'timeZone'
14+
>
1115
}) {
12-
// Parse phone number into country code and number if recipient exists
13-
const { countryCode, phoneNumber } = recipient?.phone
14-
? parsePhoneNumber(recipient.phone)
15-
: { countryCode: '', phoneNumber: '' }
16-
17-
// Convert schedule day to lowercase for select value
18-
const scheduleDay = recipient?.schedule?.day.toLowerCase() ?? ''
19-
20-
// Convert time format from "10 AM GMT+2" to "10:00" format
21-
const scheduleTime = recipient?.schedule?.time
22-
? convertTo24Hour(recipient.schedule.time)
23-
: ''
16+
const schedule = recipient?.schedule
17+
? CronExpressionParser.parse(recipient.schedule.cron, {
18+
tz: recipient.timeZone,
19+
})
20+
: null
21+
const next = schedule?.next()
2422

2523
return (
2624
<form
@@ -52,7 +50,7 @@ export function RecipientEditor({
5250
name="countryCode"
5351
className="w-full rounded-lg border p-3"
5452
required
55-
defaultValue={countryCode}
53+
defaultValue={recipient?.countryCode}
5654
>
5755
<option value="">Select Country</option>
5856
<option value="+1">United States (+1)</option>
@@ -63,17 +61,17 @@ export function RecipientEditor({
6361
</div>
6462

6563
<div>
66-
<label htmlFor="phoneNumber" className="mb-2 block">
64+
<label htmlFor="phone" className="mb-2 block">
6765
Phone Number
6866
</label>
6967
<input
7068
type="tel"
71-
id="phoneNumber"
72-
name="phoneNumber"
69+
id="phone"
70+
name="phone"
7371
placeholder="123 456 7890"
7472
className="w-full rounded-lg border p-3"
7573
required
76-
defaultValue={phoneNumber}
74+
defaultValue={recipient?.phone}
7775
/>
7876
</div>
7977
</div>
@@ -87,13 +85,13 @@ export function RecipientEditor({
8785
name="timeZone"
8886
className="w-full rounded-lg border p-3"
8987
required
90-
defaultValue="GMT+2"
88+
defaultValue={recipient?.timeZone}
9189
>
9290
<option value="">Select Time Zone</option>
93-
<option value="America/New_York">Eastern Time</option>
94-
<option value="America/Chicago">Central Time</option>
95-
<option value="America/Denver">Mountain Time</option>
96-
<option value="America/Los_Angeles">Pacific Time</option>
91+
<option value="America/New_York">America/New_York</option>
92+
<option value="America/Chicago">America/Chicago</option>
93+
<option value="America/Denver">America/Denver</option>
94+
<option value="America/Los_Angeles">America/Los_Angeles</option>
9795
{/* Add more time zones as needed */}
9896
</select>
9997
</div>
@@ -105,34 +103,42 @@ export function RecipientEditor({
105103
name="scheduleDay"
106104
className="w-full rounded-lg border p-3"
107105
required
108-
defaultValue={scheduleDay}
106+
defaultValue={next?.getDay()}
109107
>
110108
<option value="">Select Day</option>
111-
<option value="monday">Monday</option>
112-
<option value="tuesday">Tuesday</option>
113-
<option value="wednesday">Wednesday</option>
114-
<option value="thursday">Thursday</option>
115-
<option value="friday">Friday</option>
116-
<option value="saturday">Saturday</option>
117-
<option value="sunday">Sunday</option>
118-
</select>
119-
120-
<select
121-
name="scheduleTime"
122-
className="w-full rounded-lg border p-3"
123-
required
124-
defaultValue={scheduleTime}
125-
>
126-
<option value="">Select Time</option>
127-
{Array.from({ length: 24 }, (_, i) => {
128-
const hour = i.toString().padStart(2, '0')
129-
return (
130-
<option key={hour} value={`${hour}:00`}>
131-
{`${hour}:00`}
132-
</option>
133-
)
134-
})}
109+
<option value="1">Monday</option>
110+
<option value="2">Tuesday</option>
111+
<option value="3">Wednesday</option>
112+
<option value="4">Thursday</option>
113+
<option value="5">Friday</option>
114+
<option value="6">Saturday</option>
115+
<option value="0">Sunday</option>
135116
</select>
117+
<div className="flex items-center gap-1">
118+
<input
119+
type="number"
120+
name="scheduleHour"
121+
className="w-full rounded-lg border p-3"
122+
required
123+
placeholder="HH"
124+
min={0}
125+
max={23}
126+
defaultValue={next?.getHours().toString().padStart(2, '0')}
127+
/>
128+
129+
<span className="text-lg">:</span>
130+
131+
<input
132+
type="number"
133+
name="scheduleMinute"
134+
className="w-full rounded-lg border p-3"
135+
required
136+
placeholder="MM"
137+
min={0}
138+
max={59}
139+
defaultValue={next?.getMinutes().toString().padStart(2, '0')}
140+
/>
141+
</div>
136142
</div>
137143
</div>
138144

@@ -148,26 +154,3 @@ export function RecipientEditor({
148154
</form>
149155
)
150156
}
151-
152-
function parsePhoneNumber(phone: string) {
153-
// Simple parsing - assumes format like "(555) 123-4567"
154-
const match = phone.match(/^\((\d{3})\) (\d{3})-(\d{4})$/)
155-
if (!match) return { countryCode: '+1', phoneNumber: phone }
156-
return {
157-
countryCode: '+1', // Default to US
158-
phoneNumber: `${match[2]}${match[3]}`,
159-
}
160-
}
161-
162-
function convertTo24Hour(time: string) {
163-
// Convert "10 AM GMT+2" to "10:00"
164-
const match = time.match(/(\d{1,2}) (AM|PM)/)
165-
if (!match) return ''
166-
let hour = parseInt(match[1])
167-
const meridiem = match[2]
168-
169-
if (meridiem === 'PM' && hour !== 12) hour += 12
170-
if (meridiem === 'AM' && hour === 12) hour = 0
171-
172-
return `${hour.toString().padStart(2, '0')}:00`
173-
}

0 commit comments

Comments
 (0)