2025-03-25 03:52:30 -04:00

928 lines
37 KiB
TypeScript

"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>
)
}