mirror of
https://github.com/wahyd4/links.git
synced 2026-08-09 05:06:16 +10:00
Binary file not shown.
+73
-8
@@ -1,29 +1,51 @@
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from django.shortcuts import get_object_or_404
|
||||
from .models import ImageCollection, Image
|
||||
from .serializers import ImageCollectionSerializer, ImageSerializer
|
||||
from .storage import R2Storage
|
||||
import uuid
|
||||
import logging
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ImageCollectionViewSet(viewsets.ModelViewSet):
|
||||
queryset = ImageCollection.objects.all()
|
||||
serializer_class = ImageCollectionSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Create a new collection"""
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(
|
||||
{
|
||||
'status': 'success',
|
||||
'message': 'Collection created successfully',
|
||||
'data': serializer.data
|
||||
},
|
||||
status=status.HTTP_201_CREATED,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def upload_images(self, request, pk=None):
|
||||
"""API endpoint to upload images to a collection"""
|
||||
"""Upload images to a collection"""
|
||||
collection = self.get_object()
|
||||
files = request.FILES.getlist('file')
|
||||
storage = R2Storage()
|
||||
|
||||
logger.debug(f"Processing upload request for collection {collection.id}")
|
||||
logger.debug(f"Number of files: {len(files)}")
|
||||
|
||||
uploaded_images = []
|
||||
for file in files:
|
||||
try:
|
||||
logger.info(os.environ.get('R2_ENDPOINT_URL'))
|
||||
logger.debug(f"Processing file: {file.name}")
|
||||
logger.debug(f"File size: {file.size}")
|
||||
logger.debug(f"Content type: {file.content_type}")
|
||||
|
||||
# Generate unique file key
|
||||
file_key = f"images/{collection.id}/{uuid.uuid4()}/{file.name}"
|
||||
@@ -51,25 +73,68 @@ class ImageCollectionViewSet(viewsets.ModelViewSet):
|
||||
size=file.size
|
||||
)
|
||||
|
||||
logger.debug(f"Created image record: {image.id}")
|
||||
uploaded_images.append(ImageSerializer(image).data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Upload failed for {file.name}", exc_info=True)
|
||||
return Response({
|
||||
'error': str(e)
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(uploaded_images, status=status.HTTP_201_CREATED)
|
||||
return Response({
|
||||
'status': 'success',
|
||||
'message': f'Successfully uploaded {len(uploaded_images)} images',
|
||||
'data': uploaded_images
|
||||
}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@action(detail=True, methods=['delete'])
|
||||
def delete_image(self, request, pk=None):
|
||||
"""Delete an image from a collection"""
|
||||
collection = self.get_object()
|
||||
image_id = request.data.get('image_id')
|
||||
|
||||
try:
|
||||
image = collection.images.get(id=image_id)
|
||||
storage = R2Storage()
|
||||
|
||||
# Delete from storage
|
||||
if image.file_key:
|
||||
try:
|
||||
storage.delete_file(image.file_key)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete from storage: {e}")
|
||||
|
||||
# Delete from database
|
||||
image.delete()
|
||||
|
||||
return Response({
|
||||
'status': 'success',
|
||||
'message': 'Image deleted successfully'
|
||||
})
|
||||
|
||||
except Image.DoesNotExist:
|
||||
return Response({
|
||||
'status': 'error',
|
||||
'message': 'Image not found'
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
return Response({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
class ImageViewSet(viewsets.ModelViewSet):
|
||||
queryset = Image.objects.all()
|
||||
serializer_class = ImageSerializer
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
# Delete from R2 before deleting record
|
||||
"""Delete image from storage when deleting record"""
|
||||
storage = R2Storage()
|
||||
try:
|
||||
storage.delete_file(instance.file_key)
|
||||
if instance.file_key:
|
||||
storage.delete_file(instance.file_key)
|
||||
except Exception as e:
|
||||
pass # Continue with deletion even if R2 delete fails
|
||||
logger.error(f"Failed to delete from storage: {e}")
|
||||
instance.delete()
|
||||
|
||||
@@ -35,48 +35,54 @@
|
||||
</div>
|
||||
|
||||
<!-- Collection Header -->
|
||||
<div class="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">{{ collection.name }}</h1>
|
||||
<div class="flex flex-col space-y-4 sm:space-y-0 sm:flex-row sm:justify-between sm:items-start mb-6">
|
||||
<!-- Title and Description -->
|
||||
<div class="flex-1">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold text-gray-900">{{ collection.name }}</h1>
|
||||
{% if collection.description %}
|
||||
<p class="mt-2 text-gray-600">{{ collection.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-wrap gap-2 sm:space-x-2">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('uploadModal').classList.remove('hidden')"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
class="inline-flex items-center px-3 py-2 sm:px-4 sm:py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700">
|
||||
<svg class="w-5 h-5 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
|
||||
</svg>
|
||||
{% trans "Upload Images" %}
|
||||
<span class="hidden sm:inline">{% trans "Upload Images" %}</span>
|
||||
</button>
|
||||
|
||||
<a href="{% url 'collection-update' collection.pk %}"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
class="inline-flex items-center px-3 py-2 sm:px-4 sm:py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50">
|
||||
<svg class="w-5 h-5 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
{% trans "Edit Collection" %}
|
||||
<span class="hidden sm:inline">{% trans "Edit Collection" %}</span>
|
||||
</a>
|
||||
|
||||
<button type="button"
|
||||
onclick="deleteCollection('{{ collection.pk }}')"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
class="inline-flex items-center px-3 py-2 sm:px-4 sm:py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700">
|
||||
<svg class="w-5 h-5 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
{% trans "Delete Collection" %}
|
||||
<span class="hidden sm:inline">{% trans "Delete Collection" %}</span>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
onclick="window.location.href='{% url 'collection-slideshow' collection.pk %}'"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
class="inline-flex items-center px-3 py-2 sm:px-4 sm:py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700">
|
||||
<svg class="w-5 h-5 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
{% trans "Slideshow" %}
|
||||
<span class="hidden sm:inline">{% trans "Slideshow" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">{% trans "Image Collections" %}</h1>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-8">
|
||||
<div class="flex justify-between items-center mb-4 sm:mb-6">
|
||||
<h1 class="text-xl sm:text-2xl font-bold text-gray-900">{% trans "Image Collections" %}</h1>
|
||||
<a href="{% url 'collection-create' %}"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
class="inline-flex items-center px-3 py-1.5 sm:px-4 sm:py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700">
|
||||
<svg class="w-4 h-4 sm:w-5 sm:h-5 mr-1.5 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
{% trans "New Collection" %}
|
||||
@@ -16,66 +16,73 @@
|
||||
</div>
|
||||
|
||||
<!-- Collections Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-x-8 gap-y-10">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6 md:gap-8">
|
||||
{% for collection in collections %}
|
||||
<div class="group relative bg-white rounded-lg shadow-sm overflow-hidden hover:shadow-md transition-all duration-200">
|
||||
<a href="{% url 'collection-detail' collection.pk %}"
|
||||
class="block">
|
||||
<!-- Collection Preview (first 4 images) -->
|
||||
<div class="aspect-w-16 aspect-h-12 bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
<div class="grid grid-cols-2 gap-2 p-3">
|
||||
{% with images=collection.images.all|slice:":4" %}
|
||||
{% for image in images %}
|
||||
<div class="aspect-w-1 aspect-h-1 overflow-hidden rounded-lg bg-gray-200 shadow-sm">
|
||||
<img src="{{ image.get_url }}"
|
||||
alt="{{ image.title }}"
|
||||
class="object-cover w-full h-full">
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-span-2 flex flex-col items-center justify-center h-full bg-gradient-to-br from-gray-50 to-gray-100 rounded-lg border-2 border-dashed border-gray-200">
|
||||
<svg class="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<p class="mt-2 text-sm text-gray-500">{% trans "No images yet" %}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
<div class="group relative w-full">
|
||||
<!-- Fixed size container -->
|
||||
<div class="bg-white rounded-lg shadow-sm overflow-hidden hover:shadow-md transition-all duration-200 h-[280px] sm:h-[320px]">
|
||||
<a href="{% url 'collection-detail' collection.pk %}" class="block h-full">
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Image Preview Container - Fixed Height -->
|
||||
<div class="h-[160px] sm:h-[200px] bg-gradient-to-br from-gray-50 to-gray-100 p-2 sm:p-3">
|
||||
<div class="grid grid-cols-2 gap-1.5 sm:gap-2 h-full">
|
||||
{% with images=collection.images.all|slice:":4" %}
|
||||
{% for image in images %}
|
||||
<div class="aspect-w-1 aspect-h-1 overflow-hidden rounded-lg bg-gray-200 shadow-sm
|
||||
{% if forloop.counter > 2 %}hidden sm:block{% endif %}">
|
||||
<img src="{{ image.get_url }}"
|
||||
alt="{{ image.title }}"
|
||||
class="object-cover w-full h-full">
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-span-2 flex flex-col items-center justify-center h-full bg-gradient-to-br from-gray-50 to-gray-100 rounded-lg border-2 border-dashed border-gray-200">
|
||||
<svg class="w-8 h-8 sm:w-12 sm:h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<p class="mt-1 sm:mt-2 text-xs sm:text-sm text-gray-500">{% trans "No images yet" %}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Collection Info - Flex Grow to Fill Remaining Space -->
|
||||
<div class="flex-1 p-3 sm:p-4 flex flex-col">
|
||||
<h3 class="text-base sm:text-lg font-medium text-gray-900 group-hover:text-blue-600 transition-colors duration-200 line-clamp-1">
|
||||
{{ collection.name }}
|
||||
</h3>
|
||||
<p class="mt-0.5 text-xs sm:text-sm text-gray-500">
|
||||
{{ collection.images.count }} {% trans "images" %}
|
||||
</p>
|
||||
{% if collection.description %}
|
||||
<p class="mt-1 text-xs sm:text-sm text-gray-600 line-clamp-2 flex-1">
|
||||
{{ collection.description }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Collection Info -->
|
||||
<div class="p-4">
|
||||
<h3 class="text-lg font-medium text-gray-900 group-hover:text-blue-600 transition-colors duration-200">
|
||||
{{ collection.name }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
{{ collection.images.count }} {% trans "images" %}
|
||||
</p>
|
||||
{% if collection.description %}
|
||||
<p class="mt-2 text-sm text-gray-600 line-clamp-2">{{ collection.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="absolute top-2 right-2 flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||
<a href="{% url 'collection-update' collection.pk %}"
|
||||
class="p-1.5 bg-white text-gray-600 hover:text-blue-600 rounded-full hover:bg-blue-50 shadow-sm transition-colors duration-200"
|
||||
title="{% trans 'Edit Collection' %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<button onclick="deleteCollection('{{ collection.pk }}'); event.preventDefault();"
|
||||
class="p-1.5 bg-white text-gray-600 hover:text-red-600 rounded-full hover:bg-red-50 shadow-sm transition-colors duration-200"
|
||||
title="{% trans 'Delete Collection' %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="absolute top-1.5 sm:top-2 right-1.5 sm:right-2 flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||
<a href="{% url 'collection-update' collection.pk %}"
|
||||
class="p-1 sm:p-1.5 bg-white text-gray-600 hover:text-blue-600 rounded-full hover:bg-blue-50 shadow-sm transition-colors duration-200"
|
||||
title="{% trans 'Edit Collection' %}">
|
||||
<svg class="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<button onclick="deleteCollection('{{ collection.pk }}'); event.preventDefault();"
|
||||
class="p-1 sm:p-1.5 bg-white text-gray-600 hover:text-red-600 rounded-full hover:bg-red-50 shadow-sm transition-colors duration-200"
|
||||
title="{% trans 'Delete Collection' %}">
|
||||
<svg class="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
|
||||
@@ -213,6 +213,145 @@ curl -X POST http://localhost:8000/api/newsletters \
|
||||
}'
|
||||
```
|
||||
|
||||
## Collection API Usage
|
||||
|
||||
GoLinks provides RESTful APIs for managing image collections. Here are the available endpoints and their usage:
|
||||
|
||||
### List Collections
|
||||
|
||||
Retrieve a paginated list of all collections:
|
||||
```
|
||||
GET /api/collections
|
||||
GET /api/collections?page=2
|
||||
GET /api/collections?page_size=20
|
||||
```
|
||||
Response example:
|
||||
```json
|
||||
{
|
||||
"count": 10,
|
||||
"next": "http://localhost:8000/api/collections?page=2",
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "My Collection",
|
||||
"description": "A collection of images",
|
||||
"image_count": 5,
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
},
|
||||
// ... more collections
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Create a Collection
|
||||
|
||||
Create a new image collection:
|
||||
```
|
||||
POST /api/collections
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "My Collection",
|
||||
"description": "A collection of images"
|
||||
}
|
||||
```
|
||||
Response example:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Collection created successfully",
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "My Collection",
|
||||
"description": "A collection of images",
|
||||
"image_count": 0,
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Upload Images to Collection
|
||||
|
||||
Upload one or multiple images to a collection:
|
||||
```
|
||||
POST /api/collections/{collection_id}/upload_images
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: image1.jpg
|
||||
file: image2.jpg
|
||||
```
|
||||
Response example:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Successfully uploaded 2 images",
|
||||
"data": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"title": "image1.jpg",
|
||||
"content_type": "image/jpeg",
|
||||
"size": 1024000,
|
||||
"url": "https://your-r2-domain.com/images/...",
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440002",
|
||||
"title": "image2.jpg",
|
||||
"content_type": "image/jpeg",
|
||||
"size": 2048000,
|
||||
"url": "https://your-r2-domain.com/images/...",
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Delete an Image
|
||||
|
||||
Delete a specific image from a collection:
|
||||
```
|
||||
DELETE /api/images/{image_id}
|
||||
```
|
||||
Response example:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Image deleted successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Delete a Collection
|
||||
|
||||
Delete a collection and all its images:
|
||||
```
|
||||
DELETE /api/collections/{collection_id}
|
||||
```
|
||||
Response example:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Collection deleted successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Error Responses
|
||||
|
||||
In case of errors, the API will return appropriate status codes and error messages:
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Error description here"
|
||||
}
|
||||
```
|
||||
|
||||
Common status codes:
|
||||
- 200: Success
|
||||
- 201: Created
|
||||
- 400: Bad Request
|
||||
- 404: Not Found
|
||||
- 500: Internal Server Error
|
||||
|
||||
{% endfilter %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Vendored
+155
-5
@@ -645,6 +645,14 @@ video {
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.right-1\.5 {
|
||||
right: 0.375rem;
|
||||
}
|
||||
|
||||
.top-1\.5 {
|
||||
top: 0.375rem;
|
||||
}
|
||||
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
@@ -767,6 +775,10 @@ video {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.mt-0\.5 {
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.line-clamp-2 {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
@@ -781,6 +793,13 @@ video {
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
|
||||
.line-clamp-1 {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
@@ -853,6 +872,18 @@ video {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.h-8 {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.h-\[160px\] {
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.h-\[280px\] {
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.w-12 {
|
||||
width: 3rem;
|
||||
}
|
||||
@@ -889,6 +920,10 @@ video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-8 {
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.min-w-0 {
|
||||
min-width: 0px;
|
||||
}
|
||||
@@ -989,6 +1024,10 @@ video {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
@@ -1021,6 +1060,10 @@ video {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.gap-1\.5 {
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.gap-x-4 {
|
||||
-moz-column-gap: 1rem;
|
||||
column-gap: 1rem;
|
||||
@@ -1291,6 +1334,11 @@ video {
|
||||
background-color: rgb(34 197 94 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-green-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-purple-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(243 232 255 / var(--tw-bg-opacity));
|
||||
@@ -1340,11 +1388,6 @@ video {
|
||||
background-color: rgb(254 249 195 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-green-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-opacity-50 {
|
||||
--tw-bg-opacity: 0.5;
|
||||
}
|
||||
@@ -1494,6 +1537,11 @@ video {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.py-1\.5 {
|
||||
padding-top: 0.375rem;
|
||||
padding-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.pl-3 {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
@@ -2121,6 +2169,14 @@ video {
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:right-2 {
|
||||
right: 0.5rem;
|
||||
}
|
||||
|
||||
.sm\:top-2 {
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.sm\:col-span-1 {
|
||||
grid-column: span 1 / span 1;
|
||||
}
|
||||
@@ -2150,6 +2206,22 @@ video {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.sm\:mb-6 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.sm\:mr-2 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.sm\:mt-1 {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.sm\:mt-2 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.sm\:block {
|
||||
display: block;
|
||||
}
|
||||
@@ -2170,6 +2242,22 @@ video {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.sm\:h-12 {
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.sm\:h-5 {
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.sm\:h-\[200px\] {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.sm\:h-\[320px\] {
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
.sm\:w-10 {
|
||||
width: 2.5rem;
|
||||
}
|
||||
@@ -2182,6 +2270,14 @@ video {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.sm\:w-12 {
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.sm\:w-5 {
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
.sm\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -2214,6 +2310,14 @@ video {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sm\:gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sm\:gap-6 {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.sm\:space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
||||
@@ -2244,6 +2348,18 @@ video {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.sm\:p-1\.5 {
|
||||
padding: 0.375rem;
|
||||
}
|
||||
|
||||
.sm\:p-3 {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.sm\:p-4 {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.sm\:px-6 {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
@@ -2259,6 +2375,21 @@ video {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.sm\:px-4 {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.sm\:py-2 {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sm\:py-8 {
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.sm\:text-left {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -2272,6 +2403,21 @@ video {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.sm\:text-2xl {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.sm\:text-lg {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.sm\:text-3xl {
|
||||
font-size: 1.875rem;
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@@ -2290,6 +2436,10 @@ video {
|
||||
.md\:grid-cols-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.md\:gap-8 {
|
||||
gap: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
|
||||
Reference in New Issue
Block a user