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
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ const SubmissionCard = ({ submission }: SubmissionCardProps) => {
<img
src={logo}
alt={`${projectName} logo`}
className='absolute bottom-3 left-3 h-12 w-12 rounded-lg border-2 border-[#0D0E10] bg-[#0D0E10] object-cover shadow-lg'
className='absolute bottom-3 left-3 h-12 w-12 rounded-lg border-2 border-[#0D0E10] bg-[#0D0E10] object-contain shadow-lg'
/>
)}
</div>
Expand All @@ -130,12 +130,9 @@ const SubmissionCard = ({ submission }: SubmissionCardProps) => {

{/* Tags/Categories */}
<div className='flex flex-wrap gap-2 pt-2'>
<span className='text-primary rounded-md bg-[#232B20]/50 px-2.5 py-1 text-[10px] font-bold tracking-wider uppercase'>
{category}
</span>
{submission.category && (
<span className='rounded-md bg-white/5 px-2.5 py-1 text-[10px] font-bold tracking-wider text-gray-400 uppercase'>
{submission.category}
{category && (
<span className='text-primary rounded-md bg-[#232B20]/50 px-2.5 py-1 text-[10px] font-bold tracking-wider uppercase'>
{category}
</span>
)}
</div>
Expand Down
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
2 changes: 1 addition & 1 deletion components/landing-page/blog/BlogCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const BlogCard = ({ post, onCardClick }: BlogCardProps) => {
src={post.coverImage}
alt={post.title}
fill
className='object-cover transition-transform duration-500 group-hover:scale-105'
className='object-contain transition-transform duration-500 group-hover:scale-105'
sizes='(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw'
/>
{/* Gradient Overlay */}
Expand Down
10 changes: 5 additions & 5 deletions components/landing-page/blog/BlogPostDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ const BlogPostDetails: React.FC<BlogPostDetailsProps> = ({
src={post.coverImage}
alt={post.title}
fill
className='rounded-lg object-cover'
className='rounded-lg object-contain'
priority
sizes='(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 70vw'
/>
Expand Down Expand Up @@ -191,7 +191,7 @@ const BlogPostDetails: React.FC<BlogPostDetailsProps> = ({
>
<path
d='M1.5 16.5L7.79032 10.2097M16.5 1.5L10.2097 7.79032M10.2097 7.79032L5.66667 1.5H1.5L7.79032 10.2097M10.2097 7.79032L16.5 16.5H12.3333L7.79032 10.2097'
stroke='#99FF2D'
stroke='#2eedaa'
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
Expand All @@ -215,7 +215,7 @@ const BlogPostDetails: React.FC<BlogPostDetailsProps> = ({
>
<path
d='M6.68359 6.56555C6.3753 6.56555 6.08615 6.65168 5.83398 6.80286C5.86071 6.73663 5.8898 6.67102 5.91797 6.60461L6.68359 6.48743M6.68359 6.56555V6.48743M6.68359 6.56555L6.83008 6.57336C6.84126 6.53557 6.85246 6.49776 6.86426 6.46008L6.68359 6.48743M6.68359 6.56555V6.48743M13.3291 6.56555L13.5156 6.57629C13.75 6.60287 13.9698 6.68028 14.168 6.79602C14.1413 6.72999 14.1158 6.66346 14.0879 6.59778L13.3291 6.48352M13.3291 6.56555V6.48352M13.3291 6.56555V6.48352M13.3291 6.56555C13.2818 6.56555 13.2348 6.56939 13.1885 6.57336C13.1772 6.53503 13.1672 6.49633 13.1553 6.45813L13.3291 6.48352'
stroke='#99FF2D'
stroke='#2eedaa'
strokeWidth='10'
/>
</svg>
Expand All @@ -235,7 +235,7 @@ const BlogPostDetails: React.FC<BlogPostDetailsProps> = ({
>
<path
d='M9.98721 11.8403L12.6883 14.9113C13.6891 16.0491 14.1895 16.618 14.7133 16.4795C15.2371 16.341 15.4167 15.5923 15.7759 14.0949L17.7684 5.78825C18.3217 3.48194 18.5983 2.32878 17.9834 1.76C17.3685 1.19122 16.3027 1.61437 14.1711 2.46068L4.28165 6.38707C2.57679 7.06395 1.72436 7.40239 1.67024 7.98403C1.6647 8.04353 1.66461 8.10344 1.66996 8.16295C1.7223 8.74477 2.57369 9.08605 4.27647 9.7686C5.048 10.0779 5.43377 10.2325 5.71035 10.5286C5.74145 10.5619 5.77135 10.5964 5.8 10.632C6.05484 10.9486 6.16359 11.3642 6.38109 12.1954L6.78812 13.7508C6.99977 14.5596 7.10559 14.964 7.38275 15.0191C7.65991 15.0743 7.90122 14.7389 8.38384 14.0683L9.98721 11.8403ZM9.98721 11.8403L9.72237 11.5642C9.42097 11.2501 9.27026 11.0931 9.27026 10.8979C9.27026 10.7027 9.42097 10.5457 9.72237 10.2315L12.6998 7.12841'
stroke='#99FF2D'
stroke='#2eedaa'
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
Expand All @@ -259,7 +259,7 @@ const BlogPostDetails: React.FC<BlogPostDetailsProps> = ({
>
<path
d='M7.49935 14.1666H5.83268C3.5315 14.1666 1.66602 12.3012 1.66602 9.99998C1.66602 7.69879 3.5315 5.83331 5.83268 5.83331H7.49935M12.4993 14.1666H14.166C16.4672 14.1666 18.3327 12.3012 18.3327 9.99998C18.3327 7.69879 16.4672 5.83331 14.166 5.83331H12.4993M5.83268 9.99998L14.166 9.99998'
stroke='#99FF2D'
stroke='#2eedaa'
strokeWidth='1.4'
strokeLinecap='round'
strokeLinejoin='round'
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
Loading
Loading