zwischenstand

This commit is contained in:
2025-10-02 09:03:39 +00:00
parent 099530b7c8
commit f3dc0dc9bd
52 changed files with 994 additions and 5602 deletions

View File

@@ -1,21 +1,25 @@
import type { Action } from "svelte/action"
import { overlays } from "./store"
export const spaLink: Action<HTMLAnchorElement> = (node) => {
const onClick = (event: MouseEvent) => {
if (node.dataset.spaPrevent && node.dataset.spaPrevent !== "false") {
return
}
const anchor = event.currentTarget as HTMLAnchorElement
if ((anchor.target === "" || anchor.target === "_self") && anchor.href.startsWith(window.location.origin)) {
const isSelfTarget = anchor.target === "" || anchor.target === "_self"
if (isSelfTarget && anchor.href.startsWith(window.location.origin)) {
event.preventDefault()
const nextUrl = anchor.pathname + anchor.search + anchor.hash
const currentUrl = window.location.pathname + window.location.search + window.location.hash
const newUrl = anchor.pathname + anchor.search + anchor.hash
if (currentUrl === newUrl) {
window.scrollTo(0, 0)
if (nextUrl === currentUrl) {
window.scrollTo({ top: 0, behavior: "smooth" })
return
}
spaNavigate(anchor.pathname + anchor.search + anchor.hash)
spaNavigate(nextUrl)
}
}
@@ -29,26 +33,15 @@ export const spaLink: Action<HTMLAnchorElement> = (node) => {
}
export const spaNavigate = (to: string, options?: { replace?: boolean }) => {
overlays.update((current) => [])
window.scrollTo(0, 0)
//scroll to top of page
setTimeout(
() => {
window.scrollTo(0, 0)
},
100
)
if (options?.replace) {
window.history.replaceState(null, "", to)
} else {
window.history.pushState(null, "", to)
}
window.scrollTo({ top: 0, behavior: "smooth" })
}
export const spaBack = () => {
window.history.back()
}
// TODO: spaLinks container for containing {@html ...}

View File

@@ -1,323 +1,219 @@
<script lang="ts">
import { onMount } from "svelte"
import { getDBEntries } from "../../api"
import { socialIcons } from "../../config"
import { socialIcons, companyName, email, streetAddress, zipCode, localityAddress } from "../../config"
import { spaLink } from "../actions"
import { login, navigationCache } from "../store"
import CrinkledSection from "./CrinkledSection.svelte"
import Input from "./pagebuilder/blocks/form/Input.svelte"
import { submitNewsletter } from "../functions/CommerceAPIs/tibiEndpoints/actions"
import { onChange } from "./pagebuilder/profile/helper"
enum NavigationType {
MainNavigation = 0,
ServiceNavigation = 2,
LegalNavigation = 1,
}
let emailIsSubscribed = false
let navigationEntries: NavigationEntry[] = []
function elementsToCache(elements: NavigationElement[]) {
elements.forEach((el) => {
if (!el.external) {
if (!$navigationCache[el.page]) $navigationCache[el.page] = el
if (el.elements?.length > 0) elementsToCache(el.elements)
}
})
const NAVIGATION_TYPE = {
Main: 0,
Service: 1,
Legal: 2,
} as const
let legalLinks: NavigationElement[] = []
let serviceLinks: NavigationElement[] = []
let loadingNavigation = true
const resolveHref = (link: NavigationElement) => {
const base = link.page || "/"
const hash = link.hash ? (link.hash.startsWith("#") ? link.hash : `#${link.hash}`) : ""
return `${base}${hash}`
}
getDBEntries("navigation").then((navs) => {
navigationEntries = navs.sort((a, b) => a.type - b.type)
navigationEntries.forEach((nav) => elementsToCache(nav.elements))
onMount(async () => {
try {
const entries = await getDBEntries("navigation")
legalLinks = entries.find((entry) => Number(entry.type) === NAVIGATION_TYPE.Legal)?.elements ?? []
serviceLinks = entries.find((entry) => Number(entry.type) === NAVIGATION_TYPE.Service)?.elements ?? []
} catch (error) {
console.error("Unable to load footer navigation", error)
} finally {
loadingNavigation = false
}
})
export let className = ""
let sections: {
title: string
links: NavigationElement[]
}[] = []
$: if (navigationEntries.length) sections = []
let email = ""
let dataProt = false
const currentYear = new Date().getFullYear()
</script>
<CrinkledSection>
<footer class={'footer ' + className}>
<section id="content-section">
<section id="content-link-section">
{#each sections as section}
<section class="content">
<div class="row">
<div class="col">
<small class="service"><em>{section?.title}</em></small>
<nav class="sub-points">
<ul>
{#each section?.links || [] as link}
<li>
<a
class="footer-nav-point"
use:spaLink
href={link.page}><small>{link.name}</small></a
>
</li>
{/each}
</ul>
</nav>
</div>
</div>
</section>
{/each}
</section>
<section id="newsletter-section">
<h4>Newsletter</h4>
<form
on:submit|preventDefault|stopPropagation={() => {
submitNewsletter(email, dataProt).then(() => {
emailIsSubscribed = true
})
}}
>
{#if emailIsSubscribed}
<p>Du hast dich zum Newsletter angemeldet!</p>
{:else}
<Input
type="text"
placeholder="E-Mail"
bind:value={email}
id="email"
onChange={onChange}
/>
<div class="data-protection">
<Input
type="checkbox"
bind:value={dataProt}
id="dataprot"
onChange={onChange}
/>
<p>
<a
href="/datenschutz"
use:spaLink>Datenschutz</a
> zum Newsletterversand akzeptieren
</p>
</div>
<button
class="btn cta primary"
type="submit"
>
Anmelden</button
>
{/if}
</form>
</section>
</section>
<section id="icons-section">
<div class="line">
<div class="line-1"></div>
<img
alt="Symbol"
class="symbol"
src="../../../logo/logoShort.svg"
/>
<div class="line-2"></div>
</div>
<ul class="social">
{#each Object.keys(socialIcons) as icon}
<footer class="site-footer">
<div class="footer-inner">
<section class="footer-column">
<h2>{companyName}</h2>
<p>{streetAddress}, {zipCode} {localityAddress}</p>
<a
class="footer-link"
href={`mailto:${email}`}
>
{email}
</a>
<ul class="footer-social">
{#each Object.entries(socialIcons) as [name, url]}
<li>
<a
href={socialIcons[icon]}
use:spaLink
href={url}
target="_blank"
aria-label={icon}
>
<figure class="footer-icon">
<img
alt={icon}
src="../../../media/{icon}.svg"
/>
</figure></a
rel="noopener noreferrer"
aria-label={`Öffne ${name} in einem neuen Tab`}
>
{name}
</a>
</li>
{/each}
</ul>
</section>
<section id="legal-section">
<div class="wrapper">
<small class="">© 2024 | BKDF - Bin Krass Du Fass | Alle Rechte vorbehalten.</small>
<nav class="nav-points">
<ul>
{#each navigationEntries.length ? navigationEntries[NavigationType.LegalNavigation].elements : [] as link}
<li>
<section class="footer-column">
<h3>Service</h3>
{#if loadingNavigation}
<span class="footer-placeholder">Links werden geladen …</span>
{:else if serviceLinks.length}
<ul>
{#each serviceLinks as link (link.name)}
<li>
{#if link.external && link.externalUrl}
<a
class="footer-nav-point-bottom"
use:spaLink
href={link.page}><small>{link.name}</small></a
href={link.externalUrl}
target="_blank"
rel="noopener noreferrer"
>
</li>
{/each}
</ul>
</nav>
</div>
{link.name}
</a>
{:else}
<a
href={resolveHref(link)}
use:spaLink
>
{link.name}
</a>
{/if}
</li>
{/each}
</ul>
{:else}
<span class="footer-placeholder">Aktuell keine Service-Links</span>
{/if}
</section>
</footer>
</CrinkledSection>
<section class="footer-column">
<h3>Rechtliches</h3>
{#if loadingNavigation}
<span class="footer-placeholder">Links werden geladen …</span>
{:else if legalLinks.length}
<ul>
{#each legalLinks as link (link.name)}
<li>
{#if link.external && link.externalUrl}
<a
href={link.externalUrl}
target="_blank"
rel="noopener noreferrer"
>
{link.name}
</a>
{:else}
<a
href={resolveHref(link)}
use:spaLink
>
{link.name}
</a>
{/if}
</li>
{/each}
</ul>
{:else}
<span class="footer-placeholder">Aktuell keine rechtlichen Hinweise</span>
{/if}
</section>
</div>
<div class="footer-meta">© {currentYear} {companyName}. Alle Rechte vorbehalten.</div>
</footer>
<style lang="less">
@import "../../lib/assets/css/variables.less";
.footer {
&,
& * {
box-sizing: border-box;
}
background: var(--neutral-white);
.site-footer {
background-color: var(--bg-200, #0d0d0d);
color: var(--neutral-white);
padding: 3rem 1.5rem 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
position: relative;
gap: 2rem;
}
.footer-inner {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 2rem;
width: 100%;
gap: 1.5rem;
#content-section {
padding: 0px var(--horizontal-default-margin);
max-width: var(--small-max-width);
width: 100%;
display: flex;
flex-direction: row;
gap: 1.5rem;
align-items: flex-start;
justify-content: flex-start;
position: relative;
#content-link-section {
display: flex;
flex-direction: row;
gap: 1.5rem;
align-items: flex-start;
justify-content: flex-start;
flex: 1;
position: relative;
.content {
display: flex;
flex-direction: column;
gap: 1.125rem;
align-items: flex-start;
justify-content: flex-start;
width: 8rem;
position: relative;
}
}
@media @mobile {
flex-direction: column;
align-items: flex-start;
#content-link-section {
flex-direction: column;
}
}
max-width: var(--body-maxwidth);
margin: 0 auto;
}
.footer-column {
display: flex;
flex-direction: column;
gap: 0.75rem;
h2,
h3 {
margin: 0;
font-size: 1.2rem;
font-weight: 600;
}
#newsletter-section {
ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 18px;
min-width: 300px;
form {
button {
width: fit-content;
}
}
}
#icons-section {
padding: 0px var(--horizontal-default-margin);
max-width: var(--small-max-width);
width: 100%;
margin: 0 0 0 -0.0625rem;
display: flex;
gap: 0.625rem;
align-items: flex-start;
justify-content: flex-start;
flex: 1;
position: relative;
overflow: hidden;
.payments,
.social {
display: flex;
gap: 0.75rem;
figure {
height: 1.2rem;
img {
height: 100%;
width: auto;
}
}
}
.line {
padding: 0rem 1.5rem 0rem 1.5rem;
display: flex;
flex-direction: row;
gap: 1.5rem;
align-items: center;
justify-content: flex-start;
flex: 1;
position: relative;
overflow: hidden;
.line-1,
.line-2 {
border-style: solid;
border-color: var(--text-invers-100);
border-width: 0.0625rem 0 0 0;
flex: 1;
height: 0rem;
position: relative;
}
}
@media @mobile {
flex-direction: column-reverse;
align-items: center;
gap: 2rem;
.line {
width: 100%;
padding: 0px;
}
}
gap: 0.5rem;
}
#legal-section {
li {
display: flex;
justify-content: center;
width: 100%;
background: var(--bg-100);
gap: 1.2rem;
.wrapper {
max-width: var(--small-max-width);
}
}
width: 100%;
a,
small {
color: var(--text-100);
}
.footer-link,
.footer-column a {
color: inherit;
text-decoration: none;
transition: opacity 0.2s ease;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
position: relative;
gap: 1.2rem;
padding: 1.5rem var(--horizontal-default-margin);
@media @mobile {
flex-direction: column;
align-items: flex-start;
}
&:hover {
opacity: 0.75;
}
}
.nav-points {
ul {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
align-items: center;
a {
font-weight: 400;
font-family: Outfit;
}
}
}
}
.footer-social {
flex-direction: row;
gap: 0.75rem;
a {
text-transform: capitalize;
font-size: 0.9rem;
font-weight: 500;
}
}
.footer-placeholder {
font-size: 0.85rem;
opacity: 0.7;
}
.footer-meta {
text-align: center;
font-size: 0.85rem;
opacity: 0.6;
}
@media (max-width: 640px) {
.site-footer {
padding: 2.4rem 1.2rem 1.2rem;
}
.footer-social {
flex-wrap: wrap;
}
}
</style>

View File

@@ -4,7 +4,7 @@
import Icon from "./widgets/Icon.svelte"
import Notifications from "./widgets/Notifications.svelte"
import { isMobile } from "../store"
import { changeStateOfSite } from "./header/Desktop.svelte"
import { enableScrolling, stopScrolling } from "../functions/utils"
export let show: boolean = false,
size: string = "md",
@@ -13,6 +13,7 @@
let dialog: HTMLDialogElement,
dispatch = createEventDispatcher()
const scrollPosition = { top: 0, left: 0 }
const onCancel = (e: any) => {
show = false
@@ -30,19 +31,19 @@
$: if (dialog)
if (show) {
dialog.showModal()
changeStateOfSite(true)
dialog.classList.add("dialog-open")
stopScrolling(scrollPosition)
} else if (dialog.classList.contains("dialog-open")) {
changeStateOfSite(false)
dialog.classList.remove("dialog-open")
dialog.close()
enableScrolling(scrollPosition)
dispatch("close")
}
onMount(() => {
return () => {
dialog.classList.remove("dialog-open")
changeStateOfSite(false)
enableScrolling(scrollPosition)
}
})
</script>
@@ -53,7 +54,6 @@
bind:this={dialog}
on:cancel={onCancel}
on:click|stopPropagation={onDialogClick}
on:keypress
data-cy="modal"
>
{#if $$slots.title}
@@ -85,7 +85,7 @@
>
<Icon
path={mdiCloseCircleOutline}
size={$isMobile ? 24 : 32}px"
size={$isMobile ? "24px" : "32px"}
/>
</button>
</span>

View File

@@ -1,428 +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 { mdiCloseCircleOutline } from "@mdi/js"
import { isMobile, overlays } from "../store"
import Icon from "./widgets/Icon.svelte"
import { changeStateOfSite } from "./header/Desktop.svelte"
$: changeStateOfSite(!!$overlays.length)
function checkIfNoOverlayIsActive(overlays: Array<Overlay>) {
if (!overlays.length) return false
return overlays.every((overlay) => !overlay?.active)
}
$: if (checkIfNoOverlayIsActive($overlays)) {
if ($overlays.some((o) => o.hideTillDispatch)) {
// make the one before it active
const hidden = $overlays.findIndex((o) => o.hideTillDispatch)
if (hidden > 0) {
$overlays[hidden - 1].active = true
} else {
$overlays[1].active = true
}
} else {
const lastIndex = $overlays.length - 1
$overlays[lastIndex].active = true
}
}
function closeModal(i: number) {
const removedElements = $overlays.splice(i, $overlays.length - i)
const hasActive = removedElements.some((element) => element.active === true)
if (hasActive && $overlays.length > 0) {
$overlays[$overlays.length - 1].active = true
}
$overlays = $overlays
dispatched = dispatched.filter((dispatch, idx) => idx !== i)
}
let dispatched: boolean[] = []
</script>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div
class:visible={$overlays.length}
class="sidebarOverlay"
id="overlay"
role="dialog"
aria-labelledby="overlayTitle"
aria-label="Overlay"
aria-modal="true"
on:click={() => {
closeModal(0)
}}
on:keydown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
closeModal(0)
}
}}
>
<section class="dark-side"></section>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<ul
class="content-side"
on:click|stopPropagation
>
{#each $overlays as overlay, i}
<li
class="content-block"
class:active={overlay.active}
class:dark={(i + 1) % 2 == 0}
class:hideTillDispatch={overlay.hideTillDispatch}
class:hideIt={!dispatched[i]}
>
<header
role="button"
tabindex="0"
on:click={() => {
$overlays = $overlays.map((o, index) => {
o.active = index === i
return o
})
}}
on:keydown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
$overlays = $overlays.map((o, index) => {
o.active = index === i
return o
})
}
}}
>
<div
class="upper-bar"
class:noBg={i === $overlays.length - 1 ||
$overlays.filter((o) => o.hideTillDispatch).length > dispatched.filter(Boolean).length}
>
<div class="crinkle desktop-crinkle">
<img
src="/media/ModalCrinkle{(i + 1) % 2 == 0 ? 'Dark' : ''}.svg"
alt="crinkle"
/>
</div>
<div class="crinkle mobile-crinkle">
<img
src="/media/ModalCrinkle{(i + 1) % 2 == 0 ? 'Dark' : ''}Mobile.svg"
alt="crinkle"
/>
</div>
{#if typeof overlay.title === "string"}
<h2 id="overlayTitle">
{overlay.title}
</h2>
{:else}
<svelte:component this={overlay.title} />
{/if}
<div class="close">
<button
aria-label="Close"
on:click={() => {
closeModal(i)
}}
>
<Icon
path={mdiCloseCircleOutline}
size={$isMobile ? '24px' : '32px'}
/>
</button>
</div>
<div></div>
</div>
<div class="lower-bar"></div>
</header>
<div
class="scroll-container"
class:active={overlay.active}
data-simplebar
>
<div class="content-listing">
{#key overlay.reload}
<svelte:component
this={overlay.content}
on:showOverlay={(e) => {
dispatched[i] = true
}}
on:removeOverlay={() => {
closeModal(i)
}}
darkBg={(i + 1) % 2 == 0}
{...overlay.properties}
/>
{/key}
</div>
</div>
</li>
{/each}
</ul>
</div>
<style lang="less">
@import "../assets/css/variables.less";
:global .sidebarOverlay {
position: fixed;
left: 0px;
right: 0;
width: 100vw;
height: 100%;
top: 0px;
overflow: hidden;
z-index: 9999;
background: rgba(0, 0, 0, 0.2);
display: flex;
align-items: flex-end;
bottom: -200%;
top: 200%;
&.visible {
bottom: 0px;
top: 0px;
left: 0px;
}
@media @desktop {
top: 0px;
bottom: 0px;
right: -100vw;
left: 100vw;
&.visible {
right: 0px;
}
}
.dark-side {
flex-grow: 1;
height: 100%;
}
.content-side {
width: 100vw;
display: flex;
align-items: flex-end;
flex-direction: column-reverse;
@media @desktop {
flex-direction: row-reverse;
}
height: 100%;
@media @mobile {
height: 70%;
}
.content-block {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
&.hideTillDispatch {
&.hideIt {
display: none;
}
}
header {
.upper-bar {
width: 100%;
display: flex;
height: 88px;
background-color: var(--bg-100);
&.noBg {
background-color: transparent;
}
.crinkle {
height: 100%;
width: auto;
margin-right: -1px;
}
.mobile-crinkle {
display: none;
}
@media @mobile {
height: 70px;
.desktop-crinkle {
display: none;
}
.mobile-crinkle {
display: block;
}
}
h2 {
background-color: var(--neutral-white);
flex-grow: 1;
color: var(--text-invers-100);
display: flex;
align-items: flex-end;
font-weight: 700;
}
.close {
background-color: var(--neutral-white);
padding-right: 27px;
color: var(--text-invers-100);
display: flex;
align-items: flex-end;
button {
color: var(--text-invers-100);
transform: translate(-50%, 25%);
}
}
}
.lower-bar {
height: 1.2rem;
background-color: white;
width: 100%;
}
}
&.active {
@media @mobile {
height: unset;
max-height: calc(100vh - 200px);
flex-grow: 1;
}
}
&:not(.active) {
@media @mobile {
height: unset;
}
}
.simplebar-content {
height: 100%;
@media @mobile {
padding-top: 12px !important;
}
}
.simplebar-track {
@media @mobile {
margin-top: 12px !important;
margin-bottom: 12px;
}
}
// if only one child, put it to 100%
&:only-child {
height: 100%;
}
@media @desktop {
width: 50vw;
border-left: 1px solid var(--bg-invers-100);
}
@media @large_desktop {
width: max(30vw, 628px);
}
.scroll-container {
height: calc(100% - 88px - 1.2rem);
max-height: 100%;
padding-right: 1.6rem;
transition: height 0.3s;
flex-grow: 1;
background-color: var(--neutral-white);
padding-top: 2.4rem;
@media @mobile {
&:not(.active) {
height: 0px;
max-height: 0px;
padding-top: 0px;
overflow: hidden;
}
}
.simplebar-track {
background-color: rgba(13, 12, 12, 0.25);
width: 7px;
overflow: visible;
margin-left: 5px;
margin-right: 1.6rem;
margin-top: 2.4rem;
@media @mobile {
margin-right: 1rem;
}
}
.simplebar-scrollbar {
transition-duration: 0ms !important;
cursor: pointer;
&::before {
background-color: var(--bg-100);
top: -2px;
left: -2px;
opacity: 1;
border-radius: 0;
width: 11px;
height: calc(100% + 2px);
transition-delay: 0s;
}
}
}
.content-listing {
width: 100%;
height: 100%;
box-sizing: border-box;
background-color: var(--neutral-white);
display: flex;
flex-direction: column;
flex-grow: 1;
}
&.dark {
header {
.upper-bar {
background-color: var(--neutral-white);
&.noBg {
background-color: transparent;
}
h2 {
background-color: var(--bg-100);
color: var(--text-100);
}
.close {
background-color: var(--bg-100);
button {
color: var(--text-100) !important;
transform: translate(25%, 25%);
@media @mobile {
transform: translate(-50%, 25%);
}
svg {
fill: var(--text-100);
}
}
}
}
.lower-bar {
background-color: var(--bg-100);
}
}
.scroll-container {
.simplebar-content {
padding-left: 90px !important;
padding-right: 2.8rem !important;
@media @mobile {
padding-left: 38px !important;
}
}
background-color: var(--bg-100);
.simplebar-track {
background-color: rgba(255, 255, 255, 0.2);
}
.simplebar-scrollbar {
transition-duration: 0ms !important;
cursor: pointer;
&::before {
background-color: white;
}
}
}
.content-listing {
background-color: var(--bg-100);
color: var(--text-100);
}
}
}
}
}
</style>

View File

@@ -1,476 +0,0 @@
<script
context="module"
lang="ts"
>
import { enableScrolling, stopScrolling } from "../../functions/utils"
let scrollPosition ={ top: 0, left: 0 }
let whiteHeader = false
export function changeStateOfSite(menuOn: boolean) {
if (typeof window !== "undefined") {
if (menuOn) {
// make sure its not already in thsi state
const state = document.body.classList.contains("no-scroll")
if (state) {
return
}
stopScrolling(scrollPosition)
} else {
// make sure only if it is in this state
const state = document.body.classList.contains("no-scroll")
if (!state) {
return
}
// Remove the no-scroll class and reset the position
enableScrolling(scrollPosition)
}
}
}
</script>
<script lang="ts">
import { mdiAccountOutline, mdiHeartOutline, mdiMenu, mdiShoppingOutline } from "@mdi/js"
import { spaLink } from "../../actions"
import Icon from "../widgets/Icon.svelte"
import { icons } from "../../../config"
import MobileMenu from "./MobileMenu.svelte"
import { overlays } from "../../store"
import { showCartOverlay, showFavoriteOverlay } from "../../functions/helper/product"
export let elements: NavigationElement[],
bannerVisible: boolean,
scrolled: boolean,
activeSubmenu = -1
async function changeSubmenu(index: string | number) {
changeStateOfSite(true)
const submenuContainers = document.getElementsByClassName("submenu-container")
Array.from(submenuContainers).forEach((container) => container.classList.remove("shown"))
const invisibleMenu = document.getElementById(`submenu-${index}`)
invisibleMenu.classList.add("shown")
activeSubmenu = Number(index)
let headerContainer = document.getElementById("header-container")
headerContainer.classList.add("scrolled")
}
function closeSubmenu() {
const submenuContainers = document.getElementsByClassName("submenu-container")
Array.from(submenuContainers).forEach((container) => container.classList.remove("shown"))
changeStateOfSite(false)
activeSubmenu = -1
if (window.scrollY < 100) {
let headerContainer = document.getElementById("header-container")
headerContainer.classList.remove("scrolled")
}
}
let hoverTimeout: string | number | NodeJS.Timeout | undefined
let hoverCloseTimeout: string | number | NodeJS.Timeout | undefined
let isHoveringMenu = false
function closeSubmenuWithTimeout() {
hoverCloseTimeout = setTimeout(closeSubmenu, 200)
}
const openMenu = (i: number, page: NavigationElement) => {
clearTimeout(hoverTimeout)
if (page?.elements?.length) hoverTimeout = setTimeout(() => changeSubmenu(i), 500)
else hoverTimeout = setTimeout(closeSubmenu, 500)
}
$: if (isHoveringMenu) {
clearTimeout(hoverCloseTimeout)
}
</script>
<nav
class="menu"
class:scrolled={scrolled || whiteHeader}
class:not-scrolled={!scrolled && !whiteHeader}
class:active={activeSubmenu !== -1}
on:mouseover={() => (isHoveringMenu = true)}
on:focus={() => (isHoveringMenu = true)}
on:mouseleave={() => (isHoveringMenu = false)}
>
<button
aria-label="Open menu"
id="burger-menu"
class="mobile-burger"
on:click={() => {
const overlay ={
id: 'mobile-menu',
title: 'Menu',
active: true,
content: MobileMenu,
properties: {
elements,
bannerVisible,
scrolled,
},
}
$overlays = [overlay]
}}
>
<Icon
path={mdiMenu}
size="24px"
color={'#F3EED9'}
/>
</button>
<a
href="/"
use:spaLink
class="logo-container"
on:click={closeSubmenu}
aria-label="Go to homepage"
>
<img
src="../../../../logo/logo-white.svg"
alt="logo"
/>
</a>
<ul class="menuitem-container">
{#each elements as page, i (i)}
<li
class="menu-item"
class:active={i == activeSubmenu}
on:mouseenter|stopPropagation={() => openMenu(i, page)}
on:focus|stopPropagation={() => openMenu(i, page)}
on:mouseleave|stopPropagation={(e) => clearTimeout(hoverTimeout)}
>
<div class="bar">
<span></span>
<img
src="../../../../media/BottomLeftCrinkleRed.svg"
alt="crinkle"
/>
</div>
<a
aria-label={page.name}
href={page.page}
style="display: flex; flex-direction: column; align-items: center"
use:spaLink
on:mousedown={closeSubmenu}
><div style="padding: 0px; line-height: 100%;">{page.name}</div>
{#if page.elements?.length}
<Icon
path={icons.chevronDown}
color={'#F3EED9'}
props={{
'fill-rule': 'evenodd',
'clip-rule': 'evenodd',
}}
/>
{/if}
</a>
</li>
{/each}
</ul>
<ul class="icons-container">
<li class="menu-item">
<button
on:click={showFavoriteOverlay}
aria-label="Open favorites"
><Icon
color={'#F3EED9'}
size="24px"
path={mdiHeartOutline}
/></button
>
</li>
</ul>
</nav>
{#each elements as submenu, i (submenu.name)}
{#if submenu.elements?.length}
<section
role="menu"
tabindex="0"
on:mouseleave={() => !isHoveringMenu && closeSubmenuWithTimeout()}
on:focus|stopPropagation
class="submenu-container"
id={`submenu-${i}`}
style="top: 86px"
>
<ul class="sub-menu-columns">
{#each submenu?.elements.slice(0, 2) as submenu_title, i (i)}
<li class="column">
<a
use:spaLink
href={submenu_title.page}
class="submenu-title"
class:bold={submenu_title?.elements?.length}
on:click={closeSubmenu}>{submenu_title?.name}</a
>
{#if submenu_title?.elements?.length}
<ul class="sub-menu-rows">
{#each submenu_title?.elements || [] as submenu_point, j (j)}
<li>
<a
use:spaLink
href={submenu_point.page}
class="submenu-endpoint"
on:click={closeSubmenu}>{submenu_point?.name}</a
>
</li>
{/each}
</ul>
{/if}
</li>
{/each}
{#if submenu?.elements?.length > 2}
<li class="column">
<ul class="sub-menu-rows large-gap">
{#each submenu.elements.slice(2) as submenu_title, k (k)}
<li>
<a
use:spaLink
href={submenu_title.page}
class="submenu-title"
class:bold={submenu_title?.elements?.length}
on:click={closeSubmenu}>{submenu_title?.name}</a
>
{#if submenu_title?.elements?.length}
<ul class="sub-menu-rows">
{#each submenu_title?.elements || [] as submenu_point, l (l)}
<li>
<a
use:spaLink
href={submenu_point.page}
class="submenu-endpoint"
on:click={closeSubmenu}>{submenu_point?.name}</a
>
</li>
{/each}
</ul>
{/if}
</li>
{/each}
</ul>
</li>
{/if}
</ul>
<div class="img-placeholder"></div>
<section class="image-wrapper">
<div class="inner-wrapper">
<img
src="../../../../media/main-nav-pic.webp"
alt="img"
/>
<svg
width="10px"
height="50px"
viewBox="0 0 1 5"
xmlns="http://www.w3.org/2000/svg"
>
<polygon
points="0,0 1,0 0,5"
fill="white"></polygon>
</svg>
</div>
</section>
</section>
{/if}
{/each}
<style lang="less">
@import "../../assets/css/variables.less";
.menu {
max-width: var(--normal-max-width);
width: 100%;
display: flex;
height: 86px;
align-items: center;
justify-content: space-between;
&.not-scrolled a {
color: var(--text-100);
}
a {
color: var(--text-100);
}
.logo-container {
height: 64px;
display: flex;
align-items: flex-start;
img {
height: 60px;
width: auto;
object-fit: contain;
}
}
@media @desktop {
#burger-menu {
display: none;
}
}
.menuitem-container {
display: flex;
height: 100%;
justify-content: flex-start;
align-items: flex-end;
width: fit-content;
gap: 1.2rem;
padding-top: 0.55rem;
height: 100%;
.menu-item {
height: 50px;
padding: 0px 1.2rem 0.6rem 1.2rem;
position: relative;
.bar {
display: flex;
opacity: 0;
position: absolute;
top: -12px;
left: 0;
right: 0;
justify-content: flex-end;
span {
background-color: var(--primary-100);
flex-grow: 1;
margin-right: -2px;
}
img {
width: 12.169px;
height: 12px;
}
}
&.active {
.bar {
opacity: 1;
}
a {
color: var(--text-100);
}
background-color: var(--primary-100);
}
}
@media @max-tablet {
display: none;
}
}
.icons-container {
display: flex;
justify-content: flex-end;
margin-top: 14px;
gap: 1.2rem;
@media @mobile {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.15rem;
.menu-item {
&:nth-of-type(1) {
margin-right: 0.15rem;
}
&:nth-of-type(2) {
margin-left: 0.15rem;
}
}
.menu-item:nth-child(3) {
grid-column: span 2;
display: flex;
justify-content: center;
}
}
}
}
.submenu-container {
min-height: 22.3rem;
min-width: 100vw;
border-top: 4px solid var(--primary-100);
height: 0px;
z-index: 1000;
max-width: var(--normal-max-width);
background-color: var(--neutral-white);
display: flex;
justify-content: flex-start;
left: -100vw;
transition: left 300ms, opacity 300ms;
opacity: 0;
position: absolute;
overflow-y: hidden;
:global &.shown {
opacity: 1 !important;
left: 0px !important;
right: 0px;
height: fit-content;
padding-left: calc((100vw - var(--normal-max-width)) / 2);
@media (max-width: 1920px) {
padding-left: var(--horizontal-default-margin);
}
}
.sub-menu-columns {
padding: 2.4rem 0rem;
display: flex;
align-items: start;
gap: 2.4rem;
flex-wrap: wrap;
.column {
display: flex;
flex-direction: column;
min-width: 180px;
gap: 1.2rem;
.submenu-title {
&.bold {
text-transform: uppercase;
font-weight: 700;
font-style: Outfit-bold, sans-serif;
}
}
.sub-menu-rows {
display: flex;
flex-direction: column;
gap: 0.9rem;
&.large-gap {
gap: 2.4rem;
}
li {
.submenu-endpoint {
font-weight: 400 !important;
font-style: Outfit-normal, sans-serif !important;
}
display: flex;
flex-direction: column;
gap: 1.2rem;
}
}
}
}
.img-placeholder {
height: 100%;
width: 100%;
max-width: 55%;
}
.image-wrapper {
position: absolute;
right: 0px;
max-width: 50%;
height: 100%;
.inner-wrapper {
position: relative;
height: 100%;
width: 100%;
img {
height: 100%;
object-fit: cover;
}
svg {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: auto;
opacity: 1;
}
}
}
}
</style>

View File

@@ -1,125 +1,247 @@
<script lang="ts">
import { onMount } from "svelte"
import { getDBEntry } from "../../../api"
import { navigationCache, location, categories, overlays } from "../../store"
import DesktopHeader from "./Desktop.svelte"
import {
getBCGraphCategories,
mapBigcommerceCategoriesToNavigation,
} from "../../functions/CommerceAPIs/bigCommerce/categories"
import Banner from "../widgets/Banner.svelte"
import { getDBEntries } from "../../../api"
import { spaLink } from "../../actions"
import { location } from "../../store"
let navigationElements: NavigationElement[] = [],
navOpen = false,
subNavOpen: { [key: number]: boolean } ={},
windowWidth: number
const NAVIGATION_TYPE = {
Main: 0,
Service: 1,
Legal: 2,
} as const
function elementsToCache(elements: NavigationElement[]) {
elements.forEach((el) => {
if (!el.external) {
if (!$navigationCache[el.page]) $navigationCache[el.page] = el
if (el.elements?.length > 0) elementsToCache(el.elements)
}
})
let navigationEntries: NavigationEntry[] = []
let isMenuOpen = false
let loadingNavigation = true
const resolveHref = (item: NavigationElement) => {
const base = item.page || "/"
const hash = item.hash ? (item.hash.startsWith("#") ? item.hash : `#${item.hash}`) : ""
return `${base}${hash}`
}
getDBEntry("navigation", {
tree: 0,
}).then((nav) => {
navigationElements.push(...nav.elements)
navigationElements = navigationElements
})
$: if (!navOpen) subNavOpen ={}
$: if ($location) navOpen = false
let scrolled: boolean = false,
isHomepage: boolean = false,
bannerVisible = false
$: isHomepage = !$location.path || $location.path === "/"
function checkScroll() {
scrolled = window.scrollY >= 100
const isActive = (item: NavigationElement) => {
const target = resolveHref(item)
const [path] = target.split("#")
return path === $location.path
}
onMount(() => {
if (typeof window !== "undefined") {
checkScroll()
window.addEventListener("scroll", checkScroll)
return () => {
window.removeEventListener("scroll", checkScroll)
}
const closeMenu = () => {
isMenuOpen = false
}
const toggleMenu = () => {
isMenuOpen = !isMenuOpen
}
onMount(async () => {
try {
const entries = await getDBEntries("navigation")
navigationEntries = entries ?? []
} catch (error) {
console.error("Unable to load navigation", error)
} finally {
loadingNavigation = false
}
})
$: {
if (typeof window !== "undefined" && $location) {
checkScroll()
}
}
let activeSubmenu = -1
$: darkBG = isHomepage ? (scrolled ? true : activeSubmenu >= 0 || $overlays?.length) : false
$: mainNavigation = navigationEntries.find((entry) => Number(entry.type) === NAVIGATION_TYPE.Main)
</script>
<svelte:window bind:innerWidth={windowWidth} />
<Banner bind:isVisible={bannerVisible} />
<header
class="headercontainer"
id={'header-container'}
class:scrolled={darkBG}
class:homepageHeader={isHomepage}
class:bannerVisible={bannerVisible}
role="dialog"
aria-label="Hauptnavigation"
on:focus
>
<div class="padding">
{#key [bannerVisible]}
<DesktopHeader
bind:activeSubmenu={activeSubmenu}
elements={navigationElements}
bannerVisible={bannerVisible}
scrolled={darkBG}
/>
{/key}
<header class="site-header" aria-label="Primäre Navigation">
<div class="header-inner">
<a
class="brand"
href="/"
use:spaLink
on:click={closeMenu}
>
Kontextwerk
</a>
<button
class="menu-toggle"
aria-expanded={isMenuOpen}
aria-controls="primary-navigation"
on:click={toggleMenu}
>
<span class="sr-only">Navigation umschalten</span>
<span class="bar"></span>
<span class="bar"></span>
<span class="bar"></span>
</button>
<nav
id="primary-navigation"
class:open={isMenuOpen}
>
{#if loadingNavigation}
<span class="nav-placeholder">Navigation wird geladen …</span>
{:else if mainNavigation?.elements?.length}
<ul>
{#each mainNavigation.elements as item (item.name)}
<li class:active={isActive(item)}>
{#if item.external && item.externalUrl}
<a
href={item.externalUrl}
target="_blank"
rel="noopener noreferrer"
on:click={closeMenu}
>
{item.name}
</a>
{:else}
<a
href={resolveHref(item)}
use:spaLink
on:click={closeMenu}
>
{item.name}
</a>
{/if}
</li>
{/each}
</ul>
{:else}
<span class="nav-placeholder">Keine Navigationspunkte verfügbar</span>
{/if}
</nav>
</div>
</header>
<style lang="less">
@import "../../assets/css/variables.less";
@desktop: ~"only screen and (min-width: 1440px)";
.headercontainer {
display: flex;
flex-direction: column;
@media @mobile {
overflow: hidden;
}
.site-header {
position: sticky;
z-index: 5500;
top: 0px;
justify-content: space-between;
background-color: #0d0c0c;
top: 0;
z-index: 1000;
background-color: var(--neutral-white);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.header-inner {
max-width: var(--body-maxwidth);
margin: 0 auto;
padding: 1.2rem 1.6rem;
display: flex;
align-items: center;
width: 100%;
.padding {
width: 100%;
padding: 0px var(--horizontal-default-margin);
height: 100%;
justify-content: space-between;
gap: 1.2rem;
}
.brand {
font-size: 1.4rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-invers-100);
text-decoration: none;
}
.menu-toggle {
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 0.3rem;
padding: 0.6rem;
border: none;
background: none;
cursor: pointer;
.bar {
width: 1.6rem;
height: 2px;
background-color: var(--text-invers-100);
transition: transform 0.2s ease;
}
}
nav {
ul {
list-style: none;
display: flex;
align-items: flex-end;
justify-content: center;
gap: 1.2rem;
margin: 0;
padding: 0;
}
&.homepageHeader {
background-color: transparent;
li {
position: relative;
&.active a::after {
transform: scaleX(1);
}
}
&.scrolled&.homepageHeader {
box-shadow: 0px 10px 20px 0px rgba(0, 0, 0, 0.2);
background-color: var(--bg-100);
a {
text-decoration: none;
font-weight: 500;
color: var(--text-100);
padding: 0.3rem 0;
position: relative;
display: inline-flex;
&::after {
content: "";
position: absolute;
left: 0;
bottom: -0.3rem;
width: 100%;
height: 2px;
background-color: var(--accent-100, #c4102d);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.2s ease;
}
&:hover::after {
transform: scaleX(1);
}
}
.nav-placeholder {
font-size: 0.875rem;
color: var(--text-60);
}
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@media (max-width: 960px) {
.menu-toggle {
display: flex;
}
nav {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: var(--neutral-white);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
transform: translateY(-110%);
transition: transform 0.2s ease;
padding: 1.2rem 1.6rem;
&.open {
transform: translateY(0);
}
ul {
flex-direction: column;
align-items: flex-start;
gap: 0.8rem;
}
}
}
</style>

View File

@@ -1,261 +0,0 @@
<script
context="module"
lang="ts"
>
import { overlays } from "../../store"
import MobileMenuSubCategory from "./MobileMenuSubCategory.svelte"
export function openSubmodule(element: NavigationElement) {
overlays.update((o) => {
const openedAtLvl = o.findIndex((overlay) => overlay.active)
let overlays = o.map((overlay) => {
overlay.active = false
return overlay
})
overlays.splice(openedAtLvl + 1, overlays.length - openedAtLvl - 1)
const subMenuOverlay: Overlay ={
id: element.name,
title: element.name,
active: true,
content: MobileMenuSubCategory,
properties: {
element,
},
}
return [...overlays, subMenuOverlay]
})
}
</script>
<script lang="ts">
import { mdiChevronRight } from "@mdi/js"
import { spaLink, spaNavigate } from "../../actions"
import Icon from "../widgets/Icon.svelte"
import { socialIcons } from "../../../config"
export let elements: NavigationElement[]
const mainCategories = elements.filter((el) => el.type === "bigcommerce")
let selectedCategory = mainCategories[0]
const contentCategories = elements.filter((el) => el.type === "content")
//menu übereinander stacken
</script>
<section id="mobileMenu">
<section id="commerceCategories">
<ul id="mainCategories">
{#each mainCategories as mainCategory}
<li>
<button
class="cta"
class:active={selectedCategory.name === mainCategory.name}
on:click={() => {
selectedCategory = mainCategory
}}
>
{mainCategory.name}
</button>
</li>
{/each}
</ul>
<ul id="subCategories">
{#each selectedCategory.elements as subCategory}
<li>
<button
class="cta"
on:click={() => {
openSubmodule(subCategory)
}}
>
<p>{subCategory.name}</p>
<Icon path={mdiChevronRight} />
</button>
</li>
{/each}
</ul>
</section>
<section id="contentCategories">
<h4>Mehr Entdecken</h4>
<ul>
{#each contentCategories as contentCategory}
{#if !!contentCategory.name}
<li>
<button
class="cta"
on:click={() => {
if (contentCategory.elements?.length) openSubmodule(contentCategory)
else spaNavigate(contentCategory.page)
}}
>
<p>{contentCategory.name}</p>
</button>
</li>
{/if}
{/each}
</ul>
</section>
<section class="additionalLinks">
<ul>
<li>
<a
use:spaLink
href="/profile">Account</a
>
</li>
<li>
<a
use:spaLink
href="/widerrufsbelehrung">Retoure & Rückerstattung</a
>
</li>
</ul>
</section>
<section class="socialMediaLinks">
<ul class="social">
{#each Object.keys(socialIcons) as icon}
<li>
<a
href={socialIcons[icon]}
use:spaLink
target="_blank"
aria-label={icon}
>
<figure class="footer-icon">
<img
alt={icon}
src="../../../media/{icon}.svg"
/>
</figure></a
>
</li>
{/each}
</ul>
</section>
</section>
<style lang="less">
@import "../../assets/css/variables.less";
#mobileMenu {
width: 100%;
padding: 24px 1rem 24px 90px;
@media @mobile {
padding: 24px 2rem 24px 38px;
}
display: flex;
flex-direction: column;
gap: 2.4rem;
button {
color: var(--text-invers-100);
width: 100%;
text-transform: uppercase;
font-family: "Outfit-Bold", sans-serif;
font-weight: 700;
font-size: 1rem;
}
#commerceCategories {
display: flex;
flex-direction: column;
gap: 32px;
#mainCategories {
display: flex;
gap: 0px;
width: 100%;
li {
width: 0px;
flex-grow: 1;
button {
background-color: transparent;
border: 1px solid var(--bg-100);
&.active {
background-color: var(--primary-100);
color: var(--text-100);
}
}
&:nth-child(1) {
button {
border-right: 0px solid black;
}
}
&:nth-child(2) {
button {
border-left: 0px solid black;
}
}
}
}
#subCategories {
display: flex;
flex-direction: column;
gap: 12px;
li {
button {
background-color: var(--bg-300);
display: flex;
justify-content: space-between;
align-items: center;
}
}
}
}
#contentCategories {
display: flex;
flex-direction: column;
gap: 12px;
h4 {
font-family: "Outfit-Bold", sans-serif;
font-weight: 700;
font-size: 1rem;
color: var(--text-invers-100);
}
ul {
display: flex;
flex-direction: column;
gap: 12px;
li {
button {
background-color: var(--bg-300);
display: flex;
justify-content: flex-start;
}
}
}
}
.additionalLinks {
ul {
display: flex;
flex-direction: column;
gap: 12px;
li {
a {
display: flex;
justify-content: flex-start;
align-items: center;
}
}
}
}
.socialMediaLinks {
ul {
display: flex;
gap: 1.2rem;
justify-content: center;
li {
a {
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
}
</style>

View File

@@ -1,61 +0,0 @@
<script lang="ts">
import { mdiChevronRight } from "@mdi/js"
import { spaNavigate } from "../../actions"
import { openSubmodule } from "./MobileMenu.svelte"
import Icon from "../widgets/Icon.svelte"
import { overlays } from "../../store"
export let element: NavigationElement, darkBg: boolean
</script>
<ul
class="subCategory"
class:darkBg={darkBg}
>
{#each element.elements as subCategory}
<li>
<button
class="cta"
on:click={() => {
if (subCategory.elements?.length) openSubmodule(subCategory)
else {
$overlays = []
spaNavigate(subCategory.page)
}
}}
>
<p>{subCategory.name}</p>
{#if subCategory.elements?.length}
<Icon path={mdiChevronRight} />
{/if}
</button>
</li>
{/each}
</ul>
<style lang="less">
.subCategory {
display: flex;
flex-direction: column;
gap: 12px;
li {
button {
background-color: var(--bg-300);
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
}
}
&.darkBg {
background-color: transparent;
button {
background: var(--bg-200);
color: var(--text-100);
p {
color: var(--text-100);
}
}
}
}
</style>

View File

@@ -1,36 +0,0 @@
<script lang="ts">
import { spaLink } from "../../actions"
import { navigationCache } from "../../store"
export let location: LocationStore = undefined
let paths: string[] = []
$: if (location?.path?.match(/\/[^\/]+\//)) {
let _paths: string[] = []
let _p = location.path
while (_p || _p.includes("/")) {
_paths.push(_p)
// remove last part
_p = _p.replace(/\/[^\/]+\/?$/, "")
}
paths = _paths.reverse()
} else {
paths = []
}
</script>
<!-- only for paths with more levels -->
{#if paths.length}
<nav aria-label="Breadcrumbs" class="breadcrumbs">
<div class="container vp-m">
<ol>
{#each paths as path}
{#if $navigationCache[path]}
<li><a use:spaLink href={path}>{$navigationCache[path].name}</a></li>
{/if}
{/each}
</ol>
</div>
</nav>
{/if}

View File

@@ -1,301 +0,0 @@
<script lang="ts">
import { apiBaseOverride, location } from "../../store"
import blocks from "./blocks"
import CrinkledSection from "../CrinkledSection.svelte"
import MedialibImage from "../widgets/MedialibImage.svelte"
import Button from "../widgets/Button.svelte"
export let block: ContentBlock,
apiBase: string = null,
verticalPadding = true,
noHorizontalMargin = false
if (apiBase) $apiBaseOverride = apiBase
$: blockComponent = blocks[block.type] || blocks.default
let scrollTargetContainer: HTMLDivElement
let lastAnchor: string
$: if (block.anchorId && typeof window !== "undefined") {
const targetAnchor = $location.hash
if (targetAnchor !== lastAnchor && scrollTargetContainer) {
lastAnchor = targetAnchor
if (targetAnchor == "#" + block.anchorId) {
setTimeout(() => {
scrollTargetContainer.scrollIntoView({ behavior: "instant", block: "start", inline: "nearest" })
setTimeout(() => {
scrollTargetContainer.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" })
}, 600)
}, 10)
}
}
}
</script>
<CrinkledSection activated={block.crinkledSection || false}>
{#if block.anchorId}
<!-- position 100px above -->
<div style="position:relative;">
<div
bind:this={scrollTargetContainer}
style="position: absolute; top: -100px"
></div>
</div>
{/if}
<section
class:additionalHeightAtBottom={block.additionalHeightBottom}
class:headerHeightUp={block?.background?.headerHeightUp}
data-type={block.type}
class="content-section minheight-{block?.background?.minHeight} {blockComponent.sectionClass} {block.background
?.color
? block?.background?.color + '-bg'
: ''}
>
{#if block.background?.image}
<div class="background-image">
<figure>
<MedialibImage id={block?.background?.image} />
{#if block?.background?.overlay}
<div class="overlay"></div>
{/if}
</figure>
</div>
{/if}
<section
class="content"
class:narrowWidth={block.contentWidth == 1}
class:normalWidth={block.contentWidth == 2}
class:fullWidth={block.contentWidth == 0}
>
<div
class="wrapper"
class:noHorizontalMargin={noHorizontalMargin}
class:noMobileHorizontalMargin={block?.background?.noHorizontalMargin}
class:verticalPadding={verticalPadding && !block?.background?.noVerticalPadding}
>
{#if block.headline || block.subline}
<div class="headline-row">
<div
class="container bp-xl"
class:darkColor={!block?.background?.image && block?.background?.color === 'white'}
>
<!-- Überschrift Element -->
{#if block.headline}
{#if block.headlineH1}
<h1 class="h2">{block.headline}</h1>
{:else}
<h2 class:doublyLined={block.doublyLined}>{block.headline}</h2>
{/if}
{/if}
{#if block.subline}
<h3>{block.subline}</h3>
{/if}
</div>
{#if block?.headlineLink}
<button class="">
<a
href={block.headlineLink}
class="headline-link"
>
{block.headlineLinkText}
</a>
</button>
{/if}
</div>
{/if}
<svelte:component
this={blockComponent.component}
block={block}
/>
<div class="buttons">
{#each block.callToActionButtons || [] as button}
<Button button={button} />
{/each}
</div>
</div>
</section>
</section>
</CrinkledSection>
<style lang="less">
@import "../../../lib/assets/css/variables.less";
.content-section {
position: relative;
width: 100%;
overflow: hidden;
&.additionalHeightAtBottom {
padding-bottom: 3rem;
}
&.headerHeightUp {
margin-top: -86px;
}
&.minheight-normal {
height: 38rem;
min-height: 75vh;
}
&.minheight-extended {
padding-top: 145px;
height: calc(38rem + 145px);
min-height: 75vh;
}
&.homepageRow {
height: calc(38rem + 145px + 96px + 96px);
max-height: 97vh;
}
&.chapterPreview {
.content {
& > .wrapper {
margin: 0px !important;
max-width: 100% !important;
width: 100% !important;
}
}
}
&.ImproveYourselfDescription {
height: unset;
.content {
.wrapper {
display: unset !important;
}
}
}
.buttons {
display: flex;
width: 100%;
justify-content: flex-start;
padding-top: 2.4rem;
}
& .content {
position: relative;
z-index: 2;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
.headline-row {
width: 100%;
display: flex;
justify-content: space-between;
& > button {
width: fit-content;
white-space: nowrap;
a {
color: var(--white-100);
text-decoration: underline;
}
}
.container {
width: 100%;
display: flex;
flex-direction: column;
gap: 1.8rem;
h2,
h1 {
width: 100%;
}
h2.doublyLined,
h3.doublyLined {
border-top: 1px solid white;
border-bottom: 1px solid white;
padding: 1.2rem 3.6rem;
margin: 1.2rem -3.6rem;
margin-top: 0px;
width: calc(100% + 7.2rem);
font-size: 1.6rem;
}
&.darkColor {
h2,
h1,
h3 {
color: var(--text-invers-100);
}
h2.doublyLined,
h3.doublyLined {
border-top: 1px solid var(--bg-100);
border-bottom: 1px solid var(--bg-100);
padding: 1.2rem 3.6rem;
margin: 1.2rem -3.6rem;
margin-top: 0px;
width: calc(100% + 7.2rem);
font-size: 1.6rem;
}
}
margin-bottom: 2.4rem;
}
}
.wrapper {
margin: 0px var(--horizontal-default-margin);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: calc(100% - var(--horizontal-default-margin));
max-width: calc(100% - var(--horizontal-default-margin));
height: 100%;
&.verticalPadding {
padding: var(--vertical-default-margin) 0px;
}
&.noHorizontalMargin {
margin: 0px;
max-width: 100%;
width: 100%;
}
&.noMobileHorizontalMargin {
@media @mobile {
margin: 0px;
max-width: 100%;
width: 100%;
}
}
}
&.normalWidth {
.wrapper {
max-width: var(--normal-max-width);
}
}
&.narrowWidth {
.wrapper {
max-width: var(--small-max-width);
}
}
}
.background-image {
z-index: 1;
top: 0px;
overflow: hidden;
left: 0px;
right: 0px;
position: absolute;
width: 100%;
height: 100%;
figure {
width: 100%;
height: 100%;
position: relative;
.overlay {
position: absolute;
z-index: 100;
background-color: rgba(32, 28, 28, 0.3);
width: 100%;
height: 100%;
top: 0px;
left: 0px;
right: 0px;
}
}
}
}
:global .background-image > figure > img {
width: 100%;
height: 100%;
object-fit: cover;
}
:global .black-bg {
background-color: var(--bg-100);
}
</style>

View File

@@ -1,126 +0,0 @@
<script lang="ts">
import { onMount } from "svelte"
import { register } from "swiper/element/bundle"
import MedialibImage from "../widgets/MedialibImage.svelte"
export let images: string[]
export let imageHoverEffect: boolean
export let forceFullHeight: boolean
register(false)
let swiper: any
onMount(async () => {
if (swiper !== undefined) {
const response = await fetch("/dist/index.css"),
cssText = await response.text(),
params ={
injectStyles: [cssText],
}
Object.assign(swiper, params)
swiper.initialize()
}
})
</script>
{#if images.length > 1}
<div class="default-swiper">
<swiper-container
bind:this={swiper}
slides-per-view="1"
loop={true}
direction="horizontal"
effect="slide"
autoplay-delay="2500"
mousewheel={true}
navigation={true}
init={false}
speed="600"
class="relative"
>
{#each images as image (image)}
<swiper-slide class="relative">
<div
class:imageHoverEffect={imageHoverEffect}
class="image-container"
>
<MedialibImage
id={image}
filter={typeof window !== 'undefined' && window.innerWidth > 500 ? 'xl' : 'm'}
/>
</div>
</swiper-slide>
{/each}
</swiper-container>
</div>
{:else if images[0]}
<div
class="image-container single flex"
class:forceFullHeight={forceFullHeight}
class:imageHoverEffect={imageHoverEffect}
>
<MedialibImage
id={images[0]}
filter={typeof window !== 'undefined' && window.innerWidth > 500 ? 'xl' : 'm'}
/>
</div>
{/if}
<style lang="less">
@import "../../assets/css/variables.less";
:global .default-swiper {
flex: 2 !important;
.swiper-button-prev,
.swiper-button-next {
transform-origin: left;
color: #333;
transform: scale(0.3);
background-color: rgba(255, 255, 255, 0.6);
top: 50%;
padding: 10px;
height: 70px;
width: 70px;
border-radius: 50px;
}
.swiper-button-prev {
left: 6%;
}
.swiper-button-next {
right: 6%;
transform-origin: right;
}
swiper-container {
width: 100%;
swiper-slide {
width: 100% !important;
}
}
}
.image-container {
max-width: 100%;
width: 100%;
height: 100%;
overflow: visible;
max-width: @body-maxwidth;
display: flex;
align-items: flex-end;
:global &.imageHoverEffect:hover img {
transform: scale(1.05);
transform-origin: center;
}
:global & > img {
width: 100%;
height: 100%;
object-fit: contain;
}
:global &.forceFullHeight img {
width: unset;
}
@media @mobile {
:global & > img {
height: 90% !important;
}
}
}
</style>

View File

@@ -1,36 +0,0 @@
<script lang="ts">
import { BarLoader, Circle } from "svelte-loading-spinners"
export let size: string,
type: "bar" | "circle" = "circle"
</script>
<div class={type}>
{#if type == "bar"}
<BarLoader
size={size}
color="#741e20"
unit="rem"
/>
{:else}
<Circle
size={size}
color="#741e20"
unit="rem"
/>
{/if}
</div>
<style lang="less">
div {
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
padding: 2.4rem 0px;
&.circle {
justify-content: center;
align-items: center;
height: 100%;
}
}
</style>

View File

@@ -1,84 +0,0 @@
<script lang="ts">
import OpenGraph from "./OpenGraph.svelte"
import Product from "./Product.svelte"
import SchemaOrg from "./SchemaORG.svelte"
import { websiteName } from "../../../../config"
export let article: boolean = false,
createdAt: Date = null,
updatedAt: Date = null,
keywords: string = "",
metaDescription: string = "",
title: string = "",
product: BKDFProduct = null,
noIndex = false,
active = true,
FAQ = false,
FAQDetails: {
question: string
answer: string
}[] = []
let pageTitle: string
keywords += ", BinKrassDuFass, BKDF, BinKrassDuFass.de"
if (title) pageTitle = `${title} - ${websiteName}`
else pageTitle = `${websiteName}`
const openGraphProps ={
article,
datePublished: createdAt,
lastUpdated: updatedAt,
metaDescription,
pageTitle,
product,
}
const schemaOrgProps ={
createdAt,
updatedAt,
article,
description: metaDescription,
title: pageTitle,
product,
FAQ,
FAQDetails,
}
</script>
<svelte:head>
<title>{pageTitle}</title>
<meta
name="description"
content={metaDescription}
/>
<meta
name="keywords"
content={keywords}
/>
<meta
property="og:site_name"
content={websiteName}
/>
{#if noIndex || active === false}
<meta
name="robots"
content="noindex, nofollow"
/>
{:else}
<meta
name="robots"
content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1"
/>
{/if}
</svelte:head>
{#if product}
<Product product={product} />
{/if}
<OpenGraph {...openGraphProps} />
<SchemaOrg {...schemaOrgProps} />

View File

@@ -1,130 +0,0 @@
<script lang="ts">
import { baseURL, companyName } from "../../../../config"
import { location } from "../../../store"
export let article: boolean = false
export let datePublished: Date | null
export let lastUpdated: Date | null
export let metaDescription: string | null
export let pageTitle: string | null
export let product: BKDFProduct
</script>
<svelte:head>
<meta
property="og:locale"
content={'de_DE'}
/>
<meta
property="og:url"
content={$location.url}
/>
<meta
property="og:type"
content={article ? 'article' : 'website'}
/>
<meta
property="og:title"
content={pageTitle}
/>
<meta
property="og:description"
content={metaDescription}
/>
{#if product}
<meta
property="og:image"
content={product.featuredImage?.url}
/>
<meta
property="og:image:width"
content={String(product.featuredImage?.width)}
/>
<meta
property="og:image:height"
content={String(product.featuredImage?.height)}
/>
<meta
property="og:image:alt"
content={product.title}
/>
<!-- Twitter Card Tags for Product -->
<meta
name="twitter:card"
content="summary_large_image"
/>
<meta
name="twitter:title"
content={pageTitle}
/>
<meta
name="twitter:description"
content={metaDescription}
/>
<meta
name="twitter:image"
content={product.featuredImage?.url}
/>
<meta
name="twitter:image:alt"
content={product.title}
/>
{:else}
<meta
property="og:image"
content={baseURL}/api/_/assets/logo/logo-blue.svg"
/>
<meta
property="og:image:width"
content="576"
/>
<meta
property="og:image:height"
content="158"
/>
<meta
property="og:image:alt"
content="BinKrassDuFass Logo"
/>
<!-- Twitter Card Tags for Default -->
<meta
name="twitter:card"
content="summary_large_image"
/>
<meta
name="twitter:title"
content={pageTitle}
/>
<meta
name="twitter:description"
content={metaDescription}
/>
<meta
name="twitter:image"
content={baseURL}/api/_/assets/logo/logo-blue.svg"
/>
<meta
name="twitter:image:alt"
content="BinKrassDuFass Logo"
/>
{/if}
{#if article}
<meta
property="article:publisher"
content={companyName}
/>
<meta
property="article:author"
content={companyName}
/>
<meta
property="article:published_time"
content={datePublished?.toISOString()}
/>
<meta
property="article:modified_time"
content={lastUpdated?.toISOString()}
/>
{/if}
</svelte:head>

View File

@@ -1,16 +0,0 @@
<script lang="ts">
import { location } from "../../../store"
export let product: BKDFProduct
</script>
<svelte:head>
<link rel="canonical" href={$location.url} />
<meta property="product:brand" content="BinKrassDuFass" />
<meta property="product:name" content={product.title} />
<meta property="product:price:amount" content={String(product.priceRange.minVariantPrice.amount)} />
<meta property="product:price:currency" content={product.priceRange.minVariantPrice.currencyCode} />
</svelte:head>

View File

@@ -1,224 +0,0 @@
<script lang="ts">
import {
baseURL,
socialIcons,
websiteName,
streetAddress,
regionAddress,
zipCode,
countryAddress,
localityAddress,
email,
} from "../../../../config"
import { location } from "../../../store"
export let createdAt: Date | string = new Date(),
updatedAt: Date | string = new Date(),
article = false,
blog = false,
FAQ = false,
FAQDetails: {
question: string
answer: string
}[] = [],
metaDescription = "",
title = "",
product: BKDFProduct
createdAt = new Date(createdAt).toLocaleDateString("de-DE", {
year: "numeric",
month: "2-digit",
day: "2-digit",
})
updatedAt = new Date(updatedAt).toLocaleDateString("de-DE", {
year: "numeric",
month: "2-digit",
day: "2-digit",
})
const defaultProps ={
author: {
"@id": `${baseURL}/#company`,
},
publisher: {
"@id": `${baseURL}/#company`,
},
inLanguage: {
"@type": "Language",
name: "German",
},
datePublished: createdAt,
dateModified: updatedAt,
isPartOf: {
"@id": `${baseURL}/#website`,
},
}
const schemaOrgEntity ={
"@type": ["Store", "Organization"],
"@id": `${baseURL}/#company`,
name: websiteName,
url: baseURL,
email,
address: {
"@type": "PostalAddress",
streetAddress,
addressLocality: localityAddress,
addressRegion: regionAddress,
postalCode: zipCode,
addressCountry: countryAddress,
},
image: {
"@type": "ImageObject",
"@id": `${baseURL}/#image`,
inLanguage: {
"@type": "Language",
name: "German",
},
contentUrl: `${baseURL}/api/_/assets/logo/logo-blue.svg`,
width: 576,
height: 158,
caption: "BinKrassDuFass Logo",
},
logo: {
"@type": "ImageObject",
"@id": `${baseURL}/#logo`,
url: baseURL,
contentUrl: `${baseURL}/api/_/assets/logo/logo-blue.svg`,
caption: "BinKrassDuFass Logo",
inLanguage: {
"@type": "Language",
name: "German",
},
},
priceRange: "$$",
location: {
"@id": location,
},
sameAs: [Object.values(socialIcons)],
}
const schemaOrgWebsite ={
"@type": "WebSite",
"@id": `${baseURL}/#website`,
url: baseURL,
name: "BKDF",
description: "BinKrassDuFass Online Store",
...defaultProps,
}
const schemaOrgWebPage ={
"@type": "WebPage",
"@id": `${$location.url}#webpage`,
url: $location.url,
name: title,
...defaultProps,
description: metaDescription,
potentialAction: [
{
"@type": "ReadAction",
target: [$location.url],
},
],
}
let schemaOrgArticle = null
if (article || blog) {
schemaOrgArticle ={
"@type": "Article",
"@id": `${$location.url}#article`,
headline: title,
description: metaDescription,
...defaultProps,
articleSection: ["blog"],
}
}
let schemaOrgBlog = null
if (blog) {
schemaOrgBlog ={
"@type": "BlogPosting",
"@id": `${$location.url}#blog`,
headline: title,
description: metaDescription,
...defaultProps,
}
}
let schemaOrgFAQ = null
if (FAQ) {
schemaOrgFAQ ={
"@type": "FAQPage",
"@id": `${$location.url}#faq`,
...defaultProps,
mainEntity: FAQDetails.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: {
"@type": "Answer",
text: faq.answer,
},
})),
}
}
let schemaOrgProduct = null
if (product)
schemaOrgProduct ={
"@context": "http://schema.org/",
"@type": "Product",
"@id": `${$location.url}/#product`,
...defaultProps,
image: {
url: product.images[0]?.url,
caption: product.images[0]?.altText,
},
name: product.title,
description: product.description,
sku: product.sku,
brand: "BinKrassDuFass",
category: product.categories[0]?.name,
mainEntityOfPage: {
"@id": `${$location.url}#webpage`,
},
releaseDate: createdAt,
dateModified: updatedAt,
url: $location.url,
offers: {
"@type": "Offer",
availability: "http://schema.org/InStock",
url: $location.url,
price: product.priceRange.minVariantPrice.amount,
priceCurrency: product.priceRange.minVariantPrice.currencyCode,
itemCondition: "NewCondition",
seller: {
"@type": "Organization",
"@id": `${baseURL}/#company`,
},
},
}
const schemaOrgArray = [
schemaOrgEntity,
schemaOrgWebsite,
schemaOrgWebPage,
schemaOrgProduct,
schemaOrgFAQ,
schemaOrgArticle,
schemaOrgBlog,
].filter(Boolean)
const schemaOrgObject ={
"@context": "https://schema.org",
"@graph": schemaOrgArray,
}
let jsonLdString = JSON.stringify(schemaOrgObject)
let jsonLdScript = `
<script type="application/ld+json">
${jsonLdString}
${"<"}/script>
`
</script>
<svelte:head>
{@html jsonLdScript}
</svelte:head>

View File

@@ -1,30 +0,0 @@
<script lang="ts">
import ColumnsColumn from "./ColumnsColumn.svelte"
export let block: ContentBlock<"columns">
</script>
<div class="container">
{#each block.columns || [] as column}
<ColumnsColumn column={column} />
{/each}
</div>
<style lang="less">
@import "../../../../lib/assets/css/variables.less";
.container {
display: flex;
gap: 1.1rem;
max-width: var(--normal-max-width);
align-items: center;
justify-content: space-between;
width: 100%;
height: 100%;
overflow: hidden;
@media @mobile {
flex-direction: column-reverse;
:global & > section {
width: 100% !important;
}
}
}
</style>

View File

@@ -1,72 +0,0 @@
<script lang="ts">
import { apiBaseOverride } from "../../../store"
import DefaultImage from "../DefaultImage.svelte"
import ChapterDescription from "./columns/ChapterDescription.svelte"
import Cta from "./columns/CTA.svelte"
import Text from "./columns/Text.svelte"
export let column: BlockColumn
export let apiBase: string = null
if (apiBase) $apiBaseOverride = apiBase
</script>
<section
class:imageMobileBackground={column.imageMobileBackground}
class={column.colWidth > 0 ? 'col-md-' + column.colWidth + ' col-12' : 'col-md-auto col-12'}
class:align-self-start={column.verticalAlign == 'top'}
class:align-self-center={column.verticalAlign == 'middle'}
class:align-self-end={column.verticalAlign == 'bottom'}
>
{#if column.type == "text"}
<Text column={column} />
{:else if column.type == "image"}
<DefaultImage
images={column.images}
imageHoverEffect={column.imageHoverEffect}
forceFullHeight={column.forceFullHeight}
/>
{:else if column.type == "cta"}
<Cta column={column} />
{:else if column.type == "chapterDescription"}
<ChapterDescription
title={column.chapterDescription.title}
type={column.chapterDescription.type}
description={column.chapterDescription.description}
/>
{/if}
</section>
<style lang="less">
@import "../../../../lib/assets/css/variables.less";
section {
width: 0px;
flex-grow: 1;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
position: relative;
z-index: 2;
@media @mobile {
&.imageMobileBackground {
position: absolute;
bottom: 0px;
width: 100%;
height: 100%;
z-index: 1;
}
}
&.align-self-start {
justify-content: flex-start;
}
&.align-self-center {
justify-content: center;
}
&.align-self-end {
justify-content: flex-end;
}
}
</style>

View File

@@ -1,26 +0,0 @@
<script lang="ts">
import CookieSet from "../../widgets/CookieSet.svelte"
const setHeight = (element: HTMLElement) => {
element.style.height = (element.offsetWidth / 16) * 9 + "px"
}
</script>
<CookieSet cookieName={'googleMaps'} textPosition={'unten'} background={'rgba(44, 44, 44, 0.4)'}>
<iframe
title="Google Maps"
use:setHeight
id="iframe"
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2512.077224708092!2d11.023318016007138!3d50.97776317955154!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x47a4729650047377%3A0xca0e03f621729448!2sWebmakers%20GmbH!5e0!3m2!1sde!2sde!4v1571866765252!5m2!1sde!2sde"
style="border:0;"
allowfullscreen={true}
loading="lazy"
referrerpolicy="no-referrer-when-downgrade"></iframe>
</CookieSet>
<style>
iframe {
width: 100%;
max-width: 100%;
border: 0;
}
</style>

View File

@@ -1,92 +0,0 @@
<script lang="ts">
import MedialibImage from "../../widgets/MedialibImage.svelte"
import Cta from "./columns/CTA.svelte"
export let block: ContentBlock<"homepage">
const hp = block.mainHomepage
</script>
<section class="homepage">
<div class="placeholder"></div>
<div class="left">
<Cta
column={{
type: 'cta',
cta: hp.cta,
}}
/>
</div>
<div class="hp-media">
<MedialibImage
id={hp.image}
filter={typeof window !== 'undefined' && window.innerWidth > 500 ? 'xl' : 'm'}
/>
</div>
</section>
<style
lang="less"
global
>
@import "../../../assets/css/variables.less";
.homepage {
display: flex;
width: 100%;
position: relative;
height: 100%;
align-items: center;
justify-content: space-between;
transition: --color 1s ease;
margin: -96px 0;
.placeholder {
}
.left {
display: flex;
flex-direction: column;
gap: 1.5rem;
position: absolute;
z-index: 10;
transform: translateY(96px);
width: 100%;
@media @mobile {
padding: 0px var(--horizontal-default-margin);
}
@media @mobile {
gap: 0.5rem;
}
}
.hp-media {
max-width: 600px;
width: 100%;
display: flex;
height: 100%;
align-items: flex-end;
flex-grow: 1;
@media @mobile {
&::before {
//shadow
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.9) 100%);
}
}
img {
height: calc(100%);
object-fit: contain;
}
@media @mobile {
width: 100%;
}
}
@media @mobile {
flex-direction: column;
justify-content: center;
}
}
</style>

View File

@@ -1,184 +0,0 @@
<script lang="ts">
import MedialibImage from "../../widgets/MedialibImage.svelte"
export let block: ContentBlock<"improveYourselfDescription">
const des = block.improveYourselfDescription
</script>
<section class="improveYourselfDescription">
<div class="img">
<MedialibImage
id={des.image}
filter="xxl"
/>
<div class="shadow"></div>
</div>
<div class="content">
<h2 class="h1">Improve Yourself</h2>
<p>{@html des.upperDescription}</p>
<ul>
<li>
<span
><svg
xmlns="http://www.w3.org/2000/svg"
width="44"
height="32"
viewBox="0 0 44 32"
fill="none"
>
<path
d="M42.5934 9.61759L21.9008 31.2762L1.20832 9.61759L10.829 0.5H32.9727L42.5934 9.61759Z"
stroke="#EB5757"></path>
</svg>
</span>
<span style="color: #EB5757;">Fitness</span>
</li>
<li>
<span
><svg
xmlns="http://www.w3.org/2000/svg"
width="44"
height="32"
viewBox="0 0 44 32"
fill="none"
>
<path
d="M23.0956 25.0301C25.4532 25.0301 27.6755 24.4466 29.625 23.4161C26.9937 28.2334 21.8812 31.5 16.0069 31.5C7.44266 31.5 0.5 24.5574 0.5 15.9931C0.5 8.2062 6.23975 1.75968 13.719 0.653784C10.8856 3.21373 9.10412 6.9181 9.10412 11.0386C9.10412 18.7659 15.3684 25.0301 23.0956 25.0301Z"
stroke="#56F2B0"></path>
</svg>
</span>
<span style="color: #56F2B0;">Entspannung</span>
</li>
<li>
<span
><svg
xmlns="http://www.w3.org/2000/svg"
width="44"
height="32"
viewBox="0 0 44 32"
fill="none"
>
<path
d="M18.4213 6.89342L18.8989 8.42622L19.3761 6.89329C20.5237 3.20667 23.8663 0.5 27.7914 0.5C32.6671 0.5 36.6326 4.44843 36.6326 9.48563C36.6326 11.9912 35.6486 13.7553 33.9735 15.7311L18.9071 31.2817L3.82337 15.7311C2.15062 13.7562 1.16675 11.9768 1.16675 9.4684C1.16675 4.43715 5.12025 0.599827 10.0049 0.599827C13.9382 0.599827 17.2753 3.21536 18.4213 6.89342Z"
stroke="#C0F256"></path>
</svg>
</span>
<span style="color: #C0F256;">Ernährung</span>
</li>
<li>
<span
><svg
xmlns="http://www.w3.org/2000/svg"
width="44"
height="32"
viewBox="0 0 44 32"
fill="none"
>
<path
d="M16 1.16101L20.157 9.87264L20.2738 10.1176L20.5429 10.153L30.1127 11.4145L23.112 18.0601L22.9152 18.2469L22.9646 18.5137L24.7221 28.005L16.2385 23.4006L16 23.2711L15.7615 23.4006L7.27786 28.005L9.03536 18.5137L9.08477 18.2469L8.88795 18.0601L1.88728 11.4145L11.4571 10.153L11.7262 10.1176L11.843 9.87264L16 1.16101Z"
stroke="#56F2F2"></path>
</svg>
</span>
<span style="color: #56F2F2;">Weiterbildung</span>
</li>
</ul>
<p>{@html des.lowerDescription}</p>
</div>
</section>
<style
lang="less"
global
>
@import "../../../assets/css/variables.less";
.improveYourselfDescription {
display: flex;
justify-content: center;
align-items: center;
gap: 2.4rem;
max-width: 100%;
height: 100%;
.img {
width: 400px;
min-width: 400px;
max-height: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
.shadow {
display: none;
}
}
.content {
height: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 2.4rem;
z-index: 99;
p {
color: var(--text-100);
}
ul {
display: flex;
width: 100%;
justify-content: space-between;
li {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.6rem;
span {
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
}
}
}
}
@media @mobile {
flex-direction: column;
padding: 0px;
.content {
ul {
flex-direction: column;
gap: 1.2rem;
li {
flex-direction: row;
span:first-child {
width: 100px;
display: flex;
justify-content: center;
}
}
}
}
.img {
max-width: 100%;
position: absolute;
bottom: 0px;
width: 100%;
height: 100%;
z-index: 1;
.shadow {
display: block;
position: absolute;
bottom: 0px;
width: 100%;
height: 100%;
background: rgba(13, 12, 12, 0.5);
z-index: 2;
}
}
}
h2 {
color: var(--primary-200);
font-weight: 700;
}
}
</style>

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +0,0 @@
<script lang="ts">
import Content from "../../../../routes/Content.svelte"
export let block: ContentBlock<"predefinedBlock">
</script>
{#if block.predefinedBlock?.id}
<Content
id={block.predefinedBlock.id}
allwaysInView
/>
{/if}

View File

@@ -1,203 +0,0 @@
<script lang="ts">
import { onMount } from "svelte"
import { getDBEntries } from "../../../../api"
import { getVariableNameForChapter } from "../../../utils"
export let block: ContentBlock<"splittedHomepage">
let selfImprovementChapters: SelfImprovementChapter[] = []
let interval: NodeJS.Timeout
let selectedChapter = -1
let currentColor = "#741e20" // Initial color
getDBEntries("selfImprovementChapter").then((res) => {
selfImprovementChapters = res
if (selfImprovementChapters.length > 0) {
selectedChapter = -1
setTimeout(() => {
// set width and height of placeholder to elements width and height
const placeholder = document.querySelector(".placeholder")
const elements = document.querySelector(".elements")
if (placeholder && elements) {
const { width, height } = elements.getBoundingClientRect()
placeholder.style.width = `${width}px`
placeholder.style.height = `${height}px`
}
}, 10)
}
})
function startInterval() {
interval = setInterval(() => {
selectedChapter = (selectedChapter + 1) % selfImprovementChapters.length
updateShadowColor(selfImprovementChapters[selectedChapter]?.color || "#000")
}, 3000)
}
function stopInterval() {
clearInterval(interval)
}
function updateShadowColor(newColor: string) {
const filter = document.getElementById("redShadow")
const feDropShadow = filter?.querySelector("feDropShadow")
if (feDropShadow) {
feDropShadow.style.transition = "flood-color 1s ease"
feDropShadow.setAttribute("flood-color", newColor)
currentColor = newColor
}
}
onMount(() => {
startInterval()
return () => clearInterval(interval)
})
</script>
<section
class="splittedHomepage"
style="--color: {currentColor}
>
<div class="placeholder"></div>
<ul class="elements">
{#each selfImprovementChapters as chapter, i}
<li
class:selected={i == selectedChapter}
on:mouseenter={() => {
stopInterval()
selectedChapter = i
updateShadowColor(chapter.color)
}}
on:mouseleave={startInterval}
>
{#if i == selectedChapter}
<h2
class={i % 2 ? '' : 'transparent-heading'}
style={i % 2
? `color: var(${getVariableNameForChapter(chapter.type)})`
: `-webkit-text-stroke: 1px var(${getVariableNameForChapter(chapter.type)})`}
>
{chapter.title}
</h2>
{:else}
<h2 class={i % 2 ? 'white-heading' : 'transparent-heading'}>{chapter.alias}</h2>
{/if}
<p style="color: var({getVariableNameForChapter(chapter.type)} !important;">
{@html chapter.shortDescription}
</p>
</li>
{/each}
</ul>
<div class="media">
<svg
viewBox="-100 -100 1200 1275"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<filter
id="redShadow"
x="-50%"
y="-50%"
width="200%"
height="200%"
>
<feDropShadow
dx="-10"
dy="0"
stdDeviation="20"
flood-color={currentColor}></feDropShadow>
</filter>
</defs>
<path
d="M877.847 697.627H631.968L582.45 612.133H1000L904.042 446.139H481.362L605.704 231.311L680.46 360.44H854.319L685.658 69.1471L565.762 0L0 977.156L168.593 1074.55H738.253L794.132 977.772L728.062 863.552H973.941L877.847 697.627ZM204.637 924.082L396.895 592.025L589.153 924.082H204.637Z"
fill="black"
filter="url(#redShadow)"></path>
</svg>
</div>
</section>
<style lang="less">
@import "../../../assets/css/variables.less";
.splittedHomepage {
display: flex;
width: 100%;
position: relative;
height: 100%;
align-items: center;
justify-content: space-between;
transition: --color 1s ease;
.placeholder {
}
.elements {
display: flex;
flex-direction: column;
gap: 1.5rem;
position: absolute;
width: 100%;
@media @mobile {
gap: 0.5rem;
}
li {
transition: max-height 1.5s ease;
max-height: 5rem;
max-width: 900px;
h2 {
font-size: 4.5rem;
text-transform: uppercase;
font-weight: 700;
line-height: 4.5rem;
font-family: sans-serif;
@media @mobile {
font-size: 3rem;
line-height: 3rem;
}
&.transparent-heading {
@media @mobile {
font-size: 3rem;
}
font-weight: 700;
color: transparent;
position: relative;
display: inline-block;
-webkit-text-stroke: 1px white;
}
}
height: fit-content;
max-height: 10rem;
display: flex;
flex-direction: column;
p {
opacity: 0;
transition: opacity 1s;
}
&.selected {
p {
opacity: 1;
}
}
}
}
.media {
max-width: 600px;
width: 100%;
display: flex;
height: 100%;
align-items: center;
flex-grow: 1;
@media @mobile {
width: 100%;
}
svg {
width: 100%;
transition: fill 1s ease;
}
}
@media @mobile {
flex-direction: column;
}
}
</style>

View File

@@ -1,48 +0,0 @@
<script lang="ts">
import MedialibImage from "../../widgets/MedialibImage.svelte"
export let item: {
image: string
title: string
descriptions: string[]
}
export let i: number
export let isMobile: boolean
</script>
<li>
<div class="image-wrapper">
<MedialibImage id={item.image} />
<h3>
{i + 1}
</h3>
</div>
<div class="content">
<div class="title-row">
<h4>
{item.title}
</h4>
<svg
xmlns="http://www.w3.org/2000/svg"
width={isMobile ? 58 : 72}
height={isMobile ? 58 : 72}
viewBox="0 0 {isMobile ? 58 : 72} {isMobile ? 58 : 72}
fill="none"
>
<path
d="M0 {isMobile ? 58 : 72}V0L{isMobile ? 58 : 72} {isMobile ? 58 : 72}H0Z"
fill="#EB5757"></path>
</svg>
</div>
<div class="descriptions">
<ul>
{#each item.descriptions as description}
<li>
{description}
</li>
{/each}
</ul>
</div>
</div>
</li>

View File

@@ -1,172 +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 MedialibImage from "../../widgets/MedialibImage.svelte"
import Step from "./Step.svelte"
export let block: ContentBlock<"steps">
console.log("block", block)
let innerWidth = 0
$: isMobile = innerWidth < 968
</script>
<svelte:window bind:innerWidth={innerWidth} />
{#if block.steps?.horizontal}
<div
class=" product-preview-list verticalScrollbar"
data-simplebar
>
<ul class="step-blocks horizontal">
{#each block.steps.items as item, i}
<Step
item={item}
i={i}
isMobile={isMobile}
/>
{/each}
</ul>
</div>
{:else}
<ul class="step-blocks">
{#each block.steps.items as item, i}
<Step
item={item}
i={i}
isMobile={isMobile}
/>
{/each}
</ul>
{/if}
<style
lang="less"
global
>
@import "../../../assets/css/variables.less";
.step-blocks {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(560px, 1fr));
@media @mobile {
grid-template-columns: 1fr;
}
&.horizontal {
display: flex;
flex-direction: row;
overflow-x: auto;
gap: 2.4rem;
padding-bottom: 2.4rem;
& > li {
min-width: 500px;
flex-grow: 1;
height: unset !important;
@media @mobile {
min-width: 300px;
}
}
@media @mobile {
padding: 0;
}
}
gap: 2.4rem;
& > li {
display: flex;
flex-direction: column;
height: 100%;
.image-wrapper {
width: 100%;
position: relative;
aspect-ratio: 1 / 1;
h3 {
position: absolute;
right: 3.6rem;
text-align: center;
font-size: 5.5rem;
font-style: normal;
font-weight: 700;
line-height: 100%; /* 128px */
text-transform: uppercase;
font-family: sans-serif;
z-index: 4;
bottom: 2.4rem;
@media @mobile {
font-size: 3rem;
bottom: 2rem;
right: 2.4rem;
}
font-weight: 700;
color: transparent;
display: inline-block;
-webkit-text-stroke: 2px #eb5757;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.content {
margin-top: -3.6rem;
width: 100%;
position: relative;
height: 100%;
display: flex;
flex-direction: column;
z-index: 3;
.title-row {
height: 3.6rem;
max-height: 3.6rem;
z-index: 3;
width: 100%;
display: flex;
justify-content: flex-start;
align-items: center;
h4 {
background-color: #eb5757;
padding: 1.2rem 2.4rem;
font-size: 1.6rem;
max-height: 100%;
width: calc(100% - 3 * 3.6rem - 2rem);
color: var(--bg-100);
@media @mobile {
font-size: 1.2rem;
width: calc(100% - 3 * 2.4rem - 2rem);
}
}
}
.descriptions {
background-color: #eb5757;
padding: 0px;
flex-grow: 1;
ul {
display: flex;
flex-direction: column;
li {
padding: 2.4rem;
width: 100%;
color: var(--bg-100);
border-bottom: 2px solid var(--bg-100);
&:last-child {
border-bottom: none;
}
}
}
}
}
}
}
</style>

View File

@@ -1,52 +0,0 @@
<script lang="ts">
import Button from "../../../widgets/Button.svelte"
export let column: BlockColumn<"cta">
const cta = column.cta
</script>
<div
class="cta"
id="cta"
>
{#if cta.upperHeadline}
<small>
{cta.upperHeadline}
</small>
{/if}
<h1>
{cta.whiteHeadline}
{#if cta.headlineArrangement !== "row"} <br />{/if} <em class="red">{cta.redHeadline}</em>
</h1>
<p>{cta.description}</p>
<div class="buttons">
{#each cta.callToActionButtons as button}
<Button button={button} />
{/each}
</div>
</div>
<style lang="less">
#cta {
display: flex;
flex-direction: column;
gap: 1.6rem;
small {
font-weight: 700;
font-family: Outfit-Bold, sans-serif;
color: var(--text-100);
}
p {
font-size: 1.2rem;
color: var(--text-100);
}
.buttons {
text-transform: uppercase;
display: flex;
gap: 0.3rem;
}
}
</style>

View File

@@ -1,27 +0,0 @@
<script lang="ts">
import { getVariableNameForChapter } from "../../../../utils"
export let title: string, type: number, description: string
//
</script>
<div>
<h1 style="color: var({getVariableNameForChapter(type)});">
{title}
</h1>
<p style="color: var({getVariableNameForChapter(type)});">
{@html description}
</p>
</div>
<style lang="less">
h1 {
font-size: 4.8rem;
text-transform: uppercase;
margin-bottom: 2.4rem;
}
p {
font-size: 1.2rem;
}
</style>

View File

@@ -1,13 +0,0 @@
<script lang="ts">
import LinkList from "../../../widgets/LinkList.svelte"
export let column: BlockColumn<"text">
</script>
{@html column.text}
{#if column.links?.length}
<div class="button-wrap gap-m">
<!-- Buttons sitzen im Wrapper, sollten vermutlich auch flexibel / repeatable sein, mal nur einen, mal zwei, mal keiner -->
<LinkList links={column.links} />
</div>
{/if}

View File

@@ -1,193 +0,0 @@
<script lang="ts">
import { onMount } from "svelte"
import Icon from "../../../widgets/Icon.svelte"
import { mdiCalendarAccount, mdiCalendarRemove } from "@mdi/js"
export let dateValue: Date,
placeholder: string,
id: string,
editMode = true
let Datepicker: any
onMount(() => {
if (typeof window !== "undefined" && typeof document !== "undefined") {
import("vanillajs-datepicker")
.then((module) => {
Datepicker = module.Datepicker
})
.catch((err) => console.error("Failed to load datepicker:", err))
}
})
let datepicker: any
let wrapper: HTMLLabelElement
function getElementPosition(
element: HTMLElement,
options: {
minSpace: number
offsetAbove: number
} ={ minSpace: 150, offsetAbove: 75 }
): {
x: string
y: string
} {
const rect = element.getBoundingClientRect()
const scrollTop = window.scrollY || window.pageYOffset
const windowHeight = window.innerHeight
const topBody = Number(document.body.style.top.split("px")[0])
let top = rect.bottom + scrollTop + 5 - topBody
// Check if there's the specified minimum visible space below the element
if (windowHeight - rect.bottom < options.minSpace) {
// Place the element above the current element with the specified offset
top = rect.top + scrollTop - element.offsetHeight - options.offsetAbove - topBody + 100
}
return {
y: top + "px",
x: window.innerWidth - rect.right - 20 + "px",
}
}
function respawnElement(element: HTMLElement, deepCopy: boolean = true) {
var newElement = element.cloneNode(deepCopy)
element.parentNode.replaceChild(newElement, element)
return newElement
}
function hideAllModals() {
const dateModal = document.getElementById("dateModal")
dateModal.style.display = "none"
respawnElement(dateModal, false)
}
function outSideDateModalClickListener(e: MouseEvent) {
const dateModal = document.getElementById("dateModal")
if (
!dateModal.contains(e.target as Node) &&
e.target !== document &&
!(e.target as Element).classList.contains("datepicker-cell")
) {
hideDatepicker()
}
}
function dateChangeListener(e: any) {
dateValue = new Date(e.detail.date)
// element has data-for="error-message-birthday" attribute, get it by that
const el = document.querySelector(`[data-for="error-message-${id}]`)
if (el) {
el.remove()
}
hideDatepicker()
}
function hideDatepicker() {
const dateModal = document.getElementById("dateModal")
dateModal.style.display = "none"
datepicker?.destroy()
dateModal.removeEventListener("changeDate", dateChangeListener)
document.removeEventListener("click", outSideDateModalClickListener)
}
function showDatepicker() {
let dateModal = document.getElementById("dateModal")
hideAllModals()
if (datepicker) hideDatepicker()
dateModal = document.getElementById("dateModal")
dateModal.style.display = "block"
dateModal.style.top = getElementPosition(wrapper, { minSpace: 300, offsetAbove: 300 }).y
dateModal.style.right = getElementPosition(wrapper, { minSpace: 300, offsetAbove: 300 }).x
datepicker = new Datepicker(dateModal, {})
if (dateValue) datepicker.setDate(dateValue)
dateModal.addEventListener("changeDate", dateChangeListener)
document.addEventListener("click", outSideDateModalClickListener)
}
</script>
<label
bind:this={wrapper}
id={id}
class="custom-date-picker"
>
<span class:hasValue={!!dateValue || !editMode}>{placeholder}</span>
{#if editMode}
<div class="wrapper">
{#if dateValue}
<button
data-cy="custom-date-picker-text"
class="btn text transparent"
on:click|preventDefault|stopPropagation={showDatepicker}
>
{new Date(dateValue).toLocaleDateString("de-DE")}
</button>
{:else}
<div style="opacity: 0.75;">{placeholder}</div>
{/if}
<button
data-cy="custom-date-picker-icon"
aria-label="Toggle datepicker"
class="btn transparent icon"
on:click|preventDefault|stopPropagation={() => {
hideAllModals()
if (dateValue) dateValue = null
else showDatepicker()
}}
>
{#if !dateValue}
<Icon
path={mdiCalendarAccount}
width="24px"
height="24px"
/>
{:else}
<Icon
path={mdiCalendarRemove}
width="24px"
height="24px"
/>
{/if}
</button>
</div>{:else if !editMode}
{#if dateValue}
<div class="no-input">
{new Date(dateValue).toLocaleDateString("de-DE")}
</div>
{:else}
<div class="no-input">-</div>
{/if}
{/if}
</label>
<style lang="less">
@import "vanillajs-datepicker/css/datepicker.css";
:global .hiddenscript {
opacity: 0;
position: absolute;
}
.custom-date-picker:global {
min-height: 60px;
flex-grow: 1;
height: 24px;
display: flex;
align-items: center;
cursor: pointer;
justify-content: space-between;
.wrapper {
display: flex;
justify-content: space-between;
.btn.icon,
.btn.text {
display: flex;
align-items: center;
color: var(--text-invers-100);
&.text {
color: var(--text-invers-100);
}
}
.icon {
opacity: 0.75;
}
}
}
</style>

View File

@@ -1,342 +0,0 @@
<script lang="ts">
import { mdiCameraOutline, mdiInformationOutline, mdiMovieOpenOutline } from "@mdi/js"
import { apiBaseURL } from "../../../../../config"
import Icon from "../../../widgets/Icon.svelte"
import { createEventDispatcher } from "svelte"
import { newNotification } from "../../../../store"
import MedialibImage from "../../../widgets/MedialibImage.svelte"
import MedialibFile from "../../../widgets/MedialibFile.svelte"
import Modal from "../../../Modal.svelte"
import { tooltip } from "../../../../functions/utils"
const dispatch = createEventDispatcher()
export let id,
classList: string = "",
placeholder: string = "",
disabled: boolean = false,
helperText: string = "",
value: FileField = null,
collectionName: string,
entryId: string,
type: "image" | "video" = "image",
noDelete: boolean = false,
imgIsData = false
let file: File, fileInput: HTMLInputElement
let showApproveModal = false
function onChange(forceUpload = false) {
file = fileInput?.files[0]
const maxFileSize = 150 * 1024 * 1024 // 150 MB in Bytes
const supportedMimeTypes = [
// Images
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/bmp",
"image/tiff",
"image/svg+xml",
"video/mp4",
"video/webm",
"video/ogg",
"video/quicktime",
]
const supportedFormats = `
Unterstützte Bildformate: JPEG, PNG, GIF, WebP, BMP, TIFF, SVG.
Unterstützte Videoformate: MP4, WebM, Ogg.
`
const excludedMimeTypes = ["video/quicktime"]
const excludedExtensions = ["mov"]
if (file) {
const fileExtension = file?.name?.split(".")?.pop()?.toLowerCase()
if (
(excludedMimeTypes.includes(file.type) || excludedExtensions.includes(fileExtension)) &&
forceUpload !== true
) {
showApproveModal = true
return
}
if (!supportedMimeTypes.includes(file.type)) {
newNotification({
class: "error",
html: `Der Dateityp - "${file.type} wird nicht unterstützt. Bitte wähle eine unterstützte Datei aus. ${supportedFormats}`,
})
return
}
const isSupported = supportedMimeTypes.includes(file.type)
if (!isSupported) {
newNotification({
class: "error",
html: `Der Dateityp "${file.type} wird nicht unterstützt. Bitte wähle eine unterstützte Datei aus. ${supportedFormats}`,
})
return
}
const reader = new FileReader()
if (file.size > maxFileSize) {
newNotification({
class: "error",
html: "Die Datei ist zu groß. Bitte wähle eine Datei unter 150 MB aus bzw. komprimiere die ausgewählte Datei.",
})
return
}
reader.addEventListener("load", function () {
imgIsData = true
const objectURL = URL.createObjectURL(file)
value ={
path: file.name,
type: file.type,
size: file.size,
src: objectURL,
}
rerender = !rerender
dispatch("change")
})
// @ts-ignore
reader.readAsDataURL(file)
}
}
let highlight = false
let focused = false
let rerender = false
</script>
<label class="file file-input-label">
<div class="headline">
<span class:hasValue={true}>
{placeholder}
{#if helperText}
<div
use:tooltip={{
content: helperText,
}}
class="helperText"
>
<Icon path={mdiInformationOutline} />
</div>
{/if}
</span>
<Icon
path={type == 'image' ? mdiCameraOutline : mdiMovieOpenOutline}
size="24px"
/>
</div>
<div
class="file-input"
class:focused={focused}
class:highlight-container={highlight}
role="button"
aria-label="File upload"
tabindex={0}
on:dragenter|preventDefault={() => {
highlight = true
}}
on:dragover|preventDefault={() => {
highlight = true
}}
on:dragleave|preventDefault={() => {
highlight = false
}}
on:drop|preventDefault={(e) => {
highlight = false
fileInput = e.dataTransfer
onChange()
}}
>
<div class="fileContainer">
<input
type="file"
bind:this={fileInput}
on:change|stopPropagation={onChange}
readonly={disabled}
id={id}
accept={type === 'image' ? 'image/*' : 'video/*'}
on:click|stopPropagation
/>
<div
class="filePreview"
class:hasChildElement={!!value}
>
{#key rerender}
{#if value}
{#if type == "image"}
{#if typeof value === "string"}
<MedialibImage
id={value}
filter="l"
/>
{:else if imgIsData}
<img
src={value.src}
alt={value.path}
/>
{:else if typeof value.src === "string" && value.src.includes(";base64,")}
<img
src={value.src}
alt={value.path}
/>
{:else if typeof value.src === "string" && !value.src.match(/^https?:\/\//)}
<img
src={`${apiBaseURL}${collectionName}/${entryId}/${value.src}`}
alt={value.path}
/>
{/if}
{:else if type == "video"}
{#if typeof value == "string"}
<MedialibFile
id={value}
let:entry
let:src
>
<video>
<track kind="captions" />
<source
src={src}#t=0.1"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
</MedialibFile>
{:else}
<video
controls
preload="metadata"
>
<track kind="captions" />
<source
src={value.src}#t=0.1"
type="video/mp4"
/>
</video>
{/if}
{/if}
<div>
{#if noDelete}
<button
aria-label="Datei hochladen"
class="delete cta primary"
on:click|stopPropagation|preventDefault={() => {
// change file by clicking input
fileInput.click()
}}
>
Austauschen
</button>
{:else}
<button
aria-label="Datei löschen"
class="delete cta primary"
on:click|stopPropagation|preventDefault={() => {
value = null
dispatch('change')
}}
>
Löschen
</button>
{/if}
</div>
{/if}
{/key}
</div>
</div>
</div>
</label>
{#if showApproveModal}
<Modal
show={true}
on:close={() => {
showApproveModal = false
}}
>
<svelte:fragment slot="title">Dateiformat Warnung</svelte:fragment>
<p>
Deine Datei wird von einigen Browsern, insbesondere von Google Chrome, nur eingeschränkt unterstützt. Wenn
du die Datei dennoch hochladen möchtest, klicke auf "Fortfahren". Andernfalls empfehlen wir, das Video
zunächst in eine MP4-Datei zu konvertieren. Dafür stehen zahlreiche kostenlose Online-Dienste zur Verfügung.
</p>
<div
slot="footer"
class="file-warning-footer"
>
<button
class="btn cta primary"
on:click={() => {
showApproveModal = false
}}
>
Abbrechen
</button>
<button
class="btn cta secondary"
on:click={() => {
showApproveModal = false
onChange(true)
}}>Fortfahren</button
>
</div>
</Modal>
{/if}
<style
lang="less"
global
>
.file-input-label {
.headline {
display: flex;
justify-content: space-between;
width: 100%;
span {
position: relative;
display: flex;
padding: 0px !important;
align-items: center;
gap: 0.5rem;
.helperText {
color: var(--bg-100);
}
}
}
.fileContainer {
height: fit-content;
padding: 0.6rem 0px !important;
input {
display: none;
}
.filePreview {
display: flex;
flex-direction: column;
gap: 0.6rem;
min-height: 15px;
width: 100%;
height: 100%;
&.hasChildElement {
overflow: hidden;
max-width: 100%;
}
img,
video {
height: 180px;
max-height: 180px;
width: 100% !important;
background-color: var(--bg-100);
object-fit: contain !important;
}
button {
width: 100%;
}
}
}
}
</style>

View File

@@ -1,143 +0,0 @@
<script lang="ts">
import { mdiInformationOutline } from "@mdi/js"
import { tooltip } from "../../../../functions/utils"
import Icon from "../../../widgets/Icon.svelte"
export let value: any,
id,
classList: string = "",
onChange: (e: Event) => void,
type: "password" | "text" | "number" | "checkbox" | "noInput" | "textarea" | "select" = "text",
placeholder: string = "",
disabled: boolean = false,
name = "",
options: { name: string; value: string }[] = [],
selectedOptionIndex = 0,
helperText = ""
$: hasValue = Boolean(value)
const attributes ={
id,
class: classList,
placeholder,
name: name || id,
disabled: !!disabled,
}
</script>
<label
style=""
class:textarea={type == 'textarea'}
class:checkbox={type == 'checkbox'}
>
{#if type !== "checkbox"}
<span class:hasValue={hasValue || type === 'noInput'}>{placeholder}</span>
{/if}
{#if type == "checkbox"}
<input
type="checkbox"
{...attributes}
on:change={onChange}
bind:checked={value}
/>
<button
type="button"
class="checkit-span"
aria-label="Toggle checkbox"
tabindex={0}
on:click={() => {
value = !value
setTimeout(() => {
const event = new Event('change', { bubbles: true })
document.getElementById(id)?.dispatchEvent(event)
}, 10)
}}
on:keydown={(e) => {}}></button>
{/if}
{#if type == "password"}
<input
{...attributes}
on:blur={onChange}
bind:value={value}
on:change={onChange}
type="password"
class="sentry-mask"
/>
{/if}
{#if type == "text"}
<input
on:blur={onChange}
{...attributes}
bind:value={value}
on:change={onChange}
/>
{/if}
{#if type == "textarea"}
<textarea
on:blur={onChange}
{...attributes}
bind:value={value}
on:change={onChange}></textarea>
{/if}
{#if type == "number"}
<input
on:blur={onChange}
type="number"
{...attributes}
bind:value={value}
on:change={onChange}
/>
{/if}
{#if type == "noInput"}
<div class="no-input">
{#if id.includes("pass")}
************
{:else}
{Boolean(value) ? value : "-"}
{/if}
</div>
{/if}
{#if type == "select"}
<select
on:change={onChange}
{...attributes}
bind:value={value}
>
{#each options as option, index}
<option
value={option.value}
selected={index === selectedOptionIndex}
>
{option.name}
</option>
{/each}
</select>
{/if}
{#if type !== "checkbox"}
<span class="underline"></span>
{/if}
{#if helperText}
<div
use:tooltip={{
content: helperText,
}}
class="helperText"
>
<Icon path={mdiInformationOutline} />
</div>
{/if}
</label>
<style lang="less">
.checkbox {
width: 1.2rem !important;
}
.helperText {
position: absolute;
right: 0px;
color: var(--bg-100);
top: 50%;
transform: translateY(-50%);
}
</style>

View File

@@ -1,102 +0,0 @@
<script lang="ts">
import Select from "svelte-select/Select.svelte"
export let options: { label: string; value: string }[] = [],
selectedOption: { label: string; value: string } = null,
value = "",
placeholder: string,
clearable = false,
editMode = true
$: value = selectedOption?.value
$: hasValue = Boolean(value)
let focused = false
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-to-interactive-role -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-label-has-associated-control -->
<label
role="button"
tabindex="0"
on:click|stopPropagation={() => {
focused = true
}}
>
<span class:hasValue={hasValue || !editMode}>{placeholder}</span>
{#if editMode}
<Select
bind:items={options}
inputAttributes={{ autocomplete: 'on' }}
placeholder={placeholder}
showChevron={true}
bind:focused={focused}
clearable={clearable}
hideEmptyState={true}
searchable={true}
bind:value={selectedOption}
on:clear={() => (selectedOption = null)}
/>
{:else if !editMode}
<div class="no-input">{selectedOption?.label || "-"}</div>
{/if}
</label>
<style lang="less">
:global .svelte-select {
background-color: var(--neutral-white) !important;
resize: none !important;
.value-container {
input {
padding-top: 20px;
}
.selected-item {
padding-top: 19px;
}
}
.multi-item {
background-color: rgba(0, 0, 0, 0.1) !important;
border-radius: 3px !important;
}
.selected-item {
margin-left: 3px;
height: auto;
line-height: 100%;
}
input {
height: 100%;
font-size: 1rem !important;
border: 0px solid black !important;
}
* {
border-left: 0px solid black !important;
}
.list-item {
.item {
&.active {
background-color: transparent !important;
color: black !important;
}
}
}
.indicators {
color: #ccc;
& > * {
border-left: 1px solid #ccc !important;
height: 40px !important;
cursor: pointer !important;
}
.clear-select {
color: var(--primary-200) !important;
font-weight: 700 !important;
}
}
}
</style>

View File

@@ -1,66 +0,0 @@
import type { SvelteComponent } from "svelte"
import PredefinedBlock from "./PredefinedBlock.svelte"
import Columns from "./Columns.svelte"
import ProductListPreviewWidget from "../product/ProductListPreviewWidget.svelte"
import NewsletterRow from "./NewsletterRow.svelte"
import SplittedHomepage from "./SplittedHomepage.svelte"
import ImproveYourselfDescription from "./ImproveYourselfDescription.svelte"
import HomepageRow from "./HomepageRow.svelte"
import ChapterPreview from "./ChapterPreview/ChapterPreview.svelte"
import PreviewCard from "./RatingPreview/PreviewCard.svelte"
import Steps from "./Steps.svelte"
const blocks: {
[key: string]: {
sectionClass: string
component: typeof SvelteComponent<{ block?: ContentBlock }>
}
} = {
default: {
sectionClass: "",
component: Columns,
},
columns: {
sectionClass: "",
component: Columns,
},
predefinedBlock: {
sectionClass: "predefined-block",
component: PredefinedBlock,
},
productSlider: {
sectionClass: "",
component: ProductListPreviewWidget,
},
NewsletterRow: {
sectionClass: "newsletter-row",
component: NewsletterRow,
},
splittedHomepage: {
sectionClass: "",
component: SplittedHomepage,
},
improveYourselfDescription: {
sectionClass: "ImproveYourselfDescription",
component: ImproveYourselfDescription,
},
homepage: {
sectionClass: "homepageRow",
component: HomepageRow,
},
selfImprovementChapterPreview: {
sectionClass: "chapterPreview",
component: ChapterPreview,
},
ratingPreview: {
sectionClass: "ratingPreview",
component: PreviewCard,
},
stepNr: {
sectionClass: "stepNr",
component: Steps,
},
}
export default blocks

View File

@@ -7,7 +7,7 @@
<Modal
show={true}
size="sm"
on:close={(e) => {
on:close={() => {
actionApproval.set(null)
}}
>

View File

@@ -5,7 +5,7 @@
</script>
<button
class="cta {button.ctaType == 0 ? 'primary' : 'secondary'}
class={`cta ${button.ctaType === 0 ? 'primary' : 'secondary'}`}
aria-label={button.buttonText}
>
<a
@@ -13,8 +13,8 @@
href={button.page}
target={button.buttonTarget}
>
{button.buttonText}</a
>
{button.buttonText}
</a>
</button>
<style lang="less">

View File

@@ -41,17 +41,17 @@
<slot />
{:else}
<div
style="display: flex;
style={{`display: flex;
justify-content: center;
align-items: {positions[textPosition]};
background-image: url({backgroundUrl});
align-items: ${positions[textPosition]};
background-image: url(${backgroundUrl});
background-size: cover;
width: 100%;
align-self: stretch;
flex-grow: 1;
"
`}}
>
<div style="background-color: rgba(255,255,255,0.7); padding: 20px; background: {background}; width: 100%;">
<div style={{`background-color: rgba(255,255,255,0.7); padding: 20px; background: ${background}; width: 100%;`}}>
<p>Cookie ist nicht aktiviert. Bitte aktiviere ihn.</p>
</div>
</div>

View File

@@ -1,52 +1,105 @@
<script lang="ts">
import { spaLink } from "../../actions"
import { createContactRequest } from "../../functions/CommerceAPIs/tibiEndpoints/helpCenter"
import { newNotification } from "../../store"
import { cryptchaSolutionId, initCryptcha, cryptchaSolution } from "../../utils"
import Input from "../pagebuilder/blocks/form/Input.svelte"
import { onChange, validateEmail, validateField, validateInput } from "../pagebuilder/profile/helper"
export let email = "",
name = "",
description = "",
title = "Es sind noch Fragen offen?"
import { apiBaseURL } from "../../../config"
export let email = ""
export let name = ""
export let description = ""
export let title = "Es sind noch Fragen offen?"
let contactRequestSent = false
const contactRequest ={
email: email,
name: name,
description: description,
usageTerms: false,
_s: "",
_sId: "",
}
function validateContactRequest(): boolean {
const validations = [
{ id: "email", value: contactRequest.email, validator: validateEmail },
{ id: "name", value: contactRequest.name, validator: validateInput },
{ id: "description", value: contactRequest.description, validator: validateInput },
]
let isValid = true
for (const { id, value, validator } of validations) {
// @ts-ignore
if (!validateField(id, value, validator)) isValid = false
}
if (!contactRequest.usageTerms) {
newNotification({
class: "error",
html: "Bitte akzeptiere die Datenschutzerklärung.",
})
isValid = false
}
return isValid
}
let submitting = false
let cryptchaEl: HTMLDivElement
let cryptchaInitialized = false
const contactRequest = {
email,
name,
description,
usageTerms: false,
}
const emailPattern = /^(?:[a-zA-Z0-9_'^&%+\-])+(?:\.(?:[a-zA-Z0-9_'^&%+\-])+)*@(?:(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|\[(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\]$/
const validate = () => {
if (!emailPattern.test(contactRequest.email || "")) {
newNotification({ class: "error", html: "Bitte gib eine gültige E-Mail-Adresse an." })
return false
}
if (!contactRequest.name?.trim()) {
newNotification({ class: "error", html: "Bitte gib deinen Namen an." })
return false
}
if (!contactRequest.description?.trim()) {
newNotification({ class: "error", html: "Bitte beschreibe dein Anliegen." })
return false
}
if (!contactRequest.usageTerms) {
newNotification({ class: "error", html: "Bitte akzeptiere die Datenschutzerklärung." })
return false
}
if (!$cryptchaSolution || !$cryptchaSolutionId) {
newNotification({ class: "error", html: "Bitte löse die Sicherheitsabfrage." })
return false
}
return true
}
const updateField = (field: keyof typeof contactRequest) => (event: Event) => {
const target = event.currentTarget as HTMLInputElement | HTMLTextAreaElement
if (field === "usageTerms") {
contactRequest[field] = (target as HTMLInputElement).checked as never
} else {
contactRequest[field] = target.value as never
}
}
$: if (cryptchaEl && !cryptchaInitialized && contactRequest.name && contactRequest.email) {
initCryptcha(cryptchaEl)
cryptchaInitialized = true
}
const submitContactRequest = async () => {
if (!validate() || submitting) return
submitting = true
try {
const response = await fetch(`${apiBaseURL}support-requests`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
...contactRequest,
solution: $cryptchaSolution,
solutionId: $cryptchaSolutionId,
}),
})
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`)
}
newNotification({ class: "success", html: "Deine Anfrage wurde erfolgreich versendet!" })
contactRequestSent = true
} catch (error) {
console.error("Failed to submit contact request", error)
newNotification({
class: "error",
html: "Die Anfrage konnte nicht gesendet werden. Bitte versuche es später erneut.",
})
} finally {
submitting = false
}
}
</script>
<section class="small-wrapper contact">
@@ -55,91 +108,61 @@
<small>Kundenservice</small>
<h2>{title}</h2>
<p>
Konnte unser <a
href="/helpCenter"
use:spaLink>FAQ</a
> dir nicht weiterhelfen? Kein Problem! Kontaktiere uns direkt, und unser Kundenservice steht dir gerne zur
Verfügung.
Konnte unser <a href="/helpCenter" use:spaLink>FAQ</a> dir nicht weiterhelfen? Kein Problem! Kontaktiere uns direkt, und unser Kundenservice steht dir gerne zur Verfügung.
</p>
</div>
<form
class="contactForm"
on:submit|preventDefault|stopPropagation={() => {
if (validateContactRequest()) {
createContactRequest(contactRequest, $cryptchaSolutionId, $cryptchaSolution).then(() => {
newNotification({
class: 'success',
html: 'Deine Anfrage wurde erfolgreich versendet!',
})
contactRequestSent = true
})
}
}}
>
<form class="contactForm" on:submit|preventDefault|stopPropagation={submitContactRequest}>
{#if contactRequestSent}
<section class="row">
<p>
Deine Anfrage wurde erfolgreich versendet! Üblicherweise werden wir uns in den nächsten 5
Werktagen bei dir melden!
</p>
<p>Danke! Wir melden uns innerhalb der nächsten Werktage bei dir.</p>
</section>
{:else}
<section class="row">
<Input
type="text"
placeholder="E-Mail"
bind:value={contactRequest.email}
id="email"
onChange={onChange}
helperText="Wir benötigen deine E-Mail Adresse, um dir antworten zu können."
<label for="support-email">E-Mail</label>
<input
id="support-email"
type="email"
value={contactRequest.email}
on:input={updateField("email")}
required
/>
</section>
<section class="row">
<Input
<label for="support-name">Name</label>
<input
id="support-name"
type="text"
placeholder="Name"
bind:value={contactRequest.name}
id="name"
onChange={onChange}
value={contactRequest.name}
on:input={updateField("name")}
required
/>
</section>
<section class="row">
<Input
type="textarea"
placeholder="Beschreibung"
bind:value={contactRequest.description}
id="description"
onChange={onChange}
/>
<label for="support-description">Beschreibung</label>
<textarea
id="support-description"
rows={4}
value={contactRequest.description}
on:input={updateField("description")}
required
></textarea>
</section>
<section
class="row"
style="flex-direction: row !important;"
>
<Input
<section class="row checkbox-row">
<input
id="support-usageTerms"
type="checkbox"
bind:value={contactRequest.usageTerms}
onChange={onChange}
id="usageTerms"
checked={contactRequest.usageTerms}
on:change={updateField("usageTerms")}
required
/>
<p>
Ich habe die <a
use:spaLink
href="/datenschutz">Datenschutzerklärung</a
> gelesen und akzeptiere Sie.
Ich habe die <a use:spaLink href="/datenschutz">Datenschutzerklärung</a> gelesen und akzeptiere sie.
</p>
</section>
<section
class="row"
style="flex-direction: row !important;"
>
<button
class="cta primary"
type="submit"
disabled={!$cryptchaSolution}
>
Absenden
<section class="row">
<button class="cta primary" type="submit" disabled={submitting}>
{submitting ? "Wird gesendet…" : "Absenden"}
</button>
</section>
<section class="row">
@@ -152,48 +175,56 @@
<style lang="less">
@import "../../assets/css/variables.less";
.small-wrapper.contact {
display: flex;
overflow: visible;
width: 100%;
.inner-small-wrapper {
display: grid;
overflow: visible;
grid-template-columns: 1fr 1fr;
gap: 2.4rem;
@media @mobile {
grid-template-columns: 1fr;
}
.leftern {
display: flex;
flex-direction: column;
gap: 0.6rem;
height: 100%;
justify-content: center;
}
small {
font-family: Outfit-Bold;
font-size: 0.7rem;
line-height: 0.7rem;
font-weight: 700;
text-transform: uppercase;
}
p {
font-size: 1.2rem;
line-height: 1.2rem;
}
}
form {
display: flex;
flex-direction: column;
gap: 0.6rem;
.row {
padding: 0px !important;
}
button {
width: fit-content;
}
}
.inner-small-wrapper {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2.4rem;
width: 100%;
}
.leftern {
display: flex;
flex-direction: column;
gap: 0.6rem;
}
form {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.row {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.checkbox-row {
flex-direction: row;
align-items: flex-start;
gap: 1rem;
input {
margin-top: 0.3rem;
}
p {
margin: 0;
}
}
input,
textarea {
padding: 0.6rem 0.8rem;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 6px;
font: inherit;
}
</style>

View File

@@ -2,18 +2,33 @@
import { spaLink } from "../../actions"
import MedialibFile from "./MedialibFile.svelte"
export let links: BlockLink[]
export let links: BlockLink[] = []
const resolveClass = (style?: string) => {
if (!style) return "cta"
return `cta btn-${style}`
}
</script>
{#each links || [] as link}
{#if link.url || !link.file}
<a use:spaLink href={link.url} class="cta btn-{link.style} {link.style} target={link.target || undefined}
>{link.text}</a
{#each links || [] as link (link.text)}
{#if link.url}
<a
use:spaLink
href={link.url}
class={resolveClass(link.style)}
target={link.target || "_self"}
>
{:else}
{link.text}
</a>
{:else if link.file}
<MedialibFile id={link.file} let:src>
<a href={src} class="cta btn-{link.style} {link.style} target={link.target || undefined}>{link.text}</a
<a
href={src}
class={resolveClass(link.style)}
target={link.target || "_blank"}
>
{link.text}
</a>
</MedialibFile>
{/if}
{/each}

View File

@@ -1,10 +1,13 @@
<script lang="ts">
import { Circle } from "svelte-loading-spinners"
import { loadingStore } from "../../store"
export let active = false,
styles: string = "",
progress = false
export let active = false
export let styles: string = ""
export let progress = false
export let loaded = 0
export let total = 0
$: percentage = total > 0 ? Math.round((loaded / total) * 100) : 0
</script>
<div
@@ -20,7 +23,7 @@
/>
{#if progress}
<span>{Math.round(($loadingStore.loaded / $loadingStore.total) * 100) || 0} %</span>
<span>{percentage} %</span>
{/if}
</div>
{/if}

View File

@@ -24,7 +24,7 @@
{#if !$openModal || ($openModal && force)}
<!-- svelte-ignore a11y-no-noninteractive-element-to-interactive-role -->
<li
class="notification {n.class}
class={`notification ${n.class}`}
use:animateIt
on:click={() => {
removeNotification(n)

View File

@@ -1,5 +1,4 @@
import { get, writable } from "svelte/store"
import { setUser } from "../sentry"
import { baseURL } from "../config"
/*********** location **************************/
@@ -126,18 +125,5 @@ location.subscribe((l) => {
})
})
export const openModal = writable<boolean>(false)
export const overlays = writable<Overlay[]>([])
export const categories = writable<BigCommerceCategory[]>([])
export const login = writable<Login | null>(null)
login.subscribe((l) => {
setUser({
username: l?.customer?.username || l?.tokenData?.tibiId || "anonymous",
email: l?.customer?.email || l?.tokenData?.email || "anonymous",
})
})
export const modules = writable<Module[]>([])
export const backgroundImages = writable<{ standard?: string }>({})
export const isMobile = writable<boolean>(false)
export const actionApproval = writable<ActionApproval | null>(null)