Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/(landing)/hackathons/[slug]/submit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export default function SubmitProjectPage({
category: mySubmission.category,
description: mySubmission.description,
logo: mySubmission.logo,
banner: mySubmission.banner,
videoUrl: mySubmission.videoUrl,
introduction: mySubmission.introduction,
links: mySubmission.links,
Expand Down
87 changes: 76 additions & 11 deletions components/hackathons/submissions/SubmissionForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ const baseSubmissionSchema = z.object({
videoUrl: z
.union([z.string().url('Please enter a valid URL'), z.literal('')])
.optional(),
introduction: z.string().optional(),
introduction: z
.string()
.max(500, 'Introduction cannot exceed 500 characters')
.optional(),
links: z.array(
z.object({
type: z.string(),
Expand Down Expand Up @@ -155,12 +158,18 @@ const INITIAL_STEPS: Step[] = [
const LINK_TYPES = [
{ value: 'github', label: 'GitHub' },
{ value: 'demo', label: 'Demo' },
{ value: 'website', label: 'Website' },
{ value: 'documentation', label: 'Documentation' },
{ value: 'video', label: 'Video' },
{ value: 'document', label: 'Document' },
{ value: 'presentation', label: 'Presentation' },
{ value: 'other', label: 'Other' },
];

const OTHER_LINK_TYPES = ['demo', 'website', 'documentation', 'other'];
const OTHER_LINK_TYPES = ['demo', 'video', 'document', 'presentation', 'other'];

const MAX_OTHER_LINKS = 5;
const FIXED_LINK_TYPES = LINK_TYPES.map(t => t.value).filter(
v => v !== 'other'
);

const isValidUrl = (url: string | undefined): boolean => {
if (!url || String(url).trim() === '') return false;
Expand Down Expand Up @@ -517,6 +526,9 @@ const SubmissionFormContent: React.FC<SubmissionFormContentProps> = ({
};

const handleFillMockData = () => {
const participationType: 'INDIVIDUAL' | 'TEAM' = myTeam
? 'TEAM'
: 'INDIVIDUAL';
const mockData = {
projectName: 'AI-Powered Task Manager',
category: categoryOptions[0],
Expand All @@ -529,7 +541,7 @@ const SubmissionFormContent: React.FC<SubmissionFormContentProps> = ({
{ type: 'github', url: 'https://github.com/example/ai-task-manager' },
{ type: 'demo', url: 'https://demo.example.com/ai-task-manager' },
],
participationType: 'INDIVIDUAL' as const,
participationType,
};

form.reset(mockData);
Expand All @@ -539,7 +551,27 @@ const SubmissionFormContent: React.FC<SubmissionFormContentProps> = ({

const handleAddLink = () => {
const currentLinks = form.getValues('links') || [];
form.setValue('links', [...currentLinks, { type: 'github', url: '' }], {
const usedFixedTypes = new Set(
currentLinks.map(l => l.type).filter(t => t !== 'other')
);
const otherCount = currentLinks.filter(l => l.type === 'other').length;

const firstUnusedFixed = FIXED_LINK_TYPES.find(t => !usedFixedTypes.has(t));

let nextType: string;
if (firstUnusedFixed) {
nextType = firstUnusedFixed;
} else if (otherCount < MAX_OTHER_LINKS) {
nextType = 'other';
} else {
toast.error('Cannot add another link', {
description: `All fixed link types are used and you have reached the limit of ${MAX_OTHER_LINKS} "Other" links.`,
duration: 6000,
});
return;
}

form.setValue('links', [...currentLinks, { type: nextType, url: '' }], {
shouldValidate: false,
});
};
Expand Down Expand Up @@ -582,6 +614,33 @@ const SubmissionFormContent: React.FC<SubmissionFormContentProps> = ({
value: string
) => {
const currentLinks = form.getValues('links') || [];

if (field === 'type') {
if (value !== 'other') {
const isDuplicate = currentLinks.some(
(l, i) => i !== index && l.type === value
);
if (isDuplicate) {
toast.error('Duplicate link type', {
description: `"${value}" is already used. Each link type can be used at most once. Choose "Other" for additional links.`,
duration: 6000,
});
return;
}
} else {
const otherCount = currentLinks.filter(
(l, i) => i !== index && l.type === 'other'
).length;
if (otherCount >= MAX_OTHER_LINKS) {
toast.error('Too many "Other" links', {
description: `You can include at most ${MAX_OTHER_LINKS} "Other" links per submission.`,
duration: 6000,
});
return;
}
}
}

const updatedLinks = [...currentLinks];
updatedLinks[index] = { ...updatedLinks[index], [field]: value };
form.setValue('links', updatedLinks, { shouldValidate: true });
Expand Down Expand Up @@ -643,7 +702,7 @@ const SubmissionFormContent: React.FC<SubmissionFormContentProps> = ({
if (requireOtherLinks && !hasValidOtherLink) {
form.setError('links', {
message:
'At least one additional link (Demo, Website, Documentation, or Other) is required for this hackathon.',
'At least one additional link (Demo, Video, Document, Presentation, or Other) is required for this hackathon.',
});
return;
}
Expand Down Expand Up @@ -792,7 +851,7 @@ const SubmissionFormContent: React.FC<SubmissionFormContentProps> = ({
if (requireOtherLinks && !hasValidOtherLink) {
form.setError('links', {
message:
'At least one additional link (Demo, Website, Documentation, or Other) is required for this hackathon.',
'At least one additional link (Demo, Video, Document, Presentation, or Other) is required for this hackathon.',
});
setCurrentStep(2);
updateStepState(2, 'active');
Expand Down Expand Up @@ -1346,12 +1405,13 @@ const SubmissionFormContent: React.FC<SubmissionFormContentProps> = ({
<FormControl>
<Textarea
placeholder='Tell us more about your project...'
maxLength={500}
className='min-h-[100px] border-gray-700 bg-gray-800/50 text-white placeholder:text-gray-500'
{...field}
/>
</FormControl>
<FormDescription className='text-gray-400'>
Optional: Additional information about your project
Optional. {field.value?.length || 0} / 500 characters max
</FormDescription>
<FormMessage />
</FormItem>
Expand Down Expand Up @@ -1386,12 +1446,17 @@ const SubmissionFormContent: React.FC<SubmissionFormContentProps> = ({
{(requireGithub || requireOtherLinks) && (
<FormDescription className='text-gray-400'>
{requireGithub && requireOtherLinks
? 'GitHub repository link and at least one additional link (Demo, Website, Documentation, or Other) are required for this hackathon.'
? 'GitHub repository link and at least one additional link (Demo, Video, Document, Presentation, or Other) are required for this hackathon.'
: requireGithub
? 'GitHub repository link is required for this hackathon.'
: 'At least one additional link (Demo, Website, Documentation, or Other) is required for this hackathon.'}
: 'At least one additional link (Demo, Video, Document, Presentation, or Other) is required for this hackathon.'}
</FormDescription>
)}
<FormDescription className='text-gray-400'>
Each link type can be used at most once. For additional
links, choose &quot;Other&quot; (up to {MAX_OTHER_LINKS}{' '}
allowed).
</FormDescription>
{formLinks.length === 0 ? (
<p className='text-sm text-gray-400'>
No links added. Click "Add Link" to add project links.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
'use client';

import React from 'react';
import { ArrowUpRight, Github, Twitter, Globe, Link2 } from 'lucide-react';
import {
ArrowUpRight,
Github,
Twitter,
Globe,
Link2,
Video,
FileText,
Presentation,
Play,
} from 'lucide-react';
import { ScrollArea } from '@/components/ui/scroll-area';
import { motion } from 'motion/react';

Expand All @@ -15,6 +25,14 @@ const getIcon = (type: string) => {
return <Github className='h-5 w-5' />;
case 'twitter':
return <Twitter className='h-5 w-5' />;
case 'demo':
return <Play className='h-5 w-5' />;
case 'video':
return <Video className='h-5 w-5' />;
case 'document':
return <FileText className='h-5 w-5' />;
case 'presentation':
return <Presentation className='h-5 w-5' />;
case 'website':
return <Globe className='h-5 w-5' />;
default:
Expand Down
15 changes: 12 additions & 3 deletions hooks/hackathon/use-submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ export function useSubmission({
'Failed to create submission'
);
setError(errorMessage);
toast.error(errorMessage);
toast.error('Submission failed', {
description: errorMessage,
duration: 8000,
});
reportError(err, {
context: 'hackathon-createSubmission',
hackathonSlugOrId,
Expand Down Expand Up @@ -180,7 +183,10 @@ export function useSubmission({
'Failed to update submission'
);
setError(errorMessage);
toast.error(errorMessage);
toast.error('Update failed', {
description: errorMessage,
duration: 8000,
});
reportError(err, {
context: 'hackathon-updateSubmission',
submissionId,
Expand Down Expand Up @@ -214,7 +220,10 @@ export function useSubmission({
'Failed to delete submission'
);
setError(errorMessage);
toast.error(errorMessage);
toast.error('Delete failed', {
description: errorMessage,
duration: 8000,
});
throw err;
} finally {
setIsSubmitting(false);
Expand Down
1 change: 1 addition & 0 deletions lib/api/hackathons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,7 @@ export interface ParticipantSubmission {
category: string;
description: string;
logo?: string;
banner?: string;
videoUrl?: string;
introduction?: string;
links?: Array<{ type: string; url: string }>;
Expand Down
Loading