feat(course): crop cover image to 280x200 on upload

The course cover Upload tab stored the raw file and relied on CSS
object-cover to fake the 280x200 frame at display time. Route uploads
through the existing ImageCropper at a 280/200 aspect ratio so the saved
asset matches the frame. Also teach the cropper UploadTrigger to accept
drag-and-drop so the prior drop-to-upload UX is preserved.
This commit is contained in:
Rotimi Best
2026-06-16 23:50:55 +01:00
parent a3a3c5bf48
commit 219ed2eb92
2 changed files with 82 additions and 26 deletions
@@ -3,11 +3,13 @@
import type { ImageCropperUploadTriggerProps } from './types';
import { cn } from '../../tools';
let { ref = $bindable(null), children, ...rest }: ImageCropperUploadTriggerProps = $props();
let { ref = $bindable(null), class: className, children, ...rest }: ImageCropperUploadTriggerProps = $props();
const triggerState = useImageCropperTrigger();
const isDisabled = $derived(triggerState.rootState.disabled);
let isDragging = $state(false);
function handleClick(e: MouseEvent) {
if (isDisabled) {
e.preventDefault();
@@ -15,6 +17,28 @@
return false;
}
}
function handleDragOver(e: DragEvent) {
if (isDisabled) return;
e.preventDefault();
isDragging = true;
}
function handleDragLeave() {
isDragging = false;
}
function handleDrop(e: DragEvent) {
isDragging = false;
if (isDisabled) return;
const file = e.dataTransfer?.files?.[0];
if (!file) return;
e.preventDefault();
triggerState.rootState.onUpload(file);
}
</script>
<label
@@ -22,7 +46,15 @@
bind:this={ref}
for={isDisabled ? undefined : triggerState.rootState.id}
onclick={handleClick}
class={cn('ui:cursor-pointer', isDisabled ? 'ui:cursor-not-allowed ui:opacity-50 ui:pointer-events-none' : '')}
ondragover={handleDragOver}
ondragleave={handleDragLeave}
ondrop={handleDrop}
class={cn(
'ui:cursor-pointer',
isDisabled ? 'ui:cursor-not-allowed ui:opacity-50 ui:pointer-events-none' : '',
isDragging ? 'ui:ring-2 ui:ring-primary' : '',
className
)}
aria-disabled={isDisabled}
>
{@render children?.()}