Merge branch 'feat/multiple-images' into 'master'

feat: multi-image uploads

See merge request gk1623/drp-48!5
This commit is contained in:
2025-06-05 16:53:16 +00:00
3 changed files with 74 additions and 30 deletions

View File

@@ -12,13 +12,20 @@
let carousel = $state<HTMLDivElement>(); let carousel = $state<HTMLDivElement>();
let scrollPosition = $state(0); let scrollPosition = $state(0);
let scrollWidth = $state(0); let scrollWidth = $state(0);
let clientWidth = $state(0); let clientWidth = $state(1);
function updateScroll() { function updateScroll() {
scrollPosition = carousel?.scrollLeft || 0; scrollPosition = carousel?.scrollLeft || 0;
scrollWidth = carousel?.scrollWidth || 0; scrollWidth = carousel?.scrollWidth || 0;
clientWidth = carousel?.clientWidth || 0; clientWidth = carousel?.clientWidth || 1;
} }
onMount(updateScroll); onMount(() => {
const id = setInterval(() => {
if (carousel) {
updateScroll();
}
}, 1000);
return () => clearInterval(id);
});
</script> </script>
<div class="controls"> <div class="controls">
@@ -37,7 +44,8 @@
{#if scrollPosition > clientWidth / 2} {#if scrollPosition > clientWidth / 2}
<button <button
class="arrow left" class="arrow left"
onclick={() => { onclick={(e) => {
e.preventDefault();
if (carousel) carousel.scrollLeft -= carousel.clientWidth; if (carousel) carousel.scrollLeft -= carousel.clientWidth;
}} }}
> >
@@ -47,7 +55,8 @@
{#if scrollPosition < scrollWidth - clientWidth * 1.5} {#if scrollPosition < scrollWidth - clientWidth * 1.5}
<button <button
class="arrow right" class="arrow right"
onclick={() => { onclick={(e) => {
e.preventDefault();
if (carousel) carousel.scrollLeft += carousel.clientWidth; if (carousel) carousel.scrollLeft += carousel.clientWidth;
}} }}
> >
@@ -95,12 +104,14 @@
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
border-radius: 9999px; border-radius: 9999px;
} }
.arrow { .arrow,
.delete {
cursor: pointer; cursor: pointer;
width: 2rem; width: 2rem;
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
} }
.arrow img { .arrow img,
.delete img {
width: 1.4rem; width: 1.4rem;
} }
.arrow:hover { .arrow:hover {
@@ -120,14 +131,14 @@
transform: translateY(-50%); transform: translateY(-50%);
} }
.delete { .delete {
top: 0.1rem; top: 0.4rem;
right: 0.1rem; right: 0.2rem;
} }
.position { .position {
font-size: 0.8rem; font-size: 0.8rem;
bottom: 0.7rem; top: 0.4rem;
border-radius: 1rem; border-radius: 1rem;
right: 0.2rem; left: 0.2rem;
padding: 0.3rem 0.5rem; padding: 0.3rem 0.5rem;
background-color: rgba(0, 0, 0, 0.7); background-color: rgba(0, 0, 0, 0.7);
} }

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import cameraUrl from "$lib/assets/camera.svg"; import cameraUrl from "$lib/assets/camera.svg";
import Carousel from "../Carousel.svelte";
interface Props { interface Props {
name: string; name: string;
@@ -18,14 +19,37 @@
class:no-bg={files && files.length > 0} class:no-bg={files && files.length > 0}
> >
{#if files && files.length > 0} {#if files && files.length > 0}
<img src={URL.createObjectURL(files[0])} alt="uploaded study space" class="preview" /> <Carousel
urls={files
? Array(files.length)
.keys()
.map((i) => URL.createObjectURL(files![i]))
.toArray()
: []}
ondelete={(idx) => {
if (!files) return;
const dt = new DataTransfer();
for (let i = 0; i < files.length; i++) {
if (i !== idx) dt.items.add(files[i]);
}
files = dt.files;
}}
/>
{:else} {:else}
<div class="message"> <div class="message">
<img src={cameraUrl} class="icon" alt="camera icon" /> <img src={cameraUrl} class="icon" alt="camera icon" />
<span>Click to upload a photo</span> <span>Click to upload a photo</span>
</div> </div>
{/if} {/if}
<input type="file" id={name} {name} accept=".png, .jpg, .jpeg, .svg" {...rest} bind:files /> <input
type="file"
id={name}
{name}
multiple
accept=".png, .jpg, .jpeg, .svg"
{...rest}
bind:files
/>
</label> </label>
<style> <style>

View File

@@ -5,13 +5,13 @@
import Navbar from "$lib/components/Navbar.svelte"; import Navbar from "$lib/components/Navbar.svelte";
import crossUrl from "$lib/assets/cross.svg"; import crossUrl from "$lib/assets/cross.svg";
import Button from "$lib/components/Button.svelte"; import Button from "$lib/components/Button.svelte";
import Image from "$lib/components/inputs/Image.svelte"; import Images from "$lib/components/inputs/Images.svelte";
import type { Table } from "$lib"; import type { Table } from "$lib";
const { data } = $props(); const { data } = $props();
const { supabase } = $derived(data); const { supabase } = $derived(data);
let spaceImg = $state<FileList>(); let spaceImgs = $state<FileList>();
let studySpaceData = $state<Omit<Table<"study_spaces">, "id" | "created_at" | "updated_at">>({ let studySpaceData = $state<Omit<Table<"study_spaces">, "id" | "created_at" | "updated_at">>({
description: "", description: "",
building_location: "", building_location: "",
@@ -19,8 +19,7 @@
}); });
async function uploadStudySpace() { async function uploadStudySpace() {
const imageFile = spaceImg?.[0]; if (!spaceImgs || spaceImgs.length < 1) return alert("Please select an image file.");
if (!imageFile) return alert("Please select an image file.");
const { data: studySpaceInsert, error: studySpaceError } = await supabase const { data: studySpaceInsert, error: studySpaceError } = await supabase
.from("study_spaces") .from("study_spaces")
@@ -30,21 +29,31 @@
if (studySpaceError) if (studySpaceError)
return alert(`Error uploading study space: ${studySpaceError.message}`); return alert(`Error uploading study space: ${studySpaceError.message}`);
const { data: imgUpload, error: imageError } = await supabase.storage const imgUploads = await Promise.all(
Array(spaceImgs.length)
.keys()
.map(async (i) => {
const imageFile = spaceImgs![i];
const resp = await supabase.storage
.from("files_bucket") .from("files_bucket")
.upload(`public/${studySpaceInsert.id}-${imageFile.name}`, imageFile, { .upload(`public/${studySpaceInsert.id}-${imageFile.name}`, imageFile, {
contentType: imageFile.type contentType: imageFile.type
}); });
return resp;
})
);
const imageError = imgUploads.find(({ error }) => error)?.error;
if (imageError) return alert(`Error uploading image: ${imageError.message}`); if (imageError) return alert(`Error uploading image: ${imageError.message}`);
const { error: imageInsertError } = await supabase const { error: imageInsertError } = await supabase
.from("study_space_images") .from("study_space_images")
.insert({ .insert(
imgUploads.map(({ data }) => ({
study_space_id: studySpaceInsert.id, study_space_id: studySpaceInsert.id,
image_path: imgUpload.path image_path: data!.path
}) }))
.select() )
.single(); .select();
if (imageInsertError) return alert(`Error creating image: ${imageInsertError.message}`); if (imageInsertError) return alert(`Error creating image: ${imageInsertError.message}`);
alert("Thank you for your contribution!"); alert("Thank you for your contribution!");
@@ -67,7 +76,7 @@
await uploadStudySpace(); await uploadStudySpace();
}} }}
> >
<Image name="study-space-image" minHeight="16rem" bind:files={spaceImg} required /> <Images name="study-space-image" minHeight="16rem" bind:files={spaceImgs} required />
<label for="location">Enter the name:</label> <label for="location">Enter the name:</label>
<Text <Text
@@ -97,7 +106,7 @@
<div class="submit"> <div class="submit">
<Button <Button
type="submit" type="submit"
disabled={(spaceImg?.length || 0) === 0 || disabled={(spaceImgs?.length || 0) === 0 ||
!studySpaceData.location || !studySpaceData.location ||
!studySpaceData.description || !studySpaceData.description ||
!studySpaceData.building_location} !studySpaceData.building_location}