From 6c0f9d26d6b850583b7dc00d5850492030fc4b98 Mon Sep 17 00:00:00 2001 From: Nnaji Benjamin <60315147+Benjtalkshow@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:51:39 +0100 Subject: [PATCH 1/7] UI fixes (#451) * fix: improve timeline input , ui improvement and fixes for participation tab * fix: implement 2fa for email and password login * fix: fix conflict * fix: fix submission form --- .../hackathons/submissions/SubmissionForm.tsx | 20 ++++++++++--------- .../hackathons/submissions/submissionTab.tsx | 3 ++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/components/hackathons/submissions/SubmissionForm.tsx b/components/hackathons/submissions/SubmissionForm.tsx index abb2e0e4f..3c48a5347 100644 --- a/components/hackathons/submissions/SubmissionForm.tsx +++ b/components/hackathons/submissions/SubmissionForm.tsx @@ -1081,15 +1081,17 @@ const SubmissionFormContent: React.FC = ({
- + {process.env.NODE_ENV === 'development' && ( + + )}
= ({ {!isLoadingMySubmission && !mySubmission && isAuthenticated && - isRegistered && ( + isRegistered && + status !== 'upcoming' && (

You haven't submitted a project yet. From ba798d79bb3b9906840848feb14df052da6d732b Mon Sep 17 00:00:00 2001 From: Nnaji Benjamin <60315147+Benjtalkshow@users.noreply.github.com> Date: Sat, 7 Mar 2026 16:32:42 +0100 Subject: [PATCH 2/7] UI fixes (#454) * fix: improve timeline input , ui improvement and fixes for participation tab * fix: implement 2fa for email and password login * fix: fix conflict * fix: fix submission form * fix: fix hackathon submission and participant page * fix: fix hackathon submission and participant page --- .../hackathons/[slug]/HackathonPageClient.tsx | 38 ++- .../hackathons/[slug]/submit/page.tsx | 120 +++++++++ components/hackathons/hackathonBanner.tsx | 6 +- components/hackathons/hackathonStickyCard.tsx | 25 +- .../hackathons/submissions/SubmissionForm.tsx | 43 +++- .../hackathons/submissions/submissionCard.tsx | 62 ++--- .../hackathons/submissions/submissionTab.tsx | 53 ++-- .../settings/GeneralSettingsTab.tsx | 43 +++- components/stepper/Stepper.tsx | 40 ++- hooks/hackathon/use-participants.ts | 230 ++++++++++++------ lib/providers/hackathonProvider.tsx | 2 - 11 files changed, 478 insertions(+), 184 deletions(-) create mode 100644 app/(landing)/hackathons/[slug]/submit/page.tsx diff --git a/app/(landing)/hackathons/[slug]/HackathonPageClient.tsx b/app/(landing)/hackathons/[slug]/HackathonPageClient.tsx index 0c4d1adf3..3f38dce02 100644 --- a/app/(landing)/hackathons/[slug]/HackathonPageClient.tsx +++ b/app/(landing)/hackathons/[slug]/HackathonPageClient.tsx @@ -5,6 +5,8 @@ import { useRouter, useSearchParams, useParams } from 'next/navigation'; import { useHackathonData } from '@/lib/providers/hackathonProvider'; import { useRegisterHackathon } from '@/hooks/hackathon/use-register-hackathon'; import { useLeaveHackathon } from '@/hooks/hackathon/use-leave-hackathon'; +import { useSubmission } from '@/hooks/hackathon/use-submission'; +import { useAuthStatus } from '@/hooks/use-auth'; import { RegisterHackathonModal } from '@/components/hackathons/overview/RegisterHackathonModal'; import { HackathonBanner } from '@/components/hackathons/hackathonBanner'; import { HackathonNavTabs } from '@/components/hackathons/hackathonNavTabs'; @@ -45,6 +47,13 @@ export default function HackathonPageClient() { refreshCurrentHackathon, } = useHackathonData(); + const { isAuthenticated } = useAuthStatus(); + + const { submission: mySubmission } = useSubmission({ + hackathonSlugOrId: currentHackathon?.id || '', + autoFetch: !!currentHackathon && isAuthenticated, + }); + const timeline_Events = useTimelineEvents(currentHackathon, { includeEndDate: false, dateFormat: { month: 'short', day: 'numeric', year: 'numeric' }, @@ -232,7 +241,7 @@ export default function HackathonPageClient() { // Registration status const { isRegistered, - hasSubmitted, + hasSubmitted: participantHasSubmitted, setParticipant, register: registerForHackathon, } = useRegisterHackathon({ @@ -246,6 +255,8 @@ export default function HackathonPageClient() { organizationId: undefined, }); + const hasSubmitted = !!mySubmission || participantHasSubmitted; + // Leave hackathon functionality const { isLeaving, leave: leaveHackathon } = useLeaveHackathon({ hackathonSlugOrId: currentHackathon?.id || '', @@ -296,7 +307,7 @@ export default function HackathonPageClient() { }; const handleSubmitClick = () => { - router.push('?tab=submission'); + router.push(`/hackathons/${currentHackathon?.slug}/submit`); }; const handleViewSubmissionClick = () => { @@ -308,10 +319,25 @@ export default function HackathonPageClient() { }; // Set current hackathon on mount + const [isInitializing, setIsInitializing] = useState(true); + useEffect(() => { - if (hackathonId) { - setCurrentHackathon(hackathonId); - } + let isMounted = true; + + const initHackathon = async () => { + if (hackathonId) { + await setCurrentHackathon(hackathonId); + } + if (isMounted) { + setIsInitializing(false); + } + }; + + initHackathon(); + + return () => { + isMounted = false; + }; }, [hackathonId, setCurrentHackathon]); // Handle tab changes from URL @@ -349,7 +375,7 @@ export default function HackathonPageClient() { }; // Loading state - if (loading) { + if (loading || isInitializing) { return ; } diff --git a/app/(landing)/hackathons/[slug]/submit/page.tsx b/app/(landing)/hackathons/[slug]/submit/page.tsx new file mode 100644 index 000000000..f5ca054e2 --- /dev/null +++ b/app/(landing)/hackathons/[slug]/submit/page.tsx @@ -0,0 +1,120 @@ +'use client'; + +import { use, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { useHackathonData } from '@/lib/providers/hackathonProvider'; +import { useAuthStatus } from '@/hooks/use-auth'; +import { useSubmission } from '@/hooks/hackathon/use-submission'; +import { SubmissionFormContent } from '@/components/hackathons/submissions/SubmissionForm'; +import LoadingScreen from '@/features/projects/components/CreateProjectModal/LoadingScreen'; +import { Button } from '@/components/ui/button'; +import { ArrowLeft } from 'lucide-react'; +import { toast } from 'sonner'; + +export default function SubmitProjectPage({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const router = useRouter(); + const { isAuthenticated, isLoading } = useAuthStatus(); + + const resolvedParams = use(params); + const hackathonSlug = resolvedParams.slug; + + const { + currentHackathon, + loading: hackathonLoading, + setCurrentHackathon, + } = useHackathonData(); + + useEffect(() => { + if (hackathonSlug) { + setCurrentHackathon(hackathonSlug); + } + }, [hackathonSlug, setCurrentHackathon]); + + const hackathonId = currentHackathon?.id || ''; + const orgId = currentHackathon?.organizationId || undefined; + + const { + submission: mySubmission, + isFetching: isLoadingMySubmission, + fetchMySubmission, + } = useSubmission({ + hackathonSlugOrId: hackathonId || '', + autoFetch: isAuthenticated && !!hackathonId, + }); + + // Authentication check + useEffect(() => { + if (!isLoading && !isAuthenticated) { + toast.error('You must be logged in to submit a project'); + router.push( + `/auth?mode=signin&callbackUrl=/hackathons/${hackathonSlug}/submit` + ); + } + }, [isAuthenticated, isLoading, router, hackathonSlug]); + + const handleClose = () => { + router.push(`/hackathons/${hackathonSlug}`); + }; + + const handleSuccess = () => { + fetchMySubmission(); + toast.success( + mySubmission + ? 'Submission updated successfully!' + : 'Project submitted successfully!' + ); + router.push(`/hackathons/${hackathonSlug}?tab=submission`); + }; + + if ( + isLoading || + hackathonLoading || + isLoadingMySubmission || + !currentHackathon + ) { + return ; + } + + return ( +

+
+ + +
+ +
+
+
+ ); +} diff --git a/components/hackathons/hackathonBanner.tsx b/components/hackathons/hackathonBanner.tsx index dcb8ccd55..f08d27b0a 100644 --- a/components/hackathons/hackathonBanner.tsx +++ b/components/hackathons/hackathonBanner.tsx @@ -315,14 +315,14 @@ export function HackathonBanner({ {status === 'ongoing' && isRegistered && hasSubmitted && - onViewSubmissionClick && ( + onSubmitClick && ( )} diff --git a/components/hackathons/hackathonStickyCard.tsx b/components/hackathons/hackathonStickyCard.tsx index 470562408..9a4a9c2a0 100644 --- a/components/hackathons/hackathonStickyCard.tsx +++ b/components/hackathons/hackathonStickyCard.tsx @@ -238,20 +238,17 @@ export function HackathonStickyCard(props: HackathonStickyCardProps) { )} - {/* View Submission Button */} - {status === 'ongoing' && - isRegistered && - hasSubmitted && - onViewSubmissionClick && ( - - )} + {/* Edit / View Submission Button */} + {status === 'ongoing' && isRegistered && hasSubmitted && ( + + )} {/* Find Team Button */} {status === 'ongoing' && diff --git a/components/hackathons/submissions/SubmissionForm.tsx b/components/hackathons/submissions/SubmissionForm.tsx index 3c48a5347..1604189b6 100644 --- a/components/hackathons/submissions/SubmissionForm.tsx +++ b/components/hackathons/submissions/SubmissionForm.tsx @@ -125,6 +125,7 @@ interface SubmissionFormContentProps { initialData?: Partial; submissionId?: string; onSuccess?: () => void; + onClose?: () => void; } const INITIAL_STEPS: Step[] = [ @@ -198,8 +199,19 @@ const SubmissionFormContent: React.FC = ({ initialData, submissionId, onSuccess, + onClose, }) => { - const { collapse, isExpanded: open } = useExpandableScreen(); + // Use context carefully since it might not be available when used standalone + let collapse = () => {}; + let open = true; + try { + const expandableCtx = useExpandableScreen(); + collapse = expandableCtx.collapse; + open = expandableCtx.isExpanded; + } catch (e) { + // Standalone mode, not in ExpandableScreen + } + const { currentHackathon } = useHackathonData(); const { user } = useAuthStatus(); @@ -773,7 +785,11 @@ const SubmissionFormContent: React.FC = ({ } else { await create(submissionData); } - collapse(); + if (onClose) { + onClose(); + } else { + collapse(); + } onSuccess?.(); } catch { // Error handled in hook @@ -1503,22 +1519,31 @@ const SubmissionFormContent: React.FC = ({
-
+
-
+
{renderStepContent()}
{currentStep < steps.length - 1 ? ( - - - - - Edit Submission - - e.stopPropagation()}> + + + + + - - Delete Submission - - - + onEditClick?.()} + className='cursor-pointer text-gray-300 focus:bg-gray-800 focus:text-white' + > + + Edit Submission + + onDeleteClick?.()} + className='cursor-pointer text-red-500 focus:bg-red-900/20 focus:text-red-400' + > + + Delete Submission + + + +
)}
diff --git a/components/hackathons/submissions/submissionTab.tsx b/components/hackathons/submissions/submissionTab.tsx index 61f17bec7..fff6a4974 100644 --- a/components/hackathons/submissions/submissionTab.tsx +++ b/components/hackathons/submissions/submissionTab.tsx @@ -54,6 +54,7 @@ interface SubmissionTabContentProps extends SubmissionTabProps { fetchMySubmission: () => Promise; removeSubmission: (id: string) => Promise; hackathonId: string; + hackathonSlug: string; } const SubmissionTabContent: React.FC = ({ @@ -64,10 +65,10 @@ const SubmissionTabContent: React.FC = ({ fetchMySubmission, removeSubmission, hackathonId, + hackathonSlug, }) => { const { isAuthenticated } = useAuthStatus(); const router = useRouter(); - const { expand } = useExpandableScreen(); const [viewMode, setViewMode] = useState('grid'); @@ -82,7 +83,8 @@ const SubmissionTabContent: React.FC = ({ setSelectedSort, setSelectedCategory, } = useSubmissions(); - const { currentHackathon } = useHackathonData(); + const { currentHackathon, loading: isHackathonDataLoading } = + useHackathonData(); const { status } = useHackathonStatus( currentHackathon?.startDate, currentHackathon?.submissionDeadline @@ -129,6 +131,7 @@ const SubmissionTabContent: React.FC = ({ await removeSubmission(submissionToDelete); setSubmissionToDelete(null); toast.success('Submission deleted successfully'); + window.location.reload(); } catch (error) { reportError(error, { context: 'submission-delete', @@ -263,6 +266,14 @@ const SubmissionTabContent: React.FC = ({
+ {/* Loading State */} + {(isLoadingMySubmission || isHackathonDataLoading) && ( +
+ + Loading submissions... +
+ )} + {/* Submissions Grid with Create Button if no submission */} {!isLoadingMySubmission && !mySubmission && @@ -274,7 +285,7 @@ const SubmissionTabContent: React.FC = ({ You haven't submitted a project yet.

+
+ )} + {!isAuthenticated && ( -
+
setIsOpen(false)} - className='inline-flex h-9 w-full items-center justify-center gap-2 rounded-[10px] bg-[#a7f950] px-4 py-2 text-sm font-medium whitespace-nowrap text-black shadow-sm shadow-[#a7f950]/20 transition-all hover:bg-[#a7f950]/90' + className='inline-flex min-h-[44px] w-full items-center justify-center rounded-[10px] bg-[#a7f950] px-4 py-3 text-sm font-medium text-black shadow-sm shadow-[#a7f950]/20 transition-colors hover:bg-[#a7f950]/90' > Get Started setIsOpen(false)} - className='inline-flex h-9 w-full items-center justify-center gap-2 rounded-[10px] border border-white/30 px-4 py-2 text-sm font-medium whitespace-nowrap text-white transition-all hover:border-white/40 hover:bg-white/10' + className='inline-flex min-h-[44px] w-full items-center justify-center rounded-[10px] border border-white/30 px-4 py-3 text-sm font-medium text-white transition-colors hover:border-white/40 hover:bg-white/10' > Sign In From 84b1d35bb5baff35f1c586b66c31079ca493b23b Mon Sep 17 00:00:00 2001 From: Collins Ikechukwu Date: Sat, 7 Mar 2026 19:46:14 +0100 Subject: [PATCH 4/7] feat(announcements): enhance announcement rendering and markdown support - Implemented a `stripMarkdown` function to convert Markdown content to plain text for better preview handling in the announcement page. - Created an `AnnouncementPreview` component to render announcements with Markdown support, improving content display in the announcements tab. - Updated the announcement editor to utilize a dynamic Markdown editor, enhancing the editing experience for users. --- .../[hackathonId]/announcement/page.tsx | 19 +- .../announcements/AnnouncementsTab.tsx | 24 +- .../shadcn-io/announcement-editor/index.tsx | 466 ++---------------- 3 files changed, 79 insertions(+), 430 deletions(-) diff --git a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/announcement/page.tsx b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/announcement/page.tsx index 60116acbd..20a2d8177 100644 --- a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/announcement/page.tsx +++ b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/announcement/page.tsx @@ -29,6 +29,23 @@ import { import { Switch } from '@/components/ui/switch'; import { reportError } from '@/lib/error-reporting'; +/** Strip Markdown to plain text for list preview (headings, bold, links, etc.). */ +function stripMarkdown(md: string): string { + if (!md || typeof md !== 'string') return ''; + return md + .replace(/!\[[^\]]*\]\([^)]*\)/g, '') + .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') + .replace(/#{1,6}\s*/g, '') + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/\*([^*]+)\*/g, '$1') + .replace(/__([^_]+)__/g, '$1') + .replace(/_([^_]+)_/g, '$1') + .replace(/`([^`]+)`/g, '$1') + .replace(/<[^>]*>/g, '') + .replace(/\n+/g, ' ') + .trim(); +} + export default function AnnouncementPage() { const params = useParams(); const organizationId = params.id as string; @@ -298,7 +315,7 @@ export default function AnnouncementPage() { )}

- {item.content.replace(/<[^>]*>/g, '')} + {stripMarkdown(item.content)}

diff --git a/components/hackathons/announcements/AnnouncementsTab.tsx b/components/hackathons/announcements/AnnouncementsTab.tsx index 621b2304e..e10c9b69d 100644 --- a/components/hackathons/announcements/AnnouncementsTab.tsx +++ b/components/hackathons/announcements/AnnouncementsTab.tsx @@ -5,6 +5,26 @@ import { Megaphone, Pin, ArrowUpDown, ExternalLink } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { HackathonAnnouncement } from '@/lib/api/hackathons/index'; import Link from 'next/link'; +import { useMarkdown } from '@/hooks/use-markdown'; + +/** Renders announcement body as Markdown (supports both Markdown and legacy HTML). */ +function AnnouncementPreview({ content }: { content: string }) { + const raw = content?.trim() || ''; + const isLikelyHtml = raw.startsWith('<'); + const markdown = isLikelyHtml ? raw.replace(/<[^>]*>/g, ' ') : raw; + const { styledContent, loading } = useMarkdown(markdown, { + loadingDelay: 0, + }); + + if (!raw) return No content; + if (loading) return ; + + return ( +
+ {styledContent} +
+ ); +} interface AnnouncementsTabProps { announcements: HackathonAnnouncement[]; @@ -90,9 +110,7 @@ export function AnnouncementsTab({
-

- {announcement.content.replace(/<[^>]*>/g, '')} -

+
diff --git a/components/ui/shadcn-io/announcement-editor/index.tsx b/components/ui/shadcn-io/announcement-editor/index.tsx index d29c90eb1..60d8efb9d 100644 --- a/components/ui/shadcn-io/announcement-editor/index.tsx +++ b/components/ui/shadcn-io/announcement-editor/index.tsx @@ -1,40 +1,21 @@ 'use client'; import * as React from 'react'; -import { EditorContent, useEditor } from '@tiptap/react'; -import StarterKit from '@tiptap/starter-kit'; -import { Button } from '@/components/ui/button'; -import { Separator } from '@/components/ui/separator'; -import { Toggle } from '@/components/ui/toggle'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { - Bold, - Italic, - Strikethrough, - Code, - Quote, - Link as LinkIcon, - Image as ImageIcon, - Undo, - Redo, - Code2, -} from 'lucide-react'; +import dynamic from 'next/dynamic'; +import { Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '@/components/ui/dialog'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; + +const MDEditor = dynamic( + () => import('@uiw/react-md-editor').then(mod => mod.default), + { + ssr: false, + loading: () => ( +
+ +
+ ), + } +); interface AnnouncementEditorProps { content?: string; @@ -51,129 +32,6 @@ function AnnouncementEditor({ editable = true, className, }: AnnouncementEditorProps) { - const [linkUrl, setLinkUrl] = React.useState(''); - const [linkText, setLinkText] = React.useState(''); - const [imageUrl, setImageUrl] = React.useState(''); - const [embedUrl, setEmbedUrl] = React.useState(''); - const [isLinkDialogOpen, setIsLinkDialogOpen] = React.useState(false); - const [isImageDialogOpen, setIsImageDialogOpen] = React.useState(false); - const [isEmbedDialogOpen, setIsEmbedDialogOpen] = React.useState(false); - - const editor = useEditor({ - extensions: [ - StarterKit.configure({ - bulletList: { - keepMarks: true, - keepAttributes: false, - }, - orderedList: { - keepMarks: true, - keepAttributes: false, - }, - }), - ], - content, - editable, - immediatelyRender: false, - onUpdate: ({ editor }) => { - onChange?.(editor.getHTML()); - }, - editorProps: { - attributes: { - class: cn( - 'prose prose-sm sm:prose-base lg:prose-lg xl:prose-2xl mx-auto focus:outline-none', - 'min-h-[400px] border-0 p-6 text-white' - ), - }, - }, - }); - - React.useEffect(() => { - if (editor && content !== editor.getHTML()) { - editor.commands.setContent(content); - } - }, [content, editor]); - - React.useEffect(() => { - if (editor) { - editor.setOptions({ - editorProps: { - ...editor.options.editorProps, - handleDOMEvents: { - ...editor.options.editorProps?.handleDOMEvents, - drop: (view, event) => { - const files = event.dataTransfer?.files; - if (files && files.length > 0) { - const file = files[0]; - if (file.type.startsWith('image/')) { - const reader = new FileReader(); - reader.onload = e => { - const src = e.target?.result as string; - editor - .chain() - .focus() - .insertContent( - `Image` - ) - .run(); - }; - reader.readAsDataURL(file); - return true; - } - } - return false; - }, - }, - }, - }); - } - }, [editor]); - - if (!editor) { - return null; - } - - const handleInsertLink = () => { - if (linkUrl && linkText) { - editor - .chain() - .focus() - .insertContent(`${linkText}`) - .run(); - setLinkUrl(''); - setLinkText(''); - setIsLinkDialogOpen(false); - } - }; - - const handleInsertImage = () => { - if (imageUrl) { - editor - .chain() - .focus() - .insertContent( - `Image` - ) - .run(); - setImageUrl(''); - setIsImageDialogOpen(false); - } - }; - - const handleInsertEmbed = () => { - if (embedUrl) { - editor - .chain() - .focus() - .insertContent( - `` - ) - .run(); - setEmbedUrl(''); - setIsEmbedDialogOpen(false); - } - }; - return (
-
- - - - - - - - - - editor.chain().focus().toggleBold().run()} - disabled={!editor.can().chain().focus().toggleBold().run()} - className='h-8 w-8 p-0 data-[state=on]:bg-gray-800 data-[state=on]:text-white' - > - - - - editor.chain().focus().toggleItalic().run()} - disabled={!editor.can().chain().focus().toggleItalic().run()} - className='h-8 w-8 p-0 data-[state=on]:bg-gray-800 data-[state=on]:text-white' - > - - - - editor.chain().focus().toggleStrike().run()} - disabled={!editor.can().chain().focus().toggleStrike().run()} - className='h-8 w-8 p-0 data-[state=on]:bg-gray-800 data-[state=on]:text-white' - > - - - - editor.chain().focus().toggleCode().run()} - disabled={!editor.can().chain().focus().toggleCode().run()} - className='h-8 w-8 p-0 data-[state=on]:bg-gray-800 data-[state=on]:text-white' - > - - - - - editor.chain().focus().toggleBlockquote().run() - } - className='h-8 w-8 p-0 data-[state=on]:bg-gray-800 data-[state=on]:text-white' - > - - - - - - - - - - Insert Link - -
-
- - setLinkText(e.target.value)} - placeholder='Link text' - className='bg-background border-gray-800 text-white' - /> -
-
- - setLinkUrl(e.target.value)} - placeholder='https://example.com' - className='bg-background border-gray-800 text-white' - /> -
-
- - -
-
-
-
- - - - - - - - Insert Image - -
-
- - setImageUrl(e.target.value)} - placeholder='https://example.com/image.jpg' - className='bg-background border-gray-800 text-white' - /> -
-
- - -
-
-
-
- - - - - - - - Insert Embed - -
-
- - setEmbedUrl(e.target.value)} - placeholder='https://example.com/embed' - className='bg-background border-gray-800 text-white' - /> -
-
- - -
-
-
-
-
- -
- - {(!editor.getHTML() || editor.getHTML() === '

') && ( -
- {placeholder} -
- )} -
+ onChange?.(value ?? '')} + height={400} + data-color-mode='dark' + preview='edit' + hideToolbar={!editable} + visibleDragbar={editable} + textareaProps={{ + placeholder, + readOnly: !editable, + style: { + fontSize: 14, + lineHeight: 1.6, + color: '#ffffff', + backgroundColor: '#18181b', + fontFamily: 'inherit', + border: 'none', + }, + }} + style={{ + backgroundColor: '#18181b', + color: '#ffffff', + border: 'none', + }} + />
); } From 4ae17617584f912a257217b33d106cf768a65de4 Mon Sep 17 00:00:00 2001 From: Sandeep chauhan <92181599+ryzen-xp@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:04:07 +0530 Subject: [PATCH 5/7] =?UTF-8?q?feat(newsletter):=20implement=20newsletter?= =?UTF-8?q?=20API=20integration=20with=20subscribe=E2=80=A6=20(#452)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(newsletter): implement newsletter API integration with subscribe, confirm, unsubscribe, and preferences * Draft feat(newsletter): Imp some extra things * feat(newsletter): add API routes, client helpers, and UI pages --- .../newsletter/confirm/error/page.tsx | 32 ++++++ app/(landing)/newsletter/confirmed/page.tsx | 14 +++ .../newsletter/unsubscribe/error/page.tsx | 31 ++++++ .../newsletter/unsubscribed/page.tsx | 14 +++ app/api/newsletter/confirm/[token]/route.ts | 21 ++++ app/api/newsletter/preferences/route.ts | 28 ++++++ app/api/newsletter/subscribe/route.ts | 25 +---- .../newsletter/unsubscribe/[token]/route.ts | 20 ++++ app/api/newsletter/unsubscribe/route.ts | 15 +++ components/overview/Newsletter.tsx | 54 +++++++++- lib/api/waitlist.ts | 98 ++++++++++++++++--- 11 files changed, 315 insertions(+), 37 deletions(-) create mode 100644 app/(landing)/newsletter/confirm/error/page.tsx create mode 100644 app/(landing)/newsletter/confirmed/page.tsx create mode 100644 app/(landing)/newsletter/unsubscribe/error/page.tsx create mode 100644 app/(landing)/newsletter/unsubscribed/page.tsx create mode 100644 app/api/newsletter/confirm/[token]/route.ts create mode 100644 app/api/newsletter/preferences/route.ts create mode 100644 app/api/newsletter/unsubscribe/[token]/route.ts create mode 100644 app/api/newsletter/unsubscribe/route.ts diff --git a/app/(landing)/newsletter/confirm/error/page.tsx b/app/(landing)/newsletter/confirm/error/page.tsx new file mode 100644 index 000000000..84de7a0ed --- /dev/null +++ b/app/(landing)/newsletter/confirm/error/page.tsx @@ -0,0 +1,32 @@ +'use client'; +import { useSearchParams } from 'next/navigation'; +import Link from 'next/link'; +import { Suspense } from 'react'; + +const msgs: Record = { + expired: 'This confirmation link has expired.', + invalid: 'This confirmation link is invalid or already used.', +}; + +function Content() { + const p = useSearchParams(); + return ( +
+

Confirmation failed

+

+ {msgs[p.get('reason') ?? ''] ?? 'An unexpected error occurred.'} +

+ + Back to home + +
+ ); +} + +export default function Page() { + return ( + + + + ); +} diff --git a/app/(landing)/newsletter/confirmed/page.tsx b/app/(landing)/newsletter/confirmed/page.tsx new file mode 100644 index 000000000..67718529c --- /dev/null +++ b/app/(landing)/newsletter/confirmed/page.tsx @@ -0,0 +1,14 @@ +import Link from 'next/link'; +export default function NewsletterConfirmedPage() { + return ( +
+

You're subscribed! 🎉

+

+ Your subscription has been confirmed. Welcome aboard! +

+ + Back to home + +
+ ); +} diff --git a/app/(landing)/newsletter/unsubscribe/error/page.tsx b/app/(landing)/newsletter/unsubscribe/error/page.tsx new file mode 100644 index 000000000..b957980a1 --- /dev/null +++ b/app/(landing)/newsletter/unsubscribe/error/page.tsx @@ -0,0 +1,31 @@ +'use client'; +import { useSearchParams } from 'next/navigation'; +import Link from 'next/link'; +import { Suspense } from 'react'; + +const msgs: Record = { + invalid: 'This unsubscribe link is invalid or has already been used.', +}; + +function Content() { + const p = useSearchParams(); + return ( +
+

Unsubscribe failed

+

+ {msgs[p.get('reason') ?? ''] ?? 'An unexpected error occurred.'} +

+ + Back to home + +
+ ); +} + +export default function Page() { + return ( + + + + ); +} diff --git a/app/(landing)/newsletter/unsubscribed/page.tsx b/app/(landing)/newsletter/unsubscribed/page.tsx new file mode 100644 index 000000000..0d758a3ca --- /dev/null +++ b/app/(landing)/newsletter/unsubscribed/page.tsx @@ -0,0 +1,14 @@ +import Link from 'next/link'; +export default function NewsletterUnsubscribedPage() { + return ( +
+

You've been unsubscribed

+

+ You won't receive any more emails from us. +

+ + Back to home + +
+ ); +} diff --git a/app/api/newsletter/confirm/[token]/route.ts b/app/api/newsletter/confirm/[token]/route.ts new file mode 100644 index 000000000..774ba0589 --- /dev/null +++ b/app/api/newsletter/confirm/[token]/route.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const backendUrl = process.env.NEXT_PUBLIC_API_URL; +const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? ''; + +export async function GET( + _req: NextRequest, + { params }: { params: Promise<{ token: string }> } +) { + const { token } = await params; + const res = await fetch(`${backendUrl}/api/newsletter/confirm/${token}`, { + redirect: 'manual', + }); + if (res.status === 302) { + return NextResponse.redirect(`${appUrl}/newsletter/confirmed`); + } + const reason = res.status === 400 ? 'expired' : 'invalid'; + return NextResponse.redirect( + `${appUrl}/newsletter/confirm/error?reason=${reason}` + ); +} diff --git a/app/api/newsletter/preferences/route.ts b/app/api/newsletter/preferences/route.ts new file mode 100644 index 000000000..36acc11b4 --- /dev/null +++ b/app/api/newsletter/preferences/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server'; + +function normalizeBackendUrl(raw: string | undefined): string | undefined { + if (!raw) return undefined; + return raw.replace(/\/$/, '').replace(/\/api$/i, ''); +} + +const backendUrl = normalizeBackendUrl(process.env.NEXT_PUBLIC_API_URL); + +export async function PATCH(req: NextRequest) { + const body = await req.json(); + + if (!backendUrl) { + return NextResponse.json( + { message: 'Server configuration error: NEXT_PUBLIC_API_URL is not set' }, + { status: 500 } + ); + } + + const res = await fetch(`${backendUrl}/api/newsletter/preferences`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + return NextResponse.json(await res.json().catch(() => ({})), { + status: res.status, + }); +} diff --git a/app/api/newsletter/subscribe/route.ts b/app/api/newsletter/subscribe/route.ts index 9b7b0fca7..31f0f067f 100644 --- a/app/api/newsletter/subscribe/route.ts +++ b/app/api/newsletter/subscribe/route.ts @@ -4,17 +4,14 @@ export async function POST(request: NextRequest) { try { const body = await request.json(); - // Normalize API URL: remove trailing slash and /api if present - // The env var should be base URL without /api (e.g., https://api.boundlessfi.xyz) let backendUrl = process.env.NEXT_PUBLIC_API_URL || 'https://staging-api.boundlessfi.xyz'; backendUrl = backendUrl.replace(/\/$/, '').replace(/\/api$/i, ''); - const response = await fetch(`${backendUrl}/api/waitlist/subscribe`, { + const response = await fetch(`${backendUrl}/api/newsletter/subscribe`, { method: 'POST', headers: { 'Content-Type': 'application/json', - ...(request.headers.get('user-agent') && { 'User-Agent': request.headers.get('user-agent')!, }), @@ -22,25 +19,11 @@ export async function POST(request: NextRequest) { body: JSON.stringify(body), }); - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - return NextResponse.json( - { - message: errorData.message || 'Failed to subscribe to waitlist', - status: response.status, - }, - { status: response.status } - ); - } - - const data = await response.json(); - return NextResponse.json(data, { status: 200 }); + const data = await response.json().catch(() => ({})); + return NextResponse.json(data, { status: response.status }); } catch { return NextResponse.json( - { - message: 'Internal server error', - status: 500, - }, + { message: 'Internal server error', status: 500 }, { status: 500 } ); } diff --git a/app/api/newsletter/unsubscribe/[token]/route.ts b/app/api/newsletter/unsubscribe/[token]/route.ts new file mode 100644 index 000000000..dfd12ccdb --- /dev/null +++ b/app/api/newsletter/unsubscribe/[token]/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const backendUrl = process.env.NEXT_PUBLIC_API_URL; +const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? ''; + +export async function GET( + _req: NextRequest, + { params }: { params: Promise<{ token: string }> } +) { + const { token } = await params; + const res = await fetch(`${backendUrl}/api/newsletter/unsubscribe/${token}`, { + redirect: 'manual', + }); + if (res.status === 302) { + return NextResponse.redirect(`${appUrl}/newsletter/unsubscribed`); + } + return NextResponse.redirect( + `${appUrl}/newsletter/unsubscribe/error?reason=invalid` + ); +} diff --git a/app/api/newsletter/unsubscribe/route.ts b/app/api/newsletter/unsubscribe/route.ts new file mode 100644 index 000000000..64e31d8d9 --- /dev/null +++ b/app/api/newsletter/unsubscribe/route.ts @@ -0,0 +1,15 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const backendUrl = process.env.NEXT_PUBLIC_API_URL; + +export async function POST(req: NextRequest) { + const body = await req.json(); + const res = await fetch(`${backendUrl}/api/newsletter/unsubscribe`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + return NextResponse.json(await res.json().catch(() => ({})), { + status: res.status, + }); +} diff --git a/components/overview/Newsletter.tsx b/components/overview/Newsletter.tsx index eac5a25d6..f2360d1b6 100644 --- a/components/overview/Newsletter.tsx +++ b/components/overview/Newsletter.tsx @@ -25,7 +25,11 @@ import { BoundlessButton } from '../buttons'; import { Input } from '../ui/input'; import gsap from 'gsap'; import { useGSAP } from '@gsap/react'; -import { newsletterSubscribe } from '@/lib/api/waitlist'; +import { + newsletterSubscribe, + type NewsletterApiError, + type NewsletterTag, +} from '@/lib/api/waitlist'; import { Button } from '../ui/button'; const formSchema = z.object({ @@ -42,6 +46,7 @@ const Newsletter = ({ }) => { const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); + const [selectedTags, setSelectedTags] = useState([]); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { @@ -82,15 +87,23 @@ const Newsletter = ({ const onSubmit = async (values: z.infer) => { setError(null); setIsSubmitting(true); - try { await newsletterSubscribe({ email: values.email, name: values.name, + source: 'website', + tags: selectedTags, }); - } catch { - setError('Failed to submit form. Please try again.'); - setIsSubmitting(false); + onOpenChange(false); + window.location.href = '/newsletter/confirmed'; + } catch (err) { + const e = err as NewsletterApiError; + if (e.code === 'ALREADY_SUBSCRIBED') + setError('This email is already subscribed.'); + else if (e.code === 'RATE_LIMITED') + setError('Too many attempts. Please try again in a minute.'); + else if (e.code === 'INVALID_TAGS') setError('Invalid topic selection.'); + else setError('Failed to submit form. Please try again.'); } finally { setIsSubmitting(false); } @@ -186,6 +199,37 @@ const Newsletter = ({ )} /> + +
+ {( + [ + 'bounties', + 'hackathons', + 'grants', + 'updates', + ] as NewsletterTag[] + ).map(tag => ( + + ))} +
+ = { + 400: 'INVALID_TAGS', + 404: 'NOT_FOUND', + 409: 'ALREADY_SUBSCRIBED', + 429: 'RATE_LIMITED', +}; + +function throwApiError(status: number, body: { message?: string }): never { + throw { + status, + code: codeMap[status] ?? 'UNKNOWN', + message: body.message ?? 'An unexpected error occurred.', + } as NewsletterApiError; +} + export const addToWaitlist = async (data: AddToWaitlistRequest) => { const res = await fetch('/api/waitlist/subscribe', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); @@ -32,16 +76,48 @@ export const addToWaitlist = async (data: AddToWaitlistRequest) => { export const newsletterSubscribe = async (data: NewsletterSubscribeRequest) => { const res = await fetch('/api/newsletter/subscribe', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); - if (!res.ok) { - const errorData = await res.json().catch(() => ({})); - throw new Error(errorData.message || 'Failed to subscribe to newsletter'); + const body = await res.json().catch(() => ({})); + if (!res.ok) throwApiError(res.status, body); + return body as { message: string; id: string }; +}; + +export const newsletterUnsubscribe = async ( + data: NewsletterUnsubscribeRequest +) => { + const res = await fetch('/api/newsletter/unsubscribe', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + + const body = await res.json().catch(() => ({})); + if (!res.ok) throwApiError(res.status, body); + return body as { message: string }; +}; + +export const newsletterUpdatePreferences = async ( + data: NewsletterPreferencesRequest +) => { + const invalid = data.tags.filter(t => !NEWSLETTER_TAGS.includes(t)); + if (invalid.length > 0) { + throw { + status: 400, + code: 'INVALID_TAGS', + message: `Invalid tags: ${invalid.join(', ')}. Allowed: ${NEWSLETTER_TAGS.join(', ')}.`, + } as NewsletterApiError; } - return res.json(); + const res = await fetch('/api/newsletter/preferences', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + + const body = await res.json().catch(() => ({})); + if (!res.ok) throwApiError(res.status, body); + return body as { message: string }; }; From 17d37c72cbc408ac2c8b2c1f7758d17438ab9ea8 Mon Sep 17 00:00:00 2001 From: Ekene Ngwudike <94962750+Ekene001@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:33:38 +0100 Subject: [PATCH 6/7] Docs: add comprehensive bug test report for organization features (#456) * Test: add comprehensive bug test report for organization features * docs: update bug test report for organization features and address critical issues * fix: update branch name in bug test report and upgrade dompurify to version 3.3.2 --- docs/bug-test-report.md | 123 ++++++++++++++++++++++++++++++++++++++++ package-lock.json | 9 ++- 2 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 docs/bug-test-report.md diff --git a/docs/bug-test-report.md b/docs/bug-test-report.md new file mode 100644 index 000000000..e09a659ea --- /dev/null +++ b/docs/bug-test-report.md @@ -0,0 +1,123 @@ +# Bug Test Report: Organization & Onboarding Flow + +**Project:** BoundlessFi +**Date:** March 7, 2026 +**Environment:** Frontend (Local) / Backend (Staging API) +**Status:** Issue #448 Verification Complete + +--- + +## 1. Executive Summary + +Overall, core organization features (creation, search, and navigation) are stable. However, critical blockers exist within the **Invitation Lifecycle** and **Hackathon Publishing** flows. Additionally, UX refinements are needed for mobile navigation and input validation. + +--- + +## 2. Issue #448 Verification Status [COMPLETED] + +All items specified in Issue #448 have been verified for production readiness: + +| Feature | Status | Notes | +| :----------------------- | :----- | :--------------------------------------------------- | +| **Public Profile Route** | Pass | `/org/[slug]` functions correctly end-to-end. | +| **OG Metadata** | Pass | Title, description, and images validated. | +| **Settings Tabs** | Pass | Profile, Links, Members, and Transfer verified. | +| **Sidebar Actions** | Pass | Host Hackathon and Grants (disabled state) verified. | +| **Search/Sort** | Pass | Search and sort keys behave as expected. | +| **Deep-linking** | Pass | Direct URL loading for organizations is functional. | +| **Header Search** | Pass | `⌘K` navigation is bug-free. | +| **Profile Validation** | Pass | Slug/Profile validation logic is functional. | +| **Mobile Responsive** | Pass | Hamburger menu is functional on settings pages. | + +--- + +## 3. High-Priority Bugs + +### BUG-001: Invite Link Routing Mismatch (404) + +- **Issue:** Generated invite links point to `/signup` instead of the active `/auth` route. +- **Example Link:** `https://staging.boundlessfi.xyz/signup?invitationId=...` +- **Impact:** Users cannot join organizations via email. +- **Recommended Fix:** Update `getInvitationUrl` in the backend/auth-config to target `/auth`. + +### BUG-002: Invite Acceptance Logic Failure + +- **Issue:** Manually correcting the URL to `/auth` allows sign-up, but the user is not associated with the organization. +- **Actual Behavior:** User is redirected to home; status remains "Pending"; member list is not updated. +- **Expected Behavior:** User should automatically join the organization and redirect to the org dashboard. + +### BUG-003: Hackathon Publish Endpoint (404) + +- **Action:** Click **Publish** in the Preview tab for a draft hackathon. +- **Observed Error:** `PUT .../api/organizations/{orgId}/hackathons/draft/{hackathonId}/publish` returns **404 Not Found**. +- **Likely Root Cause:** Wallet balance is insufficient for publish-related on-chain or fee-dependent operations. +- **UX Gap:** The UI does not tell users that the publish failure is caused by **insufficient wallet funds**. +- **Impact:** Users cannot move hackathons from "Draft" to "Live" and do not know how to resolve it. +- **Suggestion:** Show a clear inline/toast error such as: `Insufficient wallet funds to publish. Please fund your wallet and try again.` + +**Suggested User Guidance (Fund Wallet Steps)** + +1. Open wallet settings from the profile/wallet menu. +2. Copy your connected wallet address. +3. Add funds to that wallet on the required network (via exchange transfer, bridge, or faucet for test/staging). +4. Wait for transaction confirmation and refresh the app. +5. Return to the hackathon draft and click **Publish** again. + +### BUG-004: Delete/Archive Organization Non-Functional + +- **Issue:** Actions to delete or archive an organization fail. + +**What I tested** + +- Opened `/organizations`. +- Selected an organization. +- Clicked the **Archive** button and completed the confirmation prompt. +- Clicked the **Delete** button and completed the confirmation prompt. + +**Observed Result** + +- Both actions failed. +- The organization was not archived and not deleted. + +**Expected Result** + +- Archive should move the organization to archived state. +- Delete should remove the organization. + +**Scope** + +- Test type: Manual UI test. +- Environment: Frontend local + staging backend. +- Role: Owner. +- Organizations tested: 1 organization in this pass. + +## 4. UI/UX Improvements + +### UX-001: Mobile Navigation Tabs (Host Hackathon) + +- **Observation:** Sub-navigation tabs (e.g., Participation, Edit) are difficult to access/truncated on mobile screens. +- **Suggestion:** Implement **horizontal scrollable tabs** for sub-nav menus to improve mobile accessibility. + +### UX-002: Slug Availability Feedback + +- **Issue:** The UI displays "Slug available" even for taken slugs, only failing upon final form submission. +- **Suggestion:** Real-time validation should return "Slug already taken" before the user attempts to submit. + +### UX-003: Password Strength Feedback + +- **Issue:** Form submission is blocked for weak passwords without explaining why. +- **Suggestion:** Add a helper text: _"Password must be at least 8 characters and include a number."_ + +--- + +## 5. Technical Edge Cases + +### Route Guarding: Invalid Organization IDs + +- **Issue:** Manually entering an invalid ID (e.g., `/organizations/random123/settings`) still renders the settings UI shell. +- **Recommendation:** Implement a check to return a 404 page if the Organization ID does not exist in the database. + +--- + +**Reported by:** QA Team +**Branch:** `Test/Test-the-organization-flow---manual-QA-checklist` diff --git a/package-lock.json b/package-lock.json index 32c85f04a..8a7aca793 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8970,10 +8970,13 @@ } }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", + "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", "license": "(MPL-2.0 OR Apache-2.0)", + "engines": { + "node": ">=20" + }, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } From e16dfeeb22df1f901dc8ed84cd0954bde85933fb Mon Sep 17 00:00:00 2001 From: Collins Ikechukwu Date: Sun, 8 Mar 2026 19:18:38 +0100 Subject: [PATCH 7/7] fix: update the milestone submission flow for campaign --- .../milestones/[milestoneIndex]/page.tsx | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/app/me/crowdfunding/[slug]/milestones/[milestoneIndex]/page.tsx b/app/me/crowdfunding/[slug]/milestones/[milestoneIndex]/page.tsx index 2d3463952..7aa3c0bfb 100644 --- a/app/me/crowdfunding/[slug]/milestones/[milestoneIndex]/page.tsx +++ b/app/me/crowdfunding/[slug]/milestones/[milestoneIndex]/page.tsx @@ -162,31 +162,31 @@ export default function MilestoneDetailPage({ params }: PageProps) { // Step 3: Perform blockchain transaction with Trustless Work SDK toast('Please confirm the transaction in your wallet'); - const { unsignedTransaction } = await changeMilestoneStatus( - { - contractId: campaign.escrowAddress, - milestoneIndex: (milestone.orderIndex ?? milestoneIndex).toString(), - newStatus: data.status === 'completed' ? 'completed' : 'in_progress', - newEvidence: data.submissionNotes, // Use submission notes as evidence description - serviceProvider: walletAddress || '', - }, - 'multi-release' - ); - if (!unsignedTransaction) { - throw new Error( - 'Unsigned transaction is missing from useChangeMilestoneStatusresponse.' - ); - } - - const signedXdr = await signTransaction({ - unsignedTransaction, - address: walletAddress || '', - }); - - const trxsent = await sendTransaction(signedXdr); - if (trxsent.status === 'SUCCESS') { - toast.success('Transaction confirmed on blockchain'); - } + // const { unsignedTransaction } = await changeMilestoneStatus( + // { + // contractId: campaign.escrowAddress, + // milestoneIndex: (milestone.orderIndex ?? milestoneIndex).toString(), + // newStatus: data.status === 'completed' ? 'completed' : 'in_progress', + // newEvidence: data.submissionNotes, // Use submission notes as evidence description + // serviceProvider: walletAddress || '', + // }, + // 'multi-release' + // ); + // if (!unsignedTransaction) { + // throw new Error( + // 'Unsigned transaction is missing from useChangeMilestoneStatusresponse.' + // ); + // } + + // const signedXdr = await signTransaction({ + // unsignedTransaction, + // address: walletAddress || '', + // }); + + // const trxsent = await sendTransaction(signedXdr); + // if (trxsent.status === 'SUCCESS') { + // toast.success('Transaction confirmed on blockchain'); + // } toast('Updating milestone...');