Merge pull request #14 from wahyd4/images-style

Images style
This commit is contained in:
2024-11-17 17:02:43 +11:00
committed by GitHub
6 changed files with 458 additions and 91 deletions
BIN
View File
Binary file not shown.
+73 -8
View File
@@ -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()
+22 -16
View File
@@ -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>
+69 -62
View File
@@ -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 %}
+139
View File
@@ -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>
+155 -5
View File
@@ -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) {