diff --git a/app/globals.css b/app/globals.css index 791f0afbd..bf86263c2 100644 --- a/app/globals.css +++ b/app/globals.css @@ -6,6 +6,11 @@ @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); + --color-stepper-text-inactive: var(--stepper-text-inactive); + --color-stepper-foreground: var(--stepper-foreground); + --color-stepper-border-active: var(--stepper-border-active); + --color-stepper-border: var(--stepper-border); + --color-placeholder: var(--placeholder); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); --color-sidebar-ring: var(--sidebar-ring); @@ -51,7 +56,7 @@ --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); - --primary: #a7f950; + --primary: oklch(0.95 0.24 125); --primary-foreground: #020502; --secondary: oklch(0.97 0 0); --secondary-foreground: oklch(0.205 0 0); @@ -69,6 +74,11 @@ --chart-4: oklch(0.828 0.189 84.429); --chart-5: oklch(0.769 0.188 70.08); --sidebar: oklch(0.985 0 0); + --stepper-foreground: oklch(0.2046 0 0); + --stepper-border: oklch(0.2264 0 0); + --stepper-border-active: oklch(0.3501 0.0381 190.35); + --stepper-text-inactive: oklch(0.7731 0 0); + --placeholder: oklch(0.6567 0 0); --sidebar-foreground: oklch(0.145 0 0); --sidebar-primary: oklch(0.205 0 0); --sidebar-primary-foreground: oklch(0.985 0 0); diff --git a/components/layout/header.tsx b/components/layout/header.tsx index 992fb7c69..ed23b1b7b 100644 --- a/components/layout/header.tsx +++ b/components/layout/header.tsx @@ -10,9 +10,70 @@ import BoundlessSheet from '../sheet/boundless-sheet'; import { motion } from 'framer-motion'; import { fadeInUp, slideInFromLeft, slideInFromRight } from '@/lib/motion'; import WalletConnectButton from '../wallet/WalletConnectButton'; +import ProjectSubmissionForm from '../project/ProjectSubmissionForm'; +import ProjectSubmissionLoading from '../project/ProjectSubmissionLoading'; +import ProjectSubmissionSuccess from '../project/ProjectSubmissionSuccess'; +import { Stepper } from '../stepper'; + +type Step = { + title: string; + description: string; + state: 'pending' | 'active' | 'completed'; +}; + +const initialSteps: Step[] = [ + { + title: 'Initialize', + description: 'Submit your project idea to kickstart your campaign journey.', + state: 'active', + }, + { + title: 'Project Details', + description: 'Provide detailed information about your project.', + state: 'pending', + }, + { + title: 'Review & Submit', + description: 'Review your submission and finalize your entry.', + state: 'pending', + }, +]; const Header = () => { const [open, setOpen] = useState(false); + const [submissionStatus, setSubmissionStatus] = useState('idle'); + const [steps, setSteps] = useState(initialSteps); + + const handleSuccess = () => { + setSubmissionStatus('success'); + setSteps(prevSteps => + prevSteps.map((step, index) => { + if (index === 0) { + return { ...step, state: 'completed' }; + } + if (index === 1) { + return { ...step, state: 'active' }; + } + return step; + }) + ); + }; + + const renderContent = () => { + switch (submissionStatus) { + case 'submitting': + return ; + case 'success': + return ; + default: + return ( + + ); + } + }; return ( { - + +
+ +
{renderContent()}
+
+
); }; diff --git a/components/project/ProjectSubmissionFlow.tsx b/components/project/ProjectSubmissionFlow.tsx new file mode 100644 index 000000000..7737c8794 --- /dev/null +++ b/components/project/ProjectSubmissionFlow.tsx @@ -0,0 +1,89 @@ +'use client'; + +import React, { useState } from 'react'; +import BoundlessSheet from '../sheet/boundless-sheet'; +import Stepper from '../stepper/Stepper'; +import ProjectSubmissionForm from './ProjectSubmissionForm'; +import ProjectSubmissionSuccess from './ProjectSubmissionSuccess'; +import ProjectSubmissionLoading from './ProjectSubmissionLoading'; + +type StepState = 'active' | 'pending' | 'completed'; + +type Step = { + title: string; + description: string; + state: StepState; +}; + +const steps: Step[] = [ + { + title: 'Initialize', + description: 'Submit your project idea to kickstart your campaign journey.', + state: 'active', + }, + { + title: 'Validate', + description: 'Get admin approval and gather public support through voting.', + state: 'pending', + }, + { + title: 'Launch Campaign', + description: 'Set milestones, activate escrow, and start receiving funds.', + state: 'pending', + }, +]; + +function ProjectSubmissionFlow() { + const [open, setOpen] = useState(false); + const [stepperState, setStepperState] = useState(steps); + const [submissionStatus, setSubmissionStatus] = useState('idle'); + + const handleSuccess = () => { + setSubmissionStatus('success'); + const newSteps = stepperState.map((step, index) => { + if (index === 0) { + return { ...step, state: 'completed' as const }; + } + if (index === 1) { + return { ...step, state: 'active' as const }; + } + return step; + }); + setStepperState(newSteps); + }; + + const renderContent = () => { + switch (submissionStatus) { + case 'submitting': + return ; + case 'success': + return ; + default: + return ( + + ); + } + }; + + return ( +
+ + +
+ +
{renderContent()}
+
+
+
+ ); +} + +export default ProjectSubmissionFlow; diff --git a/components/project/ProjectSubmissionForm.tsx b/components/project/ProjectSubmissionForm.tsx new file mode 100644 index 000000000..035ebab45 --- /dev/null +++ b/components/project/ProjectSubmissionForm.tsx @@ -0,0 +1,474 @@ +'use client'; + +import Image from 'next/image'; +import React, { useState, useRef, useEffect } from 'react'; +import { Textarea } from '../ui/textarea'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '../ui/select'; +import { formatBytes } from '@/lib/utils'; +import { Badge } from '../ui/badge'; +import { Trash, X } from 'lucide-react'; +import { toast } from 'sonner'; + +// Dummy data for the Select components +const fundingGoals = [ + { value: 'seed', label: 'Seed Round' }, + { value: 'series-a', label: 'Series A' }, + { value: 'public-sale', label: 'Public Sale' }, + { value: 'community-pool', label: 'Community Pool' }, +]; + +const projectTags = [ + { value: 'defi', label: 'DeFi' }, + { value: 'gaming', label: 'Gaming' }, + { value: 'nft', label: 'NFT' }, + { value: 'infrastructure', label: 'Infrastructure' }, + { value: 'public-good', label: 'Public Good' }, +]; + +interface ProjectSubmissionFormProps { + onSuccess: () => void; + setSubmissionStatus: (status: 'idle' | 'submitting' | 'success') => void; +} + +function ProjectSubmissionForm({ + onSuccess, + setSubmissionStatus, +}: ProjectSubmissionFormProps) { + // State for all form fields + const [projectTitle, setProjectTitle] = useState(''); + const [projectTagline, setProjectTagline] = useState(''); + const [projectDescription, setProjectDescription] = useState(''); + const [fundingGoal, setFundingGoal] = useState(''); + const [fundAmount, setFundAmount] = useState(''); + const [whitepaperFile, setWhitepaperFile] = useState(null); + const [thumbnailFile, setThumbnailFile] = useState(null); + const [tags, setTags] = useState([]); + const [isFormValid, setIsFormValid] = useState(false); + + // Refs for file inputs + const whitepaperInputRef = useRef(null); + const thumbnailInputRef = useRef(null); + + // Check form validity + useEffect(() => { + const allFieldsFilled = + projectTitle.trim() !== '' && + projectTagline.trim() !== '' && + projectDescription.trim() !== '' && + fundingGoal.trim() !== '' && + fundAmount.trim() !== '' && + whitepaperFile !== null && + whitepaperFile.length > 0 && + thumbnailFile !== null && + thumbnailFile.length > 0; + setIsFormValid(allFieldsFilled); + }, [ + projectTitle, + projectTagline, + projectDescription, + fundingGoal, + fundAmount, + whitepaperFile, + thumbnailFile, + ]); + + // Tag handlers + const handleAddTag = (tag: string) => { + if (tag && !tags.includes(tag)) { + setTags([...tags, tag]); + } + }; + + const handleRemoveTag = (tagToRemove: string) => { + setTags(tags.filter(tag => tag !== tagToRemove)); + }; + + // File removal handler + const handleRemoveFile = (fileType: 'whitepaper' | 'thumbnail') => { + if (fileType === 'whitepaper') { + setWhitepaperFile(null); + if (whitepaperInputRef.current) { + whitepaperInputRef.current.value = ''; + } + } else if (fileType === 'thumbnail') { + setThumbnailFile(null); + if (thumbnailInputRef.current) { + thumbnailInputRef.current.value = ''; + } + } + }; + + // Form submission handler + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!isFormValid) return; + + setSubmissionStatus('submitting'); + toast.loading('Submitting your project...'); + + const formData = new FormData(); + formData.append('name', projectTitle); + formData.append('tagline', projectTagline); + formData.append('description', projectDescription); + formData.append('funding_goal', fundingGoal); + formData.append('fund_amount', fundAmount); + formData.append('tags', JSON.stringify(tags)); + if (whitepaperFile) { + formData.append('whitepaper', whitepaperFile[0]); + } + if (thumbnailFile) { + formData.append('thumbnail', thumbnailFile[0]); + } + + try { + const response = await fetch( + 'https://www.api.boundlessfi.xyz/api/projects', + { + method: 'POST', + body: formData, + } + ); + + if (!response.ok) { + throw new Error('Project submission failed'); + } + + toast.dismiss(); + toast.success('Project submitted successfully!'); + onSuccess(); + } catch (error) { + console.error('Submission error:', error); + toast.dismiss(); + toast.error('Failed to submit project. Please try again.'); + setSubmissionStatus('idle'); + } + }; + + return ( +
+
Submit your project information
+
+ {/* Project Title */} +
+ +
+ icon + setProjectTitle(e.target.value)} + type='text' + className='w-full bg-transparent font-normal text-base text-placeholder focus:outline-none' + placeholder='Enter project title' + /> +
+
+ + {/* Project Tagline */} +
+ +
+ setProjectTagline(e.target.value)} + type='text' + className='w-full bg-transparent font-normal text-base text-placeholder focus:outline-none' + placeholder='Enter your one-liner' + /> +
+
+ + {/* Project Description */} +
+ +