zwischenstand
This commit is contained in:
@@ -2,11 +2,11 @@
|
||||
import { onMount } from "svelte"
|
||||
import Header from "./lib/components/header/Header.svelte"
|
||||
import Footer from "./lib/components/Footer.svelte"
|
||||
import Content from "./routes/Content.svelte"
|
||||
import Notifications from "./lib/components/widgets/Notifications.svelte"
|
||||
import SSRSkip from "./lib/components/SSRSkip.svelte"
|
||||
import { baseURL } from "./config"
|
||||
import { isMobile, location, openModal } from "./lib/store"
|
||||
import StaticHomepage from "./routes/StaticHomepage.svelte"
|
||||
|
||||
export let url = ""
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
<div class="app-shell">
|
||||
<Header />
|
||||
<main>
|
||||
<Content location={$location} />
|
||||
<StaticHomepage />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ export const baseURL = `${protocol}//${baseDomain}`
|
||||
export const apiBaseURL = "/api/"
|
||||
|
||||
export const websiteName = "Kontextwerk"
|
||||
export const companyName = "Kontextwerk GmbH"
|
||||
export const companyName = "Kontextwerk"
|
||||
export const email = "hello@kontextwerk.de"
|
||||
export const streetAddress = "Gertrudenstraße 3"
|
||||
export const localityAddress = "Hamburg"
|
||||
|
||||
36
frontend/src/lib/components/pagebuilder/Breadcrumbs.svelte
Normal file
36
frontend/src/lib/components/pagebuilder/Breadcrumbs.svelte
Normal file
@@ -0,0 +1,36 @@
|
||||
<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}
|
||||
301
frontend/src/lib/components/pagebuilder/ContentBlock.svelte
Normal file
301
frontend/src/lib/components/pagebuilder/ContentBlock.svelte
Normal file
@@ -0,0 +1,301 @@
|
||||
<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>
|
||||
126
frontend/src/lib/components/pagebuilder/DefaultImage.svelte
Normal file
126
frontend/src/lib/components/pagebuilder/DefaultImage.svelte
Normal file
@@ -0,0 +1,126 @@
|
||||
<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>
|
||||
36
frontend/src/lib/components/pagebuilder/Loader.svelte
Normal file
36
frontend/src/lib/components/pagebuilder/Loader.svelte
Normal file
@@ -0,0 +1,36 @@
|
||||
<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>
|
||||
84
frontend/src/lib/components/pagebuilder/SEO/Index.svelte
Normal file
84
frontend/src/lib/components/pagebuilder/SEO/Index.svelte
Normal file
@@ -0,0 +1,84 @@
|
||||
<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} />
|
||||
130
frontend/src/lib/components/pagebuilder/SEO/OpenGraph.svelte
Normal file
130
frontend/src/lib/components/pagebuilder/SEO/OpenGraph.svelte
Normal file
@@ -0,0 +1,130 @@
|
||||
<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>
|
||||
16
frontend/src/lib/components/pagebuilder/SEO/Product.svelte
Normal file
16
frontend/src/lib/components/pagebuilder/SEO/Product.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<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>
|
||||
224
frontend/src/lib/components/pagebuilder/SEO/SchemaORG.svelte
Normal file
224
frontend/src/lib/components/pagebuilder/SEO/SchemaORG.svelte
Normal file
@@ -0,0 +1,224 @@
|
||||
<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>
|
||||
@@ -0,0 +1,105 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
import Item from "./Item.svelte"
|
||||
import { selfImprovementChapters } from "../../../../store"
|
||||
|
||||
export let block: ContentBlock<"selfImprovementChapterPreview">
|
||||
const chapters = block.selfImprovementChapterPreview
|
||||
let interval: NodeJS.Timeout
|
||||
|
||||
function startInterval() {
|
||||
interval = setInterval(() => {
|
||||
selectedChapter = (selectedChapter + 1) % $selfImprovementChapters.length
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
function stopInterval() {
|
||||
clearInterval(interval)
|
||||
}
|
||||
onMount(() => {
|
||||
startInterval()
|
||||
return () => clearInterval(interval)
|
||||
})
|
||||
let selectedChapter = 0
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<ul>
|
||||
{#each $selfImprovementChapters as chapter, i}
|
||||
<li
|
||||
on:mouseenter="{() => {
|
||||
stopInterval()
|
||||
selectedChapter = i
|
||||
}}"
|
||||
on:mouseleave="{startInterval}"
|
||||
class:active="{selectedChapter === i}"
|
||||
>
|
||||
<Item
|
||||
chapter="{chapter}"
|
||||
bind:selectedChapter="{selectedChapter}"
|
||||
index="{i}"
|
||||
previewImage="{chapters.find((p) => p.chapter === chapter.id)?.previewImage}"
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
* {
|
||||
transition-duration: 0s;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
li {
|
||||
width: 100%;
|
||||
aspect-ratio: 4 / 3;
|
||||
@media (max-width: 1500px) {
|
||||
aspect-ratio: unset;
|
||||
&::before {
|
||||
float: left;
|
||||
padding-top: 125%;
|
||||
content: "";
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
content: "";
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1500px) {
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
li {
|
||||
width: 33.33%;
|
||||
}
|
||||
|
||||
li.active {
|
||||
order: -1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,350 @@
|
||||
<script lang="ts">
|
||||
import { onMount, afterUpdate } from "svelte"
|
||||
import MedialibImage from "../../../widgets/MedialibImage.svelte"
|
||||
import { spaLink } from "../../../../actions"
|
||||
import { getVariableNameForChapter } from "../../../../utils"
|
||||
|
||||
export let selectedChapter: number, index: number, chapter: SelfImprovementChapter, previewImage: string
|
||||
const color = chapter.color
|
||||
$: active = selectedChapter === index
|
||||
|
||||
let upperPart: HTMLElement
|
||||
let lowerPart: HTMLElement
|
||||
let topOverlay: HTMLElement
|
||||
let bottomOverlay: HTMLElement
|
||||
|
||||
function updateOverlayHeights() {
|
||||
if (topOverlay && bottomOverlay && upperPart && lowerPart) {
|
||||
topOverlay.style.height = `${upperPart.offsetHeight + 24}px`
|
||||
bottomOverlay.style.height = `${lowerPart.offsetHeight + 24}px`
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("resize", updateOverlayHeights)
|
||||
})
|
||||
|
||||
afterUpdate(() => {
|
||||
updateOverlayHeights()
|
||||
})
|
||||
|
||||
$: if (active) {
|
||||
updateOverlayHeights()
|
||||
} else {
|
||||
if (topOverlay && bottomOverlay) {
|
||||
topOverlay.style.height = "0px"
|
||||
bottomOverlay.style.height = "0px"
|
||||
}
|
||||
}
|
||||
function getSlug(type: number) {
|
||||
if (type == 1) return "krasskraft"
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
let colorName = getVariableNameForChapter(chapter.type)
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="chapter-preview-block"
|
||||
class:active="{active}"
|
||||
>
|
||||
{#if chapter.locked}
|
||||
<div
|
||||
class="locked"
|
||||
class:active="{active}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 72 72"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M15.75 30.165V24C15.75 18.6294 17.8835 13.4787 21.6811 9.68109C25.4787 5.88348 30.6294 3.75 36 3.75C41.3706 3.75 46.5213 5.88348 50.3189 9.68109C54.1165 13.4787 56.25 18.6294 56.25 24V30.165C59.595 30.414 61.77 31.044 63.363 32.637C66 35.271 66 39.516 66 48C66 56.484 66 60.729 63.363 63.363C60.729 66 56.484 66 48 66H24C15.516 66 11.271 66 8.637 63.363C6 60.729 6 56.484 6 48C6 39.516 6 35.271 8.637 32.637C10.227 31.044 12.405 30.414 15.75 30.165ZM20.25 24C20.25 19.8228 21.9094 15.8168 24.8631 12.8631C27.8168 9.90937 31.8228 8.25 36 8.25C40.1772 8.25 44.1832 9.90937 47.1369 12.8631C50.0906 15.8168 51.75 19.8228 51.75 24V30.012C50.601 30 49.353 30 48 30H24C22.644 30 21.399 30 20.25 30.012V24ZM42 48C42 49.5913 41.3679 51.1174 40.2426 52.2426C39.1174 53.3679 37.5913 54 36 54C34.4087 54 32.8826 53.3679 31.7574 52.2426C30.6321 51.1174 30 49.5913 30 48C30 46.4087 30.6321 44.8826 31.7574 43.7574C32.8826 42.6321 34.4087 42 36 42C37.5913 42 39.1174 42.6321 40.2426 43.7574C41.3679 44.8826 42 46.4087 42 48Z"
|
||||
fill="white"></path>
|
||||
</svg>
|
||||
<p>Coming Soon</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="background-image">
|
||||
<MedialibImage id="{previewImage}" />
|
||||
|
||||
<div
|
||||
bind:this="{topOverlay}"
|
||||
class="overlay top"
|
||||
style="background-color: var({colorName})"
|
||||
></div>
|
||||
<div
|
||||
bind:this="{bottomOverlay}"
|
||||
class="overlay bottom"
|
||||
style="background-color: var({colorName})"
|
||||
></div>
|
||||
<div
|
||||
class="overlay left"
|
||||
style="background-color: var({colorName})"
|
||||
></div>
|
||||
<div
|
||||
class="overlay right"
|
||||
style="background-color: var({colorName})"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
class:active="{active}"
|
||||
>
|
||||
<div
|
||||
bind:this="{upperPart}"
|
||||
class:active="{active}"
|
||||
class="upper-part"
|
||||
>
|
||||
<h4
|
||||
class:active="{active}"
|
||||
style="{!active ? 'color: ' + color : 'color: var(--bg-100)'}"
|
||||
>
|
||||
{chapter.alias}
|
||||
</h4>
|
||||
<h3
|
||||
class:active="{active}"
|
||||
style="{!active ? 'color: ' + color : 'color: var(--bg-100)'}"
|
||||
>
|
||||
{chapter.title}
|
||||
</h3>
|
||||
<p
|
||||
class:active="{active}"
|
||||
style="{!active ? 'color: ' + 'transparent' : 'color: var(--bg-100)'}"
|
||||
>
|
||||
{chapter.shortDescription}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
bind:this="{lowerPart}"
|
||||
class="lower-part"
|
||||
class:active="{active}"
|
||||
>
|
||||
<button
|
||||
style="background-color: var({colorName})"
|
||||
disabled="{chapter.locked}"
|
||||
>
|
||||
<a
|
||||
href="/selfimprovement/{getSlug(chapter.type)}"
|
||||
use:spaLink
|
||||
aria-disabled="{chapter.locked ? 'true' : 'false'}"
|
||||
>
|
||||
Weiter
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
@import "../../../../assets/css/variables.less";
|
||||
.chapter-preview-block {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
.locked {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
background: rgba(13, 12, 12, 0.5);
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
p {
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
svg {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
}
|
||||
@media (max-width: 1500px) {
|
||||
&:not(.active) {
|
||||
svg {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
p {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.background-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.overlay {
|
||||
position: absolute;
|
||||
transition: all 0.3s ease;
|
||||
&.top {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 0;
|
||||
}
|
||||
&.bottom {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 0;
|
||||
}
|
||||
&.left {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
&.right {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.active .background-image .overlay.top {
|
||||
height: auto;
|
||||
}
|
||||
&.active .background-image .overlay.bottom {
|
||||
height: auto;
|
||||
}
|
||||
&.active .background-image .overlay.left {
|
||||
width: 3.6rem;
|
||||
@media (max-width: 600px) {
|
||||
width: 0.6rem;
|
||||
}
|
||||
}
|
||||
&.active .background-image .overlay.right {
|
||||
width: 3.6rem;
|
||||
@media (max-width: 600px) {
|
||||
width: 0.6rem;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
transition: border-color 0.3s ease, color 0.3s ease;
|
||||
border-top: 2.4rem solid transparent;
|
||||
border-bottom: 2.4rem solid transparent;
|
||||
border-left: 3.6rem solid transparent;
|
||||
border-right: 3.6rem solid transparent;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
.upper-part {
|
||||
transition: border-color 0.3s ease, color 0.3s ease, background-color 0.3s ease;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
h4 {
|
||||
margin: 0px;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.2rem;
|
||||
color: var(--bg-100);
|
||||
}
|
||||
h3 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.2rem;
|
||||
color: var(--bg-100);
|
||||
}
|
||||
p {
|
||||
font-size: 1.2rem;
|
||||
color: var(--bg-100);
|
||||
}
|
||||
@media (max-width: 1500px) {
|
||||
h3:not(.active) {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
p:not(.active) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
h4:not(.active) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
gap: 0.6rem;
|
||||
h3 {
|
||||
font-size: 1.6rem;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
h3:not(.active) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
p {
|
||||
margin-top: 0px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
p:not(.active) {
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
h4 {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
h4:not(.active) {
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
border-bottom: 2.4rem solid transparent;
|
||||
@media (max-width: 600px) {
|
||||
border-width: 0.6rem !important;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
border-width: 0.6rem !important;
|
||||
}
|
||||
.lower-part {
|
||||
border-top: 2.4rem solid transparent;
|
||||
@media (max-width: 600px) {
|
||||
border-width: 0rem !important;
|
||||
}
|
||||
transition: border-color 0.3s ease, color 0.3s ease, background-color 0.3s ease;
|
||||
button {
|
||||
font-weight: 700;
|
||||
font-size: 1.2rem;
|
||||
text-transform: uppercase;
|
||||
padding: 6px 12px;
|
||||
@media (max-width: 1500px) {
|
||||
padding-left: 0px;
|
||||
}
|
||||
color: var(--bg-100);
|
||||
&[disabled] {
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:not(.active) {
|
||||
@media (max-width: 1500px) {
|
||||
.lower-part {
|
||||
button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<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>
|
||||
@@ -0,0 +1,72 @@
|
||||
<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>
|
||||
@@ -0,0 +1,26 @@
|
||||
<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>
|
||||
@@ -0,0 +1,92 @@
|
||||
<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>
|
||||
@@ -0,0 +1,184 @@
|
||||
<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
@@ -0,0 +1,12 @@
|
||||
<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}
|
||||
@@ -0,0 +1,261 @@
|
||||
<script
|
||||
lang="ts"
|
||||
context="module"
|
||||
>
|
||||
import "simplebar"
|
||||
import "simplebar/dist/simplebar.css"
|
||||
import ResizeObserver from "resize-observer-polyfill"
|
||||
if (typeof window !== "undefined") window.ResizeObserver = ResizeObserver
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { getCachedEntries } from "../../../../../api"
|
||||
import { getBCGraphProductsByIds } from "../../../../functions/CommerceAPIs/bigCommerce/product"
|
||||
import { backgroundImages } from "../../../../store"
|
||||
import MedialibImage from "../../../widgets/MedialibImage.svelte"
|
||||
|
||||
export let block: ContentBlock<"ratingPreview">
|
||||
let ratings: ProductRating[] = []
|
||||
let productsMap: Record<string, BKDFProduct> = {}
|
||||
getCachedEntries("rating", {
|
||||
_id: {
|
||||
$in: block?.ratingsPreview?.ratings?.map((rating) => rating.rating),
|
||||
},
|
||||
}).then((entries) => {
|
||||
getBCGraphProductsByIds(entries.map((entry) => String(entry.bigCommerceProductId))).then((products) => {
|
||||
productsMap = products.reduce((acc, product) => {
|
||||
acc[product.id] = product
|
||||
return acc
|
||||
}, {})
|
||||
ratings = entries
|
||||
})
|
||||
})
|
||||
|
||||
function formatDate(date: string) {
|
||||
return new Date(date).toLocaleDateString("de-DE", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
}
|
||||
|
||||
function returnWidthForRating(rating: ProductRating) {
|
||||
const ratingP =
|
||||
(rating.rating.quality + rating.rating.priceQualityRatio + rating.rating.comfort + rating.rating.overall) *
|
||||
5
|
||||
return ratingP
|
||||
}
|
||||
|
||||
function returnAvgForRating(rating: ProductRating) {
|
||||
const ratingP =
|
||||
(rating.rating.quality + rating.rating.priceQualityRatio + rating.rating.comfort + rating.rating.overall) /
|
||||
4
|
||||
return ratingP.toFixed(1)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if ratings.length}
|
||||
<div
|
||||
data-simplebar
|
||||
class="review-list-container horizontalScrollbar"
|
||||
>
|
||||
<ul class="review-list">
|
||||
{#each ratings as rating}
|
||||
<li class="review-item">
|
||||
<div class="upper-box">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="48"
|
||||
height="49"
|
||||
viewBox="0 0 48 49"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M48 0.25V47.5833L0 0.25H48Z"
|
||||
fill="white"></path>
|
||||
</svg>
|
||||
<div class="background-image">
|
||||
<img
|
||||
src="{productsMap[rating.bigCommerceProductId].featuredImage.url}"
|
||||
alt="{rating.title}"
|
||||
/>
|
||||
<MedialibImage
|
||||
id="{$backgroundImages?.standard}"
|
||||
filter="l"
|
||||
/>
|
||||
</div>
|
||||
<div class="overlay">
|
||||
<h3>
|
||||
{rating.title}
|
||||
</h3>
|
||||
<p>{rating.comment}</p>
|
||||
<div class="date">{formatDate(rating.review_date)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rating-bar">
|
||||
<div
|
||||
class="rating-filled"
|
||||
style="width: {returnWidthForRating(rating)}%; "
|
||||
>
|
||||
<div class="star-wrapper">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M16.7692 23.1373L16.7717 23.1388C17.1926 23.3925 17.6791 23.5165 18.1701 23.4952C18.6612 23.474 19.1351 23.3085 19.5326 23.0193C19.9301 22.7302 20.2335 22.3303 20.405 21.8697C20.5762 21.4098 20.6083 20.9097 20.4972 20.4318C20.497 20.431 20.4968 20.4302 20.4967 20.4294L19.3653 15.5223L22.8594 12.4729H22.8607L23.1422 12.2302C23.515 11.9087 23.7846 11.4842 23.9171 11.0101C24.0496 10.536 24.0392 10.0333 23.8872 9.56506C23.7352 9.09682 23.4483 8.68389 23.0626 8.37804C22.6772 8.0725 22.2103 7.88743 21.7202 7.84595C21.7197 7.84591 21.7192 7.84587 21.7187 7.84583L16.7503 7.41534L14.8029 2.78442C14.8027 2.78387 14.8024 2.78331 14.8022 2.78276C14.6125 2.32902 14.2929 1.94145 13.8836 1.66874C13.4738 1.39569 12.9924 1.25 12.5 1.25C12.0076 1.25 11.5262 1.3957 11.1164 1.66874C10.7069 1.94157 10.3872 2.32937 10.1976 2.78337C10.1974 2.78372 10.1973 2.78407 10.1971 2.78442L8.25556 7.41539L3.28596 7.84583C3.28549 7.84586 3.28502 7.8459 3.28455 7.84594C2.7945 7.8874 2.32754 8.07248 1.94214 8.37804C1.55638 8.68388 1.2695 9.09682 1.11748 9.56506C0.965459 10.0333 0.955069 10.536 1.08761 11.0101L2.05069 10.7409L1.08761 11.0101C1.21983 11.4831 1.48842 11.9066 1.85981 12.2279L5.63577 15.5275L4.50617 20.4294C4.50605 20.43 4.50592 20.4305 4.50579 20.4311C4.39454 20.9092 4.42654 21.4096 4.59781 21.8697C4.76928 22.3303 5.07273 22.7302 5.47023 23.0193C5.86773 23.3085 6.34163 23.474 6.8327 23.4952C7.32376 23.5165 7.81019 23.3925 8.23118 23.1388L8.23442 23.1368L12.4968 20.546L16.7692 23.1373Z"
|
||||
fill="#2F4858"
|
||||
stroke="white"
|
||||
stroke-width="2"></path>
|
||||
</svg>
|
||||
<span>{returnAvgForRating(rating)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.review-list-container {
|
||||
width: 100%;
|
||||
max-width: var(--normal-max-width);
|
||||
.simplebar-wrapper {
|
||||
padding-bottom: 1.2rem;
|
||||
.simplebar-content {
|
||||
max-width: var(--normal-max-width);
|
||||
& > .inner-wrapper {
|
||||
display: flex;
|
||||
gap: 2.2rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.simplebar-track {
|
||||
background-color: rgba(13, 12, 12, 0.25);
|
||||
height: 7px;
|
||||
overflow: visible;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.simplebar-scrollbar {
|
||||
transition-duration: 0ms !important;
|
||||
cursor: pointer;
|
||||
&::before {
|
||||
background-color: var(--bg-100);
|
||||
top: -2px;
|
||||
opacity: 1;
|
||||
border-radius: 0;
|
||||
height: 11px;
|
||||
left: 0px;
|
||||
|
||||
transition-delay: 0s;
|
||||
}
|
||||
}
|
||||
.review-list {
|
||||
display: flex;
|
||||
|
||||
gap: 1.2rem;
|
||||
height: 400px;
|
||||
align-items: flex-start;
|
||||
.review-item {
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
.upper-box {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
position: relative;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, #000100);
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
position: absolute;
|
||||
&:first-of-type {
|
||||
z-index: -1;
|
||||
}
|
||||
&:last-of-type {
|
||||
z-index: -2;
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: 1.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
h3 {
|
||||
color: var(--neutral-white);
|
||||
font-family: Outfit;
|
||||
font-size: 1rem;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: normal;
|
||||
}
|
||||
p {
|
||||
color: var(--neutral-white);
|
||||
font-family: Poly;
|
||||
font-size: 1rem;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
.date {
|
||||
color: var(--neutral-white);
|
||||
border-top: 1px solid var(--neutral-white);
|
||||
font-size: 0.7rem;
|
||||
padding-top: 6px;
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
.rating-bar {
|
||||
width: 100%;
|
||||
height: 9px;
|
||||
background: rgba(47, 72, 88, 0.2);
|
||||
.rating-filled {
|
||||
background: var(--text-invers-100);
|
||||
height: 100%;
|
||||
left: 0px;
|
||||
position: relative;
|
||||
.star-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
padding: 0spx;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0%;
|
||||
transform: translate(50%, -35%);
|
||||
span {
|
||||
font-family: Outfit;
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 14px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,203 @@
|
||||
<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>
|
||||
48
frontend/src/lib/components/pagebuilder/blocks/Step.svelte
Normal file
48
frontend/src/lib/components/pagebuilder/blocks/Step.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<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>
|
||||
172
frontend/src/lib/components/pagebuilder/blocks/Steps.svelte
Normal file
172
frontend/src/lib/components/pagebuilder/blocks/Steps.svelte
Normal file
@@ -0,0 +1,172 @@
|
||||
<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>
|
||||
@@ -0,0 +1,52 @@
|
||||
<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>
|
||||
@@ -0,0 +1,27 @@
|
||||
<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>
|
||||
@@ -0,0 +1,13 @@
|
||||
<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}
|
||||
@@ -0,0 +1,193 @@
|
||||
<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>
|
||||
@@ -0,0 +1,342 @@
|
||||
<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>
|
||||
143
frontend/src/lib/components/pagebuilder/blocks/form/Input.svelte
Normal file
143
frontend/src/lib/components/pagebuilder/blocks/form/Input.svelte
Normal file
@@ -0,0 +1,143 @@
|
||||
<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>
|
||||
@@ -0,0 +1,102 @@
|
||||
<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>
|
||||
66
frontend/src/lib/components/pagebuilder/blocks/index.ts
Normal file
66
frontend/src/lib/components/pagebuilder/blocks/index.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
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
|
||||
@@ -36,31 +36,7 @@
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
<section class="content-loading">
|
||||
<p>Inhalt wird geladen …</p>
|
||||
</section>
|
||||
{:else if errorMessage}
|
||||
<section class="content-error">
|
||||
<p>{errorMessage}</p>
|
||||
</section>
|
||||
{:else if contentEntry}
|
||||
<article class="content-article">
|
||||
<header>
|
||||
<h1>{contentEntry.meta?.title ?? contentEntry.name}</h1>
|
||||
{#if contentEntry.meta?.description}
|
||||
<p class="lead">{contentEntry.meta.description}</p>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
{#if contentEntry.blocks?.length}
|
||||
<section class="content-debug">
|
||||
<h2>Struktur</h2>
|
||||
<pre>{JSON.stringify(contentEntry.blocks, null, 2)}</pre>
|
||||
</section>
|
||||
{:else}
|
||||
<p>Für diesen Inhalt sind noch keine Bausteine definiert.</p>
|
||||
{/if}
|
||||
</article>
|
||||
{:else}
|
||||
<NotFound />
|
||||
{/if}
|
||||
|
||||
0
frontend/src/routes/StaticHomepage.svelte
Normal file
0
frontend/src/routes/StaticHomepage.svelte
Normal file
Reference in New Issue
Block a user