From dfb15f2676f9fea5ef9357606f31cea690977d1e Mon Sep 17 00:00:00 2001 From: wheval Date: Mon, 29 Sep 2025 10:53:34 +0100 Subject: [PATCH 1/2] feat: implement create project modal --- app/test/page.tsx | 83 +--- components/landing-page/navbar.tsx | 42 +- .../project/CreateProjectModal/Basic.tsx | 365 +++++++++++++++- .../project/CreateProjectModal/Contact.tsx | 395 +++++++++++++++++- .../project/CreateProjectModal/Details.tsx | 269 +++++++++++- .../project/CreateProjectModal/Footer.tsx | 10 +- .../project/CreateProjectModal/Milestones.tsx | 337 ++++++++++++++- .../project/CreateProjectModal/Team.tsx | 188 ++++++++- .../project/CreateProjectModal/index.tsx | 146 ++++++- 9 files changed, 1707 insertions(+), 128 deletions(-) diff --git a/app/test/page.tsx b/app/test/page.tsx index d66ac21a0..14e721bec 100644 --- a/app/test/page.tsx +++ b/app/test/page.tsx @@ -1,81 +1,4 @@ -'use client'; - -import { useState } from 'react'; -import LaunchCampaignFlow from '@/components/project/LaunchCampaignFlow'; -import BoundlessSheet from '@/components/sheet/boundless-sheet'; -import { Button } from '@/components/ui/button'; -import { Rocket } from 'lucide-react'; -import { useWalletProtection } from '@/hooks/use-wallet-protection'; -import WalletRequiredModal from '@/components/wallet/WalletRequiredModal'; - -export default function TestPage() { - const [showLaunchFlow, setShowLaunchFlow] = useState(false); - - // Wallet protection hook - const { - requireWallet, - showWalletModal, - handleWalletConnected, - closeWalletModal, - } = useWalletProtection({ - actionName: 'test launch campaign', - }); - - const handleOpenModal = () => { - requireWallet(() => setShowLaunchFlow(true)); - }; - - const handleCloseModal = () => { - setShowLaunchFlow(false); - }; - - return ( -
-
-

- Launch Campaign Test -

- -

- Click the button below to test the Launch Campaign feature -

- - - - {/* Debug info */} -
- Modal state: {showLaunchFlow ? 'Open' : 'Closed'} -
- - {/* Launch Campaign Flow Modal */} - - - - - {/* Wallet Required Modal */} - -
-
- ); +// Removed temporary test page. This file is intentionally empty. +export default function Page() { + return null; } diff --git a/components/landing-page/navbar.tsx b/components/landing-page/navbar.tsx index e67324ec2..2bfda6afa 100644 --- a/components/landing-page/navbar.tsx +++ b/components/landing-page/navbar.tsx @@ -199,9 +199,7 @@ export function Navbar() { ) : isAuthenticated ? ( ) : ( - - Get Started - + )} + Get Started + + ); + } + + return ( +
+ setCreateProjectModalOpen(true)} + className='border-white/20 text-white hover:bg-white/10' + > + + Add Project + + + Sign in + + +
+ ); +} + function MobileMenu({ isAuthenticated, isLoading, diff --git a/components/landing-page/project/CreateProjectModal/Basic.tsx b/components/landing-page/project/CreateProjectModal/Basic.tsx index 2dd154c24..18a82ea1c 100644 --- a/components/landing-page/project/CreateProjectModal/Basic.tsx +++ b/components/landing-page/project/CreateProjectModal/Basic.tsx @@ -1,7 +1,364 @@ -import React from 'react'; +'use client'; -const Basic = () => { - return
Basic
; -}; +import React, { useState } from 'react'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Label } from '@/components/ui/label'; +import { Camera, Info, Lightbulb } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface BasicProps { + onDataChange?: (data: BasicFormData) => void; + initialData?: Partial; +} + +export interface BasicFormData { + projectName: string; + logo: File | null; + vision: string; + category: string; + githubUrl: string; + websiteUrl: string; + demoVideoUrl: string; + socialLinks: string[]; +} + +const categories = [ + 'DeFi & Finance', + 'Gaming & Metaverse', + 'Social & Community', + 'Infrastructure & Tooling', + 'AI & Machine Learning', + 'Sustainability & Impact', + 'Other', +]; + +const Basic = React.forwardRef<{ validate: () => boolean }, BasicProps>( + ({ onDataChange, initialData }, ref) => { + const [formData, setFormData] = useState({ + projectName: initialData?.projectName || '', + logo: initialData?.logo || null, + vision: initialData?.vision || '', + category: initialData?.category || '', + githubUrl: initialData?.githubUrl || '', + websiteUrl: initialData?.websiteUrl || '', + demoVideoUrl: initialData?.demoVideoUrl || '', + socialLinks: initialData?.socialLinks || ['', '', ''], + }); + + const [errors, setErrors] = useState< + Partial> + >({}); + + const handleInputChange = ( + field: keyof BasicFormData, + value: string | File | string[] + ) => { + const newData = { ...formData, [field]: value }; + setFormData(newData); + + // Clear error when user starts typing + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: undefined })); + } + + onDataChange?.(newData); + }; + + const handleSocialLinkChange = (index: number, value: string) => { + const newSocialLinks = [...formData.socialLinks]; + newSocialLinks[index] = value; + handleInputChange('socialLinks', newSocialLinks); + }; + + const handleFileUpload = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + // Validate file type + if (!file.type.match(/^image\/(jpeg|png)$/)) { + setErrors(prev => ({ + ...prev, + logo: 'Only JPEG and PNG files are allowed', + })); + return; + } + + // Validate file size (2MB = 2 * 1024 * 1024 bytes) + if (file.size > 2 * 1024 * 1024) { + setErrors(prev => ({ + ...prev, + logo: 'File size must be less than 2MB', + })); + return; + } + + handleInputChange('logo', file); + } + }; + + const validateForm = (): boolean => { + const newErrors: Partial> = {}; + + if (!formData.projectName.trim()) { + newErrors.projectName = 'Project name is required'; + } + + if (!formData.logo) { + newErrors.logo = 'Logo is required'; + } + + if (!formData.vision.trim()) { + newErrors.vision = 'Vision is required'; + } else if (formData.vision.length > 300) { + newErrors.vision = 'Vision must be 300 characters or less'; + } + + if (!formData.category) { + newErrors.category = 'Category is required'; + } + + const validSocialLinks = formData.socialLinks.filter(link => link.trim()); + if (validSocialLinks.length === 0) { + newErrors.socialLinks = 'At least one social link is required'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + // Expose validation function to parent + React.useImperativeHandle(ref, () => ({ + validate: validateForm, + })); + + return ( +
+ {/* Project Name */} +
+ + handleInputChange('projectName', e.target.value)} + className={cn( + 'focus:border-primary border-[#484848] bg-[#1A1A1A] text-white placeholder:text-[#919191]', + errors.projectName && 'border-red-500' + )} + /> + {errors.projectName && ( +

{errors.projectName}

+ )} +
+ + {/* Logo/Image */} +
+ +
+ + +
+
+

+ Accepted file type:{' '} + JPEG or{' '} + PNG, and less + than 2 MB. +

+

+ A size of{' '} + 480 x 480 px is + recommended. +

+
+ {errors.logo &&

{errors.logo}

} +
+ + {/* Vision */} +
+
+ + + {formData.vision.length}/300 + +
+