From 22cd7ede24e4e9692587dcbfde2b6567c749b275 Mon Sep 17 00:00:00 2001 From: Junwei Zhao Date: Sat, 20 Jun 2026 21:16:06 +1000 Subject: [PATCH] Update --- src/components/MySubmissionsList.tsx | 132 +++++++++++++++++++++ src/components/ResubmitDialog.tsx | 103 +++++++++++++++++ src/routes/PrivateLinksPage.tsx | 167 +++++++++++++++++---------- src/styles.css | 45 ++++++++ 4 files changed, 384 insertions(+), 63 deletions(-) create mode 100644 src/components/MySubmissionsList.tsx create mode 100644 src/components/ResubmitDialog.tsx diff --git a/src/components/MySubmissionsList.tsx b/src/components/MySubmissionsList.tsx new file mode 100644 index 0000000..11fb399 --- /dev/null +++ b/src/components/MySubmissionsList.tsx @@ -0,0 +1,132 @@ +import { useCallback, useEffect, useState } from 'react'; +import { PencilLineIcon } from 'lucide-react'; +import { + ApiClientError, + type PromotionSubmission, + type SubmissionStatus, + listMySubmissions, + promoteLink, +} from '../lib/api'; +import ResubmitDialog from './ResubmitDialog'; + +const STATUS_LABELS: Record = { + pending: 'Pending', + approved: 'Approved', + rejected: 'Rejected', + needs_changes: 'Needs changes', +}; + +interface MySubmissionsListProps { + readonly onSubmitted?: () => void; +} + +export default function MySubmissionsList({ onSubmitted }: MySubmissionsListProps) { + const [submissions, setSubmissions] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [resubmitting, setResubmitting] = useState(null); + + const refresh = useCallback(async () => { + setLoading(true); + setError(null); + try { + const result = await listMySubmissions(); + setSubmissions(result.submissions ?? []); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load submissions'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + void refresh(); + }, [refresh]); + + async function handleResubmit(input: { privateLinkId: string; proposedAlias: string; note?: string }) { + if (!resubmitting) { + return; + } + try { + await promoteLink(input); + setResubmitting(null); + await refresh(); + onSubmitted?.(); + } catch (err) { + throw new Error(err instanceof ApiClientError ? err.message : 'Failed to resubmit promotion'); + } + } + + return ( +
+ {error ?

{error}

: null} + + {loading ? ( +

Loading submissions…

+ ) : submissions.length === 0 ? ( +

You have not submitted any links for promotion yet.

+ ) : ( + + + + + + + + + + + + {submissions.map((submission) => ( + + + + + + + + ))} + +
Proposed aliasPrivate linkSubmittedStatusActions
+ #{submission.proposedAlias} + {submission.note ? {submission.note} : null} + {submission.rejectionReason ? ( + Reason: {submission.rejectionReason} + ) : null} + + {submission.privateLinkAlias + ? #{submission.privateLinkAlias} + : } + {submission.privateLinkTargetUrl ? ( + {submission.privateLinkTargetUrl} + ) : null} + + + + {STATUS_LABELS[submission.status]} + + {submission.status === 'needs_changes' ? ( + + ) : ( + + )} +
+ )} + + {resubmitting ? ( + setResubmitting(null)} + /> + ) : null} +
+ ); +} diff --git a/src/components/ResubmitDialog.tsx b/src/components/ResubmitDialog.tsx new file mode 100644 index 0000000..b965997 --- /dev/null +++ b/src/components/ResubmitDialog.tsx @@ -0,0 +1,103 @@ +import { useState } from 'react'; +import type { PromotionSubmission } from '../lib/api'; + +interface ResubmitDialogProps { + readonly submission: PromotionSubmission; + readonly onSubmit: (input: { privateLinkId: string; proposedAlias: string; note?: string }) => Promise; + readonly onClose: () => void; +} + +export default function ResubmitDialog({ submission, onSubmit, onClose }: ResubmitDialogProps) { + const [proposedAlias, setProposedAlias] = useState(submission.proposedAlias); + const [note, setNote] = useState(submission.note ?? ''); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + + async function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + if (submitting) { + return; + } + setError(null); + setSubmitting(true); + try { + await onSubmit({ + privateLinkId: submission.privateLinkId, + proposedAlias: proposedAlias.trim(), + note: note.trim() ? note.trim() : undefined, + }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to resubmit promotion'); + } finally { + setSubmitting(false); + } + } + + return ( +
+
+
+

Edit & resubmit

+ +
+ +

+ Resubmitting #{submission.privateLinkAlias ?? submission.privateLinkId} + {submission.privateLinkTargetUrl ? <> → {submission.privateLinkTargetUrl} : null} +

+ + {submission.rejectionReason ? ( +
+ Requested changes: +

{submission.rejectionReason}

+
+ ) : null} + +
+ +
+ + setProposedAlias(e.target.value)} + required + maxLength={100} + autoComplete="off" + placeholder="public-alias" + disabled={submitting} + /> +
+ + The alias reviewers will publish this link under. + {proposedAlias.trim() ? #{proposedAlias.trim()} : null} + +
+ +
+ +