feat: multi-image uploads
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user