initial commit

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2025-08-24 00:59:13 +02:00
commit 1597a22dee
Se han modificado 19 ficheros con 1379 adiciones y 0 borrados

26
.gitignore vendido Archivo normal
Ver fichero

@@ -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

280
README.md Archivo normal
Ver fichero

@@ -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 <repository-url>
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/<CID>`
- **Local Gateway**: `http://localhost:8080/ipfs/<CID>` (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)

46
package.json Archivo normal
Ver fichero

@@ -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"
]
}
}

BIN
public/favicon.ico Archivo normal

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 3.8 KiB

43
public/index.html Archivo normal
Ver fichero

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Upload files, folders, and URLs to the InterPlanetary File System (IPFS) using modern Helia technology"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>IPFS Upload - Decentralized File Storage</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
public/logo192.png Archivo normal

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 5.2 KiB

BIN
public/logo512.png Archivo normal

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 9.4 KiB

25
public/manifest.json Archivo normal
Ver fichero

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

3
public/robots.txt Archivo normal
Ver fichero

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Allow: /

209
src/App.css Archivo normal
Ver fichero

@@ -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);
}

536
src/App.js Archivo normal
Ver fichero

@@ -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 (
<div style={{ backgroundColor, minHeight: '100vh', transition: 'background-color 1s ease' }}>
<Container className="py-5">
{/* Header */}
<Row className="mb-5">
<Col className="text-center">
<h1 className="display-4 text-primary fw-bold mb-3">
🌐 IPFS Upload
</h1>
<p className="lead text-muted">
Upload files, folders, and URLs to the InterPlanetary File System using modern Helia technology
</p>
</Col>
</Row>
{/* Instance Configuration */}
<Row className="mb-4">
<Col md={8} className="mx-auto">
<Card className="shadow-sm">
<Card.Header className="bg-primary text-white">
<h5 className="mb-0">🔧 Instance Configuration</h5>
</Card.Header>
<Card.Body>
<Form>
<Row>
<Col md={6}>
<Form.Check
type="radio"
id="self-instance"
name="instanceType"
label="🏠 Self Instance"
checked={instanceType === 'self'}
onChange={() => setInstanceType('self')}
/>
</Col>
<Col md={6}>
<Form.Check
type="radio"
id="custom-instance"
name="instanceType"
label="🔗 Custom Instance"
checked={instanceType === 'custom'}
onChange={() => setInstanceType('custom')}
/>
</Col>
</Row>
{instanceType === 'custom' && (
<Row className="mt-3">
<Col>
<Form.Control
type="url"
placeholder="http(s)://instance.domain:port/api/v0"
value={customAddress}
onChange={(e) => setCustomAddress(e.target.value)}
/>
<Form.Text className="text-muted">
Must use client CORS.
<a href="https://addons.mozilla.org/firefox/addon/cors-everywhere/" target="_blank" rel="noopener noreferrer" className="ms-1">
🔧 Install CORS Extension
</a>
</Form.Text>
</Col>
</Row>
)}
<Row className="mt-3">
<Col>
<Form.Check
type="checkbox"
id="pinned"
label="📌 Persist content (pinned)"
checked={pinned}
onChange={(e) => handlePinnedChange(e.target.checked)}
/>
<Form.Text className="text-muted">
Pinned content will persist on the network and won't be garbage collected
</Form.Text>
</Col>
</Row>
</Form>
</Card.Body>
</Card>
</Col>
</Row>
{/* Upload Forms */}
<Row className="mb-4">
{/* File Upload */}
<Col md={4} className="mb-3">
<UploadCard
title="Upload Files"
icon="📁"
variant="success"
loading={loading}
onUpload={handleFileUpload}
disabled={!files || files.length === 0}
>
<Form.Control
ref={fileInputRef}
type="file"
multiple
onChange={(e) => setFiles(Array.from(e.target.files))}
className="mb-3"
/>
{files && files.length > 0 && (
<small className="text-muted">
{files.length} file(s) selected
</small>
)}
</UploadCard>
</Col>
{/* Folder Upload */}
<Col md={4} className="mb-3">
<UploadCard
title="Upload Folder"
icon="📂"
variant="danger"
loading={loading}
onUpload={handleFolderUpload}
disabled={!folderFiles || folderFiles.length === 0}
>
<Form.Control
ref={folderInputRef}
type="file"
webkitdirectory=""
directory=""
multiple
onChange={(e) => setFolderFiles(Array.from(e.target.files))}
className="mb-3"
/>
{folderFiles && folderFiles.length > 0 && (
<small className="text-muted">
{folderFiles.length} file(s) in folder
</small>
)}
</UploadCard>
</Col>
{/* URL Upload */}
<Col md={4} className="mb-3">
<UploadCard
title="Upload URL"
icon="🔗"
variant="warning"
loading={loading}
onUpload={handleUrlUpload}
disabled={!urlInput || !urlInput.trim()}
>
<Form.Control
type="url"
placeholder="http(s)://server.domain/...file"
value={urlInput}
onChange={(e) => setUrlInput(e.target.value)}
className="mb-3"
/>
<Form.Text className="text-muted">
Enter a direct link to a file
</Form.Text>
</UploadCard>
</Col>
</Row>
{/* Loading Indicator */}
{loading && (
<Row className="mb-4">
<Col className="text-center">
<div className="spinner-border text-primary" role="status" style={{width: '3rem', height: '3rem'}}>
<span className="visually-hidden">Loading...</span>
</div>
<p className="mt-3 text-muted">
<strong>Processing upload...</strong><br />
<small>This may take a moment depending on file size and network conditions</small>
</p>
</Col>
</Row>
)}
{/* Upload Results */}
{uploadedFiles.length > 0 && (
<Row className="mb-4">
<Col md={10} className="mx-auto">
<div className="d-flex justify-content-between align-items-center mb-3">
<h5 className="mb-0">📋 Upload Results</h5>
<Button
variant="outline-secondary"
size="sm"
onClick={clearResults}
>
🗑️ Clear Results
</Button>
</div>
<FileList
files={uploadedFiles}
title="Uploaded Files"
onCopy={showToastMessage}
/>
</Col>
</Row>
)}
{/* Footer */}
<Row className="mt-5">
<Col className="text-center">
<Alert variant="warning" className="d-inline-block">
<strong>⚠️ Warning:</strong> Uploaded content <strong>cannot</strong> be deleted from the IPFS network
</Alert>
<div className="mt-3 text-muted">
<p className="mb-1">
Made with ❤️ by <a href="https://manalejandro.com" target="_blank" rel="noopener noreferrer">ale</a>
</p>
<small>
Powered by <a href="https://helia.io/" target="_blank" rel="noopener noreferrer">Helia</a> -
The next generation IPFS implementation
</small>
</div>
</Col>
</Row>
{/* Modal */}
<Modal show={showModal} onHide={() => setShowModal(false)} centered>
<Modal.Header closeButton className="bg-info text-white">
<Modal.Title> Information</Modal.Title>
</Modal.Header>
<Modal.Body className="text-center py-4">
<p className="mb-0">{modalMessage}</p>
</Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={() => setShowModal(false)}>
Got it!
</Button>
</Modal.Footer>
</Modal>
{/* Toast Notifications */}
<ToastContainer position="top-end" className="p-3">
<Toast
show={showToast}
onClose={() => setShowToast(false)}
delay={4000}
autohide
bg={toastVariant}
>
<Toast.Header>
<strong className="me-auto">
{toastVariant === 'success' ? '' : ''} Notification
</strong>
</Toast.Header>
<Toast.Body className={toastVariant === 'success' ? 'text-white' : 'text-white'}>
{toastMessage}
</Toast.Body>
</Toast>
</ToastContainer>
</Container>
</div>
);
}
export default App;

8
src/App.test.js Archivo normal
Ver fichero

@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

84
src/components/FileList.js Archivo normal
Ver fichero

@@ -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 (
<Card className="shadow-sm">
<Card.Header className="bg-info text-white">
<h5 className="mb-0">📋 {title}</h5>
</Card.Header>
<Card.Body className="p-0">
<ListGroup variant="flush">
{files.map((file, index) => (
<ListGroup.Item key={index} className="d-flex justify-content-between align-items-start">
<div className="me-auto">
<div className="fw-bold d-flex align-items-center">
<span className="me-2">
{file.name === 'Folder' ? '📂' : '📄'}
</span>
{file.name}
</div>
<small className="text-muted">
{formatFileSize(file.size)}
</small>
{file.pinned && (
<Badge bg="success" className="ms-2">
📌 Pinned
</Badge>
)}
</div>
<div className="text-end">
<div className="d-flex gap-2 mb-2">
<Button
variant="outline-primary"
size="sm"
href={file.url}
target="_blank"
rel="noopener noreferrer"
>
🌐 View on IPFS
</Button>
<Button
variant="outline-secondary"
size="sm"
onClick={() => copyToClipboard(file.cid)}
title="Copy CID to clipboard"
>
📋 Copy CID
</Button>
</div>
<div>
<small className="text-muted font-monospace d-block text-truncate" style={{maxWidth: '200px'}}>
{file.cid}
</small>
</div>
</div>
</ListGroup.Item>
))}
</ListGroup>
</Card.Body>
</Card>
);
};
export default FileList;

42
src/components/UploadCard.js Archivo normal
Ver fichero

@@ -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 (
<Card className={`h-100 shadow-sm border-${variant}`}>
<Card.Header className={`bg-${variant} ${variant === 'warning' ? 'text-dark' : 'text-white'}`}>
<h6 className="mb-0">
{icon} {title}
</h6>
</Card.Header>
<Card.Body className="d-flex flex-column">
{children}
<Button
variant={variant}
onClick={onUpload}
disabled={loading || disabled}
className="mt-auto"
>
{loading ? (
<>
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
Uploading...
</>
) : (
`Upload ${title.split(' ')[1]}`
)}
</Button>
</Card.Body>
</Card>
);
};
export default UploadCard;

41
src/index.css Archivo normal
Ver fichero

@@ -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);
}

17
src/index.js Archivo normal
Ver fichero

@@ -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(
<React.StrictMode>
<App />
</React.StrictMode>
);
// 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();

1
src/logo.svg Archivo normal
Ver fichero

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Después

Anchura:  |  Altura:  |  Tamaño: 2.6 KiB

13
src/reportWebVitals.js Archivo normal
Ver fichero

@@ -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;

5
src/setupTests.js Archivo normal
Ver fichero

@@ -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';