Optimize market images with pict-rs thumbnails
Market components were serving full-resolution images regardless of display size. Now uses pict-rs on-the-fly processing to serve WebP thumbnails at appropriate sizes: - ProductCard: 400px thumbnails (was full-res for 192px cards) - CartItem: 128px thumbnails (was full-res for 64px display) - CartSummary: 64px thumbnails (was full-res for 32px display) - CheckoutPage: 128px thumbnails (was full-res for 64px display) - StallView logo: 128px thumbnails (was full-res for 56px display) Adds useImageOptimizer composable wrapping ImageUploadService. Closes #8 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
667b8eebc2
commit
6e4449ac3d
6 changed files with 59 additions and 6 deletions
43
src/modules/base/composables/useImageOptimizer.ts
Normal file
43
src/modules/base/composables/useImageOptimizer.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { tryInjectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import type { ImageUploadService } from '../services/ImageUploadService'
|
||||
|
||||
/**
|
||||
* Composable for generating optimized image URLs via pict-rs
|
||||
* Handles both file aliases and full URLs
|
||||
*/
|
||||
export function useImageOptimizer() {
|
||||
const imageService = tryInjectService<ImageUploadService>(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE)
|
||||
|
||||
/**
|
||||
* Get a thumbnail URL (fast, lower quality - good for cards/lists)
|
||||
*/
|
||||
const thumbnail = (url: string | undefined, size = 256): string => {
|
||||
if (!url) return ''
|
||||
if (!imageService) return url
|
||||
return imageService.getThumbnailUrl(url, size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a resized URL (Lanczos2 filter - better quality for larger displays)
|
||||
*/
|
||||
const resized = (url: string | undefined, size = 800): string => {
|
||||
if (!url) return ''
|
||||
if (!imageService) return url
|
||||
return imageService.getResizedUrl(url, size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a blurred placeholder URL (for loading states)
|
||||
*/
|
||||
const blurred = (url: string | undefined, blur = 5): string => {
|
||||
if (!url) return ''
|
||||
if (!imageService) return url
|
||||
return imageService.getBlurredUrl(url, blur)
|
||||
}
|
||||
|
||||
return {
|
||||
thumbnail,
|
||||
resized,
|
||||
blurred
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
<!-- Product Image -->
|
||||
<div class="flex-shrink-0">
|
||||
<img
|
||||
:src="item.product.images?.[0] || '/placeholder-product.png'"
|
||||
:src="thumbnail(item.product.images?.[0], 128) || '/placeholder-product.png'"
|
||||
:alt="item.product.name"
|
||||
class="w-16 h-16 object-cover rounded-md"
|
||||
loading="lazy"
|
||||
|
|
@ -104,7 +104,7 @@
|
|||
<!-- Product Image -->
|
||||
<div class="flex-shrink-0">
|
||||
<img
|
||||
:src="item.product.images?.[0] || '/placeholder-product.png'"
|
||||
:src="thumbnail(item.product.images?.[0], 128) || '/placeholder-product.png'"
|
||||
:alt="item.product.name"
|
||||
class="w-16 h-16 object-cover rounded-md"
|
||||
loading="lazy"
|
||||
|
|
@ -203,12 +203,14 @@ import { Button } from '@/components/ui/button'
|
|||
import { Badge } from '@/components/ui/badge'
|
||||
import { Minus, Plus, Trash2 } from 'lucide-vue-next'
|
||||
import type { CartItem as CartItemType } from '../types/market'
|
||||
import { useImageOptimizer } from '@/modules/base/composables/useImageOptimizer'
|
||||
|
||||
interface Props {
|
||||
item: CartItemType
|
||||
stallId: string
|
||||
}
|
||||
|
||||
const { thumbnail } = useImageOptimizer()
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
>
|
||||
<div class="flex items-center space-x-3">
|
||||
<img
|
||||
:src="item.product.images?.[0] || '/placeholder-product.png'"
|
||||
:src="thumbnail(item.product.images?.[0], 64) || '/placeholder-product.png'"
|
||||
:alt="item.product.name"
|
||||
class="w-8 h-8 object-cover rounded"
|
||||
loading="lazy"
|
||||
|
|
@ -144,6 +144,7 @@ import { ref, computed } from 'vue'
|
|||
import { useRouter } from 'vue-router'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Shield } from 'lucide-vue-next'
|
||||
import { useImageOptimizer } from '@/modules/base/composables/useImageOptimizer'
|
||||
import type { ShippingZone } from '@/modules/market/stores/market'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -188,6 +189,7 @@ interface Props {
|
|||
}
|
||||
}
|
||||
|
||||
const { thumbnail } = useImageOptimizer()
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ import { Badge } from '@/components/ui/badge'
|
|||
import ProgressiveImage from '@/components/ui/image/ProgressiveImage.vue'
|
||||
import { ShoppingCart, Package, ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
||||
import type { Product } from '@/modules/market/stores/market'
|
||||
import { useImageOptimizer } from '@/modules/base/composables/useImageOptimizer'
|
||||
|
||||
interface Props {
|
||||
product: Product
|
||||
|
|
@ -171,6 +172,7 @@ const emit = defineEmits<{
|
|||
'view-stall': [stallId: string]
|
||||
}>()
|
||||
|
||||
const { thumbnail } = useImageOptimizer()
|
||||
const imageError = ref(false)
|
||||
const currentImageIndex = ref(0)
|
||||
|
||||
|
|
@ -188,7 +190,7 @@ const currentImage = computed(() => {
|
|||
if (productImages.value.length === 0) {
|
||||
return null
|
||||
}
|
||||
return productImages.value[currentImageIndex.value]
|
||||
return thumbnail(productImages.value[currentImageIndex.value], 400)
|
||||
})
|
||||
|
||||
// Image cycling methods
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@
|
|||
<div class="w-16 h-16 bg-muted rounded-lg flex items-center justify-center">
|
||||
<ProgressiveImage
|
||||
v-if="item.product.images?.[0]"
|
||||
:src="item.product.images[0]"
|
||||
:src="thumbnail(item.product.images[0], 128)"
|
||||
:alt="item.product.name"
|
||||
container-class="w-full h-full"
|
||||
image-class="w-full h-full object-cover rounded-lg"
|
||||
|
|
@ -291,11 +291,13 @@ import { Input } from '@/components/ui/input'
|
|||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import ProgressiveImage from '@/components/ui/image/ProgressiveImage.vue'
|
||||
import { useImageOptimizer } from '@/modules/base/composables/useImageOptimizer'
|
||||
import {
|
||||
Package,
|
||||
CheckCircle
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
const { thumbnail } = useImageOptimizer()
|
||||
const route = useRoute()
|
||||
const marketStore = useMarketStore()
|
||||
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
<div class="flex-shrink-0">
|
||||
<div v-if="stall?.logo" class="w-12 h-12 sm:w-14 sm:h-14 rounded-xl bg-gradient-to-br from-primary/20 to-accent/20 border-2 border-primary/20 shadow-lg overflow-hidden ring-2 ring-primary/10">
|
||||
<img
|
||||
:src="stall.logo"
|
||||
:src="thumbnail(stall.logo, 128)"
|
||||
:alt="stall.name"
|
||||
class="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
|
|
@ -157,7 +157,9 @@ import CartButton from '../components/CartButton.vue'
|
|||
import CategoryFilterBar from '../components/CategoryFilterBar.vue'
|
||||
import type { Product, Stall } from '../types/market'
|
||||
import type { FuzzySearchOptions } from '@/composables/useFuzzySearch'
|
||||
import { useImageOptimizer } from '@/modules/base/composables/useImageOptimizer'
|
||||
|
||||
const { thumbnail } = useImageOptimizer()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const marketStore = useMarketStore()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue