commit 1597a22deed3880f191114e10eb6907153ed5821 Author: ale Date: Sun Aug 24 00:59:13 2025 +0200 initial commit Signed-off-by: ale diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90b4581 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +*.lock +*-lock.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..16bf926 --- /dev/null +++ b/README.md @@ -0,0 +1,280 @@ +# 🌐 IPFS Upload - Decentralized File Storage + +A modern React application for uploading files, folders, and URLs to the InterPlanetary File System (IPFS) using the latest **Helia** technology. + +![IPFS Upload Demo](https://img.shields.io/badge/IPFS-Upload-blue?style=for-the-badge&logo=ipfs) +![React](https://img.shields.io/badge/React-19.1.1-61dafb?style=for-the-badge&logo=react) +![Helia](https://img.shields.io/badge/Helia-Latest-orange?style=for-the-badge) +![Bootstrap](https://img.shields.io/badge/Bootstrap-5-7952b3?style=for-the-badge&logo=bootstrap) + +## ✨ Features + +### 📁 Multiple Upload Methods +- **Individual Files**: Upload single or multiple files at once +- **Entire Folders**: Upload complete directory structures +- **Remote URLs**: Download and upload files from external URLs + +### 🔧 Flexible Configuration +- **Self-hosted Instance**: Run your own IPFS node +- **Custom Instance**: Connect to external IPFS instances +- **Content Pinning**: Ensure your content persists on the network + +### 🎨 Modern Interface +- **Responsive Design**: Works perfectly on desktop and mobile +- **Dynamic Background**: Automatically changing pastel colors +- **Real-time Feedback**: Toast notifications and progress indicators +- **Beautiful UI**: Modern Bootstrap 5 styling with custom enhancements + +### 🚀 Advanced Features +- **CID Copying**: One-click copy to clipboard +- **File Size Formatting**: Human-readable file sizes +- **Error Handling**: Comprehensive error messages +- **Memory Management**: Automatic cleanup of IPFS instances + +## đŸ› ī¸ Technology Stack + +- **Frontend**: React 19.1.1 with Hooks +- **IPFS**: Helia (next-generation IPFS implementation) +- **Styling**: Bootstrap 5 + React Bootstrap +- **Icons**: Emoji-based for universal compatibility +- **Build Tool**: Create React App + +## đŸ“Ļ Dependencies + +### Core IPFS +```json +{ + "@helia/unixfs": "latest", + "@helia/http": "latest", + "helia": "latest", + "@helia/strings": "latest", + "@libp2p/websockets": "latest" +} +``` + +### UI & Styling +```json +{ + "bootstrap": "latest", + "react-bootstrap": "latest" +} +``` + +## 🚀 Quick Start + +### Prerequisites +- Node.js (v16 or higher) +- npm or yarn +- Modern web browser with JavaScript enabled + +### Installation + +1. **Clone the repository** + ```bash + git clone + cd ipfs-upload + ``` + +2. **Install dependencies** + ```bash + npm install + ``` + +3. **Start the development server** + ```bash + npm start + ``` + +4. **Open in browser** + Navigate to [http://localhost:3000](http://localhost:3000) + +## đŸŽ¯ Usage Guide + +### 1. Configure Your IPFS Instance + +**Self Instance (Recommended for beginners)** +- Select "Self Instance" radio button +- The app will create a local IPFS node automatically + +**Custom Instance (For advanced users)** +- Select "Custom Instance" radio button +- Enter your IPFS instance URL (e.g., `http://localhost:5001/api/v0`) +- Ensure CORS is properly configured on your instance + +### 2. Enable Content Pinning (Optional) + +Check the "Persist content (pinned)" checkbox to ensure your uploaded content remains available on the IPFS network and won't be garbage collected. + +### 3. Upload Your Content + +**Upload Files** +1. Click on the file input in the green "Upload Files" card +2. Select one or multiple files +3. Click "Upload Files" + +**Upload Folders** +1. Click on the folder input in the red "Upload Folder" card +2. Select a folder (all files and subdirectories will be included) +3. Click "Upload Folder" + +**Upload from URL** +1. Enter a direct file URL in the yellow "Upload URL" card +2. Click "Upload URL" + +### 4. Access Your Files + +Once uploaded, you'll see: +- **File name** and size +- **IPFS CID** (Content Identifier) +- **Direct link** to view on IPFS +- **Copy CID** button for easy sharing +- **Pinned status** (if enabled) + +## 🔗 IPFS Access + +Your uploaded files can be accessed through: + +- **IPFS Gateway**: `https://ipfs.io/ipfs/` +- **Local Gateway**: `http://localhost:8080/ipfs/` (if running IPFS locally) +- **Any IPFS Gateway**: Replace the domain with your preferred gateway + +## âš ī¸ Important Notes + +### Security Considerations +- **Content Permanence**: Files uploaded to IPFS cannot be deleted +- **Public Network**: Content is publicly accessible via CID +- **CORS Requirements**: Custom instances must have CORS enabled + +### Browser Compatibility +- Modern browsers (Chrome, Firefox, Safari, Edge) +- JavaScript must be enabled +- Local storage access required + +### File Size Limitations +- Browser memory limitations apply +- Large files may take longer to process +- Consider file size when using self-hosted instances + +## đŸ—ī¸ Project Structure + +``` +src/ +├── components/ +│ ├── FileList.js # Component for displaying uploaded files +│ └── UploadCard.js # Reusable upload card component +├── App.js # Main application component +├── App.css # Custom styles +├── index.js # Application entry point +└── index.css # Global styles +``` + +## 🔧 Development + +### Available Scripts + +- `npm start` - Start development server +- `npm test` - Run test suite +- `npm run build` - Build for production +- `npm run eject` - Eject from Create React App (irreversible) + +### Custom Scripts + +You can add these to your `package.json`: + +```json +{ + "scripts": { + "analyze": "npm run build && npx serve -s build", + "lint": "eslint src --ext .js,.jsx", + "format": "prettier --write src/**/*.{js,jsx,css,md}" + } +} +``` + +## 🚀 Deployment + +### Build for Production + +```bash +npm run build +``` + +This creates an optimized build in the `build/` folder. + +### Deployment Options + +- **GitHub Pages**: Use `gh-pages` package +- **Netlify**: Connect your repository for automatic deploys +- **Vercel**: Import your project for instant deployment +- **Static Hosting**: Upload the `build/` folder to any static host + +### Environment Variables + +Create a `.env` file for configuration: + +```env +REACT_APP_DEFAULT_GATEWAY=https://ipfs.io +REACT_APP_ENABLE_ANALYTICS=false +``` + +## 🤝 Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +### Development Guidelines + +1. Follow existing code style +2. Add tests for new features +3. Update documentation as needed +4. Ensure all tests pass before submitting + +### Reporting Issues + +Please include: +- Browser and version +- Steps to reproduce +- Expected vs actual behavior +- Console errors (if any) + +## 📄 License + +This project is open source and available under the [MIT License](LICENSE). + +## 🙏 Acknowledgments + +- **IPFS Team** for the amazing decentralized storage protocol +- **Helia Team** for the modern IPFS implementation +- **React Team** for the powerful UI framework +- **Bootstrap Team** for the beautiful CSS framework + +## 📞 Support + +- **Documentation**: Check this README and inline comments +- **Issues**: Use GitHub Issues for bug reports +- **Discussions**: Use GitHub Discussions for questions + +--- + +**Made with â¤ī¸ by [ale](https://manalejandro.com)** + +*Powered by [Helia](https://helia.io/) - The next generation IPFS implementation* + +### Analyzing the Bundle Size + +This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) + +### Making a Progressive Web App + +This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) + +### Advanced Configuration + +This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) + +### Deployment + +This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) + +### `npm run build` fails to minify + +This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) diff --git a/package.json b/package.json new file mode 100644 index 0000000..6a75e93 --- /dev/null +++ b/package.json @@ -0,0 +1,46 @@ +{ + "name": "ipfs-upload", + "version": "0.1.0", + "private": true, + "dependencies": { + "@helia/http": "^2.2.1", + "@helia/strings": "^4.1.0", + "@helia/unixfs": "^5.1.0", + "@libp2p/websockets": "^9.2.19", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^13.5.0", + "bootstrap": "^5.3.7", + "helia": "^5.5.1", + "react": "^19.1.1", + "react-bootstrap": "^2.10.10", + "react-dom": "^19.1.1", + "react-scripts": "5.0.1", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..a11777c Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..3e1a449 --- /dev/null +++ b/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + IPFS Upload - Decentralized File Storage + + + +
+ + + diff --git a/public/logo192.png b/public/logo192.png new file mode 100644 index 0000000..fc44b0a Binary files /dev/null and b/public/logo192.png differ diff --git a/public/logo512.png b/public/logo512.png new file mode 100644 index 0000000..a4e47a6 Binary files /dev/null and b/public/logo512.png differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..b04caaa --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "IPFS Upload", + "name": "IPFS Upload", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..349360b --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Allow: / diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..7b6bb6f --- /dev/null +++ b/src/App.css @@ -0,0 +1,209 @@ +/* Custom styles for IPFS Upload App */ + +body { + font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif; + transition: background-color 1s ease; +} + +.App { + text-align: center; +} + +/* Custom card styling */ +.card { + border-radius: 12px; + border: none; + transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; +} + +.card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15) !important; +} + +.card-header { + border-radius: 12px 12px 0 0 !important; + border-bottom: none; + font-weight: 600; +} + +/* Upload cards specific styling */ +.border-success .card-header { + background: linear-gradient(135deg, #28a745, #20c997) !important; +} + +.border-danger .card-header { + background: linear-gradient(135deg, #dc3545, #fd7e14) !important; +} + +.border-warning .card-header { + background: linear-gradient(135deg, #ffc107, #fd7e14) !important; +} + +/* Button styling */ +.btn { + border-radius: 8px; + font-weight: 500; + transition: all 0.2s ease-in-out; + text-decoration: none; +} + +.btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +/* Form controls */ +.form-control { + border-radius: 8px; + border: 2px solid #e9ecef; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.form-control:focus { + border-color: #80bdff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +/* Progress bars */ +.progress { + border-radius: 50px; + height: 8px; + background-color: rgba(0, 0, 0, 0.1); +} + +.progress-bar { + border-radius: 50px; + transition: width 0.3s ease; +} + +/* List group items */ +.list-group-item { + border-radius: 8px !important; + border: none; + margin-bottom: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Alert styling */ +.alert { + border-radius: 12px; + border: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +/* Modal styling */ +.modal-content { + border-radius: 12px; + border: none; + box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); +} + +.modal-header { + border-radius: 12px 12px 0 0; + border-bottom: none; +} + +/* Badge styling */ +.badge { + border-radius: 20px; + font-size: 0.7rem; + padding: 4px 8px; +} + +/* Spinner animation */ +.spinner-border { + width: 3rem; + height: 3rem; +} + +/* File input styling */ +input[type="file"] { + border-radius: 8px; +} + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: #888; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #555; +} + +/* Animation for cards */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translate3d(0, 40px, 0); + } + to { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} + +.card { + animation: fadeInUp 0.5s ease-out; +} + +/* Typography improvements */ +.display-4 { + font-weight: 700; + background: linear-gradient(135deg, #007bff, #6610f2); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Link styling */ +a { + text-decoration: none; + transition: color 0.2s ease-in-out; +} + +a:hover { + text-decoration: underline; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .display-4 { + font-size: 2.5rem; + } + + .card { + margin-bottom: 1rem; + } + + .container { + padding-left: 15px; + padding-right: 15px; + } +} + +/* Custom checkbox and radio styling */ +.form-check-input:checked { + background-color: #007bff; + border-color: #007bff; +} + +.form-check-input:focus { + border-color: #80bdff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +/* Improve text contrast on dynamic backgrounds */ +.text-on-dynamic-bg { + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); +} diff --git a/src/App.js b/src/App.js new file mode 100644 index 0000000..34fd829 --- /dev/null +++ b/src/App.js @@ -0,0 +1,536 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Container, Row, Col, Card, Form, Button, Alert, Modal, Toast, ToastContainer } from 'react-bootstrap'; +import { createHelia } from 'helia'; +import { unixfs } from '@helia/unixfs'; +import { createHeliaHTTP } from '@helia/http'; +import UploadCard from './components/UploadCard'; +import FileList from './components/FileList'; +import './App.css'; +import 'bootstrap/dist/css/bootstrap.min.css'; + +function App() { + const [instanceType, setInstanceType] = useState('self'); + const [customAddress, setCustomAddress] = useState(''); + const [pinned, setPinned] = useState(false); + const [loading, setLoading] = useState(false); + const [files, setFiles] = useState([]); + const [folderFiles, setFolderFiles] = useState([]); + const [urlInput, setUrlInput] = useState(''); + const [uploadedFiles, setUploadedFiles] = useState([]); + const [showModal, setShowModal] = useState(false); + const [modalMessage, setModalMessage] = useState(''); + const [backgroundColor, setBackgroundColor] = useState('#CCDDEE'); + const [showToast, setShowToast] = useState(false); + const [toastMessage, setToastMessage] = useState(''); + const [toastVariant, setToastVariant] = useState('success'); + + const heliaRef = useRef(null); + const fileInputRef = useRef(null); + const folderInputRef = useRef(null); + + // Function to generate random pastel colors + const getRandomColor = () => { + const letters = 'CDE'; + let color = '#'; + for (let i = 0; i < 6; i++) { + color += letters[Math.floor(Math.random() * letters.length)]; + } + return color; + }; + + // Change background color every 30 seconds + useEffect(() => { + const changeColor = () => { + setBackgroundColor(getRandomColor()); + }; + + const interval = setInterval(changeColor, 30000); + return () => clearInterval(interval); + }, []); + + // Cleanup Helia instance on unmount + useEffect(() => { + return () => { + if (heliaRef.current) { + heliaRef.current.stop().catch(console.error); + } + }; + }, []); + + // Show toast notification + const showToastMessage = (message, variant = 'success') => { + setToastMessage(message); + setToastVariant(variant); + setShowToast(true); + }; + + // Show modal with message + const showMessage = (message) => { + setModalMessage(message); + setShowModal(true); + }; + + // Initialize Helia instance + const initializeHelia = async () => { + try { + if (heliaRef.current) { + await heliaRef.current.stop(); + } + + let helia; + if (instanceType === 'custom') { + if (!customAddress || !customAddress.trim()) { + throw new Error('Custom address is required'); + } + try { + new URL(customAddress); + helia = await createHeliaHTTP({ url: customAddress }); + } catch (err) { + throw new Error('Invalid URL format'); + } + } else { + helia = await createHelia({ + repo: `ipfs-${Math.floor(Math.random() * 10000)}` + }); + } + + heliaRef.current = helia; + return helia; + } catch (error) { + throw error; + } + }; + + // Upload files to IPFS + const uploadFiles = async (filesToUpload, isFolder = false, isUrl = false) => { + setLoading(true); + setUploadedFiles([]); + + try { + const helia = await initializeHelia(); + const fs = unixfs(helia); + + if (isUrl) { + // Handle URL upload + try { + const response = await fetch(filesToUpload); + if (!response.ok) throw new Error('Failed to fetch URL'); + + const blob = await response.blob(); + const fileName = filesToUpload.split('/').pop() || 'downloaded-file'; + const file = new File([blob], fileName, { type: blob.type }); + + const fileData = new Uint8Array(await file.arrayBuffer()); + const cid = await fs.addFile({ path: fileName, content: fileData }); + + const result = { + name: fileName, + cid: cid.toString(), + size: file.size, + url: `https://ipfs.io/ipfs/${cid.toString()}?filename=${fileName}` + }; + + if (pinned) { + await helia.pins.add(cid); + result.pinned = true; + } + + setUploadedFiles([result]); + showToastMessage('URL uploaded successfully!'); + } catch (error) { + throw new Error(`Error uploading URL: ${error.message}`); + } + } else { + // Handle file/folder upload + + if (isFolder && filesToUpload.length > 0) { + // Handle folder upload using addAll + const fileObjects = []; + + for (const file of filesToUpload) { + const path = file.webkitRelativePath || file.name; + const fileData = new Uint8Array(await file.arrayBuffer()); + fileObjects.push({ path, content: fileData }); + } + + // Add all files with directory structure + const results = []; + for await (const result of fs.addAll(fileObjects, { wrapWithDirectory: true })) { + if (result.path === '') { + // This is the root directory + const folderResult = { + name: 'Folder', + cid: result.cid.toString(), + size: Array.from(filesToUpload).reduce((acc, file) => acc + file.size, 0), + url: `https://ipfs.io/ipfs/${result.cid.toString()}?filename=Folder` + }; + + if (pinned) { + await helia.pins.add(result.cid); + folderResult.pinned = true; + } + + results.push(folderResult); + break; // We only want the root directory result + } + } + + setUploadedFiles(results); + showToastMessage('Folder uploaded successfully!'); + } else { + // Handle individual files using addAll + const fileObjects = []; + + for (const file of filesToUpload) { + const fileData = new Uint8Array(await file.arrayBuffer()); + fileObjects.push({ path: file.name, content: fileData }); + } + + const results = []; + for await (const result of fs.addAll(fileObjects)) { + const fileResult = { + name: result.path, + cid: result.cid.toString(), + size: filesToUpload.find(f => f.name === result.path)?.size || 0, + url: `https://ipfs.io/ipfs/${result.cid.toString()}?filename=${result.path}` + }; + + if (pinned) { + await helia.pins.add(result.cid); + fileResult.pinned = true; + } + + results.push(fileResult); + } + + setUploadedFiles(results); + showToastMessage(`${results.length} file(s) uploaded successfully!`); + } + } + + if (pinned) { + showMessage('Content uploaded and pinned successfully! It will persist on the network.'); + } + + } catch (error) { + console.error('Upload error:', error); + showMessage(`Error: ${error.message}`); + showToastMessage(`Upload failed: ${error.message}`, 'danger'); + } finally { + setLoading(false); + } + }; + + // Handle file upload + const handleFileUpload = () => { + if (!files || files.length === 0) { + showMessage('Please select files to upload'); + return; + } + uploadFiles(files); + }; + + // Handle folder upload + const handleFolderUpload = () => { + if (!folderFiles || folderFiles.length === 0) { + showMessage('Please select a folder to upload'); + return; + } + uploadFiles(folderFiles, true); + }; + + // Handle URL upload + const handleUrlUpload = () => { + if (!urlInput || !urlInput.trim()) { + showMessage('Please enter a URL to upload'); + return; + } + + try { + new URL(urlInput.trim()); + } catch { + showMessage('Please enter a valid URL'); + return; + } + + uploadFiles(urlInput.trim(), false, true); + }; + + // Handle pinned checkbox change + const handlePinnedChange = (checked) => { + setPinned(checked); + if (checked) { + showMessage('When pinned, content will persist on the IPFS network and won\'t be garbage collected.'); + } + }; + + // Clear uploaded files + const clearResults = () => { + setUploadedFiles([]); + setFiles([]); + setFolderFiles([]); + setUrlInput(''); + if (fileInputRef.current) fileInputRef.current.value = ''; + if (folderInputRef.current) folderInputRef.current.value = ''; + }; + + return ( +
+ + {/* Header */} + + +

+ 🌐 IPFS Upload +

+

+ Upload files, folders, and URLs to the InterPlanetary File System using modern Helia technology +

+ +
+ + {/* Instance Configuration */} + + + + +
🔧 Instance Configuration
+
+ +
+ + + setInstanceType('self')} + /> + + + setInstanceType('custom')} + /> + + + + {instanceType === 'custom' && ( + + + setCustomAddress(e.target.value)} + /> + + Must use client CORS. + + 🔧 Install CORS Extension + + + + + )} + + + + handlePinnedChange(e.target.checked)} + /> + + Pinned content will persist on the network and won't be garbage collected + + + +
+
+
+ +
+ + {/* Upload Forms */} + + {/* File Upload */} + + + setFiles(Array.from(e.target.files))} + className="mb-3" + /> + {files && files.length > 0 && ( + + {files.length} file(s) selected + + )} + + + + {/* Folder Upload */} + + + setFolderFiles(Array.from(e.target.files))} + className="mb-3" + /> + {folderFiles && folderFiles.length > 0 && ( + + {folderFiles.length} file(s) in folder + + )} + + + + {/* URL Upload */} + + + setUrlInput(e.target.value)} + className="mb-3" + /> + + Enter a direct link to a file + + + + + + {/* Loading Indicator */} + {loading && ( + + +
+ Loading... +
+

+ Processing upload...
+ This may take a moment depending on file size and network conditions +

+ +
+ )} + + {/* Upload Results */} + {uploadedFiles.length > 0 && ( + + +
+
📋 Upload Results
+ +
+ + +
+ )} + + {/* Footer */} + + + + âš ī¸ Warning: Uploaded content cannot be deleted from the IPFS network + +
+

+ Made with â¤ī¸ by ale +

+ + Powered by Helia - + The next generation IPFS implementation + +
+ +
+ + {/* Modal */} + setShowModal(false)} centered> + + â„šī¸ Information + + +

{modalMessage}

+
+ + + +
+ + {/* Toast Notifications */} + + setShowToast(false)} + delay={4000} + autohide + bg={toastVariant} + > + + + {toastVariant === 'success' ? '✅' : '❌'} Notification + + + + {toastMessage} + + + +
+
+ ); +} + +export default App; diff --git a/src/App.test.js b/src/App.test.js new file mode 100644 index 0000000..1f03afe --- /dev/null +++ b/src/App.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/src/components/FileList.js b/src/components/FileList.js new file mode 100644 index 0000000..112d9b8 --- /dev/null +++ b/src/components/FileList.js @@ -0,0 +1,84 @@ +import React from 'react'; +import { Card, ListGroup, Badge, Button } from 'react-bootstrap'; + +const FileList = ({ files, title, onCopy }) => { + if (!files || files.length === 0) { + return null; + } + + const copyToClipboard = (text) => { + navigator.clipboard.writeText(text).then(() => { + if (onCopy) onCopy('CID copied to clipboard!'); + }).catch(() => { + if (onCopy) onCopy('Failed to copy CID'); + }); + }; + + const formatFileSize = (bytes) => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + return ( + + +
📋 {title}
+
+ + + {files.map((file, index) => ( + +
+
+ + {file.name === 'Folder' ? '📂' : '📄'} + + {file.name} +
+ + {formatFileSize(file.size)} + + {file.pinned && ( + + 📌 Pinned + + )} +
+
+
+ + +
+
+ + {file.cid} + +
+
+
+ ))} +
+
+
+ ); +}; + +export default FileList; diff --git a/src/components/UploadCard.js b/src/components/UploadCard.js new file mode 100644 index 0000000..d81efa7 --- /dev/null +++ b/src/components/UploadCard.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { Card, Button } from 'react-bootstrap'; + +const UploadCard = ({ + title, + icon, + variant, + loading, + onUpload, + children, + disabled = false +}) => { + return ( + + +
+ {icon} {title} +
+
+ + {children} + + +
+ ); +}; + +export default UploadCard; diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..7d8d5cc --- /dev/null +++ b/src/index.css @@ -0,0 +1,41 @@ +/* Global styles for IPFS Upload App */ + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + line-height: 1.6; +} + +code { + font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace; +} + +/* Improve default focus styles */ +*:focus { + outline: none; +} + +/* Smooth scrolling */ +html { + scroll-behavior: smooth; +} + +/* Ensure full height */ +#root { + min-height: 100vh; +} + +/* Custom selection color */ +::selection { + background-color: rgba(0, 123, 255, 0.2); +} + +::-moz-selection { + background-color: rgba(0, 123, 255, 0.2); +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..d563c0f --- /dev/null +++ b/src/index.js @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; +import reportWebVitals from './reportWebVitals'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/src/logo.svg b/src/logo.svg new file mode 100644 index 0000000..9dfc1c0 --- /dev/null +++ b/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js new file mode 100644 index 0000000..5253d3a --- /dev/null +++ b/src/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/src/setupTests.js b/src/setupTests.js new file mode 100644 index 0000000..8f2609b --- /dev/null +++ b/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom';