← スキル一覧に戻る

media-storage
by Smarter-Poker
Smarter-Poker-World-Hub
⭐ 0🍴 0📅 2026年1月26日
SKILL.md
name: Media & Storage description: Handle image/video uploads, optimization, and Supabase storage
Media & Storage Skill
Overview
Handle all media uploads, optimization, and storage using Supabase Storage.
Storage Buckets
| Bucket | Purpose | Public |
|---|---|---|
avatars | User profile pictures | Yes |
posts | Social media images/videos | Yes |
cards | Playing card assets | Yes |
private | User documents | No |
Image Upload
async function uploadImage(file, bucket, path) {
// Validate file
const maxSize = 5 * 1024 * 1024; // 5MB
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
if (file.size > maxSize) {
throw new Error('File too large (max 5MB)');
}
if (!allowedTypes.includes(file.type)) {
throw new Error('Invalid file type');
}
// Generate unique filename
const ext = file.name.split('.').pop();
const filename = `${Date.now()}_${crypto.randomUUID()}.${ext}`;
const fullPath = `${path}/${filename}`;
// Upload
const { data, error } = await supabase.storage
.from(bucket)
.upload(fullPath, file, {
cacheControl: '3600',
upsert: false
});
if (error) throw error;
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from(bucket)
.getPublicUrl(fullPath);
return publicUrl;
}
Image Optimization
async function optimizeImage(file, maxWidth = 1200, quality = 0.8) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const scale = Math.min(1, maxWidth / img.width);
canvas.width = img.width * scale;
canvas.height = img.height * scale;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob(resolve, 'image/webp', quality);
};
img.src = URL.createObjectURL(file);
});
}
Avatar Upload with Crop
async function uploadAvatar(userId, file) {
// Optimize to square 400x400
const optimized = await cropToSquare(file, 400);
// Upload
const url = await uploadImage(optimized, 'avatars', userId);
// Update profile
await supabase.from('profiles')
.update({ avatar_url: url })
.eq('id', userId);
return url;
}
Video Upload
async function uploadVideo(file, onProgress) {
const maxSize = 100 * 1024 * 1024; // 100MB
const allowedTypes = ['video/mp4', 'video/webm', 'video/quicktime'];
if (file.size > maxSize) {
throw new Error('Video too large (max 100MB)');
}
if (!allowedTypes.includes(file.type)) {
throw new Error('Invalid video type');
}
const filename = `${Date.now()}_${crypto.randomUUID()}.mp4`;
// Use tus for resumable uploads
const { data, error } = await supabase.storage
.from('posts')
.upload(`videos/${filename}`, file, {
cacheControl: '3600',
upsert: false
});
return supabase.storage.from('posts').getPublicUrl(`videos/${filename}`).data.publicUrl;
}
Image Component with Lazy Loading
import Image from 'next/image';
function OptimizedImage({ src, alt, width, height, ...props }) {
const [isLoaded, setIsLoaded] = useState(false);
return (
<div className={`image-wrapper ${isLoaded ? 'loaded' : 'loading'}`}>
<Image
src={src}
alt={alt}
width={width}
height={height}
onLoadingComplete={() => setIsLoaded(true)}
placeholder="blur"
blurDataURL="/placeholder.png"
{...props}
/>
</div>
);
}
Delete Media
async function deleteMedia(bucket, path) {
const { error } = await supabase.storage
.from(bucket)
.remove([path]);
if (error) throw error;
}
Components
ImageUploader.jsx- Drag & drop uploadAvatarCropper.jsx- Crop to squareVideoPlayer.jsx- Custom video playerMediaGallery.jsx- Grid display
スコア
総合スコア
50/100
リポジトリの品質指標に基づく評価
✓SKILL.md
SKILL.mdファイルが含まれている
+20
○LICENSE
ライセンスが設定されている
0/10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
レビュー
💬
レビュー機能は近日公開予定です