strip v1
This commit is contained in:
@@ -1,122 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getDBEntry } from "../../../api"
|
||||
import CrinkledSection from "../CrinkledSection.svelte"
|
||||
import ContentBlock from "../pagebuilder/ContentBlock.svelte"
|
||||
import MedialibImage from "../widgets/MedialibImage.svelte"
|
||||
|
||||
export let entryId: string
|
||||
export let thumbnail: string
|
||||
export let sources: Source[]
|
||||
let entry: ContentEntry
|
||||
getDBEntry("content", { _id: entryId }).then((res) => {
|
||||
entry = res
|
||||
console.log("entry", entry)
|
||||
})
|
||||
</script>
|
||||
|
||||
<section class="blog-entry">
|
||||
<section class="thumbnail-image">
|
||||
<img
|
||||
src="../../../../media/topRightCrinkleDarkAndUnedged.svg"
|
||||
class="crinkle"
|
||||
alt="crinkle"
|
||||
/>
|
||||
{#if entry}
|
||||
<MedialibImage id="{thumbnail}" />
|
||||
{/if}
|
||||
</section>
|
||||
<section class="content-wrapper">
|
||||
<CrinkledSection
|
||||
activated="{true}"
|
||||
border="{false}"
|
||||
brightBackground="{false}"
|
||||
bigVersion="{true}"
|
||||
icon="bottomLeftDark"
|
||||
>
|
||||
<section class="content">
|
||||
{#if entry}
|
||||
{#each entry.blocks as block}
|
||||
<ContentBlock block="{block}" />
|
||||
{/each}
|
||||
{/if}
|
||||
</section>
|
||||
<section class="sources">
|
||||
<h4>Quellen:</h4>
|
||||
<ul>
|
||||
{#each sources as { source, url }}
|
||||
<li>
|
||||
<a
|
||||
href="{url}"
|
||||
target="_blank"
|
||||
>
|
||||
{source}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
</CrinkledSection>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.blog-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: var(--small-max-width);
|
||||
width: 100%;
|
||||
.thumbnail-image {
|
||||
width: 100%;
|
||||
height: 520px;
|
||||
position: relative;
|
||||
margin-botom: -3.6rem;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.crinkle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
.content-wrapper {
|
||||
padding: 0px 2.4rem;
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
background: var(--bg-100);
|
||||
width: 100%;
|
||||
* {
|
||||
color: var(--text-100) !important;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
.sources {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
color: var(--text-100);
|
||||
padding: 1.2rem 2.4rem;
|
||||
border-top: 1px solid var(--neutral-white);
|
||||
margin-top: 1.2rem;
|
||||
h4,
|
||||
a {
|
||||
padding: 0.6rem 0px;
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,133 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getCachedEntry } from "../../../api"
|
||||
import { getCalendarWeekFromDate } from "../../utils"
|
||||
import Steps from "../pagebuilder/blocks/Steps.svelte"
|
||||
import BlogTabSwitch from "../widgets/BlogTabSwitch.svelte"
|
||||
import MedialibImage from "../widgets/MedialibImage.svelte"
|
||||
import ChallengeBlog from "./Blog.svelte"
|
||||
|
||||
export let slug: string
|
||||
let challenge: BKDFChallenge
|
||||
getCachedEntry("selfImprovementChallenge", {
|
||||
slug: slug,
|
||||
}).then((res) => {
|
||||
challenge = res
|
||||
})
|
||||
let activeTab = 1
|
||||
</script>
|
||||
|
||||
<div class="rows-detailed-challenge">
|
||||
<section class="challenge-detailed-preview">
|
||||
{#if challenge}
|
||||
<h2>Weekly Challenge #{getCalendarWeekFromDate(challenge.activeAt)}</h2>
|
||||
|
||||
<div class="preview-img-wrapper">
|
||||
<MedialibImage id="{challenge.images.preview}" />
|
||||
</div>
|
||||
|
||||
<h1>{challenge.title}</h1>
|
||||
{/if}
|
||||
</section>
|
||||
<BlogTabSwitch
|
||||
tabs="{['Einleitung', 'Informationen']}"
|
||||
bind:selectedTab="{activeTab}"
|
||||
/>
|
||||
{#if activeTab == 0}
|
||||
<section class="introduction">
|
||||
{#if challenge}
|
||||
{#each challenge.introduction as intro}
|
||||
<p>{@html intro}</p>
|
||||
{/each}
|
||||
{/if}
|
||||
</section>
|
||||
{#if challenge}
|
||||
<section class="invitation">
|
||||
{challenge?.howItWorks?.invitation}
|
||||
</section>
|
||||
<Steps block="{challenge?.howItWorks}" />
|
||||
{/if}
|
||||
{:else if challenge}
|
||||
<ChallengeBlog
|
||||
entryId="{challenge.blog.blogId}"
|
||||
thumbnail="{challenge.blog.thumbnail}"
|
||||
sources="{challenge.blog.sources}"
|
||||
/>
|
||||
{/if}
|
||||
<div style="padding: 3.6rem 0px;"></div>
|
||||
</div>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.rows-detailed-challenge {
|
||||
max-width: var(--normal-max-width);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2.4rem;
|
||||
.challenge-detailed-preview {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
max-width: var(--normal-max-width);
|
||||
|
||||
h2 {
|
||||
color: transparent;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
-webkit-text-stroke: 1px var(--krass-kraft-primary);
|
||||
padding: 1.2rem 0px;
|
||||
line-height: 4.8rem;
|
||||
font-size: 4.8rem;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.preview-img-wrapper {
|
||||
width: 100%;
|
||||
height: 518px;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
span {
|
||||
color: var(--text-300);
|
||||
}
|
||||
h1 {
|
||||
font-size: 3.6rem;
|
||||
line-height: 3.6rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
.introduction {
|
||||
display: flex;
|
||||
|
||||
& > p {
|
||||
padding: 0px 1.2rem;
|
||||
color: var(--text-100);
|
||||
border-right: 1px solid white;
|
||||
* {
|
||||
color: var(--text-100);
|
||||
}
|
||||
&:first-child {
|
||||
padding-left: 0px;
|
||||
}
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
padding-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.invitation {
|
||||
padding: 2.4rem;
|
||||
color: white;
|
||||
border: 1px solid white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,70 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { spaLink } from "../../actions"
|
||||
import MedialibImage from "../widgets/MedialibImage.svelte"
|
||||
|
||||
export let challenge: BKDFChallenge
|
||||
</script>
|
||||
|
||||
<li class="challenge-preview-item">
|
||||
<a
|
||||
class="thumbnail-wrapper"
|
||||
href="/selfimprovement/krasskraft/challenge/{challenge.slug}"
|
||||
use:spaLink
|
||||
>
|
||||
<MedialibImage id="{challenge.images.preview}" />
|
||||
</a>
|
||||
<div class="challenge-footer">
|
||||
<!-- <span>
|
||||
KW {getCalendarWeekFromDate(challenge.activeAt)}
|
||||
</span>-->
|
||||
<h3>
|
||||
{challenge.title}
|
||||
</h3>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
@import "../../assets/css/variables.less";
|
||||
li.challenge-preview-item {
|
||||
max-width: 27rem;
|
||||
width: 100%;
|
||||
@media @mobile {
|
||||
width: calc(100vw - 2 * var(--horizontal-default-margin) - 2px);
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
a.thumbnail-wrapper {
|
||||
width: 100%;
|
||||
aspect-ratio: 1.2 / 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
.challenge-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
padding-bottom: 1.2rem;
|
||||
border-bottom: 1px solid var(--krass-kraft-primary);
|
||||
span {
|
||||
color: var(--text-300);
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,67 +0,0 @@
|
||||
<script
|
||||
lang="ts"
|
||||
context="module"
|
||||
>
|
||||
import "simplebar"
|
||||
import "simplebar/dist/simplebar.css"
|
||||
import ResizeObserver from "resize-observer-polyfill"
|
||||
if (typeof window !== "undefined") window.ResizeObserver = ResizeObserver
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import ChallengePreview from "./ChallengePreview.svelte"
|
||||
export let challenges: BKDFChallenge[] = []
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="product-preview-list verticalScrollbar"
|
||||
data-simplebar
|
||||
>
|
||||
<ul class="inner-wrapper">
|
||||
{#each challenges as challenge}
|
||||
<ChallengePreview challenge="{challenge}" />
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.product-preview-list {
|
||||
width: 100%;
|
||||
max-width: var(--normal-max-width);
|
||||
|
||||
.simplebar-wrapper {
|
||||
padding-bottom: 1.2rem;
|
||||
.simplebar-content {
|
||||
max-width: var(--normal-max-width);
|
||||
& > .inner-wrapper {
|
||||
display: flex;
|
||||
gap: 2.2rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.simplebar-track {
|
||||
background-color: rgba(13, 12, 12, 0.25);
|
||||
height: 7px;
|
||||
overflow: visible;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.simplebar-scrollbar {
|
||||
transition-duration: 0ms !important;
|
||||
cursor: pointer;
|
||||
&::before {
|
||||
background-color: var(--bg-100);
|
||||
top: -2px;
|
||||
opacity: 1;
|
||||
border-radius: 0;
|
||||
height: 11px;
|
||||
left: 0px;
|
||||
|
||||
transition-delay: 0s;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,99 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getDBEntries } from "../../../api"
|
||||
import { spaLink } from "../../actions"
|
||||
import Columns from "../pagebuilder/blocks/Columns.svelte"
|
||||
import ChallengePreviewList from "./ChallengePreviewList.svelte"
|
||||
|
||||
export let chapter: SelfImprovementChapter
|
||||
let challenges: BKDFChallenge[] = []
|
||||
getDBEntries("selfImprovementChallenge", {
|
||||
type: 1,
|
||||
}).then((res) => {
|
||||
challenges = res
|
||||
console.log(challenges)
|
||||
})
|
||||
</script>
|
||||
|
||||
<section class="row hp-row">
|
||||
<Columns
|
||||
block="{{
|
||||
type: 'columns',
|
||||
columns: [
|
||||
{
|
||||
type: 'chapterDescription',
|
||||
verticalAlign: 'middle',
|
||||
chapterDescription: {
|
||||
title: chapter.title,
|
||||
description: chapter.description,
|
||||
type: chapter.type,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
images: [chapter.previewImage],
|
||||
verticalAlign: 'middle',
|
||||
imageMobileBackground: true,
|
||||
},
|
||||
],
|
||||
}}"
|
||||
/>
|
||||
</section>
|
||||
<section class="challenges-row">
|
||||
<div class="headline">
|
||||
<div class="headline-col">
|
||||
<h2>Challenges</h2>
|
||||
</div>
|
||||
|
||||
<button class="">
|
||||
<a
|
||||
href="/selfimprovement/krasskraft/challenges"
|
||||
use:spaLink
|
||||
>
|
||||
Alle Anzeigen
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
<ChallengePreviewList challenges="{challenges}" />
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
max-width: var(--normal-max-width);
|
||||
}
|
||||
.row {
|
||||
flex-direction: row;
|
||||
}
|
||||
.hp-row {
|
||||
height: calc(38rem + 145px + 96px + 96px);
|
||||
max-height: 97vh;
|
||||
}
|
||||
.headline {
|
||||
width: 100%;
|
||||
margin-bottom: 1.2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
|
||||
h2 {
|
||||
font-size: 4.8rem;
|
||||
font-weight: 500;
|
||||
color: transparent;
|
||||
font-family: sans-serif;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
-webkit-text-stroke: 2px var(--krass-kraft-primary);
|
||||
}
|
||||
button {
|
||||
a {
|
||||
color: var(--krass-kraft-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
.challenges-row {
|
||||
margin-bottom: 3.6rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,105 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
import Item from "./Item.svelte"
|
||||
import { selfImprovementChapters } from "../../../../store"
|
||||
|
||||
export let block: ContentBlock<"selfImprovementChapterPreview">
|
||||
const chapters = block.selfImprovementChapterPreview
|
||||
let interval: NodeJS.Timeout
|
||||
|
||||
function startInterval() {
|
||||
interval = setInterval(() => {
|
||||
selectedChapter = (selectedChapter + 1) % $selfImprovementChapters.length
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
function stopInterval() {
|
||||
clearInterval(interval)
|
||||
}
|
||||
onMount(() => {
|
||||
startInterval()
|
||||
return () => clearInterval(interval)
|
||||
})
|
||||
let selectedChapter = 0
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<ul>
|
||||
{#each $selfImprovementChapters as chapter, i}
|
||||
<li
|
||||
on:mouseenter="{() => {
|
||||
stopInterval()
|
||||
selectedChapter = i
|
||||
}}"
|
||||
on:mouseleave="{startInterval}"
|
||||
class:active="{selectedChapter === i}"
|
||||
>
|
||||
<Item
|
||||
chapter="{chapter}"
|
||||
bind:selectedChapter="{selectedChapter}"
|
||||
index="{i}"
|
||||
previewImage="{chapters.find((p) => p.chapter === chapter.id)?.previewImage}"
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
* {
|
||||
transition-duration: 0s;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
li {
|
||||
width: 100%;
|
||||
aspect-ratio: 4 / 3;
|
||||
@media (max-width: 1500px) {
|
||||
aspect-ratio: unset;
|
||||
&::before {
|
||||
float: left;
|
||||
padding-top: 125%;
|
||||
content: "";
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
content: "";
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1500px) {
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
li {
|
||||
width: 33.33%;
|
||||
}
|
||||
|
||||
li.active {
|
||||
order: -1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,350 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount, afterUpdate } from "svelte"
|
||||
import MedialibImage from "../../../widgets/MedialibImage.svelte"
|
||||
import { spaLink } from "../../../../actions"
|
||||
import { getVariableNameForChapter } from "../../../../utils"
|
||||
|
||||
export let selectedChapter: number, index: number, chapter: SelfImprovementChapter, previewImage: string
|
||||
const color = chapter.color
|
||||
$: active = selectedChapter === index
|
||||
|
||||
let upperPart: HTMLElement
|
||||
let lowerPart: HTMLElement
|
||||
let topOverlay: HTMLElement
|
||||
let bottomOverlay: HTMLElement
|
||||
|
||||
function updateOverlayHeights() {
|
||||
if (topOverlay && bottomOverlay && upperPart && lowerPart) {
|
||||
topOverlay.style.height = `${upperPart.offsetHeight + 24}px`
|
||||
bottomOverlay.style.height = `${lowerPart.offsetHeight + 24}px`
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("resize", updateOverlayHeights)
|
||||
})
|
||||
|
||||
afterUpdate(() => {
|
||||
updateOverlayHeights()
|
||||
})
|
||||
|
||||
$: if (active) {
|
||||
updateOverlayHeights()
|
||||
} else {
|
||||
if (topOverlay && bottomOverlay) {
|
||||
topOverlay.style.height = "0px"
|
||||
bottomOverlay.style.height = "0px"
|
||||
}
|
||||
}
|
||||
function getSlug(type: number) {
|
||||
if (type == 1) return "krasskraft"
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
let colorName = getVariableNameForChapter(chapter.type)
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="chapter-preview-block"
|
||||
class:active="{active}"
|
||||
>
|
||||
{#if chapter.locked}
|
||||
<div
|
||||
class="locked"
|
||||
class:active="{active}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 72 72"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M15.75 30.165V24C15.75 18.6294 17.8835 13.4787 21.6811 9.68109C25.4787 5.88348 30.6294 3.75 36 3.75C41.3706 3.75 46.5213 5.88348 50.3189 9.68109C54.1165 13.4787 56.25 18.6294 56.25 24V30.165C59.595 30.414 61.77 31.044 63.363 32.637C66 35.271 66 39.516 66 48C66 56.484 66 60.729 63.363 63.363C60.729 66 56.484 66 48 66H24C15.516 66 11.271 66 8.637 63.363C6 60.729 6 56.484 6 48C6 39.516 6 35.271 8.637 32.637C10.227 31.044 12.405 30.414 15.75 30.165ZM20.25 24C20.25 19.8228 21.9094 15.8168 24.8631 12.8631C27.8168 9.90937 31.8228 8.25 36 8.25C40.1772 8.25 44.1832 9.90937 47.1369 12.8631C50.0906 15.8168 51.75 19.8228 51.75 24V30.012C50.601 30 49.353 30 48 30H24C22.644 30 21.399 30 20.25 30.012V24ZM42 48C42 49.5913 41.3679 51.1174 40.2426 52.2426C39.1174 53.3679 37.5913 54 36 54C34.4087 54 32.8826 53.3679 31.7574 52.2426C30.6321 51.1174 30 49.5913 30 48C30 46.4087 30.6321 44.8826 31.7574 43.7574C32.8826 42.6321 34.4087 42 36 42C37.5913 42 39.1174 42.6321 40.2426 43.7574C41.3679 44.8826 42 46.4087 42 48Z"
|
||||
fill="white"></path>
|
||||
</svg>
|
||||
<p>Coming Soon</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="background-image">
|
||||
<MedialibImage id="{previewImage}" />
|
||||
|
||||
<div
|
||||
bind:this="{topOverlay}"
|
||||
class="overlay top"
|
||||
style="background-color: var({colorName})"
|
||||
></div>
|
||||
<div
|
||||
bind:this="{bottomOverlay}"
|
||||
class="overlay bottom"
|
||||
style="background-color: var({colorName})"
|
||||
></div>
|
||||
<div
|
||||
class="overlay left"
|
||||
style="background-color: var({colorName})"
|
||||
></div>
|
||||
<div
|
||||
class="overlay right"
|
||||
style="background-color: var({colorName})"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
class:active="{active}"
|
||||
>
|
||||
<div
|
||||
bind:this="{upperPart}"
|
||||
class:active="{active}"
|
||||
class="upper-part"
|
||||
>
|
||||
<h4
|
||||
class:active="{active}"
|
||||
style="{!active ? 'color: ' + color : 'color: var(--bg-100)'}"
|
||||
>
|
||||
{chapter.alias}
|
||||
</h4>
|
||||
<h3
|
||||
class:active="{active}"
|
||||
style="{!active ? 'color: ' + color : 'color: var(--bg-100)'}"
|
||||
>
|
||||
{chapter.title}
|
||||
</h3>
|
||||
<p
|
||||
class:active="{active}"
|
||||
style="{!active ? 'color: ' + 'transparent' : 'color: var(--bg-100)'}"
|
||||
>
|
||||
{chapter.shortDescription}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
bind:this="{lowerPart}"
|
||||
class="lower-part"
|
||||
class:active="{active}"
|
||||
>
|
||||
<button
|
||||
style="background-color: var({colorName})"
|
||||
disabled="{chapter.locked}"
|
||||
>
|
||||
<a
|
||||
href="/selfimprovement/{getSlug(chapter.type)}"
|
||||
use:spaLink
|
||||
aria-disabled="{chapter.locked ? 'true' : 'false'}"
|
||||
>
|
||||
Weiter
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
@import "../../../../assets/css/variables.less";
|
||||
.chapter-preview-block {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
.locked {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
background: rgba(13, 12, 12, 0.5);
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
p {
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
svg {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
}
|
||||
@media (max-width: 1500px) {
|
||||
&:not(.active) {
|
||||
svg {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
p {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.background-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.overlay {
|
||||
position: absolute;
|
||||
transition: all 0.3s ease;
|
||||
&.top {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 0;
|
||||
}
|
||||
&.bottom {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 0;
|
||||
}
|
||||
&.left {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
&.right {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.active .background-image .overlay.top {
|
||||
height: auto;
|
||||
}
|
||||
&.active .background-image .overlay.bottom {
|
||||
height: auto;
|
||||
}
|
||||
&.active .background-image .overlay.left {
|
||||
width: 3.6rem;
|
||||
@media (max-width: 600px) {
|
||||
width: 0.6rem;
|
||||
}
|
||||
}
|
||||
&.active .background-image .overlay.right {
|
||||
width: 3.6rem;
|
||||
@media (max-width: 600px) {
|
||||
width: 0.6rem;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
transition: border-color 0.3s ease, color 0.3s ease;
|
||||
border-top: 2.4rem solid transparent;
|
||||
border-bottom: 2.4rem solid transparent;
|
||||
border-left: 3.6rem solid transparent;
|
||||
border-right: 3.6rem solid transparent;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
.upper-part {
|
||||
transition: border-color 0.3s ease, color 0.3s ease, background-color 0.3s ease;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
h4 {
|
||||
margin: 0px;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.2rem;
|
||||
color: var(--bg-100);
|
||||
}
|
||||
h3 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.2rem;
|
||||
color: var(--bg-100);
|
||||
}
|
||||
p {
|
||||
font-size: 1.2rem;
|
||||
color: var(--bg-100);
|
||||
}
|
||||
@media (max-width: 1500px) {
|
||||
h3:not(.active) {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
p:not(.active) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
h4:not(.active) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
gap: 0.6rem;
|
||||
h3 {
|
||||
font-size: 1.6rem;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
h3:not(.active) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
p {
|
||||
margin-top: 0px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
p:not(.active) {
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
h4 {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
h4:not(.active) {
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
border-bottom: 2.4rem solid transparent;
|
||||
@media (max-width: 600px) {
|
||||
border-width: 0.6rem !important;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
border-width: 0.6rem !important;
|
||||
}
|
||||
.lower-part {
|
||||
border-top: 2.4rem solid transparent;
|
||||
@media (max-width: 600px) {
|
||||
border-width: 0rem !important;
|
||||
}
|
||||
transition: border-color 0.3s ease, color 0.3s ease, background-color 0.3s ease;
|
||||
button {
|
||||
font-weight: 700;
|
||||
font-size: 1.2rem;
|
||||
text-transform: uppercase;
|
||||
padding: 6px 12px;
|
||||
@media (max-width: 1500px) {
|
||||
padding-left: 0px;
|
||||
}
|
||||
color: var(--bg-100);
|
||||
&[disabled] {
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:not(.active) {
|
||||
@media (max-width: 1500px) {
|
||||
.lower-part {
|
||||
button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,261 +0,0 @@
|
||||
<script
|
||||
lang="ts"
|
||||
context="module"
|
||||
>
|
||||
import "simplebar"
|
||||
import "simplebar/dist/simplebar.css"
|
||||
import ResizeObserver from "resize-observer-polyfill"
|
||||
if (typeof window !== "undefined") window.ResizeObserver = ResizeObserver
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { getCachedEntries } from "../../../../../api"
|
||||
import { getBCGraphProductsByIds } from "../../../../functions/CommerceAPIs/bigCommerce/product"
|
||||
import { backgroundImages } from "../../../../store"
|
||||
import MedialibImage from "../../../widgets/MedialibImage.svelte"
|
||||
|
||||
export let block: ContentBlock<"ratingPreview">
|
||||
let ratings: ProductRating[] = []
|
||||
let productsMap: Record<string, BKDFProduct> = {}
|
||||
getCachedEntries("rating", {
|
||||
_id: {
|
||||
$in: block?.ratingsPreview?.ratings?.map((rating) => rating.rating),
|
||||
},
|
||||
}).then((entries) => {
|
||||
getBCGraphProductsByIds(entries.map((entry) => String(entry.bigCommerceProductId))).then((products) => {
|
||||
productsMap = products.reduce((acc, product) => {
|
||||
acc[product.id] = product
|
||||
return acc
|
||||
}, {})
|
||||
ratings = entries
|
||||
})
|
||||
})
|
||||
|
||||
function formatDate(date: string) {
|
||||
return new Date(date).toLocaleDateString("de-DE", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
}
|
||||
|
||||
function returnWidthForRating(rating: ProductRating) {
|
||||
const ratingP =
|
||||
(rating.rating.quality + rating.rating.priceQualityRatio + rating.rating.comfort + rating.rating.overall) *
|
||||
5
|
||||
return ratingP
|
||||
}
|
||||
|
||||
function returnAvgForRating(rating: ProductRating) {
|
||||
const ratingP =
|
||||
(rating.rating.quality + rating.rating.priceQualityRatio + rating.rating.comfort + rating.rating.overall) /
|
||||
4
|
||||
return ratingP.toFixed(1)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if ratings.length}
|
||||
<div
|
||||
data-simplebar
|
||||
class="review-list-container horizontalScrollbar"
|
||||
>
|
||||
<ul class="review-list">
|
||||
{#each ratings as rating}
|
||||
<li class="review-item">
|
||||
<div class="upper-box">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="48"
|
||||
height="49"
|
||||
viewBox="0 0 48 49"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M48 0.25V47.5833L0 0.25H48Z"
|
||||
fill="white"></path>
|
||||
</svg>
|
||||
<div class="background-image">
|
||||
<img
|
||||
src="{productsMap[rating.bigCommerceProductId].featuredImage.url}"
|
||||
alt="{rating.title}"
|
||||
/>
|
||||
<MedialibImage
|
||||
id="{$backgroundImages?.standard}"
|
||||
filter="l"
|
||||
/>
|
||||
</div>
|
||||
<div class="overlay">
|
||||
<h3>
|
||||
{rating.title}
|
||||
</h3>
|
||||
<p>{rating.comment}</p>
|
||||
<div class="date">{formatDate(rating.review_date)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rating-bar">
|
||||
<div
|
||||
class="rating-filled"
|
||||
style="width: {returnWidthForRating(rating)}%; "
|
||||
>
|
||||
<div class="star-wrapper">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M16.7692 23.1373L16.7717 23.1388C17.1926 23.3925 17.6791 23.5165 18.1701 23.4952C18.6612 23.474 19.1351 23.3085 19.5326 23.0193C19.9301 22.7302 20.2335 22.3303 20.405 21.8697C20.5762 21.4098 20.6083 20.9097 20.4972 20.4318C20.497 20.431 20.4968 20.4302 20.4967 20.4294L19.3653 15.5223L22.8594 12.4729H22.8607L23.1422 12.2302C23.515 11.9087 23.7846 11.4842 23.9171 11.0101C24.0496 10.536 24.0392 10.0333 23.8872 9.56506C23.7352 9.09682 23.4483 8.68389 23.0626 8.37804C22.6772 8.0725 22.2103 7.88743 21.7202 7.84595C21.7197 7.84591 21.7192 7.84587 21.7187 7.84583L16.7503 7.41534L14.8029 2.78442C14.8027 2.78387 14.8024 2.78331 14.8022 2.78276C14.6125 2.32902 14.2929 1.94145 13.8836 1.66874C13.4738 1.39569 12.9924 1.25 12.5 1.25C12.0076 1.25 11.5262 1.3957 11.1164 1.66874C10.7069 1.94157 10.3872 2.32937 10.1976 2.78337C10.1974 2.78372 10.1973 2.78407 10.1971 2.78442L8.25556 7.41539L3.28596 7.84583C3.28549 7.84586 3.28502 7.8459 3.28455 7.84594C2.7945 7.8874 2.32754 8.07248 1.94214 8.37804C1.55638 8.68388 1.2695 9.09682 1.11748 9.56506C0.965459 10.0333 0.955069 10.536 1.08761 11.0101L2.05069 10.7409L1.08761 11.0101C1.21983 11.4831 1.48842 11.9066 1.85981 12.2279L5.63577 15.5275L4.50617 20.4294C4.50605 20.43 4.50592 20.4305 4.50579 20.4311C4.39454 20.9092 4.42654 21.4096 4.59781 21.8697C4.76928 22.3303 5.07273 22.7302 5.47023 23.0193C5.86773 23.3085 6.34163 23.474 6.8327 23.4952C7.32376 23.5165 7.81019 23.3925 8.23118 23.1388L8.23442 23.1368L12.4968 20.546L16.7692 23.1373Z"
|
||||
fill="#2F4858"
|
||||
stroke="white"
|
||||
stroke-width="2"></path>
|
||||
</svg>
|
||||
<span>{returnAvgForRating(rating)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.review-list-container {
|
||||
width: 100%;
|
||||
max-width: var(--normal-max-width);
|
||||
.simplebar-wrapper {
|
||||
padding-bottom: 1.2rem;
|
||||
.simplebar-content {
|
||||
max-width: var(--normal-max-width);
|
||||
& > .inner-wrapper {
|
||||
display: flex;
|
||||
gap: 2.2rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.simplebar-track {
|
||||
background-color: rgba(13, 12, 12, 0.25);
|
||||
height: 7px;
|
||||
overflow: visible;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.simplebar-scrollbar {
|
||||
transition-duration: 0ms !important;
|
||||
cursor: pointer;
|
||||
&::before {
|
||||
background-color: var(--bg-100);
|
||||
top: -2px;
|
||||
opacity: 1;
|
||||
border-radius: 0;
|
||||
height: 11px;
|
||||
left: 0px;
|
||||
|
||||
transition-delay: 0s;
|
||||
}
|
||||
}
|
||||
.review-list {
|
||||
display: flex;
|
||||
|
||||
gap: 1.2rem;
|
||||
height: 400px;
|
||||
align-items: flex-start;
|
||||
.review-item {
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
.upper-box {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
position: relative;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, #000100);
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
position: absolute;
|
||||
&:first-of-type {
|
||||
z-index: -1;
|
||||
}
|
||||
&:last-of-type {
|
||||
z-index: -2;
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: 1.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
h3 {
|
||||
color: var(--neutral-white);
|
||||
font-family: Outfit;
|
||||
font-size: 1rem;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: normal;
|
||||
}
|
||||
p {
|
||||
color: var(--neutral-white);
|
||||
font-family: Poly;
|
||||
font-size: 1rem;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
.date {
|
||||
color: var(--neutral-white);
|
||||
border-top: 1px solid var(--neutral-white);
|
||||
font-size: 0.7rem;
|
||||
padding-top: 6px;
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
.rating-bar {
|
||||
width: 100%;
|
||||
height: 9px;
|
||||
background: rgba(47, 72, 88, 0.2);
|
||||
.rating-filled {
|
||||
background: var(--text-invers-100);
|
||||
height: 100%;
|
||||
left: 0px;
|
||||
position: relative;
|
||||
.star-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
padding: 0spx;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0%;
|
||||
transform: translate(50%, -35%);
|
||||
span {
|
||||
font-family: Outfit;
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 14px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,70 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { deleteCartItem, updateCartItem } from "../../../functions/CommerceAPIs/bigCommerce/cart"
|
||||
import { deleteCookie } from "../../../functions/utils"
|
||||
import ProductInCartPreview from "../product/ProductInCartPreview.svelte"
|
||||
import LoadingWrapper from "../../widgets/LoadingWrapper.svelte"
|
||||
const dispatcher = createEventDispatcher()
|
||||
async function removeItem(id: string) {
|
||||
if (cart.lines.length == 1) {
|
||||
deleteCookie("cartId")
|
||||
await deleteCartItem(cart.id, id, true)
|
||||
dispatcher("removeCart")
|
||||
} else cart = await deleteCartItem(cart.id, id)
|
||||
}
|
||||
interface UpdateItemQuantity {
|
||||
merchandiseId: string
|
||||
productId: string
|
||||
quantity: number
|
||||
entityId: string
|
||||
}
|
||||
async function updateItemQuantity(line: UpdateItemQuantity) {
|
||||
cart = await updateCartItem(cart.id, line)
|
||||
}
|
||||
export let cart: BKDFCart,
|
||||
hideActions = false,
|
||||
showQuantity: boolean = false
|
||||
let loading = -1
|
||||
</script>
|
||||
|
||||
<ul class="products">
|
||||
{#each cart.lines as item, i (item?.id)}
|
||||
<li class="product">
|
||||
<LoadingWrapper
|
||||
active="{loading == i}"
|
||||
styles=" display: flex; width: 100%;flex-direction: column; gap: 24px;"
|
||||
>
|
||||
<ProductInCartPreview
|
||||
item="{item}"
|
||||
hideActions="{hideActions}"
|
||||
showQuantity="{showQuantity}"
|
||||
on:updateQuantity="{(e) => {
|
||||
loading = i
|
||||
updateItemQuantity({
|
||||
merchandiseId: item.merchandise.id,
|
||||
quantity: e.detail.quantity,
|
||||
productId: item.merchandise.product.id,
|
||||
entityId: item.id,
|
||||
}).finally(() => (loading = -1))
|
||||
}}"
|
||||
on:remove="{() => {
|
||||
loading = i
|
||||
removeItem(item.id).finally(() => (loading = -1))
|
||||
}}"
|
||||
/></LoadingWrapper
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style lang="less">
|
||||
.products {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
.product {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,200 +0,0 @@
|
||||
<script
|
||||
lang="ts"
|
||||
context="module"
|
||||
>
|
||||
import "simplebar"
|
||||
import "simplebar/dist/simplebar.css"
|
||||
import ResizeObserver from "resize-observer-polyfill"
|
||||
if (typeof window !== "undefined") window.ResizeObserver = ResizeObserver
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { mdiCartCheck } from "@mdi/js"
|
||||
import { getCart } from "../../../functions/CommerceAPIs/bigCommerce/cart"
|
||||
import { deleteCookie, getCookie } from "../../../functions/utils"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import CartProducts from "./CartProducts.svelte"
|
||||
import Loader from "../Loader.svelte"
|
||||
import { newNotification } from "../../../store"
|
||||
import { minimumForFreeShipping } from "../../../../config"
|
||||
|
||||
const cartId = getCookie("cartId")
|
||||
export let cart: BKDFCart | null = null,
|
||||
hideActions = false,
|
||||
showQuantity: boolean = false,
|
||||
shippingIncludedInTotal = false
|
||||
|
||||
let loading = false
|
||||
async function setCart(id: string) {
|
||||
loading = true
|
||||
cart = await getCart(id).catch(() => {
|
||||
loading = false
|
||||
newNotification({
|
||||
class: "error",
|
||||
html: `Fehler beim Laden des Warenkorbs. Bitte laden Sie die Seite neu.`,
|
||||
})
|
||||
deleteCookie("cartId")
|
||||
})
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function refetchCartAndGoToCheckout() {
|
||||
loading = true
|
||||
cart = await getCart(cartId).catch(() => {
|
||||
loading = false
|
||||
newNotification({
|
||||
class: "error",
|
||||
html: `Fehler beim Laden des Warenkorbs. Bitte laden Sie die Seite neu.`,
|
||||
})
|
||||
deleteCookie("cartId")
|
||||
})
|
||||
loading = false
|
||||
if (cart) {
|
||||
window.location.href = cart.checkoutUrl
|
||||
}
|
||||
}
|
||||
if (cartId && !cart) setCart(cartId)
|
||||
</script>
|
||||
|
||||
{#if cart}
|
||||
<div
|
||||
class="product-listing-wrapper"
|
||||
data-simplebar
|
||||
>
|
||||
<section class="product-listing-inner">
|
||||
<CartProducts
|
||||
bind:cart="{cart}"
|
||||
on:removeCart="{() => (cart = null)}"
|
||||
hideActions="{hideActions}"
|
||||
showQuantity="{showQuantity}"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
<section
|
||||
class="cart-summary"
|
||||
style="{cart.checkoutUrl ? '' : 'height: unset'} "
|
||||
>
|
||||
{#if Number(cart.cost.discountedAmount.amount) > 0}
|
||||
<div class="discount">
|
||||
<div>Rabatt</div>
|
||||
<div>-{cart.cost.discountedAmount.amount} €</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if Number(cart?.cost?.couponDiscount?.amount || 0) > 0}
|
||||
<div class="discount">
|
||||
<div>Rabatt Codes</div>
|
||||
<div>-{cart.cost.couponDiscount.amount} €</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="discount">
|
||||
<div>Versand</div>
|
||||
<div>
|
||||
{#if Number(cart.cost.amount.amount) >= minimumForFreeShipping}
|
||||
<span>Kostenfrei</span>
|
||||
{:else}
|
||||
<span>5,00 €</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="total">
|
||||
<em>Gesamt</em>
|
||||
<div>
|
||||
{(
|
||||
Number(cart.cost.amount.amount) +
|
||||
(Number(cart.cost.amount.amount) >= minimumForFreeShipping || shippingIncludedInTotal ? 0 : 5)
|
||||
).toFixed(2)} €
|
||||
</div>
|
||||
</div>
|
||||
{#if cart.checkoutUrl}
|
||||
<button
|
||||
class="checkout"
|
||||
on:click="{() => refetchCartAndGoToCheckout()}"
|
||||
><Icon path="{mdiCartCheck}" /> <span>Zur Kasse</span></button
|
||||
>
|
||||
{/if}
|
||||
</section>
|
||||
{:else if loading}
|
||||
<Loader size="3" />
|
||||
{:else}
|
||||
<p class="no-products">Keine Produkte im Warenkorb</p>
|
||||
{/if}
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
.no-products {
|
||||
padding-left: 90px;
|
||||
@media @mobile {
|
||||
padding-left: 38px;
|
||||
}
|
||||
}
|
||||
.product-listing-wrapper {
|
||||
width: 100%;
|
||||
max-height: calc(100% - 255px);
|
||||
@media @mobile {
|
||||
max-height: calc(100% - 200px);
|
||||
}
|
||||
flex-grow: 1;
|
||||
.product-listing-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding: 24px 1rem 24px 90px;
|
||||
@media @mobile {
|
||||
padding: 24px 2.8rem 24px 38px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cart-summary {
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
padding: 24px 1rem 24px 90px;
|
||||
@media @mobile {
|
||||
padding: 24px 2rem 24px 38px;
|
||||
}
|
||||
border-top: 1px solid var(--text-300);
|
||||
height: 255px;
|
||||
@media @mobile {
|
||||
height: 200px;
|
||||
padding-top: 1.2rem;
|
||||
padding-bottom: 1.2rem;
|
||||
}
|
||||
.discount,
|
||||
.total {
|
||||
padding: 0.6rem 1.2rem;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.discount {
|
||||
border-bottom: 1px solid var(--text-300);
|
||||
}
|
||||
.total {
|
||||
background-color: var(--bg-100);
|
||||
color: var(--neutral-white);
|
||||
em {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
line-height: 0.7rem;
|
||||
}
|
||||
}
|
||||
.checkout {
|
||||
width: 100%;
|
||||
padding: 0.6rem 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--neutral-white);
|
||||
font-size: 1rem;
|
||||
line-height: 0.7rem;
|
||||
background-color: var(--primary-100);
|
||||
box-shadow: 0px -2px 0px 0px rgba(0, 0, 0, 0.25);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 35px 0px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,86 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { getCart } from "../../../functions/CommerceAPIs/bigCommerce/cart"
|
||||
import { getBCGraphProductsByCategory } from "../../../functions/CommerceAPIs/bigCommerce/product"
|
||||
import { getCookie } from "../../../functions/utils"
|
||||
import ProductPreview from "../product/ProductPreview.svelte"
|
||||
const dispatch = createEventDispatcher()
|
||||
const cartId = getCookie("cartId")
|
||||
let products: BKDFProduct[] = []
|
||||
async function loadProductRecommendations() {
|
||||
const cart = await getCart(cartId).catch((error) => {
|
||||
console.log(error)
|
||||
})
|
||||
if (cart) {
|
||||
const categories = cart.lines.map((l) => {
|
||||
return l.merchandise.product.categories[0].id
|
||||
})
|
||||
const productsOfCategories: BKDFProduct[] = []
|
||||
const promises = categories.map(async (category) => {
|
||||
const products = await getBCGraphProductsByCategory(category)
|
||||
if (products.length > 0) {
|
||||
products.forEach((p) => {
|
||||
if (!productsOfCategories.find((product) => product.id === p.id)) {
|
||||
productsOfCategories.push(p)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
if (productsOfCategories.length == 0) {
|
||||
dispatch("removeOverlay")
|
||||
return
|
||||
}
|
||||
// take 5 random indices of products not already in cart and every index only once and make sure at
|
||||
let i = 0
|
||||
const randomIndizes: number[] = []
|
||||
while (randomIndizes.length < 5) {
|
||||
i++
|
||||
if (i > 100) {
|
||||
break
|
||||
}
|
||||
const randomIndex = Math.floor(Math.random() * productsOfCategories.length)
|
||||
if (
|
||||
!randomIndizes.includes(randomIndex) &&
|
||||
!cart.lines.find((line) => line.merchandise.product.id === productsOfCategories[randomIndex].id)
|
||||
) {
|
||||
randomIndizes.push(randomIndex)
|
||||
}
|
||||
}
|
||||
|
||||
products = randomIndizes.map((index) => productsOfCategories[index])
|
||||
dispatch("showOverlay")
|
||||
} else dispatch("removeOverlay")
|
||||
}
|
||||
loadProductRecommendations()
|
||||
</script>
|
||||
|
||||
{#if products.length > 0}
|
||||
<div class="product-overlay-listing-wrapper">
|
||||
<ul>
|
||||
{#each products as product}
|
||||
<ProductPreview
|
||||
product="{product}"
|
||||
brightVersion="{true}"
|
||||
/>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.product-overlay-listing-wrapper {
|
||||
li.product-preview {
|
||||
width: 100% !important;
|
||||
}
|
||||
& > ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,60 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getBCGraphProductsByIds } from "../../../functions/CommerceAPIs/bigCommerce/product"
|
||||
import { wishlist } from "../../../store"
|
||||
import Loader from "../Loader.svelte"
|
||||
import ProductInCartPreview from "../product/ProductInCartPreview.svelte"
|
||||
async function loadProducts(): Promise<Partial<BKDFCartItem[]>> {
|
||||
const products = await getBCGraphProductsByIds($wishlist.items.map((p) => String(p.product_id)))
|
||||
const productsFormated: Partial<BKDFCartItem[]> = products.map((p) => {
|
||||
const variant = p.variants.find(
|
||||
(v) =>
|
||||
Number(v.id) ==
|
||||
Number($wishlist.items.find((w) => String(w.product_id) === String(p.id))?.variant_id)
|
||||
)
|
||||
return {
|
||||
previewImage: p.featuredImage,
|
||||
merchandise: {
|
||||
id: variant.id,
|
||||
selectedOptions: variant?.selectedOptions,
|
||||
product: p,
|
||||
},
|
||||
}
|
||||
})
|
||||
return productsFormated
|
||||
}
|
||||
let reload = false
|
||||
</script>
|
||||
|
||||
{#key $wishlist?.items?.length}
|
||||
{#if !$wishlist?.items?.length}
|
||||
<p>Keine Produkte in der Wunschliste</p>
|
||||
{:else}
|
||||
{#await loadProducts()}
|
||||
<Loader size="3" />
|
||||
{:then products}
|
||||
{#if products.length == 0}
|
||||
<p>Keine Produkte in der Wunschliste</p>
|
||||
{:else}
|
||||
<ul>
|
||||
{#each products as product}
|
||||
<li>
|
||||
<ProductInCartPreview
|
||||
item="{product}"
|
||||
on:removedFavorite="{() => (reload = !reload)}"
|
||||
hideActions="{true}"
|
||||
showFavoriteActionButtons="{true}"
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>{/if}
|
||||
{/await}
|
||||
{/if}
|
||||
{/key}
|
||||
|
||||
<style lang="less">
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,44 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { login } from "../../../store"
|
||||
import Button from "../../widgets/Button.svelte"
|
||||
import FavoriteListItems from "./FavoriteListItems.svelte"
|
||||
</script>
|
||||
|
||||
<section class="wishlistWrapper">
|
||||
{#if $login}
|
||||
<FavoriteListItems />
|
||||
{:else}
|
||||
<p>Um deine Favoriten zu sehen, musst du eingeloggt sein.</p>
|
||||
<div>
|
||||
<Button
|
||||
button="{{
|
||||
ctaType: 0,
|
||||
page: '/profile/login',
|
||||
buttonTarget: '_self',
|
||||
buttonText: 'Einloggen',
|
||||
}}"
|
||||
/>
|
||||
<Button
|
||||
button="{{
|
||||
ctaType: 1,
|
||||
page: '/profile/register',
|
||||
buttonTarget: '_self',
|
||||
buttonText: 'Registrieren',
|
||||
}}"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
.wishlistWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
padding: 24px 1rem 24px 90px;
|
||||
@media @mobile {
|
||||
padding: 24px 2rem 24px 38px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,134 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getCachedEntries } from "../../../../api"
|
||||
import { spaLink } from "../../../actions"
|
||||
import MedialibImage from "../../widgets/MedialibImage.svelte"
|
||||
import Loader from "../Loader.svelte"
|
||||
import CardWrapper from "../profile/CardWrapper.svelte"
|
||||
export let chapter: HelpCenterChapter
|
||||
let questions: ContentEntry[]
|
||||
let loading = true
|
||||
getCachedEntries("content", { type: "helpcenterQuestion", path: { $in: chapter.questions.map((q) => q.page) } })
|
||||
.then((res) => {
|
||||
questions = res
|
||||
})
|
||||
.finally(() => (loading = false))
|
||||
</script>
|
||||
|
||||
<CardWrapper>
|
||||
<div
|
||||
class="iconCardWrapper"
|
||||
slot="header"
|
||||
>
|
||||
<div class="inner-wrapper">
|
||||
<div class="blackBox"></div>
|
||||
<MedialibImage id="{chapter.brightIcon}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="innerWrapper">
|
||||
<h3>{chapter.title}</h3>
|
||||
<ul class="questions">
|
||||
{#if loading}
|
||||
<Loader size="3" />
|
||||
{:else if questions}
|
||||
{#each questions as question, i}
|
||||
{#if i < 3}
|
||||
<li class="question">
|
||||
<a
|
||||
href="/helpCenter/{chapter.slug}{question.path}"
|
||||
use:spaLink
|
||||
>
|
||||
{question.question}
|
||||
</a>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<p>Es wurden keine Fragen gefunden.</p>
|
||||
{/if}
|
||||
</ul>
|
||||
<div class="action-row">
|
||||
<button class="cta secondary">
|
||||
<a
|
||||
href="/helpCenter/{chapter.slug}"
|
||||
use:spaLink>weitere Fragen</a
|
||||
></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</CardWrapper>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.iconCardWrapper {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
width: 4.8rem;
|
||||
height: 4.8rem;
|
||||
.inner-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.blackBox {
|
||||
position: absolute;
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
background-color: var(--bg-100);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
img {
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
position: relative;
|
||||
object-fit: contain;
|
||||
z-index: 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
.innerWrapper {
|
||||
padding: 0px 2.4rem 2.4rem 2.4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 1.8rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
h3 {
|
||||
font-size: 1.6rem;
|
||||
line-height: 1.6rem;
|
||||
font-weight: 700;
|
||||
border-bottom: 2px solid var(--bg-100);
|
||||
padding-bottom: 0.6rem;
|
||||
}
|
||||
ul {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
flex-grow: 1;
|
||||
gap: 0.6rem !important;
|
||||
width: 100%;
|
||||
li {
|
||||
padding: 0.6rem 1.2rem;
|
||||
border-radius: 2px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--bg-100);
|
||||
a {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
font-family: Outfit;
|
||||
}
|
||||
}
|
||||
}
|
||||
.action-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,182 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiArrowLeft } from "@mdi/js"
|
||||
import { spaLink } from "../../../actions"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import { getHelpCenterChapters } from "../../../functions/CommerceAPIs/tibiEndpoints/helpCenter"
|
||||
import Loader from "../Loader.svelte"
|
||||
import MedialibImage from "../../widgets/MedialibImage.svelte"
|
||||
import ChapterQuestionPreview from "./ChapterQuestionPreview.svelte"
|
||||
import ChapterQuestionDetailed from "./ChapterQuestionDetailed.svelte"
|
||||
|
||||
export let location: LocationStore
|
||||
$: pathBefore = location.path.split("/").slice(0, -1).join("/")
|
||||
let chapters: HelpCenterChapter[]
|
||||
getHelpCenterChapters().then((res) => {
|
||||
chapters = res
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="chapterDetailed">
|
||||
<div class="return">
|
||||
<a
|
||||
use:spaLink
|
||||
href="{pathBefore}"><Icon path="{mdiArrowLeft}" /> <span>Zurück</span></a
|
||||
>
|
||||
</div>
|
||||
<div class="main">
|
||||
<ul class="nav">
|
||||
{#if !Array.isArray(chapters)}
|
||||
<Loader size="3" />
|
||||
{:else}
|
||||
{#each chapters as chapter}
|
||||
<li class:active="{location.path.includes(`/helpCenter/${chapter.slug}`)}">
|
||||
<a
|
||||
href="/helpCenter/{chapter.slug}"
|
||||
use:spaLink
|
||||
>
|
||||
<span class="icon">
|
||||
<MedialibImage
|
||||
id="{location.path.includes(`/helpCenter/${chapter.slug}`)
|
||||
? chapter.brightIcon
|
||||
: chapter.darkIcon}"
|
||||
/>
|
||||
</span>
|
||||
{chapter.title}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
{#if pathBefore == "/helpCenter" || pathBefore == "/helpCenter/"}
|
||||
<ul class="questions">
|
||||
{#if !Array.isArray(chapters)}
|
||||
<Loader size="3" />
|
||||
{:else}
|
||||
{#each chapters as chapter}
|
||||
<ChapterQuestionPreview
|
||||
chapter="{chapter}"
|
||||
location="{location}"
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
{:else}
|
||||
<ChapterQuestionDetailed location="{location}" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
@import "../../../../lib/assets/css/variables.less";
|
||||
.chapterDetailed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
.return {
|
||||
a {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
font-weight: 400;
|
||||
font-family: Outfit;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
gap: 2.4rem;
|
||||
@media @mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
.nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 360px;
|
||||
|
||||
@media @mobile {
|
||||
min-width: 100%;
|
||||
}
|
||||
li {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-100);
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
background-color: var(--neutral-white);
|
||||
a {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0.6rem 1.2rem;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
.icon {
|
||||
min-height: 1.2rem;
|
||||
min-width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
width: 1.2rem;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
color: var(--bg-100);
|
||||
}
|
||||
&.active {
|
||||
background: var(--bg-100);
|
||||
a {
|
||||
color: var(--neutral-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.questions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
flex-grow: 1;
|
||||
.question {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
padding: 0.6rem 0px;
|
||||
border-bottom: 1px solid var(--bg-100);
|
||||
cursor: pointer;
|
||||
a {
|
||||
color: var(--bg-100);
|
||||
font-weight: 400;
|
||||
font-family: Outfit;
|
||||
}
|
||||
button {
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
min-width: 1.6rem;
|
||||
min-height: 1.6rem;
|
||||
border: 2px solid var(--bg-100);
|
||||
background-color: var(--neutral-white);
|
||||
transform: rotate(45deg);
|
||||
color: var(--bg-100);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
&:hover {
|
||||
button {
|
||||
transform: rotate(0);
|
||||
background-color: var(--bg-100);
|
||||
color: var(--neutral-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,51 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getCachedEntry } from "../../../../api"
|
||||
import NotFound from "../../../../routes/NotFound.svelte"
|
||||
import ContentBlock from "../ContentBlock.svelte"
|
||||
import Loader from "../Loader.svelte"
|
||||
import Index from "../SEO/Index.svelte"
|
||||
export let location: LocationStore
|
||||
let question: ContentEntry
|
||||
let loading = true
|
||||
getCachedEntry("content", {
|
||||
type: "helpcenterQuestion",
|
||||
path: `/${location.path.split("/").filter(Boolean).pop()}`,
|
||||
})
|
||||
.then((res) => {
|
||||
question = res
|
||||
})
|
||||
.finally(() => {
|
||||
loading = false
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
<Loader size="3" />
|
||||
{:else if question}
|
||||
<Index
|
||||
title="{question.question} - BinKrassDuFass"
|
||||
keywords="Hilfe, FAQ, Fragen, Antworten, BinKrassDuFass"
|
||||
metaDescription="Antwort auf die Frage {question.question}."
|
||||
article="{true}"
|
||||
/>
|
||||
<div class="blocks">
|
||||
{#each question.blocks || [] as block, idx}
|
||||
<ContentBlock
|
||||
block="{block}"
|
||||
noHorizontalMargin="{true}"
|
||||
verticalPadding="{idx !== 0}"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<NotFound />
|
||||
{/if}
|
||||
|
||||
<style lang="less">
|
||||
.blocks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,47 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiLinkVariant } from "@mdi/js"
|
||||
import { getCachedEntries } from "../../../../api"
|
||||
import { spaLink, spaNavigate } from "../../../actions"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import Loader from "../Loader.svelte"
|
||||
import Index from "../SEO/Index.svelte"
|
||||
|
||||
export let chapter: HelpCenterChapter, location: LocationStore
|
||||
getCachedEntries("content", {
|
||||
type: "helpcenterQuestion",
|
||||
path: { $in: chapter.questions.map((q) => q.page) },
|
||||
}).then((res) => {
|
||||
questions = res
|
||||
})
|
||||
let questions: ContentEntry[]
|
||||
</script>
|
||||
|
||||
{#if location.path.includes(`/helpCenter/${chapter.slug}`)}
|
||||
<Index
|
||||
title="{chapter.title} - BinKrassDuFass"
|
||||
keywords="Hilfe, FAQ, Fragen, Antworten, BinKrassDuFass"
|
||||
metaDescription="Häufig gestellte Fragen und Antworten zu BinKrassDuFass im Themengebiet {chapter.title}."
|
||||
/>
|
||||
{#if !Array.isArray(questions)}
|
||||
<Loader size="3" />
|
||||
{:else}
|
||||
{#each questions as question}
|
||||
<li class="question">
|
||||
<a
|
||||
href="/helpCenter/{chapter.slug}{question.path}"
|
||||
use:spaLink
|
||||
>
|
||||
{question.question}
|
||||
</a>
|
||||
<button
|
||||
aria-label="Link zur Frage"
|
||||
on:click="{() => {
|
||||
spaNavigate(`/helpCenter/${chapter.slug}${question.path}`)
|
||||
}}"
|
||||
>
|
||||
<Icon path="{mdiLinkVariant}" />
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -1,89 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { icons } from "../../../../config"
|
||||
import { addProductToCart } from "../../../functions/helper/product"
|
||||
import { newNotification } from "../../../store"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import ToggleFavorite from "./ToggleFavorite.svelte"
|
||||
import ProductQuantity from "./widgets/ProductQuantity.svelte"
|
||||
export let variant: BKDFProductVariant,
|
||||
possibleVariants: BKDFProductVariant[],
|
||||
mobileFormat = false
|
||||
let quantity = 1
|
||||
|
||||
function noVariantError() {
|
||||
newNotification({
|
||||
class: "error",
|
||||
html: `Bitte wähle eine eindeutige Variante aus. Du musst sowohl eine Farbe als auch eine Größe auswählen.`,
|
||||
})
|
||||
}
|
||||
|
||||
function addToCart(variant: BKDFProductVariant, quantity: number) {
|
||||
loading = true
|
||||
addProductToCart(variant, quantity)
|
||||
.then(() => {
|
||||
loading = false
|
||||
})
|
||||
.catch(() => {
|
||||
loading = false
|
||||
})
|
||||
}
|
||||
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
<div
|
||||
id="actionRow"
|
||||
class="actionRow"
|
||||
class:mobileFormat="{mobileFormat}"
|
||||
>
|
||||
<ProductQuantity bind:quantity="{quantity}" />
|
||||
<button
|
||||
disabled="{loading}"
|
||||
id="addToCart"
|
||||
class="cta primary"
|
||||
on:click="{() => {
|
||||
if (!variant) return noVariantError()
|
||||
addToCart(variant, quantity)
|
||||
}}"
|
||||
>
|
||||
<Icon
|
||||
path="{icons.shoppingBag}"
|
||||
color="#F3EED9"
|
||||
width="{24}px"
|
||||
height="{24}px"
|
||||
props="{{ 'fill-rule': 'evenodd', 'clip-rule': 'evenodd' }}"
|
||||
/>
|
||||
IN DIE TASCHE</button
|
||||
>
|
||||
<ToggleFavorite
|
||||
productId="{Number(!variant ? possibleVariants[0]?.parentId : variant?.parentId)}"
|
||||
variantid="{Number(!variant ? possibleVariants[0]?.id : variant?.id)}"
|
||||
bigFormat="{mobileFormat}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
.actionRow {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-top: 1.2rem;
|
||||
grid-template-columns: 1fr 8fr 1fr;
|
||||
&.mobileFormat {
|
||||
grid-template-columns: 1fr 3fr;
|
||||
gap: 0;
|
||||
// give thrid item full width not just 1 fr
|
||||
}
|
||||
#addToCart {
|
||||
border-radius: 2px;
|
||||
padding: 0.6rem 1.2rem 0.6rem 1.2rem;
|
||||
flex-grow: 1;
|
||||
font-weight: 700;
|
||||
font-family: Outfit-Bold, sans-serif;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,37 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
|
||||
import { icons } from "../../../../config"
|
||||
import { addProductToCart } from "../../../functions/helper/product"
|
||||
|
||||
export let productId, variantId: number
|
||||
</script>
|
||||
|
||||
<button
|
||||
aria-label="In den Warenkorb legen"
|
||||
on:click="{() => {
|
||||
addProductToCart(
|
||||
{
|
||||
id: variantId,
|
||||
parentId: productId,
|
||||
},
|
||||
1
|
||||
)
|
||||
}}"
|
||||
>
|
||||
<Icon
|
||||
path="{icons.shoppingBag}"
|
||||
color="#2f4858"
|
||||
width="{24}px"
|
||||
height="{24}px"
|
||||
props="{{ 'fill-rule': 'evenodd', 'clip-rule': 'evenodd' }}"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<style lang="less">
|
||||
button {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
</style>
|
||||
@@ -1,3 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let CL: CompleteYourLook
|
||||
</script>
|
||||
@@ -1,31 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getCachedEntries } from "../../../../api"
|
||||
import ContentBlock from "../ContentBlock.svelte"
|
||||
export let bigCommerceProductId: number
|
||||
getCachedEntries("content", {
|
||||
products: bigCommerceProductId,
|
||||
type: "product",
|
||||
}).then((res) => {
|
||||
contentEntries = res
|
||||
})
|
||||
let contentEntries: ContentEntry
|
||||
</script>
|
||||
|
||||
<div class="dRows">
|
||||
{#each contentEntries || [] as contentEntry}
|
||||
{#each contentEntry.blocks || [] as block, idx}
|
||||
<ContentBlock
|
||||
block="{block}"
|
||||
noHorizontalMargin="{true}"
|
||||
/>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.dRows {
|
||||
}
|
||||
</style>
|
||||
@@ -1,71 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { spaNavigate } from "../../../actions"
|
||||
import { categories } from "../../../store"
|
||||
import FilterBlock from "./widgets/FilterBlock.svelte"
|
||||
function findCategoryByPath(categories: Category[], path: string): Category {
|
||||
for (const category of categories) {
|
||||
if (category.path === path) {
|
||||
return category
|
||||
}
|
||||
const foundInChildren = findCategoryByPath(category.children, path)
|
||||
if (foundInChildren) {
|
||||
return foundInChildren
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
function getDepthOfCategory(categories: Category[], path: string): number {
|
||||
for (const category of categories) {
|
||||
if (category.path === path) {
|
||||
return 0
|
||||
}
|
||||
const foundInChildren = getDepthOfCategory(category.children, path)
|
||||
if (foundInChildren !== undefined) {
|
||||
return foundInChildren + 1
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
export let path: string
|
||||
const category = findCategoryByPath($categories, path)
|
||||
</script>
|
||||
|
||||
{#if category}
|
||||
<div class="filter-wrapper">
|
||||
<FilterBlock
|
||||
title="{category.name}"
|
||||
deletable="{getDepthOfCategory($categories, path) > 0}"
|
||||
active="{true}"
|
||||
on:click="{() => {
|
||||
if (getDepthOfCategory($categories, path) > 0) {
|
||||
const newPath = path.endsWith('/') ? path.slice(0, -1) : path
|
||||
spaNavigate(`/collections/${newPath.split('/').slice(0, -1).join('/')}`)
|
||||
}
|
||||
}}"
|
||||
/>
|
||||
{#each category.children as subcategory}
|
||||
<FilterBlock
|
||||
title="{subcategory.name}"
|
||||
active="{false}"
|
||||
on:click="{() => {
|
||||
spaNavigate(`/collections${subcategory.path}`)
|
||||
}}"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="less">
|
||||
.filter-wrapper {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
gap: 12px;
|
||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none; /* Safari and Chrome */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,141 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { backgroundImages } from "../../../store"
|
||||
import MedialibImage from "../../widgets/MedialibImage.svelte"
|
||||
|
||||
export let src: string, alt: string
|
||||
|
||||
let shouldZoom = false,
|
||||
imagePosition: HTMLButtonElement,
|
||||
zoomFactor = 3, // Example zoom factor, adjust for stronger or weaker zoom
|
||||
initialClickPosition = { x: 0, y: 0 },
|
||||
currentPosition = { x: 0, y: 0 },
|
||||
coordX = 0,
|
||||
coordY = 0,
|
||||
lastTouchMoveEvent: number = 0
|
||||
|
||||
function handleMousemove(event: MouseEvent) {
|
||||
if (!shouldZoom) return
|
||||
updatePosition(event.clientX, event.clientY)
|
||||
}
|
||||
|
||||
function handleTouchMove(event: TouchEvent) {
|
||||
if (!shouldZoom) return
|
||||
event.preventDefault()
|
||||
const now = Date.now()
|
||||
if (now - lastTouchMoveEvent < 10) return
|
||||
lastTouchMoveEvent = now
|
||||
updatePosition(event.touches[0].clientX, event.touches[0].clientY)
|
||||
}
|
||||
|
||||
function handleClick(event: MouseEvent | TouchEvent) {
|
||||
shouldZoom = !shouldZoom
|
||||
if (shouldZoom) {
|
||||
if (event instanceof MouseEvent) handleMousemove(event)
|
||||
else if (event instanceof TouchEvent) handleTouchMove(event)
|
||||
} else resetPosition()
|
||||
}
|
||||
|
||||
function updatePosition(clientX: number, clientY: number) {
|
||||
var rect = imagePosition.getBoundingClientRect()
|
||||
currentPosition.x = clientX - rect.left
|
||||
currentPosition.y = clientY - rect.top
|
||||
coordX = currentPosition.x * -1 * zoomFactor
|
||||
coordY = currentPosition.y * -1 * zoomFactor
|
||||
}
|
||||
|
||||
function resetPosition() {
|
||||
initialClickPosition.x = 0
|
||||
initialClickPosition.y = 0
|
||||
coordX = 0
|
||||
coordY = 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="imageholder">
|
||||
<button
|
||||
class="image"
|
||||
aria-label="zoom in image"
|
||||
class:allowZoom="{shouldZoom}"
|
||||
on:click="{handleClick}"
|
||||
on:mousemove="{handleMousemove}"
|
||||
on:touchmove="{handleTouchMove}"
|
||||
bind:this="{imagePosition}"
|
||||
>
|
||||
<img
|
||||
src="{src.replace('2000w', '800w')}"
|
||||
alt="{alt}"
|
||||
/>
|
||||
<div class="zoomimg">
|
||||
<img
|
||||
src="{src}"
|
||||
alt="{alt}"
|
||||
style="left: {coordX}px; top: {coordY}px;"
|
||||
/>
|
||||
<div class="background-img-product">
|
||||
<MedialibImage
|
||||
id="{$backgroundImages['standard']}"
|
||||
filter="l"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="background-img-product">
|
||||
<MedialibImage
|
||||
id="{$backgroundImages['standard']}"
|
||||
filter="l"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
.imageholder {
|
||||
height: 100%;
|
||||
width: fit-content;
|
||||
flex-shrink: 0;
|
||||
.image {
|
||||
background: var(--bg-300);
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.zoomimg {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
background: var(--bg-300);
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0px;
|
||||
bottom: 0;
|
||||
right: 0px;
|
||||
img {
|
||||
position: absolute;
|
||||
width: 400%;
|
||||
height: auto;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition: top 0s ease, left 0s ease;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
cursor: cell;
|
||||
}
|
||||
&.allowZoom:hover {
|
||||
cursor: crosshair;
|
||||
> .zoomimg {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,92 +0,0 @@
|
||||
<script
|
||||
context="module"
|
||||
lang="ts"
|
||||
>
|
||||
import { getDBEntries } from "../../../../api"
|
||||
|
||||
const slideInterval = 5000
|
||||
let productBenefits: productBenefit[] = []
|
||||
getDBEntries("productBenefit").then((data) => {
|
||||
productBenefits = data
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
|
||||
let shownSlide = 0,
|
||||
slideIntervalId: NodeJS.Timeout
|
||||
|
||||
function startSlideInterval() {
|
||||
slideIntervalId = setInterval(() => {
|
||||
shownSlide = (shownSlide + 1) % productBenefits.length
|
||||
}, slideInterval)
|
||||
}
|
||||
|
||||
function stopSlideInterval() {
|
||||
clearInterval(slideIntervalId)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
startSlideInterval()
|
||||
return () => stopSlideInterval()
|
||||
})
|
||||
</script>
|
||||
|
||||
<section class="slider">
|
||||
{#each productBenefits as benefit, index}
|
||||
<button
|
||||
class="slide-button {index === shownSlide ? 'selected' : ''}"
|
||||
on:click="{() => (shownSlide = index)}"
|
||||
on:mouseover="{stopSlideInterval}"
|
||||
on:focus="{stopSlideInterval}"
|
||||
on:mouseout="{startSlideInterval}"
|
||||
on:blur="{startSlideInterval}"
|
||||
>
|
||||
<em>{benefit.title}</em>
|
||||
<p>{benefit.description}</p>
|
||||
</button>
|
||||
{/each}
|
||||
<section class="slider-lines">
|
||||
{#each productBenefits as _, index}
|
||||
<div class="slider-line {index === shownSlide ? 'selected' : ''}"></div>
|
||||
{/each}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
.slider {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.slide-button {
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
padding: 5px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
transition: background-color 0.5s ease-in-out;
|
||||
&.selected {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
.slider-lines {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
.slider-line {
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background-color: grey;
|
||||
margin: 0 2px;
|
||||
transition: background-color 0.5s ease-in-out;
|
||||
&.selected {
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,101 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { getCurrentVariant } from "../../../functions/helper/product"
|
||||
import MedialibImage from "../../widgets/MedialibImage.svelte"
|
||||
import { backgroundImages } from "../../../store"
|
||||
|
||||
export let variants: BKDFProductVariant[],
|
||||
backgroundColor: string = "black",
|
||||
selectedColor: BKDFProductSelectedOption,
|
||||
selectedSize: BKDFProductSelectedOption,
|
||||
product: BKDFProduct,
|
||||
variantImageMappedObject: any
|
||||
|
||||
const dispatcher = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<ul class="productVariants {backgroundColor}">
|
||||
{#each variants.map((v) => v.selectedOptions.find((o) => o.name === "Farbe").value) as value, i}
|
||||
<li>
|
||||
<button
|
||||
class:active="{value === selectedColor?.value}"
|
||||
class:available="{selectedSize
|
||||
? getCurrentVariant(product, [{ name: 'Farbe', value }, selectedSize])?.availableForSale
|
||||
? true
|
||||
: false
|
||||
: true}"
|
||||
on:click="{() => {
|
||||
dispatcher('colorChange', {
|
||||
color: value,
|
||||
})
|
||||
}}"
|
||||
aria-label="Produkt Variante auswählen"
|
||||
>
|
||||
<div class="background-img-product">
|
||||
<MedialibImage
|
||||
id="{$backgroundImages['standard']}"
|
||||
filter="l"
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
src="{variantImageMappedObject[variants?.[i]?.id]?.[0]?.url?.replace('2000w', '64w')}"
|
||||
alt="{variants[i].defaultImage?.altText}"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style lang="less">
|
||||
.productVariants {
|
||||
width: 100%;
|
||||
height: 3.4rem;
|
||||
padding: 2px 0px;
|
||||
overflow: scroll;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
&.black {
|
||||
background-color: black;
|
||||
}
|
||||
&.white {
|
||||
background-attachment: white;
|
||||
}
|
||||
display: flex;
|
||||
button {
|
||||
cursor: pointer;
|
||||
border: 2px solid black;
|
||||
border-radius: 2px;
|
||||
width: 3.4rem;
|
||||
height: 100%;
|
||||
border: 2px solid black;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
position: relative;
|
||||
&.available {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
}
|
||||
&.active {
|
||||
border: 2px solid var(--primary-200);
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
&.white {
|
||||
button {
|
||||
border: 2px solid white;
|
||||
&.active {
|
||||
border: 2px solid var(--primary-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,165 +0,0 @@
|
||||
<script
|
||||
context="module"
|
||||
lang="ts"
|
||||
>
|
||||
import {
|
||||
getCurrentVariant,
|
||||
getProductVariantsThatDifferInOptions,
|
||||
getProductVariantsThatDifferInOptionsGivenSelectedOption,
|
||||
mapVariantImages,
|
||||
} from "../../../functions/helper/product"
|
||||
|
||||
const optionTypes: { name: OptionNames; inputName: string }[] = [
|
||||
{ name: "Farbe", inputName: "color" },
|
||||
{ name: "Größe", inputName: "size" },
|
||||
]
|
||||
|
||||
function updateSelectedOptions(
|
||||
selectedOptions: BKDFProductSelectedOption[],
|
||||
optionName: OptionNames,
|
||||
value: string
|
||||
): BKDFProductSelectedOption[] {
|
||||
const newOptions = [...selectedOptions]
|
||||
const index = newOptions.findIndex((option) => option.name === optionName)
|
||||
if (index !== -1) newOptions[index].value = value
|
||||
else newOptions.push({ name: optionName, value })
|
||||
|
||||
return newOptions
|
||||
}
|
||||
|
||||
function updateColorVariants(
|
||||
product: BKDFProduct,
|
||||
selectedOptions: BKDFProductSelectedOption[]
|
||||
): { possibleColorVariants: BKDFProductVariant[]; colorVariantImageMappedObject: { [key: string]: Image[] } } {
|
||||
const selectedSizeOption = selectedOptions.find((option) => option.name === "Größe")
|
||||
const possibleColorVariants = getProductVariantsThatDifferInOptionsGivenSelectedOption(
|
||||
selectedSizeOption,
|
||||
product.variants
|
||||
)
|
||||
const colorVariantImageMappedObject = mapVariantImages(product, possibleColorVariants)
|
||||
return { possibleColorVariants, colorVariantImageMappedObject }
|
||||
}
|
||||
|
||||
function updateSizeVariants(
|
||||
product: BKDFProduct,
|
||||
selectedOptions: BKDFProductSelectedOption[]
|
||||
): BKDFProductVariant[] {
|
||||
const selectedColorOption = selectedOptions.find((option) => option.name === "Farbe")
|
||||
return getProductVariantsThatDifferInOptionsGivenSelectedOption(selectedColorOption, product.variants)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import ActionRow from "./ActionRow.svelte"
|
||||
import ProductColorPreviewList from "./ProductColorPreviewList.svelte"
|
||||
import ProductSizesPreviewList from "./ProductSizesPreviewList.svelte"
|
||||
|
||||
export let product: BKDFProduct,
|
||||
mobileFormat = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let possibleColorVariants: BKDFProductVariant[] = [],
|
||||
colorVariantImageMappedObject: { [key: string]: Image[] } = {},
|
||||
possibleSizeVariants: BKDFProductVariant[] = [],
|
||||
selectedVariant: BKDFProductVariant,
|
||||
selectedOptions: BKDFProductSelectedOption[] = []
|
||||
|
||||
function handleOptionChange(optionName: OptionNames, value: string) {
|
||||
selectedOptions = updateSelectedOptions(selectedOptions, optionName, value)
|
||||
const currentVariant = getCurrentVariant(product, selectedOptions)
|
||||
if (!!currentVariant) {
|
||||
;({ possibleColorVariants, colorVariantImageMappedObject } = updateColorVariants(
|
||||
product,
|
||||
selectedOptions.filter((option) => option?.name === "Größe")
|
||||
))
|
||||
handleVariantChange(currentVariant)
|
||||
}
|
||||
}
|
||||
|
||||
function handleVariantChange(variant: BKDFProductVariant) {
|
||||
selectedVariant = variant
|
||||
dispatch("variantChange", {
|
||||
variant: selectedVariant,
|
||||
previewImages: colorVariantImageMappedObject[selectedVariant.id],
|
||||
})
|
||||
}
|
||||
|
||||
const reactOnSizingChange = () => {
|
||||
;({ possibleColorVariants, colorVariantImageMappedObject } = updateColorVariants(
|
||||
product,
|
||||
selectedOptions.filter((option) => option?.name === "Größe")
|
||||
))
|
||||
}
|
||||
|
||||
function reactOnColorChange() {
|
||||
possibleSizeVariants = updateSizeVariants(
|
||||
product,
|
||||
selectedOptions.filter((option) => option?.name === "Farbe")
|
||||
)
|
||||
if (!selectedVariant)
|
||||
dispatch("colorChange", {
|
||||
previewImages:
|
||||
colorVariantImageMappedObject[Object.keys(mapVariantImages(product, possibleSizeVariants))[0]],
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
possibleColorVariants = getProductVariantsThatDifferInOptions(["Farbe"], product.variants)
|
||||
possibleSizeVariants = getProductVariantsThatDifferInOptions(["Größe"], product.variants)
|
||||
colorVariantImageMappedObject = mapVariantImages(product, possibleColorVariants)
|
||||
if (!selectedVariant)
|
||||
dispatch("colorChange", {
|
||||
previewImages: colorVariantImageMappedObject[Object.keys(colorVariantImageMappedObject)[0]],
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<section id="configurator">
|
||||
{#each optionTypes as { name }}
|
||||
{#if name == "Farbe"}
|
||||
<ProductColorPreviewList
|
||||
variants="{possibleColorVariants}"
|
||||
selectedSize="{selectedOptions.find((o) => o.name === 'Größe')}"
|
||||
selectedColor="{selectedOptions.find((o) => o.name === 'Farbe')}"
|
||||
product="{product}"
|
||||
variantImageMappedObject="{colorVariantImageMappedObject}"
|
||||
backgroundColor="white"
|
||||
on:colorChange="{(event) => {
|
||||
handleOptionChange(name, event.detail.color)
|
||||
reactOnColorChange()
|
||||
}}"
|
||||
/>
|
||||
{:else if name == "Größe"}
|
||||
<ProductSizesPreviewList
|
||||
possibleSizeVariants="{possibleSizeVariants}"
|
||||
selectedSize="{selectedOptions.find((o) => o.name === 'Größe')}"
|
||||
product="{product}"
|
||||
selectedColor="{selectedOptions.find((o) => o.name === 'Farbe')}"
|
||||
on:sizeChange="{(e) => {
|
||||
handleOptionChange(name, e.detail.size)
|
||||
reactOnSizingChange()
|
||||
}}"
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<ActionRow
|
||||
variant="{selectedVariant}"
|
||||
possibleVariants="{possibleSizeVariants}"
|
||||
mobileFormat="{mobileFormat}"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
#configurator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
margin-top: 1.2rem;
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
</style>
|
||||
@@ -1,202 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { spaLink } from "../../../actions"
|
||||
|
||||
export let product: BKDFProduct
|
||||
let shownIndex: number = -1
|
||||
</script>
|
||||
|
||||
<section id="ExtendableProductDetails">
|
||||
<div
|
||||
class="box"
|
||||
class:opened="{shownIndex === 0}"
|
||||
>
|
||||
<div
|
||||
class="upper"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:keydown
|
||||
on:click="{() => {
|
||||
if (shownIndex === 0) shownIndex = -1
|
||||
else shownIndex = 0
|
||||
}}"
|
||||
>
|
||||
<h4>Beschreibung</h4>
|
||||
<div>
|
||||
<img
|
||||
src="../../../../../media/add-diamond.svg"
|
||||
alt="diamond"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
class:closed="{shownIndex !== 0}"
|
||||
>
|
||||
{@html product.descriptionHtml}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="box"
|
||||
class:opened="{shownIndex === 2}"
|
||||
>
|
||||
<div
|
||||
class="upper"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:keydown
|
||||
on:click="{() => {
|
||||
if (shownIndex === 2) shownIndex = -1
|
||||
else shownIndex = 2
|
||||
}}"
|
||||
>
|
||||
<h4>Lieferung & Rücksendung</h4>
|
||||
<div>
|
||||
<img
|
||||
src="../../../../../media/add-diamond.svg"
|
||||
alt="diamond"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
class:closed="{shownIndex !== 2}"
|
||||
>
|
||||
<ul>
|
||||
<li>
|
||||
<h4>Versandkosten ab 100 Euro gratis</h4>
|
||||
<p>
|
||||
Ab einem Bestellwert von 100 Euro liefern wir deine Bestellung versandkostenfrei. Die Lieferzeit
|
||||
beträgt in der Regel 7 Werktage, sodass du deine neuen Sportklamotten schnell in den Händen
|
||||
hältst.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>Rücksendung</h4>
|
||||
<p>
|
||||
Du kannst deine Bestellung innerhalb von 30 Tagen nach Erhalt der Ware zurücksenden. Die Kosten
|
||||
für die Rücksendung übernehmen wir nicht. Dir wird ein Versandschein zur Verfügung gestellt,
|
||||
wobei die Kosten dafür von dem zurückerstatteten Betrag abgezogen werden. Die Rücksendung kann
|
||||
ganz bequem über unsere Retourenfunktion in deinem persönlichen Konto abgewickelt werden.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>Mehr Details</h4>
|
||||
<p>
|
||||
Weitere Informationen zur Rücksendung findest du in unserer ausführlichen <a
|
||||
href="/widerrufsbelehrung"
|
||||
use:spaLink
|
||||
>
|
||||
Widerrufsbelehrung
|
||||
</a>
|
||||
sowie in unserem
|
||||
<a
|
||||
href="/helpcenter"
|
||||
use:spaLink>HelpCenter</a
|
||||
>. Bei Fragen stehen wir dir jederzeit zur Verfügung.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="box"
|
||||
class:opened="{shownIndex === 1}"
|
||||
>
|
||||
<div
|
||||
class="upper"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:keydown
|
||||
on:click="{() => {
|
||||
if (shownIndex === 1) shownIndex = -1
|
||||
else shownIndex = 1
|
||||
}}"
|
||||
>
|
||||
<h4>GPSR-bezogene Produktinformationen der EU</h4>
|
||||
<div>
|
||||
<img
|
||||
src="../../../../../media/add-diamond.svg"
|
||||
alt="diamond"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
class:closed="{shownIndex !== 1}"
|
||||
>
|
||||
<ul>
|
||||
<li>
|
||||
Hersteller-Kontaktinformationen
|
||||
<ul>
|
||||
<li>Name: Robin Grenzdörfer</li>
|
||||
<li>Email-Adresse: support@binkrassdufass.de</li>
|
||||
<li>Postanschrift: Robin Grenzdörfer; Schultheißstraße 39; 85049 Ingolstadt; Deutschland</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>EU-Garantie: 2 Jahre</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
#ExtendableProductDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 72px;
|
||||
.box {
|
||||
border-bottom: 2px solid var(--text-invers-100);
|
||||
display: flex;
|
||||
transition: padding-bottom 1s;
|
||||
padding-bottom: 0px;
|
||||
flex-direction: column;
|
||||
padding-top: 20px;
|
||||
gap: 20px;
|
||||
&.opened {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.upper {
|
||||
cursor: pointer;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
div {
|
||||
transition: transform 1s;
|
||||
img {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
}
|
||||
}
|
||||
div {
|
||||
}
|
||||
}
|
||||
&.opened {
|
||||
.upper {
|
||||
div {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
max-height: 1000px;
|
||||
overflow: hidden;
|
||||
transition: max-height 1s ease-in;
|
||||
ul {
|
||||
list-style-type: none;
|
||||
li {
|
||||
margin-bottom: 1.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.closed {
|
||||
max-height: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,99 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
import MagnifyableProductImage from "./MagnifyableProductImage.svelte"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import { mdiMagnifyPlusOutline } from "@mdi/js"
|
||||
import { tooltip } from "../../../functions/utils"
|
||||
|
||||
export let previewImages: Image[] = [],
|
||||
mobileView = false
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
const firstImageInSlider = document.querySelector("#productPreviewSlider .wrapper .imageholder")
|
||||
|
||||
if (firstImageInSlider) {
|
||||
const firstImageWidth = firstImageInSlider.clientWidth
|
||||
const wrapper = document.querySelector("#productPreviewSlider .wrapper")
|
||||
if (wrapper) {
|
||||
const viewportWidth = window.innerWidth
|
||||
if (firstImageWidth > viewportWidth) {
|
||||
// Scroll to the center of the first image with smooth transition
|
||||
wrapper.scrollTo({
|
||||
left: (firstImageWidth - viewportWidth) / 2,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
</script>
|
||||
|
||||
<section
|
||||
id="productPreviewSlider"
|
||||
class:mobileView="{mobileView}"
|
||||
>
|
||||
<div class="wrapper">
|
||||
{#each previewImages as previewImage}
|
||||
<MagnifyableProductImage
|
||||
src="{previewImage?.url}"
|
||||
alt="{previewImage?.altText}"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="icon"
|
||||
use:tooltip="{{
|
||||
content: 'Clicke auf das Bild um zu zoomen',
|
||||
}}"
|
||||
>
|
||||
<Icon
|
||||
path="{mdiMagnifyPlusOutline}"
|
||||
size="2.4rem"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
#productPreviewSlider {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: min(65vh, 38.25rem);
|
||||
position: relative;
|
||||
&.mobileView {
|
||||
height: calc(90vh - 86px);
|
||||
}
|
||||
.icon {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
z-index: 2;
|
||||
bottom: 1rem;
|
||||
height: 2.4rem;
|
||||
width: 2.4rem;
|
||||
}
|
||||
.wrapper {
|
||||
display: flex;
|
||||
gap: 0px;
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
position: relative;
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--primary-100);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: black;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,155 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
import ProductQuantity from "./widgets/ProductQuantity.svelte"
|
||||
import { mdiDeleteCircle, mdiOpenInApp } from "@mdi/js"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import ProductPricetags from "./widgets/ProductPricetags.svelte"
|
||||
import ToggleFavorite from "./ToggleFavorite.svelte"
|
||||
import { spaLink } from "../../../actions"
|
||||
import MedialibImage from "../../widgets/MedialibImage.svelte"
|
||||
import { backgroundImages } from "../../../store"
|
||||
export let item: BKDFCartItem,
|
||||
hideActions: boolean = false,
|
||||
showQuantity: boolean = false,
|
||||
showFavoriteActionButtons: boolean = false
|
||||
const product = item.merchandise.product
|
||||
const selectedColor = item.merchandise.selectedOptions?.find((o) => o.name === "Farbe")?.value
|
||||
const previewImage = product.images.find((i) => JSON.parse(i.altText).Farbe === selectedColor)
|
||||
const dispatcher = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<section class="productCartPreview">
|
||||
<div class="imageContainer">
|
||||
<div class="background-img-product">
|
||||
<MedialibImage
|
||||
id="{$backgroundImages['standard']}"
|
||||
filter="m"
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
src="{previewImage?.url?.replace('2000w', '120w')}"
|
||||
alt="{previewImage?.altText}"
|
||||
/>
|
||||
</div>
|
||||
<div class="informations">
|
||||
<section class="details">
|
||||
<h5>{product.title}</h5>
|
||||
{#if item.merchandise.selectedOptions}
|
||||
<small>{item.merchandise.selectedOptions?.map((o) => o.value).join(" | ")}</small>
|
||||
{/if}
|
||||
<slot />
|
||||
<div class="">
|
||||
<ProductPricetags
|
||||
product="{product}"
|
||||
smallVersion="{true}"
|
||||
/>
|
||||
|
||||
{#if !hideActions}
|
||||
<div class="actions">
|
||||
<ProductQuantity
|
||||
quantity="{item.quantity}"
|
||||
on:updateQuantity="{(e) => dispatcher('updateQuantity', e.detail)}"
|
||||
/>
|
||||
<button
|
||||
on:click="{() => dispatcher('remove')}"
|
||||
aria-label="Entfernen"
|
||||
><Icon
|
||||
path="{mdiDeleteCircle}"
|
||||
size="18px"
|
||||
/></button
|
||||
>
|
||||
</div>
|
||||
{:else if showQuantity}
|
||||
<div class="actions">
|
||||
{item.quantity} Stk.
|
||||
</div>
|
||||
{:else if showFavoriteActionButtons}
|
||||
<div class="actions">
|
||||
<ToggleFavorite
|
||||
on:removedFavorite
|
||||
productId="{Number(product.id)}"
|
||||
variantid="{Number(item.merchandise.id)}"
|
||||
/>
|
||||
<button aria-label="Produktseite öffnen"
|
||||
><a
|
||||
href="/product{item.merchandise.product.handle}"
|
||||
use:spaLink
|
||||
><Icon
|
||||
path="{mdiOpenInApp}"
|
||||
size="24px"
|
||||
/></a
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
.productCartPreview {
|
||||
display: flex;
|
||||
gap: 25px;
|
||||
width: 100%;
|
||||
padding-bottom: 1.2rem;
|
||||
border-bottom: 1px solid var(--bg-300);
|
||||
align-items: center;
|
||||
|
||||
.imageContainer {
|
||||
max-height: 6rem;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: auto;
|
||||
img {
|
||||
height: 100%;
|
||||
max-height: 6rem;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
}
|
||||
}
|
||||
.informations {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-grow: 1;
|
||||
.details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
small {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
& > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
button {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
& > div {
|
||||
gap: 12px;
|
||||
flex-direction: column-reverse;
|
||||
max-width: 150px;
|
||||
.actions {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,142 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { spaLink } from "../../../actions"
|
||||
import {
|
||||
getBCGraphBestsellingProducts,
|
||||
getBCGraphFeaturedProducts,
|
||||
getBCGraphNewestProducts,
|
||||
getBCGraphProductsByCategory,
|
||||
getBCGraphProductsByIds,
|
||||
} from "../../../functions/CommerceAPIs/bigCommerce/product"
|
||||
import Loader from "../Loader.svelte"
|
||||
import ProductPreviewList from "./ProductPreviewList.svelte"
|
||||
|
||||
export let block: ContentBlock<"productSlider">
|
||||
const productSlider = block.productSlider
|
||||
let loadProducts: any
|
||||
async function setProductsByIds() {
|
||||
if ("productIds" in productSlider) {
|
||||
const products = await getBCGraphProductsByIds(productSlider.productIds)
|
||||
const orderedProducts = productSlider.productIds.map((id) => products.find((product) => product.id == id))
|
||||
return orderedProducts.filter((product) => product !== undefined)
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
if (productSlider.productSource == "manual") {
|
||||
loadProducts = setProductsByIds
|
||||
} else if (productSlider.productSource == "category") {
|
||||
async function setProductsByCategory() {
|
||||
if ("categoryId" in productSlider) {
|
||||
return getBCGraphProductsByCategory(Number(productSlider.categoryId))
|
||||
}
|
||||
}
|
||||
loadProducts = setProductsByCategory
|
||||
} else if (productSlider.productSource == "bestseller") {
|
||||
async function setProductsByBestseller() {
|
||||
return getBCGraphBestsellingProducts()
|
||||
}
|
||||
loadProducts = setProductsByBestseller
|
||||
} else if (productSlider.productSource == "newProducts") {
|
||||
async function setNewProducts() {
|
||||
return getBCGraphNewestProducts()
|
||||
}
|
||||
loadProducts = setNewProducts
|
||||
} else if (productSlider.productSource == "featured") {
|
||||
async function setFeaturedProducts() {
|
||||
return getBCGraphFeaturedProducts()
|
||||
}
|
||||
loadProducts = setFeaturedProducts
|
||||
} else {
|
||||
loadProducts = setProductsByIds
|
||||
}
|
||||
let products: any[]
|
||||
let loading = true
|
||||
let error
|
||||
loadProducts()
|
||||
.then((res) => {
|
||||
products = res
|
||||
|
||||
if (productSlider.maxProducts && products.length > productSlider.maxProducts) {
|
||||
const randomIndizes: number[] = []
|
||||
while (randomIndizes.length < productSlider.maxProducts) {
|
||||
const randomIndex = Math.floor(Math.random() * products.length)
|
||||
if (!randomIndizes.includes(randomIndex)) {
|
||||
randomIndizes.push(randomIndex)
|
||||
}
|
||||
}
|
||||
products = randomIndizes.map((i) => products[i])
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
error = e
|
||||
})
|
||||
.finally(() => {
|
||||
loading = false
|
||||
})
|
||||
const cta = productSlider.callToActionButtons[0]
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
<Loader
|
||||
size="4"
|
||||
type="bar"
|
||||
/>
|
||||
{:else if products}
|
||||
<div class="product-preview-list-wrapper">
|
||||
{#if productSlider.headline}
|
||||
<div class="headline">
|
||||
<div class="headline-col">
|
||||
{#if productSlider.topLine}
|
||||
<small>
|
||||
{productSlider.topLine}
|
||||
</small>
|
||||
{/if}
|
||||
|
||||
<h2>{productSlider.headline}</h2>
|
||||
</div>
|
||||
{#if cta}
|
||||
<button class="">
|
||||
<a
|
||||
href="{cta.page}"
|
||||
use:spaLink>{cta.buttonText}</a
|
||||
>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<ProductPreviewList products="{products}" />
|
||||
</div>
|
||||
{:else}
|
||||
<p>Es wurden keine Produkte gefunden.</p>
|
||||
{/if}
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
@import "../../../assets/css/variables.less";
|
||||
.product-preview-list-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
width: 100%;
|
||||
.headline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 1.6rem;
|
||||
.headline-col {
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
color: var(--primary-100);
|
||||
}
|
||||
|
||||
small {
|
||||
font-weight: 700;
|
||||
font-family: Outfit-Bold, sans-serif;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,135 +0,0 @@
|
||||
<script lang="ts">
|
||||
import TopRightCrinkle from "../../widgets/TopRightCrinkle.svelte"
|
||||
|
||||
import ProductConfigurator from "./ProductConfigurator.svelte"
|
||||
import ProductExtendableBoxes from "./ProductExtendableBoxes.svelte"
|
||||
import ProductImagePreview from "./ProductImagePreview.svelte"
|
||||
import ProductSummary from "./ProductSummary.svelte"
|
||||
import RecommendedProducts from "./RecommendedProducts.svelte"
|
||||
import ProductBenefits from "./widgets/ProductBenefits.svelte"
|
||||
import ProductRatings from "./widgets/ProductRatings.svelte"
|
||||
import DynamicRows from "./DynamicRows.svelte"
|
||||
|
||||
export let product: BKDFProduct
|
||||
let previewImages: Image[] = [],
|
||||
selectedVariant: BKDFProductVariant | null = null
|
||||
</script>
|
||||
|
||||
<section id="productPage">
|
||||
<div class="product-page-wrapper">
|
||||
<section class="lefternSide">
|
||||
<div class="crinkle">
|
||||
<TopRightCrinkle
|
||||
edges="{false}"
|
||||
brightColor="{true}"
|
||||
/>
|
||||
</div>
|
||||
<div class="outerWrapper">
|
||||
<div class="inner-container">
|
||||
{#if previewImages?.length}
|
||||
<ProductImagePreview previewImages="{previewImages}" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if product}
|
||||
<DynamicRows bigCommerceProductId="{Number(product.id)}" />
|
||||
{/if}
|
||||
|
||||
{#if product}
|
||||
<ProductExtendableBoxes product="{product}" />
|
||||
{/if}
|
||||
{#if product}
|
||||
<ProductRatings bigCommerceProductId="{Number(product.id)}" />
|
||||
<RecommendedProducts product="{product}" />
|
||||
{/if}
|
||||
</section>
|
||||
<section class="righternSide">
|
||||
{#if product}
|
||||
<ProductSummary
|
||||
product="{product}"
|
||||
productTitleAsHeading="{true}"
|
||||
/>
|
||||
<ProductConfigurator
|
||||
product="{product}"
|
||||
on:colorChange="{(e) => {
|
||||
previewImages = e.detail.previewImages
|
||||
}}"
|
||||
on:variantChange="{(e) => {
|
||||
selectedVariant = e.detail.variant
|
||||
previewImages = e.detail.previewImages
|
||||
}}"
|
||||
/>
|
||||
{/if}
|
||||
<ProductBenefits />
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
#productPage {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding-bottom: 3rem;
|
||||
|
||||
justify-content: center;
|
||||
.product-page-wrapper {
|
||||
margin: 0 var(--horizontal-default-margin);
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: var(--normal-max-width);
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
|
||||
.lefternSide {
|
||||
min-height: 100%;
|
||||
width: 0px;
|
||||
flex-grow: 2;
|
||||
margin-bottom: 2.4rem;
|
||||
position: relative;
|
||||
overflow-x: visible;
|
||||
.icon {
|
||||
}
|
||||
.crinkle {
|
||||
position: sticky;
|
||||
width: calc(100% + 5px);
|
||||
height: 0px;
|
||||
overflow: visible;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
top: 146px;
|
||||
|
||||
z-index: 100;
|
||||
}
|
||||
.outerWrapper {
|
||||
height: min(65vh, 38.25rem);
|
||||
width: 100%;
|
||||
.inner-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
@media (min-width: 1770px) {
|
||||
min-width: calc(100% + (100vw - var(--normal-max-width)) / 2);
|
||||
left: calc(-1 * (100vw - var(--normal-max-width)) / 2);
|
||||
}
|
||||
@media (max-width: 1769px) {
|
||||
left: calc(-1 * var(--horizontal-default-margin));
|
||||
min-width: calc(100% + var(--horizontal-default-margin));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.righternSide {
|
||||
width: 0px;
|
||||
flex-grow: 1;
|
||||
|
||||
padding-left: 3.6rem;
|
||||
position: sticky;
|
||||
top: 140px;
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,106 +0,0 @@
|
||||
<script lang="ts">
|
||||
import TopRightCrinkle from "../../widgets/TopRightCrinkle.svelte"
|
||||
import DynamicRows from "./DynamicRows.svelte"
|
||||
import ProductConfigurator from "./ProductConfigurator.svelte"
|
||||
import ProductExtendableBoxes from "./ProductExtendableBoxes.svelte"
|
||||
import ProductImagePreview from "./ProductImagePreview.svelte"
|
||||
import ProductSummary from "./ProductSummary.svelte"
|
||||
import RecommendedProducts from "./RecommendedProducts.svelte"
|
||||
import ProductRatings from "./widgets/ProductRatings.svelte"
|
||||
|
||||
export let product: BKDFProduct
|
||||
let previewImages: Image[] = [],
|
||||
selectedVariant: BKDFProductVariant | null = null
|
||||
</script>
|
||||
|
||||
<div class="crinkle">
|
||||
<TopRightCrinkle
|
||||
edges="{false}"
|
||||
brightColor="{false}"
|
||||
bigVersion="{true}"
|
||||
>
|
||||
<svg
|
||||
slot="icon"
|
||||
width="72"
|
||||
height="71"
|
||||
viewBox="0 0 72 71"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
id="Vector 3"
|
||||
d="M72 0V71L0 0H72Z"
|
||||
fill="#0d0c0c"></path>
|
||||
</svg>
|
||||
</TopRightCrinkle>
|
||||
</div>
|
||||
<section id="productPage">
|
||||
{#if previewImages?.length}
|
||||
<ProductImagePreview
|
||||
previewImages="{previewImages}"
|
||||
mobileView="{true}"
|
||||
/>
|
||||
{/if}
|
||||
<div class="content-wrapper">
|
||||
<ProductSummary
|
||||
product="{product}"
|
||||
productTitleAsHeading="{true}"
|
||||
/>
|
||||
<ProductConfigurator
|
||||
product="{product}"
|
||||
on:colorChange="{(e) => {
|
||||
previewImages = e.detail.previewImages
|
||||
}}"
|
||||
on:variantChange="{(e) => {
|
||||
selectedVariant = e.detail.variant
|
||||
previewImages = e.detail.previewImages
|
||||
}}"
|
||||
mobileFormat="{true}"
|
||||
/>
|
||||
{#if product}
|
||||
<DynamicRows bigCommerceProductId="{Number(product.id)}" />
|
||||
{/if}
|
||||
<ProductExtendableBoxes product="{product}" />
|
||||
{#if product}
|
||||
<ProductRatings bigCommerceProductId="{Number(product.id)}" />
|
||||
<RecommendedProducts product="{product}" />
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
.crinkle {
|
||||
width: calc(100% + 5px);
|
||||
height: 0px;
|
||||
overflow: visible;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
top: 84px;
|
||||
width: 100%;
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
z-index: 1000;
|
||||
}
|
||||
#productPage {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 3rem;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.content-wrapper {
|
||||
padding: 0 var(--horizontal-default-margin);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
max-width: var(--normal-max-width);
|
||||
position: relative;
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,326 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
addProductToCart,
|
||||
getOptionValues,
|
||||
getProductVariantsThatDifferInOptions,
|
||||
mapVariantImages,
|
||||
} from "../../../functions/helper/product"
|
||||
import ProductColorPreviewList from "./ProductColorPreviewList.svelte"
|
||||
import ToggleFavorite from "./ToggleFavorite.svelte"
|
||||
import ProductSummary from "./ProductSummary.svelte"
|
||||
import { spaLink } from "../../../actions"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import { icons } from "../../../../config"
|
||||
import LoadingWrapper from "../../widgets/LoadingWrapper.svelte"
|
||||
import MedialibImage from "../../widgets/MedialibImage.svelte"
|
||||
import { backgroundImages, overlays } from "../../../store"
|
||||
|
||||
export let product: BKDFProduct,
|
||||
shownImage = product.featuredImage,
|
||||
brightVersion = false
|
||||
|
||||
const variants = getProductVariantsThatDifferInOptions(["Farbe"], product.variants),
|
||||
variantImageMappedObject = mapVariantImages(product, variants)
|
||||
let currentVariant = variants[0],
|
||||
hover = false
|
||||
|
||||
function handleMouseOver() {
|
||||
hover = true
|
||||
shownImage = variantImageMappedObject[currentVariant.id][1] || variantImageMappedObject[currentVariant.id][0]
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
hover = false
|
||||
shownImage = variantImageMappedObject[currentVariant.id][0]
|
||||
}
|
||||
|
||||
function handleColorChange(event: CustomEvent) {
|
||||
currentVariant = variants.find(
|
||||
(v) => v.selectedOptions.find((o) => o.name === "Farbe")?.value === event.detail.color
|
||||
)
|
||||
shownImage = variantImageMappedObject[currentVariant.id][0]
|
||||
}
|
||||
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
<li
|
||||
class="product-preview"
|
||||
class:brightVersion="{brightVersion}"
|
||||
>
|
||||
<section
|
||||
class="productImage"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:focus
|
||||
on:mouseover="{handleMouseOver}"
|
||||
on:mouseleave="{handleMouseLeave}"
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
class:active="{hover}"
|
||||
aria-label="Produktinformationen"
|
||||
>
|
||||
<div class="favorite">
|
||||
<button
|
||||
class="favorite-toggle"
|
||||
on:click|stopPropagation
|
||||
on:mouseover|stopPropagation
|
||||
on:focus|stopPropagation
|
||||
aria-label="Favorisieren"
|
||||
>
|
||||
<ToggleFavorite
|
||||
productId="{product.id}"
|
||||
variantid="{currentVariant.id}"
|
||||
brightVersion="{brightVersion}"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="cart-btn"
|
||||
aria-label="In den Warenkorb legen"
|
||||
on:click|preventDefault|stopPropagation="{() => {
|
||||
handleMouseOver()
|
||||
}}"
|
||||
>
|
||||
<Icon
|
||||
path="{icons.shoppingBag}"
|
||||
color="{brightVersion ? '#FFF' : '#2F4858'}"
|
||||
width="1.2rem"
|
||||
height="1.2rem"
|
||||
props="{{ 'fill-rule': 'evenodd', 'clip-rule': 'evenodd' }}"
|
||||
/>
|
||||
</button>
|
||||
<img
|
||||
src="../../../../media/topRightCrinkle{brightVersion ? 'Dark' : 'Bright'}AndUnedged.svg"
|
||||
alt="crinkle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="sizing">
|
||||
<em>CHOOSE YOUR STYLE</em>
|
||||
<ul>
|
||||
{#each getOptionValues(product, "Größe") as size}
|
||||
<li>
|
||||
<button
|
||||
aria-label="Größe wählen"
|
||||
on:click="{() => {
|
||||
let loading = true
|
||||
const selectedColor = currentVariant.selectedOptions.find(
|
||||
(o) => o.name === 'Farbe'
|
||||
)?.value
|
||||
const selectedVariant = product.variants.find(
|
||||
(v) =>
|
||||
v.selectedOptions.find((o) => o.name === 'Farbe')?.value ===
|
||||
selectedColor &&
|
||||
v.selectedOptions.find((o) => o.name === 'Größe')?.value === size
|
||||
)
|
||||
addProductToCart(selectedVariant, 1).then(() => {
|
||||
loading = false
|
||||
if ($overlays.length > 0) {
|
||||
$overlays[0].reload = !$overlays[0].reload
|
||||
}
|
||||
})
|
||||
}}">{size}</button
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{#key loading}
|
||||
<div>
|
||||
<LoadingWrapper active="{loading}">
|
||||
<a
|
||||
href="/product{product.handle}"
|
||||
use:spaLink
|
||||
on:focus
|
||||
aria-label="Produktseite öffnen"
|
||||
>
|
||||
<div class="background-img-product">
|
||||
<MedialibImage
|
||||
id="{$backgroundImages['standard']}"
|
||||
filter="l"
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
src="{shownImage?.url.replace('2000w', '540w')}"
|
||||
alt="{shownImage?.altText}"
|
||||
/>
|
||||
</a>
|
||||
</LoadingWrapper>
|
||||
</div>
|
||||
{/key}
|
||||
</section>
|
||||
|
||||
<ProductColorPreviewList
|
||||
variants="{variants}"
|
||||
bind:currentVariant="{currentVariant}"
|
||||
backgroundColor="black"
|
||||
on:colorChange="{handleColorChange}"
|
||||
variantImageMappedObject="{variantImageMappedObject}"
|
||||
/>
|
||||
|
||||
<ProductSummary
|
||||
product="{product}"
|
||||
brightVersion="{brightVersion}"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
:global .background-img-product {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
.product-preview {
|
||||
overflow-y: hidden;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1.2rem;
|
||||
max-width: 27rem;
|
||||
width: 100%;
|
||||
@media @mobile {
|
||||
width: calc(100vw - 2 * var(--horizontal-default-margin) - 2px);
|
||||
}
|
||||
.productImage {
|
||||
position: relative;
|
||||
|
||||
a {
|
||||
width: 100%;
|
||||
aspect-ratio: 1.2 / 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
div[role="dialog"] {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
height: 3.6rem;
|
||||
width: 3.6rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
img {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
}
|
||||
|
||||
.favorite {
|
||||
height: 3.6rem;
|
||||
position: relative;
|
||||
width: 3.6rem;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-direction: column;
|
||||
|
||||
z-index: 100;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
right: 0px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.cart-btn {
|
||||
display: none;
|
||||
width: 100% !important;
|
||||
z-index: 999;
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
.favorite-toggle {
|
||||
margin-top: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
@media @mobile {
|
||||
justify-content: flex-end;
|
||||
height: 4rem;
|
||||
width: 4rem;
|
||||
flex-direction: column-reverse;
|
||||
.cart-btn {
|
||||
display: block;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.favorite-toggle {
|
||||
margin-top: 10px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
img {
|
||||
top: -3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.sizing {
|
||||
background: rgba(13, 12, 12, 0.5);
|
||||
padding: 0.6rem 0rem 0.6rem 0rem;
|
||||
display: flex;
|
||||
z-index: 99;
|
||||
width: 100%;
|
||||
right: -100%;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
overflow: hidden;
|
||||
em {
|
||||
color: var(--text-100);
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-size: 0.7rem;
|
||||
line-height: 0.7rem;
|
||||
font-family: Outfit-Bold, sans-serif;
|
||||
}
|
||||
ul {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
li {
|
||||
button {
|
||||
padding: 0.3rem 0.6rem 0.3rem 0.6rem;
|
||||
border-right: 1px solid var(--text-100);
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
color: var(--text-100);
|
||||
&:hover {
|
||||
color: var(--text-invers-100);
|
||||
background-color: var(--text-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
width: 100%;
|
||||
.sizing {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,67 +0,0 @@
|
||||
<script
|
||||
lang="ts"
|
||||
context="module"
|
||||
>
|
||||
import "simplebar"
|
||||
import "simplebar/dist/simplebar.css"
|
||||
import ResizeObserver from "resize-observer-polyfill"
|
||||
if (typeof window !== "undefined") window.ResizeObserver = ResizeObserver
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import ProductPreview from "./ProductPreview.svelte"
|
||||
export let products: BKDFProduct[] = []
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="product-preview-list verticalScrollbar"
|
||||
data-simplebar
|
||||
>
|
||||
<ul class="inner-wrapper">
|
||||
{#each products as product (product.id)}
|
||||
<ProductPreview product="{product}" />
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.product-preview-list {
|
||||
width: 100%;
|
||||
max-width: var(--normal-max-width);
|
||||
|
||||
.simplebar-wrapper {
|
||||
padding-bottom: 1.2rem;
|
||||
.simplebar-content {
|
||||
max-width: var(--normal-max-width);
|
||||
& > .inner-wrapper {
|
||||
display: flex;
|
||||
gap: 2.2rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.simplebar-track {
|
||||
background-color: rgba(13, 12, 12, 0.25);
|
||||
height: 7px;
|
||||
overflow: visible;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.simplebar-scrollbar {
|
||||
transition-duration: 0ms !important;
|
||||
cursor: pointer;
|
||||
&::before {
|
||||
background-color: var(--bg-100);
|
||||
top: -2px;
|
||||
opacity: 1;
|
||||
border-radius: 0;
|
||||
height: 11px;
|
||||
left: 0px;
|
||||
|
||||
transition-delay: 0s;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,183 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { getCurrentVariant } from "../../../functions/helper/product"
|
||||
import Modal from "../../Modal.svelte"
|
||||
import SizeChart from "./widgets/SizeChart.svelte"
|
||||
import { modules } from "../../../store"
|
||||
import { getTibiProduct } from "../../../functions/CommerceAPIs/tibiEndpoints/product"
|
||||
|
||||
export let possibleSizeVariants: BKDFProductVariant[] = [],
|
||||
selectedColor: BKDFProductSelectedOption,
|
||||
selectedSize: BKDFProductSelectedOption,
|
||||
product: BKDFProduct
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let showSizingChart = false
|
||||
let forcedWarning = ""
|
||||
async function loadSizeChart() {
|
||||
loading = true
|
||||
const tibiProduct = await getTibiProduct(Number(product.id))
|
||||
const sizingChart = tibiProduct.sizingChart || ({} as TSizeChart)
|
||||
if (tibiProduct.forcedWarning && tibiProduct.forcedWarning.trim().length > 0) {
|
||||
forcedWarning = tibiProduct.forcedWarning
|
||||
}
|
||||
if (sizingChart && "columns" in sizingChart)
|
||||
sizingChart.columns = (sizingChart?.columns || []).map((column) => {
|
||||
if ($modules?.length)
|
||||
column.germanLabelTranslation =
|
||||
$modules.find((module) => module.label === column.label)?.germanLabelTranslation || column.label
|
||||
return column
|
||||
})
|
||||
return sizingChart
|
||||
}
|
||||
let sizingChart: TSizeChart
|
||||
let loading = false
|
||||
$: if ($modules?.length && !sizingChart && !loading)
|
||||
loadSizeChart()
|
||||
.then((res) => {
|
||||
// @ts-ignore
|
||||
sizingChart = res
|
||||
})
|
||||
.finally(() => {
|
||||
loading = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<section id="sizing">
|
||||
<div class="heading">
|
||||
<em><small>CHOOSE SIZE</small></em>
|
||||
<button
|
||||
on:click|preventDefault|stopPropagation="{() => {
|
||||
showSizingChart = true
|
||||
}}">Größentabelle</button
|
||||
>
|
||||
</div>
|
||||
|
||||
<ul class="sizes">
|
||||
{#each possibleSizeVariants.map((v) => v.selectedOptions.find((o) => o.name === "Größe").value) as value}
|
||||
<li
|
||||
class:active="{selectedSize?.value === value}"
|
||||
class="{selectedColor
|
||||
? getCurrentVariant(product, [{ name: 'Größe', value }, selectedColor])?.availableForSale
|
||||
? 'forSale'
|
||||
: 'notForSale'
|
||||
: 'forSale'}"
|
||||
>
|
||||
<button
|
||||
on:click="{() => {
|
||||
dispatch('sizeChange', { size: value })
|
||||
}}"
|
||||
>
|
||||
{value}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<div class="top-heading">
|
||||
{#if forcedWarning}
|
||||
<strong class="hint">Heads Up</strong>
|
||||
<p class="warning">{@html forcedWarning}</p>
|
||||
{:else if sizingChart && sizingChart.generalDescription?.includes("fällt klein aus")}
|
||||
<strong class="hint">Heads Up</strong>
|
||||
<p class="warning">
|
||||
Dieser Artikel fällt <strong>klein</strong> aus. Daher empfehlen wir, eine Nummer größer zu bestellen.
|
||||
</p>
|
||||
{:else if sizingChart && sizingChart.generalDescription?.includes("fällt groß aus")}
|
||||
<strong class="hint">Heads Up</strong>
|
||||
<p class="warning">
|
||||
Dieser Artikel fällt <strong>groß</strong> aus. Daher empfehlen wir, eine Nummer kleiner zu bestellen.
|
||||
</p>
|
||||
{:else if sizingChart && sizingChart.generalDescription?.includes("wenn deine Maße zwischen den Größen liegen")}
|
||||
<strong class="hint">Heads Up</strong>
|
||||
<p class="warning">
|
||||
Solltest du unsicher sein, welche Größe du bestellen sollstest, empfehlen wir, die größere Größe zu
|
||||
wählen.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
{#if showSizingChart}
|
||||
<Modal
|
||||
show="{true}"
|
||||
on:close="{() => {
|
||||
showSizingChart = false
|
||||
}}"
|
||||
>
|
||||
<svelte:fragment slot="title">Größen</svelte:fragment>
|
||||
|
||||
<div class="sizeChart">
|
||||
{#if !loading}
|
||||
<SizeChart sizingChart="{sizingChart}" />
|
||||
{/if}
|
||||
</div>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style lang="less">
|
||||
h2 {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
#sizing {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
.heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
button {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
}
|
||||
.top-heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
.hint {
|
||||
font-size: 0.7rem;
|
||||
color: var(--primary-100);
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
}
|
||||
.warning {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
.sizes {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
li {
|
||||
flex-grow: 1;
|
||||
height: 2rem;
|
||||
|
||||
min-width: 4rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-300);
|
||||
&.notForSale {
|
||||
button {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
&.active {
|
||||
border: 1px solid var(--primary-100);
|
||||
}
|
||||
button {
|
||||
padding: 0rem 1.2rem 0rem 1.2rem;
|
||||
cursor: pointer !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,104 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiInformationOutline } from "@mdi/js"
|
||||
|
||||
import ProductPricetags from "./widgets/ProductPricetags.svelte"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import { tooltip } from "../../../functions/utils"
|
||||
|
||||
export let product: BKDFProduct,
|
||||
productTitleAsHeading: boolean = false,
|
||||
brightVersion: boolean = false
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="productDetails1"
|
||||
class:brightVersion="{brightVersion}"
|
||||
>
|
||||
{#if productTitleAsHeading}
|
||||
<h1>{product.title}</h1>
|
||||
{:else}
|
||||
<em>{product.title}</em>
|
||||
{/if}
|
||||
{#if product.title.includes("QR")}
|
||||
<div
|
||||
use:tooltip="{{
|
||||
content: `Der QR-Code auf diesem Produkt leitet dich zu deinem persönlichen Gym-Profil auf BinKrassDuFass.de weiter. Hier kannst du deine Trainingsergebnisse und Rekorde hochladen. Dein Profil kannst du in den Accounteinstellungen verwalten.`,
|
||||
}}"
|
||||
class="helperText"
|
||||
>
|
||||
<Icon path="{mdiInformationOutline}" />
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="productDetails2"
|
||||
class:brightVersion="{brightVersion}"
|
||||
>
|
||||
<ProductPricetags product="{product}" />
|
||||
{#if product.reviews.avgRating}
|
||||
<div class="avgRating">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M21.9842 10.7231L17.7561 14.4131L19.0226 19.9069C19.0897 20.1941 19.0705 20.4946 18.9677 20.771C18.8648 21.0474 18.6827 21.2874 18.4442 21.4608C18.2057 21.6343 17.9214 21.7336 17.6267 21.7464C17.3321 21.7591 17.0402 21.6847 16.7876 21.5325L11.9961 18.6262L7.21483 21.5325C6.96223 21.6847 6.67038 21.7591 6.37574 21.7464C6.0811 21.7336 5.79676 21.6343 5.55826 21.4608C5.31976 21.2874 5.13769 21.0474 5.03481 20.771C4.93193 20.4946 4.9128 20.1941 4.97983 19.9069L6.24451 14.4187L2.01545 10.7231C1.79177 10.5302 1.63003 10.2755 1.5505 9.99107C1.47098 9.7066 1.47721 9.40498 1.56842 9.12404C1.65964 8.84309 1.83176 8.59533 2.06322 8.41182C2.29468 8.22832 2.57517 8.11723 2.86951 8.0925L8.44389 7.60968L10.6198 2.41968C10.7335 2.14735 10.9251 1.91473 11.1707 1.75111C11.4163 1.58749 11.7047 1.50018 11.9998 1.50018C12.2949 1.50018 12.5834 1.58749 12.829 1.75111C13.0745 1.91473 13.2662 2.14735 13.3798 2.41968L15.5623 7.60968L21.1348 8.0925C21.4292 8.11723 21.7097 8.22832 21.9411 8.41182C22.1726 8.59533 22.3447 8.84309 22.4359 9.12404C22.5271 9.40498 22.5334 9.7066 22.4538 9.99107C22.3743 10.2755 22.2126 10.5302 21.9889 10.7231H21.9842Z"
|
||||
fill="#2F4858"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
{product.reviews.avgRating.toFixed(1)}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
.productDetails1 {
|
||||
margin-top: 1.2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
button {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
em,
|
||||
h1 {
|
||||
color: var(--text-invers-100);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-style: Outfit;
|
||||
}
|
||||
&.brightVersion {
|
||||
em,
|
||||
h1 {
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
.productDetails2 {
|
||||
margin-top: 1.2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.avgRating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
color: var(--text-invers-100);
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-family: Outfit-Bold, sans-serif;
|
||||
}
|
||||
&.brightVersion {
|
||||
.avgRating {
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,43 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { spaLink } from "../../../actions"
|
||||
import { getBCGraphProductsByCategory } from "../../../functions/CommerceAPIs/bigCommerce/product"
|
||||
import ProductPreviewList from "./ProductPreviewList.svelte"
|
||||
export let product: BKDFProduct
|
||||
let products: BKDFProduct[] = []
|
||||
|
||||
getBCGraphProductsByCategory(product.categories[0]?.id).then((data) => {
|
||||
products = data.filter((el) => el.id !== product.id).slice(0, 5)
|
||||
})
|
||||
let innerWidth = window.innerWidth
|
||||
let isMobile = innerWidth < 768
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth="{innerWidth}" />
|
||||
|
||||
{#if products.length}
|
||||
<div class="product-preview-list-wrapper">
|
||||
<div class="headline">
|
||||
<div class="headline-col">
|
||||
<h2>
|
||||
<!-- svelte-ignore empty-block -->
|
||||
Weitere Produkte {#if isMobile}{:else}aus dieser Kategorie{/if}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<button class="">
|
||||
<a
|
||||
href="/collections{product.categories[0]?.path}"
|
||||
use:spaLink>Alle Anzeigen</a
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ProductPreviewList products="{products}" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="less">
|
||||
.product-preview-list-wrapper {
|
||||
margin-top: 2.4rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,77 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiHeart, mdiHeartOutline } from "@mdi/js"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import { login, newNotification, wishlist } from "../../../store"
|
||||
import { addWishlistEntry, removeWishlistEntry } from "../../../functions/CommerceAPIs/tibiEndpoints/wishlist"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
export let productId: number,
|
||||
variantid: number,
|
||||
bigFormat = false,
|
||||
brightVersion: boolean = false
|
||||
function checkIfIsOnWishlist(productId: number, wishlist: RestWishlist) {
|
||||
if (!wishlist || !wishlist.items || !wishlist.items.length) return false
|
||||
return wishlist?.items.some((p) => p.product_id == productId)
|
||||
}
|
||||
const dispatch = createEventDispatcher()
|
||||
let isOnWishlist = false
|
||||
$: isOnWishlist = checkIfIsOnWishlist(productId, $wishlist)
|
||||
</script>
|
||||
|
||||
<button
|
||||
class:isOnWishlist="{isOnWishlist}"
|
||||
class:bigFormat="{bigFormat}"
|
||||
class="{bigFormat ? 'cta tertiary' : ''}"
|
||||
class:brightVersion="{brightVersion}"
|
||||
on:click="{() => {
|
||||
if (!$login) {
|
||||
newNotification({
|
||||
html: 'Bitte logge dich ein, um Produkte zur Wunschliste hinzuzufügen',
|
||||
class: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
if (isOnWishlist) {
|
||||
const variantid = $wishlist.items.find((p) => p.product_id == productId).variant_id
|
||||
removeWishlistEntry(productId, variantid).then(() => {
|
||||
newNotification({
|
||||
html: 'Produkt wurde von der Wunschliste entfernt',
|
||||
class: 'success',
|
||||
})
|
||||
dispatch('removedFavorite')
|
||||
})
|
||||
} else {
|
||||
addWishlistEntry(productId, variantid).then(() => {
|
||||
newNotification({
|
||||
html: 'Produkt wurde zur Wunschliste hinzugefügt',
|
||||
class: 'success',
|
||||
})
|
||||
})
|
||||
}
|
||||
}}"
|
||||
>
|
||||
<Icon
|
||||
path="{isOnWishlist ? mdiHeart : mdiHeartOutline}"
|
||||
size="1.2rem"
|
||||
/>
|
||||
{#if bigFormat} zu Favoriten hinzufügen {/if}
|
||||
</button>
|
||||
|
||||
<style lang="less">
|
||||
button {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
color: var(--text-invers-100);
|
||||
&.bigFormat {
|
||||
grid-column: span 2;
|
||||
margin-top: 1.2rem;
|
||||
border: 1px solid var(--bg-100);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
&.brightVersion {
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,123 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Icon from "../../../widgets/Icon.svelte"
|
||||
import { mdiCloseCircleOutline, mdiFolderOutline } from "@mdi/js"
|
||||
|
||||
export let title,
|
||||
active,
|
||||
deletable = true
|
||||
const dispatcher = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<button
|
||||
class:active="{active}"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
dispatcher('click')
|
||||
}}"
|
||||
aria-label="filter {title}"
|
||||
>
|
||||
<div class="title">
|
||||
<span> {title}</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="46"
|
||||
height="38"
|
||||
viewBox="0 0 46 38"
|
||||
fill="none"
|
||||
>
|
||||
<g transform="rotate(180 23 19)">
|
||||
<path
|
||||
d="M44.5352 0V-1H43.5352H5H2.56171L4.29786 0.712034L42.8331 38.712L44.5352 40.3905V38V0Z"
|
||||
fill="#0D0C0C"
|
||||
stroke-width="2"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="right">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="39"
|
||||
height="38"
|
||||
viewBox="0 0 39 38"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M38.5352 0V38L0 0H38.5352Z"
|
||||
fill="#0D0C0C"></path>
|
||||
</svg>
|
||||
<span>
|
||||
{#if active}
|
||||
{#if deletable}
|
||||
<Icon
|
||||
path="{mdiCloseCircleOutline}"
|
||||
size="24px"
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<Icon
|
||||
path="{mdiFolderOutline}"
|
||||
size="24px"
|
||||
/>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<style lang="less">
|
||||
button {
|
||||
display: flex;
|
||||
border-radius: 4px;
|
||||
background: var(--bg-300);
|
||||
|
||||
.title {
|
||||
padding: 9.5px 12px;
|
||||
margin-right: 45px;
|
||||
line-height: 0.7rem;
|
||||
height: 100%;
|
||||
font-size: 1rem;
|
||||
color: var(--text-invers-100);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: var(--bg-300);
|
||||
position: relative;
|
||||
span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
svg {
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
position: absolute;
|
||||
display: none;
|
||||
transform: translateX(calc(100% - 2px));
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
.title {
|
||||
background-color: var(--bg-100);
|
||||
color: var(--neutral-white);
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
position: relative;
|
||||
.right {
|
||||
display: flex;
|
||||
background-color: var(--bg-100);
|
||||
height: 100%;
|
||||
padding: 9.5px 12px;
|
||||
svg {
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
position: absolute;
|
||||
transform: translateX(calc(-100% - 11.5px));
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,131 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
import { getDBEntries } from "../../../../../api"
|
||||
|
||||
let benefits: ProductBenefit[] = []
|
||||
getDBEntries("productBenefit").then((res) => {
|
||||
benefits = res
|
||||
setTimeout(() => {
|
||||
const progressBars = document.querySelectorAll(".progress-bar")
|
||||
progressBars.forEach((progressBar, i) => {
|
||||
if (i === activeBenefit) progressBar.classList.add("active")
|
||||
else progressBar.classList.remove("active")
|
||||
})
|
||||
}, 10)
|
||||
})
|
||||
|
||||
let activeBenefit = 0
|
||||
|
||||
onMount(() => {
|
||||
let interval = setInterval(() => {
|
||||
activeBenefit = (activeBenefit + 1) % benefits.length
|
||||
// give corresponding progressbar class active and remove it form the other ones
|
||||
const progressBars = document.querySelectorAll(".progress-bar")
|
||||
progressBars.forEach((progressBar, i) => {
|
||||
if (i === activeBenefit) {
|
||||
progressBar.classList.add("active")
|
||||
} else {
|
||||
progressBar.classList.remove("active")
|
||||
}
|
||||
})
|
||||
}, 4500)
|
||||
return () => clearInterval(interval)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="benefits-wrapper">
|
||||
<ul class="benefits">
|
||||
{#each benefits as benefit, i}
|
||||
<li class="benefit {i === activeBenefit ? 'active' : ''}">
|
||||
<h4>{benefit.title}</h4>
|
||||
<p>{@html benefit.description}</p>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<div class="currentActive">
|
||||
{#each benefits as _}
|
||||
<div class="wrapper">
|
||||
<div class="progress-bar"></div>
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.benefits-wrapper {
|
||||
overflow: hidden;
|
||||
padding: 1.2rem 0px;
|
||||
margin-top: 1.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.9rem;
|
||||
|
||||
.benefits {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 60px;
|
||||
.benefit {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transition: left 0.3s linear, opacity 0.3s linear;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: -500px;
|
||||
h4 {
|
||||
font-family: Outfit-Bold;
|
||||
}
|
||||
p {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
&.active {
|
||||
opacity: 1;
|
||||
left: 0px;
|
||||
pointer-events: auto;
|
||||
transition-delay: 0.3s;
|
||||
transition: opacity 0.3s linear, left 0s linear;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.currentActive {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
.wrapper {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
max-width: 2.4rem;
|
||||
.dot {
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
background: var(--bg-300);
|
||||
}
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
left: 0px;
|
||||
width: 0px;
|
||||
transition: width 4.5s linear;
|
||||
&.active {
|
||||
height: 100%;
|
||||
background: var(--primary-100);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,35 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Pricetag from "../../../widgets/Pricetag.svelte"
|
||||
|
||||
export let product: BKDFProduct
|
||||
export let smallVersion: boolean = false
|
||||
const retailPrice = Number(product.retailPrice.amount),
|
||||
salePrice = Number(product.salePrice.amount),
|
||||
hasDiscount = !isNaN(salePrice) && salePrice < retailPrice
|
||||
</script>
|
||||
|
||||
<div class="pricetag">
|
||||
{#if hasDiscount}
|
||||
<Pricetag
|
||||
oldPrice="{false}"
|
||||
discount="{true}"
|
||||
price="{product.salePrice}"
|
||||
smallVersion="{smallVersion}"
|
||||
/>
|
||||
{/if}
|
||||
<Pricetag
|
||||
oldPrice="{hasDiscount}"
|
||||
discount="{false}"
|
||||
price="{product.retailPrice}"
|
||||
smallVersion="{smallVersion}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
.pricetag {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-left: 7px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,56 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiMinusCircleOutline, mdiPlusCircleOutline } from "@mdi/js"
|
||||
import Icon from "../../../widgets/Icon.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
const dispatch = createEventDispatcher()
|
||||
export let quantity = 1,
|
||||
lowerBound = 1
|
||||
</script>
|
||||
|
||||
<div class="quantity">
|
||||
<button
|
||||
aria-label="weniger"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
quantity = Math.max(lowerBound, quantity - 1)
|
||||
dispatch('updateQuantity', { quantity })
|
||||
}}"><Icon path="{mdiMinusCircleOutline}" /></button
|
||||
>
|
||||
<div class="wrapper">
|
||||
<small> {quantity}</small>
|
||||
</div>
|
||||
<button
|
||||
aria-label="mehr"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
quantity = Math.max(lowerBound, quantity + 1)
|
||||
dispatch('updateQuantity', { quantity })
|
||||
}}"><Icon path="{mdiPlusCircleOutline}" /></button
|
||||
>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
.quantity {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 6px;
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 25px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
small {
|
||||
height: fit-content;
|
||||
border-bottom: 1px solid var(--bg-200);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding-bottom: 6px;
|
||||
padding-top: 6px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
button {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,254 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getTibiProduct } from "../../../../functions/CommerceAPIs/tibiEndpoints/product"
|
||||
import Loader from "../../Loader.svelte"
|
||||
|
||||
export let bigCommerceProductId: number
|
||||
let ratingDetails: any
|
||||
|
||||
async function setRating() {
|
||||
getTibiProduct(bigCommerceProductId).then((LocalProduct) => {
|
||||
// fill rating details
|
||||
const ratings = LocalProduct.ratings
|
||||
ratingDetails = {
|
||||
ratings,
|
||||
overview: {
|
||||
fiveStar: {
|
||||
count: ratings.filter((r) => r.rating.overall === 5).length,
|
||||
percentage:
|
||||
ratings.length > 0
|
||||
? (ratings.filter((r) => r.rating.overall === 5).length / ratings.length) * 100
|
||||
: 0,
|
||||
},
|
||||
fourStar: {
|
||||
count: ratings.filter((r) => r.rating.overall === 4).length,
|
||||
percentage:
|
||||
ratings.length > 0
|
||||
? (ratings.filter((r) => r.rating.overall === 4).length / ratings.length) * 100
|
||||
: 0,
|
||||
},
|
||||
threeStar: {
|
||||
count: ratings.filter((r) => r.rating.overall === 3).length,
|
||||
percentage:
|
||||
ratings.length > 0
|
||||
? (ratings.filter((r) => r.rating.overall === 3).length / ratings.length) * 100
|
||||
: 0,
|
||||
},
|
||||
twoStar: {
|
||||
count: ratings.filter((r) => r.rating.overall === 2).length,
|
||||
percentage:
|
||||
ratings.length > 0
|
||||
? (ratings.filter((r) => r.rating.overall === 2).length / ratings.length) * 100
|
||||
: 0,
|
||||
},
|
||||
oneStar: {
|
||||
count: ratings.filter((r) => r.rating.overall === 1).length,
|
||||
percentage:
|
||||
ratings.length > 0
|
||||
? (ratings.filter((r) => r.rating.overall === 1).length / ratings.length) * 100
|
||||
: 0,
|
||||
},
|
||||
},
|
||||
detailed: {
|
||||
// percentage is relative to how close it is to a 5/5 so 1 would be 20% and 5 would be 100%, count is null
|
||||
|
||||
quality: {
|
||||
label: "Qualität",
|
||||
bad: "schlecht",
|
||||
good: "Perfekt",
|
||||
percentage:
|
||||
(ratings.reduce((acc, r) => acc + r.rating.quality, 0) / (ratings.length * 5)) * 100,
|
||||
},
|
||||
priceQualityRatio: {
|
||||
label: "Preis/Leistung",
|
||||
bad: "schlecht",
|
||||
good: "Perfekt",
|
||||
percentage:
|
||||
(ratings.reduce((acc, r) => acc + r.rating.priceQualityRatio, 0) / (ratings.length * 5)) *
|
||||
100,
|
||||
},
|
||||
comfort: {
|
||||
label: "Tragekomfort",
|
||||
bad: "unbequem",
|
||||
good: "Perfekt",
|
||||
percentage:
|
||||
(ratings.reduce((acc, r) => acc + r.rating.comfort, 0) / (ratings.length * 5)) * 100,
|
||||
},
|
||||
overall: {
|
||||
label: "Gesamt",
|
||||
bad: "schlecht",
|
||||
good: "Perfekt",
|
||||
percentage:
|
||||
(ratings.reduce((acc, r) => acc + r.rating.overall, 0) / (ratings.length * 5)) * 100,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
return ratingDetails
|
||||
}
|
||||
setRating()
|
||||
</script>
|
||||
|
||||
<section class="ratings">
|
||||
<div>
|
||||
<h2>Bewertungen</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
{#if !ratingDetails}
|
||||
<Loader size="3" />
|
||||
{:else if ratingDetails.ratings.length === 0}
|
||||
<p>Es gibt noch keine Bewertungen für dieses Produkt.</p>
|
||||
{:else}
|
||||
<div>
|
||||
<h3>Überblick</h3>
|
||||
<ul>
|
||||
{#each Object.keys(ratingDetails.overview) as key, i}
|
||||
<li>
|
||||
<div class="star-lvl">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M21.9842 10.7232L17.7561 14.4132L19.0226 19.9069C19.0897 20.1941 19.0705 20.4947 18.9677 20.7711C18.8648 21.0475 18.6827 21.2874 18.4442 21.4609C18.2057 21.6344 17.9214 21.7337 17.6267 21.7464C17.3321 21.7592 17.0402 21.6848 16.7876 21.5326L11.9961 18.6263L7.21483 21.5326C6.96223 21.6848 6.67038 21.7592 6.37574 21.7464C6.0811 21.7337 5.79676 21.6344 5.55826 21.4609C5.31976 21.2874 5.13769 21.0475 5.03481 20.7711C4.93193 20.4947 4.9128 20.1941 4.97983 19.9069L6.24451 14.4188L2.01545 10.7232C1.79177 10.5303 1.63003 10.2756 1.5505 9.99113C1.47098 9.70666 1.47721 9.40504 1.56842 9.1241C1.65964 8.84315 1.83176 8.59539 2.06322 8.41188C2.29468 8.22838 2.57517 8.11729 2.86951 8.09256L8.44389 7.60974L10.6198 2.41974C10.7335 2.14742 10.9251 1.9148 11.1707 1.75117C11.4163 1.58755 11.7047 1.50024 11.9998 1.50024C12.2949 1.50024 12.5834 1.58755 12.829 1.75117C13.0745 1.9148 13.2662 2.14742 13.3798 2.41974L15.5623 7.60974L21.1348 8.09256C21.4292 8.11729 21.7097 8.22838 21.9411 8.41188C22.1726 8.59539 22.3447 8.84315 22.4359 9.1241C22.5271 9.40504 22.5334 9.70666 22.4538 9.99113C22.3743 10.2756 22.2126 10.5303 21.9889 10.7232H21.9842Z"
|
||||
fill="#2F4858"></path>
|
||||
</svg>
|
||||
<span>{i + 1}</span>
|
||||
</div>
|
||||
<div class="percentage-bar">
|
||||
<div
|
||||
style="width: {ratingDetails.overview[key].percentage == 100
|
||||
? 'calc(100% + 4px)'
|
||||
: ratingDetails.overview[key].percentage + '%'}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="count">{ratingDetails.overview[key].count}</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Ø Bewertungen</h3>
|
||||
<ul style="gap: 0.9rem;">
|
||||
<!--detailed props-->
|
||||
|
||||
{#each Object.keys(ratingDetails.detailed) as key}
|
||||
<li>
|
||||
<div class="label">
|
||||
{ratingDetails.detailed[key].label}
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="percentage-bar">
|
||||
<div
|
||||
style="width: {ratingDetails.detailed[key].percentage == 100
|
||||
? 'calc(100% + 4px)'
|
||||
: `${ratingDetails.detailed[key].percentage}%`};"
|
||||
></div>
|
||||
</div>
|
||||
<div class="labels">
|
||||
<span class="leftern">
|
||||
{ratingDetails.detailed[key].bad}
|
||||
</span>
|
||||
<span class="rightern">
|
||||
{ratingDetails.detailed[key].good}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
.ratings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
width: 100%;
|
||||
h2,
|
||||
h3 {
|
||||
color: var(--text-invers-100);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
|
||||
gap: 2.4rem;
|
||||
width: 100%;
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
flex-grow: 1;
|
||||
width: 0px;
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
width: 100%;
|
||||
li {
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
.star-lvl {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 2.4rem;
|
||||
font-weight: 700;
|
||||
font-family: Outfit-Bold;
|
||||
}
|
||||
.label {
|
||||
width: 120px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.count {
|
||||
width: 1.2rem;
|
||||
text-align: center;
|
||||
}
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
.percentage-bar {
|
||||
flex-grow: 1;
|
||||
border: 2px solid white;
|
||||
background-color: var(--bg-300);
|
||||
height: 0.6rem;
|
||||
position: relative;
|
||||
& > div {
|
||||
position: absolute;
|
||||
left: -2px;
|
||||
top: -2px;
|
||||
bottom: -2px;
|
||||
background-color: var(--primary-100);
|
||||
}
|
||||
}
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
flex-grow: 1;
|
||||
.percentage-bar {
|
||||
width: 100%;
|
||||
}
|
||||
.labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 1680px) {
|
||||
flex-direction: column;
|
||||
& > div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,3 +0,0 @@
|
||||
<script lang="ts">
|
||||
// share this page on social media
|
||||
</script>
|
||||
@@ -1,90 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let sizingChart: SizeChart
|
||||
</script>
|
||||
|
||||
{#if sizingChart && sizingChart.columns && sizingChart.columns.length}
|
||||
<div class="size-chart-wrapper">
|
||||
<h3>Messe dich selbst</h3>
|
||||
<p>
|
||||
{#if sizingChart.generalDescription}
|
||||
{@html sizingChart.generalDescription?.replace(
|
||||
"Die Maße werden von den Lieferanten zur Verfügung gestellt.",
|
||||
""
|
||||
)}
|
||||
{/if}
|
||||
</p>
|
||||
<div class="img-wrapper">
|
||||
<img
|
||||
src="{sizingChart.imageURL}"
|
||||
alt="größentabelle"
|
||||
/>
|
||||
<p>
|
||||
{#if sizingChart.imageDescription}
|
||||
{@html sizingChart.imageDescription
|
||||
.replace("Eine Brust", "A Brust")
|
||||
.replace("Eine Länge", "A Länge")
|
||||
.replace("Eine Taille", "A Taille")
|
||||
.replace("Eine Hüfte", "A Hüfte")
|
||||
.replace("Eine Schulter", "A Schulter")}{/if}
|
||||
</p>
|
||||
</div>
|
||||
<table class="size-chart">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Größe</th>
|
||||
{#each sizingChart.columns || [] as column}
|
||||
<th>{column.germanLabelTranslation || column.label}</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each sizingChart.availableSizes || [] as size, index}
|
||||
<tr>
|
||||
<td>{size}</td>
|
||||
{#each sizingChart.columns as column}
|
||||
<td>{column.sizes[index]} cm</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{:else}
|
||||
<p>Keine Größentabelle verfügbar</p>
|
||||
{/if}
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.size-chart-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
h3 {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
.img-wrapper {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
p {
|
||||
& > p {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1rem;
|
||||
@media (max-width: 768px) {
|
||||
margin-bottom: 0.6rem;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,82 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getCustomer } from "../../../functions/CommerceAPIs/tibiEndpoints/customer"
|
||||
import { login } from "../../../store"
|
||||
import Loader from "../Loader.svelte"
|
||||
import Addresses from "./account/Addresses.svelte"
|
||||
import IndentifyingData from "./account/IndentifyingData.svelte"
|
||||
import PersonalData from "./account/PersonalData.svelte"
|
||||
let reload = false
|
||||
</script>
|
||||
|
||||
<section class="wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<h2>Deine Daten</h2>
|
||||
{#key reload}
|
||||
{#await getCustomer($login.tokenData.tibiId)}
|
||||
<Loader
|
||||
size="4"
|
||||
type="bar"
|
||||
/>
|
||||
{:then customer}
|
||||
<div class="grid">
|
||||
<div class="item1">
|
||||
<PersonalData customer="{customer}" />
|
||||
</div>
|
||||
<div class="item2">
|
||||
<Addresses customer="{customer}" />
|
||||
</div>
|
||||
<div class="item3">
|
||||
<IndentifyingData
|
||||
customer="{customer}"
|
||||
on:update="{() => {
|
||||
reload = !reload
|
||||
}}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/await}{/key}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
.wrapper {
|
||||
flex-grow: 1;
|
||||
margin: 0px var(--horizontal-default-margin);
|
||||
margin-bottom: var(--vertical-default-margin);
|
||||
.inner-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
.grid {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
@media (min-width: 1100px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
column-gap: 2.4rem;
|
||||
row-gap: 2.4rem;
|
||||
.item1 {
|
||||
width: 100%;
|
||||
grid-column: 1;
|
||||
}
|
||||
.item2 {
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 3;
|
||||
grid-column: 2;
|
||||
@media (max-width: 1099px) {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.item3 {
|
||||
grid-column: 1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,93 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { germanyStates } from "../../../../config"
|
||||
import Input from "../blocks/form/Input.svelte"
|
||||
import Select from "../blocks/form/Select.svelte"
|
||||
import { onChange } from "./helper"
|
||||
|
||||
export let address: BigCommerceAddress
|
||||
const stateOptions = germanyStates.map((state) => ({
|
||||
label: state.name,
|
||||
value: state.name,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{address.first_name}"
|
||||
placeholder="Vorname*"
|
||||
id="firstName"
|
||||
/>
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{address.last_name}"
|
||||
placeholder="Nachname*"
|
||||
id="lastName"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{address.address1}"
|
||||
placeholder="Straße*"
|
||||
id="address1"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{address.address2}"
|
||||
placeholder="Zusatz"
|
||||
id="address2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{address.postal_code}"
|
||||
placeholder="PLZ*"
|
||||
id="postalCode"
|
||||
/>
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{address.city}"
|
||||
placeholder="Stadt*"
|
||||
id="city"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<Select
|
||||
bind:value="{address.state_or_province}"
|
||||
options="{stateOptions}"
|
||||
placeholder="Bundesland*"
|
||||
selectedOption="{stateOptions[0]}"
|
||||
/>
|
||||
<Select
|
||||
bind:value="{address.country_code}"
|
||||
options="{[{ label: 'Deutschland', value: 'DE' }]}"
|
||||
selectedOption="{{ label: 'Deutschland', value: 'DE' }}"
|
||||
placeholder="Land*"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{address.phone}"
|
||||
placeholder="Telefonnummer"
|
||||
id="phone"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 1.2rem;
|
||||
@media @mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,137 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
const dispatcher = createEventDispatcher()
|
||||
export let clickable = false
|
||||
export let red = false
|
||||
</script>
|
||||
|
||||
<li class="{red ? 'red' : ''}">
|
||||
{#if clickable}
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
dispatcher('click')
|
||||
}}"
|
||||
on:keydown|stopPropagation|preventDefault="{(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
dispatcher('click')
|
||||
}
|
||||
}}"
|
||||
>
|
||||
<div class="header">
|
||||
<span class="bar"></span><span class="crinkle"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
>
|
||||
<path
|
||||
d="M1.32395 0.999996L50 49"
|
||||
stroke="{red ? '#EB5757' : '#0D0C0C'}"></path>
|
||||
</svg></span
|
||||
>
|
||||
</div>
|
||||
<div class="body">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="header">
|
||||
<slot name="header" />
|
||||
<span class="bar"></span>
|
||||
<span class="crinkle">
|
||||
{#if red}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="49"
|
||||
height="48"
|
||||
viewBox="0 0 49 48"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M0.323948 -8.07009e-06L49 48L0.323948 48L0.323948 -8.07009e-06Z"
|
||||
fill="#EB5757"></path>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
>
|
||||
<path
|
||||
d="M1.32395 0.999996L50 49"
|
||||
stroke="{red ? '#EB5757' : '#0D0C0C'}"></path>
|
||||
</svg>{/if}
|
||||
</span>
|
||||
<div class="rightCorner">
|
||||
<slot name="rightCorner" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
|
||||
<style lang="less">
|
||||
li {
|
||||
flex-grow: 1;
|
||||
width: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& > div[role="button"] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
height: 2.4rem;
|
||||
position: relative;
|
||||
.rightCorner {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
.bar {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
border-radius: 4px 0px;
|
||||
border-top: 1px solid var(--bg-100);
|
||||
border-left: 1px solid var(--bg-100);
|
||||
}
|
||||
.crinkle {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
overflow: hidden;
|
||||
margin-left: -2px;
|
||||
}
|
||||
}
|
||||
.body {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
gap: 1.2rem;
|
||||
border: 1px solid var(--bg-100);
|
||||
border-top: 0px solid black;
|
||||
border-radius: 0px 0px 4px 4px;
|
||||
}
|
||||
&.red {
|
||||
.bar {
|
||||
background-color: #eb5757;
|
||||
border-color: #eb5757;
|
||||
}
|
||||
.body {
|
||||
background-color: #eb5757;
|
||||
border-color: #eb5757;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,107 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { spaNavigate } from "../../../actions"
|
||||
import { postLogin } from "../../../functions/CommerceAPIs/tibiEndpoints/customer"
|
||||
import { newNotification } from "../../../store"
|
||||
import Input from "../blocks/form/Input.svelte"
|
||||
import { onChange, validateField, validateInput } from "./helper"
|
||||
|
||||
const loginDetails: {
|
||||
email: string
|
||||
password: string
|
||||
} = {
|
||||
email: "",
|
||||
password: "",
|
||||
}
|
||||
|
||||
function loginUser() {
|
||||
const validations = [
|
||||
{ id: "email", value: loginDetails.email, validator: validateInput },
|
||||
{ id: "password", value: loginDetails.password, validator: validateInput },
|
||||
]
|
||||
|
||||
let isValid = true
|
||||
|
||||
for (const { id, value, validator } of validations) {
|
||||
// @ts-ignore
|
||||
if (!validateField(id, value, validator)) isValid = false
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
postLogin({
|
||||
email: loginDetails.email,
|
||||
password: loginDetails.password,
|
||||
})
|
||||
.then(() => {
|
||||
newNotification({ html: "Erfolgreich eingeloggt.", class: "success" })
|
||||
spaNavigate("/profile")
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.data.status === 403)
|
||||
newNotification({ html: "Falsche E-Mail oder falsches Passwort.", class: "error" })
|
||||
else
|
||||
newNotification({
|
||||
html: "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es später erneut oder aktualisiere die Seite. Sollte das Problem weiterhin bestehen, wende dich bitte an den Kundenservice.",
|
||||
class: "error",
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="small-wrapper">
|
||||
<form
|
||||
class="login"
|
||||
on:submit|preventDefault|stopPropagation="{loginUser}"
|
||||
>
|
||||
<h2>Melde dich mit deinem Account an</h2>
|
||||
<section class="row">
|
||||
<Input
|
||||
placeholder="E-Mail*"
|
||||
id="email"
|
||||
onChange="{onChange}"
|
||||
bind:value="{loginDetails.email}"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Passwort*"
|
||||
id="password"
|
||||
type="password"
|
||||
onChange="{onChange}"
|
||||
bind:value="{loginDetails.password}"
|
||||
/>
|
||||
</section>
|
||||
<section class="submit row">
|
||||
<button
|
||||
type="submit"
|
||||
class="cta primary">Anmelden</button
|
||||
>
|
||||
</section>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
.small-wrapper {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
form {
|
||||
@media @mobile {
|
||||
.row {
|
||||
padding: 0px;
|
||||
}
|
||||
h2,
|
||||
h3 {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
.submit {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
button {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,184 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiLandRowsHorizontal, mdiTableLarge } from "@mdi/js"
|
||||
import { getTibiRestOrders } from "../../../functions/CommerceAPIs/tibiEndpoints/orders"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import OrderTable from "./order/list/OrderTableRow.svelte"
|
||||
import OrderCard from "./order/list/OrderCard.svelte"
|
||||
import OrderDetailedView from "./order/OrderDetailedView.svelte"
|
||||
import { location, newNotification } from "../../../store"
|
||||
import { spaNavigate } from "../../../actions"
|
||||
import Loader from "../Loader.svelte"
|
||||
let viewType: "card" | "table" = "table"
|
||||
let id = ""
|
||||
if ($location && !$location.path.endsWith("/orders") && !$location.path.endsWith("/orders/")) {
|
||||
if ($location.path.includes("return")) {
|
||||
id = $location.path.split("/return")[0].split("/").filter(Boolean).pop()
|
||||
} else if ($location.path.includes("rate")) {
|
||||
id = $location.path.split("/rate")[0].split("/").filter(Boolean).pop()
|
||||
} else id = $location.path.split("/").filter(Boolean).pop()
|
||||
}
|
||||
const minTableWidth = 1000
|
||||
let innerWidth = typeof "window" !== "undefined" ? window.innerWidth : 360
|
||||
$: if (innerWidth < minTableWidth) viewType = "card"
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth="{innerWidth}" />
|
||||
|
||||
{#if id}
|
||||
<OrderDetailedView orderId="{id}" />
|
||||
{:else}
|
||||
<section class="wrapper order-table">
|
||||
<div class="inner-wrapper">
|
||||
<div class="headline">
|
||||
<div>
|
||||
<h2>Deine Bestellungen</h2>
|
||||
<p>Hier findest du alle Bestellungen, die du bei uns getätigt hast.</p>
|
||||
</div>
|
||||
{#if innerWidth >= minTableWidth}
|
||||
<ul>
|
||||
<li>
|
||||
<button
|
||||
on:click="{() => (viewType = 'table')}"
|
||||
class:active="{viewType == 'table'}"
|
||||
aria-label="Tabellenansicht"
|
||||
>
|
||||
<Icon
|
||||
path="{mdiLandRowsHorizontal}"
|
||||
size="1.2rem"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<button
|
||||
aria-label="Kartenansicht"
|
||||
on:click="{() => (viewType = 'card')}"
|
||||
class:active="{viewType == 'card'}"
|
||||
>
|
||||
<Icon
|
||||
path="{mdiTableLarge}"
|
||||
size="1.2rem"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
{#await getTibiRestOrders()}
|
||||
<Loader
|
||||
size="4"
|
||||
type="bar"
|
||||
/>
|
||||
{:then orders}
|
||||
<ul class="{viewType}">
|
||||
{#each orders as order}
|
||||
{#if viewType === "card"}
|
||||
<OrderCard
|
||||
order="{order}"
|
||||
on:click="{() => {
|
||||
if (order.status_id == 0) {
|
||||
newNotification({
|
||||
html: 'Deine Bestellung wurde nicht abgeschlossen. Bitte schließe die Bestellung ab, um sie hier zu öffnen. Sollte etwas abgebucht worden sein, kontaktieren Sie bitte unseren Support.',
|
||||
class: 'error',
|
||||
})
|
||||
} else spaNavigate('/profile/orders/' + order.tibiId)
|
||||
}}"
|
||||
/>
|
||||
{:else}
|
||||
<OrderTable
|
||||
order="{order}"
|
||||
on:click="{() => {
|
||||
if (order.status_id == 0) {
|
||||
newNotification({
|
||||
html: 'Deine Bestellung wurde nicht abgeschlossen. Bitte schließe die Bestellung ab, um sie hier zu öffnen. Sollte etwas abgebucht worden sein, kontaktieren Sie bitte unseren Support.',
|
||||
class: 'error',
|
||||
})
|
||||
} else spaNavigate('/profile/orders/' + order.tibiId)
|
||||
}}"
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
{/await}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
.order-table:global {
|
||||
.inner-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 2.4rem;
|
||||
|
||||
.headline {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
h2 {
|
||||
color: var(--text-invers-100);
|
||||
text-align: left;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: row;
|
||||
width: fit-content;
|
||||
color: var(--text-invers-100);
|
||||
gap: 12px;
|
||||
button {
|
||||
color: var(--text-invers-100);
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
&.active {
|
||||
background-color: var(--text-invers-100);
|
||||
color: var(--neutral-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
& > ul {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
& > li {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
&.card {
|
||||
display: grid;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
gap: 2.4rem;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@media (max-width: 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@media (min-width: 1500px) {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 1920px) {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
& > li {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,95 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { spaNavigate } from "../../../actions"
|
||||
import { resetPassword } from "../../../functions/CommerceAPIs/tibiEndpoints/customer"
|
||||
|
||||
import { newNotification } from "../../../store"
|
||||
import Input from "../blocks/form/Input.svelte"
|
||||
import { checkPassword, onChange, validateField } from "./helper"
|
||||
|
||||
const resetDetails = {
|
||||
password1: "",
|
||||
password2: "",
|
||||
}
|
||||
|
||||
function loginUser() {
|
||||
const validations = [
|
||||
{ id: "password1", value: resetDetails.password1, validator: checkPassword },
|
||||
{ id: "password2", value: resetDetails.password2, validator: checkPassword },
|
||||
]
|
||||
let isValid = true
|
||||
|
||||
for (const { id, value, validator } of validations) {
|
||||
// @ts-ignore
|
||||
if (!validateField(id, value, validator)) isValid = false
|
||||
}
|
||||
if (resetDetails.password1 !== resetDetails.password2) {
|
||||
isValid = false
|
||||
newNotification({ html: "Passwörter stimmen nicht überein", class: "error" })
|
||||
}
|
||||
if (isValid) {
|
||||
resetPassword({
|
||||
password: resetDetails.password1,
|
||||
})
|
||||
.then(() => {
|
||||
newNotification({ html: "Passwort wurde erfolgreich zurückgesetzt", class: "success" })
|
||||
spaNavigate("/profile/login")
|
||||
})
|
||||
.catch((error) => {
|
||||
newNotification({
|
||||
html: "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es später erneut oder aktualisiere die Seite. Sollte das Problem weiterhin bestehen, wende dich bitte an den Kundenservice.",
|
||||
class: "error",
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="small-wrapper">
|
||||
<form
|
||||
class="login"
|
||||
on:submit|preventDefault|stopPropagation="{loginUser}"
|
||||
>
|
||||
<h2>Setze dein Passwort zurück</h2>
|
||||
|
||||
<section class="row">
|
||||
<Input
|
||||
placeholder="Passwort*"
|
||||
id="password1"
|
||||
type="password"
|
||||
onChange="{onChange}"
|
||||
bind:value="{resetDetails.password1}"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Passwort wiederholen*"
|
||||
type="password"
|
||||
id="password2"
|
||||
onChange="{onChange}"
|
||||
bind:value="{resetDetails.password2}"
|
||||
/>
|
||||
</section>
|
||||
<section class="submit row">
|
||||
<button
|
||||
type="submit"
|
||||
class="cta primary">Zurücksetzen</button
|
||||
>
|
||||
</section>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
.small-wrapper {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
form {
|
||||
.submit {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
button {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,113 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { requestResetPassword } from "../../../functions/CommerceAPIs/tibiEndpoints/customer"
|
||||
import { newNotification } from "../../../store"
|
||||
import Input from "../blocks/form/Input.svelte"
|
||||
import { onChange, validateField, validateInput } from "./helper"
|
||||
import { initCryptcha, resetCryptcha, cryptchaSolution, cryptchaSolutionId } from "../../../utils"
|
||||
const PWReset: {
|
||||
email: string
|
||||
} = {
|
||||
email: "",
|
||||
}
|
||||
|
||||
let requestSuccessfull = false
|
||||
function requestPWReset() {
|
||||
const validations = [{ id: "email", value: PWReset.email, validator: validateInput }]
|
||||
let isValid = true
|
||||
|
||||
for (const { id, value, validator } of validations) {
|
||||
// @ts-ignore
|
||||
if (!validateField(id, value, validator)) isValid = false
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
requestResetPassword({ email: PWReset.email, _s: $cryptchaSolution, _sId: $cryptchaSolutionId })
|
||||
.then(() => {
|
||||
newNotification({
|
||||
html: "Passwort zurücksetzen E-Mail wurde erfolgreich gesendet!",
|
||||
class: "success",
|
||||
})
|
||||
requestSuccessfull = true
|
||||
resetCryptcha()
|
||||
})
|
||||
.catch((error) => {
|
||||
newNotification({
|
||||
html: "Ein unerwarteter Fehler ist aufgetreten. Prüfe die richtigkeit deiner E-Mail. Bitte versuche es später erneut oder aktualisiere die Seite. Sollte das Problem weiterhin bestehen, wende dich bitte an den Kundenservice.",
|
||||
class: "error",
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
let cryptchaEl: HTMLDivElement
|
||||
|
||||
let cryptchaInitialized = false
|
||||
|
||||
$: if (cryptchaEl && !cryptchaInitialized && PWReset.email) {
|
||||
initCryptcha(cryptchaEl)
|
||||
cryptchaInitialized = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="small-wrapper">
|
||||
<form
|
||||
class="login"
|
||||
on:submit|preventDefault|stopPropagation="{requestPWReset}"
|
||||
>
|
||||
<h2>Setze dein Passwort zurück</h2>
|
||||
{#if requestSuccessfull}
|
||||
<p class="row">
|
||||
Wir haben dir eine E-Mail mit weiteren Anweisungen gesendet. Falls du sie nicht erhalten hast, überprüfe
|
||||
bitte deinen Spam-Ordner oder versuche es später erneut. Sollte das Problem weiterhin bestehen, wende
|
||||
dich bitte an den Kundenservice.
|
||||
</p>
|
||||
{:else}
|
||||
<section class="row">
|
||||
<Input
|
||||
placeholder="E-Mail*"
|
||||
id="email"
|
||||
onChange="{onChange}"
|
||||
bind:value="{PWReset.email}"
|
||||
helperText="Bitte gib die E-Mail-Adresse ein, die mit deinem Account verknüpft ist. Wir senden dir eine E-Mail mit einem Link, um dein Passwort zurückzusetzen."
|
||||
/>
|
||||
</section>
|
||||
<section class="submit row">
|
||||
<button
|
||||
type="submit"
|
||||
disabled="{!$cryptchaSolution}"
|
||||
class="cta primary">Anfragen</button
|
||||
>
|
||||
</section>
|
||||
<section class="row">
|
||||
<div bind:this="{cryptchaEl}"></div>
|
||||
</section>
|
||||
{/if}
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
.small-wrapper {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
form {
|
||||
@media @mobile {
|
||||
.row {
|
||||
padding: 0px;
|
||||
}
|
||||
h2,
|
||||
h3 {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
.submit {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
button {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,95 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiExitToApp, mdiOpenInNew } from "@mdi/js"
|
||||
import { getCustomer, postLogout } from "../../../functions/CommerceAPIs/tibiEndpoints/customer"
|
||||
import { actionApproval, login } from "../../../store"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import SocialMediaRow from "./profile/SocialMediaRow.svelte"
|
||||
import EditableRecords from "./profile/EditableRecords.svelte"
|
||||
import { spaLink } from "../../../actions"
|
||||
let bigCommerceCustomer: BigCommerceCustomer
|
||||
$: if (!!$login) getCustomer($login.customer.id).then((customer) => (bigCommerceCustomer = customer))
|
||||
</script>
|
||||
|
||||
<section class="wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<h2>
|
||||
{bigCommerceCustomer?.first_name || ""}
|
||||
{bigCommerceCustomer?.last_name || ""}
|
||||
<button
|
||||
on:click="{() => {
|
||||
$actionApproval = {
|
||||
modalTitle: 'Ausloggen',
|
||||
modalText: 'Möchtest du dich wirklich ausloggen?',
|
||||
callback: postLogout,
|
||||
}
|
||||
}}"
|
||||
aria-label="Ausloggen"
|
||||
><Icon
|
||||
path="{mdiExitToApp}"
|
||||
size="34px"
|
||||
/></button
|
||||
>
|
||||
</h2>
|
||||
<SocialMediaRow />
|
||||
<div class="records">
|
||||
<h3>Meine Rekorde</h3>
|
||||
<p>Alle unten gelisteten Rekorde sind automatisch und umgehend öffentlich über folgenden Link Einsehbar.</p>
|
||||
{#if !bigCommerceCustomer?.form_fields?.find((f) => f?.name == "username")?.value}
|
||||
<p>
|
||||
Um ein öffentliches Profil zu haben, benötigst du einen Nutzernamen! <a
|
||||
use:spaLink
|
||||
href="/profile/account">Klicke hier.</a
|
||||
>
|
||||
</p>
|
||||
{:else}
|
||||
<a href="/@{bigCommerceCustomer?.form_fields?.find((f) => f?.name == 'username')?.value || ''}">
|
||||
<Icon
|
||||
path="{mdiOpenInNew}"
|
||||
size="24px"
|
||||
/>
|
||||
{location.origin.split("https://").pop()}/@{(
|
||||
bigCommerceCustomer?.form_fields?.find((f) => f?.name == "username")?.value || ""
|
||||
).toLowerCase()}
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
<EditableRecords />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
.wrapper {
|
||||
.inner-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
h2,
|
||||
h2 button {
|
||||
color: var(--text-invers-100);
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
align-items: center;
|
||||
height: fit-content;
|
||||
}
|
||||
.records {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
h3 {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
a {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
align-items: center;
|
||||
color: var(--text-invers-100);
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,280 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { spaLink, spaNavigate } from "../../../actions"
|
||||
import { registerBCGraphCustomer } from "../../../functions/CommerceAPIs/bigCommerce/customer"
|
||||
import { subscribeToNewsletter } from "../../../functions/CommerceAPIs/tibiEndpoints/actions"
|
||||
import { checkUsernameTaken } from "../../../functions/CommerceAPIs/tibiEndpoints/customer"
|
||||
import { newNotification } from "../../../store"
|
||||
import Input from "../blocks/form/Input.svelte"
|
||||
import AddressForm from "./AddressForm.svelte"
|
||||
import {
|
||||
appendErrorMessage,
|
||||
checkPassword,
|
||||
createEmptyAddress,
|
||||
onChange,
|
||||
validateAddress,
|
||||
validateEmail,
|
||||
validateField,
|
||||
validateUsername,
|
||||
} from "./helper"
|
||||
|
||||
const registerDetails: {
|
||||
email: string
|
||||
password: string
|
||||
newsletter: boolean
|
||||
dataProtection: boolean
|
||||
username: string
|
||||
address: BigCommerceAddress
|
||||
} = {
|
||||
email: "",
|
||||
password: "",
|
||||
username: "",
|
||||
newsletter: false,
|
||||
dataProtection: false,
|
||||
address: createEmptyAddress(),
|
||||
}
|
||||
|
||||
async function submitUser() {
|
||||
const validations = [
|
||||
{ id: "email", value: registerDetails.email, validator: validateEmail },
|
||||
{ id: "password", value: registerDetails.password, validator: checkPassword },
|
||||
{ id: "username", value: registerDetails.username, validator: validateUsername },
|
||||
]
|
||||
let isValid = validateAddress(registerDetails.address)
|
||||
|
||||
for (const { id, value, validator } of validations) {
|
||||
// @ts-ignore
|
||||
if (!validateField(id, value, validator)) isValid = false
|
||||
}
|
||||
|
||||
if (!registerDetails.dataProtection) {
|
||||
const element = document.getElementById("data") as HTMLInputElement
|
||||
element.focus()
|
||||
element.classList.add("error")
|
||||
const container = document.getElementById("dataProtection") as HTMLDivElement
|
||||
appendErrorMessage(container, "Bitte akzeptiere die Datenschutzbestimmungen", container)
|
||||
newNotification({ html: "Bitte akzeptiere die Datenschutzbestimmungen", class: "error" })
|
||||
isValid = false
|
||||
}
|
||||
await checkUsernameTaken(registerDetails.username.toLowerCase())
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
const element = document.getElementById("username") as HTMLInputElement
|
||||
element.focus()
|
||||
element.classList.add("error")
|
||||
const container = document.getElementById("username") as HTMLDivElement
|
||||
appendErrorMessage(container, "Dieser Nutzername ist bereits vergeben", container)
|
||||
newNotification({ html: "Dieser Nutzername ist bereits vergeben", class: "error" })
|
||||
isValid = false
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
newNotification({
|
||||
html: "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es später erneut oder aktualisiere die Seite. Sollte das Problem weiterhin bestehen, wende dich bitte an den Kundenservice.",
|
||||
class: "error",
|
||||
})
|
||||
})
|
||||
|
||||
if (isValid) {
|
||||
registerBCGraphCustomer({
|
||||
firstName: registerDetails.address.first_name,
|
||||
lastName: registerDetails.address.last_name,
|
||||
phone: registerDetails.address.phone,
|
||||
address: {
|
||||
address1: registerDetails.address.address1,
|
||||
address2: registerDetails.address.address2,
|
||||
city: registerDetails.address.city,
|
||||
postalCode: registerDetails.address.postal_code,
|
||||
stateOrProvince: registerDetails.address.state_or_province,
|
||||
countryCode: registerDetails.address.country_code,
|
||||
firstName: registerDetails.address.first_name,
|
||||
lastName: registerDetails.address.last_name,
|
||||
phone: registerDetails.address.phone,
|
||||
},
|
||||
formFields: { texts: { fieldEntityId: 30, text: registerDetails.username.toLowerCase() } },
|
||||
email: registerDetails.email.toLowerCase(),
|
||||
password: registerDetails.password,
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
newNotification({
|
||||
html: "Account wurde erfolgreich erstellt. Bitte melde dich nun an! Es kann bis zu 5 Minuten dauern, bis du Zugriff auf deinen Account hast.",
|
||||
class: "success",
|
||||
})
|
||||
if (registerDetails.newsletter) subscribeToNewsletter(registerDetails.email)
|
||||
|
||||
spaNavigate("/profile/login")
|
||||
})
|
||||
.catch((error) => {
|
||||
newNotification({
|
||||
html: "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es später erneut oder aktualisiere die Seite. Sollte das Problem weiterhin bestehen, wende dich bitte an den Kundenservice.",
|
||||
class: "error",
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="small-wrapper">
|
||||
<form
|
||||
class="register"
|
||||
on:submit|preventDefault|stopPropagation="{submitUser}"
|
||||
>
|
||||
<h2>Erstelle einen Account</h2>
|
||||
<section class="row">
|
||||
<p>
|
||||
Um einen Account zu löschen, ist die Kommunikation mit unserem Kundenservice notwendig. Aus Gründen der
|
||||
Bestellungsverfolgung können wir diese Funktion leider nicht automatisiert anbieten.
|
||||
</p>
|
||||
</section>
|
||||
<section class="row">
|
||||
<Input
|
||||
placeholder="E-Mail*"
|
||||
id="email"
|
||||
onChange="{onChange}"
|
||||
bind:value="{registerDetails.email}"
|
||||
helperText="Deine E-Mail-Adresse wird für die Kommunikation mit dir verwendet. Wir geben deine Daten nicht an Dritte weiter. "
|
||||
/>
|
||||
<Input
|
||||
placeholder="Passwort*"
|
||||
id="password"
|
||||
type="password"
|
||||
onChange="{onChange}"
|
||||
bind:value="{registerDetails.password}"
|
||||
helperText="Dein Passwort muss mindestens 7 Zeichen lang sein, einen Buchstaben sowie eine Zahl enthalten. Für mehr Sicherheit empfehlen wir die Nutzung eines Passwortgenerators. Keine Sorge, dein Browser speichert das Passwort für dich, sodass du es dir nicht merken musst."
|
||||
/>
|
||||
</section>
|
||||
<section class="row">
|
||||
<Input
|
||||
placeholder="Nutzername*"
|
||||
id="username"
|
||||
onChange="{onChange}"
|
||||
bind:value="{registerDetails.username}"
|
||||
helperText="Dein Benutzername ist der Name deines persönlichen Gym-Profils. Er wird öffentlich angezeigt und dient als eindeutige Referenz für dein Profil."
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="row">
|
||||
<div class="address">
|
||||
<AddressForm bind:address="{registerDetails.address}" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="newsletter-check row">
|
||||
<div>
|
||||
<Input
|
||||
type="checkbox"
|
||||
classList="checkit"
|
||||
id="newsletter"
|
||||
onChange="{onChange}"
|
||||
bind:value="{registerDetails.newsletter}"
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
Kreuze hier an, um E-Mails zu unseren Produkten, Sales, exklusivem Content und mehr zu erhalten. Siehe
|
||||
unsere <a href="/datenschutz">Datenschutzerklärung</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="dataProtection row"
|
||||
id="dataProtection"
|
||||
>
|
||||
<div
|
||||
class="container"
|
||||
id="dataProtectionContainer"
|
||||
>
|
||||
<div>
|
||||
<Input
|
||||
type="checkbox"
|
||||
id="data"
|
||||
classList="checkit"
|
||||
onChange="{(e) => {
|
||||
onChange(e)
|
||||
const dP = document.getElementById('dataProtection')
|
||||
const dPE = dP?.querySelector('.error-message')
|
||||
if (dPE) {
|
||||
dP?.removeChild(dPE)
|
||||
}
|
||||
}}"
|
||||
bind:value="{registerDetails.dataProtection}"
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
Ich habe die <a
|
||||
use:spaLink
|
||||
href="/datenschutz">Datenschutzbestimmungen</a
|
||||
> gelesen und akzeptiere sie.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="submit row">
|
||||
<button
|
||||
type="submit"
|
||||
class="cta primary"
|
||||
>Registrieren
|
||||
</button>
|
||||
</section>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
.small-wrapper {
|
||||
form {
|
||||
@media @mobile {
|
||||
.row {
|
||||
padding: 0px;
|
||||
}
|
||||
h2,
|
||||
h3 {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
padding: 2.4rem 0rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
.address {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
.newsletter-check {
|
||||
display: flex;
|
||||
flex-direction: row !important;
|
||||
gap: 1.2rem;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
.dataProtection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
padding-bottom: 1.2rem;
|
||||
gap: 0.6rem;
|
||||
width: 100%;
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.submit {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
button {
|
||||
@media @mobile {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,94 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiPencil, mdiTrashCanOutline } from "@mdi/js"
|
||||
import Icon from "../../../widgets/Icon.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let address: BigCommerceAddress,
|
||||
showDelete = false
|
||||
const dispatcher = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<li>
|
||||
<div class="wrapper">
|
||||
<h4>{address.first_name || ""} {address.last_name || ""}</h4>
|
||||
<div class="content">
|
||||
<p>{address.address1}</p>
|
||||
{#if address.address2}
|
||||
<p>{address.address2}</p>
|
||||
{/if}
|
||||
<p>
|
||||
{address.postal_code}
|
||||
{address.city}
|
||||
{address.state_or_province ? `(${address.state_or_province})` : ""}
|
||||
</p>
|
||||
<p>{address.country}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-line">
|
||||
<p>Wohnadresse</p>
|
||||
|
||||
<span>
|
||||
<button
|
||||
class="btn transparent"
|
||||
on:click="{() => dispatcher('edit')}"
|
||||
aria-label="Bearbeiten"
|
||||
>
|
||||
<Icon path="{mdiPencil}" />
|
||||
</button>
|
||||
{#if showDelete}
|
||||
<button
|
||||
class="btn transparent"
|
||||
on:click="{() => dispatcher('delete')}"
|
||||
aria-label="Löschen"
|
||||
>
|
||||
<Icon path="{mdiTrashCanOutline}" />
|
||||
</button>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<style lang="less">
|
||||
li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.wrapper {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
border: 1px solid var(--bg-100);
|
||||
padding: 1.2rem;
|
||||
h4 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
.action-line {
|
||||
background: var(--bg-100);
|
||||
padding: 6px 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 26px;
|
||||
justify-content: space-between;
|
||||
p,
|
||||
span {
|
||||
color: var(--neutral-white);
|
||||
text-transform: uppercase;
|
||||
font-size: 0.7rem;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,213 +0,0 @@
|
||||
<script lang="ts">
|
||||
import CardWrapper from "../CardWrapper.svelte"
|
||||
import { login, newNotification } from "../../../../store"
|
||||
import Address from "./Address.svelte"
|
||||
import AddressForm from "../AddressForm.svelte"
|
||||
import Icon from "../../../widgets/Icon.svelte"
|
||||
import { mdiPlusCircleOutline } from "@mdi/js"
|
||||
import { createEmptyAddress, removeAllErrors, validateAddress } from "../helper"
|
||||
import {
|
||||
updateCustomerAddress,
|
||||
addCustomerAddress,
|
||||
deleteCustomerAddress,
|
||||
getCustomerAddresses,
|
||||
} from "../../../../functions/CommerceAPIs/tibiEndpoints/customer"
|
||||
import Loader from "../../Loader.svelte"
|
||||
|
||||
async function updateAddress(editedAddress: BigCommerceAddress, addresses: BigCommerceAddress[]) {
|
||||
if (editedAddress.id) {
|
||||
await updateCustomerAddress($login.tokenData.tibiId, editedAddress).then((updatedAddress) => {
|
||||
const index = addresses.findIndex((a) => a.id === updatedAddress.id)
|
||||
addresses[index] = updatedAddress
|
||||
})
|
||||
} else {
|
||||
const identicalAddress = addresses.find((a) => {
|
||||
return (
|
||||
a.address1 === editedAddress.address1 &&
|
||||
a.address2 === editedAddress.address2 &&
|
||||
a.city === editedAddress.city &&
|
||||
a.state_or_province === editedAddress.state_or_province &&
|
||||
a.postal_code === editedAddress.postal_code &&
|
||||
a.country_code === editedAddress.country_code
|
||||
)
|
||||
})
|
||||
if (identicalAddress) {
|
||||
newNotification({
|
||||
class: "warning",
|
||||
html: "Diese Adresse ist bereits gespeichert!",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await addCustomerAddress($login.tokenData.tibiId, editedAddress).then((newAddress) => {
|
||||
addresses.push(newAddress)
|
||||
})
|
||||
}
|
||||
}
|
||||
let editedAddress: BigCommerceAddress = undefined
|
||||
let form: HTMLFormElement
|
||||
let rerender = 0
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<CardWrapper>
|
||||
<div class="inner-wrapper">
|
||||
<h3>
|
||||
{editedAddress
|
||||
? !editedAddress.id
|
||||
? "Adresse hinzufügen"
|
||||
: "Adresse bearbeiten"
|
||||
: "Gespeicherte Adressen"}
|
||||
</h3>
|
||||
{#await getCustomerAddresses($login.tokenData.tibiId)}
|
||||
<Loader
|
||||
size="4"
|
||||
type="circle"
|
||||
/>
|
||||
{:then addresses}
|
||||
<div class="inner-body">
|
||||
{#if editedAddress}
|
||||
<form bind:this="{form}">
|
||||
<div class="address">
|
||||
<AddressForm bind:address="{editedAddress}" />
|
||||
</div>
|
||||
<div class="action-button-line">
|
||||
<button
|
||||
class="cta primary"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
editedAddress = undefined
|
||||
removeAllErrors(form)
|
||||
}}">{editedAddress.id ? "Änderungen" : "Adresse"} verwerfen</button
|
||||
>
|
||||
<button
|
||||
class="cta secondary"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
const isValid = validateAddress(editedAddress)
|
||||
if (isValid)
|
||||
updateAddress(editedAddress, addresses).then(
|
||||
() => (editedAddress = undefined)
|
||||
)
|
||||
}}">{editedAddress.id ? "Änderungen" : "Adresse"} speichern</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
{:else}
|
||||
<ul>
|
||||
{#key rerender}
|
||||
{#each addresses as address, i}
|
||||
{#if address}
|
||||
<Address
|
||||
showDelete="{addresses.length > 1}"
|
||||
address="{address}"
|
||||
on:edit="{() => {
|
||||
editedAddress = JSON.parse(JSON.stringify(address))
|
||||
}}"
|
||||
on:delete="{() => {
|
||||
deleteCustomerAddress($login.tokenData.tibiId, address.id).then(() => {
|
||||
let addressIndex = addresses.findIndex((a) => a.id === address.id)
|
||||
addresses.splice(addressIndex, 1)
|
||||
rerender += 1
|
||||
})
|
||||
}}"
|
||||
/>
|
||||
{/if}
|
||||
{/each}{/key}
|
||||
</ul>
|
||||
<button
|
||||
class="action-line"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
editedAddress = createEmptyAddress()
|
||||
}}"
|
||||
>
|
||||
<p>Neue Adresse hinzufügen</p>
|
||||
<span><Icon path="{mdiPlusCircleOutline}" /> </span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
</CardWrapper>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../../assets/css/variables.less";
|
||||
.wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
.inner-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 1.2rem;
|
||||
height: 100%;
|
||||
h3 {
|
||||
padding: 0px 1.2rem;
|
||||
color: var(--text-invers-100);
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.inner-body {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
flex-grow: 1;
|
||||
justify-content: space-between;
|
||||
form {
|
||||
.address {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
}
|
||||
ul {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@media @mobile {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
padding: 0px 2.4rem;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
.action-button-line,
|
||||
.action-line {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.action-line {
|
||||
background: var(--bg-100);
|
||||
padding: 0.6rem 3.3rem 0.6rem 3.3rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
min-height: 2.4rem;
|
||||
justify-content: space-between;
|
||||
p,
|
||||
span {
|
||||
color: var(--neutral-white);
|
||||
}
|
||||
}
|
||||
|
||||
.action-button-line {
|
||||
min-height: 2.4rem;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
button {
|
||||
width: 0px;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,384 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiPencil } from "@mdi/js"
|
||||
import Icon from "../../../widgets/Icon.svelte"
|
||||
import { checkPassword, onChange, removeAllErrors, validateEmail, validateField, validateUsername } from "../helper"
|
||||
import Input from "../../blocks/form/Input.svelte"
|
||||
import CardWrapper from "../CardWrapper.svelte"
|
||||
import { login, newNotification } from "../../../../store"
|
||||
import {
|
||||
changeEmail,
|
||||
changePassword,
|
||||
changeUsername,
|
||||
} from "../../../../functions/CommerceAPIs/tibiEndpoints/customer"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
export let customer: BigCommerceCustomer
|
||||
const dispatch = createEventDispatcher()
|
||||
const passwordChangeDetails = {
|
||||
currentPassword: "",
|
||||
password1: "",
|
||||
password2: "",
|
||||
}
|
||||
const emailChangeDetails = {
|
||||
email: "",
|
||||
currentPassword: "",
|
||||
}
|
||||
|
||||
const usernameChangeDetails = {
|
||||
username: "",
|
||||
currentPassword: "",
|
||||
}
|
||||
function changePasswordValidator() {
|
||||
const validations = [
|
||||
{ id: "password1", value: passwordChangeDetails.password1, validator: checkPassword },
|
||||
{ id: "password2", value: passwordChangeDetails.password2, validator: checkPassword },
|
||||
]
|
||||
let isValid = true
|
||||
|
||||
for (const { id, value, validator } of validations) {
|
||||
// @ts-ignore
|
||||
if (!validateField(id, value, validator)) isValid = false
|
||||
}
|
||||
if (passwordChangeDetails.password1 !== passwordChangeDetails.password2) {
|
||||
isValid = false
|
||||
newNotification({ html: "Passwörter stimmen nicht überein", class: "error" })
|
||||
}
|
||||
return isValid
|
||||
}
|
||||
|
||||
function changeEmailValidator() {
|
||||
const validations = [{ id: "eMail", value: emailChangeDetails.email, validator: validateEmail }]
|
||||
let isValid = true
|
||||
|
||||
for (const { id, value, validator } of validations) {
|
||||
// @ts-ignore
|
||||
if (!validateField(id, value, validator)) isValid = false
|
||||
}
|
||||
return isValid
|
||||
}
|
||||
|
||||
function changeUsernameValidator() {
|
||||
const validations = [{ id: "username", value: usernameChangeDetails.username, validator: validateUsername }]
|
||||
let isValid = true
|
||||
|
||||
for (const { id, value, validator } of validations) {
|
||||
// @ts-ignore
|
||||
if (!validateField(id, value, validator)) isValid = false
|
||||
}
|
||||
return isValid
|
||||
}
|
||||
|
||||
let form: HTMLFormElement
|
||||
let editMode: "" | "password" | "email" | "username" = ""
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<CardWrapper>
|
||||
<form
|
||||
bind:this="{form}"
|
||||
class:editMode="{editMode}"
|
||||
>
|
||||
<h3>Identifizierende Daten</h3>
|
||||
{#if editMode}
|
||||
{#if editMode === "password"}
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{passwordChangeDetails.currentPassword}"
|
||||
placeholder="Aktuelles Passwort"
|
||||
id="currentPassword"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{passwordChangeDetails.password1}"
|
||||
placeholder="Neues Passwort"
|
||||
id="password1"
|
||||
type="password"
|
||||
/>
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{passwordChangeDetails.password2}"
|
||||
placeholder="Neues Passwort wiederholen"
|
||||
id="password2"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
{:else if editMode === "email"}
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{emailChangeDetails.currentPassword}"
|
||||
placeholder="Aktuelles Passwort"
|
||||
id="currentPassword"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{emailChangeDetails.email}"
|
||||
placeholder="Neue E-Mail"
|
||||
id="eMail"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{:else if editMode == "username"}
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{usernameChangeDetails.currentPassword}"
|
||||
placeholder="Aktuelles Passwort"
|
||||
id="currentPassword"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{usernameChangeDetails.username}"
|
||||
placeholder="Neuer Nutzername"
|
||||
id="username"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="row">
|
||||
<div class="details">
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{() => {}}"
|
||||
value="{customer.email}"
|
||||
placeholder="E-Mail"
|
||||
id="eMail"
|
||||
disabled="{true}"
|
||||
type="{editMode ? 'text' : 'noInput'}"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Passwort"
|
||||
id="password"
|
||||
onChange="{onChange}"
|
||||
type="noInput"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{() => {}}"
|
||||
value="{customer.form_fields.find((field) => field.name === 'username')?.value}"
|
||||
placeholder="Nutzername"
|
||||
id="userName"
|
||||
type="noInput"
|
||||
disabled="{true}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if editMode}
|
||||
<div class="action-button-line">
|
||||
<button
|
||||
class="cta primary"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
editMode = ''
|
||||
removeAllErrors(form)
|
||||
}}">Änderungen verwerfen</button
|
||||
>
|
||||
<button
|
||||
class="cta secondary"
|
||||
on:click|stopPropagation|preventDefault="{async () => {
|
||||
if (editMode === 'password') {
|
||||
if (changePasswordValidator()) {
|
||||
const res = await changePassword(
|
||||
$login.tokenData.tibiId,
|
||||
passwordChangeDetails.currentPassword,
|
||||
passwordChangeDetails.password1
|
||||
)
|
||||
if (res) {
|
||||
editMode = ''
|
||||
passwordChangeDetails.currentPassword = ''
|
||||
passwordChangeDetails.password1 = ''
|
||||
passwordChangeDetails.password2 = ''
|
||||
dispatch('update')
|
||||
}
|
||||
}
|
||||
} else if (editMode === 'email') {
|
||||
if (changeEmailValidator()) {
|
||||
const res = await changeEmail(
|
||||
$login.tokenData.tibiId,
|
||||
emailChangeDetails.email,
|
||||
emailChangeDetails.currentPassword
|
||||
)
|
||||
if (res) {
|
||||
editMode = ''
|
||||
customer.email = emailChangeDetails.email
|
||||
emailChangeDetails.email = ''
|
||||
emailChangeDetails.currentPassword = ''
|
||||
location.reload()
|
||||
dispatch('update')
|
||||
}
|
||||
}
|
||||
} else if (editMode === 'username') {
|
||||
if (changeUsernameValidator()) {
|
||||
const res = await changeUsername(
|
||||
$login.tokenData.tibiId,
|
||||
usernameChangeDetails.username,
|
||||
usernameChangeDetails.currentPassword
|
||||
)
|
||||
if (res) {
|
||||
editMode = ''
|
||||
const index = customer.form_fields.findIndex(
|
||||
(field) => field.name === 'username'
|
||||
)
|
||||
if (index !== -1) {
|
||||
customer.form_fields[index].value = usernameChangeDetails.username
|
||||
} else if (customer.form_fields) {
|
||||
customer.form_fields.push({
|
||||
name: 'username',
|
||||
value: usernameChangeDetails.username,
|
||||
})
|
||||
} else {
|
||||
customer.form_fields = [
|
||||
{ name: 'username', value: usernameChangeDetails.username },
|
||||
]
|
||||
}
|
||||
usernameChangeDetails.username = ''
|
||||
usernameChangeDetails.currentPassword = ''
|
||||
//location.reload()
|
||||
dispatch('update')
|
||||
}
|
||||
}
|
||||
}
|
||||
}}">Änderungen speichern</button
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="action-line">
|
||||
<button
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
editMode = 'email'
|
||||
}}"
|
||||
>
|
||||
<p>Email</p>
|
||||
|
||||
<span><Icon path="{mdiPencil}" /> </span></button
|
||||
>
|
||||
|
||||
<button
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
editMode = 'password'
|
||||
}}"
|
||||
>
|
||||
<p>Passwort</p>
|
||||
|
||||
<span><Icon path="{mdiPencil}" /> </span></button
|
||||
>
|
||||
<button
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
editMode = 'username'
|
||||
}}"
|
||||
>
|
||||
<p>Nutzername</p>
|
||||
|
||||
<span><Icon path="{mdiPencil}" /> </span></button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
</CardWrapper>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../../assets/css/variables.less";
|
||||
.wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
form {
|
||||
h3 {
|
||||
padding: 0px 2.4rem;
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
.row {
|
||||
padding: 0px 2.4rem;
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
@media (max-width: 500px) {
|
||||
gap: 1.2rem;
|
||||
&:first-of-type {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
.details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
.row {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.editMode {
|
||||
@media (max-width: 500px) {
|
||||
.row {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
.action-button-line,
|
||||
.action-line {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
button {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 0px;
|
||||
flex-grow: 1;
|
||||
@media (max-width: 500px) {
|
||||
flex-grow: unset;
|
||||
width: fit-content;
|
||||
gap: 7px;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
.action-line {
|
||||
background: var(--bg-100);
|
||||
padding: 0.6rem 3.3rem 0.6rem 3.3rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
min-height: 2.4rem;
|
||||
justify-content: space-between;
|
||||
@media (max-width: 500px) {
|
||||
padding: 1.2rem 3.3rem 1.2rem 3.3rem;
|
||||
height: 3.6rem;
|
||||
}
|
||||
p,
|
||||
span {
|
||||
color: var(--neutral-white);
|
||||
}
|
||||
}
|
||||
|
||||
.action-button-line {
|
||||
min-height: 2.4rem;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
button {
|
||||
width: 0px;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,225 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiPencil } from "@mdi/js"
|
||||
import Icon from "../../../widgets/Icon.svelte"
|
||||
import CardWrapper from "../CardWrapper.svelte"
|
||||
import Input from "../../blocks/form/Input.svelte"
|
||||
import { appendErrorMessage, onChange, removeAllErrors, validateBirthday, validateInput } from "../helper"
|
||||
import { login, newNotification } from "../../../../store"
|
||||
import {} from "../../../../functions/CommerceAPIs/bigCommerce/customer"
|
||||
import Select from "../../blocks/form/Select.svelte"
|
||||
import Calendar from "../../blocks/form/Calendar.svelte"
|
||||
import { updateCustomer } from "../../../../functions/CommerceAPIs/tibiEndpoints/customer"
|
||||
export let customer: BigCommerceCustomer
|
||||
let editMode = false
|
||||
const birthday = (customer.form_fields || []).find((field) => field.name === "birthday")?.value
|
||||
const gender = (customer.form_fields || []).find((field) => field.name == "gender")?.value as "male" | "female"
|
||||
const personalData = {
|
||||
firstName: customer.first_name,
|
||||
lastName: customer.last_name,
|
||||
userName: customer.form_fields.find((field) => field.name === "username")?.value || "",
|
||||
email: customer.email,
|
||||
birthday: birthday ? new Date(birthday) : undefined,
|
||||
gender: gender ? gender : "",
|
||||
}
|
||||
|
||||
function validateField(
|
||||
elementId: string,
|
||||
value: string | boolean,
|
||||
validator: (value: string | boolean) => string | null
|
||||
): boolean {
|
||||
const element = document.getElementById(elementId) as HTMLInputElement
|
||||
const error = validator(value)
|
||||
if (error) {
|
||||
element.classList.add("error")
|
||||
appendErrorMessage(element, error)
|
||||
newNotification({ html: error, class: "error" })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
function formatDate(dateString: string) {
|
||||
const date = new Date(dateString)
|
||||
// Korrigiert die Zeitzonenverschiebung
|
||||
const timezoneOffset = date.getTimezoneOffset() * 60000 // in ms
|
||||
const localISOTime = new Date(date.getTime() - timezoneOffset).toISOString().slice(0, 10)
|
||||
return localISOTime
|
||||
}
|
||||
function submitUser() {
|
||||
const validations = [
|
||||
{ id: "firstName", value: personalData.firstName, validator: validateInput },
|
||||
{ id: "lastName", value: personalData.lastName, validator: validateInput },
|
||||
{ id: "userName", value: personalData.userName, validator: validateInput },
|
||||
{ id: "birthday", value: personalData.birthday, validator: validateBirthday },
|
||||
{ id: "gender", value: personalData.gender, validator: validateInput },
|
||||
]
|
||||
let isValid = true
|
||||
for (const { id, value, validator } of validations) {
|
||||
// @ts-ignore
|
||||
if (!validateField(id, value, validator)) isValid = false
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
customer.first_name = personalData.firstName
|
||||
customer.last_name = personalData.lastName
|
||||
updateCustomer($login.tokenData.tibiId, {
|
||||
id: customer.id,
|
||||
first_name: personalData.firstName,
|
||||
last_name: personalData.lastName,
|
||||
form_fields: [
|
||||
{
|
||||
name: "gender",
|
||||
value: personalData.gender,
|
||||
customer_id: customer.id,
|
||||
},
|
||||
{
|
||||
name: "birthday",
|
||||
value: personalData.birthday ? formatDate(personalData.birthday) : undefined,
|
||||
customer_id: customer.id,
|
||||
},
|
||||
{
|
||||
name: "username",
|
||||
value: personalData.userName,
|
||||
customer_id: customer.id,
|
||||
},
|
||||
],
|
||||
}).then(() => (editMode = false))
|
||||
}
|
||||
}
|
||||
|
||||
const genderOptions = [
|
||||
{ label: "Männlich", value: "male" },
|
||||
{ label: "Weiblich", value: "female" },
|
||||
]
|
||||
let form: HTMLFormElement
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<CardWrapper>
|
||||
<form
|
||||
bind:this="{form}"
|
||||
class:editMode="{editMode}"
|
||||
>
|
||||
<h3>Persönliche Daten</h3>
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{() => {}}"
|
||||
bind:value="{personalData.email}"
|
||||
placeholder="E-Mail*"
|
||||
id="eMail"
|
||||
disabled="{true}"
|
||||
type="{editMode ? 'text' : 'noInput'}"
|
||||
helperText="{editMode
|
||||
? `Sie können die E-Mail Adresse nur im Abschnitt 'Identifizierende Daten' ändern.`
|
||||
: ``}"
|
||||
/>
|
||||
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{personalData.userName}"
|
||||
placeholder="Nutzername*"
|
||||
id="userName"
|
||||
disabled="{true}"
|
||||
type="{editMode ? 'text' : 'noInput'}"
|
||||
helperText="{editMode
|
||||
? `Dein Nutzername wird öffentlich angezeigt. Sie können diesen nur im Abschnitt 'Identifizierende Daten' ändern.`
|
||||
: ``}"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{personalData.firstName}"
|
||||
placeholder="Vorname*"
|
||||
id="firstName"
|
||||
type="{editMode ? 'text' : 'noInput'}"
|
||||
/>
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{personalData.lastName}"
|
||||
placeholder="Nachname*"
|
||||
id="lastName"
|
||||
type="{editMode ? 'text' : 'noInput'}"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Calendar
|
||||
bind:dateValue="{personalData.birthday}"
|
||||
placeholder="Geburtsdatum"
|
||||
id="birthday"
|
||||
editMode="{editMode}"
|
||||
/>
|
||||
<div
|
||||
id="gender"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<Select
|
||||
bind:value="{personalData.gender}"
|
||||
options="{genderOptions}"
|
||||
placeholder="Gender*"
|
||||
on:change="{() => {
|
||||
const el = document.getElementById('gender')
|
||||
el.classList.remove('error')
|
||||
const parent = el.parentElement
|
||||
const errorNode = parent?.querySelector('.error-message')
|
||||
if (errorNode) {
|
||||
parent?.removeChild(errorNode)
|
||||
}
|
||||
}}"
|
||||
selectedOption="{personalData.gender
|
||||
? {
|
||||
label:
|
||||
personalData.gender == 'male'
|
||||
? 'Männlich'
|
||||
: personalData.gender == 'female'
|
||||
? 'Weiblich'
|
||||
: '',
|
||||
value: personalData.gender,
|
||||
}
|
||||
: undefined}"
|
||||
clearable="{true}"
|
||||
editMode="{editMode}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if editMode}
|
||||
<div class="action-button-line">
|
||||
<button
|
||||
class="cta primary"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
editMode = false
|
||||
personalData.firstName = customer.first_name
|
||||
personalData.lastName = customer.last_name
|
||||
removeAllErrors(form)
|
||||
}}">Änderungen verwerfen</button
|
||||
>
|
||||
<button
|
||||
class="cta secondary"
|
||||
on:click|stopPropagation|preventDefault="{submitUser}">Änderungen speichern</button
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="action-line"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
editMode = true
|
||||
}}"
|
||||
>
|
||||
<p>Angaben bearbeiten</p>
|
||||
|
||||
<span><Icon path="{mdiPencil}" /> </span>
|
||||
</button>
|
||||
{/if}
|
||||
</form>
|
||||
</CardWrapper>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../../assets/css/variables.less";
|
||||
.wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
form {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,191 +0,0 @@
|
||||
import { get } from "svelte/store"
|
||||
import { emailRegex, phoneRegex } from "../../../../config"
|
||||
import { location, newNotification } from "../../../store"
|
||||
|
||||
export function checkPassword(password: string): string | null {
|
||||
if (password.length < 7) return "Das Passwort muss mindestens 7 Zeichen lang sein."
|
||||
|
||||
const hasAlphabetic = /[a-zA-Z]/.test(password)
|
||||
if (!hasAlphabetic) return "Das Passwort muss mindestens einen alphabetischen Buchstaben enthalten."
|
||||
|
||||
const hasNumeric = /[0-9]/.test(password)
|
||||
if (!hasNumeric) return "Das Passwort muss mindestens eine Zahl enthalten."
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function validateEmail(email: string): string | null {
|
||||
return emailRegex.test(email) ? null : "Email hat kein gültiges Format."
|
||||
}
|
||||
|
||||
export function validatePhone(phone: string): string | null {
|
||||
return !phone ? null : phoneRegex.test(phone) ? null : "Telefonnummer hat kein gültiges Format."
|
||||
}
|
||||
|
||||
export function validateInput(input: string): string | null {
|
||||
return input?.length > 0 ? null : "Dieses Feld ist erforderlich."
|
||||
}
|
||||
|
||||
export function validateNumberInput(input: number): string | null {
|
||||
return !isNaN(input) ? null : "Dieses Feld ist erforderlich."
|
||||
}
|
||||
|
||||
export function validateDataProtection(checked: boolean): string | null {
|
||||
return checked ? null : "Bitte akzeptiere die Datenschutzbestimmungen."
|
||||
}
|
||||
export function validateBirthday(birthday: Date): string | null {
|
||||
// check if reasonable
|
||||
if (!birthday) return null
|
||||
const now = new Date()
|
||||
if (birthday >= now) return "Bitte gib ein gültiges Geburtsdatum ein."
|
||||
return null
|
||||
}
|
||||
|
||||
export function validateUsername(username: string): string | null {
|
||||
const asciiRegex = /^[\x00-\x7F]+$/
|
||||
const noSpacesRegex = /^\S+$/
|
||||
const validCharsRegex = /^[a-zA-Z0-9-_]+$/
|
||||
if (!username) return "Der Benutzername darf nicht leer sein."
|
||||
if (!asciiRegex.test(username))
|
||||
return "Der Benutzername darf nur Buchstaben, Zahlen und Symbole enthalten, die auf einer normalen Tastatur zu finden sind."
|
||||
if (!noSpacesRegex.test(username)) return "Der Benutzername darf keine Leerzeichen enthalten."
|
||||
if (!validCharsRegex.test(username))
|
||||
return "Der Benutzername darf nur Buchstaben, Zahlen, Bindestriche und Unterstriche enthalten."
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function onChange(e: Event, element = e.currentTarget as HTMLInputElement) {
|
||||
const el = element
|
||||
el.classList.remove("error")
|
||||
|
||||
const parent = el.parentElement
|
||||
const errorNode = parent?.querySelector(".error-message")
|
||||
if (errorNode) {
|
||||
parent?.removeChild(errorNode)
|
||||
}
|
||||
}
|
||||
|
||||
export function removeAllErrors(form: HTMLFormElement) {
|
||||
const errorNodes = form.querySelectorAll(".error-message")
|
||||
errorNodes.forEach((node) => node.remove())
|
||||
|
||||
const errorInputs = form.querySelectorAll(".error")
|
||||
errorInputs.forEach((input) => input.classList.remove("error"))
|
||||
}
|
||||
|
||||
export function appendErrorMessage(
|
||||
element: HTMLInputElement | HTMLDivElement,
|
||||
message: string,
|
||||
parentEl?: HTMLElement
|
||||
) {
|
||||
const parent = parentEl || element.parentElement
|
||||
if (parent) {
|
||||
// Use a unique identifier for the error message
|
||||
const errorMessageId = `error-message-${element.name || element.id}`
|
||||
|
||||
let existingErrorNode = parent.querySelector(`.error-message[data-for="${errorMessageId}"]`)
|
||||
if (existingErrorNode) {
|
||||
existingErrorNode.textContent = message
|
||||
} else {
|
||||
const errorNode = document.createElement("div")
|
||||
errorNode.className = "error-message"
|
||||
errorNode.dataset.for = errorMessageId
|
||||
errorNode.textContent = message
|
||||
parent.appendChild(errorNode)
|
||||
}
|
||||
|
||||
// Calculate and set the left position
|
||||
const errorNode = parent.querySelector(`.error-message[data-for="${errorMessageId}"]`)
|
||||
if (errorNode) {
|
||||
const elementRect = element.getBoundingClientRect()
|
||||
const parentRect = parent.getBoundingClientRect()
|
||||
const leftPosition =
|
||||
elementRect.left -
|
||||
parentRect.left +
|
||||
0.7 * parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||
errorNode.style.left = `${leftPosition}px`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getQueryParam(param: string) {
|
||||
return new URLSearchParams(get(location).search).get(param)
|
||||
}
|
||||
export function validateField(
|
||||
elementId: string,
|
||||
value: string | boolean,
|
||||
validator: (value: string | boolean) => string | null
|
||||
): boolean {
|
||||
const element = document.getElementById(elementId) as HTMLInputElement
|
||||
const error = validator(value)
|
||||
if (error) {
|
||||
element.classList.add("error")
|
||||
appendErrorMessage(element, error)
|
||||
newNotification({ html: error, class: "error" })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function validateAddress(address: BigCommerceAddress): boolean {
|
||||
const validations = [
|
||||
{ id: "firstName", value: address.first_name, validator: validateInput },
|
||||
{ id: "lastName", value: address.last_name, validator: validateInput },
|
||||
{ id: "address1", value: address.address1, validator: validateInput },
|
||||
{ id: "postalCode", value: address.postal_code, validator: validateInput },
|
||||
{ id: "city", value: address.city, validator: validateInput },
|
||||
]
|
||||
|
||||
let isValid = true
|
||||
|
||||
for (const { id, value, validator } of validations) {
|
||||
// @ts-ignore
|
||||
if (!validateField(id, value, validator)) isValid = false
|
||||
}
|
||||
return isValid
|
||||
}
|
||||
|
||||
export function createEmptyAddress(): BigCommerceAddress {
|
||||
return {
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
address1: "",
|
||||
address2: "",
|
||||
city: "",
|
||||
country_code: "DE",
|
||||
postal_code: "",
|
||||
phone: "",
|
||||
address_type: "residential",
|
||||
company: "",
|
||||
country: "germany",
|
||||
customer_id: undefined,
|
||||
state_or_province: undefined,
|
||||
id: undefined,
|
||||
form_fields: [],
|
||||
}
|
||||
}
|
||||
export async function convertObjectURLToOriginalFormat(value: FileField): Promise<FileField> {
|
||||
if (value.src.startsWith("blob:")) {
|
||||
const response = await fetch(value.src)
|
||||
const blob = await response.blob()
|
||||
const reader = new FileReader()
|
||||
|
||||
return new Promise((resolve) => {
|
||||
reader.onloadend = () => {
|
||||
const result = reader.result
|
||||
const originalFormat = {
|
||||
path: value.path,
|
||||
type: value.type,
|
||||
size: value.size,
|
||||
src: result instanceof ArrayBuffer ? result.toString() : result,
|
||||
}
|
||||
resolve(originalFormat)
|
||||
}
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
} else {
|
||||
// If src is not an objectURL, return the value as is
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiArrowLeft } from "@mdi/js"
|
||||
import {
|
||||
getOrderReturnRequests,
|
||||
getOrderRevokeRequests,
|
||||
getTibiRestOrder,
|
||||
} from "../../../../functions/CommerceAPIs/tibiEndpoints/orders"
|
||||
import Icon from "../../../widgets/Icon.svelte"
|
||||
import ShippingOverview from "./overview/ShippingOverview.svelte"
|
||||
import OrderOverview from "./overview/OrderOverview.svelte"
|
||||
import OrderDetails from "./overview/OrderDetails.svelte"
|
||||
import Loader from "../../Loader.svelte"
|
||||
import { spaLink } from "../../../../actions"
|
||||
import OrderReturnView from "./return/OrderReturnView.svelte"
|
||||
import { location, login } from "../../../../store"
|
||||
import { getShownIdFraction } from "./helper"
|
||||
import ReturnOverview from "./return/ReturnOverview.svelte"
|
||||
import OrderCancelledBanner from "./widgets/OrderCancelledBanner.svelte"
|
||||
import OrderProblemBanner from "./widgets/OrderProblemBanner.svelte"
|
||||
import OrderRating from "./rating/OrderRating.svelte"
|
||||
|
||||
import CustomerSupportRequest from "../../../widgets/CustomerSupportRequest.svelte"
|
||||
|
||||
export let orderId: string
|
||||
function formatDate(date: string) {
|
||||
return new Date(date).toLocaleDateString("de-DE", { day: "numeric", month: "long", year: "numeric" })
|
||||
}
|
||||
let shownReturnRequest: string
|
||||
let refresh = false
|
||||
</script>
|
||||
|
||||
<section class="small-wrapper">
|
||||
<div class="order-table">
|
||||
{#key refresh}
|
||||
{#await getTibiRestOrder(orderId)}
|
||||
<Loader
|
||||
size="4"
|
||||
type="bar"
|
||||
/>
|
||||
{:then order}
|
||||
<div class="headline">
|
||||
<a
|
||||
use:spaLink
|
||||
href="{$location.path.split('/').slice(0, -1).join('/')}"
|
||||
><Icon path="{mdiArrowLeft}" /> <span>Zurück</span></a
|
||||
>
|
||||
|
||||
<h2>Deine Bestellung #{getShownIdFraction(orderId)}</h2>
|
||||
<p>vom {formatDate(order.date_created)}</p>
|
||||
</div>
|
||||
|
||||
{#if $location.path.includes("return")}
|
||||
<OrderReturnView
|
||||
order="{order}"
|
||||
on:updateOrder="{() => {
|
||||
refresh = !refresh
|
||||
}}"
|
||||
/>
|
||||
{:else if $location.path.includes("rate")}
|
||||
<OrderRating order="{order}" />
|
||||
{:else}
|
||||
{#await getOrderReturnRequests(order.id) then returnRequests}
|
||||
{#if returnRequests.length > 0}
|
||||
<div class="tabs">
|
||||
<button
|
||||
class:active="{!Boolean(shownReturnRequest)}"
|
||||
on:click="{() => (shownReturnRequest = '')}">Übersicht</button
|
||||
>
|
||||
{#each returnRequests as returnRequest}
|
||||
<button
|
||||
class:active="{shownReturnRequest == returnRequest.id}"
|
||||
on:click="{() => (shownReturnRequest = returnRequest.id)}"
|
||||
>
|
||||
Stornierung #{getShownIdFraction(returnRequest.id)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if shownReturnRequest}
|
||||
{#key shownReturnRequest}
|
||||
<ReturnOverview
|
||||
order="{order}"
|
||||
returnRequest="{returnRequests.find((r) => r.id == shownReturnRequest)}"
|
||||
on:updateOrder="{() => {
|
||||
refresh = !refresh
|
||||
}}"
|
||||
/>{/key}
|
||||
{/if}
|
||||
{/await}
|
||||
{#if order.status_id == 13 || order.status_id == 6}
|
||||
<OrderProblemBanner />
|
||||
{/if}
|
||||
{#if !shownReturnRequest}
|
||||
<div class="grid">
|
||||
<div class="item1">
|
||||
{#await getOrderRevokeRequests(order.id) then revokeRequests}
|
||||
{#if revokeRequests.length > 0}
|
||||
<OrderCancelledBanner revokeRequests="{revokeRequests}" />
|
||||
{:else}
|
||||
<ShippingOverview
|
||||
order="{order}"
|
||||
on:updateOrder="{() => (refresh = !refresh)}"
|
||||
/>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
<div class="item2">
|
||||
<OrderOverview
|
||||
order="{order}"
|
||||
on:updateOrder="{() => (refresh = !refresh)}"
|
||||
/>
|
||||
</div>
|
||||
<div class="item3"><OrderDetails order="{order}" /></div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{:catch}
|
||||
<div class="headline">
|
||||
<a
|
||||
use:spaLink
|
||||
href="{$location.path.split('/').slice(0, -1).join('/')}"
|
||||
><Icon path="{mdiArrowLeft}" /> <span>Zurück</span></a
|
||||
>
|
||||
|
||||
<h2>Fehler</h2>
|
||||
</div>
|
||||
<p>
|
||||
Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es später erneut oder aktualisiere die
|
||||
Seite. Sollte das Problem weiterhin bestehen, wende dich bitte an den Kundenservice.
|
||||
</p>
|
||||
{/await}
|
||||
{/key}
|
||||
</div>
|
||||
</section>
|
||||
<CustomerSupportRequest
|
||||
title="Du hast Fragen zu deiner Bestellung?"
|
||||
email="{$login.customer.email}"
|
||||
description="Ich habe ein Problem mit der Bestellung Nr. #{getShownIdFraction(orderId)}..."
|
||||
/>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../../assets/css/variables.less";
|
||||
|
||||
section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.order-table {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 2.4rem;
|
||||
gap: 2.4rem;
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
flex-wrap: wrap;
|
||||
button {
|
||||
color: var(--text-invers-100);
|
||||
border-bottom: 3px solid var(--text-invers-100);
|
||||
opacity: 0.5;
|
||||
font-size: 1.2rem;
|
||||
white-space: nowrap;
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.headline {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
margin-top: 2.4rem;
|
||||
button,
|
||||
a,
|
||||
h2,
|
||||
p {
|
||||
color: var(--text-invers-100);
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
span {
|
||||
font-size: 0.7rem;
|
||||
line-height: 0.7rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
.grid {
|
||||
width: 100%;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
@media @desktop {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
column-gap: 2.4rem;
|
||||
row-gap: 2.4rem;
|
||||
.item1 {
|
||||
width: 100%;
|
||||
grid-column: 1;
|
||||
}
|
||||
.item2 {
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 3;
|
||||
grid-column: 2;
|
||||
@media @max-tablet {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.item3 {
|
||||
grid-column: 1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,145 +0,0 @@
|
||||
type OrderStatus = {
|
||||
code: number
|
||||
name: string
|
||||
description: string
|
||||
color: string
|
||||
icon: any
|
||||
}
|
||||
|
||||
import {
|
||||
mdiAlertCircleOutline,
|
||||
mdiClockOutline,
|
||||
mdiTruckFast,
|
||||
mdiTruckOutline,
|
||||
mdiCashRefund,
|
||||
mdiCancel,
|
||||
mdiThumbDownOutline,
|
||||
mdiCreditCardClockOutline,
|
||||
mdiStorefrontOutline,
|
||||
mdiTruckDeliveryOutline,
|
||||
mdiCheckAll,
|
||||
mdiTimerSandComplete,
|
||||
mdiAccountAlertOutline,
|
||||
mdiHandCoinOutline,
|
||||
mdiCashMultiple,
|
||||
} from "@mdi/js"
|
||||
|
||||
export const orderStatuses: OrderStatus[] = [
|
||||
{
|
||||
code: 0,
|
||||
name: "Unvollständig",
|
||||
description:
|
||||
"Ein unvollständiger Auftrag, bei dem der Käufer die Zahlungsseite erreicht hat, aber die Transaktion nicht abgeschlossen hat.",
|
||||
color: "#FF0000", // Red
|
||||
icon: mdiAlertCircleOutline, // Unvollständig
|
||||
},
|
||||
{
|
||||
code: 1,
|
||||
name: "Ausstehend",
|
||||
description: "Der Kunde hat den Checkout-Prozess gestartet, ihn aber nicht abgeschlossen.",
|
||||
color: "#FFA500", // Orange
|
||||
icon: mdiClockOutline, // Ausstehend
|
||||
},
|
||||
{
|
||||
code: 2,
|
||||
name: "Versandt",
|
||||
description:
|
||||
"Die Bestellung wurde versandt, der Empfang wurde jedoch nicht bestätigt; der Verkäufer hat die Aktion 'Artikel versenden' verwendet.",
|
||||
color: "#0000FF", // Blue
|
||||
icon: mdiTruckFast, // Versandt
|
||||
},
|
||||
{
|
||||
code: 3,
|
||||
name: "Teilweise Versandt",
|
||||
description:
|
||||
"Nur einige Artikel der Bestellung wurden versandt, da einige Produkte nur vorbestellt sind oder aus anderen Gründen.",
|
||||
color: "#1E90FF", // Dodger Blue
|
||||
icon: mdiTruckOutline, // Teilweise Versandt
|
||||
},
|
||||
{
|
||||
code: 4,
|
||||
name: "Erstattet",
|
||||
description: "Der Verkäufer hat die Aktion 'Rückerstattung' verwendet.",
|
||||
color: "#008000", // Green
|
||||
icon: mdiCashRefund, // Erstattet
|
||||
},
|
||||
{
|
||||
code: 5,
|
||||
name: "Storniert",
|
||||
description:
|
||||
"Der Verkäufer hat eine Bestellung aufgrund einer Bestandsinkonsistenz oder aus anderen Gründen storniert.",
|
||||
color: "#A9A9A9", // Dark Gray
|
||||
icon: mdiCancel, // Storniert
|
||||
},
|
||||
{
|
||||
code: 6,
|
||||
name: "Abgelehnt",
|
||||
description:
|
||||
"Der Verkäufer hat die Bestellung als abgelehnt markiert, weil eine manuelle Zahlung nicht erfolgte oder aus anderen Gründen.",
|
||||
color: "#FF4500", // Orange Red
|
||||
icon: mdiThumbDownOutline, // Abgelehnt
|
||||
},
|
||||
{
|
||||
code: 7,
|
||||
name: "Zahlung ausstehend",
|
||||
description: "Der Kunde hat den Checkout-Prozess abgeschlossen, aber die Zahlung muss noch bestätigt werden.",
|
||||
color: "#FFD700", // Gold
|
||||
icon: mdiCreditCardClockOutline, // Zahlung ausstehend
|
||||
},
|
||||
{
|
||||
code: 8,
|
||||
name: "Abholung ausstehend",
|
||||
description:
|
||||
"Die Bestellung wurde zusammengestellt und wartet auf die Abholung durch den Kunden an einem vom Verkäufer angegebenen Ort.",
|
||||
color: "#8B4513", // Saddle Brown
|
||||
icon: mdiStorefrontOutline, // Abholung ausstehend
|
||||
},
|
||||
{
|
||||
code: 9,
|
||||
name: "Versand ausstehend",
|
||||
description:
|
||||
"Die Bestellung wurde zusammengestellt und verpackt und wartet auf die Abholung durch einen Versanddienstleister.",
|
||||
color: "#00BFFF", // Deep Sky Blue
|
||||
icon: mdiTruckDeliveryOutline, // Versand ausstehend
|
||||
},
|
||||
{
|
||||
code: 10,
|
||||
name: "Abgeschlossen",
|
||||
description: "Der Kunde hat für sein digitales Produkt bezahlt und seine Datei(en) stehen zum Download bereit.",
|
||||
color: "#32CD32", // Lime Green
|
||||
icon: mdiCheckAll, // Abgeschlossen
|
||||
},
|
||||
{
|
||||
code: 11,
|
||||
name: "Erfüllung ausstehend",
|
||||
description: "Der Kunde hat den Checkout-Prozess abgeschlossen und die Zahlung wurde bestätigt.",
|
||||
color: "#F2C94C", // Yellow
|
||||
icon: mdiTimerSandComplete, // Erfüllung ausstehend
|
||||
},
|
||||
{
|
||||
code: 12,
|
||||
name: "Manuelle Überprüfung erforderlich",
|
||||
description: "Die Bestellung ist in der Warteschleife, während ein Aspekt manuell bestätigt werden muss.",
|
||||
color: "#FF69B4", // Hot Pink
|
||||
icon: mdiAccountAlertOutline, // Manuelle Überprüfung erforderlich
|
||||
},
|
||||
{
|
||||
code: 13,
|
||||
name: "Umstritten",
|
||||
description:
|
||||
"Der Kunde hat ein Streitbeilegungsverfahren für die PayPal-Transaktion eingeleitet, mit der die Bestellung bezahlt wurde.",
|
||||
color: "#FF6347", // Tomato
|
||||
icon: mdiHandCoinOutline, // Umstritten
|
||||
},
|
||||
{
|
||||
code: 14,
|
||||
name: "Teilweise erstattet",
|
||||
description: "Der Verkäufer hat die Bestellung teilweise zurückerstattet.",
|
||||
color: "#2E8B57", // Sea Green
|
||||
icon: mdiCashMultiple, // Teilweise erstattet
|
||||
},
|
||||
]
|
||||
|
||||
export function getShownIdFraction(id: string): string {
|
||||
return id.slice(12, 24).toUpperCase()
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { parsePriceTag } from "../../../../../utils"
|
||||
import CardWrapper from "../../CardWrapper.svelte"
|
||||
import ProductImages from "../widgets/ProductImages.svelte"
|
||||
import OrderStatus from "../widgets/orderStatus.svelte"
|
||||
export let order: V2OrderResponseBase
|
||||
|
||||
function formatDate(date: string) {
|
||||
return new Date(date).toLocaleDateString("de-DE", { day: "numeric", month: "long", year: "numeric" })
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardWrapper
|
||||
clickable="{true}"
|
||||
on:click
|
||||
>
|
||||
<div class="wrapper">
|
||||
<div>
|
||||
<ProductImages
|
||||
circularRepresentation="{false}"
|
||||
productVariants="{order.productObjs}"
|
||||
/>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div>
|
||||
<div class="col">
|
||||
<div>Bestellnummer #{order.id}</div>
|
||||
<em>{formatDate(order.date_created)}</em>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="col">
|
||||
<div>Artikel</div>
|
||||
<em>{order.productObjs.length}</em>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div>Gesamt</div>
|
||||
<em>{parsePriceTag(order.total_inc_tax)}</em>
|
||||
</div>
|
||||
{#if Number(order.discount_amount) > 0}
|
||||
<div class="col">
|
||||
<div>Rabbatt</div>
|
||||
<em>-{parsePriceTag(order.discount_amount)}</em>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<OrderStatus order="{order}" />
|
||||
</div>
|
||||
</div>
|
||||
</CardWrapper>
|
||||
|
||||
<style lang="less">
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 1.2rem;
|
||||
width: 405px;
|
||||
min-width: 100%;
|
||||
@media (max-width: 600px) {
|
||||
max-width: calc(100vw - 2px - 2 * var(--horizontal-default-margin));
|
||||
}
|
||||
max-width: calc((100vw - 2px - 2 * var(--horizontal-default-margin)) / 2 - 2.4em);
|
||||
padding: 0px 2.4rem 2.4rem 2.4rem;
|
||||
& > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
& > div {
|
||||
padding: 6px 0px;
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
width: 100%;
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1.2rem;
|
||||
div {
|
||||
color: var(--text-invers-100);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
em {
|
||||
color: var(--text-invers-100);
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,94 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { parsePriceTag } from "../../../../../utils"
|
||||
import CardWrapper from "../../CardWrapper.svelte"
|
||||
import ProductImages from "../widgets/ProductImages.svelte"
|
||||
import OrderStatus from "../widgets/orderStatus.svelte"
|
||||
export let order: V2OrderResponseBase
|
||||
|
||||
function formatDate(date: string) {
|
||||
return new Date(date).toLocaleDateString("de-DE", { day: "numeric", month: "long", year: "numeric" })
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardWrapper
|
||||
clickable="{true}"
|
||||
on:click
|
||||
>
|
||||
<div class="wrapper">
|
||||
<div class="content">
|
||||
<div
|
||||
class="col"
|
||||
style="width: 252px;"
|
||||
>
|
||||
<ProductImages productVariants="{order.productObjs}" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<div>Bestellnummer #{order.id}</div>
|
||||
<em>{formatDate(order.date_created)}</em>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div>Artikel</div>
|
||||
<em>{order.productObjs.length}</em>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div>Gesamt</div>
|
||||
<em>{parsePriceTag(order.total_inc_tax)}</em>
|
||||
</div>
|
||||
{#if Number(order.discount_amount) > 0}
|
||||
<div class="col">
|
||||
<div>Rabbatt</div>
|
||||
<em>-{parsePriceTag(order.discount_amount)}</em>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<OrderStatus order="{order}" />
|
||||
</div>
|
||||
</CardWrapper>
|
||||
|
||||
<style lang="less">
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
padding: 0px 48px 48px 48px;
|
||||
.content {
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
@media (min-width: 1200px) {
|
||||
gap: 2.4rem;
|
||||
}
|
||||
.col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 24px;
|
||||
div {
|
||||
color: var(--text-invers-100);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
em {
|
||||
color: var(--text-invers-100);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
.status {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
p {
|
||||
color: var(--text-invers-100);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
span {
|
||||
border-radius: 50%;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,54 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { login } from "../../../../../store"
|
||||
import CardWrapper from "../../CardWrapper.svelte"
|
||||
export let order: V2OrderResponseBase
|
||||
const sA = order?.shipping_addressObjs?.[0]
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<CardWrapper>
|
||||
<div class="header"><h3>Bestelldetails</h3></div>
|
||||
<div class="content">
|
||||
<div>
|
||||
<h4>Kontaktinformationen</h4>
|
||||
<p>{sA.first_name} {sA.last_name}</p>
|
||||
<p>{$login?.customer?.email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Rechnungsadresse</h4>
|
||||
<p>{sA.first_name} {sA.last_name}</p>
|
||||
<p>{sA.street_1}</p>
|
||||
<p>{sA.zip} {sA.city}</p>
|
||||
<p>{sA.country}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardWrapper>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
.wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
padding: 0px 2.4rem;
|
||||
h3 {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
}
|
||||
.content {
|
||||
padding: 0px 2.4rem;
|
||||
padding-bottom: 2.4rem;
|
||||
display: flex;
|
||||
gap: 2.4rem;
|
||||
flex-wrap: wrap;
|
||||
h4 {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,162 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { checkIfOrderMayArrived, getLastDayForReturn } from "../../../../../../../../api/hooks/config-client"
|
||||
import { spaLink } from "../../../../../actions"
|
||||
import {
|
||||
createOrderRevokeRequest,
|
||||
getOrderRevokeRequests,
|
||||
} from "../../../../../functions/CommerceAPIs/tibiEndpoints/orders"
|
||||
import { location, newNotification } from "../../../../../store"
|
||||
import Modal from "../../../../Modal.svelte"
|
||||
import CardWrapper from "../../CardWrapper.svelte"
|
||||
import OriginalCartPreview from "./OriginalCartPreview.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
export let order: V2OrderResponseBase
|
||||
const dispatch = createEventDispatcher()
|
||||
let showCancelOrderModal = false
|
||||
let orderRevokeRequests = []
|
||||
getOrderRevokeRequests(order.id).then((res) => (orderRevokeRequests = res))
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<CardWrapper>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h3>Bestellübersicht</h3>
|
||||
</div>
|
||||
{#if orderRevokeRequests.length > 0}
|
||||
<p>
|
||||
Du hast die Bestellung abgebrochen. Das Geld wird innerhalb von 14 Tagen auf das Ursprüngliche
|
||||
Zahlungsmittel zurück gebucht. Sollte das nicht geschehen, wende dich bitte an den Kundenservice.
|
||||
</p>
|
||||
{:else}
|
||||
<p>
|
||||
Du kannst deine bestellten Produkte <a
|
||||
use:spaLink
|
||||
href="{$location.path}/rate"
|
||||
>Bewerten.
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
{#if orderRevokeRequests.length == 0}
|
||||
{#if !order.status || order.status == "draft"}
|
||||
{#if order.status_id !== 5}
|
||||
<p>
|
||||
Du kannst die Bestellung noch <button
|
||||
on:click="{() => {
|
||||
showCancelOrderModal = true
|
||||
}}">abbrechen.</button
|
||||
>
|
||||
</p>
|
||||
{/if}
|
||||
{:else if (order.shipments || []).length > 0 && order.shipments[0].sentAt && checkIfOrderMayArrived(new Date(order.shipments[0].sentAt))}
|
||||
<p>
|
||||
Du kannst die Bestellung noch bis zum einschließlich {new Date(
|
||||
getLastDayForReturn(order.statusSetAt)
|
||||
).toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" })}
|
||||
<a
|
||||
use:spaLink
|
||||
href="{$location.path}/return">Zurückgeben.</a
|
||||
>
|
||||
</p>
|
||||
{:else if order.status == "onhold" || order.status == "partial" || order.status == "inprocess" || order.status == "fulfilled"}
|
||||
<p>
|
||||
Du kannst die Bestellung noch <a
|
||||
use:spaLink
|
||||
href="{$location.path}/return">Zurückgeben.</a
|
||||
>
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="content">
|
||||
<OriginalCartPreview order="{order}" />
|
||||
</div>
|
||||
</CardWrapper>
|
||||
</div>
|
||||
{#if showCancelOrderModal}
|
||||
<Modal
|
||||
show="{true}"
|
||||
on:close="{() => {
|
||||
showCancelOrderModal = false
|
||||
}}"
|
||||
>
|
||||
<svelte:fragment slot="title">Abbruch der Bestellung</svelte:fragment>
|
||||
<p>
|
||||
Bist du dir sicher, die Bestellung abzubrechen? Die Ware wird nicht versandt und das Geld innerhalb von 14
|
||||
Tagen auf das Ursprüngliche Zahlungsmittel zurück gebucht.
|
||||
</p>
|
||||
<div
|
||||
slot="footer"
|
||||
class="cancel-order-footer"
|
||||
>
|
||||
<button
|
||||
class="btn cta primary"
|
||||
disabled="{loading}"
|
||||
on:click="{() => {
|
||||
loading = true
|
||||
createOrderRevokeRequest({
|
||||
status: 'cancelled',
|
||||
bigCommerceId: order.id,
|
||||
}).then(() => {
|
||||
dispatch('updateOrder')
|
||||
newNotification({
|
||||
html: 'Die Bestellung wurde erfolgreich abgebrochen.',
|
||||
class: 'success',
|
||||
})
|
||||
showCancelOrderModal = false
|
||||
loading = false
|
||||
})
|
||||
}}"
|
||||
>
|
||||
Bestellung abbrechen
|
||||
</button>
|
||||
<button
|
||||
class="btn cta secondary"
|
||||
disabled="{loading}"
|
||||
on:click="{() => {
|
||||
showCancelOrderModal = false
|
||||
}}">Zurück</button
|
||||
>
|
||||
</div>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style lang="less">
|
||||
h2 {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
padding: 0px 2.4rem;
|
||||
h3 {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
p {
|
||||
button {
|
||||
font-size: 1rem;
|
||||
color: var(--primary-100);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
padding: 0px 2.4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
}
|
||||
.cancel-order-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
@@ -1,84 +0,0 @@
|
||||
<script
|
||||
context="module"
|
||||
lang="ts"
|
||||
>
|
||||
export async function createCart(order: V2OrderResponseBase): Promise<BKDFCart> {
|
||||
const productLines = await Promise.all(
|
||||
order.productObjs.map(async (productObj) => {
|
||||
const product = await getBCGraphProduct(String(productObj.product_id))
|
||||
product.retailPrice = {
|
||||
amount: String(Number(productObj.base_total) / 1.19),
|
||||
currencyCode: order.currency_code,
|
||||
}
|
||||
product.salePrice = {
|
||||
amount: String(Number(productObj.total_inc_tax) / 1.19),
|
||||
currencyCode: order.currency_code,
|
||||
}
|
||||
return {
|
||||
id: productObj.id,
|
||||
title: productObj.name,
|
||||
quantity: productObj.quantity,
|
||||
cost: {
|
||||
totalAmount: {
|
||||
amount: productObj.price_inc_tax,
|
||||
currencyCode: order.currency_code,
|
||||
},
|
||||
},
|
||||
merchandise: {
|
||||
id: productObj.product_id,
|
||||
title: productObj.name,
|
||||
selectedOptions: productObj.product_options.map((option) => {
|
||||
return {
|
||||
name: option.display_name_customer,
|
||||
value: option.display_value_customer,
|
||||
}
|
||||
}),
|
||||
product: product,
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const cart: BKDFCart = {
|
||||
id: "dummyCartObj",
|
||||
cost: {
|
||||
discountedAmount: {
|
||||
amount: Number(order.discount_amount).toFixed(2),
|
||||
currencyCode: order.currency_code,
|
||||
},
|
||||
couponDiscount: {
|
||||
amount: Number(order.coupon_discount).toFixed(2),
|
||||
currencyCode: order.currency_code,
|
||||
},
|
||||
amount: {
|
||||
amount: Number(order.total_inc_tax).toFixed(2),
|
||||
currencyCode: order.currency_code,
|
||||
},
|
||||
},
|
||||
lines: productLines,
|
||||
}
|
||||
return cart
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { getBCGraphProduct } from "../../../../../functions/CommerceAPIs/bigCommerce/product"
|
||||
import OverlayCart from "../../../cart/OverlayCart.svelte"
|
||||
import Loader from "../../../Loader.svelte"
|
||||
|
||||
export let order: V2OrderResponseBase
|
||||
</script>
|
||||
|
||||
{#await createCart(order)}
|
||||
<Loader
|
||||
size="4"
|
||||
type="bar"
|
||||
/>
|
||||
{:then cart}
|
||||
<OverlayCart
|
||||
cart="{cart}"
|
||||
hideActions="{true}"
|
||||
showQuantity="{true}"
|
||||
shippingIncludedInTotal="{true}"
|
||||
/>
|
||||
{/await}
|
||||
@@ -1,139 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiCheckAll, mdiMessageAlert, mdiTimerSand, mdiTruckDelivery } from "@mdi/js"
|
||||
import StateLogPreview from "./StateLogPreview.svelte"
|
||||
import CardWrapper from "../../CardWrapper.svelte"
|
||||
export let order: V2OrderResponseBase
|
||||
const orderRecognized = {
|
||||
state: "Bestellung eingegangen",
|
||||
date: order.statusSetAt,
|
||||
active: true,
|
||||
icon: mdiCheckAll,
|
||||
}
|
||||
const orderInProcess = {
|
||||
state: "Wir arbeiten dran",
|
||||
date: order.statusSetAt,
|
||||
active: true,
|
||||
icon: mdiTimerSand,
|
||||
}
|
||||
|
||||
const orderFinished = {
|
||||
state: "Bestellung fertiggestellt",
|
||||
date: order.statusSetAt,
|
||||
active: true,
|
||||
icon: mdiCheckAll,
|
||||
}
|
||||
|
||||
const orderShipped = {
|
||||
state: "Versandt",
|
||||
date: order.statusSetAt,
|
||||
active: true,
|
||||
icon: mdiTruckDelivery,
|
||||
}
|
||||
const orderFailed = {
|
||||
state: "Fehlgeschlagen",
|
||||
note: "Bitte setz dich mit uns in Verbindung!",
|
||||
date: order.statusSetAt,
|
||||
active: true,
|
||||
icon: mdiMessageAlert,
|
||||
}
|
||||
|
||||
function generateStateHistoryBasedOnOrderStatus(): StateHistory[] {
|
||||
const stateHistory: StateHistory[] = []
|
||||
if (order.status == "pending" || order.status == "draft") {
|
||||
stateHistory.push(orderRecognized)
|
||||
orderInProcess.active = false
|
||||
orderInProcess.date = undefined
|
||||
stateHistory.push(orderInProcess)
|
||||
orderFinished.active = false
|
||||
orderFinished.date = undefined
|
||||
stateHistory.push(orderFinished)
|
||||
orderShipped.active = false
|
||||
orderShipped.date = undefined
|
||||
stateHistory.push(orderShipped)
|
||||
} else if (order.status == "inprocess") {
|
||||
orderRecognized.date = undefined
|
||||
stateHistory.push(orderRecognized)
|
||||
stateHistory.push(orderInProcess)
|
||||
orderFinished.active = false
|
||||
orderFinished.date = undefined
|
||||
stateHistory.push(orderFinished)
|
||||
orderShipped.active = false
|
||||
orderShipped.date = undefined
|
||||
stateHistory.push(orderShipped)
|
||||
} else if ((order.shipments || []).length > 0) {
|
||||
orderRecognized.date = undefined
|
||||
stateHistory.push(orderRecognized)
|
||||
orderInProcess.date = undefined
|
||||
stateHistory.push(orderInProcess)
|
||||
stateHistory.push(orderFinished)
|
||||
orderShipped.date = new Date(order.shipments[0]?.sentAt)
|
||||
stateHistory.push(orderShipped)
|
||||
} else if (order.status == "fulfilled") {
|
||||
orderRecognized.date = undefined
|
||||
stateHistory.push(orderRecognized)
|
||||
orderInProcess.date = undefined
|
||||
stateHistory.push(orderInProcess)
|
||||
stateHistory.push(orderFinished)
|
||||
orderShipped.date = undefined
|
||||
orderShipped.active = false
|
||||
stateHistory.push(orderShipped)
|
||||
} else if (order.status == "failed") {
|
||||
stateHistory.push(orderFailed)
|
||||
}
|
||||
|
||||
return stateHistory
|
||||
}
|
||||
const stateHistory = generateStateHistoryBasedOnOrderStatus()
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<CardWrapper>
|
||||
<div class="header">
|
||||
<h3>Lieferung</h3>
|
||||
{#each order.shipments || [] as shipment}
|
||||
<p>
|
||||
<a
|
||||
target="_blank"
|
||||
href="{shipment.trackingUrl}">{shipment.carrier} {shipment.trackingNumber}</a
|
||||
>
|
||||
</p>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="content">
|
||||
<StateLogPreview stateHistory="{stateHistory}" />
|
||||
</div>
|
||||
</CardWrapper>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
.wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
padding: 0px 2.4rem;
|
||||
h3 {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
p,
|
||||
p a {
|
||||
color: var(--text-invers-100);
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
font-family: Outfit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
padding: 0px 2.4rem;
|
||||
padding-bottom: 2.4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,181 +0,0 @@
|
||||
<script
|
||||
context="module"
|
||||
lang="ts"
|
||||
>
|
||||
export function formatGermanDateTime(date: string | Date) {
|
||||
if (!date) return ""
|
||||
date = new Date(date)
|
||||
const months = [
|
||||
"Januar",
|
||||
"Februar",
|
||||
"März",
|
||||
"April",
|
||||
"Mai",
|
||||
"Juni",
|
||||
"Juli",
|
||||
"August",
|
||||
"September",
|
||||
"Oktober",
|
||||
"November",
|
||||
"Dezember",
|
||||
]
|
||||
|
||||
const day = date.getDate()
|
||||
const month = months[date.getMonth()]
|
||||
const year = date.getFullYear()
|
||||
const hours = date.getHours().toString().padStart(2, "0")
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0")
|
||||
|
||||
return `am ${day}. ${month} ${year}, ${hours}:${minutes} Uhr`
|
||||
}
|
||||
export function formatGermanDate(date: string | Date) {
|
||||
if (!date) return ""
|
||||
date = new Date(date)
|
||||
const months = [
|
||||
"Januar",
|
||||
"Februar",
|
||||
"März",
|
||||
"April",
|
||||
"Mai",
|
||||
"Juni",
|
||||
"Juli",
|
||||
"August",
|
||||
"September",
|
||||
"Oktober",
|
||||
"November",
|
||||
"Dezember",
|
||||
]
|
||||
|
||||
const day = date.getDate()
|
||||
const month = months[date.getMonth()]
|
||||
const year = date.getFullYear()
|
||||
|
||||
return `zum ${day}. ${month} ${year}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Icon from "../../../../widgets/Icon.svelte"
|
||||
|
||||
export let stateHistory: StateHistory[]
|
||||
</script>
|
||||
|
||||
<ol>
|
||||
{#each stateHistory as state, i}
|
||||
<li class:active="{state.active}">
|
||||
<div class="left">
|
||||
<div class="symbol">
|
||||
<div class="icon">
|
||||
<Icon path="{state.icon}" />
|
||||
</div>
|
||||
<div class="black-box"></div>
|
||||
</div>
|
||||
{#if i !== stateHistory.length - 1}
|
||||
<div class="line"></div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="right">
|
||||
<p>{state.state}</p>
|
||||
<small>{formatGermanDateTime(state.date)}</small>
|
||||
{#if state.note}
|
||||
<small>{state.note}</small>
|
||||
{/if}
|
||||
{#if state.links}
|
||||
{#each state.links as link}
|
||||
<small
|
||||
><a
|
||||
href="{link.link}"
|
||||
download>{link.text}</a
|
||||
></small
|
||||
>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
|
||||
<style lang="less">
|
||||
ol {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
li {
|
||||
display: flex;
|
||||
gap: 1.8rem;
|
||||
|
||||
.left {
|
||||
height: 100%;
|
||||
padding-left: 1.5rem;
|
||||
width: 1.8rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.symbol {
|
||||
height: 2.7rem;
|
||||
width: 2.7rem;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
color: var(--bg-100);
|
||||
|
||||
.icon {
|
||||
color: var(--bg-100);
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
.black-box {
|
||||
position: absolute;
|
||||
height: 1.8rem;
|
||||
width: 1.8rem;
|
||||
transform: translate(-50%, 0);
|
||||
background-color: var(--neutral-white);
|
||||
border: 1px solid var(--bg-100);
|
||||
}
|
||||
}
|
||||
.line {
|
||||
width: 4px;
|
||||
height: 1.5rem;
|
||||
background-color: var(--bg-100);
|
||||
margin-left: -2px;
|
||||
margin-top: 6px;
|
||||
|
||||
height: 2.5rem;
|
||||
}
|
||||
}
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
padding-top: 1rem;
|
||||
p {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
small {
|
||||
color: var(--text-invers-150);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
.symbol {
|
||||
color: var(--neutral-white);
|
||||
.icon {
|
||||
color: var(--neutral-white);
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
.black-box {
|
||||
position: absolute;
|
||||
transform: rotate(45deg);
|
||||
transform-origin: top left;
|
||||
top: 0.15rem;
|
||||
left: 0px;
|
||||
background-color: var(--bg-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,101 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getOrderReturnRequests } from "../../../../../functions/CommerceAPIs/tibiEndpoints/orders"
|
||||
import Loader from "../../../Loader.svelte"
|
||||
import CardWrapper from "../../CardWrapper.svelte"
|
||||
import { createCart } from "../overview/OriginalCartPreview.svelte"
|
||||
import { getTibiRatings } from "../../../../../functions/CommerceAPIs/tibiEndpoints/rating"
|
||||
import ProductRating from "./ProductRating.svelte"
|
||||
export let order: V2OrderResponseBase
|
||||
let existingReturnRequests: OrderReturnRequest[]
|
||||
getOrderReturnRequests(order.id).then((res) => {
|
||||
existingReturnRequests = res
|
||||
})
|
||||
let ratings: ProductRating[]
|
||||
|
||||
getTibiRatings(order.id).then((res) => {
|
||||
ratings = res
|
||||
})
|
||||
</script>
|
||||
|
||||
<ul class="orderRatingsView">
|
||||
{#await createCart(order)}
|
||||
<Loader
|
||||
size="4"
|
||||
type="bar"
|
||||
/>
|
||||
{:then cart}
|
||||
{#if ratings}
|
||||
{#each Array.from(new Map(cart.lines.map((line) => [line.merchandise.product.id, line])).values()) as line}
|
||||
<CardWrapper>
|
||||
<div class="inner-wrapper">
|
||||
<div class="upper">
|
||||
<h3>Bewertung: {line.merchandise.title}</h3>
|
||||
</div>
|
||||
{#if ratings.find((r) => r.bigCommerceProductId === Number(line.merchandise.product.id))}
|
||||
<ProductRating
|
||||
orderId="{order.id}"
|
||||
item="{line}"
|
||||
productRating="{ratings.find(
|
||||
(r) => r.bigCommerceProductId === Number(line.merchandise.product.id)
|
||||
)}"
|
||||
/>
|
||||
{:else}
|
||||
<ProductRating
|
||||
orderId="{order.id}"
|
||||
item="{line}"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</CardWrapper>
|
||||
{/each}
|
||||
{/if}
|
||||
{/await}
|
||||
</ul>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
@import "../../../../../assets/css/variables.less";
|
||||
.orderRatingsView {
|
||||
gap: 2.4rem;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@media @mobile {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
li {
|
||||
width: 100% !important;
|
||||
.inner-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
padding: 0px 2.4rem;
|
||||
padding-bottom: 2.4rem;
|
||||
.upper {
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.lower {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
.cta {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,222 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createRating, updateRating } from "../../../../../functions/CommerceAPIs/tibiEndpoints/rating"
|
||||
import { newNotification } from "../../../../../store"
|
||||
import Input from "../../../blocks/form/Input.svelte"
|
||||
import { onChange } from "../../helper"
|
||||
|
||||
export let item: BKDFCartItem, orderId: number
|
||||
const product = item.merchandise.product
|
||||
const selectedColor = item.merchandise.selectedOptions?.find((o) => o.name === "Farbe")?.value
|
||||
const previewImages = product.images.filter((i) => JSON.parse(i.altText).Farbe === selectedColor)
|
||||
export let productRating: ProductRating = {
|
||||
status: "pending",
|
||||
title: "",
|
||||
comment: "",
|
||||
bigcommerceOrderId: orderId,
|
||||
bigCommerceProductId: Number(item.merchandise.product.id),
|
||||
rating: {
|
||||
quality: undefined,
|
||||
priceQualityRatio: undefined,
|
||||
comfort: undefined,
|
||||
overall: undefined,
|
||||
},
|
||||
review_date: new Date(),
|
||||
}
|
||||
const ratingOptions = [
|
||||
{ label: "Qualität", property: "quality" },
|
||||
{ label: "Preis/Leistungs", property: "priceQualityRatio" },
|
||||
{ label: "Tragekomfort", property: "comfort" },
|
||||
{ label: "Gesamt", property: "overall" },
|
||||
]
|
||||
|
||||
function validateRating() {
|
||||
let valid = true
|
||||
for (const [key, value] of Object.entries(productRating.rating)) {
|
||||
if (isNaN(value)) {
|
||||
const el = document.getElementById(`prop-${key}-${item.id}`)
|
||||
el.style.color = "var(--primary-100)"
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
if (!valid) {
|
||||
newNotification({
|
||||
html: "Bitte prüfe deine Eingabe.",
|
||||
class: "error",
|
||||
})
|
||||
}
|
||||
return valid
|
||||
}
|
||||
</script>
|
||||
|
||||
<form
|
||||
on:submit|preventDefault|stopPropagation="{() => {
|
||||
if (validateRating()) {
|
||||
if (productRating.id) {
|
||||
// update rating
|
||||
updateRating(productRating).then(() => {
|
||||
newNotification({
|
||||
html: 'Deine Bewertung wurde erfolgreich geändert.',
|
||||
class: 'success',
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// create rating
|
||||
createRating(productRating).then((res) => {
|
||||
productRating = res
|
||||
newNotification({
|
||||
html: 'Deine Bewertung wurde erfolgreich abgegeben.',
|
||||
class: 'success',
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}}"
|
||||
>
|
||||
<div class="product-images rectengularRepresentation">
|
||||
{#each previewImages as image, i}
|
||||
{#if i < 3}
|
||||
<li
|
||||
class="product-image-wrapper"
|
||||
style="z-index: {previewImages.length - i}"
|
||||
>
|
||||
<img
|
||||
src="{image?.url?.replace('2000w', '600w')}"
|
||||
alt="{image?.altText}"
|
||||
/>
|
||||
</li>
|
||||
{:else if i === 3}
|
||||
<li
|
||||
class="product-amount-wrapper"
|
||||
style="z-index: {previewImages?.length - i}"
|
||||
>
|
||||
<em>+ {previewImages?.length - i}</em>
|
||||
<small>Bilder</small>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="inputs">
|
||||
<Input
|
||||
id="title-{item.id}"
|
||||
placeholder="Titel"
|
||||
bind:value="{productRating.title}"
|
||||
onChange="{onChange}"
|
||||
/>
|
||||
<Input
|
||||
id="description-{item.id}"
|
||||
placeholder="Beschreibung"
|
||||
bind:value="{productRating.comment}"
|
||||
type="textarea"
|
||||
onChange="{onChange}"
|
||||
/>
|
||||
{#each ratingOptions as option}
|
||||
<div class="ratingOption">
|
||||
<div
|
||||
class="label"
|
||||
id="prop-{option.property}-{item.id}"
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
<div class="stars">
|
||||
{#each Array.from({ length: 5 }, (v, k) => k) as star}
|
||||
<label class="star-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="star-checkbox"
|
||||
value="{star}"
|
||||
on:change="{() => {
|
||||
//@ts-ignore
|
||||
productRating.rating[option.property] = star + 1
|
||||
const el = document.getElementById(`prop-${option.property}-${item.id}`)
|
||||
el.style.color = 'var(--text-invers-100)'
|
||||
}}"
|
||||
/>
|
||||
<svg
|
||||
class:filled="{productRating.rating[option.property] > star}"
|
||||
class="star"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18.75 8.125h-6.719L10 1.875l-2.031 6.25H1.25l5.469 3.75-2.11 6.25L10 14.219l5.39 3.906-2.109-6.25 5.469-3.75z"
|
||||
stroke="#464646"
|
||||
stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<button
|
||||
class="cta primary"
|
||||
disabled="{productRating.status !== 'pending'}"
|
||||
type="submit">Bewertung {productRating.id ? "verändern" : "absenden"}</button
|
||||
>
|
||||
{#if productRating.status !== "pending"}
|
||||
<small class="red">Deine Bewertung wurde bereits veröffentlicht.</small>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<style lang="less">
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
width: 100%;
|
||||
.action-row {
|
||||
display: flex;
|
||||
button {
|
||||
width: fit-content;
|
||||
}
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
width: 100%;
|
||||
.ratingOption {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
.label {
|
||||
font-weight: 700;
|
||||
font-family: Outfit-Bold;
|
||||
}
|
||||
.star-checkbox {
|
||||
display: none;
|
||||
}
|
||||
.star-container {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
.stars {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
|
||||
height: 1.2rem;
|
||||
align-items: center;
|
||||
label {
|
||||
height: 100%;
|
||||
.star {
|
||||
height: 100%;
|
||||
background-color: transparent;
|
||||
border: 0px solid black;
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
&.filled path {
|
||||
stroke: var(--primary-100);
|
||||
fill: var(--primary-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,208 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { checkIfOrderMayArrived, getLastDayForReturn } from "../../../../../../../../api/hooks/config-client"
|
||||
import {
|
||||
createOrderReturnRequest,
|
||||
getOrderReturnRequests,
|
||||
} from "../../../../../functions/CommerceAPIs/tibiEndpoints/orders"
|
||||
import { location, newNotification } from "../../../../../store"
|
||||
import Loader from "../../../Loader.svelte"
|
||||
import CardWrapper from "../../CardWrapper.svelte"
|
||||
import { createCart } from "../overview/OriginalCartPreview.svelte"
|
||||
import ReturnProductLine from "./ReturnProductLine.svelte"
|
||||
import { spaNavigate } from "../../../../../actions"
|
||||
import { convertObjectURLToOriginalFormat } from "../../helper"
|
||||
import LoadingWrapper from "../../../../widgets/LoadingWrapper.svelte"
|
||||
export let order: V2OrderResponseBase
|
||||
const dispatch = createEventDispatcher()
|
||||
let existingReturnRequests: OrderReturnRequest[]
|
||||
getOrderReturnRequests(order.id).then((res) => {
|
||||
existingReturnRequests = res
|
||||
})
|
||||
const returnRequest: OrderReturnRequest = {
|
||||
bigCommerceId: order.id,
|
||||
products: [],
|
||||
}
|
||||
order.productObjs.forEach((productObj) => {
|
||||
returnRequest.products.push({
|
||||
productId: productObj.id,
|
||||
quantity: 0,
|
||||
returnReason: undefined,
|
||||
image: undefined,
|
||||
})
|
||||
})
|
||||
let loading = false
|
||||
async function createReturnRequest() {
|
||||
const someHaveQuantity = returnRequest.products.some((product) => product.quantity > 0)
|
||||
if (!someHaveQuantity) {
|
||||
newNotification({
|
||||
class: "error",
|
||||
html: "Bitte wähle mindestens ein Produkt aus, um eine Rückgabe zu beauftragen.",
|
||||
})
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < returnRequest.products.length; i++) {
|
||||
const product = returnRequest.products[i]
|
||||
|
||||
if (product.quantity > 0 && !product.returnReason) {
|
||||
newNotification({
|
||||
class: "error",
|
||||
html: "Bitte wähle einen Grund für die Rückgabe aus.",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
loading = true
|
||||
await createOrderReturnRequest(returnRequest)
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
newNotification({
|
||||
class: "success",
|
||||
html: "Rückgabe wurde erfolgreich beantragt.",
|
||||
})
|
||||
spaNavigate($location.path.split("/").filter(Boolean).slice(0, -1).join("/"))
|
||||
window.location.reload()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
newNotification({
|
||||
class: "error",
|
||||
html: "Rückgabe konnte nicht beantragt werden. Bitte wenden Sie sich an den Support.",
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
loading = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="orderReturnView">
|
||||
<CardWrapper>
|
||||
<div class="inner-wrapper">
|
||||
<div class="upper">
|
||||
<h3>Rückgabe</h3>
|
||||
{#if (order.shipments || []).length > 0 && order.shipments[0].sentAt && checkIfOrderMayArrived(new Date(order.shipments[0].sentAt))}
|
||||
<p>
|
||||
Bis zum {new Date(getLastDayForReturn(order.shipments[0].sentAt)).toLocaleDateString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
})}
|
||||
</p>
|
||||
{/if}
|
||||
<p>
|
||||
Bitte beachte, dass wir die Kosten für das Versandlabel vom erstatteten Betrag abziehen. Solltest du
|
||||
mehrere Rücksendungen für eine Bestellung vornehmen, werden die zusätzlichen Kosten von deinem
|
||||
Erstattungsbetrag abgezogen.
|
||||
</p>
|
||||
<p>
|
||||
Bitte sende uns keine Artikel zurück, die über eine normale Prüfung hinaus getragen, benutzt oder
|
||||
beschädigt wurden. Solche Artikel können nicht vollständig erstattet werden. Jede Rücksendung wird
|
||||
von uns sorgfältig geprüft, und nur Ware, die sich in einwandfreiem Zustand befindet, wird
|
||||
vollständig erstattet. Für mehr Details haben wir eine <a href="/widerrufsbelehrung">
|
||||
Widerrufsbelehrung
|
||||
</a>.
|
||||
</p>
|
||||
<p>
|
||||
Bitte beachten Sie auch unsere Datenschutzbestimmungen, die Sie unter <a href="/datenschutz">
|
||||
Datenschutz
|
||||
</a> finden.
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
class="lower"
|
||||
on:submit|preventDefault|stopPropagation="{() => {
|
||||
createReturnRequest()
|
||||
}}"
|
||||
>
|
||||
{#await createCart(order)}
|
||||
<Loader
|
||||
size="4"
|
||||
type="bar"
|
||||
/>
|
||||
{:then cart}
|
||||
{#if !!existingReturnRequests}
|
||||
<ul>
|
||||
{#each cart.lines as line, i}
|
||||
<ReturnProductLine
|
||||
product="{line}"
|
||||
bind:returnRequestProduct="{returnRequest.products[i]}"
|
||||
orderReturnRequests="{existingReturnRequests}"
|
||||
orderReturnRequest="{returnRequest}"
|
||||
/>
|
||||
{/each}
|
||||
</ul>
|
||||
<LoadingWrapper active="{loading}">
|
||||
<button class="cta primary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.9991 2.75C11.4023 2.75 10.83 2.98705 10.4081 3.40901C9.98611 3.83097 9.74906 4.40326 9.74906 5V5.26C10.3061 5.25 10.9171 5.25 11.5891 5.25H12.4101C13.0801 5.25 13.6921 5.25 14.2501 5.26V5C14.2501 4.70444 14.1918 4.41178 14.0787 4.13873C13.9656 3.86568 13.7997 3.6176 13.5907 3.40866C13.3817 3.19971 13.1335 3.034 12.8604 2.92098C12.5873 2.80797 12.2946 2.74987 11.9991 2.75ZM15.7491 5.328V5C15.7491 4.00544 15.354 3.05161 14.6507 2.34835C13.9474 1.64509 12.9936 1.25 11.9991 1.25C11.0045 1.25 10.0507 1.64509 9.34741 2.34835C8.64415 3.05161 8.24906 4.00544 8.24906 5V5.328C8.10606 5.34 7.96906 5.354 7.83506 5.371C6.82506 5.496 5.99306 5.758 5.28506 6.345C4.57806 6.932 4.16706 7.702 3.85806 8.672C3.55806 9.612 3.33206 10.819 3.04806 12.338L3.02706 12.448C2.62506 14.591 2.30906 16.28 2.25006 17.611C2.19006 18.976 2.39406 20.106 3.16406 21.033C3.93406 21.961 5.00706 22.369 6.35906 22.562C7.67906 22.75 9.39606 22.75 11.5771 22.75H12.4221C14.6021 22.75 16.3201 22.75 17.6391 22.562C18.9911 22.369 20.0651 21.961 20.8351 21.033C21.6051 20.106 21.8071 18.976 21.7481 17.611C21.6901 16.28 21.3731 14.591 20.9711 12.448L20.9511 12.338C20.6661 10.819 20.4391 9.611 20.1411 8.672C19.8311 7.702 19.4211 6.932 18.7131 6.345C18.0061 5.758 17.1731 5.495 16.1631 5.371C16.0253 5.35411 15.8873 5.33977 15.7491 5.328ZM8.01906 6.86C7.16406 6.965 6.64706 7.164 6.24306 7.5C5.84006 7.834 5.54906 8.305 5.28706 9.127C5.02006 9.967 4.80906 11.085 4.51306 12.664C4.09706 14.881 3.80206 16.464 3.74906 17.677C3.69706 18.867 3.88906 19.557 4.31806 20.076C4.74806 20.593 5.39106 20.908 6.57106 21.076C7.77106 21.248 9.38306 21.25 11.6391 21.25H12.3591C14.6161 21.25 16.2261 21.248 17.4271 21.077C18.6071 20.908 19.2501 20.593 19.6801 20.076C20.1101 19.558 20.3011 18.868 20.2501 17.676C20.1961 16.465 19.9011 14.881 19.4851 12.664C19.1891 11.084 18.9791 9.968 18.7111 9.127C18.4491 8.305 18.1591 7.834 17.7551 7.499C17.3511 7.164 16.8351 6.965 15.9791 6.859C15.1031 6.751 13.9661 6.75 12.3591 6.75H11.6391C10.0321 6.75 8.89506 6.751 8.01906 6.86ZM9.46906 11.47C9.60968 11.3295 9.80031 11.2507 9.99906 11.2507C10.1978 11.2507 10.3884 11.3295 10.5291 11.47L11.9991 12.94L13.4691 11.47C13.5377 11.3963 13.6205 11.3372 13.7125 11.2962C13.8045 11.2552 13.9038 11.2332 14.0045 11.2314C14.1052 11.2296 14.2053 11.2482 14.2987 11.2859C14.392 11.3236 14.4769 11.3797 14.5481 11.451C14.6193 11.5222 14.6755 11.607 14.7132 11.7004C14.7509 11.7938 14.7694 11.8938 14.7676 11.9945C14.7659 12.0952 14.7438 12.1945 14.7028 12.2865C14.6618 12.3785 14.6027 12.4613 14.5291 12.53L13.0591 14L14.5291 15.47C14.6027 15.5387 14.6618 15.6215 14.7028 15.7135C14.7438 15.8055 14.7659 15.9048 14.7676 16.0055C14.7694 16.1062 14.7509 16.2062 14.7132 16.2996C14.6755 16.393 14.6193 16.4778 14.5481 16.549C14.4769 16.6203 14.392 16.6764 14.2987 16.7141C14.2053 16.7518 14.1052 16.7704 14.0045 16.7686C13.9038 16.7668 13.8045 16.7448 13.7125 16.7038C13.6205 16.6628 13.5377 16.6037 13.4691 16.53L11.9991 15.06L10.5291 16.53C10.4604 16.6037 10.3776 16.6628 10.2856 16.7038C10.1936 16.7448 10.0943 16.7668 9.99358 16.7686C9.89288 16.7704 9.79285 16.7518 9.69946 16.7141C9.60607 16.6764 9.52124 16.6203 9.45002 16.549C9.3788 16.4778 9.32266 16.393 9.28493 16.2996C9.24721 16.2062 9.22869 16.1062 9.23046 16.0055C9.23224 15.9048 9.25428 15.8055 9.29528 15.7135C9.33627 15.6215 9.39537 15.5387 9.46906 15.47L10.9391 14L9.46906 12.53C9.32861 12.3894 9.24972 12.1988 9.24972 12C9.24972 11.8012 9.32861 11.6106 9.46906 11.47Z"
|
||||
fill="#F3EED9"></path>
|
||||
</svg>
|
||||
Rückgabe Beauftragen
|
||||
</button>
|
||||
</LoadingWrapper>
|
||||
{/if}
|
||||
{/await}
|
||||
</form>
|
||||
</div>
|
||||
</CardWrapper>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
.orderReturnView:global {
|
||||
display: flex;
|
||||
|
||||
gap: 2.4rem;
|
||||
width: 100%;
|
||||
& > li {
|
||||
width: 100%;
|
||||
}
|
||||
.loadingWrapper {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
.inner-wrapper {
|
||||
padding: 0px 2.4rem;
|
||||
padding-bottom: 2.4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
gap: 2.4rem;
|
||||
.upper {
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.lower {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
width: 100%;
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
width: 100%;
|
||||
}
|
||||
* {
|
||||
}
|
||||
.cta {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,330 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiArchiveArrowUpOutline, mdiArchiveCheckOutline, mdiCashCheck, mdiCashClock } from "@mdi/js"
|
||||
import {
|
||||
deleteOrderReturnRequest,
|
||||
getOrderReturnRequests,
|
||||
} from "../../../../../functions/CommerceAPIs/tibiEndpoints/orders"
|
||||
|
||||
import Loader from "../../../Loader.svelte"
|
||||
import ProductInCartPreview from "../../../product/ProductInCartPreview.svelte"
|
||||
import CardWrapper from "../../CardWrapper.svelte"
|
||||
import { createCart } from "../overview/OriginalCartPreview.svelte"
|
||||
import { getLabelByValueForReturnReason } from "./ReturnProductLine.svelte"
|
||||
import StateLogPreview, { formatGermanDate } from "../overview/StateLogPreview.svelte"
|
||||
import { getShownIdFraction } from "../helper"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { apiBaseOverride, newNotification } from "../../../../../store"
|
||||
import { apiBaseURL } from "../../../../../../config"
|
||||
import ImagesRow from "../../../../widgets/inputs/imageUpload/ImagesRow.svelte"
|
||||
export let order: V2OrderResponseBase, returnRequest: OrderReturnRequest
|
||||
const dispatch = createEventDispatcher()
|
||||
let existingReturnRequests: OrderReturnRequest[]
|
||||
getOrderReturnRequests(order.id).then((res) => {
|
||||
existingReturnRequests = res
|
||||
})
|
||||
const stateRequested = {
|
||||
state: "Rückgabe angefragt",
|
||||
date: new Date(returnRequest.insertTime),
|
||||
active: true,
|
||||
icon: mdiArchiveArrowUpOutline,
|
||||
}
|
||||
const stateAccepted = {
|
||||
state: "Rückgabe akzeptiert",
|
||||
links: [],
|
||||
date: new Date(returnRequest.updateTime),
|
||||
active: true,
|
||||
icon: mdiArchiveCheckOutline,
|
||||
}
|
||||
const stateAwaited = {
|
||||
state: "Rückgabe erwartet",
|
||||
active: true,
|
||||
icon: mdiCashClock,
|
||||
note:
|
||||
returnRequest.status == "approved"
|
||||
? "Bitte sende uns die Ware bis spätestens " +
|
||||
formatGermanDate(new Date(new Date(returnRequest.updateTime).getTime() + 14 * 24 * 60 * 60 * 1000)) +
|
||||
" zu. Bewahre den Versandbeleg auf."
|
||||
: "",
|
||||
}
|
||||
const statePaid = {
|
||||
state: "Rückerstattung veranlasst.",
|
||||
note:
|
||||
returnRequest.status == "refunded"
|
||||
? "Die Rückerstattung wurde auf deine ursprüngliche Zahlungsmethode zurückgebucht."
|
||||
: "",
|
||||
date: new Date(returnRequest.updateTime),
|
||||
active: true,
|
||||
icon: mdiCashCheck,
|
||||
}
|
||||
const stateFailed = {
|
||||
state: "Rückgabe fehlgeschlagen",
|
||||
date: new Date(returnRequest.updateTime),
|
||||
note: "Bitte setz dich mit uns in Verbindung.",
|
||||
active: true,
|
||||
icon: mdiArchiveCheckOutline,
|
||||
}
|
||||
const stateRejected = {
|
||||
state: "Rückzahlung Abgelehnt",
|
||||
note: "Bitte setz dich mit uns in Verbindung.",
|
||||
date: new Date(returnRequest.updateTime),
|
||||
active: true,
|
||||
icon: mdiArchiveCheckOutline,
|
||||
}
|
||||
const stateHistory: StateHistory[] = []
|
||||
;(returnRequest?.returnShppingLabels || []).forEach((label, i) => {
|
||||
let text = ""
|
||||
if (returnRequest.returnShppingLabels.length > 1) {
|
||||
text = `Versandlabel Nr. ${i + 1} herunterladen`
|
||||
} else {
|
||||
text = `Versandlabel herunterladen`
|
||||
}
|
||||
stateAccepted.links.push({
|
||||
link: `${$apiBaseOverride ? $apiBaseOverride : apiBaseURL}orderReturnRequest/${returnRequest.id}/${
|
||||
label.label.src
|
||||
}`,
|
||||
text: text,
|
||||
})
|
||||
})
|
||||
if (returnRequest.status == "pending" || !returnRequest.status) {
|
||||
stateHistory.push(stateRequested)
|
||||
stateAccepted.date = undefined
|
||||
stateAccepted.active = false
|
||||
stateHistory.push(stateAccepted)
|
||||
stateAwaited.date = undefined
|
||||
stateAwaited.active = false
|
||||
stateHistory.push(stateAwaited)
|
||||
statePaid.date = undefined
|
||||
statePaid.active = false
|
||||
stateHistory.push(statePaid)
|
||||
} else if (returnRequest.status == "approved") {
|
||||
stateRequested.date = undefined
|
||||
stateHistory.push(stateRequested)
|
||||
stateAccepted.date = undefined
|
||||
|
||||
stateHistory.push(stateAccepted)
|
||||
|
||||
stateHistory.push(stateAwaited)
|
||||
statePaid.date = undefined
|
||||
statePaid.active = false
|
||||
stateHistory.push(statePaid)
|
||||
} else if (returnRequest.status == "refunded") {
|
||||
stateRequested.date = undefined
|
||||
stateHistory.push(stateRequested)
|
||||
stateAccepted.date = undefined
|
||||
stateHistory.push(stateAccepted)
|
||||
stateAwaited.date = undefined
|
||||
stateHistory.push(stateAwaited)
|
||||
stateHistory.push(statePaid)
|
||||
} else if (returnRequest.status == "failed") {
|
||||
stateHistory.push(stateFailed)
|
||||
} else if (returnRequest.status == "rejected") {
|
||||
stateRequested.date = undefined
|
||||
stateHistory.push(stateRequested)
|
||||
stateHistory.push(stateRejected)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul class="orderReturnPreview">
|
||||
<CardWrapper>
|
||||
<div class="inner-wrapper">
|
||||
<div class="upper">
|
||||
<h3>Das geht zurück</h3>
|
||||
</div>
|
||||
<div class="lower">
|
||||
{#await createCart(order)}
|
||||
<Loader
|
||||
size="4"
|
||||
type="bar"
|
||||
/>
|
||||
{:then cart}
|
||||
{#if !!existingReturnRequests}
|
||||
<ul>
|
||||
{#each cart.lines.filter((line) => returnRequest.products.findIndex((product) => {
|
||||
return product.productId == Number(line.id) && product.quantity > 0
|
||||
}) >= 0) as line}
|
||||
<li>
|
||||
<div class="col">
|
||||
<p style="font-size: 0.7rem;">
|
||||
{returnRequest.products.find((p) => p.productId == Number(line.id))
|
||||
?.quantity} Stk.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col grow">
|
||||
<ProductInCartPreview
|
||||
item="{line}"
|
||||
hideActions="{true}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col"
|
||||
style="min-width: 300px;"
|
||||
>
|
||||
<span>Grund der Rückgabe</span>
|
||||
<p style="font-size: 0.7rem; ">
|
||||
{getLabelByValueForReturnReason(
|
||||
returnRequest.products.find((p) => p.productId == Number(line.id))
|
||||
?.returnReason
|
||||
) || "-"}
|
||||
</p>
|
||||
{#if returnRequest.products.find((p) => p.productId == Number(line.id))?.attachedImages?.length}
|
||||
<ImagesRow
|
||||
disabled="{true}"
|
||||
mediaEntries="{returnRequest.products.find(
|
||||
(p) => p.productId == Number(line.id)
|
||||
)?.attachedImages}"
|
||||
/>{/if}
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
</CardWrapper>
|
||||
<CardWrapper>
|
||||
<div class="inner-wrapper">
|
||||
<div class="state-header">
|
||||
<h3>Rückgabestatus</h3>
|
||||
<p>Rückgabe-ID: #{getShownIdFraction(returnRequest.id)}</p>
|
||||
</div>
|
||||
<div class="state-content">
|
||||
<StateLogPreview stateHistory="{stateHistory}" />
|
||||
</div>
|
||||
{#if !returnRequest.status || returnRequest.status == "pending"}
|
||||
<div class="state-buttons">
|
||||
<button
|
||||
class="cta primary"
|
||||
on:click="{(e) => {
|
||||
// disable button
|
||||
e.target.disabled = true
|
||||
deleteOrderReturnRequest(returnRequest.id)
|
||||
.then(() => {
|
||||
dispatch('updateOrder')
|
||||
newNotification({
|
||||
html: 'Die Rückgabe wurde erfolgreich abgebrochen.',
|
||||
class: 'success',
|
||||
})
|
||||
location.reload()
|
||||
})
|
||||
.catch(() => {
|
||||
e.target.disabled = false
|
||||
newNotification({
|
||||
html: 'Die Rückgabe konnte nicht abgebrochen werden. Bitte lade die Seite neu und versuche es erneut oder kontaktiere uns.',
|
||||
class: 'error',
|
||||
})
|
||||
})
|
||||
}}">RÜCKGABE ABBRECHEN</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</CardWrapper>
|
||||
</ul>
|
||||
|
||||
<style lang="less">
|
||||
.orderReturnPreview:global {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 2fr;
|
||||
gap: 2.4rem;
|
||||
width: 100%;
|
||||
& > li {
|
||||
width: 100% !important;
|
||||
}
|
||||
.productCartPreview {
|
||||
border-bottom: 0px solid black !important;
|
||||
height: 6rem;
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
@media (max-width: 1250px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
& > * {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.inner-wrapper {
|
||||
padding: 0px 2.4rem;
|
||||
padding-bottom: 2.4rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 2.4rem;
|
||||
.state-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.upper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.lower {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
li {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 1.2rem;
|
||||
padding-bottom: 1.2rem;
|
||||
border-bottom: 1px solid var(--bg-300);
|
||||
overflow-x: auto;
|
||||
@media (max-width: 750px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
.col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: fit-content;
|
||||
justify-content: center;
|
||||
gap: 0.6rem;
|
||||
position: relative;
|
||||
&.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
span {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
line-height: 1rem;
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
top: 0rem;
|
||||
}
|
||||
p {
|
||||
margin-top: 1rem;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
.imgWrapper {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
height: 4rem;
|
||||
img {
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.cta {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,218 +0,0 @@
|
||||
<script
|
||||
lang="ts"
|
||||
context="module"
|
||||
>
|
||||
export const returnReasons = [
|
||||
{
|
||||
label: "Falsche Größe",
|
||||
value: "wrongSize",
|
||||
},
|
||||
{
|
||||
label: "Falsche Farbe",
|
||||
value: "wrongColor",
|
||||
},
|
||||
{
|
||||
label: "Falscher Artikel erhalten",
|
||||
value: "wrongProduct",
|
||||
},
|
||||
{
|
||||
label: "Beschädigter oder defekter Artikel",
|
||||
value: "damaged",
|
||||
},
|
||||
{
|
||||
label: "Zu groß",
|
||||
value: "tooLarge",
|
||||
},
|
||||
{
|
||||
label: "Zu klein",
|
||||
value: "tooSmall",
|
||||
},
|
||||
{
|
||||
label: "Artikel entspricht nicht der Beschreibung",
|
||||
value: "notAsDescribed",
|
||||
},
|
||||
{
|
||||
label: "Nicht zufrieden mit der Qualität",
|
||||
value: "poorQuality",
|
||||
},
|
||||
{
|
||||
label: "Artikel sieht anders aus als auf dem Bild",
|
||||
value: "looksDifferent",
|
||||
},
|
||||
{
|
||||
label: "Artikel nicht mehr benötigt",
|
||||
value: "noLongerNeeded",
|
||||
},
|
||||
{
|
||||
label: "Preis-Leistungs-Verhältnis ungenügend",
|
||||
value: "poorValue",
|
||||
},
|
||||
{
|
||||
label: "Anderer Grund",
|
||||
value: "other",
|
||||
},
|
||||
]
|
||||
|
||||
export function getLabelByValueForReturnReason(value: string) {
|
||||
return returnReasons.find((reason) => reason.value === value)?.label
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import ImagesRow from "../../../../widgets/inputs/imageUpload/ImagesRow.svelte"
|
||||
import Select from "../../../blocks/form/Select.svelte"
|
||||
import ProductInCartPreview from "../../../product/ProductInCartPreview.svelte"
|
||||
import ProductQuantity from "../../../product/widgets/ProductQuantity.svelte"
|
||||
|
||||
export let product: BKDFCartItem,
|
||||
returnRequestProduct: OrderReturnRequestProduct,
|
||||
orderReturnRequests: OrderReturnRequest[],
|
||||
orderReturnRequest: OrderReturnRequest
|
||||
|
||||
let orderedQuantity = product.quantity
|
||||
let alreadyReturnedQuantity = 0
|
||||
orderReturnRequests.forEach((returnRequest) => {
|
||||
returnRequest.products.forEach((returnRequestProduct) => {
|
||||
if (returnRequestProduct.productId == product.id) {
|
||||
orderedQuantity -= returnRequestProduct.quantity
|
||||
alreadyReturnedQuantity += returnRequestProduct.quantity
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// test
|
||||
</script>
|
||||
|
||||
<li
|
||||
class:disabled="{orderedQuantity <= 0}"
|
||||
class="returnProductLine"
|
||||
>
|
||||
<div class="selectInput desktop">
|
||||
<ProductQuantity
|
||||
bind:quantity="{returnRequestProduct.quantity}"
|
||||
lowerBound="{0}"
|
||||
on:updateQuantity="{() => {
|
||||
if (returnRequestProduct.quantity > orderedQuantity) {
|
||||
returnRequestProduct.quantity = orderedQuantity
|
||||
}
|
||||
}}"
|
||||
/>
|
||||
</div>
|
||||
<ProductInCartPreview
|
||||
item="{product}"
|
||||
hideActions="{true}"
|
||||
>
|
||||
<span class="amount">Menge: {orderedQuantity}</span>
|
||||
{#if alreadyReturnedQuantity > 0}
|
||||
<span class="alreadyReturned">Bereits storniert: {alreadyReturnedQuantity} Stk.</span>
|
||||
{/if}
|
||||
</ProductInCartPreview>
|
||||
<div class="selectInput mobile">
|
||||
<div class="label">Zurückgeben Menge:</div>
|
||||
<ProductQuantity
|
||||
bind:quantity="{returnRequestProduct.quantity}"
|
||||
lowerBound="{0}"
|
||||
on:updateQuantity="{() => {
|
||||
if (returnRequestProduct.quantity > orderedQuantity) {
|
||||
returnRequestProduct.quantity = orderedQuantity
|
||||
}
|
||||
}}"
|
||||
/>
|
||||
</div>
|
||||
<div class="inputs">
|
||||
<div class="selectWrapper">
|
||||
<Select
|
||||
options="{returnReasons}"
|
||||
clearable="{false}"
|
||||
placeholder="Grund der Rückgabe"
|
||||
bind:value="{returnRequestProduct.returnReason}"
|
||||
/>
|
||||
</div>
|
||||
<div class="imageWrapper">
|
||||
<ImagesRow bind:mediaEntries="{returnRequestProduct.attachedImages}" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../../../assets/css/variables.less";
|
||||
.returnProductLine:global {
|
||||
padding-bottom: 1.2rem;
|
||||
border-bottom: 1px solid var(--bg-300);
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.alreadyReturned {
|
||||
color: var(--primary-100);
|
||||
font-size: 0.7rem;
|
||||
line-height: 0.7rem;
|
||||
}
|
||||
.amount {
|
||||
color: var(--text-invers-100);
|
||||
font-size: 0.7rem;
|
||||
line-height: 0.7rem;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.selectInput {
|
||||
min-width: 100px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
&.mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
align-self: center;
|
||||
.label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.productCartPreview {
|
||||
border-bottom: 0px solid black !important;
|
||||
height: 6rem;
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
.inputs {
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
height: 100%;
|
||||
align-items: flex-end;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
& > * {
|
||||
flex-grow: 1;
|
||||
width: 0px;
|
||||
}
|
||||
}
|
||||
@media @mobile {
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
align-items: flex-start;
|
||||
.inputs {
|
||||
gap: 2.4rem;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
& > * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.selectInput {
|
||||
&.desktop {
|
||||
display: none;
|
||||
}
|
||||
&.mobile {
|
||||
display: flex;
|
||||
}
|
||||
justify-content: flex-start;
|
||||
gap: 1.2rem;
|
||||
width: 100%;
|
||||
.label {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,61 +0,0 @@
|
||||
<script lang="ts">
|
||||
import CardWrapper from "../../CardWrapper.svelte"
|
||||
|
||||
export let revokeRequests: OrderRevokeRequest[] = []
|
||||
</script>
|
||||
|
||||
<div class="order_cancelled-wrapper">
|
||||
<CardWrapper red="{true}">
|
||||
<div class="order_cancelled">
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M24 2.5C12.126 2.5 2.5 12.126 2.5 24C2.5 35.874 12.126 45.5 24 45.5C35.874 45.5 45.5 35.874 45.5 24C45.5 12.126 35.874 2.5 24 2.5ZM5.5 24C5.5 19.0935 7.4491 14.3879 10.9185 10.9185C14.3879 7.4491 19.0935 5.5 24 5.5C28.568 5.5 32.752 7.156 35.98 9.9C35.9664 9.91307 35.9531 9.92641 35.94 9.94L9.94 35.94C9.92646 35.9531 9.91313 35.9665 9.9 35.98C7.05305 32.6383 5.49272 28.39 5.5 24ZM12.02 38.1C15.5605 41.1171 20.1077 42.6897 24.7557 42.5045C29.4037 42.3193 33.8114 40.3899 37.1006 37.1006C40.3899 33.8114 42.3193 29.4037 42.5045 24.7557C42.6897 20.1077 41.1171 15.5605 38.1 12.02C38.0876 12.0336 38.0749 12.0469 38.062 12.06L12.062 38.06C12.0489 38.0722 12.0356 38.0842 12.022 38.096"
|
||||
fill="white"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Diese Bestellung wurde abgebrochen.</h3>
|
||||
{#if revokeRequests[0].status == "pending"}
|
||||
<p>Die Rückerstattung wird bearbeitet.</p>
|
||||
{:else}
|
||||
<p>Die Rückerstattung wurde veranlasst.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</CardWrapper>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
.order_cancelled-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
.order_cancelled {
|
||||
width: 100%;
|
||||
padding: 1.2rem;
|
||||
|
||||
padding-top: 0px;
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
h3 {
|
||||
color: var(--neutral-white);
|
||||
}
|
||||
p {
|
||||
color: var(--neutral-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,55 +0,0 @@
|
||||
<script lang="ts">
|
||||
import CardWrapper from "../../CardWrapper.svelte"
|
||||
</script>
|
||||
|
||||
<div class="order_cancelled-wrapper">
|
||||
<CardWrapper red="{true}">
|
||||
<div class="order_cancelled">
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M24 2.5C12.126 2.5 2.5 12.126 2.5 24C2.5 35.874 12.126 45.5 24 45.5C35.874 45.5 45.5 35.874 45.5 24C45.5 12.126 35.874 2.5 24 2.5ZM5.5 24C5.5 19.0935 7.4491 14.3879 10.9185 10.9185C14.3879 7.4491 19.0935 5.5 24 5.5C28.568 5.5 32.752 7.156 35.98 9.9C35.9664 9.91307 35.9531 9.92641 35.94 9.94L9.94 35.94C9.92646 35.9531 9.91313 35.9665 9.9 35.98C7.05305 32.6383 5.49272 28.39 5.5 24ZM12.02 38.1C15.5605 41.1171 20.1077 42.6897 24.7557 42.5045C29.4037 42.3193 33.8114 40.3899 37.1006 37.1006C40.3899 33.8114 42.3193 29.4037 42.5045 24.7557C42.6897 20.1077 41.1171 15.5605 38.1 12.02C38.0876 12.0336 38.0749 12.0469 38.062 12.06L12.062 38.06C12.0489 38.0722 12.0356 38.0842 12.022 38.096"
|
||||
fill="white"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Es gibt ein Problem mit der Bestellung</h3>
|
||||
<p>Bitte setz dich mit uns in verbindung</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardWrapper>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
.order_cancelled-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
.order_cancelled {
|
||||
width: 100%;
|
||||
padding: 1.2rem;
|
||||
|
||||
padding-top: 0px;
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
h3 {
|
||||
color: var(--neutral-white);
|
||||
}
|
||||
p {
|
||||
color: var(--neutral-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,119 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getBCGraphProductsByIds } from "../../../../../functions/CommerceAPIs/bigCommerce/product"
|
||||
|
||||
export let productVariants: V2OrderProductsResponseBase[],
|
||||
circularRepresentation: boolean = true
|
||||
let images: any = []
|
||||
getBCGraphProductsByIds(productVariants.map((p) => String(p.product_id))).then((products) => {
|
||||
productVariants.forEach((e, i) => {
|
||||
const selectedColor = e.product_options.find((o) => o.display_name === "Farbe")?.display_value
|
||||
const previewImage = products.find((p) =>
|
||||
p.images.find((i) => JSON.parse(i.altText).Farbe === selectedColor)
|
||||
)
|
||||
images.push(previewImage)
|
||||
})
|
||||
images = images
|
||||
})
|
||||
</script>
|
||||
|
||||
<ul
|
||||
class="product-images {circularRepresentation
|
||||
? 'circularRepresentation'
|
||||
: 'rectengularRepresentation'} amount-of-images-{images.length}"
|
||||
>
|
||||
{#each images as image, i}
|
||||
{#if i < 3}
|
||||
<li
|
||||
class="product-image-wrapper"
|
||||
style="z-index: {images.length - i}"
|
||||
>
|
||||
<img
|
||||
src="{image?.featuredImage?.url?.replace('2000w', '120w')}"
|
||||
alt="{image?.featuredImage?.altText}"
|
||||
/>
|
||||
</li>
|
||||
{:else if i === 3}
|
||||
<li
|
||||
class="product-amount-wrapper"
|
||||
style="z-index: {images?.length - i}"
|
||||
>
|
||||
<em>+ {images?.length - i}</em>
|
||||
<small>Items</small>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.product-images.circularRepresentation {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0px;
|
||||
li {
|
||||
min-width: 3.6rem;
|
||||
min-height: 3.6rem;
|
||||
width: 3.6rem;
|
||||
height: 3.6rem;
|
||||
border: 1px solid var(--neutral-white);
|
||||
border-radius: 100px;
|
||||
background-color: var(--bg-300);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-invers-100);
|
||||
margin-right: -12px;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 100px;
|
||||
}
|
||||
em {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
small {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
.product-images.rectengularRepresentation {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 2px;
|
||||
&.amount-of-images-1 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
li {
|
||||
width: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
background-color: var(--bg-300);
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-invers-100);
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
em {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
small {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,68 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiCheckAll, mdiPrinter3d, mdiTimerSandComplete } from "@mdi/js"
|
||||
import { orderStatuses } from "../helper"
|
||||
import Icon from "../../../../widgets/Icon.svelte"
|
||||
|
||||
export let order: V2OrderResponseBase
|
||||
let color: string, name: string, icon: any
|
||||
if (order.status == "pending" || order.status == "draft" || order.status_id == 14 || order.status_id == 4) {
|
||||
color = orderStatuses[order.status_id].color
|
||||
name = orderStatuses[order.status_id]?.name
|
||||
icon = orderStatuses[order.status_id]?.icon
|
||||
} else if (order.status == "inprocess") {
|
||||
color = "#F2C94C"
|
||||
name = "in Arbeit"
|
||||
icon = mdiPrinter3d
|
||||
} else if ((order.shipments || []).length > 0) {
|
||||
color = "#27AE60"
|
||||
name = "Versandt"
|
||||
icon = mdiCheckAll
|
||||
} else if (order.status == "fulfilled") {
|
||||
color = "#F2994A"
|
||||
name = "fertiggestellt"
|
||||
icon = mdiTimerSandComplete
|
||||
} else {
|
||||
color = orderStatuses[order.status_id].color
|
||||
name = orderStatuses[order.status_id]?.name
|
||||
icon = orderStatuses[order.status_id]?.icon
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="status">
|
||||
<div
|
||||
class="wrapper"
|
||||
style="border-color: {color}"
|
||||
>
|
||||
<p>{name}</p>
|
||||
<span style="background-color: {color}; "
|
||||
><Icon
|
||||
path="{icon}"
|
||||
size="1.2rem"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
.status {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
.wrapper {
|
||||
display: flex;
|
||||
border: 1px solid black;
|
||||
border-radius: 2px;
|
||||
align-items: center;
|
||||
p {
|
||||
color: var(--text-invers-100);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 400;
|
||||
padding: 0px 12px;
|
||||
}
|
||||
span {
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,263 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { xhrApiCall, deleteDBEntry } from "../../../../../api"
|
||||
import { spaLink } from "../../../../actions"
|
||||
import { updateInternalCustomer } from "../../../../functions/CommerceAPIs/tibiEndpoints/customer"
|
||||
import { login, newNotification } from "../../../../store"
|
||||
import Modal from "../../../Modal.svelte"
|
||||
import LoadingWrapper from "../../../widgets/LoadingWrapper.svelte"
|
||||
import Input from "../../blocks/form/Input.svelte"
|
||||
import {
|
||||
convertObjectURLToOriginalFormat,
|
||||
onChange,
|
||||
validateField,
|
||||
validateInput,
|
||||
validateNumberInput,
|
||||
} from "../helper"
|
||||
import RecordEditMask from "./RecordEditMask.svelte"
|
||||
|
||||
$: records = $login?.customer?.personalRecords || []
|
||||
if (!records) records = []
|
||||
function initRecord(): PersonalRecord {
|
||||
return {
|
||||
title: "",
|
||||
description: "",
|
||||
priority: 1,
|
||||
ageAtRecording: undefined,
|
||||
recording: null,
|
||||
thumbnail: null,
|
||||
}
|
||||
}
|
||||
let loading = -1
|
||||
let reload = false
|
||||
let approvedUsageTerms = false
|
||||
let showUsageTermsModal = false
|
||||
let createOrUpdateParams: any[] = []
|
||||
async function createOrUpdateRecord(record: PersonalRecord, update = false, id: number, fileChange = true) {
|
||||
if (!approvedUsageTerms && !update) {
|
||||
showUsageTermsModal = true
|
||||
createOrUpdateParams = [record, update, id, fileChange]
|
||||
return
|
||||
}
|
||||
loading = id
|
||||
const validations = [
|
||||
{ id: "title" + id, value: record.title, validator: validateInput },
|
||||
{ id: "ageAtRecording" + id, value: record.ageAtRecording, validator: validateNumberInput },
|
||||
]
|
||||
|
||||
let isValid = true
|
||||
|
||||
for (const { id, value, validator } of validations) {
|
||||
// @ts-ignore
|
||||
if (!validateField(id, value, validator)) isValid = false
|
||||
}
|
||||
if (!record.recording) {
|
||||
isValid = false
|
||||
newNotification({ html: "Bitte wähle ein Video aus.", class: "error" })
|
||||
return
|
||||
}
|
||||
if (record.ageAtRecording > 120 && record.ageAtRecording < 0) {
|
||||
isValid = false
|
||||
newNotification({ html: "Bitte gib ein Alter zwischen 0 und 120 an.", class: "error" })
|
||||
return
|
||||
}
|
||||
if (!record.priority) {
|
||||
record.priority = 1
|
||||
}
|
||||
if (isValid) {
|
||||
record.ageAtRecording = parseInt(String(record.ageAtRecording))
|
||||
|
||||
if (fileChange)
|
||||
newNotification({
|
||||
html: "Der Videoupload kann einige Zeit in Anspruch nehmen. Bitte Schließe nicht das Fenster und warte, bis die Ladeanimation beendet ist.",
|
||||
class: "warning",
|
||||
})
|
||||
if (typeof record.recording !== "string") {
|
||||
record.recording = await convertObjectURLToOriginalFormat(record.recording)
|
||||
const videoRes = await xhrApiCall(
|
||||
"medialib",
|
||||
{
|
||||
method: "POST",
|
||||
},
|
||||
{
|
||||
category: "personalRecordsVideo",
|
||||
title: record.title,
|
||||
alt: "personalRecords",
|
||||
file: record.recording,
|
||||
}
|
||||
)
|
||||
record.recording = videoRes.data?.id
|
||||
}
|
||||
if (record.thumbnail && typeof record.thumbnail !== "string") {
|
||||
record.thumbnail = await convertObjectURLToOriginalFormat(record.thumbnail)
|
||||
|
||||
const imgRes = await xhrApiCall(
|
||||
"medialib",
|
||||
{
|
||||
method: "POST",
|
||||
},
|
||||
{
|
||||
category: "personalRecordsThumbnail",
|
||||
title: record.title,
|
||||
alt: "personalRecords",
|
||||
file: record.thumbnail,
|
||||
}
|
||||
)
|
||||
record.thumbnail = imgRes.data?.id
|
||||
}
|
||||
if (update) {
|
||||
records = records.map((r, i) => (id == i + 1 ? record : r))
|
||||
} else records = [...records, record]
|
||||
$login.customer.personalRecords = records
|
||||
await updateInternalCustomer($login.customer.id, $login.customer).then((res) => {
|
||||
approvedUsageTerms = false
|
||||
if (!update) newRecord = initRecord()
|
||||
setTimeout(() => (reload = !reload), 100)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRecord(i: number) {
|
||||
if (records[i].recording) {
|
||||
await deleteDBEntry("medialib", records[i].recording as string)
|
||||
}
|
||||
if (records[i].thumbnail) {
|
||||
await deleteDBEntry("medialib", records[i].thumbnail as string)
|
||||
}
|
||||
|
||||
records = records.filter((_, j) => i !== j)
|
||||
|
||||
$login.customer.personalRecords = records
|
||||
|
||||
await updateInternalCustomer($login.customer.id, $login.customer).finally(() => {
|
||||
loading = -1
|
||||
reload = !reload
|
||||
})
|
||||
}
|
||||
|
||||
let newRecord = initRecord()
|
||||
</script>
|
||||
|
||||
{#key reload}
|
||||
<ul class="editable-records">
|
||||
<LoadingWrapper
|
||||
active="{loading == 0}"
|
||||
styles="width: 100%; height: 100%;"
|
||||
progress="{true}"
|
||||
>
|
||||
<RecordEditMask
|
||||
bind:record="{newRecord}"
|
||||
entryId="{$login?.tokenData?.tibiId}"
|
||||
index="{0}"
|
||||
newRecord="{true}"
|
||||
on:actionButton="{() => {
|
||||
loading = 0
|
||||
createOrUpdateRecord(newRecord, false, 0).finally(() => (loading = -1))
|
||||
}}"
|
||||
/></LoadingWrapper
|
||||
>
|
||||
{#each records as _, i}
|
||||
<LoadingWrapper
|
||||
active="{loading == i + 1}"
|
||||
styles="width: 100%; height: 100%;"
|
||||
progress="{true}"
|
||||
>
|
||||
<RecordEditMask
|
||||
bind:record="{records[i]}"
|
||||
entryId="{$login?.tokenData?.tibiId}"
|
||||
index="{i + 1}"
|
||||
on:update="{(e) => {
|
||||
let fileChange = true
|
||||
if (e?.detail?.noFileChange) fileChange = false
|
||||
|
||||
createOrUpdateRecord(records[i], true, i + 1, fileChange).finally(() => (loading = -1))
|
||||
}}"
|
||||
on:actionButton="{() => {
|
||||
deleteRecord(i)
|
||||
}}"
|
||||
/>
|
||||
</LoadingWrapper>
|
||||
{/each}
|
||||
</ul>
|
||||
{/key}
|
||||
|
||||
{#if showUsageTermsModal}
|
||||
<Modal
|
||||
size="md"
|
||||
show="{true}"
|
||||
on:close="{() => {
|
||||
showUsageTermsModal = false
|
||||
}}"
|
||||
>
|
||||
<svelte:fragment slot="title">Nutzungsbedingungen</svelte:fragment>
|
||||
<div
|
||||
class="form"
|
||||
style="flex-direction: row !important;"
|
||||
>
|
||||
<Input
|
||||
type="checkbox"
|
||||
bind:value="{approvedUsageTerms}"
|
||||
onChange="{onChange}"
|
||||
id="usageTerms"
|
||||
/>
|
||||
<p>
|
||||
Ich habe die <a
|
||||
use:spaLink
|
||||
href="/nutzungsbedingungen">Nutzungsbedingungen</a
|
||||
> gelesen und akzeptiere Sie. Auch achte ich darauf, nicht gegen die Urheber- und Datenschutzrechte von Dritten
|
||||
zu verstoßen.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
slot="footer"
|
||||
class="md-footer"
|
||||
>
|
||||
<button
|
||||
class="cta secondary"
|
||||
on:click="{() => (showUsageTermsModal = false)}"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
class="cta primary"
|
||||
on:click="{() => {
|
||||
if (!approvedUsageTerms) {
|
||||
newNotification({ html: 'Bitte akzeptiere die Nutzungsbedingungen.', class: 'error' })
|
||||
return
|
||||
}
|
||||
showUsageTermsModal = false
|
||||
|
||||
//@ts-ignore
|
||||
createOrUpdateRecord(...createOrUpdateParams).finally(() => (loading = -1))
|
||||
}}"
|
||||
>Hochladen
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style lang="less">
|
||||
.editable-records:global {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2.4rem;
|
||||
.loadingWrapper,
|
||||
li {
|
||||
padding: 0px;
|
||||
width: 100%;
|
||||
max-width: 550px;
|
||||
}
|
||||
li {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.form {
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
align-items: center;
|
||||
}
|
||||
.md-footer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
@@ -1,325 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiContentSaveMove, mdiImagePlusOutline, mdiPlus, mdiTrashCanOutline } from "@mdi/js"
|
||||
import Input from "../../blocks/form/Input.svelte"
|
||||
import CardWrapper from "../CardWrapper.svelte"
|
||||
import { onChange } from "../helper"
|
||||
import Icon from "../../../widgets/Icon.svelte"
|
||||
import FileInput from "../../blocks/form/FileInput.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import VideoPreview from "./VideoPreview.svelte"
|
||||
import { actionApproval } from "../../../../store"
|
||||
|
||||
const dispatcher = createEventDispatcher()
|
||||
|
||||
export let record: PersonalRecord,
|
||||
newRecord = false,
|
||||
index: number,
|
||||
entryId: string
|
||||
|
||||
let uploadMode = false
|
||||
|
||||
// Define the type for previous values
|
||||
type PreviousValues = {
|
||||
title: string
|
||||
ageAtRecording: number
|
||||
description: string
|
||||
}
|
||||
|
||||
// Initialize previous values
|
||||
let previousValues: PreviousValues = {
|
||||
title: record.title,
|
||||
ageAtRecording: record.ageAtRecording,
|
||||
description: record.description,
|
||||
}
|
||||
|
||||
function onChangeWrapper(e: Event) {
|
||||
const { name, value } = e.target as HTMLInputElement
|
||||
if (value != previousValues[name]) {
|
||||
previousValues = { ...previousValues, [name]: value }
|
||||
onChange(e)
|
||||
if (!newRecord) dispatcher("update", { ...record, noFileChange: true })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardWrapper>
|
||||
<button
|
||||
slot="rightCorner"
|
||||
class="action-button"
|
||||
aria-label="{newRecord ? 'Speichern' : 'Löschen'}"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
if (newRecord) {
|
||||
dispatcher('actionButton')
|
||||
} else {
|
||||
$actionApproval = {
|
||||
modalTitle: 'Rekord löschen',
|
||||
modalText: 'Möchtest du den Rekord wirklich löschen?',
|
||||
callback: () => {
|
||||
dispatcher('actionButton')
|
||||
},
|
||||
}
|
||||
}
|
||||
}}"
|
||||
>
|
||||
{#if newRecord}
|
||||
<Icon path="{mdiContentSaveMove}" />
|
||||
{:else}
|
||||
<Icon path="{mdiTrashCanOutline}" />
|
||||
{/if}
|
||||
</button>
|
||||
<form on:submit|preventDefault|stopPropagation>
|
||||
{#if uploadMode}
|
||||
<div class="upload-files">
|
||||
<div></div>
|
||||
<div class="inputs">
|
||||
<FileInput
|
||||
bind:value="{record.thumbnail}"
|
||||
id="thumbnail{index}"
|
||||
placeholder="Thumbnail"
|
||||
collectionName="medialib"
|
||||
entryId="{newRecord ? '-' : record.thumbnail}"
|
||||
helperText="Ein Thumbnail für das Video. Wird es weggelassen, wird der erste Frame des Videos als Thumbnail verwendet."
|
||||
imgIsData="{newRecord}"
|
||||
on:change="{() => {
|
||||
if (!newRecord) dispatcher('update', record)
|
||||
}}"
|
||||
/>
|
||||
<FileInput
|
||||
bind:value="{record.recording}"
|
||||
on:change="{() => {
|
||||
if (!newRecord) dispatcher('update', record)
|
||||
}}"
|
||||
id="video{index}"
|
||||
placeholder="Video"
|
||||
collectionName="medialib"
|
||||
type="video"
|
||||
noDelete="{true}"
|
||||
helperText="Das Video, das hochgeladen werden soll. Optimal wäre das WebM-Format. Auch MP4 und OGG sind möglich. Nutze andernfalls ein Tool zum Konvertieren. Die Datei darf nicht größer als 150MB sein. "
|
||||
entryId="{newRecord ? '-' : record.recording}"
|
||||
imgIsData="{newRecord}"
|
||||
/>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button
|
||||
on:click|preventDefault|stopPropagation="{() => {
|
||||
uploadMode = false
|
||||
}}">SCHLIESSEN</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{:else if record.recording}
|
||||
<VideoPreview
|
||||
imgIsData="{newRecord}"
|
||||
value="{record.recording}"
|
||||
thumbnail="{record.thumbnail}"
|
||||
entryId="{entryId}"
|
||||
on:edit="{() => {
|
||||
uploadMode = true
|
||||
}}"
|
||||
/>
|
||||
{:else}
|
||||
<button
|
||||
class="selectFiles"
|
||||
on:click|preventDefault|stopPropagation="{() => {
|
||||
uploadMode = true
|
||||
}}"
|
||||
>
|
||||
<div class="icon">
|
||||
<div class="image">
|
||||
<Icon
|
||||
path="{mdiImagePlusOutline}"
|
||||
size="24px"
|
||||
/>
|
||||
</div>
|
||||
<div class="add">
|
||||
<Icon
|
||||
path="{mdiPlus}"
|
||||
size="24px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p>Rekordvideo <br /> hochladen</p>
|
||||
</button>
|
||||
{/if}
|
||||
<div class="lower">
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChangeWrapper}"
|
||||
bind:value="{record.title}"
|
||||
placeholder="Titel"
|
||||
id="title{index}"
|
||||
name="title"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChangeWrapper}"
|
||||
type="number"
|
||||
bind:value="{record.ageAtRecording}"
|
||||
placeholder="Alter"
|
||||
id="ageAtRecording{index}"
|
||||
name="ageAtRecording"
|
||||
helperText="Alter zum Zeitpunkt der Aufnahme."
|
||||
/>
|
||||
<Input
|
||||
onChange="{onChangeWrapper}"
|
||||
type="number"
|
||||
bind:value="{record.priority}"
|
||||
placeholder="Priorität"
|
||||
id="priority{index}"
|
||||
name="priority"
|
||||
helperText="Je höher die Priorität, desto weiter oben wird der Rekord angezeigt."
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Input
|
||||
type="textarea"
|
||||
onChange="{onChangeWrapper}"
|
||||
bind:value="{record.description}"
|
||||
placeholder="Beschreibung"
|
||||
id="description{index}"
|
||||
name="description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if newRecord}
|
||||
<div style="display: flex; width: 100%;gap: 2px;">
|
||||
<button
|
||||
style="flex-grow: 1;"
|
||||
class="cta secondary"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
record = {
|
||||
title: '',
|
||||
ageAtRecording: undefined,
|
||||
description: '',
|
||||
recording: '',
|
||||
thumbnail: '',
|
||||
priority: 0,
|
||||
}
|
||||
previousValues = {
|
||||
title: '',
|
||||
ageAtRecording: undefined,
|
||||
description: '',
|
||||
}
|
||||
uploadMode = false
|
||||
}}">Zurücksetzen</button
|
||||
>
|
||||
<button
|
||||
class="cta primary"
|
||||
style="flex-grow: 1;"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
dispatcher('actionButton')
|
||||
}}">Hinzufügen</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
</CardWrapper>
|
||||
|
||||
<style lang="less">
|
||||
.action-button {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
form {
|
||||
.row {
|
||||
padding: 0px;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
padding: 0px 2.4rem;
|
||||
padding-bottom: 2.4rem;
|
||||
width: 100%;
|
||||
|
||||
.selectFiles,
|
||||
.upload-files {
|
||||
width: 100%;
|
||||
|
||||
aspect-ratio: 1/1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
padding: 1.2rem;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
background-color: var(--bg-300);
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
border: 2px solid var(--bg-100);
|
||||
transform: rotate(-45deg);
|
||||
transition: transform 0.3s ease-out, background-color 0.3s ease-out;
|
||||
background-color: transparent;
|
||||
z-index: 1;
|
||||
.image,
|
||||
.add {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
transform-origin: center;
|
||||
}
|
||||
.image {
|
||||
color: var(--bg-100);
|
||||
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.add {
|
||||
color: var(--neutral-white);
|
||||
display: none;
|
||||
}
|
||||
&:hover {
|
||||
.image {
|
||||
display: none;
|
||||
}
|
||||
.add {
|
||||
display: block;
|
||||
}
|
||||
background-color: var(--bg-100);
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
p {
|
||||
width: 95px;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
.upload-files {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
& > div {
|
||||
width: 100%;
|
||||
}
|
||||
.inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1.2rem;
|
||||
button {
|
||||
color: var(--text-invers-100);
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
.lower {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,121 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiExclamationThick } from "@mdi/js"
|
||||
import CardWrapper from "../CardWrapper.svelte"
|
||||
import VideoPreview from "./VideoPreview.svelte"
|
||||
import Icon from "../../../widgets/Icon.svelte"
|
||||
import Modal from "../../../Modal.svelte"
|
||||
import { reportCustomerRecord } from "../../../../functions/CommerceAPIs/tibiEndpoints/customer"
|
||||
import { newNotification } from "../../../../store"
|
||||
export let records: PersonalRecord[] = [],
|
||||
entryId: string
|
||||
|
||||
let reportRecordModal: any
|
||||
</script>
|
||||
|
||||
<ul class="records">
|
||||
{#each records || [] as record}
|
||||
<CardWrapper>
|
||||
<button
|
||||
slot="rightCorner"
|
||||
aria-label="Beitrag melden"
|
||||
class="rightCorner"
|
||||
on:click="{() => {
|
||||
reportRecordModal = { customerId: entryId, record: record?.title }
|
||||
}}"
|
||||
>
|
||||
<Icon
|
||||
path="{mdiExclamationThick}"
|
||||
size="1.5rem"
|
||||
/>
|
||||
</button>
|
||||
<div class="inner-wrapper">
|
||||
{#if record.recording}
|
||||
<VideoPreview
|
||||
age="{record.ageAtRecording}"
|
||||
value="{record.recording}"
|
||||
thumbnail="{record.thumbnail}"
|
||||
entryId="{entryId}"
|
||||
/>
|
||||
{/if}
|
||||
<div class="content">
|
||||
<h3>{record.title}</h3>
|
||||
<p>{record.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardWrapper>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{#if !!reportRecordModal}
|
||||
<Modal
|
||||
show="{true}"
|
||||
on:close="{() => (reportRecordModal = false)}"
|
||||
>
|
||||
<svelte:fragment slot="title">Beitrag Melden</svelte:fragment>
|
||||
<div>Haben du einen unangemessenen Beitrag gefunden? Melde diesen hier.</div>
|
||||
<div
|
||||
slot="footer"
|
||||
class="footer"
|
||||
style="display: flex;
|
||||
gap: 0.3rem;
|
||||
flex-wrap: wrap;"
|
||||
>
|
||||
<button
|
||||
class="cta secondary"
|
||||
on:click="{() => (reportRecordModal = false)}">Abbrechen</button
|
||||
>
|
||||
<button
|
||||
class="cta primary"
|
||||
on:click="{() => {
|
||||
reportCustomerRecord(reportRecordModal.customerId, reportRecordModal.record).then(() => {
|
||||
reportRecordModal = false
|
||||
newNotification({ html: 'Der Beitrag wurde erfolgreich gemeldet.', class: 'success' })
|
||||
})
|
||||
}}">Melden</button
|
||||
>
|
||||
</div>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
@import "../../../../assets/css/variables.less";
|
||||
.records {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2.4rem;
|
||||
|
||||
li {
|
||||
min-width: 550px;
|
||||
width: 550px;
|
||||
max-width: 550px;
|
||||
@media @mobile {
|
||||
max-width: calc(100vw - var(--horizontal-default-margin) * 2);
|
||||
min-width: calc(100vw - var(--horizontal-default-margin) * 2);
|
||||
}
|
||||
.rightCorner {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
.inner-wrapper {
|
||||
padding: 0px 2.4rem 2.4rem 2.4rem;
|
||||
display: flex;
|
||||
gap: 2.4rem;
|
||||
flex-direction: column;
|
||||
.content {
|
||||
padding: 0px 0.7rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
h3 {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
p {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,315 +0,0 @@
|
||||
<script
|
||||
lang="ts"
|
||||
context="module"
|
||||
>
|
||||
export function getValidLink(link: string): string {
|
||||
if (!link.includes("https://")) {
|
||||
if (link.includes("http://")) link = link.replace("http://", "")
|
||||
link = "https://" + link
|
||||
}
|
||||
return link
|
||||
}
|
||||
export function getUsernameFromLink(link: string): string {
|
||||
const url = new URL(getValidLink(link))
|
||||
return url.pathname.split("/").filter(Boolean).pop() || ""
|
||||
}
|
||||
export const socialMediaLinks: SocialMediaLink[] = [
|
||||
{ name: "instagram", link: "instagramLink", icon: "../../../../../../media/instagram.svg" },
|
||||
{ name: "facebook", link: "facebookLink", icon: "../../../../../../media/facebook.svg" },
|
||||
{ name: "twitter", link: "twitterLink", icon: "../../../../../../media/twitter.svg" },
|
||||
{ name: "tiktok", link: "tiktokLink", icon: "../../../../../../media/tiktok.svg" },
|
||||
{ name: "youtube", link: "youtubeLink", icon: "../../../../../../media/youtube.svg" },
|
||||
]
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { mdiClose, mdiDeleteOutline, mdiLinkVariant, mdiTrashCanOutline } from "@mdi/js"
|
||||
import { login, newNotification } from "../../../../store"
|
||||
import Icon from "../../../widgets/Icon.svelte"
|
||||
import { updateInternalCustomer } from "../../../../functions/CommerceAPIs/tibiEndpoints/customer"
|
||||
|
||||
let cs: Customer = $login.customer
|
||||
$: cs = $login.customer
|
||||
$: if (cs && !cs.socialMediaAccounts) cs.socialMediaAccounts = {}
|
||||
|
||||
function getPlaceholder(name: string): string {
|
||||
if (name === "instagram") return "instagram.com/binkrassdufass"
|
||||
if (name === "facebook") return "facebook.com/binkrassdufass"
|
||||
if (name === "twitter") return "x.com/krass_du"
|
||||
if (name === "tiktok") return "tiktok.com/@binkrassdufass"
|
||||
if (name === "youtube") return "youtube.com/@binkrassdufass"
|
||||
}
|
||||
|
||||
function checkValidLink(link: string, name: keyof SocialMediaLinks): boolean {
|
||||
switch (name) {
|
||||
case "instagramLink":
|
||||
return link.includes("instagram.com/")
|
||||
case "facebookLink":
|
||||
return link.includes("facebook.com/")
|
||||
case "twitterLink":
|
||||
return link.includes("x.com/")
|
||||
case "tiktokLink":
|
||||
return link.includes("tiktok.com/")
|
||||
case "youtubeLink":
|
||||
return link.includes("youtube.com/")
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
let editSM: keyof SocialMediaLinks | "" = "",
|
||||
editedLink = "",
|
||||
open = false
|
||||
|
||||
function customerHasSocialMediaAccounts(cs): boolean {
|
||||
return socialMediaLinks.some((sm) => !!cs.socialMediaAccounts[sm.link])
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="social-media-manager">
|
||||
<div
|
||||
class="social-media-list"
|
||||
class:has-value="{socialMediaLinks && socialMediaLinks.length && customerHasSocialMediaAccounts(cs)}"
|
||||
>
|
||||
{#if socialMediaLinks && socialMediaLinks.length && customerHasSocialMediaAccounts(cs)}
|
||||
{#each socialMediaLinks as socialMedia}
|
||||
{#if cs?.socialMediaAccounts?.[socialMedia?.link]}
|
||||
<a
|
||||
href="{getValidLink(cs.socialMediaAccounts[socialMedia.link])}"
|
||||
target="_blank"
|
||||
>
|
||||
<figure class="footer-icon">
|
||||
<img
|
||||
alt="{socialMedia.name}"
|
||||
src="{socialMedia.icon}"
|
||||
/>
|
||||
</figure>
|
||||
<p>
|
||||
{#if !getUsernameFromLink(cs.socialMediaAccounts[socialMedia.link]).startsWith("@")}
|
||||
@{/if}{getUsernameFromLink(cs.socialMediaAccounts[socialMedia.link])}
|
||||
</p>
|
||||
<button
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
cs.socialMediaAccounts[socialMedia.link] = ''
|
||||
updateInternalCustomer($login.customer.id, cs).then(() => {})
|
||||
}}"
|
||||
>
|
||||
<Icon path="{mdiDeleteOutline}" />
|
||||
</button>
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<p>Noch kein Social Media Konto hinzugefügt. Klicke rechts, um das zu ändern.</p>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- svelte-ignore missing-declaration -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="addSMButton"
|
||||
class:open="{open}"
|
||||
>
|
||||
<div class="interactive-line">
|
||||
<div></div>
|
||||
{#each socialMediaLinks as socialMedia}
|
||||
{#if !editSM}
|
||||
<button
|
||||
aria-label="{socialMedia.name}"
|
||||
on:click="{() => {
|
||||
editSM = socialMedia.link
|
||||
editedLink = cs.socialMediaAccounts[socialMedia.link] || ''
|
||||
}}"
|
||||
>
|
||||
<figure class="footer-icon">
|
||||
<img
|
||||
alt="{socialMedia.name}"
|
||||
src="{socialMedia.icon}"
|
||||
/>
|
||||
</figure>
|
||||
</button>
|
||||
{:else if editSM === socialMedia.link}
|
||||
<form
|
||||
class="smlink-input"
|
||||
style="flex-direction: row !important;"
|
||||
>
|
||||
<button
|
||||
aria-label="Profil entfernen"
|
||||
class="closeOrOpen footer-icon"
|
||||
style="color: var(--text-invers-100);"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
editSM = ''
|
||||
editedLink = ''
|
||||
delete cs.socialMediaAccounts[socialMedia.link]
|
||||
updateInternalCustomer($login.customer.id, cs).then(() => {})
|
||||
}}"
|
||||
>
|
||||
<Icon path="{mdiTrashCanOutline}" />
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="{getPlaceholder(socialMedia.name)}"
|
||||
bind:value="{editedLink}"
|
||||
/>
|
||||
</form>
|
||||
{/if}
|
||||
{/each}
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="action-button">
|
||||
{#if !!editSM}
|
||||
<button
|
||||
aria-label="Profil speichern"
|
||||
class="add-link"
|
||||
on:click="{() => {
|
||||
//@ts-ignore
|
||||
editedLink = editedLink.trim()
|
||||
editedLink = editedLink.toLowerCase()
|
||||
if (checkValidLink(editedLink, editSM)) {
|
||||
cs.socialMediaAccounts[editSM] = editedLink
|
||||
editSM = ''
|
||||
editedLink = ''
|
||||
updateInternalCustomer($login.customer.id, cs).then(() => {})
|
||||
} else {
|
||||
// show error
|
||||
newNotification({ html: 'Ungültiger Link.', class: 'error' })
|
||||
}
|
||||
}}"
|
||||
>
|
||||
<Icon path="{mdiLinkVariant}" />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="closeOrOpen"
|
||||
aria-label="Social Media hinzufügen"
|
||||
on:click="{() => {
|
||||
open = !open
|
||||
editSM = ''
|
||||
editedLink = ''
|
||||
}}"
|
||||
>
|
||||
<Icon path="{mdiClose}" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
section.social-media-manager:global {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.2rem;
|
||||
|
||||
.social-media-list {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 1.2rem;
|
||||
&.has-value {
|
||||
overflow-x: auto;
|
||||
//hide scrollbar
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
flex-grow: 1;
|
||||
a {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
color: var(--text-invers-100);
|
||||
button {
|
||||
color: var(--text-invers-100);
|
||||
opacity: 0;
|
||||
}
|
||||
&:hover {
|
||||
button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.addSMButton {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-right: 1.2rem;
|
||||
flex-grow: 1;
|
||||
padding: 1rem 0px;
|
||||
min-width: 250px;
|
||||
.interactive-line {
|
||||
max-height: calc(2.4rem);
|
||||
height: 2.4rem;
|
||||
width: 0px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
background-color: var(--bg-100);
|
||||
gap: 0.6rem;
|
||||
transition: width 0.3s ease-out, padding 0.3s ease-out;
|
||||
img {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
color: var(--neutral-white);
|
||||
}
|
||||
}
|
||||
.action-button {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 2px solid var(--bg-100);
|
||||
transform: rotate(-45deg);
|
||||
svg {
|
||||
color: var(--bg-100);
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
}
|
||||
transition: transform 0.3s ease-out 0.3s, border-color 0.3s ease-out 0.3s,
|
||||
background-color 0.3s ease-out 0.3s;
|
||||
}
|
||||
|
||||
&.open {
|
||||
gap: 2px;
|
||||
.action-button {
|
||||
height: 2.4rem;
|
||||
transform: rotate(0deg);
|
||||
.closeOrOpen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
svg {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
background-color: var(--bg-100);
|
||||
transition: transform 0.3s ease-out, border-color 0.3s ease-out, background-color 0.3s ease-out;
|
||||
}
|
||||
.interactive-line {
|
||||
transition: width 0.3s ease-out 0.3s, padding 0.3s ease-out 0.3s;
|
||||
width: 210px;
|
||||
|
||||
.smlink-input {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding-right: 10px;
|
||||
input {
|
||||
outline: none;
|
||||
border: none;
|
||||
height: 26px;
|
||||
padding: 12px 6px;
|
||||
border-bottom: 1px solid var(--bg-200);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,265 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount, onDestroy } from "svelte"
|
||||
import videojs from "video.js"
|
||||
|
||||
import "video.js/dist/video-js.css"
|
||||
import { apiBaseURL } from "../../../../../config"
|
||||
import { mdiImageEditOutline } from "@mdi/js"
|
||||
import Icon from "../../../widgets/Icon.svelte"
|
||||
import MedialibFile, { loadMedialibEntry } from "../../../widgets/MedialibFile.svelte"
|
||||
import { getDBEntry } from "../../../../../api"
|
||||
|
||||
export let entryId: string,
|
||||
value: FileField | string,
|
||||
thumbnail: FileField | string = null,
|
||||
age: number = undefined,
|
||||
imgIsData = false
|
||||
|
||||
let videoElement: HTMLVideoElement
|
||||
let player: any
|
||||
const dispatcher = createEventDispatcher()
|
||||
let thumbnailSrc = ""
|
||||
console.log(thumbnail)
|
||||
async function initializePlayer() {
|
||||
console.log("initializePlayer", videoElement)
|
||||
if (!videoElement) return
|
||||
|
||||
if (player) {
|
||||
player.dispose()
|
||||
}
|
||||
|
||||
player = videojs(videoElement, {
|
||||
id: "video-" + entryId,
|
||||
controls: true,
|
||||
autoplay: false,
|
||||
preload: "auto",
|
||||
language: "de",
|
||||
poster: thumbnail ? (typeof thumbnail === "object" ? thumbnail.src : thumbnailSrc) : undefined,
|
||||
fluid: true,
|
||||
fill: true,
|
||||
bigPlayButton: true,
|
||||
inactivityTimeout: 0,
|
||||
controlBar: {
|
||||
playToggle: false,
|
||||
volumePanel: {
|
||||
inline: true,
|
||||
},
|
||||
},
|
||||
userActions: {
|
||||
hotkeys: true,
|
||||
click: true,
|
||||
},
|
||||
})
|
||||
player.on("touchstart", function (e) {
|
||||
if (e.target.nodeName === "VIDEO") {
|
||||
if (player.paused()) {
|
||||
this.play()
|
||||
} else {
|
||||
this.pause()
|
||||
}
|
||||
}
|
||||
})
|
||||
player.controlBar.show()
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (typeof thumbnail === "string") {
|
||||
await loadThumbnail()
|
||||
}
|
||||
mounted = true
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (player) {
|
||||
player.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
async function loadThumbnail() {
|
||||
try {
|
||||
if (typeof thumbnail === "string") {
|
||||
let entry =
|
||||
typeof window !== "undefined"
|
||||
? await loadMedialibEntry(thumbnail)
|
||||
: await getDBEntry("medialib", { _id: thumbnail })
|
||||
if (entry?.file?.src) {
|
||||
thumbnailSrc = apiBaseURL + "medialib/" + thumbnail + "/" + entry.file.src
|
||||
}
|
||||
} else if (thumbnail && typeof thumbnail === "object" && thumbnail.src) {
|
||||
thumbnailSrc = thumbnail.src
|
||||
} else if (!thumbnail) {
|
||||
thumbnailSrc = `${value.src}#t=0.1`
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function customPlay() {
|
||||
if (player) {
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
let mounted = false
|
||||
let loading = false
|
||||
$: if (mounted && !loading && videoElement) setTimeout(initializePlayer, 100)
|
||||
$: props = {
|
||||
class: "video-js vjs-default-skin vjs-fill",
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="video-container">
|
||||
{#if !age}
|
||||
<button
|
||||
class="edit"
|
||||
aria-label="Edit"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
dispatcher('edit')
|
||||
}}"
|
||||
>
|
||||
<Icon
|
||||
size="24px"
|
||||
path="{mdiImageEditOutline}"
|
||||
/>
|
||||
</button>
|
||||
{/if}
|
||||
{#if age}
|
||||
<div class="age">
|
||||
<span>ALTER</span>
|
||||
<p>{age}</p>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="98"
|
||||
height="96"
|
||||
viewBox="0 0 98 96"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M97.3521 96L0 0L97.3521 0L97.3521 96Z"
|
||||
fill="white"></path>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
{#if imgIsData}
|
||||
<div class="video-wrapper">
|
||||
<video
|
||||
bind:this="{videoElement}"
|
||||
{...props}
|
||||
>
|
||||
<track kind="captions" />
|
||||
<source
|
||||
src="{typeof value === 'object' ? value.src : value}{thumbnail ? '' : '#t=0.1'}"
|
||||
type="{typeof value === 'object' ? value.type : 'video/mp4'}"
|
||||
/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
{:else}
|
||||
<MedialibFile
|
||||
id="{value}"
|
||||
let:entry
|
||||
let:src
|
||||
bind:loading="{loading}"
|
||||
>
|
||||
<div class="video-wrapper">
|
||||
<video
|
||||
bind:this="{videoElement}"
|
||||
{...props}
|
||||
>
|
||||
<track kind="captions" />
|
||||
<source
|
||||
src="{src}{thumbnail ? '' : '#t=0.1'}"
|
||||
type="{entry.file.type}"
|
||||
/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
</MedialibFile>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
@import "video.js/dist/video-js.css";
|
||||
.video-container {
|
||||
position: relative;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
margin: auto;
|
||||
.edit {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
padding: 6px;
|
||||
color: white;
|
||||
z-index: 10;
|
||||
background-color: #332d2c61;
|
||||
}
|
||||
.age {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
|
||||
p,
|
||||
span {
|
||||
z-index: 11;
|
||||
position: relative;
|
||||
font-weight: 700;
|
||||
}
|
||||
span {
|
||||
font-size: 0.7rem;
|
||||
line-height: 0.7rem;
|
||||
}
|
||||
p {
|
||||
font-weight: 700;
|
||||
}
|
||||
svg {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
right: -1px;
|
||||
z-index: 10;
|
||||
height: 4.4rem;
|
||||
width: 4.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.video-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.video-js {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.vjs-tech {
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.vjs-fill {
|
||||
width: 100%; /* Volle Breite auch für Videos ohne Thumbnail */
|
||||
height: 100%;
|
||||
}
|
||||
.vjs-control-bar {
|
||||
z-index: 10;
|
||||
}
|
||||
.vjs-button {
|
||||
}
|
||||
.vjs-big-play-button {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
color: var(--primary-100) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,77 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { formatPrice } from "../../../../../api/hooks/config-client"
|
||||
|
||||
export let price: BKDFMoney
|
||||
export let discount: boolean
|
||||
export let oldPrice: boolean
|
||||
export let smallVersion = false
|
||||
</script>
|
||||
|
||||
<span
|
||||
class:discount="{discount}"
|
||||
class:oldprice="{oldPrice}"
|
||||
class:smallVersion="{smallVersion}"
|
||||
>
|
||||
<img
|
||||
src="../../../../media/bottomRightCrinkle{oldPrice ? 'Blue' : 'Red'}{smallVersion ? 'XS' : ''}.svg"
|
||||
id="lefternCrinkle"
|
||||
alt="crinkle"
|
||||
/>
|
||||
<em>
|
||||
{formatPrice(Number(price.amount) * 1.19)}
|
||||
</em>
|
||||
<img
|
||||
id="righternCrinkle"
|
||||
src="../../../../media/topLeftCrinkle{oldPrice ? 'Blue' : 'Red'}{smallVersion ? 'XS' : ''}.svg"
|
||||
alt="crinkle"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<style lang="less">
|
||||
span {
|
||||
background: var(--primary-100);
|
||||
padding: 6px 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: fit-content;
|
||||
gap: 0.5rem;
|
||||
position: relative;
|
||||
font-size: 20px;
|
||||
|
||||
&.oldprice {
|
||||
background-color: var(--text-invers-100);
|
||||
em {
|
||||
color: var(--text-invers-150);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
}
|
||||
#righternCrinkle {
|
||||
right: -7px;
|
||||
}
|
||||
em {
|
||||
color: var(--neutral-white);
|
||||
font-weight: 400;
|
||||
font-size: 1em;
|
||||
line-height: 14px;
|
||||
font-family: "Outfit", sans-serif;
|
||||
font-style: normal;
|
||||
}
|
||||
#lefternCrinkle {
|
||||
left: -7px;
|
||||
}
|
||||
&.smallVersion {
|
||||
#righternCrinkle {
|
||||
right: -6px;
|
||||
}
|
||||
|
||||
em {
|
||||
font-size: 0.7em;
|
||||
line-height: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,135 +0,0 @@
|
||||
<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>
|
||||
@@ -1,114 +0,0 @@
|
||||
<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>
|
||||
@@ -1,299 +0,0 @@
|
||||
<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>
|
||||
@@ -1,154 +0,0 @@
|
||||
<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>
|
||||
@@ -1,47 +0,0 @@
|
||||
<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>
|
||||
@@ -1,41 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<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>
|
||||
@@ -1,139 +0,0 @@
|
||||
import { get } from "svelte/store"
|
||||
import { bigCommerceFetch } from "."
|
||||
import { api, postDBEntry } from "../../../../api"
|
||||
import { login } from "../../../store"
|
||||
import { BKDFFromBigCommerceLineItems, bigCommerceToBKDFCart } from "./mapper"
|
||||
import { createCartMutation } from "./mutations/cart"
|
||||
import { getStoreProductsQuery } from "./queries/product"
|
||||
|
||||
const getBigCommerceProductsWithCheckoutInformation = async (cartId: string) => {
|
||||
const _login = get(login)
|
||||
|
||||
let cartRes
|
||||
if (_login) {
|
||||
cartRes = await api(`dummyCartEndpoint/${cartId}?checkout=true&loggedInCheckout=true`, {
|
||||
method: "GET",
|
||||
useJwt: true,
|
||||
})
|
||||
} else cartRes = await api(`dummyCartEndpoint/${cartId}?checkout=true`, {})
|
||||
|
||||
const cart = cartRes.data?.data?.cart
|
||||
if (!cart) {
|
||||
return {
|
||||
productsByIdList: null,
|
||||
cart: null,
|
||||
checkoutUrl: null,
|
||||
}
|
||||
}
|
||||
const checkoutUrl = cartRes.data.data.checkoutURL
|
||||
const lines = BKDFFromBigCommerceLineItems(cart.lineItems)
|
||||
const productIds = lines.map(({ merchandiseId }) => parseInt(merchandiseId))
|
||||
const bigCommerceProductListRes = await bigCommerceFetch<BigCommerceProductsOperation>({
|
||||
query: getStoreProductsQuery,
|
||||
variables: {
|
||||
entityIds: productIds,
|
||||
},
|
||||
cache: "no-store",
|
||||
})
|
||||
const bigCommerceProductList = bigCommerceProductListRes.body.data.site.products.edges.map(
|
||||
(product) => product.node
|
||||
)
|
||||
const createProductList = (idList: number[], products: BigCommerceProduct[]) => {
|
||||
return idList.map((productId) => {
|
||||
const productData = products.find(({ entityId }) => entityId == productId)!
|
||||
return {
|
||||
productId,
|
||||
productData,
|
||||
}
|
||||
})
|
||||
}
|
||||
const bigCommerceProducts = createProductList(productIds, bigCommerceProductList)
|
||||
return {
|
||||
productsByIdList: bigCommerceProducts,
|
||||
cart,
|
||||
checkoutUrl,
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCart(
|
||||
lines: { merchandiseId: string; quantity: number; productId?: string }[]
|
||||
): Promise<BKDFCart | null> {
|
||||
let bigCommerceCart: BigCommerceCart
|
||||
|
||||
const res = await bigCommerceFetch<BigCommerceCreateCartOperation>({
|
||||
query: createCartMutation,
|
||||
variables: {
|
||||
createCartInput: {
|
||||
lineItems: lines.map(({ merchandiseId, quantity, productId }) => ({
|
||||
productEntityId: parseInt(productId!, 10),
|
||||
variantEntityId: parseInt(merchandiseId, 10),
|
||||
quantity,
|
||||
})),
|
||||
},
|
||||
},
|
||||
cache: "no-store",
|
||||
})
|
||||
bigCommerceCart = res.body.data.cart.createCart.cart
|
||||
const { cart, productsByIdList, checkoutUrl } = await getBigCommerceProductsWithCheckoutInformation(
|
||||
bigCommerceCart.entityId
|
||||
)
|
||||
if (!cart || !productsByIdList) return null
|
||||
return bigCommerceToBKDFCart(cart, productsByIdList, checkoutUrl)
|
||||
}
|
||||
|
||||
export async function addCartItem(
|
||||
cartId: string,
|
||||
lines: { merchandiseId: string; quantity: number; productId?: string }[]
|
||||
): Promise<BKDFCart | null> {
|
||||
await postDBEntry("dummyCartEndpoint", {
|
||||
operation: "add",
|
||||
cartId,
|
||||
lineItems: lines.map(({ merchandiseId, quantity, productId }) => ({
|
||||
product_id: productId,
|
||||
variant_id: merchandiseId,
|
||||
quantity,
|
||||
})),
|
||||
})
|
||||
|
||||
const { cart, productsByIdList, checkoutUrl } = await getBigCommerceProductsWithCheckoutInformation(cartId)
|
||||
if (!cart || !productsByIdList) return null
|
||||
return bigCommerceToBKDFCart(cart, productsByIdList, checkoutUrl)
|
||||
}
|
||||
|
||||
export async function updateCartItem(
|
||||
cartId: string,
|
||||
line: { merchandiseId: string; productId: string; quantity: number; entityId: string }
|
||||
): Promise<BKDFCart | null> {
|
||||
await postDBEntry("dummyCartEndpoint", {
|
||||
operation: "update",
|
||||
lineItem: {
|
||||
product_id: line.productId,
|
||||
variant_id: line.merchandiseId,
|
||||
quantity: line.quantity,
|
||||
},
|
||||
cartId,
|
||||
entityId: line.entityId,
|
||||
})
|
||||
|
||||
const { cart, productsByIdList, checkoutUrl } = await getBigCommerceProductsWithCheckoutInformation(cartId)
|
||||
if (!cart || !productsByIdList) return null
|
||||
return bigCommerceToBKDFCart(cart, productsByIdList, checkoutUrl)
|
||||
}
|
||||
|
||||
export async function deleteCartItem(cartId: string, lineId: string, lastItem = false): Promise<BKDFCart | null> {
|
||||
await postDBEntry("dummyCartEndpoint", {
|
||||
operation: "delete",
|
||||
cartId,
|
||||
entityId: lineId,
|
||||
})
|
||||
if (lastItem) return
|
||||
const { cart, productsByIdList, checkoutUrl } = await getBigCommerceProductsWithCheckoutInformation(cartId)
|
||||
if (!cart || !productsByIdList) return null
|
||||
return bigCommerceToBKDFCart(cart, productsByIdList, checkoutUrl)
|
||||
}
|
||||
|
||||
export async function getCart(cartId: string): Promise<BKDFCart | null> {
|
||||
const { cart, productsByIdList, checkoutUrl } = await getBigCommerceProductsWithCheckoutInformation(cartId)
|
||||
if (!cart || !productsByIdList) return null
|
||||
return bigCommerceToBKDFCart(cart, productsByIdList, checkoutUrl)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { bigCommerceFetch } from "."
|
||||
|
||||
import { getStoreCategorieTreeQuery } from "./queries/category"
|
||||
import { getMenuQuery } from "./queries/menu"
|
||||
|
||||
export function mapBigcommerceCategoriesToNavigation(
|
||||
bigCommerceCategories: BigCommerceCategory[]
|
||||
): NavigationElement[] {
|
||||
function convertCategoryToNavigationEntry(category: BigCommerceCategory): NavigationElement {
|
||||
const navEl = {
|
||||
name: category.name,
|
||||
page: "/collections" + category.path,
|
||||
external: false,
|
||||
elements: category.children?.map(convertCategoryToNavigationEntry),
|
||||
type: "bigcommerce" as const, // Change the type to "bigcommerce"
|
||||
seo: category.seo,
|
||||
}
|
||||
return navEl
|
||||
}
|
||||
|
||||
return bigCommerceCategories.map(convertCategoryToNavigationEntry)
|
||||
}
|
||||
|
||||
export async function getBCGraphCategories(): Promise<BigCommerceCategory[]> {
|
||||
const res = await bigCommerceFetch<BigCommerceCategoryOperation>({
|
||||
query: getStoreCategorieTreeQuery,
|
||||
})
|
||||
return res?.body?.data?.site?.categoryTree
|
||||
}
|
||||
|
||||
export const getBCGraphCategoryEntityIdbyHandle = async (handle: string) => {
|
||||
const resp = await bigCommerceFetch<BigCommerceMenuOperation>({
|
||||
query: getMenuQuery,
|
||||
})
|
||||
const recursiveFindCollectionId = (list: BigCommerceCategoryTreeItem[], slug: string): number => {
|
||||
const collectionId = list
|
||||
.flatMap((item): number | null => {
|
||||
if (item.path.includes(slug!)) return item.entityId
|
||||
|
||||
if (item.children && item.children.length) return recursiveFindCollectionId(item.children!, slug)
|
||||
|
||||
return null
|
||||
})
|
||||
.filter((id) => typeof id === "number")[0]
|
||||
|
||||
return collectionId!
|
||||
}
|
||||
|
||||
return recursiveFindCollectionId(resp.body.data.site.categoryTree, handle)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { bigCommerceFetch } from "."
|
||||
import { pushErrors } from "./errors"
|
||||
import { registerCustomerMutation } from "./mutations/customer"
|
||||
|
||||
export async function registerBCGraphCustomer(
|
||||
customerDetails: RegisterCustomerInput
|
||||
): Promise<BigCommerceCustomer | null> {
|
||||
const res = await bigCommerceFetch<BigCommerceRegisterCustomerOperation>({
|
||||
query: registerCustomerMutation,
|
||||
variables: {
|
||||
registerCustomerInput: customerDetails,
|
||||
},
|
||||
cache: "no-store",
|
||||
})
|
||||
|
||||
const bigCommerceCustomer = res.body.data.customer.registerCustomer.customer
|
||||
if (!bigCommerceCustomer) {
|
||||
const errors = res.body.data.customer.registerCustomer.errors
|
||||
if (errors) pushErrors(errors)
|
||||
|
||||
return null
|
||||
}
|
||||
return bigCommerceCustomer
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { newNotification } from "../../../store"
|
||||
|
||||
export function pushErrors(errors: BigCommerceError[]) {
|
||||
errors.forEach((error) => {
|
||||
let html
|
||||
if (error.__typename === "EmailAlreadyInUseError")
|
||||
html =
|
||||
"Diese Email-Adresse ist bereits in Verwendung. Bitte logge dich ein oder verwende eine andere Email-Adresse."
|
||||
newNotification({
|
||||
class: "error",
|
||||
html: html || error.message,
|
||||
})
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user