Initial commit

This commit is contained in:
2025-10-02 08:54:03 +02:00
commit ea54638227
1642 changed files with 53677 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
<script lang="ts">
import Modal from "../../../Modal.svelte"
import { apiBaseURL } from "../../../../../config"
import { createEventDispatcher } from "svelte"
import UploadImage from "./UploadImageWidget.svelte"
import Icon from "../../Icon.svelte"
import { mdiDeleteOutline } from "@mdi/js"
const dispatch = createEventDispatcher()
export let images: MedialibEntry[] = []
</script>
<Modal
show="{true}"
size="xl"
on:close="{() => {
dispatch('closeImageLibrary')
}}"
>
<svelte:fragment slot="title">
<img
src="../../../../../../media/solar_camera-outline.svg"
alt="icon"
/>
FOTOS
</svelte:fragment>
<div class="images">
<UploadImage on:uploadImage="{() => dispatch('uploadImage')}" />
{#each images as image}
<div class="image">
<div class="image-wrapper">
<img
src="{`${apiBaseURL}medialib/${image.id}/${image.file.src}`}"
alt="Image Preview"
class="image"
aria-hidden="true"
/>
<button
class="removeElement"
on:click|preventDefault|stopPropagation="{() => {
dispatch('removeImage', image.id)
}}"
>
<Icon
path="{mdiDeleteOutline}"
size="1rem"
/>
</button>
</div>
</div>
{/each}
</div>
</Modal>
<style lang="less">
@import "../../../../assets/css/variables.less";
h3.label {
display: flex;
gap: 0.6rem;
align-items: center;
}
.images {
display: grid;
grid-template-columns: repeat(auto-fill, 12rem);
gap: 6px;
.image {
width: 12rem;
display: flex;
flex-direction: column;
.image-wrapper {
height: 12rem;
width: 100%;
position: relative;
background-color: var(--bg-100);
width: 100%;
aspect-ratio: 1/1;
img {
width: 100%;
height: unset;
aspect-ratio: 1/1;
object-fit: cover;
border-radius: 12px;
}
@media @mobile {
width: min(200px, 40vw);
min-width: 50%;
height: 50%;
aspect-ratio: unset;
img {
aspect-ratio: unset;
height: 100%;
}
}
position: relative;
.removeElement {
top: 0.6rem;
right: 0.6rem;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
width: 1.6rem;
height: 1.6rem;
border-radius: 48px;
background: var(--bg-100);
img {
width: 1.2rem;
height: 1.2rem;
}
}
border-radius: 12px;
img {
width: 100%;
border-radius: 12px;
height: 100%;
object-fit: cover;
}
}
@media @mobile {
flex-direction: row;
gap: 6px;
width: 100%;
}
}
@media @mobile {
display: flex;
flex-direction: column;
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,114 @@
<script lang="ts">
import { createEventDispatcher } from "svelte"
import ImagePreview from "./imagePreview.svelte"
export let imageData: MedialibEntry[] = [],
disabled: boolean = false
const dispatch = createEventDispatcher()
const handleClick = (e: Event) => {
if (!disabled) dispatch("openImageModal")
}
let innerWidth = 0
$: isMobile = innerWidth < 1700
$: amountOfVisibleImages = isMobile ? 2 : 12
</script>
<svelte:window bind:innerWidth="{innerWidth}" />
<div class="image-preview-row">
{#each imageData.slice(0, amountOfVisibleImages) as image}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
on:click|stopPropagation|preventDefault="{handleClick}"
class="image-wrapper"
>
<ImagePreview image="{image}" />
</div>
{/each}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
{#if imageData.length > amountOfVisibleImages}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="image-wrapper"
on:click|stopPropagation|preventDefault="{handleClick}"
>
<ImagePreview image="{imageData[amountOfVisibleImages]}" />
{#if imageData.length > amountOfVisibleImages + 1}
<div class="overlay">+{imageData.length - amountOfVisibleImages}</div>
{/if}
</div>
{/if}
{#if !disabled}
<button
class="editImages"
disabled="{disabled}"
on:click|stopPropagation|preventDefault="{() => dispatch('uploadImage')}"
>
<img
src="../../../../../../media/solar_pen-outline.svg"
alt="icon"
/>
</button>
<button
class="editImages"
disabled="{disabled}"
on:click|stopPropagation|preventDefault="{handleClick}"
>
<img
src="../../../../../../media/solar_album-outline.svg"
alt="icon"
/>
</button>
{/if}
</div>
<style lang="less">
.image-preview-row {
display: flex;
gap: 6px;
}
.image-wrapper,
.editImages {
height: 2.7rem;
width: 2.7rem;
position: relative;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
color: white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
font-family: Inter;
font-size: 0.5rem;
font-style: normal;
font-weight: 700;
line-height: normal;
text-transform: uppercase;
}
}
.editImages {
border-radius: 6px;
border: 1px solid var(--text-invers-100);
img {
height: 1.2rem;
width: 1.2rem;
}
}
</style>

View File

@@ -0,0 +1,299 @@
<script lang="ts">
import Modal from "../../../Modal.svelte"
import Icon from "../../Icon.svelte"
import { mdiCloudUploadOutline, mdiDeleteOutline, mdiRefresh } from "@mdi/js"
import { isMobile, newNotification } from "../../../../store"
import LoadingWrapper from "../../LoadingWrapper.svelte"
import { openCameraInput, openFileInput } from "./helper"
import { apiBaseURL } from "../../../../../config"
import { postDBEntry } from "../../../../../api"
export let imagesData: MedialibEntry[] = []
export let showImageUploadModal: boolean
let sessionImages: MedialibEntry[] = []
let modalStates = {
isUploading: false,
}
function readInput(input: HTMLInputElement) {
return Array.from(input.files).map(
(file) =>
new Promise<MedialibEntry>(async (resolve, reject) => {
const reader = new FileReader()
reader.onload = async (e: ProgressEvent<FileReader>) => {
try {
const fileData = e.target?.result
if (fileData) {
const entry = await postDBEntry("medialib", {
file: {
path: file.name,
type: file.type,
size: file.size,
src: typeof fileData === "string" ? fileData : "",
},
title: "Beweisfoto",
alt: "Beweisfoto",
type: "returnOrderFoto",
})
resolve(entry.data)
}
} catch (error) {
reject(error)
}
}
reader.readAsDataURL(file)
})
)
}
const handleFileChange = async (event: Event) => {
const input = event.target as HTMLInputElement
modalStates.isUploading = true
newNotification({
html: "Bilder werden hochgeladen...",
class: "info",
})
if (input.files) {
const filePromises = readInput(input)
try {
let images = await Promise.all(filePromises)
if (!sessionImages || sessionImages.length === 0) sessionImages = []
sessionImages.push(...images)
sessionImages = [...sessionImages]
imagesData = [...imagesData, ...images]
} catch (error) {
console.error("Error uploading files:", error)
newNotification({
html: "Fehler beim Hochladen der Dateien",
class: "error",
})
} finally {
modalStates.isUploading = false
}
} else {
modalStates.isUploading = false
}
}
const replaceImage = async (index: number) => {
loadingIndex = index
// Upload the new image
openFileInput(async (event) => {
const input = event.target as HTMLInputElement
const filePromises = readInput(input)
try {
const newImage = await Promise.all(filePromises)
const imgDataIndx = imagesData.findIndex((img) => img.id === sessionImages[index].id)
imagesData[imgDataIndx] = newImage[0] // Replace the image in the imagesData array
imagesData = [...imagesData] // Update imagesData to reflect the change
sessionImages[index] = newImage[0] // Replace the image in the session array
sessionImages = [...sessionImages] // Update sessionImages to reflect the change
newNotification({
html: "Bild wurde ersetzt",
class: "success",
})
loadingIndex = -1
} catch (error) {
console.error("Error replacing file:", error)
newNotification({
html: "Fehler beim Ersetzen der Datei",
class: "error",
})
loadingIndex = -1
}
})
}
const deleteImage = async (index: number) => {
const imgDataIndx = imagesData.findIndex((img) => img.id === sessionImages[index].id)
imagesData.splice(imgDataIndx, 1) // Remove the image from the imagesData array
imagesData = [...imagesData] // Update imagesData
sessionImages.splice(index, 1) // Remove the image from the session array
sessionImages = [...sessionImages] // Update sessionImages
newNotification({
html: "Bild wurde entfernt",
class: "success",
})
}
let gotTriggered = false
$: if (showImageUploadModal && !gotTriggered) {
gotTriggered = true
openCameraInput(handleFileChange)
}
let loadingIndex = -1
</script>
<Modal
bind:show="{showImageUploadModal}"
size="md"
on:close="{() => {
showImageUploadModal = false
}}"
>
<svelte:fragment slot="title">
<Icon
path="{mdiCloudUploadOutline}"
size="2.4rem"
color="#2f4858"
/>
Bilder hochladen oder aufnehmen
</svelte:fragment>
<p class="content-modal">
Bitte wählen Sie die Bilder aus, die Sie hochladen oder aufnehmen möchten. Sie können mehrere Bilder
gleichzeitig auswählen. Auf dem Computer müssen Sie die Bilder aus Ihrer Bibliothek auswählen. Auf einem
Smartphone können Sie die Kamera verwenden, um Bilder aufzunehmen.
</p>
<div class="image-preview upload">
{#each sessionImages as image, index}
<LoadingWrapper active="{loadingIndex === index}">
<div class="image-container">
{#if image}
<div class="img-wrapper">
<img
src="{`${apiBaseURL}medialib/${image.id}/${image.file.src}`}"
alt="Image Preview"
class="image"
aria-hidden="true"
/>
<div class="actionRow">
<button
class="replace-button"
on:click|preventDefault|stopPropagation="{() => replaceImage(index)}"
>
<Icon
path="{mdiRefresh}"
size="1rem"
/>
</button>
<button
class="delete-button"
on:click|preventDefault|stopPropagation="{() => deleteImage(index)}"
>
<Icon
path="{mdiDeleteOutline}"
size="1rem"
/>
</button>
</div>
</div>
{/if}
</div>
</LoadingWrapper>
{/each}
</div>
<div slot="footer">
<LoadingWrapper
active="{modalStates.isUploading}"
styles="display: flex; justify-content: space-between;"
>
<button
class="cta primary"
on:click|preventDefault|stopPropagation="{() => openFileInput(handleFileChange)}"
>
<Icon
path="{mdiCloudUploadOutline}"
size="1.2rem"
/>
<span> {$isMobile ? "Auswählen" : "Bilder auswählen"}</span>
</button>
<button
class="cta primary"
on:click|preventDefault|stopPropagation="{() => openFileInput(handleFileChange)}"
>
<Icon
path="{mdiCloudUploadOutline}"
size="1.2rem"
/>
<span> {$isMobile ? "Aufnehmen" : "Bilder Aufnehmen "}</span>
</button>
</LoadingWrapper>
</div>
</Modal>
<style lang="less">
@import "../../../../assets/css/variables.less";
h3.label {
display: flex;
gap: 0.6rem;
align-items: center;
}
:global .upload.image-preview {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-top: 1.2rem;
& > div {
width: fit-content !important;
}
}
.cta.primary {
display: flex;
align-items: center;
gap: 0.5rem;
}
.img-wrapper {
width: 100%;
aspect-ratio: 1/1;
max-width: 300px;
@media @mobile {
max-width: 100%;
}
img {
width: 100%;
aspect-ratio: 1/1;
object-fit: cover;
border-radius: 12px;
}
@media @mobile {
width: min(200px, 40vw);
min-width: 100%;
height: 100%;
aspect-ratio: unset;
img {
aspect-ratio: unset;
height: 100%;
}
}
position: relative;
.actionRow {
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
top: 0.6rem;
left: 0.6rem;
right: 0.6rem;
button {
display: flex;
justify-content: center;
align-items: center;
width: 1.6rem;
height: 1.6rem;
border-radius: 48px;
color: white;
background: var(--bg-100);
}
}
}
</style>

View File

@@ -0,0 +1,154 @@
<script lang="ts">
import { getCachedEntries } from "../../../../../api"
import ImageFieldLibrary from "./ImageFieldLibrary.svelte"
import ImagePreviewRow from "./ImagePreviewRow.svelte"
import ImageUpload from "./ImageUpload.svelte"
export let mediaEntries: string[],
disabled: boolean = false
let imagesData: MedialibEntry[]
let loading = true
if (mediaEntries?.length > 0) {
getCachedEntries("medialib", {
_id: {
$in: mediaEntries,
},
}).then((res) => {
imagesData = res
loading = false
})
} else {
imagesData = []
loading = false
}
let modalStates = {
showImageUploadModal: false,
}
const handleImageClick = () => {
modalStates.showImageUploadModal = true
}
$: {
const newmediaEntriess = imagesData?.map((image) => image.id)
if (JSON.stringify(newmediaEntriess) !== JSON.stringify(mediaEntries) && !loading) {
if (newmediaEntriess.length == 0 && !mediaEntries) {
} else mediaEntries = newmediaEntriess
}
}
let showFileLibrary = false
let reopenImageLibrary = false
$: if (!modalStates.showImageUploadModal && reopenImageLibrary && noModalVisible) {
reopenImageLibrary = false
showFileLibrary = true
}
let noModalVisible = false
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="image-row {imagesData?.length == 0 ? 'smallOne' : ''}"
class:disabled="{disabled}"
on:click|stopPropagation|preventDefault="{() => {
if (imagesData?.length == 0) handleImageClick()
}}"
>
{#if !disabled}
<h3
class="label"
style="{imagesData?.length > 0 ? 'padding-bottom: 6px;' : ''}"
>
<img
src="../../../../../../media/solar_camera-outline.svg"
alt="icon"
/>
<p>FOTOS</p>
</h3>{/if}
{#if imagesData?.length === 0}
<button
on:click|preventDefault
class="editImages"
>
<img
src="../../../../../../media/solar_album-outline.svg"
alt="icon"
/>
</button>
{:else}
<ImagePreviewRow
imageData="{imagesData}"
disabled="{disabled}"
on:uploadImage="{handleImageClick}"
on:openImageModal="{() => {
showFileLibrary = true
}}"
/>
{/if}
</div>
{#if showFileLibrary}
<ImageFieldLibrary
bind:images="{imagesData}"
on:removeImage="{(e) => {
imagesData = imagesData?.filter((image) => image.id !== e.detail)
}}"
on:closeImageLibrary="{() => {
showFileLibrary = false
}}"
on:uploadImage="{() => {
showFileLibrary = false
reopenImageLibrary = true
handleImageClick()
}}"
/>
{/if}
{#if imagesData && modalStates.showImageUploadModal}
<ImageUpload
bind:imagesData="{imagesData}"
bind:showImageUploadModal="{modalStates.showImageUploadModal}"
/>
{/if}
<style lang="less">
.image-row {
display: flex;
flex-direction: column;
gap: 0.6rem;
padding: 0.6rem;
border-bottom: 1px solid var(--text-invers-100);
&.disabled {
padding: 0px;
border: none;
}
background: white;
&.smallOne {
flex-direction: row;
justify-content: space-between;
padding: 0px 0.6rem;
cursor: pointer;
.label {
min-height: 54px;
}
}
}
h3.label {
display: flex;
padding: 0px;
margin: 0px;
gap: 0.6rem;
align-items: center;
}
:global .modalMap {
width: 100%;
.mapwrapper {
height: 400px !important;
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,47 @@
<script lang="ts">
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
</script>
<button
class="uploadImage"
on:click|preventDefault|stopPropagation="{() => {
dispatch('uploadImage')
}}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="25"
viewBox="0 0 24 25"
fill="none"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.47 15.97C11.6106 15.8295 11.8012 15.7507 12 15.7507C12.1988 15.7507 12.3894 15.8295 12.53 15.97L14.53 17.97C14.6037 18.0387 14.6628 18.1215 14.7038 18.2135C14.7448 18.3055 14.7668 18.4048 14.7686 18.5055C14.7704 18.6062 14.7518 18.7062 14.7141 18.7996C14.6764 18.893 14.6203 18.9778 14.549 19.049C14.4778 19.1203 14.393 19.1764 14.2996 19.2141C14.2062 19.2518 14.1062 19.2704 14.0055 19.2686C13.9048 19.2668 13.8055 19.2448 13.7135 19.2038C13.6215 19.1628 13.5387 19.1037 13.47 19.03L12.75 18.31V22.5C12.75 22.6989 12.671 22.8897 12.5303 23.0303C12.3897 23.171 12.1989 23.25 12 23.25C11.8011 23.25 11.6103 23.171 11.4697 23.0303C11.329 22.8897 11.25 22.6989 11.25 22.5V18.31L10.53 19.03C10.4613 19.1037 10.3785 19.1628 10.2865 19.2038C10.1945 19.2448 10.0952 19.2668 9.99452 19.2686C9.89382 19.2704 9.79379 19.2518 9.7004 19.2141C9.60701 19.1764 9.52218 19.1203 9.45096 19.049C9.37974 18.9778 9.3236 18.893 9.28588 18.7996C9.24816 18.7062 9.22963 18.6062 9.23141 18.5055C9.23319 18.4048 9.25523 18.3055 9.29622 18.2135C9.33721 18.1215 9.39631 18.0387 9.47 17.97L11.47 15.97Z"
fill="#26292C"></path>
<path
d="M12.476 4.25C9.726 4.25 7.512 6.45 7.512 9.147C7.512 9.609 7.577 10.056 7.697 10.478C8.194 10.622 8.66 10.838 9.08 11.118C9.16639 11.1703 9.24132 11.2396 9.3003 11.3216C9.35927 11.4036 9.40108 11.4966 9.4232 11.5952C9.44533 11.6938 9.44732 11.7958 9.42906 11.8951C9.41079 11.9944 9.37265 12.0891 9.31692 12.1733C9.26118 12.2576 9.18901 12.3297 9.10472 12.3853C9.02044 12.441 8.92578 12.4791 8.82642 12.4972C8.72706 12.5154 8.62506 12.5133 8.52652 12.4911C8.42799 12.4689 8.33495 12.427 8.253 12.368C7.66992 11.9817 6.98546 11.7767 6.286 11.779C4.325 11.779 2.75 13.349 2.75 15.265C2.75 17.181 4.325 18.75 6.286 18.75C6.48491 18.75 6.67568 18.829 6.81633 18.9697C6.95698 19.1103 7.036 19.3011 7.036 19.5C7.036 19.6989 6.95698 19.8897 6.81633 20.0303C6.67568 20.171 6.48491 20.25 6.286 20.25C3.513 20.25 1.25 18.026 1.25 15.265C1.25 12.56 3.42 10.372 6.114 10.282C6.04614 9.90748 6.012 9.52762 6.012 9.147C6.012 5.606 8.914 2.75 12.476 2.75C15.634 2.75 18.272 4.994 18.831 7.971C21.131 8.948 22.75 11.209 22.75 13.853C22.75 16.927 20.562 19.484 17.657 20.106C17.4625 20.1476 17.2594 20.1103 17.0924 20.0022C16.9254 19.8941 16.8081 19.724 16.7665 19.5295C16.7249 19.335 16.7622 19.1319 16.8703 18.9649C16.9784 18.7979 17.1485 18.6806 17.343 18.639C19.583 18.159 21.25 16.193 21.25 13.853C21.25 11.716 19.86 9.891 17.912 9.225C17.3886 9.04599 16.8392 8.95476 16.286 8.955C15.703 8.955 15.146 9.055 14.628 9.235C14.4414 9.29593 14.2383 9.28126 14.0624 9.19415C13.8865 9.10704 13.7517 8.95443 13.6871 8.76909C13.6224 8.58374 13.633 8.38043 13.7166 8.2028C13.8001 8.02516 13.95 7.88737 14.134 7.819C15.1039 7.48042 16.1401 7.37591 17.158 7.514C16.8085 6.55419 16.1712 5.72564 15.3333 5.14147C14.4953 4.5573 13.4975 4.24597 12.476 4.25Z"
fill="#26292C"></path>
</svg>
<p>Bilder hochladen</p>
</button>
<style lang="less">
@import "../../../../assets/css/variables.less";
.uploadImage {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.6rem;
border-radius: 12px;
border: 2px solid var(--text-invers-100);
p {
color: var(--text-invers-100);
}
width: 100%;
aspect-ratio: 1/1;
}
</style>

View File

@@ -0,0 +1,41 @@
export const openFileInput = (callback: (e: Event) => Promise<void>) => {
const input = document.createElement("input")
input.type = "file"
input.accept = "image/*"
input.multiple = true
input.onchange = async (e: Event) => {
await callback(e)
setTimeout(() => {
input.remove() // Delay the removal slightly
}, 100)
}
input.click()
}
export const openCameraInput = (callback: (e: Event) => Promise<void>) => {
const input = document.createElement("input")
input.type = "file"
input.accept = "image/*"
input.capture = "environment"
input.style.position = "absolute"
input.style.left = "-9999px" // Hide the input off-screen
document.body.appendChild(input)
const handleChange = async (e: Event) => {
console.log("HandleChange triggered") // Log to check if this triggers
e.stopPropagation()
await callback(e)
setTimeout(() => {
input.value = "" // Reset input
input.remove() // Remove element
}, 1000) // Delay removal
}
// Try using 'input' event listener as an alternative
input.addEventListener("input", handleChange)
input.focus()
input.click()
}

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { apiBaseURL } from "../../../../../config"
export let image: MedialibEntry
</script>
<img
src="{`${apiBaseURL}medialib/${image.id}/${image.file.src}`}"
alt="Image Preview"
class="image"
aria-hidden="true"
/>
<style lang="less">
.image {
height: 100%;
width: 100%;
object-fit: cover;
border-radius: 4px;
}
</style>