メインコンテンツまでスキップ

複数のアップローダー

同じページで複数の独立したアップロードインスタンスを使用します。

別々のアップロードゾーン

import { useDropup } from '@samithahansaka/dropup';

function MultipleUploadZones() {
// プロフィール画像アップローダー
const profilePic = useDropup({
accept: 'image/*',
maxFiles: 1,
multiple: false,
upload: { url: '/api/upload/profile' },
});

// カバー写真アップローダー
const coverPhoto = useDropup({
accept: 'image/*',
maxFiles: 1,
multiple: false,
maxWidth: 1920,
maxHeight: 1080,
upload: { url: '/api/upload/cover' },
});

// ドキュメントアップローダー
const documents = useDropup({
accept: '.pdf,.doc,.docx',
maxFiles: 10,
maxSize: 10 * 1024 * 1024,
upload: { url: '/api/upload/documents' },
});

return (
<div style={styles.container}>
{/* プロフィール画像 */}
<div style={styles.section}>
<h3>プロフィール画像</h3>
<div
{...profilePic.getDropProps()}
style={{
...styles.dropzone,
...styles.profileZone,
}}
>
<input {...profilePic.getInputProps()} />
{profilePic.files[0]?.preview ? (
<img
src={profilePic.files[0].preview}
alt="プロフィール"
style={styles.profilePreview}
/>
) : (
<span>プロフィール画像をドロップ</span>
)}
</div>
</div>

{/* カバー写真 */}
<div style={styles.section}>
<h3>カバー写真</h3>
<div
{...coverPhoto.getDropProps()}
style={{
...styles.dropzone,
...styles.coverZone,
}}
>
<input {...coverPhoto.getInputProps()} />
{coverPhoto.files[0]?.preview ? (
<img
src={coverPhoto.files[0].preview}
alt="カバー"
style={styles.coverPreview}
/>
) : (
<span>カバー写真をドロップ(最大1920x1080)</span>
)}
</div>
</div>

{/* ドキュメント */}
<div style={styles.section}>
<h3>ドキュメント</h3>
<div
{...documents.getDropProps()}
style={styles.dropzone}
>
<input {...documents.getInputProps()} />
<span>PDFまたはWordドキュメントをドロップ(最大10件)</span>
</div>

{documents.files.length > 0 && (
<ul style={styles.fileList}>
{documents.files.map(file => (
<li key={file.id}>
{file.name}
<button onClick={() => documents.actions.remove(file.id)}>×</button>
</li>
))}
</ul>
)}
</div>

{/* すべてアップロードボタン */}
<button
onClick={() => {
profilePic.actions.upload();
coverPhoto.actions.upload();
documents.actions.upload();
}}
disabled={
profilePic.state.isUploading ||
coverPhoto.state.isUploading ||
documents.state.isUploading
}
style={styles.uploadButton}
>
すべてアップロード
</button>
</div>
);
}

const styles = {
container: {
maxWidth: 600,
margin: '0 auto',
},
section: {
marginBottom: 30,
},
dropzone: {
border: '2px dashed #ccc',
borderRadius: 8,
padding: 20,
textAlign: 'center' as const,
cursor: 'pointer',
},
profileZone: {
width: 150,
height: 150,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
profilePreview: {
width: '100%',
height: '100%',
objectFit: 'cover' as const,
},
coverZone: {
height: 200,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
coverPreview: {
width: '100%',
height: '100%',
objectFit: 'cover' as const,
},
fileList: {
listStyle: 'none',
padding: 0,
marginTop: 10,
},
uploadButton: {
width: '100%',
padding: 16,
fontSize: 16,
backgroundColor: '#2196f3',
color: 'white',
border: 'none',
borderRadius: 8,
cursor: 'pointer',
},
};

カテゴリ別アップロード

import { useDropup } from '@samithahansaka/dropup';
import { useState } from 'react';

type Category = 'images' | 'videos' | 'documents';

function CategorizedUploader() {
const [activeCategory, setActiveCategory] = useState<Category>('images');

const uploaders = {
images: useDropup({
accept: 'image/*',
upload: { url: '/api/upload/images' },
}),
videos: useDropup({
accept: 'video/*',
maxSize: 100 * 1024 * 1024, // 100MB
upload: { url: '/api/upload/videos' },
}),
documents: useDropup({
accept: '.pdf,.doc,.docx,.xls,.xlsx',
upload: { url: '/api/upload/documents' },
}),
};

const current = uploaders[activeCategory];

const getCounts = () => ({
images: uploaders.images.files.length,
videos: uploaders.videos.files.length,
documents: uploaders.documents.files.length,
});

const counts = getCounts();

return (
<div style={styles.container}>
{/* カテゴリタブ */}
<div style={styles.tabs}>
{(['images', 'videos', 'documents'] as Category[]).map(cat => (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
style={{
...styles.tab,
...(activeCategory === cat ? styles.activeTab : {}),
}}
>
{cat === 'images' ? '画像' : cat === 'videos' ? '動画' : 'ドキュメント'}
{counts[cat] > 0 && (
<span style={styles.badge}>{counts[cat]}</span>
)}
</button>
))}
</div>

{/* ドロップゾーン */}
<div
{...current.getDropProps()}
style={{
...styles.dropzone,
borderColor: current.state.isDragging ? '#2196f3' : '#ccc',
}}
>
<input {...current.getInputProps()} />
<p>{activeCategory === 'images' ? '画像' : activeCategory === 'videos' ? '動画' : 'ドキュメント'}をここにドロップ</p>
</div>

{/* ファイルリスト */}
<div style={styles.fileList}>
{current.files.map(file => (
<div key={file.id} style={styles.fileItem}>
{file.preview && (
<img src={file.preview} alt="" style={styles.thumb} />
)}
<span style={styles.fileName}>{file.name}</span>
<span style={styles.status}>{file.status}</span>
<button onClick={() => current.actions.remove(file.id)}>×</button>
</div>
))}
</div>

{/* アクション */}
<div style={styles.actions}>
<button
onClick={() => current.actions.upload()}
disabled={current.state.isUploading || current.files.length === 0}
>
{activeCategory === 'images' ? '画像' : activeCategory === 'videos' ? '動画' : 'ドキュメント'}をアップロード
</button>
<button
onClick={() => {
Object.values(uploaders).forEach(u => u.actions.upload());
}}
>
すべてのカテゴリをアップロード
</button>
</div>
</div>
);
}

const styles = {
container: {
maxWidth: 600,
margin: '0 auto',
},
tabs: {
display: 'flex',
gap: 8,
marginBottom: 20,
},
tab: {
padding: '10px 20px',
border: '1px solid #ccc',
backgroundColor: 'white',
cursor: 'pointer',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
gap: 8,
},
activeTab: {
backgroundColor: '#2196f3',
color: 'white',
borderColor: '#2196f3',
},
badge: {
backgroundColor: 'rgba(0,0,0,0.2)',
padding: '2px 8px',
borderRadius: 10,
fontSize: 12,
},
dropzone: {
border: '2px dashed',
borderRadius: 8,
padding: 40,
textAlign: 'center' as const,
marginBottom: 20,
},
fileList: {
marginBottom: 20,
},
fileItem: {
display: 'flex',
alignItems: 'center',
gap: 12,
padding: 8,
borderBottom: '1px solid #eee',
},
thumb: {
width: 40,
height: 40,
objectFit: 'cover' as const,
borderRadius: 4,
},
fileName: {
flex: 1,
},
status: {
color: '#666',
fontSize: 12,
},
actions: {
display: 'flex',
gap: 12,
},
};

複数アップロード付きフォーム

import { useDropup } from '@samithahansaka/dropup';
import { useState } from 'react';

function ProductForm() {
const [formData, setFormData] = useState({
name: '',
description: '',
price: '',
});

const mainImage = useDropup({
accept: 'image/*',
maxFiles: 1,
multiple: false,
});

const gallery = useDropup({
accept: 'image/*',
maxFiles: 5,
});

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

// まず画像をアップロード
await Promise.all([
mainImage.actions.upload(),
gallery.actions.upload(),
]);

// 次にアップロードされたURLでフォームを送信
const productData = {
...formData,
mainImage: mainImage.files[0]?.uploadedUrl,
gallery: gallery.files.map(f => f.uploadedUrl).filter(Boolean),
};

await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(productData),
});
};

return (
<form onSubmit={handleSubmit} style={styles.form}>
<h2>商品を追加</h2>

{/* テキストフィールド */}
<div style={styles.field}>
<label>商品名</label>
<input
type="text"
value={formData.name}
onChange={e => setFormData(d => ({ ...d, name: e.target.value }))}
style={styles.input}
/>
</div>

<div style={styles.field}>
<label>説明</label>
<textarea
value={formData.description}
onChange={e => setFormData(d => ({ ...d, description: e.target.value }))}
style={styles.textarea}
/>
</div>

<div style={styles.field}>
<label>価格</label>
<input
type="number"
value={formData.price}
onChange={e => setFormData(d => ({ ...d, price: e.target.value }))}
style={styles.input}
/>
</div>

{/* メイン画像 */}
<div style={styles.field}>
<label>メイン画像</label>
<div
{...mainImage.getDropProps()}
style={styles.imageUpload}
>
<input {...mainImage.getInputProps()} />
{mainImage.files[0]?.preview ? (
<img
src={mainImage.files[0].preview}
alt="メイン"
style={styles.previewImage}
/>
) : (
<span>メイン商品画像をドロップ</span>
)}
</div>
</div>

{/* ギャラリー */}
<div style={styles.field}>
<label>ギャラリー画像(最大5枚)</label>
<div
{...gallery.getDropProps()}
style={styles.galleryUpload}
>
<input {...gallery.getInputProps()} />
<span>ギャラリー画像をドロップ</span>
</div>

{gallery.files.length > 0 && (
<div style={styles.galleryPreview}>
{gallery.files.map(file => (
<div key={file.id} style={styles.galleryItem}>
{file.preview && (
<img src={file.preview} alt="" style={styles.galleryThumb} />
)}
<button
type="button"
onClick={() => gallery.actions.remove(file.id)}
style={styles.removeBtn}
>
×
</button>
</div>
))}
</div>
)}
</div>

<button
type="submit"
disabled={
mainImage.state.isUploading ||
gallery.state.isUploading
}
style={styles.submitBtn}
>
{mainImage.state.isUploading || gallery.state.isUploading
? 'アップロード中...'
: '商品を追加'}
</button>
</form>
);
}

const styles = {
form: {
maxWidth: 500,
margin: '0 auto',
},
field: {
marginBottom: 20,
},
input: {
width: '100%',
padding: 10,
border: '1px solid #ccc',
borderRadius: 4,
},
textarea: {
width: '100%',
padding: 10,
border: '1px solid #ccc',
borderRadius: 4,
minHeight: 100,
},
imageUpload: {
border: '2px dashed #ccc',
borderRadius: 8,
padding: 20,
textAlign: 'center' as const,
height: 200,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
previewImage: {
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain' as const,
},
galleryUpload: {
border: '2px dashed #ccc',
borderRadius: 8,
padding: 20,
textAlign: 'center' as const,
},
galleryPreview: {
display: 'flex',
gap: 8,
marginTop: 12,
flexWrap: 'wrap' as const,
},
galleryItem: {
position: 'relative' as const,
},
galleryThumb: {
width: 80,
height: 80,
objectFit: 'cover' as const,
borderRadius: 4,
},
removeBtn: {
position: 'absolute' as const,
top: -8,
right: -8,
width: 24,
height: 24,
border: 'none',
borderRadius: '50%',
backgroundColor: '#f44336',
color: 'white',
cursor: 'pointer',
},
submitBtn: {
width: '100%',
padding: 16,
backgroundColor: '#2196f3',
color: 'white',
border: 'none',
borderRadius: 8,
fontSize: 16,
cursor: 'pointer',
},
};

アップローダー間の状態共有

import { useDropup } from '@samithahansaka/dropup';
import { useMemo } from 'react';

function SharedStateUploaders() {
const uploader1 = useDropup({ accept: 'image/*' });
const uploader2 = useDropup({ accept: 'video/*' });
const uploader3 = useDropup({ accept: '.pdf' });

// すべてのアップローダーの統計を結合
const combinedStats = useMemo(() => {
const allFiles = [
...uploader1.files,
...uploader2.files,
...uploader3.files,
];

return {
totalFiles: allFiles.length,
totalSize: allFiles.reduce((sum, f) => sum + f.size, 0),
uploading: allFiles.filter(f => f.status === 'uploading').length,
complete: allFiles.filter(f => f.status === 'complete').length,
isAnyUploading:
uploader1.state.isUploading ||
uploader2.state.isUploading ||
uploader3.state.isUploading,
};
}, [uploader1.files, uploader2.files, uploader3.files]);

return (
<div>
{/* 統計バナー */}
<div style={styles.stats}>
<span>合計: {combinedStats.totalFiles}ファイル</span>
<span>サイズ: {(combinedStats.totalSize / 1024 / 1024).toFixed(1)} MB</span>
<span>アップロード中: {combinedStats.uploading}</span>
<span>完了: {combinedStats.complete}</span>
</div>

{/* アップロードゾーン */}
<div style={styles.zones}>
<UploadZone
label="画像"
uploader={uploader1}
icon="🖼️"
/>
<UploadZone
label="動画"
uploader={uploader2}
icon="🎬"
/>
<UploadZone
label="PDF"
uploader={uploader3}
icon="📄"
/>
</div>

{/* グローバルアップロードボタン */}
<button
onClick={() => {
uploader1.actions.upload();
uploader2.actions.upload();
uploader3.actions.upload();
}}
disabled={combinedStats.isAnyUploading}
style={styles.globalButton}
>
すべてアップロード({combinedStats.totalFiles}ファイル)
</button>
</div>
);
}

function UploadZone({
label,
uploader,
icon,
}: {
label: string;
uploader: ReturnType<typeof useDropup>;
icon: string;
}) {
return (
<div style={styles.zone}>
<div {...uploader.getDropProps()} style={styles.dropzone}>
<input {...uploader.getInputProps()} />
<span style={styles.icon}>{icon}</span>
<span>{label}</span>
<span style={styles.count}>{uploader.files.length}</span>
</div>
</div>
);
}

const styles = {
stats: {
display: 'flex',
gap: 20,
padding: 16,
backgroundColor: '#f5f5f5',
borderRadius: 8,
marginBottom: 20,
},
zones: {
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: 16,
marginBottom: 20,
},
zone: {
flex: 1,
},
dropzone: {
border: '2px dashed #ccc',
borderRadius: 8,
padding: 30,
textAlign: 'center' as const,
display: 'flex',
flexDirection: 'column' as const,
alignItems: 'center',
gap: 8,
},
icon: {
fontSize: 32,
},
count: {
backgroundColor: '#2196f3',
color: 'white',
padding: '2px 8px',
borderRadius: 10,
fontSize: 12,
},
globalButton: {
width: '100%',
padding: 16,
fontSize: 16,
backgroundColor: '#4caf50',
color: 'white',
border: 'none',
borderRadius: 8,
cursor: 'pointer',
},
};