diff --git a/package-lock.json b/package-lock.json index 6a45738..dbf84fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "react-next-tww", "version": "0.1.0", "dependencies": { - "@decleanup/contracts": "^1.0.3", + "@decleanup/contracts": "^1.0.4", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-dropdown-menu": "^2.1.3", @@ -262,9 +262,9 @@ } }, "node_modules/@decleanup/contracts": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@decleanup/contracts/-/contracts-1.0.3.tgz", - "integrity": "sha512-K2ZhGOca1l1UmEkKo46R0jcu2F9QY0Vq/2PH++R2CjzAAtUc5SusRopA8D6sjRYbM34CYOkPeN63dq7E1e8h9w==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@decleanup/contracts/-/contracts-1.0.4.tgz", + "integrity": "sha512-tKciIO28NMM9Lsg6df+DTCO7wiDIs/qxHq1POUZPndiiEXqg0fVSW2+h1UjmunshLh+i0ZLB6kecboIEHDJn/w==", "license": "MIT", "dependencies": { "ethers": "^5.7.2" diff --git a/package.json b/package.json index e53dfd4..b5a7d41 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ ] }, "dependencies": { - "@decleanup/contracts": "^1.0.3", + "@decleanup/contracts": "^1.0.4", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-dropdown-menu": "^2.1.3", diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index acb9542..372eb16 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -6,6 +6,8 @@ import PreviewPage from '@/components/modals/PreviewModal' import Link from 'next/link' import { useState } from 'react' import Image from 'next/image' +import { useChainId, useSwitchChain } from 'wagmi' +import { arbitrumSepolia } from 'wagmi/chains' interface LongButtonProps { text: string @@ -15,16 +17,39 @@ interface LongButtonProps { export default function Page() { const [isModalOpen, setIsModalOpen] = useState(false) const [isUploadModalOpen, setIsUploadModalOpen] = useState(false) - const [_uploadedImages, setUploadedImages] = useState([]) + const [uploadedIpfsUri, setUploadedIpfsUri] = useState('') const [isShareModal, setIsShareModal] = useState(false) - const handleSubmit = (images: File[]) => { - setUploadedImages(images) + const chainId = useChainId() + const { switchChain } = useSwitchChain() + + const handleSubmit = (ipfsUri: string) => { + setUploadedIpfsUri(ipfsUri) setIsShareModal(true) - console.log('Uploaded images:', images) + console.log('Uploaded IPFS URI:', ipfsUri) + } + + const handleSwitchToArbitrumSepolia = () => { + switchChain({ chainId: arbitrumSepolia.id }) } + return (
+ {/* Network Warning Banner */} + {chainId !== 421614 && ( +
+
+ ⚠️ Wrong Network! Please switch to Arbitrum Sepolia + +
+
+ )} +
{/* 24 WEEKS STREAK*/} @@ -157,7 +182,11 @@ export default function Page() {
-
@@ -193,6 +222,7 @@ export default function Page() { onClose={() => { setIsShareModal(false) }} + ipfsUri={uploadedIpfsUri} />
) @@ -203,7 +233,7 @@ function LongButton({ text, isNotBlack }: LongButtonProps) { @@ -46,124 +105,186 @@ const PreviewPage: React.FC = ({
{/* Status Banner */} -
+
-
+
-

Submission Under Review

-

Your cleanup photos are being reviewed by our team

+

+ Submission Under Review +

+

+ Your cleanup photos are being reviewed by our team +

{/* Content */} -
-
+
+
{/* Images Section */}
-

Your Cleanup Journey

- - {cleanupPicture.before || cleanupPicture.after ? ( -
+

+ Your Cleanup Journey +

+ + {loading ? ( +
+
+ +
+

+ Loading Images... +

+

+ Fetching your cleanup photos from IPFS +

+
+ ) : error ? ( +
+
+ +
+

+ Error Loading Images +

+

{error}

+
+ ) : metadata ? ( +
{/* Before Image */} - {cleanupPicture.before && ( -
-
-
- 1 -
-

Before

+
+
+
+ 1
-
- Before cleanup -
- Before -
+

+ Before +

+
+
+ Before cleanup { + console.error('Failed to load before image') + e.currentTarget.style.display = 'none' + }} + /> +
+ Before
- )} +
{/* After Image */} - {cleanupPicture.after && ( -
-
-
- 2 -
-

After

+
+
+
+ 2
-
- After cleanup -
- After -
+

+ After +

+
+
+ After cleanup { + console.error('Failed to load after image') + e.currentTarget.style.display = 'none' + }} + /> +
+ After
- )} +
) : ( -
-
+
+
-

No Images to Preview

-

Upload your before and after photos to see them here

+

+ No Images to Preview +

+

+ Upload your before and after photos to see them here +

)}
{/* Status Panel */}
-
+
-

Review Status

- +

+ Review Status +

+ {/* Review Timeline */}
-
+
-

Submitted

-

Photos uploaded successfully

+

+ Submitted +

+

+ Photos uploaded successfully +

- +
-
+
-

Under Review

-

Team is verifying your cleanup

+

+ Under Review +

+

+ Team is verifying your cleanup +

- +
-
+
-

Approved

-

Rewards will be distributed

+

+ Approved +

+

+ Rewards will be distributed +

-

What's Next?

+

+ What\'s Next? +

⏱️ @@ -171,7 +292,7 @@ const PreviewPage: React.FC = ({
🏆 -

You'll receive your new level after approval

+

You'll receive your new level after approval

💬 @@ -181,12 +302,12 @@ const PreviewPage: React.FC = ({
- - - @@ -200,4 +321,4 @@ const PreviewPage: React.FC = ({ ) } -export default PreviewPage \ No newline at end of file +export default PreviewPage diff --git a/src/components/modals/UploadModal.tsx b/src/components/modals/UploadModal.tsx index 9a2c46f..5307fa3 100644 --- a/src/components/modals/UploadModal.tsx +++ b/src/components/modals/UploadModal.tsx @@ -1,30 +1,43 @@ import { useState } from 'react' -import { X, Send, Camera, Upload, CheckCircle, ArrowRight, AlertCircle } from 'lucide-react' - -// Import components (you'll need to make sure these exist or create them) -// import ImageUploader from '../imageUploader/ImageUploader' -// import UploadInstructions from '../imageUploader/UploadInstructions' -// import SocialConsentCheckbox from '../imageUploader/SocialConsentCheckbox' -// import { useCleanupContext } from '@/context/ContextApi' - -// If you don't have these components, we'll create inline versions -const ImageUploadModal = ({ - isOpen, - onClose, - onSubmit, - userAddress = '' -}) => { - const [beforeImage, setBeforeImage] = useState(null) - const [afterImage, setAfterImage] = useState(null) +import { + X, + Send, + Camera, + Upload, + CheckCircle, + ArrowRight, + AlertCircle, +} from 'lucide-react' +import { useSubmissionOperations } from '@/hooks/useSubmissionOperation' +import { useAccount } from 'wagmi' + +interface ImageUploadModalProps { + isOpen: boolean + onClose: () => void + onSubmit: (ipfsUri: string) => void + userAddress?: string +} + +const ImageUploadModal = ({ + isOpen, + onClose, + onSubmit, + userAddress = '', +}: ImageUploadModalProps) => { + const { address } = useAccount() + const { createSubmission } = useSubmissionOperations() + + const [beforeImage, setBeforeImage] = useState(null) + const [afterImage, setAfterImage] = useState(null) const [cleanupDate, setCleanupDate] = useState('') const [cleanupTime, setCleanupTime] = useState('') const [step, setStep] = useState(1) const [checkBox, setCheckBox] = useState(false) const [isDragging, setIsDragging] = useState(false) const [uploadStatus, setUploadStatus] = useState({ - status: 'idle', + status: 'idle' as 'idle' | 'uploading' | 'success' | 'error', message: '', - progress: 0 + progress: 0, }) // If you have the context, uncomment this: @@ -33,28 +46,28 @@ const ImageUploadModal = ({ if (!isOpen) return null // Validate image file types - const validateImageFile = (file) => { + const validateImageFile = (file: File) => { const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'] const maxSize = 10 * 1024 * 1024 // 10MB - + if (!validTypes.includes(file.type)) { setUploadStatus({ status: 'error', message: 'Please upload valid image files (JPEG, PNG, WebP)', - progress: 0 + progress: 0, }) return false } - + if (file.size > maxSize) { setUploadStatus({ status: 'error', message: 'Image files must be smaller than 10MB', - progress: 0 + progress: 0, }) return false } - + return true } @@ -62,21 +75,24 @@ const ImageUploadModal = ({ const testPinataConnection = async () => { const PINATA_API_KEY = process.env.NEXT_PUBLIC_PINATA_API_KEY const PINATA_SECRET_KEY = process.env.NEXT_PUBLIC_PINATA_SECRET_KEY - + if (!PINATA_API_KEY || !PINATA_SECRET_KEY) { console.error('Pinata API keys not configured') return false } try { - const response = await fetch('https://api.pinata.cloud/data/testAuthentication', { - method: 'GET', - headers: { - 'pinata_api_key': PINATA_API_KEY, - 'pinata_secret_api_key': PINATA_SECRET_KEY, + const response = await fetch( + 'https://api.pinata.cloud/data/testAuthentication', + { + method: 'GET', + headers: { + pinata_api_key: PINATA_API_KEY, + pinata_secret_api_key: PINATA_SECRET_KEY, + }, }, - }) - + ) + const result = await response.json() console.log('Pinata connection test:', result) return response.ok @@ -87,42 +103,47 @@ const ImageUploadModal = ({ } // Upload single file to Pinata - const uploadSingleFile = async (file, filename) => { + const uploadSingleFile = async (file: File, filename: string) => { const PINATA_API_KEY = process.env.NEXT_PUBLIC_PINATA_API_KEY const PINATA_SECRET_KEY = process.env.NEXT_PUBLIC_PINATA_SECRET_KEY - + if (!PINATA_API_KEY || !PINATA_SECRET_KEY) { throw new Error('Pinata API keys not configured') } const formData = new FormData() formData.append('file', file, filename) - + const pinataMetadata = { name: filename, keyvalues: { - userAddress, + userAddress: userAddress || address, type: 'cleanup-image', - timestamp: new Date().toISOString() - } + timestamp: new Date().toISOString(), + }, } - + formData.append('pinataMetadata', JSON.stringify(pinataMetadata)) - const response = await fetch('https://api.pinata.cloud/pinning/pinFileToIPFS', { - method: 'POST', - headers: { - 'pinata_api_key': PINATA_API_KEY, - 'pinata_secret_api_key': PINATA_SECRET_KEY, + const response = await fetch( + 'https://api.pinata.cloud/pinning/pinFileToIPFS', + { + method: 'POST', + headers: { + pinata_api_key: PINATA_API_KEY, + pinata_secret_api_key: PINATA_SECRET_KEY, + }, + body: formData, }, - body: formData, - }) + ) const responseText = await response.text() - + if (!response.ok) { console.error('Pinata response:', responseText) - throw new Error(`Pinata upload failed: ${response.status} ${response.statusText} - ${responseText}`) + throw new Error( + `Pinata upload failed: ${response.status} ${response.statusText} - ${responseText}`, + ) } const result = JSON.parse(responseText) @@ -130,66 +151,71 @@ const ImageUploadModal = ({ } // Create and upload metadata JSON - const uploadMetadata = async (metadata) => { + const uploadMetadata = async (metadata: Record) => { const PINATA_API_KEY = process.env.NEXT_PUBLIC_PINATA_API_KEY const PINATA_SECRET_KEY = process.env.NEXT_PUBLIC_PINATA_SECRET_KEY - + if (!PINATA_API_KEY || !PINATA_SECRET_KEY) { throw new Error('Pinata API keys not configured') } const metadataBlob = new Blob([JSON.stringify(metadata, null, 2)], { - type: 'application/json' + type: 'application/json', }) const formData = new FormData() formData.append('file', metadataBlob, 'cleanup-metadata.json') - + const pinataMetadata = { name: 'Cleanup Submission Metadata', keyvalues: { userAddress: metadata.userAddress, type: 'cleanup-metadata', - timestamp: metadata.submissionTimestamp - } + timestamp: metadata.submissionTimestamp, + }, } - + formData.append('pinataMetadata', JSON.stringify(pinataMetadata)) - const response = await fetch('https://api.pinata.cloud/pinning/pinFileToIPFS', { - method: 'POST', - headers: { - 'pinata_api_key': PINATA_API_KEY, - 'pinata_secret_api_key': PINATA_SECRET_KEY, + const response = await fetch( + 'https://api.pinata.cloud/pinning/pinFileToIPFS', + { + method: 'POST', + headers: { + pinata_api_key: PINATA_API_KEY, + pinata_secret_api_key: PINATA_SECRET_KEY, + }, + body: formData, }, - body: formData, - }) + ) const responseText = await response.text() - + if (!response.ok) { console.error('Pinata metadata response:', responseText) - throw new Error(`Metadata upload failed: ${response.status} ${response.statusText}`) + throw new Error( + `Metadata upload failed: ${response.status} ${response.statusText}`, + ) } const result = JSON.parse(responseText) return result.IpfsHash } - const handleDragOver = (e) => { + const handleDragOver = (e: React.DragEvent) => { e.preventDefault() setIsDragging(true) } - const handleDragLeave = (e) => { + const handleDragLeave = (e: React.DragEvent) => { e.preventDefault() setIsDragging(false) } - const handleDrop = (e, type) => { + const handleDrop = (e: React.DragEvent, type: 'before' | 'after') => { e.preventDefault() setIsDragging(false) - + if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { const file = e.dataTransfer.files[0] if (validateImageFile(file)) { @@ -202,7 +228,10 @@ const ImageUploadModal = ({ } } - const handleFileSelect = (e, type) => { + const handleFileSelect = ( + e: React.ChangeEvent, + type: 'before' | 'after', + ) => { if (e.target.files && e.target.files.length > 0) { const file = e.target.files[0] if (validateImageFile(file)) { @@ -221,7 +250,7 @@ const ImageUploadModal = ({ setUploadStatus({ status: 'error', message: 'Please accept the social consent checkbox', - progress: 0 + progress: 0, }) return } @@ -230,7 +259,7 @@ const ImageUploadModal = ({ setUploadStatus({ status: 'error', message: 'Please provide both before and after photos', - progress: 0 + progress: 0, }) return } @@ -239,7 +268,7 @@ const ImageUploadModal = ({ setUploadStatus({ status: 'error', message: 'Please provide the cleanup date and time', - progress: 0 + progress: 0, }) return } @@ -247,52 +276,56 @@ const ImageUploadModal = ({ setUploadStatus({ status: 'uploading', message: 'Testing connection...', - progress: 5 + progress: 5, }) try { // Test Pinata connection first const connectionTest = await testPinataConnection() if (!connectionTest) { - throw new Error('Failed to connect to Pinata. Please check your API keys.') + throw new Error( + 'Failed to connect to Pinata. Please check your API keys.', + ) } setUploadStatus({ status: 'uploading', message: 'Uploading before image...', - progress: 20 + progress: 20, }) // Upload before image const beforeImageHash = await uploadSingleFile( - beforeImage, - `before_${Date.now()}.${beforeImage.name.split('.').pop()}` + beforeImage, + `before_${Date.now()}.${beforeImage.name.split('.').pop()}`, ) setUploadStatus({ status: 'uploading', message: 'Uploading after image...', - progress: 50 + progress: 50, }) // Upload after image const afterImageHash = await uploadSingleFile( - afterImage, - `after_${Date.now()}.${afterImage.name.split('.').pop()}` + afterImage, + `after_${Date.now()}.${afterImage.name.split('.').pop()}`, ) setUploadStatus({ status: 'uploading', message: 'Creating metadata...', - progress: 80 + progress: 80, }) // Create comprehensive metadata const submissionTimestamp = new Date().toISOString() - const cleanupDateTime = new Date(`${cleanupDate}T${cleanupTime}`).toISOString() - + const cleanupDateTime = new Date( + `${cleanupDate}T${cleanupTime}`, + ).toISOString() + const metadata = { - userAddress, + userAddress: userAddress || address, submissionTimestamp, cleanupDateTime, images: { @@ -301,43 +334,55 @@ const ImageUploadModal = ({ ipfsUri: `ipfs://${beforeImageHash}`, name: beforeImage.name, size: beforeImage.size, - type: beforeImage.type + type: beforeImage.type, }, after: { hash: afterImageHash, ipfsUri: `ipfs://${afterImageHash}`, name: afterImage.name, size: afterImage.size, - type: afterImage.type - } + type: afterImage.type, + }, }, version: '1.0', - platform: 'cleanup-app' + platform: 'cleanup-app', } // Upload metadata const metadataHash = await uploadMetadata(metadata) + const ipfsUri = `ipfs://${metadataHash}` + + setUploadStatus({ + status: 'uploading', + message: 'Submitting to blockchain...', + progress: 90, + }) + + // Submit to blockchain using the submission hook + if (createSubmission) { + const tx = await createSubmission(ipfsUri) + console.log('Submission transaction:', tx) + } setUploadStatus({ status: 'success', - message: 'Upload successful!', - progress: 100 + message: 'Upload and submission successful!', + progress: 100, }) // Submit the metadata IPFS URI (which contains links to both images) - onSubmit(`ipfs://${metadataHash}`) + onSubmit(ipfsUri) // Reset form setTimeout(() => { handleClose() }, 1500) - } catch (error) { console.error('Upload error:', error) setUploadStatus({ status: 'error', message: `Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`, - progress: 0 + progress: 0, }) } } @@ -353,109 +398,138 @@ const ImageUploadModal = ({ onClose() } - const ImageUploader = ({ image, onImageChange, label, type, stepNumber }) => ( -
-
-
-
+ const ImageUploader = ({ + image, + onImageChange, + label, + type, + stepNumber, + }: { + image: File | null + onImageChange: (file: File | null) => void + label: string + type: 'before' | 'after' + stepNumber: number + }) => ( +
+
+
+
{stepNumber}
-

{stepNumber === 1 ? 'Before' : 'After'}

+

+ {stepNumber === 1 ? 'Before' : 'After'} +

-

{label}

+

{label}

handleDrop(e, type)} + onDrop={e => handleDrop(e, type)} > {image ? ( -
-
+
+
{`${type}
-
+
- Image uploaded + Image uploaded
-

{image.name}

+

{image.name}

) : ( -
-
- +
+
+
-

+

Click to upload or drag and drop

-

PNG, JPG, GIF up to 10MB

+

PNG, JPG, GIF up to 10MB

)} - + handleFileSelect(e, type)} - className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + type='file' + accept='image/*' + onChange={e => handleFileSelect(e, type)} + className='absolute inset-0 h-full w-full cursor-pointer opacity-0' />
) const ProgressBar = () => ( -
-
-
= 1 ? 'bg-black text-[#FAFF00]' : 'bg-gray-200 text-gray-600' - }`}> +
+
+
= 1 ? 'bg-black text-[#FAFF00]' : 'bg-gray-200 text-gray-600' + }`} + > {beforeImage ? : '1'}
-
= 2 ? 'bg-black' : 'bg-gray-200'}`} /> -
= 2 ? 'bg-black text-[#FAFF00]' : 'bg-gray-200 text-gray-600' - }`}> +
= 2 ? 'bg-black' : 'bg-gray-200'}`} + /> +
= 2 ? 'bg-black text-[#FAFF00]' : 'bg-gray-200 text-gray-600' + }`} + > {afterImage ? : '2'}
-
-
+
+
) - const isFormValid = beforeImage && afterImage && cleanupDate && cleanupTime && checkBox + const isFormValid = + beforeImage && afterImage && cleanupDate && cleanupTime && checkBox return ( -
-
+
+
{/* Header */} -
-
-

Share Your Cleanup Impact

+
+
+

+ Share Your Cleanup Impact +

{/* Content */} -
+
{/* Desktop view */} -
-
+
+
- + {/* Right sidebar for desktop */} -
+
{/* Date and Time Fields */} -
-

Cleanup Details

- +
+

+ Cleanup Details +

+
-
- +
-
@@ -522,30 +606,34 @@ const ImageUploadModal = ({ {/* Upload Status */} {uploadStatus.status !== 'idle' && ( -
-
+
+
{uploadStatus.status === 'uploading' && ( - + )} {uploadStatus.status === 'success' && ( - + )} {uploadStatus.status === 'error' && ( - + )} - + {uploadStatus.message}
- + {uploadStatus.status === 'uploading' && ( -
+
@@ -557,44 +645,54 @@ const ImageUploadModal = ({
{/* Mobile view */} -
+
{step === 1 ? (
- + {/* Date and Time Fields for mobile */} -
-

Cleanup Details

- +
+

+ Cleanup Details +

+
-
- +
-
@@ -604,38 +702,42 @@ const ImageUploadModal = ({ )} {/* Upload Status for mobile */} {uploadStatus.status !== 'idle' && ( -
-
+
+
{uploadStatus.status === 'uploading' && ( - + )} {uploadStatus.status === 'success' && ( - + )} {uploadStatus.status === 'error' && ( - + )} - + {uploadStatus.message}
- + {uploadStatus.status === 'uploading' && ( -
+
@@ -645,27 +747,29 @@ const ImageUploadModal = ({
{/* Social consent */} -
-