Skip to content

Commit 074ccbf

Browse files
committed
feat: refactor proposal routing to use path parameters and enhance proposal submission flow
1 parent 3b9253d commit 074ccbf

9 files changed

Lines changed: 104 additions & 185 deletions

File tree

src/app/(cfp)/cfp/proposal/[[...id]]/loading.tsx

Lines changed: 0 additions & 90 deletions
This file was deleted.

src/app/(cfp)/cfp/proposal/[id]/page.tsx

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ import { getAuthSession } from '@/lib/auth'
66
import { getSpeaker } from '@/lib/speaker/sanity'
77
import { getConferenceForCurrentDomain } from '@/lib/conference/sanity'
88
import { ProposalReadOnlyView } from '@/components/cfp/ProposalReadOnlyView'
9+
import { ProposalForm } from '@/components/cfp/ProposalForm'
10+
import { ProposalGuidanceSidebar } from '@/components/cfp/ProposalGuidanceSidebar'
911
import { PostConferenceVideoPanel } from '@/components/cfp/PostConferenceVideoPanel'
1012
import { PostConferenceAudienceFeedbackPanel } from '@/components/cfp/PostConferenceAudienceFeedbackPanel'
1113
import { ProposalAttachmentsPanel } from '@/components/proposal/ProposalAttachmentsPanel'
1214
import { isConferenceOver } from '@/lib/conference/state'
1315
import { BackLink } from '@/components/BackButton'
1416
import { buildUrlWithImpersonation } from '@/lib/impersonation'
17+
import { Speaker } from '@/lib/speaker/types'
1518

1619
interface ProposalViewPageProps {
1720
params: Promise<{
@@ -63,11 +66,6 @@ export default async function ProposalViewPage({
6366
notFound()
6467
}
6568

66-
// Editable proposals (draft/submitted) should use the edit form
67-
if (proposal.status === 'draft' || proposal.status === 'submitted') {
68-
redirect(`/cfp/proposal?id=${id}`)
69-
}
70-
7169
const { speaker: currentUserSpeaker, err: speakerError } = await getSpeaker(
7270
session.speaker._id,
7371
)
@@ -110,6 +108,61 @@ export default async function ProposalViewPage({
110108

111109
const backUrl = buildUrlWithImpersonation('/cfp/list', session)
112110

111+
// Editable proposals (draft/submitted) render the edit form
112+
if (proposal.status === 'draft' || proposal.status === 'submitted') {
113+
let speakerData: { name: string; email: string } = currentUserSpeaker
114+
115+
if (proposal.speakers && Array.isArray(proposal.speakers)) {
116+
const currentUserSpeakerData = proposal.speakers.find(
117+
(s): s is Speaker =>
118+
typeof s === 'object' &&
119+
s !== null &&
120+
'_id' in s &&
121+
s._id === session.speaker._id,
122+
)
123+
if (currentUserSpeakerData) {
124+
speakerData = currentUserSpeakerData
125+
}
126+
}
127+
128+
return (
129+
<div className="mx-auto max-w-7xl">
130+
<div className="mb-6">
131+
<h1 className="font-space-grotesk text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
132+
{proposal.status === 'draft' ? 'Edit Draft' : 'Edit Proposal'}
133+
</h1>
134+
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
135+
{proposal.status === 'draft'
136+
? 'Continue working on your draft. Save your progress or submit when ready.'
137+
: 'Update your proposal details'}
138+
</p>
139+
</div>
140+
141+
<div className="flex gap-6">
142+
<div className="flex-1">
143+
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-700 dark:bg-gray-800">
144+
<ProposalForm
145+
key={id}
146+
initialProposal={proposal}
147+
initialSpeaker={speakerData}
148+
proposalId={id}
149+
userEmail={session.speaker.email}
150+
conference={conference}
151+
allowedFormats={conference.formats}
152+
currentUserSpeaker={currentUserSpeaker}
153+
initialStatus={proposal.status}
154+
/>
155+
</div>
156+
</div>
157+
158+
<div className="hidden w-80 shrink-0 lg:block">
159+
<ProposalGuidanceSidebar conference={conference} />
160+
</div>
161+
</div>
162+
</div>
163+
)
164+
}
165+
113166
return (
114167
<div className="mx-auto max-w-7xl">
115168
<div className="mb-6">

src/app/(cfp)/cfp/proposal/[[...id]]/page.tsx renamed to src/app/(cfp)/cfp/proposal/page.tsx

Lines changed: 14 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ import {
77
Level,
88
FormError,
99
ProposalInput,
10-
Status,
11-
ProposalExisting,
1210
} from '@/lib/proposal/types'
13-
import { getProposalSanity } from '@/lib/proposal/server'
1411
import { countActiveProposals } from '@/lib/proposal/utils'
1512
import { Speaker } from '@/lib/speaker/types'
1613
import { ProposalForm } from '@/components/cfp/ProposalForm'
@@ -21,32 +18,28 @@ import { headers } from 'next/headers'
2118
import { getSpeaker } from '@/lib/speaker/sanity'
2219
import { getConferenceForCurrentDomain } from '@/lib/conference/sanity'
2320

24-
export default async function ProposalPage({
25-
params,
21+
export default async function NewProposalPage({
2622
searchParams,
2723
}: {
28-
params: Promise<{ id?: string[] }>
2924
searchParams: Promise<{ id?: string }>
3025
}) {
3126
await connection()
3227

33-
// Support both /cfp/proposal/[id] and /cfp/proposal?id=[id] patterns
34-
const routeParams = await params
35-
const queryParams = await searchParams
36-
const proposalId = routeParams.id?.[0] || queryParams.id
28+
// Redirect old ?id= URLs to the path-based route
29+
const { id } = await searchParams
30+
if (id) {
31+
redirect(`/cfp/proposal/${id}`)
32+
}
3733

3834
const headersList = await headers()
3935
const fullUrl = headersList.get('x-url') || ''
4036
const session = await getAuthSession({ url: fullUrl })
4137

4238
if (!session?.speaker) {
43-
const callbackUrl = proposalId
44-
? `/cfp/proposal/${proposalId}`
45-
: '/cfp/proposal'
46-
return redirect(`/api/auth/signin?callbackUrl=${callbackUrl}`)
39+
return redirect('/api/auth/signin?callbackUrl=/cfp/proposal')
4740
}
4841

49-
let proposal: ProposalInput = {
42+
const proposal: ProposalInput = {
5043
title: '',
5144
language: Language.norwegian,
5245
description: [],
@@ -60,7 +53,6 @@ export default async function ProposalPage({
6053
let speaker = { name: '', email: '' }
6154
let loadingError: FormError | null = null
6255
let currentUserSpeaker: Speaker | null = null
63-
let proposalStatus: Status | undefined
6456

6557
const { conference, error } = await getConferenceForCurrentDomain({
6658
topics: true,
@@ -74,7 +66,7 @@ export default async function ProposalPage({
7466
}
7567
}
7668

77-
if (conference && !proposalId) {
69+
if (conference) {
7870
const { isCfpOpen } = await import('@/lib/conference/state')
7971
if (!isCfpOpen(conference)) {
8072
const contactEmail = conference.cfpEmail || conference.contactEmail
@@ -104,8 +96,9 @@ export default async function ProposalPage({
10496
}
10597
} else {
10698
currentUserSpeaker = fetchedSpeaker
99+
speaker = fetchedSpeaker
107100

108-
if (!proposalId && conference && !loadingError) {
101+
if (conference && !loadingError) {
109102
const { getProposals } = await import('@/lib/proposal/data/sanity')
110103
const { proposals: existingProposals } = await getProposals({
111104
speakerId: session.speaker._id,
@@ -133,69 +126,14 @@ export default async function ProposalPage({
133126
}
134127
}
135128

136-
try {
137-
if (proposalId) {
138-
const { proposal: fetchedProposal, proposalError } =
139-
await getProposalSanity({
140-
id: proposalId,
141-
speakerId: session.speaker._id,
142-
isOrganizer: false,
143-
})
144-
if (proposalError) {
145-
console.error('Error loading proposal:', proposalError)
146-
loadingError = {
147-
type: 'Server Error',
148-
message: 'Failed to load proposal.',
149-
}
150-
} else if (!fetchedProposal) {
151-
loadingError = { type: 'Not Found', message: 'Proposal not found.' }
152-
} else {
153-
proposal = fetchedProposal
154-
proposalStatus = (fetchedProposal as ProposalExisting).status
155-
if (
156-
fetchedProposal.speakers &&
157-
Array.isArray(fetchedProposal.speakers) &&
158-
fetchedProposal.speakers.length > 0
159-
) {
160-
const currentUserSpeakerData = fetchedProposal.speakers.find(
161-
(s): s is Speaker =>
162-
typeof s === 'object' &&
163-
s !== null &&
164-
'_id' in s &&
165-
s._id === session.speaker._id,
166-
)
167-
168-
if (currentUserSpeakerData) {
169-
speaker = currentUserSpeakerData
170-
} else if (currentUserSpeaker) {
171-
speaker = currentUserSpeaker
172-
}
173-
}
174-
}
175-
} else if (currentUserSpeaker) {
176-
speaker = currentUserSpeaker
177-
}
178-
} catch (error) {
179-
console.error('Error loading data:', error)
180-
loadingError = { type: 'Server Error', message: 'Failed to load data.' }
181-
}
182-
183129
return (
184130
<div className="mx-auto max-w-7xl">
185131
<div className="mb-6">
186132
<h1 className="font-space-grotesk text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
187-
{proposalId
188-
? proposalStatus === Status.draft
189-
? 'Edit Draft'
190-
: 'Edit Proposal'
191-
: 'Submit Presentation'}
133+
Submit Presentation
192134
</h1>
193135
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
194-
{proposalId
195-
? proposalStatus === Status.draft
196-
? 'Continue working on your draft. Save your progress or submit when ready.'
197-
: 'Update your proposal details'
198-
: 'Become our next speaker and share your knowledge with the community!'}
136+
Become our next speaker and share your knowledge with the community!
199137
</p>
200138
</div>
201139

@@ -235,15 +173,13 @@ export default async function ProposalPage({
235173
<div className="flex-1">
236174
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-700 dark:bg-gray-800">
237175
<ProposalForm
238-
key={proposalId || 'new'}
176+
key="new"
239177
initialProposal={proposal}
240178
initialSpeaker={speaker}
241-
proposalId={proposalId}
242179
userEmail={session.speaker.email}
243180
conference={conference}
244181
allowedFormats={conference.formats}
245182
currentUserSpeaker={currentUserSpeaker}
246-
initialStatus={proposalStatus}
247183
/>
248184
</div>
249185
</div>

src/app/(cfp)/cfp/submit/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default async function Submit({
1010
const { id: proposalId } = (await searchParams) || {}
1111

1212
if (proposalId) {
13-
redirect(`/cfp/proposal?id=${proposalId}`)
13+
redirect(`/cfp/proposal/${proposalId}`)
1414
}
1515

1616
redirect('/cfp/proposal')

src/components/cfp/CompactProposalList.stories.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,11 +307,34 @@ export const EditableShowsEditLinks: Story = {
307307
expect(editLink).toBeInTheDocument()
308308
expect(editLink).toHaveAttribute(
309309
'href',
310-
expect.stringContaining('/cfp/proposal?id=talk-1'),
310+
expect.stringContaining('/cfp/proposal/talk-1'),
311311
)
312312
},
313313
}
314314

315+
export const AllLinksUsePathParams: Story = {
316+
args: {
317+
proposals: [
318+
createMockProposal('draft-1', 'Draft Talk', Status.draft),
319+
createMockProposal('submitted-1', 'Submitted Talk', Status.submitted),
320+
createMockProposal('accepted-1', 'Accepted Talk', Status.accepted),
321+
],
322+
canEdit: true,
323+
},
324+
play: async ({ canvasElement }) => {
325+
const canvas = within(canvasElement)
326+
const allLinks = canvas.getAllByRole('link')
327+
for (const link of allLinks) {
328+
const href = link.getAttribute('href') || ''
329+
if (href.includes('/cfp/proposal')) {
330+
// All proposal links must use path params, never query params
331+
expect(href).not.toContain('?id=')
332+
expect(href).toMatch(/\/cfp\/proposal\/[a-z0-9-]+/)
333+
}
334+
}
335+
},
336+
}
337+
315338
export const ReadOnlyHidesEditLinks: Story = {
316339
args: {
317340
proposals: [createMockProposal('talk-1', 'My Talk', Status.submitted)],

0 commit comments

Comments
 (0)