"use client" import type React from "react" import { useState, useEffect, useRef } from "react" import { useSearchParams } from "next/navigation" import Link from "next/link" import { FileText, Upload, CheckCircle, Loader2, ArrowLeft, ShieldAlert, ShieldCheck, AlertTriangle, Info, RefreshCw } from "lucide-react" import { CoinsStacked } from "@/icons" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { useWalletStore } from "@/lib/wallet-store" import { Badge } from "@/components/ui/badge" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { ethers } from "ethers" import { usePrivadoId } from "@/lib/privado-id" import { PERSPECTIVE_CONTRACT_ABI, PERSPECTIVE_CONTRACT_ADDRESS, FEE_CONTRACT_ADDRESS, FEE_CONTRACT_ABI } from "@/lib/constants" import { useTrustData } from "@/hooks/use-trust-data" // Mock issues data for the dropdown const mockIssues = [ { id: "climate-action", title: "Climate Action Policies", category: "Environmental Policy" }, { id: "education-reform", title: "Education System Reform", category: "Education" }, { id: "healthcare-access", title: "Healthcare Accessibility", category: "Healthcare" }, { id: "housing-affordability", title: "Housing Affordability Crisis", category: "Infrastructure" }, { id: "digital-privacy", title: "Digital Privacy Regulations", category: "Technology" } ] // Generate a simple CAPTCHA const generateCaptcha = () => { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'; let captcha = ''; for (let i = 0; i < 6; i++) { captcha += chars.charAt(Math.floor(Math.random() * chars.length)); } return captcha; }; export default function SubmitPage() { const { walletConnected, walletAddress, isVerified, citizenshipVerified, eligibilityVerified, verificationStatus, perspectivesSubmittedToday: perspectivesSubmitted, incrementPerspectiveCount } = useWalletStore() const { trustScore, dailyLimit, isLoading, updateUserTrust } = useTrustData() const searchParams = useSearchParams() const issueParam = searchParams.get('issue') const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitted, setIsSubmitted] = useState(false) const [ipfsHash, setIpfsHash] = useState("") const [selectedIssue, setSelectedIssue] = useState<string | null>(null) const [title, setTitle] = useState("") const [content, setContent] = useState("") const [captchaInput, setCaptchaInput] = useState("") const [captchaValue, setCaptchaValue] = useState("") const [captchaVerified, setCaptchaVerified] = useState(false) const [selectedFile, setSelectedFile] = useState<File | null>(null) const [fileError, setFileError] = useState<string>("") const fileInputRef = useRef<HTMLInputElement>(null) const [submissionError, setSubmissionError] = useState<string | null>(null) const [uploadingFile, setUploadingFile] = useState(false) const { verifyIdentity } = usePrivadoId() // Economic barrier states const [showFeeModal, setShowFeeModal] = useState(false) const [transactionState, setTransactionState] = useState<'idle' | 'pending' | 'confirmed' | 'error'>('idle') const [transactionHash, setTransactionHash] = useState<string | null>(null) const [contentToSubmit, setContentToSubmit] = useState<{ issueId: string; text: string; fileUrl?: string; } | null>(null) // Fee amount in POL const FEE_AMOUNT = "0.01" // Check rate limit for unverified users const isRateLimited = !isVerified && perspectivesSubmitted >= 5 // Set the selected issue from URL parameter if available useEffect(() => { if (issueParam) { setSelectedIssue(issueParam) } }, [issueParam]) // Generate CAPTCHA on load useEffect(() => { if (!isVerified) { refreshCaptcha(); } }, [isVerified]); // Get the selected issue details const selectedIssueDetails = selectedIssue ? mockIssues.find(issue => issue.id === selectedIssue) : null const refreshCaptcha = () => { setCaptchaValue(generateCaptcha()); setCaptchaInput(""); setCaptchaVerified(false); }; const validateCaptcha = () => { const isValid = captchaInput.trim() === captchaValue; setCaptchaVerified(isValid); return isValid; }; // Add file validation function const validateFile = (file: File): boolean => { // Reset error message setFileError(""); // Check file type const validTypes = ['application/pdf', 'image/jpeg', 'image/png']; if (!validTypes.includes(file.type)) { setFileError("Invalid file type. Please upload a PDF, JPEG, or PNG file."); return false; } // Check file size (5MB limit) const maxSize = 5 * 1024 * 1024; // 5MB in bytes if (file.size > maxSize) { setFileError("File is too large. Maximum size is 5MB."); return false; } return true; } // Add file change handler const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0] if (file) { if (validateFile(file)) { setSelectedFile(file) } else { e.target.value = '' // Reset input setSelectedFile(null) } } } // Add smart contract submission function const submitToBlockchain = async ( issueId: string, text: string, fileUrl?: string ) => { try { const provider = new ethers.providers.Web3Provider( window.ethereum as ethers.providers.ExternalProvider || undefined ); const signer = provider.getSigner() const contract = new ethers.Contract( PERSPECTIVE_CONTRACT_ADDRESS, PERSPECTIVE_CONTRACT_ABI, signer ) // Prepare metadata including verification status const metadata = { isVerified, timestamp: Date.now(), fileUrl, submitterAddress: await signer.getAddress() } // Submit perspective to smart contract const tx = await contract.submitPerspective( issueId, text, JSON.stringify(metadata), { gasLimit: 500000 } ) // Wait for transaction confirmation await tx.wait() return tx.hash } catch (error) { console.error("Blockchain submission error:", error) throw error } } // Update file upload handler with additional CAPTCHA check const handleFileUpload = async (file: File): Promise<string> => { if (!isVerified && !captchaVerified) { setFileError("Please complete CAPTCHA verification first"); document.getElementById('captcha-section')?.scrollIntoView({ behavior: 'smooth' }); return ""; } setUploadingFile(true); try { // TODO: Replace with actual IPFS upload await new Promise(resolve => setTimeout(resolve, 1000)); const mockIpfsUrl = `ipfs://Qm...${file.name}`; return mockIpfsUrl; } catch (error) { console.error("File upload error:", error); setFileError("Failed to upload file. Please try again."); return ""; } finally { setUploadingFile(false); } }; // Handle fee payment and submission const handleFeePayment = async () => { if (!walletConnected || !contentToSubmit) return; setTransactionState('pending'); try { const provider = new ethers.providers.Web3Provider( window.ethereum as ethers.providers.ExternalProvider || undefined ); const signer = provider.getSigner(); const contract = new ethers.Contract( FEE_CONTRACT_ADDRESS, FEE_CONTRACT_ABI, signer ); // Convert the fee amount to wei const feeInWei = ethers.utils.parseEther(FEE_AMOUNT); // Call the payFeeAndSubmit function from our smart contract const tx = await contract.payFeeAndSubmit( contentToSubmit.issueId, contentToSubmit.text, contentToSubmit.fileUrl || "", isVerified, { value: feeInWei, gasLimit: 500000 } ); // Wait for transaction confirmation await tx.wait(); setTransactionHash(tx.hash); setTransactionState('confirmed'); // Update user trust score if unverified if (!isVerified) { try { await updateUserTrust('submit_perspective'); } catch (trustError) { console.error("Failed to update trust score:", trustError); } } setIpfsHash(tx.hash); setIsSubmitted(true); incrementPerspectiveCount(); setShowFeeModal(false); } catch (error: any) { console.error("Fee payment failed:", error); setTransactionState('error'); setSubmissionError(error.message || "Failed to process fee payment. Please try again."); } }; // Update submit handler to check for verification const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setSubmissionError(null); if (!walletConnected || !selectedIssue) return; // Verify identity status if needed if (!isVerified) { if (!validateCaptcha()) { setSubmissionError("CAPTCHA validation failed. Please try again."); refreshCaptcha(); return; } if (isRateLimited) { setSubmissionError("Daily submission limit reached. Please verify your identity for unlimited access."); return; } } setIsSubmitting(true); try { // Handle file upload if present let fileUrl = ""; if (selectedFile) { fileUrl = await handleFileUpload(selectedFile); if (!fileUrl) { setIsSubmitting(false); return; // Upload failed } } // For unverified users, prompt for fee payment if (!isVerified) { setContentToSubmit({ issueId: selectedIssue, text: content, fileUrl: fileUrl || undefined }); setShowFeeModal(true); setIsSubmitting(false); return; } // For verified users, proceed with normal submission const txHash = await submitToBlockchain(selectedIssue, content, fileUrl || undefined); setIpfsHash(txHash); setIsSubmitted(true); incrementPerspectiveCount(); } catch (error: any) { console.error("Submission failed:", error); setSubmissionError(error.message || "Failed to submit perspective. Please try again."); } finally { setIsSubmitting(false); } }; // Render verification banner based on status const renderVerificationBanner = () => { if (!isVerified) { return ( <Alert variant="default" className="mb-6 border-amber-300 bg-amber-50 dark:bg-amber-950/30 dark:border-amber-800"> <AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400" /> <AlertTitle className="text-amber-800 dark:text-amber-400">Limited Functionality</AlertTitle> <AlertDescription className="text-amber-700 dark:text-amber-500"> As an unverified user, you're in read-only mode with limited actions. You can submit up to {dailyLimit} perspectives per day. <Button variant="outline" size="sm" className="mt-2 bg-amber-600 text-white hover:bg-amber-700 border-amber-700" asChild > <Link href="/profile/verify">Verify Identity</Link> </Button> </AlertDescription> </Alert> ); } else if (citizenshipVerified && !eligibilityVerified) { return ( <Alert variant="default" className="mb-6 border-amber-300 bg-amber-50 dark:bg-amber-950/30 dark:border-amber-800"> <ShieldCheck className="h-4 w-4 text-amber-600 dark:text-amber-400" /> <AlertTitle className="text-amber-800 dark:text-amber-400">Citizenship Verified</AlertTitle> <AlertDescription className="text-amber-700 dark:text-amber-500"> Your citizenship is confirmed. You can submit up to 20 perspectives per day. Complete your eligibility attestation to unlock unlimited submissions and full voting power. <Button variant="outline" size="sm" className="mt-2 bg-amber-600 text-white hover:bg-amber-700 border-amber-700" asChild > <Link href="/profile/verify">Complete Verification</Link> </Button> </AlertDescription> </Alert> ); } else if (citizenshipVerified && eligibilityVerified) { return ( <Alert variant="default" className="mb-6 border-green-300 bg-green-50 dark:bg-green-950/30 dark:border-green-800"> <ShieldCheck className="h-4 w-4 text-green-600 dark:text-green-400" /> <AlertTitle className="text-green-800 dark:text-green-400">Fully Verified</AlertTitle> <AlertDescription className="text-green-700 dark:text-green-500"> You're fully verified with unlimited perspective submissions and enhanced influence on outcomes. </AlertDescription> </Alert> ); } return null; }; if (!walletConnected) { return ( <div className="container mx-auto px-4 py-8"> <div className="mx-auto max-w-3xl text-center"> <h1 className="text-3xl font-bold tracking-tight sm:text-4xl">Connect Your Wallet</h1> <p className="mt-4 text-lg text-muted-foreground"> Please connect your wallet to submit a perspective. </p> </div> </div> ) } if (isSubmitted) { return ( <div className="container mx-auto px-4 py-8"> <div className="mx-auto max-w-3xl text-center"> <div className="mb-4 flex justify-center"> <CheckCircle className="h-12 w-12 text-green-500 dark:text-green-400" /> </div> <h1 className="text-3xl font-bold tracking-tight sm:text-4xl">Perspective Submitted!</h1> <p className="mt-4 text-lg text-muted-foreground"> Your perspective has been successfully submitted and stored on IPFS. </p> <p className="mt-2 text-sm text-muted-foreground"> IPFS Hash: {ipfsHash} </p> {!isVerified && ( <p className="mt-4 text-sm bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300 border border-blue-200 dark:border-blue-800 rounded-md p-3"> <Info className="h-4 w-4 inline-block mr-2" /> Your fee of {FEE_AMOUNT} POL is refundable if your submission remains unflagged for 24 hours. </p> )} <div className="mt-8 flex flex-col sm:flex-row justify-center gap-4"> <Button variant="outline" onClick={() => { setIsSubmitted(false) setTitle("") setContent("") if (!isVerified) { refreshCaptcha(); } }} > Submit Another Perspective </Button> {selectedIssue && ( <Button asChild> <Link href={`/issues/${selectedIssue}`}> View Issue </Link> </Button> )} </div> </div> </div> ) } return ( <div className="container mx-auto px-4 py-10 max-w-4xl"> <div className="mx-auto max-w-3xl"> <Button variant="ghost" size="sm" asChild className="mb-4"> <Link href="/issues"> <ArrowLeft className="mr-2 h-4 w-4" /> Back to Issues </Link> </Button> {renderVerificationBanner()} {/* Fee payment modal */} <Dialog open={showFeeModal} onOpenChange={setShowFeeModal}> <DialogContent className="sm:max-w-md"> <DialogHeader> <DialogTitle className="flex items-center"> <CoinsStacked className="h-5 w-5 mr-2 text-blue-500" /> Fee Required for Unverified Users </DialogTitle> <DialogDescription> This action requires a {FEE_AMOUNT} POL fee, which is refundable if your submission is not flagged within 24 hours. </DialogDescription> </DialogHeader> <div className="my-6"> <div className="rounded-md p-4 bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300 border border-blue-200 dark:border-blue-800"> <p className="text-sm"> <Info className="h-4 w-4 inline-block mr-2" /> This small fee helps prevent spam and abuse. The fee will be automatically refunded after 24 hours if your submission isn't flagged for moderation. </p> </div> {transactionState === 'pending' && ( <div className="flex flex-col items-center justify-center mt-4 p-4 bg-muted/20 rounded-md"> <Loader2 className="h-8 w-8 animate-spin text-primary mb-2" /> <p className="text-sm font-medium">Processing Transaction...</p> <p className="text-xs text-muted-foreground mt-1">Please confirm in your wallet and wait for confirmation</p> </div> )} {transactionState === 'error' && ( <div className="mt-4 p-4 bg-red-50 dark:bg-red-950/50 text-red-700 dark:text-red-400 rounded-md border border-red-200 dark:border-red-900"> <AlertTriangle className="h-4 w-4 inline-block mr-2" /> <span className="font-medium">Transaction Error</span> <p className="text-sm mt-1">{submissionError}</p> </div> )} </div> <DialogFooter className="gap-2 sm:justify-between sm:gap-0"> <Button variant="outline" onClick={() => setShowFeeModal(false)} disabled={transactionState === 'pending'} > Cancel </Button> <Button onClick={handleFeePayment} className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-800" disabled={transactionState === 'pending' || !walletConnected} > Pay {FEE_AMOUNT} POL & Submit </Button> </DialogFooter> </DialogContent> </Dialog> <h1 className="text-3xl font-bold mb-2">Submit Your Perspective</h1> <p className="text-muted-foreground mb-6"> Share your thoughts on important topics and contribute to the discourse </p> {/* Daily submission limit indicator - only for unverified users */} {!isVerified && ( <div className="mb-6 p-4 bg-primary/10 dark:bg-primary/20 rounded-lg border border-primary/20 dark:border-primary/30"> <div className="flex items-start"> <div className="flex-1"> <h3 className="text-sm font-medium text-foreground dark:text-primary-foreground mb-1">Daily Submission Limit</h3> <div className="flex items-center"> <div className="flex-1 mr-4"> <div className="h-2 w-full bg-muted dark:bg-muted/50 rounded-full overflow-hidden border border-primary/10"> <div className="h-full bg-primary rounded-full" style={{ width: `${(perspectivesSubmitted / dailyLimit) * 100}%` }} ></div> </div> </div> <div className="text-sm font-medium text-foreground dark:text-primary-foreground"> {perspectivesSubmitted} / {dailyLimit} </div> </div> <p className="mt-2 text-xs text-muted-foreground dark:text-primary-foreground"> {trustScore < 100 ? "Increase your trust score to get more daily submissions." : "You've reached the maximum trust score for an unverified user."} {" "} <a href="/profile/verify" className="font-medium underline"> Verify your identity </a>{" "} for unlimited submissions. </p> </div> </div> </div> )} {/* Fee notice for unverified users */} {!isVerified && ( <Alert className="mb-6 bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800"> <CoinsStacked className="h-4 w-4" /> <AlertTitle>Fee Required for Unverified Users</AlertTitle> <AlertDescription> Submitting a perspective requires a {FEE_AMOUNT} POL fee, which will be refunded if your submission remains unflagged for 24 hours. </AlertDescription> </Alert> )} {perspectivesSubmitted >= dailyLimit && !isVerified ? ( <div className="bg-yellow-50 border border-yellow-200 text-yellow-800 rounded-lg p-4 mb-6 dark:bg-yellow-900/50 dark:border-yellow-800 dark:text-yellow-300"> <div className="flex"> <div className="flex-shrink-0"> <svg className="h-5 w-5 text-yellow-400 dark:text-yellow-300" viewBox="0 0 20 20" fill="currentColor"> <path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" /> </svg> </div> <div className="ml-3"> <h3 className="text-sm font-medium">Daily submission limit reached</h3> <div className="mt-2 text-sm"> <p> You've reached your daily submission limit. Come back tomorrow or{" "} <a href="/profile/verify" className="font-medium underline"> verify your identity </a>{" "} for unlimited submissions. </p> </div> </div> </div> </div> ) : ( <Card className="dark:border-border"> <CardHeader> <CardTitle>New Perspective</CardTitle> <CardDescription> Fill out the form below to submit your perspective. Be specific and constructive in your feedback. </CardDescription> </CardHeader> <CardContent> <form onSubmit={handleSubmit} className="space-y-6"> {/* Issue Selection */} <div className="space-y-2"> <Label htmlFor="issue" className="flex items-center"> Select Issue <span className="text-red-500 ml-1">*</span> <span className="text-xs text-muted-foreground ml-2">(Required)</span> </Label> {selectedIssueDetails ? ( <div className="mb-4"> <div className="flex items-center justify-between p-3 border rounded-md"> <div> <p className="font-medium">{selectedIssueDetails.title}</p> <Badge variant="outline" className="mt-1">{selectedIssueDetails.category}</Badge> </div> <Button type="button" variant="ghost" size="sm" onClick={() => setSelectedIssue(null)} > Change </Button> </div> <p className="text-xs text-muted-foreground mt-2"> Your perspective will be linked to this issue </p> </div> ) : ( <div> <Select value={selectedIssue || ""} onValueChange={(value) => setSelectedIssue(value)} required > <SelectTrigger> <SelectValue placeholder="Choose an issue to respond to" /> </SelectTrigger> <SelectContent> {mockIssues.map((issue) => ( <SelectItem key={issue.id} value={issue.id}> {issue.title} ({issue.category}) </SelectItem> ))} </SelectContent> </Select> <div className="mt-2 flex justify-between items-center"> <p className="text-xs text-muted-foreground"> Can't find the issue you're looking for? </p> <Button type="button" variant="link" size="sm" asChild className="p-0 h-auto"> <Link href="/issues/propose">Propose a new issue</Link> </Button> </div> </div> )} </div> {!selectedIssue && ( <Alert className="mb-4"> <AlertTitle>Note</AlertTitle> <AlertDescription> All perspectives must be linked to a specific issue. This helps organize discussions and generate meaningful insights. </AlertDescription> </Alert> )} {/* Title */} <div className="space-y-2"> <Label htmlFor="title">Title</Label> <Input id="title" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Give your perspective a clear, concise title" className="w-full" required /> </div> {/* Content */} <div className="space-y-2"> <Label htmlFor="content">Your Perspective</Label> <Textarea id="content" value={content} onChange={(e) => setContent(e.target.value)} placeholder="Share your thoughts in detail. Consider including specific examples or suggestions." className={`min-h-[200px] ${content.length > 1000 ? 'border-red-500 focus-visible:ring-red-500' : ''}`} required /> <div className="flex justify-between items-center mt-1"> <p className={`text-sm ${content.length > 1000 ? 'text-red-500 dark:text-red-400 font-medium' : 'text-muted-foreground'}`}> {content.length > 1000 ? 'Perspective exceeds 1000 characters' : ''} </p> <p className="text-sm text-muted-foreground"> {content.length}/1000 characters </p> </div> </div> {/* Attachments */} <div className="space-y-2"> <Label className="flex items-center"> Attachments <span className="text-xs text-muted-foreground ml-2">(Optional)</span> </Label> <div className="space-y-4"> <div className="flex flex-col sm:flex-row sm:items-center gap-4"> <input type="file" ref={fileInputRef} onChange={handleFileChange} accept=".pdf,.jpg,.jpeg,.png" className="hidden" disabled={!isVerified && !captchaVerified} /> <Button type="button" variant="outline" className="gap-2 bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900 hover:text-blue-800 dark:hover:text-blue-200" onClick={() => { if (!isVerified && !captchaVerified) { // Scroll to CAPTCHA section when trying to upload without verification document.getElementById('captcha-section')?.scrollIntoView({ behavior: 'smooth' }); return; } fileInputRef.current?.click(); }} disabled={!isVerified && !captchaVerified} > <Upload className="h-4 w-4" /> {!isVerified && !captchaVerified ? 'Verify to Upload' : 'Attach File'} </Button> {selectedFile && ( <div className="flex items-center gap-2 text-sm"> <FileText className="h-4 w-4 text-muted-foreground" /> <span className="text-muted-foreground">{selectedFile.name}</span> <Button type="button" variant="ghost" size="sm" className="h-auto p-1 text-red-500 hover:text-red-700" onClick={() => { setSelectedFile(null) if (fileInputRef.current) fileInputRef.current.value = '' }} > Remove </Button> </div> )} </div> {fileError && ( <p className="text-sm text-red-500 dark:text-red-400">{fileError}</p> )} <div className="text-sm text-muted-foreground space-y-1"> <p>Files must be PDF, JPEG, or PNG, and under 5MB.</p> {!isVerified && !captchaVerified && ( <Alert className="mt-2 bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800"> <Info className="h-4 w-4" /> <AlertTitle>Verification Required</AlertTitle> <AlertDescription> Please complete the CAPTCHA verification below to upload files. <Button type="button" variant="outline" size="sm" className="mt-2 w-full bg-blue-100 dark:bg-blue-900 hover:bg-blue-200 dark:hover:bg-blue-800 border-blue-300 dark:border-blue-700 dark:text-blue-300" onClick={() => document.getElementById('captcha-section')?.scrollIntoView({ behavior: 'smooth' })} > Go to Verification </Button> </AlertDescription> </Alert> )} </div> </div> </div> {/* CAPTCHA for Unverified Users */} {!isVerified && ( <div id="captcha-section" className="space-y-2"> <Label htmlFor="captcha" className="flex items-center"> Human Verification <span className="text-red-500 ml-1">*</span> {!captchaVerified && ( <span className="text-xs text-blue-600 ml-2">(Required for unverified users)</span> )} </Label> <div className="border rounded-md p-4 bg-muted/30"> <div className="flex items-center justify-between mb-3"> <div className="flex-1 text-center"> <div className="inline-block px-4 py-2 bg-primary/10 dark:bg-primary/20 rounded font-mono text-lg tracking-widest text-foreground dark:text-primary-foreground" style={{ letterSpacing: '0.5em', userSelect: 'none' }} > {captchaValue} </div> </div> <Button type="button" variant="ghost" size="sm" onClick={refreshCaptcha} className="ml-2" > <RefreshCw className="h-4 w-4" /> <span className="sr-only">Refresh CAPTCHA</span> </Button> </div> <div className="flex gap-2"> <Input id="captcha" value={captchaInput} onChange={(e) => setCaptchaInput(e.target.value)} placeholder="Enter the code shown above" className="flex-1" required={!isVerified} /> <Button type="button" variant="outline" onClick={validateCaptcha} className="bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900 hover:text-blue-800 dark:hover:text-blue-200" > Verify </Button> </div> {captchaVerified && ( <p className="text-green-600 dark:text-green-400 text-sm mt-1 flex items-center"> <CheckCircle className="h-3 w-3 mr-1" /> Verification successful - You can now upload files </p> )} </div> </div> )} {/* Error Message */} {submissionError && ( <Alert variant="destructive" className="mt-4"> <AlertTriangle className="h-4 w-4" /> <AlertTitle>Error</AlertTitle> <AlertDescription>{submissionError}</AlertDescription> </Alert> )} {/* Submit Button */} <div className="mt-6"> <button type="submit" disabled={ !walletConnected || (!isVerified && !captchaVerified) || (perspectivesSubmitted >= dailyLimit && !isVerified) || isSubmitting } className={` w-full py-3 px-4 rounded-md font-medium text-white ${isSubmitting ? 'bg-muted-foreground cursor-not-allowed' : 'bg-primary hover:bg-primary/90'} ${(!walletConnected || (!isVerified && !captchaVerified) || (perspectivesSubmitted >= dailyLimit && !isVerified)) ? 'opacity-50 cursor-not-allowed' : ''} transition duration-200 `} > {isSubmitting ? 'Submitting...' : 'Submit Perspective'} </button> </div> </form> </CardContent> </Card> )} {/* Guidelines */} <Card className="mt-8 dark:border-border"> <CardHeader> <CardTitle>Submission Guidelines</CardTitle> <CardDescription>Tips for submitting an effective perspective</CardDescription> </CardHeader> <CardContent> <ul className="list-disc space-y-2 pl-4 text-muted-foreground"> <li>Be specific and provide concrete examples</li> <li>Focus on constructive solutions rather than just problems</li> <li>Consider the impact on different stakeholders</li> <li>Support your perspective with data when possible</li> <li>Keep your tone professional and respectful</li> <li>Review your submission for clarity before posting</li> </ul> </CardContent> </Card> </div> </div> ) }