field.onChange(value)}
+ onClick={() =>
+ !isRegistrationClosed && field.onChange(value)
+ }
+ disabled={isRegistrationClosed}
className={cn(
'flex w-full items-start gap-3 rounded-lg border p-4 text-left transition-all',
isSelected
? 'border-primary/50 bg-primary/10'
- : 'border-zinc-800 bg-zinc-900/30 hover:border-zinc-700 hover:bg-zinc-900/50'
+ : 'border-zinc-800 bg-zinc-900/30 hover:border-zinc-700 hover:bg-zinc-900/50',
+ isRegistrationClosed &&
+ 'cursor-not-allowed opacity-60'
)}
>
{
const next = Math.min(
(field.value || 2) + 1,
@@ -459,6 +496,7 @@ export default function ParticipantTab({
diff --git a/components/organization/hackathons/new/tabs/RewardsTab.tsx b/components/organization/hackathons/new/tabs/RewardsTab.tsx
index 56f58f92..7867f012 100644
--- a/components/organization/hackathons/new/tabs/RewardsTab.tsx
+++ b/components/organization/hackathons/new/tabs/RewardsTab.tsx
@@ -318,6 +318,7 @@ export default function RewardsTab({
prizeAmount: '0',
description: '',
currency: 'USDC',
+ rank: 1,
passMark: 80,
},
{
@@ -326,6 +327,7 @@ export default function RewardsTab({
prizeAmount: '0',
description: '',
currency: 'USDC',
+ rank: 2,
passMark: 70,
},
{
@@ -334,6 +336,7 @@ export default function RewardsTab({
prizeAmount: '0',
description: '',
currency: 'USDC',
+ rank: 3,
passMark: 50,
},
],
@@ -384,6 +387,7 @@ export default function RewardsTab({
prizeAmount: String(Math.round((baseAmount * percentage) / 100)),
description: '',
currency: 'USDC',
+ rank: idx + 1,
passMark: 80 - idx * 10,
}));
replace(newTiers);
@@ -408,6 +412,7 @@ export default function RewardsTab({
prizeAmount: '0',
description: '',
currency: 'USDC',
+ rank: fields.length + 1,
passMark: 0,
});
toast.success('Prize tier added');
@@ -426,10 +431,13 @@ export default function RewardsTab({
try {
if (onSave) {
await onSave(data);
- toast.success('Rewards saved successfully');
}
- } catch {
- toast.error('Failed to save rewards. Please try again.');
+ } catch (error: any) {
+ const message = error.response?.data?.message || error.message;
+ const errorMessage = Array.isArray(message) ? message[0] : message;
+ toast.error(
+ errorMessage || 'Failed to save rewards settings. Please try again.'
+ );
}
};
diff --git a/components/organization/hackathons/new/tabs/schemas/infoSchema.ts b/components/organization/hackathons/new/tabs/schemas/infoSchema.ts
index 853049bc..831379c2 100644
--- a/components/organization/hackathons/new/tabs/schemas/infoSchema.ts
+++ b/components/organization/hackathons/new/tabs/schemas/infoSchema.ts
@@ -52,7 +52,15 @@ export const infoSchema = z
city: z.string().optional(),
venueName: z.string().optional(),
venueAddress: z.string().optional(),
- slug: z.string().optional(),
+ slug: z
+ .string()
+ .min(3, 'Slug must be at least 3 characters')
+ .max(50, 'Slug must be less than 50 characters')
+ .regex(
+ /^[a-z0-9-]+$/,
+ 'Slug can only contain lowercase letters, numbers, and hyphens'
+ )
+ .optional(),
})
.superRefine((data, ctx) => {
if (data.venueType === 'physical') {
diff --git a/components/organization/hackathons/new/tabs/schemas/rewardsSchema.ts b/components/organization/hackathons/new/tabs/schemas/rewardsSchema.ts
index ec82e9d0..8b319b21 100644
--- a/components/organization/hackathons/new/tabs/schemas/rewardsSchema.ts
+++ b/components/organization/hackathons/new/tabs/schemas/rewardsSchema.ts
@@ -11,6 +11,7 @@ export const prizeTierSchema = z.object({
),
description: z.string().optional(),
currency: z.string().optional().default('USDC'),
+ rank: z.number().int().min(1),
passMark: z.number().min(0).max(100),
});
diff --git a/components/organization/hackathons/rewards/AnnouncementSection.tsx b/components/organization/hackathons/rewards/AnnouncementSection.tsx
index d5ebee5c..c9699715 100644
--- a/components/organization/hackathons/rewards/AnnouncementSection.tsx
+++ b/components/organization/hackathons/rewards/AnnouncementSection.tsx
@@ -4,6 +4,7 @@ import React, { useState } from 'react';
import { Edit2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useMarkdown } from '@/hooks/use-markdown';
+import { sanitizeHtml } from '@/lib/utils/renderHtml';
interface AnnouncementSectionProps {
announcement: string;
@@ -16,49 +17,55 @@ export default function AnnouncementSection({
}: AnnouncementSectionProps) {
const [isExpanded, setIsExpanded] = useState(false);
- const announcementContent = useMarkdown(announcement || '', {
+ const announcementRaw = announcement || '';
+ const isLongAnnouncement = announcementRaw.length > 300;
+
+ const markdownToParse =
+ !isExpanded && isLongAnnouncement
+ ? announcementRaw.substring(0, 300) + '...'
+ : announcementRaw;
+
+ const announcementContent = useMarkdown(markdownToParse, {
breaks: true,
gfm: true,
});
+ const handleToggleExpand = () => setIsExpanded(!isExpanded);
+
+ const sanitizedContent = sanitizeHtml(announcementContent.content);
+
return (
-
-
-
Announcement
+
+
+
Announcement
-
+
{announcement ? (
<>
500
- ? announcementContent.content.substring(0, 500) + '...'
- : announcementContent.content,
- }}
+ className='markdown-content text-gray-400'
+ dangerouslySetInnerHTML={sanitizedContent}
/>
- {announcementContent.content.length > 500 && (
+ {isLongAnnouncement && (
)}
>
) : (
-
No announcement added yet.
+
No announcement added yet.
)}
diff --git a/components/organization/hackathons/rewards/AnnouncementStep.tsx b/components/organization/hackathons/rewards/AnnouncementStep.tsx
index 4b0ca8c6..10d9b90e 100644
--- a/components/organization/hackathons/rewards/AnnouncementStep.tsx
+++ b/components/organization/hackathons/rewards/AnnouncementStep.tsx
@@ -14,17 +14,19 @@ export const AnnouncementStep: React.FC
= ({
onAnnouncementChange,
}) => {
return (
-
+
-
+
-
- This message will be displayed publicly with the winners announcement.
+
+ Displayed publicly with the winners announcement.
diff --git a/components/organization/hackathons/rewards/CreateMilestonesButton.tsx b/components/organization/hackathons/rewards/CreateMilestonesButton.tsx
deleted file mode 100644
index 416e6321..00000000
--- a/components/organization/hackathons/rewards/CreateMilestonesButton.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-'use client';
-
-import React, { useState, useMemo } from 'react';
-import { BoundlessButton } from '@/components/buttons';
-import { Trophy } from 'lucide-react';
-import { Submission } from './types';
-import { HackathonEscrowData } from '@/lib/api/hackathons';
-import { PrizeTier } from '@/components/organization/hackathons/new/tabs/schemas/rewardsSchema';
-import { useWalletAddresses } from '@/hooks/use-wallet-addresses';
-import { useMilestoneCreation } from '@/hooks/use-milestone-creation';
-import { CreateMilestonesDialog } from './CreateMilestonesDialog';
-
-interface CreateMilestonesButtonProps {
- submissions: Submission[];
- prizeTiers: PrizeTier[];
- escrow: HackathonEscrowData | null;
- organizationId: string;
- hackathonId: string;
- onSuccess?: () => void;
-}
-
-export default function CreateMilestonesButton({
- submissions,
- prizeTiers,
- escrow,
- organizationId,
- hackathonId,
- onSuccess,
-}: CreateMilestonesButtonProps) {
- const [isOpen, setIsOpen] = useState(false);
-
- const winners = useMemo(
- () => submissions.filter(s => s.rank !== undefined && s.rank !== null),
- [submissions]
- );
-
- const hasWinners = winners.length > 0;
- const canCreateMilestones = escrow?.isFunded && hasWinners;
-
- const { walletAddresses, errors, setErrors, handleWalletAddressChange } =
- useWalletAddresses({
- isOpen,
- winners,
- });
-
- const { isLoading, createMilestones } = useMilestoneCreation({
- winners,
- prizeTiers,
- escrow,
- organizationId,
- hackathonId,
- walletAddresses,
- setErrors,
- onSuccess,
- });
-
- const handleCreateMilestones = async () => {
- try {
- await createMilestones();
- setIsOpen(false);
- } catch {
- // Error is handled in the hook
- }
- };
-
- if (!hasWinners) {
- return null;
- }
-
- return (
- <>
-
setIsOpen(true)}
- disabled={!canCreateMilestones}
- className='gap-2'
- >
-
- Create Milestones
-
-
-
- >
- );
-}
diff --git a/components/organization/hackathons/rewards/PodiumCard.tsx b/components/organization/hackathons/rewards/PodiumCard.tsx
index 1c2f7ac6..e6c1374f 100644
--- a/components/organization/hackathons/rewards/PodiumCard.tsx
+++ b/components/organization/hackathons/rewards/PodiumCard.tsx
@@ -108,11 +108,9 @@ export default function PodiumCard({ rank, submission }: PodiumCardProps) {
>
{submission ? (
<>
-
+
- {submission.name.charAt(0) || 'U'}
+ {submission.name?.charAt(0) || 'U'}
>
) : (
diff --git a/components/organization/hackathons/rewards/PodiumSection.tsx b/components/organization/hackathons/rewards/PodiumSection.tsx
index f51bdd33..b618fb9b 100644
--- a/components/organization/hackathons/rewards/PodiumSection.tsx
+++ b/components/organization/hackathons/rewards/PodiumSection.tsx
@@ -35,10 +35,10 @@ export default function PodiumSection({
const thirdPlace = winners.find(s => s.rank === 3);
return (
-
-
-
-
+
+ {secondPlace &&
}
+ {firstPlace &&
}
+ {thirdPlace &&
}
);
}
@@ -47,13 +47,16 @@ export default function PodiumSection({
- {Array.from({ length: maxRank }, (_, i) => i + 1).map(rank => {
- const winner = winners.find(s => s.rank === rank);
- return
;
- })}
+ {winners.map(winner => (
+
+ ))}
);
}
diff --git a/components/organization/hackathons/rewards/PreviewStep.tsx b/components/organization/hackathons/rewards/PreviewStep.tsx
index c999d3da..0cac8ebf 100644
--- a/components/organization/hackathons/rewards/PreviewStep.tsx
+++ b/components/organization/hackathons/rewards/PreviewStep.tsx
@@ -14,7 +14,11 @@ interface PreviewStepProps {
}>;
announcement: string;
onEditAnnouncement: () => void;
- getPrizeForRank: (rank: number) => string;
+ getPrizeForRank: (rank: number) => {
+ amount: string;
+ currency: string;
+ label: string;
+ };
}
export const PreviewStep: React.FC
= ({
@@ -25,11 +29,10 @@ export const PreviewStep: React.FC = ({
getPrizeForRank,
}) => {
return (
-
-
-
Preview
-
- Review winners and announcement before publishing
+
+
+
+ Review winners and announcement before triggering distribution.
diff --git a/components/organization/hackathons/rewards/PublishWinnersWizard.tsx b/components/organization/hackathons/rewards/PublishWinnersWizard.tsx
index 3dea661f..d364a6d6 100644
--- a/components/organization/hackathons/rewards/PublishWinnersWizard.tsx
+++ b/components/organization/hackathons/rewards/PublishWinnersWizard.tsx
@@ -13,9 +13,7 @@ import { Submission } from './types';
import { HackathonEscrowData } from '@/lib/api/hackathons';
import { PrizeTier } from '@/components/organization/hackathons/new/tabs/schemas/rewardsSchema';
import { useWizardSteps } from '@/hooks/use-wizard-steps';
-import { useWalletAddresses } from '@/hooks/use-wallet-addresses';
import { usePublishWinners } from '@/hooks/use-publish-winners';
-import { WalletsStep } from './WalletsStep';
import { AnnouncementStep } from './AnnouncementStep';
import { PreviewStep } from './PreviewStep';
import { WizardStepIndicator } from './WizardStepIndicator';
@@ -53,7 +51,6 @@ export default function PublishWinnersWizard({
);
const [announcement, setAnnouncement] = useState('');
- const [milestonesCreated, setMilestonesCreated] = useState(false);
const {
currentStep,
@@ -64,21 +61,13 @@ export default function PublishWinnersWizard({
handleBack,
} = useWizardSteps({ open, escrow });
- const { walletAddresses, handleWalletAddressChange } = useWalletAddresses({
- isOpen: open,
- winners,
- });
-
const { isPublishing, publishWinners } = usePublishWinners({
winners,
prizeTiers,
escrow,
organizationId,
hackathonId,
- walletAddresses,
announcement,
- milestonesCreated,
- setMilestonesCreated,
onSuccess: () => {
onOpenChange(false);
if (onSuccess) {
@@ -99,8 +88,8 @@ export default function PublishWinnersWizard({
const mappedPrizeTiers = useMemo(
() =>
- prizeTiers.map((tier, index) => ({
- rank: index + 1,
+ prizeTiers.map(tier => ({
+ rank: tier.rank,
prizeAmount: tier.prizeAmount,
currency: tier.currency,
})),
@@ -110,28 +99,29 @@ export default function PublishWinnersWizard({
const getPrizeForRank = (rank: number) => {
const tier = mappedPrizeTiers.find(t => t.rank === rank);
if (tier) {
- const amount = parseFloat(tier.prizeAmount).toLocaleString('en-US');
- return `${amount} ${tier.currency}`;
+ const amount = parseFloat(tier.prizeAmount || '0').toLocaleString(
+ 'en-US'
+ );
+ const currency = tier.currency || 'USDC';
+ return { amount, currency, label: `${amount} ${currency}` };
}
- return rank === 1
- ? '10,000 USDC'
- : rank === 2
- ? '5,000 USDC'
- : '8,000 USDC';
+ return { amount: '0', currency: 'USDC', label: 'No prize configured' };
};
return (