Skip to content

Commit ff55c3b

Browse files
Refactor calls admin UI and improve call detail layout
Co-authored-by: me <me@kentcdodds.com>
1 parent e3b5cb5 commit ff55c3b

File tree

3 files changed

+209
-89
lines changed

3 files changed

+209
-89
lines changed

app/routes/calls_.admin+/$callId.tsx

Lines changed: 108 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ import {
1414
RecordingForm,
1515
type RecordingFormData,
1616
} from '#app/components/calls/submit-recording-form.tsx'
17-
import { Field } from '#app/components/form-elements.tsx'
18-
import { Spacer } from '#app/components/spacer.tsx'
19-
import { Paragraph } from '#app/components/typography.tsx'
17+
import { MailIcon } from '#app/components/icons.tsx'
18+
import { H4, H6, Paragraph } from '#app/components/typography.tsx'
2019
import { type KCDHandle } from '#app/types.ts'
2120
import {
2221
getErrorForAudio,
@@ -191,42 +190,88 @@ function CallListing({ call }: { call: SerializeFrom<typeof loader>['call'] }) {
191190

192191
return (
193192
<section
194-
className={`set-color-team-current-${call.user.team.toLowerCase()}`}
193+
className={`set-color-team-current-${call.user.team.toLowerCase()} rounded-lg bg-gray-100 p-6 dark:bg-gray-800 lg:p-8`}
195194
>
196-
<strong className="text-team-current">{call.user.firstName}</strong> (
197-
<a href={`mailto:${call.user.email}`}>{call.user.email}</a>) asked on{' '}
198-
{call.formattedCreatedAt}
199-
<br />
200-
<strong>{call.title}</strong>
201-
<Paragraph>{call.description}</Paragraph>
195+
{/* Header */}
196+
<div className="mb-6 border-b border-gray-200 pb-6 dark:border-gray-700">
197+
<div className="flex flex-wrap items-start justify-between gap-4">
198+
<div>
199+
<H4 as="h2" className="mb-2">
200+
{call.title}
201+
</H4>
202+
<div className="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-slate-400">
203+
<span className="font-medium text-team-current">
204+
{call.user.firstName}
205+
</span>
206+
<span></span>
207+
<a
208+
href={`mailto:${call.user.email}`}
209+
className="inline-flex items-center gap-1 hover:underline"
210+
>
211+
<MailIcon size={14} />
212+
{call.user.email}
213+
</a>
214+
<span></span>
215+
<span>{call.formattedCreatedAt}</span>
216+
</div>
217+
</div>
218+
<Form method="delete">
219+
<input type="hidden" name="callId" value={call.id} />
220+
<Button type="submit" variant="danger" size="small" {...dc.getButtonProps()}>
221+
{dc.doubleCheck ? 'You sure?' : 'Delete'}
222+
</Button>
223+
</Form>
224+
</div>
225+
</div>
226+
227+
{/* Description */}
228+
<div className="mb-6">
229+
<H6 as="h3" className="mb-2">
230+
Description
231+
</H6>
232+
<Paragraph className="whitespace-pre-wrap text-gray-600 dark:text-slate-300">
233+
{call.description}
234+
</Paragraph>
235+
</div>
236+
237+
{/* Audio Player */}
202238
{audioURL ? (
203-
<div className="my-6 flex flex-wrap items-center gap-6">
204-
<audio
205-
className="flex-1"
206-
style={{ minWidth: '300px' }}
207-
ref={(el) => setAudioEl(el)}
208-
src={audioURL}
209-
controls
210-
preload="metadata"
211-
/>
212-
<Field
213-
value={playbackRate}
214-
onChange={(e) => setPlaybackRate(Number(e.target.value))}
215-
label="Playback rate"
216-
name="playbackRate"
217-
type="number"
218-
max="3"
219-
min="0.5"
220-
step="0.1"
221-
/>
239+
<div className="rounded-lg bg-gray-200 p-4 dark:bg-gray-700">
240+
<H6 as="h3" className="mb-3">
241+
Listen to Call
242+
</H6>
243+
<div className="flex flex-col gap-4 lg:flex-row lg:items-center">
244+
<audio
245+
className="w-full flex-1"
246+
ref={(el) => setAudioEl(el)}
247+
src={audioURL}
248+
controls
249+
preload="metadata"
250+
/>
251+
<div className="flex items-center gap-2 lg:w-auto">
252+
<label
253+
htmlFor="playbackRate"
254+
className="whitespace-nowrap text-sm text-gray-500 dark:text-slate-400"
255+
>
256+
Speed:
257+
</label>
258+
<input
259+
id="playbackRate"
260+
type="range"
261+
min="0.5"
262+
max="3"
263+
step="0.1"
264+
value={playbackRate}
265+
onChange={(e) => setPlaybackRate(Number(e.target.value))}
266+
className="w-20"
267+
/>
268+
<span className="w-10 text-sm font-medium text-gray-700 dark:text-slate-300">
269+
{playbackRate}x
270+
</span>
271+
</div>
272+
</div>
222273
</div>
223274
) : null}
224-
<Form method="delete">
225-
<input type="hidden" name="callId" value={call.id} />
226-
<Button type="submit" variant="danger" {...dc.getButtonProps()}>
227-
{dc.doubleCheck ? 'You sure?' : 'Delete'}
228-
</Button>
229-
</Form>
230275
</section>
231276
)
232277
}
@@ -238,25 +283,35 @@ function RecordingDetailScreen() {
238283
const user = useUser()
239284

240285
return (
241-
<div key={data.call.id}>
286+
<div key={data.call.id} className="flex flex-col gap-6">
242287
<CallListing call={data.call} />
243-
<Spacer size="xs" />
244-
<strong>Record your response:</strong>
245-
<Spacer size="2xs" />
246-
{responseAudio ? (
247-
<RecordingForm
248-
audio={responseAudio}
249-
data={{
250-
fields: { ...data.call, ...actionData?.fields },
251-
errors: { ...actionData?.errors },
252-
}}
253-
/>
254-
) : (
255-
<CallRecorder
256-
onRecordingComplete={(recording) => setResponseAudio(recording)}
257-
team={user.team}
258-
/>
259-
)}
288+
289+
{/* Response Recording Section */}
290+
<div className="rounded-lg border-2 border-dashed border-gray-300 p-6 dark:border-gray-600 lg:p-8">
291+
<H6 as="h3" className="mb-4">
292+
Record Your Response
293+
</H6>
294+
<Paragraph className="mb-6 text-gray-500 dark:text-slate-400">
295+
Record your response to this call. Once submitted, the response will
296+
be stitched together with the original call and published to the
297+
podcast.
298+
</Paragraph>
299+
300+
{responseAudio ? (
301+
<RecordingForm
302+
audio={responseAudio}
303+
data={{
304+
fields: { ...data.call, ...actionData?.fields },
305+
errors: { ...actionData?.errors },
306+
}}
307+
/>
308+
) : (
309+
<CallRecorder
310+
onRecordingComplete={(recording) => setResponseAudio(recording)}
311+
team={user.team}
312+
/>
313+
)}
314+
</div>
260315
</div>
261316
)
262317
}

app/routes/calls_.admin+/_admin.tsx

Lines changed: 85 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ import {
44
redirect,
55
type LoaderFunctionArgs,
66
} from '@remix-run/node'
7-
import { Link, Outlet, useLoaderData } from '@remix-run/react'
7+
import { Link, Outlet, useLoaderData, useParams } from '@remix-run/react'
8+
import { clsx } from 'clsx'
9+
import { Grid } from '#app/components/grid.tsx'
10+
import { Spacer } from '#app/components/spacer.tsx'
11+
import { H2, H6, Paragraph } from '#app/components/typography.tsx'
812
import { type KCDHandle } from '#app/types.ts'
9-
import { getAvatarForUser } from '#app/utils/misc.tsx'
13+
import { formatDate, getAvatarForUser } from '#app/utils/misc.tsx'
1014
import { prisma } from '#app/utils/prisma.server.ts'
1115
import { requireAdminUser } from '#app/utils/session.server.ts'
1216
import { useRootData } from '#app/utils/use-root-data.ts'
@@ -60,45 +64,91 @@ export async function loader({ request }: LoaderFunctionArgs) {
6064
export default function CallListScreen() {
6165
const data = useLoaderData<typeof loader>()
6266
const { requestInfo } = useRootData()
67+
const params = useParams()
68+
const selectedCallId = params.callId
69+
6370
return (
64-
<div className="px-6">
65-
<main className="flex gap-8">
66-
<div className="w-52 overscroll-auto">
71+
<main className="mb-24 mt-12 lg:mb-48 lg:mt-24">
72+
<Grid>
73+
<div className="col-span-full mb-8 lg:mb-12">
74+
<H2>Calls Admin</H2>
75+
<H2 variant="secondary" as="p">
76+
{data.calls.length} pending{' '}
77+
{data.calls.length === 1 ? 'call' : 'calls'}
78+
</H2>
79+
</div>
80+
81+
{/* Sidebar - Call List */}
82+
<div className="col-span-full lg:col-span-4">
83+
<H6 as="h3" className="mb-4">
84+
Pending Calls
85+
</H6>
6786
{data.calls.length ? (
68-
<ul>
69-
{data.calls.map((call) => {
70-
const avatar = getAvatarForUser(call.user, {
71-
origin: requestInfo.origin,
72-
})
73-
return (
74-
<li
75-
key={call.id}
76-
className={`mb-6 set-color-team-current-${call.user.team.toLowerCase()}`}
77-
>
78-
<Link to={call.id} className="mb-1 block">
79-
<img
80-
alt={avatar.alt}
81-
src={avatar.src}
82-
className="block h-16 rounded-full"
83-
/>
84-
{call.title}
85-
</Link>
86-
<small>
87-
{call.description.slice(0, 30)}
88-
{call.description.length > 30 ? '...' : null}
89-
</small>
90-
</li>
91-
)
92-
})}
93-
</ul>
87+
<nav aria-label="Call list">
88+
<ul className="flex flex-col gap-4">
89+
{data.calls.map((call) => {
90+
const avatar = getAvatarForUser(call.user, {
91+
origin: requestInfo.origin,
92+
})
93+
const isSelected = selectedCallId === call.id
94+
return (
95+
<li
96+
key={call.id}
97+
className={`set-color-team-current-${call.user.team.toLowerCase()}`}
98+
>
99+
<Link
100+
to={call.id}
101+
className={clsx(
102+
'group relative block rounded-lg p-4 transition focus:outline-none',
103+
isSelected
104+
? 'bg-gray-100 ring-2 ring-team-current dark:bg-gray-800'
105+
: 'hover:bg-gray-100 focus:bg-gray-100 dark:hover:bg-gray-800 dark:focus:bg-gray-800',
106+
)}
107+
>
108+
<div className="flex items-start gap-4">
109+
<img
110+
alt={avatar.alt}
111+
src={avatar.src}
112+
className="h-12 w-12 flex-none rounded-full object-cover ring-2 ring-team-current"
113+
/>
114+
<div className="min-w-0 flex-1">
115+
<p className="text-primary truncate font-medium">
116+
{call.title}
117+
</p>
118+
<p className="mt-1 truncate text-sm text-gray-500 dark:text-slate-500">
119+
{call.user.firstName}{call.user.email}
120+
</p>
121+
<p className="mt-2 line-clamp-2 text-sm text-gray-500 dark:text-slate-400">
122+
{call.description}
123+
</p>
124+
<p className="mt-2 text-xs text-gray-400 dark:text-slate-600">
125+
{formatDate(call.updatedAt)}
126+
</p>
127+
</div>
128+
</div>
129+
</Link>
130+
</li>
131+
)
132+
})}
133+
</ul>
134+
</nav>
94135
) : (
95-
<p>No calls.</p>
136+
<div className="rounded-lg bg-gray-100 p-8 text-center dark:bg-gray-800">
137+
<Paragraph className="text-gray-500 dark:text-slate-500">
138+
No pending calls 🎉
139+
</Paragraph>
140+
</div>
96141
)}
97142
</div>
98-
<div className="flex-1">
143+
144+
{/* Spacer for mobile */}
145+
<Spacer size="xs" className="col-span-full block lg:hidden" />
146+
147+
{/* Main Content Area */}
148+
<div className="col-span-full lg:col-span-7 lg:col-start-6">
99149
<Outlet />
100150
</div>
101-
</main>
102-
</div>
151+
</Grid>
152+
</main>
103153
)
104154
}

app/routes/calls_.admin+/index.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
1+
import { MicrophoneIcon } from '#app/components/icons.tsx'
2+
import { H4, Paragraph } from '#app/components/typography.tsx'
13
import { type KCDHandle } from '#app/types.ts'
24

35
export const handle: KCDHandle = {
46
getSitemapEntries: () => null,
57
}
68

79
export default function NoCallSelected() {
8-
return <div>Select a call</div>
10+
return (
11+
<div className="flex h-full min-h-[300px] flex-col items-center justify-center rounded-lg bg-gray-100 p-8 text-center dark:bg-gray-800 lg:min-h-[500px]">
12+
<div className="mb-6 rounded-full bg-gray-200 p-6 text-gray-400 dark:bg-gray-700 dark:text-gray-500">
13+
<MicrophoneIcon size={48} />
14+
</div>
15+
<H4 as="h2" className="mb-2">
16+
Select a call
17+
</H4>
18+
<Paragraph className="max-w-md text-gray-500 dark:text-slate-400">
19+
Choose a call from the list to listen, respond, and publish it to the
20+
podcast.
21+
</Paragraph>
22+
</div>
23+
)
924
}

0 commit comments

Comments
 (0)