Skip to content

Commit e01b630

Browse files
committed
fix: improve UI consistency and enhance proposal management features
1 parent 9365f18 commit e01b630

7 files changed

Lines changed: 130 additions & 94 deletions

File tree

src/app/(admin)/admin/proposals/[id]/page.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ export default async function ProposalDetailPage({
5656
return (
5757
<div className="flex h-full min-h-screen flex-col lg:flex-row">
5858
<div className="min-w-0 flex-1">
59-
<div className="mx-auto max-w-4xl p-4 lg:p-0">
60-
<div className="mb-8">
61-
<div className="mb-4 flex items-center justify-between">
59+
<div className="mx-auto max-w-4xl p-4">
60+
<div className="mb-5">
61+
<div className="mb-3 flex items-center justify-between">
6262
<BackLink fallbackUrl="/admin/proposals">
6363
Back to Proposals
6464
</BackLink>
@@ -86,7 +86,7 @@ export default async function ProposalDetailPage({
8686
</div>
8787

8888
<div className="w-full lg:w-96 lg:shrink-0">
89-
<div className="space-y-4 p-4 lg:p-4">
89+
<div className="space-y-4 p-4">
9090
<ProposalPublishedContent
9191
proposalId={proposal._id}
9292
currentVideoUrl={getProposalVideoUrl(proposal)}
@@ -100,13 +100,13 @@ export default async function ProposalDetailPage({
100100
status={proposal.status}
101101
conferenceStartDate={conference.startDate}
102102
/>
103+
<ProposalReviewPanel
104+
proposalId={proposal._id}
105+
initialReviews={proposal.reviews || []}
106+
currentUser={session?.speaker}
107+
domain={domain}
108+
/>
103109
</div>
104-
<ProposalReviewPanel
105-
proposalId={proposal._id}
106-
initialReviews={proposal.reviews || []}
107-
currentUser={session?.speaker}
108-
domain={domain}
109-
/>
110110
</div>
111111
</div>
112112
)

src/components/admin/AdminActionBar.tsx

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,14 @@ export function AdminActionBar({
153153

154154
return (
155155
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800">
156-
<div className="flex flex-wrap items-center justify-between gap-4">
156+
<div className="flex items-center justify-between gap-4">
157157
<div className="flex min-w-0 flex-wrap items-center gap-4">
158158
<div className="flex shrink-0 items-center gap-2">
159159
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
160160
Status:
161161
</span>
162162
<span
163-
className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
164-
proposal.status === 'accepted'
163+
className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${proposal.status === 'accepted'
165164
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
166165
: proposal.status === 'waitlisted'
167166
? 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200'
@@ -170,7 +169,7 @@ export function AdminActionBar({
170169
: proposal.status === 'submitted'
171170
? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
172171
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'
173-
}`}
172+
}`}
174173
>
175174
{proposal.status.charAt(0).toUpperCase() +
176175
proposal.status.slice(1)}
@@ -252,31 +251,25 @@ export function AdminActionBar({
252251
)}
253252
</div>
254253

255-
<div className="flex shrink-0 items-center gap-2">
254+
<div className="flex shrink-0 items-center gap-1.5">
256255
<AdminButton
257256
size="xs"
258257
onClick={handleEditProposal}
259-
title="Edit proposal"
258+
title="Edit proposal (⌘E)"
260259
>
261260
<PencilIcon className="h-3 w-3" />
262-
<span>Edit</span>
263-
<kbd className="ml-1 hidden rounded border border-indigo-400 bg-indigo-500 px-1.5 py-0.5 text-xs font-semibold text-white sm:inline dark:border-indigo-600 dark:bg-indigo-700">
264-
⌘E
265-
</kbd>
261+
Edit
266262
</AdminButton>
267263

268264
{speakers.length > 0 && (
269265
<AdminButton
270266
color="purple"
271267
size="xs"
272268
onClick={handlePreviewSpeaker}
273-
title="Preview speaker profile"
269+
title="Preview speaker profile (⌘P)"
274270
>
275271
<EyeIcon className="h-3 w-3" />
276-
<span>Preview</span>
277-
<kbd className="ml-1 hidden rounded border border-purple-400 bg-purple-500 px-1.5 py-0.5 text-xs font-semibold text-white sm:inline dark:border-purple-600 dark:bg-purple-700">
278-
⌘P
279-
</kbd>
272+
Preview
280273
</AdminButton>
281274
)}
282275

@@ -285,13 +278,10 @@ export function AdminActionBar({
285278
color="blue"
286279
size="xs"
287280
onClick={handleEmailSpeakers}
288-
title={`Email ${speakers.length === 1 ? speakers.filter((s) => s.email)[0]?.name : `${speakers.filter((s) => s.email).length} speakers`}`}
281+
title={`Email speaker (⌘M)`}
289282
>
290283
<EnvelopeIcon className="h-3 w-3" />
291-
<span>Email</span>
292-
<kbd className="ml-1 hidden rounded border border-blue-400 bg-blue-500 px-1.5 py-0.5 text-xs font-semibold text-white sm:inline dark:border-blue-600 dark:bg-blue-700">
293-
⌘M
294-
</kbd>
284+
Email
295285
</AdminButton>
296286
)}
297287

src/components/admin/AdminButton.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const variantStyles: Record<Exclude<AdminButtonVariant, 'primary'>, string> = {
3939
}
4040

4141
const sizeStyles: Record<AdminButtonSize, string> = {
42-
xs: 'px-3 py-1.5 text-xs',
42+
xs: 'px-2 py-1 text-xs',
4343
sm: 'px-3 py-2 text-sm',
4444
md: 'px-4 py-2 text-sm',
4545
}
@@ -54,7 +54,7 @@ export function AdminButton({
5454
return (
5555
<button
5656
className={clsx(
57-
'inline-flex items-center justify-center gap-2 rounded-md font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
57+
'inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
5858
variant === 'primary' ? colorStyles[color] : variantStyles[variant],
5959
sizeStyles[size],
6060
className,

src/components/admin/ProposalDetail.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ function SpeakerCard({ speaker, requiresTravelFunding }: SpeakerCardProps) {
7373
const shouldShowExpand = bioText.length > 150
7474

7575
return (
76-
<div className="rounded-lg bg-gray-50 p-6 dark:bg-gray-800">
77-
<div className="mb-4 flex items-center justify-between">
78-
<h2 className="text-lg font-medium text-gray-900 dark:text-white">
76+
<div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
77+
<div className="mb-3 flex items-center justify-between">
78+
<h2 className="text-sm font-semibold text-gray-900 dark:text-white">
7979
{speaker.name}
8080
</h2>
8181
{requiresTravelFunding && (
@@ -174,21 +174,21 @@ export function ProposalDetail({ proposal }: ProposalDetailProps) {
174174

175175
return (
176176
<div>
177-
<div className="border-b border-gray-200 py-5 dark:border-gray-700">
177+
<div className="border-b border-gray-200 pb-4 dark:border-gray-700">
178178
<div className="flex items-center justify-between">
179179
<div className="min-w-0 flex-1">
180-
<h1 className="text-2xl font-bold text-gray-900 sm:text-3xl dark:text-white">
180+
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
181181
{proposal.title}
182182
</h1>
183183
</div>
184184
</div>
185185
</div>
186186

187-
<div className="py-5">
188-
<div className="grid grid-cols-1 gap-8 xl:grid-cols-3">
189-
<div className="space-y-8 lg:col-span-2">
187+
<div className="pt-4">
188+
<div className="grid grid-cols-1 gap-6 xl:grid-cols-3">
189+
<div className="space-y-6 lg:col-span-2">
190190
<div>
191-
<h2 className="mb-4 text-lg font-medium text-gray-900 dark:text-white">
191+
<h2 className="mb-3 text-lg font-medium text-gray-900 dark:text-white">
192192
Description
193193
</h2>
194194
<div className="text-gray-600 dark:text-gray-300">
@@ -205,7 +205,7 @@ export function ProposalDetail({ proposal }: ProposalDetailProps) {
205205

206206
{proposal.outline && (
207207
<div>
208-
<h2 className="mb-4 text-lg font-medium text-gray-900 dark:text-white">
208+
<h2 className="mb-3 text-lg font-medium text-gray-900 dark:text-white">
209209
Outline
210210
</h2>
211211
<div className="prose prose-sm dark:prose-invert max-w-none text-gray-600 dark:text-gray-300">
@@ -216,7 +216,7 @@ export function ProposalDetail({ proposal }: ProposalDetailProps) {
216216

217217
{topics && topics.length > 0 && (
218218
<div>
219-
<h2 className="mb-4 text-lg font-medium text-gray-900 dark:text-white">
219+
<h2 className="mb-3 text-lg font-medium text-gray-900 dark:text-white">
220220
Topics
221221
</h2>
222222
<div className="flex flex-wrap gap-2">
@@ -239,7 +239,7 @@ export function ProposalDetail({ proposal }: ProposalDetailProps) {
239239
speaker?.submittedTalks && speaker.submittedTalks.length > 0,
240240
) && (
241241
<div>
242-
<h2 className="mb-4 text-lg font-medium text-gray-900 dark:text-white">
242+
<h2 className="mb-3 text-lg font-medium text-gray-900 dark:text-white">
243243
Other Submissions
244244
</h2>
245245
<div className="space-y-3">
@@ -317,7 +317,7 @@ export function ProposalDetail({ proposal }: ProposalDetailProps) {
317317
speaker.previousAcceptedTalks.length > 0,
318318
) && (
319319
<div>
320-
<h2 className="mb-4 text-lg font-medium text-gray-900 dark:text-white">
320+
<h2 className="mb-3 text-lg font-medium text-gray-900 dark:text-white">
321321
Previous Accepted Talks
322322
</h2>
323323
<div className="space-y-3">
@@ -405,7 +405,7 @@ export function ProposalDetail({ proposal }: ProposalDetailProps) {
405405
)}
406406
</div>
407407

408-
<div className="space-y-6">
408+
<div className="space-y-4">
409409
{speakers.length > 0 ? (
410410
speakers.map((speaker, index) => (
411411
<SpeakerCard
@@ -415,7 +415,7 @@ export function ProposalDetail({ proposal }: ProposalDetailProps) {
415415
/>
416416
))
417417
) : (
418-
<div className="rounded-lg bg-gray-50 p-6 dark:bg-gray-800">
418+
<div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
419419
<p className="text-sm text-gray-500 italic dark:text-gray-400">
420420
Speaker information not available
421421
</p>

src/components/admin/ProposalManagementModal.tsx

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ import { Topic } from '@/lib/topic/types'
2020
import { extractSpeakerIds } from '@/lib/proposal/utils'
2121
import { validateProposalForAdmin } from '@/lib/proposal/validation'
2222
import { SpeakerMultiSelect } from '@/components/admin/SpeakerMultiSelect'
23+
import { ProposalCoSpeaker } from '@/components/cfp/ProposalCoSpeaker'
24+
import { CoSpeakerInvitationMinimal } from '@/lib/cospeaker/types'
25+
import { Speaker } from '@/lib/speaker/types'
26+
import { extractSpeakersFromProposal } from '@/lib/proposal/utils'
2327
import { api } from '@/lib/trpc/client'
2428
import { useQueryClient } from '@tanstack/react-query'
2529
import { ProposalDetailsForm } from '@/components/proposal/ProposalDetailsForm'
@@ -105,6 +109,19 @@ export function ProposalManagementModal({
105109
Record<string, string>
106110
>({})
107111

112+
const getInitialCoSpeakers = (): Speaker[] => {
113+
if (!editingProposal) return []
114+
const allSpeakers = extractSpeakersFromProposal(editingProposal)
115+
return allSpeakers.slice(1)
116+
}
117+
118+
const [coSpeakers, setCoSpeakers] = useState<Speaker[]>(
119+
getInitialCoSpeakers(),
120+
)
121+
const [invitations, setInvitations] = useState<CoSpeakerInvitationMinimal[]>(
122+
editingProposal?.coSpeakerInvitations || [],
123+
)
124+
108125
// Validate that topics are properly expanded - this will throw a helpful error
109126
// if the parent page forgot to pass `topics: true` to getConferenceForCurrentDomain
110127
useEffect(() => {
@@ -186,32 +203,37 @@ export function ProposalManagementModal({
186203
setProposalData(
187204
editingProposal
188205
? {
189-
title: editingProposal.title || '',
190-
description: editingProposal.description || [],
191-
language: editingProposal.language || Language.norwegian,
192-
format:
193-
editingProposal.format ||
194-
conference.formats?.[0] ||
195-
Format.lightning_10,
196-
level: editingProposal.level || Level.beginner,
197-
audiences: editingProposal.audiences || [],
198-
topics: validTopics,
199-
outline: editingProposal.outline || '',
200-
tos: true,
201-
}
206+
title: editingProposal.title || '',
207+
description: editingProposal.description || [],
208+
language: editingProposal.language || Language.norwegian,
209+
format:
210+
editingProposal.format ||
211+
conference.formats?.[0] ||
212+
Format.lightning_10,
213+
level: editingProposal.level || Level.beginner,
214+
audiences: editingProposal.audiences || [],
215+
topics: validTopics,
216+
outline: editingProposal.outline || '',
217+
tos: true,
218+
}
202219
: {
203-
title: '',
204-
description: [],
205-
language: Language.norwegian,
206-
format: conference.formats?.[0] || Format.lightning_10,
207-
level: Level.beginner,
208-
audiences: [],
209-
topics: [],
210-
outline: '',
211-
tos: false,
212-
},
220+
title: '',
221+
description: [],
222+
language: Language.norwegian,
223+
format: conference.formats?.[0] || Format.lightning_10,
224+
level: Level.beginner,
225+
audiences: [],
226+
topics: [],
227+
outline: '',
228+
tos: false,
229+
},
213230
)
214231
setSelectedSpeakerIds(extractSpeakerIds(editingProposal?.speakers) || [])
232+
const allSpeakers = editingProposal
233+
? extractSpeakersFromProposal(editingProposal)
234+
: []
235+
setCoSpeakers(allSpeakers.slice(1))
236+
setInvitations(editingProposal?.coSpeakerInvitations || [])
215237
}
216238
// eslint-disable-next-line react-hooks/exhaustive-deps -- conference.formats and editingProposal are stable, isOpen triggers reset
217239
}, [isOpen])
@@ -379,6 +401,32 @@ export function ProposalManagementModal({
379401
/>
380402
</div>
381403

404+
{editingProposal && (
405+
<div className="mb-6">
406+
<ProposalCoSpeaker
407+
selectedSpeakers={coSpeakers}
408+
onSpeakersChange={(speakers) => {
409+
setCoSpeakers(speakers)
410+
const primaryId = selectedSpeakerIds[0]
411+
const allIds = [
412+
primaryId,
413+
...speakers.map((s) => s._id),
414+
].filter(Boolean)
415+
setSelectedSpeakerIds(allIds)
416+
}}
417+
format={proposalData.format}
418+
proposalId={editingProposal._id}
419+
pendingInvitations={invitations}
420+
onInvitationSent={(inv) =>
421+
setInvitations((prev) => [...prev, inv])
422+
}
423+
onInvitationCanceled={(id) =>
424+
setInvitations((prev) => prev.filter((inv) => inv._id !== id))
425+
}
426+
/>
427+
</div>
428+
)}
429+
382430
{/* Proposal Details Section */}
383431
<ProposalDetailsForm
384432
proposal={proposalData}

0 commit comments

Comments
 (0)