10 changed files with 241 additions and 43 deletions
@ -0,0 +1,90 @@
@@ -0,0 +1,90 @@
|
||||
<script lang="ts"> |
||||
import { generateDarkPastelColor } from '$lib/utils/image_utils'; |
||||
import { fade } from 'svelte/transition'; |
||||
import { quintOut } from 'svelte/easing'; |
||||
|
||||
let { |
||||
src, |
||||
alt, |
||||
eventId, |
||||
className = 'w-full h-full object-cover', |
||||
placeholderClassName = '', |
||||
}: { |
||||
src: string; |
||||
alt: string; |
||||
eventId: string; |
||||
className?: string; |
||||
placeholderClassName?: string; |
||||
} = $props(); |
||||
|
||||
let imageLoaded = $state(false); |
||||
let imageError = $state(false); |
||||
let imgElement = $state<HTMLImageElement | null>(null); |
||||
|
||||
const placeholderColor = $derived.by(() => generateDarkPastelColor(eventId)); |
||||
|
||||
function loadImage() { |
||||
if (!imgElement) return; |
||||
|
||||
imgElement.onload = () => { |
||||
// Small delay to ensure smooth transition |
||||
setTimeout(() => { |
||||
imageLoaded = true; |
||||
}, 100); |
||||
}; |
||||
|
||||
imgElement.onerror = () => { |
||||
imageError = true; |
||||
}; |
||||
|
||||
// Set src after setting up event handlers |
||||
imgElement.src = src; |
||||
} |
||||
|
||||
function bindImg(element: HTMLImageElement) { |
||||
imgElement = element; |
||||
// Load image immediately when element is bound |
||||
loadImage(); |
||||
} |
||||
</script> |
||||
|
||||
<div class="relative w-full h-full"> |
||||
<!-- Placeholder --> |
||||
<div |
||||
class="absolute inset-0 {placeholderClassName}" |
||||
style="background-color: {placeholderColor};" |
||||
class:hidden={imageLoaded} |
||||
> |
||||
</div> |
||||
|
||||
<!-- Image --> |
||||
<img |
||||
bind:this={imgElement} |
||||
{src} |
||||
{alt} |
||||
class="{className} {imageLoaded ? 'opacity-100' : 'opacity-0'}" |
||||
style="transition: opacity 0.2s ease-out;" |
||||
loading="lazy" |
||||
decoding="async" |
||||
class:hidden={imageError} |
||||
onload={() => { |
||||
setTimeout(() => { |
||||
imageLoaded = true; |
||||
}, 100); |
||||
}} |
||||
onerror={() => { |
||||
imageError = true; |
||||
}} |
||||
/> |
||||
|
||||
<!-- Error state --> |
||||
{#if imageError} |
||||
<div |
||||
class="absolute inset-0 flex items-center justify-center bg-gray-200 dark:bg-gray-700 {placeholderClassName}" |
||||
> |
||||
<div class="text-gray-500 dark:text-gray-400 text-xs"> |
||||
Failed to load |
||||
</div> |
||||
</div> |
||||
{/if} |
||||
</div> |
||||
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
/** |
||||
* Generate a dark-pastel color based on a string (like an event ID) |
||||
* @param seed - The string to generate a color from |
||||
* @returns A dark-pastel hex color |
||||
*/ |
||||
export function generateDarkPastelColor(seed: string): string { |
||||
// Create a simple hash from the seed string
|
||||
let hash = 0; |
||||
for (let i = 0; i < seed.length; i++) { |
||||
const char = seed.charCodeAt(i); |
||||
hash = ((hash << 5) - hash) + char; |
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
} |
||||
|
||||
// Use the hash to generate lighter pastel colors
|
||||
// Keep values in the 120-200 range for better pastel effect
|
||||
const r = Math.abs(hash) % 80 + 120; // 120-200 range
|
||||
const g = Math.abs(hash >> 8) % 80 + 120; // 120-200 range
|
||||
const b = Math.abs(hash >> 16) % 80 + 120; // 120-200 range
|
||||
|
||||
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; |
||||
} |
||||
|
||||
/** |
||||
* Test function to verify color generation |
||||
* @param eventId - The event ID to test |
||||
* @returns The generated color |
||||
*/ |
||||
export function testColorGeneration(eventId: string): string { |
||||
return generateDarkPastelColor(eventId); |
||||
}
|
||||
Loading…
Reference in new issue