Initial commit
This commit is contained in:
320
frontend/src/App.svelte
Normal file
320
frontend/src/App.svelte
Normal file
@@ -0,0 +1,320 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
backgroundImages,
|
||||
categories,
|
||||
isMobile,
|
||||
location,
|
||||
modules,
|
||||
newNotification,
|
||||
openModal,
|
||||
selfImprovementChapters,
|
||||
shopStatus,
|
||||
wishlist,
|
||||
} from "./lib/store"
|
||||
import Footer from "./lib/components/Footer.svelte"
|
||||
import Header from "./lib/components/header/Header.svelte"
|
||||
import Notifications from "./lib/components/widgets/Notifications.svelte"
|
||||
import { baseURL } from "./config"
|
||||
import Product from "./routes/Product.svelte"
|
||||
import SidebarOverlay from "./lib/components/SidebarOverlay.svelte"
|
||||
import SSRSkip from "./lib/components/SSRSkip.svelte"
|
||||
import DateModal from "./lib/components/widgets/DateModal.svelte"
|
||||
import Content from "./routes/Content.svelte"
|
||||
import Products from "./routes/Products.svelte"
|
||||
import Profile from "./routes/Profile.svelte"
|
||||
import { getWishlist } from "./lib/functions/CommerceAPIs/tibiEndpoints/wishlist"
|
||||
import PublicProfile from "./routes/PublicProfile.svelte"
|
||||
import { api, getDBEntries } from "./api"
|
||||
import NewsletterRow from "./lib/components/pagebuilder/blocks/NewsletterRow.svelte"
|
||||
import HelpCenter from "./routes/HelpCenter.svelte"
|
||||
import { checkIfStoreIsOpen } from "./lib/functions/CommerceAPIs/tibiEndpoints/store"
|
||||
import Input from "./lib/components/pagebuilder/blocks/form/Input.svelte"
|
||||
import { onChange } from "./lib/components/pagebuilder/profile/helper"
|
||||
import RedirectToPublicProfile from "./routes/RedirectToPublicProfile.svelte"
|
||||
import { onMount } from "svelte"
|
||||
import ActionApproval from "./lib/components/widgets/ActionApproval.svelte"
|
||||
import KrassKraftChapter from "./routes/KrassKraftChapter.svelte"
|
||||
|
||||
export let url = ""
|
||||
if (url) {
|
||||
// ssr
|
||||
let l = url.split("?")
|
||||
$location = {
|
||||
path: l[0],
|
||||
search: l.length > 1 ? decodeURIComponent(`?${l[1]}`) : "",
|
||||
hash: "",
|
||||
push: false,
|
||||
pop: false,
|
||||
url: `${baseURL}/${l[0]}?${l[1]}`,
|
||||
}
|
||||
}
|
||||
|
||||
let oldPath: string
|
||||
location.subscribe((l) => {
|
||||
if (l.push && oldPath != l.path) window.scrollTo(0, 0)
|
||||
oldPath = l.path
|
||||
})
|
||||
getWishlist().then(wishlist.set)
|
||||
|
||||
api(`module`, {
|
||||
method: "GET",
|
||||
}).then((res) => {
|
||||
$modules = res.data
|
||||
})
|
||||
|
||||
api("productBackgroundImage", {
|
||||
method: "GET",
|
||||
}).then((res) => {
|
||||
res.data.forEach((element) => {
|
||||
$backgroundImages[element.type] = element.image
|
||||
})
|
||||
})
|
||||
|
||||
checkIfStoreIsOpen().then((status) => {
|
||||
if ($shopStatus.loggedIn) return
|
||||
if (!status || status.status === "open") {
|
||||
shopStatus.set({
|
||||
status: "open",
|
||||
loggedIn: true,
|
||||
password: status?.password,
|
||||
})
|
||||
} else {
|
||||
shopStatus.set({
|
||||
status: "login",
|
||||
loggedIn: false,
|
||||
password: status.password,
|
||||
})
|
||||
}
|
||||
})
|
||||
onMount(() => {
|
||||
const updateModalState = () => {
|
||||
$openModal = document.getElementsByClassName("dialog-open").length > 0
|
||||
}
|
||||
const interval = setInterval(updateModalState, 100)
|
||||
return () => clearInterval(interval)
|
||||
})
|
||||
let login = {
|
||||
password: "",
|
||||
}
|
||||
let googleCookiesAllowed = false
|
||||
let googleCookieName = "googleAnalytics"
|
||||
let metaPixelCookieName = "metaPixel"
|
||||
let metaPixelCookiesAllowed = false
|
||||
getDBEntries("selfImprovementChapter").then((res) => {
|
||||
$selfImprovementChapters = res
|
||||
})
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("ccAccept", (e) => {
|
||||
// @ts-ignore
|
||||
if (e.detail.includes(googleCookieName)) googleCookiesAllowed = true
|
||||
if (e.detail.includes(metaPixelCookieName)) metaPixelCookiesAllowed = true
|
||||
})
|
||||
function checkCookie(cookieName: string) {
|
||||
var allCookies = decodeURIComponent(document.cookie)
|
||||
var cookies = allCookies.split(";")
|
||||
var ccTagCookies: string[] = []
|
||||
cookies.forEach((e) => {
|
||||
e.includes("ccTags") ? (ccTagCookies = e.split(",")) : void 0
|
||||
})
|
||||
for (var i = 0; i < ccTagCookies.length; i++) {
|
||||
var c = ccTagCookies[i]
|
||||
while (c.charAt(0) == " ") c = c.substring(1)
|
||||
if (c == cookieName) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
googleCookiesAllowed = checkCookie(googleCookieName)
|
||||
metaPixelCookiesAllowed = checkCookie(metaPixelCookieName)
|
||||
}
|
||||
|
||||
let innerWidth = 0
|
||||
$: $isMobile = innerWidth < 1700
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth="{innerWidth}" />
|
||||
<svelte:head>
|
||||
{#if googleCookiesAllowed}
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-SH85R88QE0"
|
||||
></script>
|
||||
<script>
|
||||
console.log("GoogleCookiesAllowed ist aktiv.")
|
||||
window.dataLayer = window.dataLayer || []
|
||||
function gtag() {
|
||||
dataLayer.push(arguments)
|
||||
}
|
||||
gtag("js", new Date())
|
||||
gtag("config", "G-SH85R88QE0")
|
||||
</script>
|
||||
{/if}
|
||||
|
||||
{#if metaPixelCookiesAllowed}
|
||||
<!-- Meta Pixel Code -->
|
||||
<!-- Meta Pixel Code -->
|
||||
<!-- Meta Pixel Code -->
|
||||
<!-- Meta Pixel Code -->
|
||||
<!-- Meta Pixel Code -->
|
||||
<!-- Meta Pixel Code -->
|
||||
<!-- Meta Pixel Code -->
|
||||
<!-- Meta Pixel Code -->
|
||||
<!-- Meta Pixel Code -->
|
||||
<script>
|
||||
console.log("MetaPixelCookiesAllowed ist aktiv.")
|
||||
!(function (f, b, e, v, n, t, s) {
|
||||
if (f.fbq) return
|
||||
n = f.fbq = function () {
|
||||
n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments)
|
||||
}
|
||||
if (!f._fbq) f._fbq = n
|
||||
n.push = n
|
||||
n.loaded = !0
|
||||
n.version = "2.0"
|
||||
n.queue = []
|
||||
t = b.createElement(e)
|
||||
t.async = !0
|
||||
t.src = v
|
||||
s = b.getElementsByTagName(e)[0]
|
||||
s.parentNode.insertBefore(t, s)
|
||||
})(window, document, "script", "https://connect.facebook.net/en_US/fbevents.js")
|
||||
fbq("init", "1117933239951751")
|
||||
fbq("track", "PageView")
|
||||
</script>
|
||||
<noscript
|
||||
><img
|
||||
height="1"
|
||||
width="1"
|
||||
style="display: none"
|
||||
src="https://www.facebook.com/tr?id=1117933239951751&ev=PageView&noscript=1"
|
||||
/></noscript
|
||||
>
|
||||
<!-- End Meta Pixel Code -->
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<div>
|
||||
<SidebarOverlay />
|
||||
<main>
|
||||
<Header />
|
||||
{#if $shopStatus.status === "open" || $shopStatus.loggedIn}
|
||||
{#if $location?.path?.toLowerCase().startsWith("/product/")}
|
||||
{#key $location?.path}
|
||||
<Product handle="{$location.path.split('/product/')[1]}" />
|
||||
{/key}
|
||||
{:else if $location?.path?.toLowerCase().startsWith("/redirecttoprofile/")}
|
||||
<RedirectToPublicProfile
|
||||
id="{$location.path.toLowerCase().split('/redirecttoprofile/').filter(Boolean).pop()}"
|
||||
/>
|
||||
{:else if $location?.path?.toLowerCase().startsWith("/@")}
|
||||
<PublicProfile />
|
||||
{:else if $location?.path?.toLowerCase().startsWith("/collections/")}
|
||||
{#key $location?.path}
|
||||
{#if !!$categories.length}
|
||||
<Products
|
||||
categoryPath="{$location?.path?.split('/').filter((c) => c !== 'collections' && !!c)}"
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
{:else if $location?.path?.toLowerCase().startsWith("/profile")}
|
||||
{#key $location?.path}
|
||||
<Profile />
|
||||
{/key}
|
||||
{:else if $location?.path?.toLowerCase().startsWith("/helpcenter")}
|
||||
<HelpCenter location="{$location}" />
|
||||
{:else if $location?.path?.toLowerCase().startsWith("/selfimprovement/krasskraft")}
|
||||
<KrassKraftChapter />
|
||||
{:else}
|
||||
<Content location="{$location}" />
|
||||
{/if}
|
||||
{:else}
|
||||
<form
|
||||
on:submit|preventDefault|stopPropagation="{() => {
|
||||
if (login.password === $shopStatus.password) {
|
||||
shopStatus.set({
|
||||
status: 'login',
|
||||
loggedIn: true,
|
||||
password: login.password,
|
||||
})
|
||||
newNotification({
|
||||
class: 'success',
|
||||
html: 'Erfolgreich eingeloggt',
|
||||
})
|
||||
} else {
|
||||
newNotification({
|
||||
class: 'error',
|
||||
html: 'Falsches Passwort',
|
||||
})
|
||||
}
|
||||
}}"
|
||||
>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
bind:value="{login.password}"
|
||||
placeholder="Passwort"
|
||||
onChange="{onChange}"
|
||||
/>
|
||||
<button
|
||||
class="cta primary"
|
||||
type="submit">Login</button
|
||||
>
|
||||
</form>
|
||||
{/if}
|
||||
<div class="crossGap"></div>
|
||||
<Footer />
|
||||
</main>
|
||||
|
||||
<SSRSkip />
|
||||
<Notifications />
|
||||
</div>
|
||||
<ActionApproval />
|
||||
<DateModal />
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
@import "./lib/assets/css/variables.less";
|
||||
@import "./lib/assets/css/main.less";
|
||||
@import "swiper/swiper-bundle.min.css";
|
||||
@import "swiper/modules/effect-fade/effect-fade";
|
||||
@import "swiper/modules/navigation/navigation";
|
||||
@import "swiper/modules/pagination/pagination";
|
||||
@import "../assets/fonts/fonts.css";
|
||||
@import "./lib/assets/css/formular.less";
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: "Outfit", sans-serif;
|
||||
background-color: var(--bg-100);
|
||||
button {
|
||||
font-family: "Outfit", sans-serif;
|
||||
}
|
||||
@media @mobile {
|
||||
font-size: 16px;
|
||||
}
|
||||
@media @min-tablet {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--neutral-white);
|
||||
min-height: 100vh;
|
||||
align-items: center;
|
||||
.crossGap {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
42
frontend/src/admin.ts
Normal file
42
frontend/src/admin.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { SvelteComponent } from "svelte"
|
||||
import ContentBlock from "./lib/components/pagebuilder/ContentBlock.svelte"
|
||||
import ColumnsColumn from "./lib/components/pagebuilder/blocks/ColumnsColumn.svelte"
|
||||
|
||||
function getRenderedElement(
|
||||
component: typeof SvelteComponent,
|
||||
options?: { props: { [key: string]: any }; addCss?: string[] },
|
||||
nestedElements?: { tagName: string; className?: string }[]
|
||||
) {
|
||||
const el = document.createElement("div")
|
||||
el.attachShadow({ mode: "open" })
|
||||
|
||||
const body = document.createElement("body")
|
||||
|
||||
// build nested divs with css classes
|
||||
let target: HTMLElement = body
|
||||
nestedElements?.forEach((e) => {
|
||||
const newElement = document.createElement(e.tagName)
|
||||
if (e.className) newElement.className = e.className
|
||||
target.appendChild(newElement)
|
||||
target = newElement
|
||||
})
|
||||
|
||||
el.shadowRoot.appendChild(body)
|
||||
|
||||
options?.addCss?.forEach((css) => {
|
||||
const link = document.createElement("link")
|
||||
link.rel = "stylesheet"
|
||||
link.href = css
|
||||
link.type = "text/css"
|
||||
el.shadowRoot.appendChild(link)
|
||||
})
|
||||
|
||||
new component({
|
||||
target: target,
|
||||
props: options?.props,
|
||||
})
|
||||
|
||||
return el
|
||||
}
|
||||
|
||||
export { ContentBlock, ColumnsColumn, getRenderedElement }
|
||||
377
frontend/src/api.ts
Normal file
377
frontend/src/api.ts
Normal file
@@ -0,0 +1,377 @@
|
||||
import { get } from "svelte/store"
|
||||
import { apiRequest, obj2str } from "../../api/hooks/lib/ssr"
|
||||
import { apiBaseOverride, loadingStore, login, newNotification } from "./lib/store"
|
||||
import { postLogin } from "./lib/functions/CommerceAPIs/tibiEndpoints/customer"
|
||||
import { apiClientBaseURL } from "../../api/hooks/config-client"
|
||||
import * as sentry from "./sentry"
|
||||
/////////////////// LOADING BAR
|
||||
export const debounce = (callback: Function, wait: number) => {
|
||||
let timeout: number | NodeJS.Timeout | null = null
|
||||
return (...args: any[]) => {
|
||||
const next = () => callback(...args)
|
||||
clearTimeout(timeout as number)
|
||||
timeout = setTimeout(next, wait)
|
||||
}
|
||||
}
|
||||
|
||||
interface RequestProgressState {
|
||||
downLoaded: number
|
||||
downTotal: number
|
||||
upLoaded: number
|
||||
upTotal: number
|
||||
}
|
||||
|
||||
const inProgess = new Map<object, RequestProgressState>()
|
||||
|
||||
function updateProgressMap(ref: object, active: boolean, direction?: "up" | "down", loaded?: number, total?: number) {
|
||||
let state = inProgess.get(ref)
|
||||
if (active) {
|
||||
if (!state) {
|
||||
state = {
|
||||
downLoaded: 0,
|
||||
downTotal: 0,
|
||||
upLoaded: 0,
|
||||
upTotal: 0,
|
||||
}
|
||||
}
|
||||
if (direction === "down") {
|
||||
state.downLoaded = loaded || 0
|
||||
state.downTotal = total || 0
|
||||
}
|
||||
if (direction === "up") {
|
||||
state.upLoaded = loaded || 0
|
||||
state.upTotal = total || 0
|
||||
}
|
||||
inProgess.set(ref, state)
|
||||
} else if (state) {
|
||||
inProgess.delete(ref)
|
||||
}
|
||||
|
||||
setLoadingStore()
|
||||
}
|
||||
|
||||
const setLoadingStore = debounce(() => {
|
||||
let active = false
|
||||
let loaded = 0
|
||||
let total = 0
|
||||
for (let state of inProgess.values()) {
|
||||
active = true
|
||||
loaded += state.downLoaded + state.upLoaded
|
||||
total += state.downTotal + state.upTotal
|
||||
}
|
||||
loadingStore.set({
|
||||
active,
|
||||
loaded,
|
||||
total,
|
||||
})
|
||||
}, 100)
|
||||
|
||||
export const xhrApiCall = async <T>(
|
||||
endpoint: string,
|
||||
options: ApiOptions = {},
|
||||
body?: any,
|
||||
showProgress = true
|
||||
): Promise<{ data: T; count?: number; status: number }> => {
|
||||
const ref = {}
|
||||
if (showProgress) updateProgressMap(ref, true)
|
||||
|
||||
if (!options.headers) options.headers = {}
|
||||
if (options.method) options.method = options.method.toUpperCase()
|
||||
if (options.useJwt) options.headers.Authorization = `Bearer ${(await getLogin())?.tokenString}`
|
||||
|
||||
let xhrParams: Record<string, any> = { count: 1 }
|
||||
if (options.filter) xhrParams["filter"] = encodeURIComponent(JSON.stringify(options.filter))
|
||||
if (options.sort) xhrParams["sort"] = options.sort
|
||||
if (options.limit) xhrParams["limit"] = options.limit
|
||||
if (options.offset) xhrParams["offset"] = options.offset
|
||||
if (options.projection) xhrParams["projection"] = options.projection
|
||||
if (options.lookup) xhrParams["lookup"] = options.lookup
|
||||
|
||||
if (options.params) {
|
||||
Object.keys(options.params).forEach((p) => {
|
||||
xhrParams[p] = encodeURIComponent(options.params[p])
|
||||
})
|
||||
}
|
||||
|
||||
let method = options.method || "GET"
|
||||
let url = (endpoint.startsWith("/") ? "" : apiClientBaseURL) + endpoint
|
||||
let queryString = new URLSearchParams(xhrParams).toString()
|
||||
if (queryString) url += `?${queryString}`
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
|
||||
xhr.open(method, url, true)
|
||||
|
||||
Object.keys(options.headers).forEach((key) => {
|
||||
xhr.setRequestHeader(key, options.headers[key])
|
||||
})
|
||||
|
||||
xhr.upload.onprogress = (event) => {
|
||||
if (showProgress) {
|
||||
updateProgressMap(ref, true, "up", event.loaded, event.total)
|
||||
if (options.onUploadProgress) {
|
||||
options.onUploadProgress(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
xhr.onprogress = (event) => {
|
||||
if (showProgress) {
|
||||
updateProgressMap(ref, true, "down", event.loaded, event.total)
|
||||
if (options.onDownloadProgress) {
|
||||
options.onDownloadProgress(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
xhr.onload = () => {
|
||||
updateProgressMap(ref, false)
|
||||
const responseHeaders = xhr.getAllResponseHeaders()
|
||||
const headers: Record<string, string> = {}
|
||||
responseHeaders
|
||||
.trim()
|
||||
.split(/[\r\n]+/)
|
||||
.forEach((line) => {
|
||||
const parts = line.split(": ")
|
||||
const header = parts.shift()
|
||||
const value = parts.join(": ")
|
||||
headers[header] = value
|
||||
})
|
||||
|
||||
const response = {
|
||||
data: JSON.parse(xhr.responseText),
|
||||
count: parseInt(headers["x-results-count"], 10),
|
||||
status: xhr.status,
|
||||
}
|
||||
|
||||
if (xhr.status < 200 || xhr.status >= 400) {
|
||||
if (xhr.status === 401) {
|
||||
login.set(null)
|
||||
newNotification({
|
||||
html: "Nicht autorisiert. Bitte melde dich sich erneut an.",
|
||||
class: "error",
|
||||
})
|
||||
}
|
||||
reject(response)
|
||||
} else {
|
||||
resolve(response)
|
||||
}
|
||||
}
|
||||
|
||||
xhr.onerror = () => {
|
||||
updateProgressMap(ref, false)
|
||||
reject({
|
||||
data: {
|
||||
...xhr.response,
|
||||
},
|
||||
status: xhr.status,
|
||||
count: 0,
|
||||
})
|
||||
}
|
||||
|
||||
if (method === "PUT" || method === "POST") {
|
||||
xhr.setRequestHeader("Content-Type", "application/json")
|
||||
xhr.send(JSON.stringify(body))
|
||||
} else {
|
||||
xhr.send()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// fetch polyfill
|
||||
// [MIT License](LICENSE.md) © [Jason Miller](https://jasonformat.com/)
|
||||
const _f = function (url: string, options?: { [key: string]: any }) {
|
||||
if (typeof XMLHttpRequest === "undefined") {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
options = options || {}
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = new XMLHttpRequest()
|
||||
const keys: string[] = []
|
||||
// @ts-ignore
|
||||
const all = []
|
||||
const headers = {}
|
||||
|
||||
const response = () => ({
|
||||
ok: ((request.status / 100) | 0) == 2, // 200-299
|
||||
statusText: request.statusText,
|
||||
status: request.status,
|
||||
url: request.responseURL,
|
||||
text: () => Promise.resolve(request.responseText),
|
||||
json: () => Promise.resolve(request.responseText).then(JSON.parse),
|
||||
blob: () => Promise.resolve(new Blob([request.response])),
|
||||
clone: response,
|
||||
headers: {
|
||||
// @ts-ignore
|
||||
keys: () => keys,
|
||||
// @ts-ignore
|
||||
entries: () => all,
|
||||
// @ts-ignore
|
||||
get: (n) => headers[n.toLowerCase()],
|
||||
// @ts-ignore
|
||||
has: (n) => n.toLowerCase() in headers,
|
||||
},
|
||||
})
|
||||
|
||||
request.open(options.method || "get", url, true)
|
||||
|
||||
request.onload = () => {
|
||||
request
|
||||
.getAllResponseHeaders()
|
||||
// @ts-ignore
|
||||
.replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm, (m, key, value) => {
|
||||
keys.push((key = key.toLowerCase()))
|
||||
all.push([key, value])
|
||||
// @ts-ignore
|
||||
headers[key] = headers[key] ? `${headers[key]},${value}` : value
|
||||
})
|
||||
resolve(response())
|
||||
}
|
||||
|
||||
request.onerror = reject
|
||||
|
||||
request.withCredentials = options.credentials == "include"
|
||||
|
||||
for (const i in options.headers) {
|
||||
request.setRequestHeader(i, options.headers[i])
|
||||
}
|
||||
|
||||
request.send(options.body || null)
|
||||
})
|
||||
}
|
||||
|
||||
// fetch must be declared after sentry import to get the hijacked fetch
|
||||
// @ts-ignore
|
||||
export const _fetch: typeof fetch =
|
||||
typeof fetch === "undefined" ? (typeof window === "undefined" ? _f : window.fetch || _f) : fetch
|
||||
|
||||
export const api = async <T>(
|
||||
endpoint: string,
|
||||
options?: ApiOptions,
|
||||
body?: any
|
||||
): Promise<{ data: T; count: number } | any> => {
|
||||
const _apiBaseOverride = get(apiBaseOverride) || ""
|
||||
if (!options.headers) options.headers = {}
|
||||
if (options.method) options.method = options.method.toUpperCase()
|
||||
if (options.useJwt) {
|
||||
if (options.jwtToUse) options.headers.Authorization = `Bearer ${options.jwtToUse}`
|
||||
else {
|
||||
try {
|
||||
let login = await getLogin()
|
||||
options.headers.Authorization = `Bearer ${login?.tokenString}`
|
||||
} catch (e) {
|
||||
return { data: { data: null } }
|
||||
}
|
||||
}
|
||||
}
|
||||
let data = await apiRequest(_apiBaseOverride + endpoint, options, body, sentry, _fetch)
|
||||
|
||||
return data
|
||||
}
|
||||
let refreshingToken: Promise<Login> | null
|
||||
export const getLogin = async (signal?: AbortSignal): Promise<Login | null> => {
|
||||
let _l = get(login)
|
||||
if ((_l?.tokenData?.exp || 0) - 60 < new Date().getTime() / 1000) {
|
||||
// token expired, relogin via refresh cookie, run only one in parallel
|
||||
if (!refreshingToken)
|
||||
refreshingToken = postLogin({
|
||||
email: null,
|
||||
password: null,
|
||||
})
|
||||
|
||||
try {
|
||||
_l = await refreshingToken
|
||||
} finally {
|
||||
refreshingToken = null
|
||||
}
|
||||
}
|
||||
|
||||
return _l
|
||||
}
|
||||
// init login state
|
||||
getLogin().catch((e) => {
|
||||
console.log("getting login via refresh cookie failed:", e)
|
||||
})
|
||||
const cache: {
|
||||
[key: string]: {
|
||||
expire: number
|
||||
data: any
|
||||
}
|
||||
} = {}
|
||||
|
||||
type EntryTypeSwitch<T> = T extends "medialib"
|
||||
? MedialibEntry
|
||||
: T extends "content"
|
||||
? ContentEntry
|
||||
: T extends "navigation"
|
||||
? NavigationEntry
|
||||
: T extends "tag"
|
||||
? TagEntry
|
||||
: T extends "dummyCartEndpoint"
|
||||
? DummyCartEndpoint
|
||||
: T extends "productBenefit"
|
||||
? ProductBenefit
|
||||
: T extends "selfImprovementChapter"
|
||||
? SelfImprovementChapter
|
||||
: T extends "rating"
|
||||
? ProductRating
|
||||
: T extends "bigCommerceProduct"
|
||||
? LocalProduct
|
||||
: T extends "selfImprovementChallenge"
|
||||
? BKDFChallenge
|
||||
: never
|
||||
|
||||
export async function getDBEntries<T extends CollectionName>(
|
||||
collectionName: T,
|
||||
filter?: { [key: string]: any },
|
||||
sort: string = "sort",
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
projection?: string
|
||||
): Promise<EntryTypeSwitch<T>[]> {
|
||||
const c = await api<EntryTypeSwitch<T>[]>(collectionName, { filter, sort, limit, offset, projection })
|
||||
return c.data
|
||||
}
|
||||
|
||||
export async function getCachedEntries<T extends CollectionName>(
|
||||
collectionName: T,
|
||||
filter?: { [key: string]: any },
|
||||
sort: string = "sort",
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
projection?: string
|
||||
): Promise<EntryTypeSwitch<T>[]> {
|
||||
const filterStr = obj2str({ collectionName, filter, sort, limit, offset, projection })
|
||||
if (cache[filterStr] && cache[filterStr].expire >= Date.now()) {
|
||||
return cache[filterStr].data
|
||||
}
|
||||
const entries = await getDBEntries<T>(collectionName, filter, sort, limit, offset, projection)
|
||||
const inOneHour = Date.now() + 1000 * 60 * 60
|
||||
cache[filterStr] = { expire: inOneHour, data: entries }
|
||||
return entries
|
||||
}
|
||||
|
||||
export async function getDBEntry<T extends CollectionName>(collectionName: T, filter: { [key: string]: any }) {
|
||||
return (await getDBEntries<T>(collectionName, filter, "_id"))?.[0]
|
||||
}
|
||||
|
||||
export async function getCachedEntry<T extends CollectionName>(collectionName: T, filter: { [key: string]: any }) {
|
||||
return (await getCachedEntries<T>(collectionName, filter, "_id"))?.[0]
|
||||
}
|
||||
|
||||
export async function postDBEntry<T extends CollectionName>(collectionName: T, entry: EntryTypeSwitch<T>) {
|
||||
return api<EntryTypeSwitch<T>>(collectionName, { method: "POST" }, entry)
|
||||
}
|
||||
|
||||
export async function putDBEntry<T extends CollectionName>(collectionName: T, entry: EntryTypeSwitch<Partial<T>>) {
|
||||
return api<EntryTypeSwitch<T>>(collectionName + "/" + entry.id, { method: "PUT" }, entry)
|
||||
}
|
||||
|
||||
export async function deleteDBEntry<T extends CollectionName>(collectionName: T, id: string) {
|
||||
return api<EntryTypeSwitch<T>>(collectionName + "/" + id, {
|
||||
method: "DELETE",
|
||||
useJwt: true,
|
||||
})
|
||||
}
|
||||
249
frontend/src/config.ts
Normal file
249
frontend/src/config.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import configClient from "../../api/hooks/config-client"
|
||||
import * as sentry from "./sentry"
|
||||
|
||||
export const sentryDSN = "https://2ec76e3f86078b8020f23269f207e7b3@sentry.basehosts.de/5"
|
||||
export const sentryTracingOrigins = [
|
||||
"localhost",
|
||||
"bkdf-tibi-2024.code.testversion.online",
|
||||
"www.binkrassdufass.de",
|
||||
"binkrassdufass.de",
|
||||
/^\//,
|
||||
/binkrassdufass/,
|
||||
/bkdf-tibi-2024/,
|
||||
/bkdf/,
|
||||
]
|
||||
export const sentryEnvironment: string = "local"
|
||||
export const release = configClient.release
|
||||
console.log("Release: ", release)
|
||||
|
||||
// need to execute early for fetch wrapping
|
||||
sentry.init(sentryDSN, sentryTracingOrigins, sentryEnvironment, release)
|
||||
|
||||
const createCartRedirectUrl = () => {
|
||||
const localCache: CheckoutCache = {
|
||||
activeCartId: null,
|
||||
data: null,
|
||||
}
|
||||
|
||||
return async (cartId: string): Promise<StorefrontCheckoutResponse> => {
|
||||
if (localCache.activeCartId !== cartId || !localCache.data) {
|
||||
const response = await fetch(
|
||||
`${bigcommerceBaseURL}/stores/${bigcommerceStoreHash}/v3/carts/${cartId}/redirect_urls`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"content-type": "application/json",
|
||||
"x-auth-token": storefrontToken,
|
||||
},
|
||||
}
|
||||
)
|
||||
const data = (await response.json()) as StorefrontCheckoutResponse
|
||||
|
||||
localCache.activeCartId = cartId
|
||||
localCache.data = data
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
return localCache.data
|
||||
}
|
||||
}
|
||||
const domain = "www.binkrassdufass.de"
|
||||
const storefrontTokens = {
|
||||
lokal: "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJjaWQiOlsxXSwiY29ycyI6WyJodHRwczovL2Jpbi1rcmFzcy1kdS1mYXNzLm15YmlnY29tbWVyY2UuY29tIiwiaHR0cHM6Ly9ia2RmLXRpYmktMjAyNC5jb2RlLnRlc3R2ZXJzaW9uLm9ubGluZSJdLCJlYXQiOjIxNDc0ODMzNDMsImlhdCI6MTcxOTk0NDg5NywiaXNzIjoiQkMiLCJzaWQiOjEwMDMxODEwMTksInN1YiI6IjN6eDVkd2Q5bjloN3c3MmQyeWc4ODQ1ZzJiMjhtNXgiLCJzdWJfdHlwZSI6MiwidG9rZW5fdHlwZSI6MX0.A8NcMd9tbhZqLZ1USaIWcE_CY7OYdrNL8ASvgtg6RTimC2xZxGpiNLOkJAN1CjeRyb_SO9vyFVlArK8H9i8VGg",
|
||||
live: "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJjaWQiOlsxXSwiY29ycyI6WyJodHRwczovL2Jpbi1rcmFzcy1kdS1mYXNzLm15YmlnY29tbWVyY2UuY29tIiwiaHR0cHM6Ly9iaW5rcmFzc2R1ZmFzcy5kZSJdLCJlYXQiOjIxNDc0ODMzNDMsImlhdCI6MTcyMTI0NTc4NywiaXNzIjoiQkMiLCJzaWQiOjEwMDMxODEwMTksInN1YiI6IjN6eDVkd2Q5bjloN3c3MmQyeWc4ODQ1ZzJiMjhtNXgiLCJzdWJfdHlwZSI6MiwidG9rZW5fdHlwZSI6MX0.Ze8OLAj9PLVDa_327m0ExlTmcVksUb7aWtT4RZn6ThHvvVSh1wBJ0RcVkHv2f_lkGV54_St89QUZOcXcnWauVg",
|
||||
wwwLive:
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJjaWQiOlsxXSwiY29ycyI6WyJodHRwczovL2Jpbi1rcmFzcy1kdS1mYXNzLm15YmlnY29tbWVyY2UuY29tIiwiaHR0cHM6Ly93d3cuYmlua3Jhc3NkdWZhc3MuZGUiXSwiZWF0IjoyMTQ3NDgzMzQzLCJpYXQiOjE3MjEyNDY2ODAsImlzcyI6IkJDIiwic2lkIjoxMDAzMTgxMDE5LCJzdWIiOiIzeng1ZHdkOW45aDd3NzJkMnlnODg0NWcyYjI4bTV4Iiwic3ViX3R5cGUiOjIsInRva2VuX3R5cGUiOjF9.eT2KK_NputrRdSkgHIkoPtAPZ5SfDCNBM9VNs1uhPhsiKi4UFwX56rqdOqWq2d4VA7ahdckaiK8iCJSKDgIQ-w",
|
||||
tibiAdminLive:
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJjaWQiOlsxXSwiY29ycyI6WyJodHRwczovL2Jpbi1rcmFzcy1kdS1mYXNzLm15YmlnY29tbWVyY2UuY29tIiwiaHR0cHM6Ly9kZXYudGliaWNtcy5kZSJdLCJlYXQiOjIxNDc0ODMzNDMsImlhdCI6MTcyMjAyMTY4MSwiaXNzIjoiQkMiLCJzaWQiOjEwMDMxODEwMTksInN1YiI6IjN6eDVkd2Q5bjloN3c3MmQyeWc4ODQ1ZzJiMjhtNXgiLCJzdWJfdHlwZSI6MiwidG9rZW5fdHlwZSI6MX0.YH_QYb0J4MTqKDyi1i0ccrTg0KKb90ZNYGlpjnjOWwVllVveALe_EproFRP5R-E_5jNKA-lmu-sjYom0j2qzyw",
|
||||
tibiAdminLokal:
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJjaWQiOlsxXSwiY29ycyI6WyJodHRwczovL2Jpbi1rcmFzcy1kdS1mYXNzLm15YmlnY29tbWVyY2UuY29tIiwiaHR0cHM6Ly9ia2RmLXRpYmktMjAyNC10aWJpYWRtaW4tZGV2LmNvZGUudGVzdHZlcnNpb24ub25saW5lIl0sImVhdCI6MjE0NzQ4MzM0MywiaWF0IjoxNzIyMDIxNjMzLCJpc3MiOiJCQyIsInNpZCI6MTAwMzE4MTAxOSwic3ViIjoiM3p4NWR3ZDluOWg3dzcyZDJ5Zzg4NDVnMmIyOG01eCIsInN1Yl90eXBlIjoyLCJ0b2tlbl90eXBlIjoxfQ.lER0xvA6AVpLPVDqsEIntxbLR1vbQYBcYN44Sv8LRMFDIqug5ytWpzKCy5SwJ53jJZZzy6aBbz1MyUavpY9SGw",
|
||||
}
|
||||
function getStorefrontToken() {
|
||||
if (typeof window !== "undefined") {
|
||||
if (document.location.hostname.includes("dev.tibicms.de")) {
|
||||
return storefrontTokens.tibiAdminLive
|
||||
} else if (document.location.hostname.includes("bkdf-tibi-2024-tibiadmin-dev")) {
|
||||
return storefrontTokens.tibiAdminLokal
|
||||
} else if (document.location.hostname.includes("bkdf-tibi-2024")) {
|
||||
return storefrontTokens.lokal
|
||||
} else if (document.location.hostname.includes("binkrassdufass.de")) {
|
||||
if (document.location.hostname.includes("www")) {
|
||||
return storefrontTokens.wwwLive
|
||||
}
|
||||
return storefrontTokens.live
|
||||
} else {
|
||||
return storefrontTokens.wwwLive
|
||||
}
|
||||
} else {
|
||||
return storefrontTokens.wwwLive
|
||||
}
|
||||
}
|
||||
|
||||
export const apiBaseURL = "/api/",
|
||||
baseDomain = typeof window !== "undefined" ? document?.location?.hostname : domain,
|
||||
baseURL = "https://" + baseDomain,
|
||||
bigcommerceBaseURL = "https://store-punbvyqteo.mybigcommerce.com",
|
||||
bigcommerceStoreHash = "punbvyqteo",
|
||||
bigcommerceChannelId = 1578456, // oder 1 kp testen was geht und was nicht
|
||||
bigcommerceSiteId = 1001,
|
||||
storefrontToken = getStorefrontToken(),
|
||||
websiteName = "BinKrassDuFass",
|
||||
companyName = "Robin Grenzdörfer",
|
||||
streetAddress = "Eugen Richter Straße 3",
|
||||
localityAddress = "Erfurt",
|
||||
zipCode = "99085",
|
||||
regionAddress = "Thüringen",
|
||||
countryAddress = "Germany",
|
||||
email = "info@binkrassdufass.de",
|
||||
minimumForFreeShipping = 100,
|
||||
memoizedCartRedirectUrl = createCartRedirectUrl(),
|
||||
defaultSort: SortFilterItem = {
|
||||
title: "Relevance",
|
||||
slug: null,
|
||||
sortKey: "RELEVANCE",
|
||||
reverse: false,
|
||||
},
|
||||
sorting: SortFilterItem[] = [
|
||||
defaultSort,
|
||||
{ title: "Trending", slug: "trending-desc", sortKey: "BEST_SELLING", reverse: false }, // asc
|
||||
{ title: "Latest arrivals", slug: "latest-desc", sortKey: "CREATED_AT", reverse: true },
|
||||
{ title: "Price: Low to high", slug: "price-asc", sortKey: "PRICE", reverse: false }, // asc
|
||||
{ title: "Price: High to low", slug: "price-desc", sortKey: "PRICE", reverse: true },
|
||||
],
|
||||
TAGS = {
|
||||
collections: "collections",
|
||||
products: "products",
|
||||
cart: "cart",
|
||||
},
|
||||
HIDDEN_PRODUCT_TAG = "nextjs-frontend-hidden",
|
||||
DEFAULT_OPTION = "Default Title"
|
||||
|
||||
export enum BigCommerceSortKeys {
|
||||
A_TO_Z = "A_TO_Z",
|
||||
BEST_REVIEWED = "BEST_REVIEWED",
|
||||
BEST_SELLING = "BEST_SELLING",
|
||||
RELEVANCE = "RELEVANCE",
|
||||
FEATURED = "FEATURED",
|
||||
HIGHEST_PRICE = "HIGHEST_PRICE",
|
||||
LOWEST_PRICE = "LOWEST_PRICE",
|
||||
NEWEST = "NEWEST",
|
||||
Z_TO_A = "Z_TO_A",
|
||||
}
|
||||
|
||||
export enum BKDFSortKeys {
|
||||
RELEVANCE = "RELEVANCE",
|
||||
BEST_SELLING = "BEST_SELLING",
|
||||
CREATED_AT = "CREATED_AT",
|
||||
PRICE = "PRICE",
|
||||
}
|
||||
|
||||
export enum BKDFToBigCommerceSortKeys {
|
||||
RELEVANCE = "RELEVANCE",
|
||||
BEST_SELLING = "BEST_SELLING",
|
||||
CREATED_AT = "NEWEST",
|
||||
PRICE = "LOWEST_PRICE",
|
||||
PRICE_ON_REVERSE = "HIGHEST_PRICE",
|
||||
}
|
||||
// need to execute early for fetch wrapping
|
||||
//
|
||||
interface StorefrontCheckoutResponse {
|
||||
data?: {
|
||||
cart_url: string
|
||||
checkout_url: string
|
||||
embedded_checkout_url: string
|
||||
}
|
||||
status: number
|
||||
}
|
||||
|
||||
type CheckoutCache = {
|
||||
activeCartId: string | null
|
||||
data: StorefrontCheckoutResponse | null
|
||||
}
|
||||
|
||||
export const icons = {
|
||||
shoppingBag:
|
||||
"M12 3C11.4033 3 10.831 3.23705 10.409 3.65901C9.98709 4.08097 9.75003 4.65326 9.75003 5.25V5.51C10.307 5.5 10.918 5.5 11.59 5.5H12.411C13.081 5.5 13.693 5.5 14.251 5.51V5.25C14.251 4.95444 14.1928 4.66178 14.0797 4.38873C13.9665 4.11568 13.8007 3.8676 13.5917 3.65866C13.3826 3.44971 13.1345 3.284 12.8614 3.17098C12.5883 3.05797 12.2956 2.99987 12 3ZM15.75 5.578V5.25C15.75 4.25544 15.3549 3.30161 14.6517 2.59835C13.9484 1.89509 12.9946 1.5 12 1.5C11.0055 1.5 10.0516 1.89509 9.34838 2.59835C8.64512 3.30161 8.25003 4.25544 8.25003 5.25V5.578C8.10703 5.59 7.97003 5.604 7.83603 5.621C6.82603 5.746 5.99403 6.008 5.28603 6.595C4.57903 7.182 4.16803 7.952 3.85903 8.922C3.55903 9.862 3.33303 11.069 3.04903 12.588L3.02803 12.698C2.62603 14.841 2.31003 16.53 2.25103 17.861C2.19103 19.226 2.39503 20.356 3.16503 21.283C3.93503 22.211 5.00803 22.619 6.36003 22.812C7.68003 23 9.39703 23 11.578 23H12.423C14.603 23 16.321 23 17.64 22.812C18.992 22.619 20.066 22.211 20.836 21.283C21.606 20.356 21.808 19.226 21.749 17.861C21.691 16.53 21.374 14.841 20.972 12.698L20.952 12.588C20.667 11.069 20.44 9.861 20.142 8.922C19.832 7.952 19.422 7.182 18.714 6.595C18.007 6.008 17.174 5.745 16.164 5.621C16.0263 5.60411 15.8883 5.58977 15.75 5.578ZM8.02003 7.11C7.16503 7.215 6.64803 7.414 6.24403 7.75C5.84103 8.084 5.55003 8.555 5.28803 9.377C5.02103 10.217 4.81003 11.335 4.51403 12.914C4.09803 15.131 3.80303 16.714 3.75003 17.927C3.69803 19.117 3.89003 19.807 4.31903 20.326C4.74903 20.843 5.39203 21.158 6.57203 21.326C7.77203 21.498 9.38403 21.5 11.64 21.5H12.36C14.617 21.5 16.227 21.498 17.428 21.327C18.608 21.158 19.251 20.843 19.681 20.326C20.111 19.808 20.302 19.118 20.251 17.926C20.197 16.715 19.902 15.131 19.486 12.914C19.19 11.334 18.98 10.218 18.712 9.377C18.45 8.555 18.16 8.084 17.756 7.749C17.352 7.414 16.836 7.215 15.98 7.109C15.104 7.001 13.967 7 12.36 7H11.64C10.033 7 8.89603 7.001 8.02003 7.11ZM9.12303 10.51C9.22022 10.5261 9.31328 10.5613 9.39689 10.6134C9.4805 10.6655 9.55302 10.7335 9.61032 10.8137C9.66762 10.8938 9.70856 10.9845 9.73082 11.0804C9.75308 11.1764 9.75621 11.2758 9.74003 11.373L8.74003 17.373C8.72388 17.4702 8.68875 17.5632 8.63663 17.6468C8.58452 17.7304 8.51646 17.8029 8.43632 17.8602C8.35618 17.9175 8.26554 17.9585 8.16958 17.9807C8.07362 18.003 7.97421 18.0062 7.87703 17.99C7.77986 17.9738 7.68681 17.9387 7.60321 17.8866C7.51961 17.8345 7.44709 17.7664 7.3898 17.6863C7.33251 17.6061 7.29156 17.5155 7.26929 17.4195C7.24703 17.3236 7.24388 17.2242 7.26003 17.127L8.26003 11.127C8.27617 11.0298 8.31129 10.9368 8.3634 10.8531C8.4155 10.7695 8.48357 10.697 8.56371 10.6397C8.64385 10.5824 8.7345 10.5415 8.83047 10.5192C8.92644 10.497 9.02585 10.4938 9.12303 10.51ZM14.877 10.51C14.9742 10.4938 15.0736 10.497 15.1696 10.5192C15.2656 10.5415 15.3562 10.5824 15.4364 10.6397C15.5165 10.697 15.5846 10.7695 15.6367 10.8531C15.6888 10.9368 15.7239 11.0298 15.74 11.127L16.74 17.127C16.7727 17.3233 16.726 17.5244 16.6103 17.6863C16.4946 17.8481 16.3193 17.9574 16.123 17.99C15.9268 18.0226 15.7256 17.9759 15.5637 17.8602C15.4019 17.7445 15.2927 17.5693 15.26 17.373L14.26 11.373C14.2439 11.2758 14.247 11.1764 14.2692 11.0804C14.2915 10.9845 14.3325 10.8938 14.3897 10.8137C14.447 10.7335 14.5196 10.6655 14.6032 10.6134C14.6868 10.5613 14.7798 10.5261 14.877 10.51Z",
|
||||
|
||||
chevronDown:
|
||||
"M4.42999 8.512C4.49412 8.43717 4.57236 8.37572 4.66025 8.33115C4.74814 8.28658 4.84395 8.25977 4.94221 8.25225C5.04046 8.24473 5.13924 8.25664 5.23289 8.28731C5.32653 8.31798 5.41322 8.36681 5.48799 8.431L12 14.012L18.512 8.431C18.6638 8.30912 18.8571 8.25107 19.0509 8.2691C19.2448 8.28713 19.424 8.37984 19.5508 8.52762C19.6775 8.6754 19.7418 8.86668 19.7301 9.06101C19.7184 9.25535 19.6316 9.43751 19.488 9.569L12.488 15.569C12.3521 15.6855 12.179 15.7495 12 15.7495C11.821 15.7495 11.6479 15.6855 11.512 15.569L4.51199 9.569C4.36118 9.43951 4.26793 9.25545 4.25274 9.05726C4.23756 8.85907 4.30167 8.66295 4.43099 8.512",
|
||||
}
|
||||
|
||||
export const emailRegex =
|
||||
/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/
|
||||
|
||||
export const phoneRegex = /^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$/
|
||||
|
||||
export const germanyStates = [
|
||||
{
|
||||
name: "Baden-Württemberg",
|
||||
code: "DE-BW",
|
||||
},
|
||||
{
|
||||
name: "Bayern",
|
||||
code: "DE-BY",
|
||||
},
|
||||
{
|
||||
name: "Berlin",
|
||||
code: "DE-BE",
|
||||
},
|
||||
{
|
||||
name: "Brandenburg",
|
||||
code: "DE-BB",
|
||||
},
|
||||
{
|
||||
name: "Bremen",
|
||||
code: "DE-HB",
|
||||
},
|
||||
{
|
||||
name: "Hamburg",
|
||||
code: "DE-HH",
|
||||
},
|
||||
{
|
||||
name: "Hessen",
|
||||
code: "DE-HE",
|
||||
},
|
||||
{
|
||||
name: "Mecklenburg-Vorpommern",
|
||||
code: "DE-MV",
|
||||
},
|
||||
{
|
||||
name: "Niedersachsen",
|
||||
code: "DE-NI",
|
||||
},
|
||||
{
|
||||
name: "Nordrhein-Westfalen",
|
||||
code: "DE-NW",
|
||||
},
|
||||
{
|
||||
name: "Rheinland-Pfalz",
|
||||
code: "DE-RP",
|
||||
},
|
||||
{
|
||||
name: "Saarland",
|
||||
code: "DE-SL",
|
||||
},
|
||||
{
|
||||
name: "Sachsen",
|
||||
code: "DE-SN",
|
||||
},
|
||||
{
|
||||
name: "Sachsen-Anhalt",
|
||||
code: "DE-ST",
|
||||
},
|
||||
{
|
||||
name: "Schleswig-Holstein",
|
||||
code: "DE-SH",
|
||||
},
|
||||
{
|
||||
name: "Thüringen",
|
||||
code: "DE-TH",
|
||||
},
|
||||
]
|
||||
export const socialIcons = {
|
||||
facebook: "https://www.facebook.com/binkrassdufass",
|
||||
instagram: "https://www.instagram.com/binkrassdufass",
|
||||
tiktok: "https://www.tiktok.com/@binkrassdufass",
|
||||
youtube: "https://www.youtube.com/@binkrassdufass",
|
||||
}
|
||||
11
frontend/src/index.ts
Normal file
11
frontend/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import App from "./App.svelte"
|
||||
|
||||
let appContainer = document?.getElementById("appContainer")
|
||||
const hydrate = true //import.meta?.env?.MODE !== "development"
|
||||
const app = new App({
|
||||
target: appContainer,
|
||||
props: {},
|
||||
hydrate,
|
||||
})
|
||||
|
||||
export default app
|
||||
54
frontend/src/lib/actions.ts
Normal file
54
frontend/src/lib/actions.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { Action } from "svelte/action"
|
||||
import { overlays } from "./store"
|
||||
|
||||
export const spaLink: Action<HTMLAnchorElement> = (node) => {
|
||||
const onClick = (event: MouseEvent) => {
|
||||
if (node.dataset.spaPrevent && node.dataset.spaPrevent !== "false") {
|
||||
return
|
||||
}
|
||||
const anchor = event.currentTarget as HTMLAnchorElement
|
||||
if ((anchor.target === "" || anchor.target === "_self") && anchor.href.startsWith(window.location.origin)) {
|
||||
event.preventDefault()
|
||||
const currentUrl = window.location.pathname + window.location.search + window.location.hash
|
||||
const newUrl = anchor.pathname + anchor.search + anchor.hash
|
||||
if (currentUrl === newUrl) {
|
||||
window.scrollTo(0, 0)
|
||||
return
|
||||
}
|
||||
spaNavigate(anchor.pathname + anchor.search + anchor.hash)
|
||||
}
|
||||
}
|
||||
|
||||
node.addEventListener("click", onClick)
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener("click", onClick)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const spaNavigate = (to: string, options?: { replace?: boolean }) => {
|
||||
overlays.update((current) => [])
|
||||
window.scrollTo(0, 0)
|
||||
//scroll to top of page
|
||||
setTimeout(
|
||||
() => {
|
||||
window.scrollTo(0, 0)
|
||||
},
|
||||
|
||||
100
|
||||
)
|
||||
|
||||
if (options?.replace) {
|
||||
window.history.replaceState(null, "", to)
|
||||
} else {
|
||||
window.history.pushState(null, "", to)
|
||||
}
|
||||
}
|
||||
|
||||
export const spaBack = () => {
|
||||
window.history.back()
|
||||
}
|
||||
|
||||
// TODO: spaLinks container for containing {@html ...}
|
||||
@@ -0,0 +1,252 @@
|
||||
@import "../../../variables.less";
|
||||
.my-form {
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
.data-protection {
|
||||
margin: 5px 0px;
|
||||
box-shadow: 0 0 25px 10px rgba(24, 24, 24, 0.05);
|
||||
}
|
||||
|
||||
.success-message {
|
||||
h1 {
|
||||
color: @main-color;
|
||||
font-size: 36px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
.invalidBlocks {
|
||||
border: 2px solid rgb(255, 0, 0) !important;
|
||||
position: relative;
|
||||
&::after {
|
||||
font-size: 0.9rem !important;
|
||||
color: red !important;
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
content: "Bitte wähle entweder eine Kartenanzahl oder einen Wunschbetrag aus.";
|
||||
}
|
||||
}
|
||||
|
||||
.border-red {
|
||||
border-color: red !important;
|
||||
}
|
||||
|
||||
.no-margin {
|
||||
margin-top: 15px !important;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
border: 2px solid red !important;
|
||||
}
|
||||
.error-message {
|
||||
font-size: 0.9rem !important;
|
||||
color: red !important;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
}
|
||||
|
||||
.checkit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checkit-span {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border: 1px solid grey;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[type="checkbox"]:checked + span:before {
|
||||
content: "\2714";
|
||||
position: absolute;
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
.form-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
h3 {
|
||||
font-weight: bold !important;
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
@media @tablet {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.form-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 1.5rem;
|
||||
|
||||
.date {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
label {
|
||||
width: 100% !important;
|
||||
font-size: inherit;
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
.form-cols {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.form-column {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
width: 100%;
|
||||
gap: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--hover-color);
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
.data-protection {
|
||||
padding: 10px 20px;
|
||||
border: 0px solid white;
|
||||
border-bottom: 3px solid @main-color;
|
||||
outline: 0px solid white;
|
||||
color: black;
|
||||
background-color: @background-color;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.data-protection {
|
||||
display: flex;
|
||||
|
||||
flex-wrap: nowrap;
|
||||
gap: 5px !important;
|
||||
justify-content: start;
|
||||
flex-direction: row !important;
|
||||
}
|
||||
|
||||
input[type="date"] {
|
||||
font-family: inherit;
|
||||
width: 100% !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 10px 20px;
|
||||
border: 0;
|
||||
border-bottom: 3px solid @main-color;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
select:focus {
|
||||
border-bottom-color: @main-color;
|
||||
}
|
||||
|
||||
#time-select {
|
||||
appearance: none;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 20px center;
|
||||
background-size: 18px;
|
||||
|
||||
option {
|
||||
padding: 10px 20px;
|
||||
background-color: @background-color;
|
||||
}
|
||||
}
|
||||
@media @mobile {
|
||||
.date {
|
||||
width: 100vw !important;
|
||||
}
|
||||
}
|
||||
@media @tablet {
|
||||
.date {
|
||||
width: 100% !important;
|
||||
}
|
||||
.form-cols {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
}
|
||||
i .max-width {
|
||||
max-width: @body-maxwidth !important;
|
||||
}
|
||||
|
||||
.datasec {
|
||||
display: flex;
|
||||
align-items: center !important;
|
||||
.link {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: flex-end !important;
|
||||
text-decoration: underline;
|
||||
margin-right: 3px;
|
||||
color: rgb(14, 91, 146);
|
||||
}
|
||||
}
|
||||
|
||||
.additional {
|
||||
display: flex;
|
||||
@media @mobile {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
@media @tablet {
|
||||
flex-direction: row !important;
|
||||
}
|
||||
gap: 2.5rem;
|
||||
div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
}
|
||||
.submit-request {
|
||||
flex: 1;
|
||||
box-shadow: 0 0 25px 10px rgba(0, 0, 0, 0.05);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
background-color: @main-color;
|
||||
color: @background-color;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
155
frontend/src/lib/assets/css/components/widgets/modal.less
Normal file
155
frontend/src/lib/assets/css/components/widgets/modal.less
Normal file
@@ -0,0 +1,155 @@
|
||||
dialog {
|
||||
&.modal {
|
||||
cursor: default;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid @border-lightgrey;
|
||||
border-radius: @space-sm;
|
||||
transition: all 0.4s ease-in-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translateY(-2rem);
|
||||
display: none;
|
||||
overflow: unset;
|
||||
z-index: 9998;
|
||||
inset: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
|
||||
&::backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.grid-item {
|
||||
flex-wrap: wrap !important;
|
||||
div {
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
&.modal-xs {
|
||||
width: 30vw;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
&.modal-sm {
|
||||
width: 40vw;
|
||||
max-height: 90vh;
|
||||
|
||||
@media screen and (max-width: 1250px) {
|
||||
width: 50vw;
|
||||
}
|
||||
@media screen and (max-width: 1050px) {
|
||||
width: 60vw;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
width: 95vw;
|
||||
}
|
||||
}
|
||||
|
||||
&.modal-md {
|
||||
width: 50vw;
|
||||
max-height: 90vh;
|
||||
|
||||
@media screen and (max-width: 1250px) {
|
||||
width: 60vw;
|
||||
}
|
||||
@media screen and (max-width: 1050px) {
|
||||
width: 70vw;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
width: 95vw;
|
||||
}
|
||||
}
|
||||
|
||||
&.modal-lg {
|
||||
width: 60vw;
|
||||
max-height: 90vh;
|
||||
|
||||
@media screen and (max-width: 1250px) {
|
||||
width: 70vw;
|
||||
}
|
||||
@media screen and (max-width: 1050px) {
|
||||
width: 80vw;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
width: 95vw;
|
||||
}
|
||||
}
|
||||
|
||||
&.modal-xl {
|
||||
width: 80vw;
|
||||
max-height: 90vh;
|
||||
|
||||
@media screen and (max-width: 1050px) {
|
||||
width: 90vw;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
width: 95vw;
|
||||
}
|
||||
}
|
||||
|
||||
&.modal-full {
|
||||
width: 98vw;
|
||||
height: 98vw;
|
||||
max-height: 98vh;
|
||||
}
|
||||
|
||||
& .modal-close {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: @space-md;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
& .modal-content {
|
||||
padding: @space-md @space-lg;
|
||||
border-radius: @space-lg;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
& .modal-header {
|
||||
height: 45px;
|
||||
padding: @space-sm @space-md;
|
||||
border-top-left-radius: @space-sm;
|
||||
border-top-right-radius: @space-sm;
|
||||
color: @textcolor;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
background: @lightgrey;
|
||||
|
||||
& > * {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
& .modal-footer {
|
||||
margin-top: @space-lg;
|
||||
padding: 0 @space-lg @space-lg @space-lg;
|
||||
border-bottom-left-radius: @space-sm;
|
||||
border-bottom-right-radius: @space-sm;
|
||||
|
||||
& > * {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
gap: @space-sm;
|
||||
}
|
||||
}
|
||||
|
||||
&[open] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
}
|
||||
186
frontend/src/lib/assets/css/formular.less
Normal file
186
frontend/src/lib/assets/css/formular.less
Normal file
@@ -0,0 +1,186 @@
|
||||
form,
|
||||
.form {
|
||||
h2,
|
||||
h3 {
|
||||
padding: 0px 2.4rem;
|
||||
color: var(--text-invers-100);
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
|
||||
.row {
|
||||
padding: 0px 2.4rem;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 1.2rem;
|
||||
position: relative;
|
||||
.row {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
gap: 0.6rem;
|
||||
.row {
|
||||
gap: 0.6rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.action-button-line,
|
||||
.action-line {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.action-line {
|
||||
background: var(--bg-100);
|
||||
padding: 0.6rem 3.3rem 0.6rem 3.3rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
min-height: 2.4rem;
|
||||
justify-content: space-between;
|
||||
p,
|
||||
span {
|
||||
color: var(--neutral-white);
|
||||
}
|
||||
}
|
||||
.data-protection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
width: 100%;
|
||||
|
||||
p {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
.action-button-line {
|
||||
min-height: 2.4rem;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
button {
|
||||
width: 0px;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
.error-message {
|
||||
font-size: 0.7rem;
|
||||
line-height: 0.7rem;
|
||||
color: var(--primary-100);
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
}
|
||||
.checkit-span {
|
||||
height: 1.2rem;
|
||||
width: 1.2rem;
|
||||
min-height: 1.2rem;
|
||||
min-width: 1.2rem;
|
||||
border: 1px solid grey;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
[type="checkbox"]:checked + .checkit-span:before {
|
||||
content: "\2714";
|
||||
position: absolute;
|
||||
color: var(--text-invers-100);
|
||||
transform-origin: bottom;
|
||||
}
|
||||
label {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
height: 60px;
|
||||
max-height: 60px;
|
||||
&.checkbox {
|
||||
height: 1.2rem;
|
||||
max-height: 1.2rem;
|
||||
}
|
||||
|
||||
span {
|
||||
opacity: 0;
|
||||
transition: opacity 0s;
|
||||
padding: 0px 19px;
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
text-align: left;
|
||||
&.hasValue {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
font-size: 0.7rem;
|
||||
line-height: 0.7rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
p {
|
||||
padding: 0px 19px;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
&.textarea,
|
||||
&.file {
|
||||
height: auto;
|
||||
max-height: none;
|
||||
}
|
||||
& > input,
|
||||
& > div.svelte-select,
|
||||
& > div.wrapper,
|
||||
& > .no-input,
|
||||
& > textarea,
|
||||
.fileContainer {
|
||||
width: 100% !important;
|
||||
padding: 0px 18px 12px 18px !important;
|
||||
height: 30px !important;
|
||||
max-height: 30px !important;
|
||||
border: 0px solid black !important;
|
||||
border-radius: 0px !important;
|
||||
border-bottom: 1px solid var(--text-invers-100) !important;
|
||||
text-align: start !important;
|
||||
color: var(--text-invers-100) !important;
|
||||
font-size: 1rem !important;
|
||||
&.fileContainer {
|
||||
max-height: none !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
&.svelte-select {
|
||||
min-height: 30px !important;
|
||||
& > .value-container {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border-bottom: 1px solid var(--primary-100) !important;
|
||||
}
|
||||
&.error {
|
||||
border-bottom: 3px solid var(--primary-100) !important;
|
||||
}
|
||||
|
||||
&.checkit {
|
||||
display: none !important;
|
||||
}
|
||||
&.no-input {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
&[type="checkbox"] {
|
||||
border-bottom: 0px solid black !important;
|
||||
}
|
||||
}
|
||||
textarea {
|
||||
min-height: 170px;
|
||||
}
|
||||
}
|
||||
}
|
||||
364
frontend/src/lib/assets/css/main.less
Normal file
364
frontend/src/lib/assets/css/main.less
Normal file
@@ -0,0 +1,364 @@
|
||||
a,
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
* {
|
||||
box-sizing: border-box !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
appearance: none;
|
||||
background: none;
|
||||
line-height: 100%;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body.no-scroll,
|
||||
body.no-overflow {
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: var(--neutral-white);
|
||||
}
|
||||
body {
|
||||
color: var(--text-invers-100);
|
||||
height: 100%;
|
||||
background-color: var(--bg-100) !important;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
* {
|
||||
transition: background-color 0.5s ease, max-height 0.5s, height 0.5s ease, width 0.5s ease, flex 0.5s ease,
|
||||
opacity 0.5s ease, top 0.5s ease, bottom 0.5s ease, left 0.5s ease, right 0.5s ease, transform 0.5s ease;
|
||||
}
|
||||
/* General scrollbar styles for all webkit browsers */
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 11px; /* Apply height for horizontal scrollbars as well */
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: var(--neutral-white);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: var(--bg-100);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
/* Specific styles for your horizontal scrollbar */
|
||||
.horizontalScrollbar::-webkit-scrollbar {
|
||||
height: 11px;
|
||||
}
|
||||
|
||||
.horizontalScrollbar::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-100);
|
||||
}
|
||||
|
||||
.horizontalScrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(13, 12, 12, 0.25);
|
||||
border-top: 3px solid transparent;
|
||||
border-bottom: 3px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
/* Firefox scrollbar styles */
|
||||
@-moz-document url-prefix() {
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--neutral-white) var(--bg-100);
|
||||
}
|
||||
.horizontalScrollbar {
|
||||
scrollbar-height: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
/* iOS Safari specific styles */
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
.horizontalScrollbar {
|
||||
overflow: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.horizontalScrollbar::-webkit-scrollbar {
|
||||
height: 11px;
|
||||
}
|
||||
.horizontalScrollbar::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-100);
|
||||
}
|
||||
.horizontalScrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(13, 12, 12, 0.25);
|
||||
border-top: 3px solid transparent;
|
||||
border-bottom: 3px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
}
|
||||
|
||||
h2,
|
||||
.h2,
|
||||
h2 em {
|
||||
font-size: 2rem;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-family: Outfit-Regular, sans-serif;
|
||||
line-height: 100%;
|
||||
color: var(--text-100);
|
||||
@media @mobile {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
}
|
||||
.h1,
|
||||
h1,
|
||||
.h1 em,
|
||||
h1 em {
|
||||
font-size: 3.2rem;
|
||||
font-weight: 700;
|
||||
font-family: Outfit-Bold, sans-serif;
|
||||
line-height: 100%;
|
||||
font-style: normal;
|
||||
color: var(--text-100);
|
||||
@media @mobile {
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.h2 {
|
||||
font-size: 2rem;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-family: Outfit-Regular, sans-serif;
|
||||
line-height: 100%;
|
||||
color: var(--text-100);
|
||||
@media @mobile {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
.red {
|
||||
color: var(--primary-200) !important;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
font-family: Outfit-Bold, sans-serif;
|
||||
line-height: 100%;
|
||||
color: var(--text-100);
|
||||
@media @mobile {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
em {
|
||||
font-size: 1rem;
|
||||
font-family: Outfit-Bold, sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
button {
|
||||
color: var(--text-100);
|
||||
cursor: pointer;
|
||||
}
|
||||
p {
|
||||
font-size: 1rem;
|
||||
color: var(--text-invers-100);
|
||||
&.headline-description {
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-100);
|
||||
font-family: Outfit-Regular, sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-invers-100);
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
font-family: Outfit-Bold, sans-serif;
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--text-invers-100);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 400;
|
||||
em {
|
||||
font-style: normal;
|
||||
color: var(--text-invers-100);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
font-family: Outfit-Bold, sans-serif;
|
||||
}
|
||||
}
|
||||
button.cta {
|
||||
padding: 0.6rem 1.2rem 0.6rem 1.2rem;
|
||||
border-radius: 0.1rem;
|
||||
color: var(--text-100);
|
||||
font-family: "Outfit-Bold", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
a {
|
||||
color: var(--text-100);
|
||||
text-transform: uppercase;
|
||||
font-family: "Outfit-Bold", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
}
|
||||
&.primary {
|
||||
background-color: var(--primary-100);
|
||||
}
|
||||
&.secondary {
|
||||
background-color: var(--text-invers-100);
|
||||
}
|
||||
&.tertiary {
|
||||
background-color: transparent;
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
&[disabled] {
|
||||
background-color: var(--bg-100);
|
||||
color: var(--text-100);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
section.wrapper,
|
||||
section.small-wrapper {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-bottom: var(--vertical-default-margin);
|
||||
justify-content: center;
|
||||
flex-direction: row !important;
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
&.small-wrapper > * {
|
||||
max-width: var(--small-max-width);
|
||||
width: 100%;
|
||||
margin: 2.4rem var(--horizontal-default-margin);
|
||||
display: flex;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
&.wrapper > * {
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
max-width: var(--normal-max-width);
|
||||
width: 100%;
|
||||
margin: 2.4rem var(--horizontal-default-margin);
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
tr {
|
||||
height: 36px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
thead {
|
||||
th {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-invers-100);
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
&:nth-child(odd) {
|
||||
background-color: var(--bg-300);
|
||||
}
|
||||
td {
|
||||
&:first-child {
|
||||
font-weight: 700;
|
||||
}
|
||||
white-space: nowrap;
|
||||
color: var(--text-invers-100);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media @mobile {
|
||||
thead {
|
||||
th {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// Tooltips
|
||||
.tooltip {
|
||||
min-width: 200px;
|
||||
width: fit-content;
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
z-index: 9999 !important;
|
||||
position: fixed;
|
||||
margin-left: -100px;
|
||||
bottom: 100%;
|
||||
background: #fff;
|
||||
box-shadow: 0px 1px 10px 1px rgba(0, 0, 0, 0.2);
|
||||
color: black;
|
||||
border-radius: 5px;
|
||||
max-width: min(50vw, 400px);
|
||||
.extrawide {
|
||||
width: 300px;
|
||||
}
|
||||
.bubbleding {
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
border: solid transparent;
|
||||
content: "";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border-color: rgba(255, 255, 255, 0);
|
||||
border-top-color: #ffffff;
|
||||
border-width: 12px;
|
||||
margin-left: -12px;
|
||||
}
|
||||
.tooltip-close {
|
||||
@media (min-width: 1100px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
bottom: 0;
|
||||
|
||||
.bubbleding {
|
||||
top: -24px;
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
80
frontend/src/lib/assets/css/variables.less
Normal file
80
frontend/src/lib/assets/css/variables.less
Normal file
@@ -0,0 +1,80 @@
|
||||
/* Figma Styles of your File */
|
||||
:root {
|
||||
/* Colors */
|
||||
--wire: linear-gradient(-77.29deg, rgba(0, 0, 0, 0) 0%, rgba(13, 12, 12, 1) 44.86416280269623%),
|
||||
linear-gradient(98.68deg, rgba(0, 0, 0, 0) 0%, rgba(13, 12, 12, 1) 44.86416280269623%),
|
||||
linear-gradient(180deg, rgba(51, 45, 44, 1) 0%, rgba(51, 45, 44, 0) 100%);
|
||||
--bg-grey-cultured: linear-gradient(to left, #0d0c0c, #0d0c0c);
|
||||
|
||||
/* Fonts */
|
||||
--quote-font-family: Poly-Italic, sans-serif;
|
||||
--quote-font-size: 20px;
|
||||
--quote-line-height: normal;
|
||||
--quote-font-weight: 400;
|
||||
--quote-font-style: italic;
|
||||
|
||||
/* Effects */
|
||||
--glow-box-shadow: 0px 0px 20px 0px rgba(209, 231, 224, 1);
|
||||
--redglow-box-shadow: 0px 0px 24px 0px rgba(116, 30, 32, 1), 0px 0px 20px 0px rgba(116, 30, 32, 1);
|
||||
--blackglow-box-shadow: 0px 10px 20px 0px rgba(0, 0, 0, 1);
|
||||
--buttunshadow-box-shadow: inset 0px -2px 0px 0px rgba(0, 0, 0, 0.25);
|
||||
--redneonglow-box-shadow: 0px 0px 20px 0px rgba(255, 13, 0, 1);
|
||||
--blueneonglow-box-shadow: 0px 0px 20px 0px rgba(0, 255, 240, 1);
|
||||
--innershadow-box-shadow: inset 0px 0px 10px 0px rgba(0, 0, 0, 0.25);
|
||||
--redneonglow2-box-shadow: 0px 0px 20px 0px rgba(116, 30, 32, 1), inset 0px 0px 1px 0px rgba(0, 0, 0, 1);
|
||||
--dropshadow-box-shadow: 0px 10px 20px 0px rgba(0, 0, 0, 0.2);
|
||||
|
||||
/* COLOR NEUTRAL */
|
||||
--text-100: #f3eed9;
|
||||
--text-200: #d1e7e0;
|
||||
--text-invers-100: #2f4858;
|
||||
--text-invers-200: #741e20;
|
||||
--primary-100: #741e20;
|
||||
--primary-200: #ad514c;
|
||||
--bg-100: #0d0c0c;
|
||||
--bg-200: #332d2c;
|
||||
--neutral-white: #ffffff;
|
||||
--text-300: #625755;
|
||||
--bg-300: #eceaea;
|
||||
--text-invers-150: #6d97b0;
|
||||
--krass-kraft-primary: #eb5757;
|
||||
--crazy-crave-control-primary: #c0f256;
|
||||
--krass-kreativ-primary: #56f2f2;
|
||||
--crazy-calm-primary: #56f2b0;
|
||||
|
||||
--vertical-default-margin: 3rem;
|
||||
--small-max-width: 1392px;
|
||||
--normal-max-width: 1776px;
|
||||
--horizontal-default-margin: 1.2rem;
|
||||
}
|
||||
|
||||
@space-xs: 0.25rem;
|
||||
@space-sm: 0.5rem;
|
||||
@space-md: 1rem;
|
||||
@space-lg: 2rem;
|
||||
@space-xl: 3rem;
|
||||
|
||||
@font-family: "Open Sans", sans-serif;
|
||||
|
||||
@large_desktop: ~"only screen and (min-width: 1440px) ";
|
||||
@desktop: ~"only screen and (min-width: 1224px)";
|
||||
|
||||
@tablet: ~"only screen and (min-width: 968px) and (max-width: 1223px)";
|
||||
@mobile: ~"only screen and (min-width: 100px) and (max-width: 967px)";
|
||||
@min-tablet: ~"only screen and (min-width: 968px)";
|
||||
@max-tablet: ~"only screen and (max-width: 1223px)";
|
||||
@body-maxwidth: 1300px;
|
||||
|
||||
@media @tablet {
|
||||
:root {
|
||||
--horizontal-default-margin: 2.4rem;
|
||||
}
|
||||
}
|
||||
@media @large_desktop {
|
||||
:root {
|
||||
--horizontal-default-margin: 3.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 2200px) {
|
||||
}
|
||||
69
frontend/src/lib/components/CrinkledSection.svelte
Normal file
69
frontend/src/lib/components/CrinkledSection.svelte
Normal file
@@ -0,0 +1,69 @@
|
||||
<script>
|
||||
import TopRightCrinkle from "./widgets/TopRightCrinkle.svelte"
|
||||
export let brightBackground = true
|
||||
export let border = true
|
||||
export let activated = true
|
||||
export let icon = ""
|
||||
export let bigVersion = false
|
||||
</script>
|
||||
|
||||
{#if activated}
|
||||
<section
|
||||
id="crinkled-section"
|
||||
class="{brightBackground ? 'bright' : 'dark'}"
|
||||
>
|
||||
<div class="upper-row">
|
||||
<div
|
||||
class="bar"
|
||||
class:border="{border}"
|
||||
></div>
|
||||
<TopRightCrinkle
|
||||
edges="{border}"
|
||||
brightColor="{!brightBackground}"
|
||||
icon="{icon}"
|
||||
bigVersion="{bigVersion}"
|
||||
/>
|
||||
</div>
|
||||
<slot />
|
||||
</section>
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
|
||||
<style lang="less">
|
||||
#crinkled-section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
z-index: 2000;
|
||||
margin-top: -3rem;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
.upper-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-start;
|
||||
height: 3rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.bar {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
background: var(--neutral-white);
|
||||
&.border {
|
||||
border-top: 2px solid black;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.dark {
|
||||
.bar {
|
||||
background: var(--bg-100);
|
||||
&.border {
|
||||
border-top: 2px solid var(--neutral-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
323
frontend/src/lib/components/Footer.svelte
Normal file
323
frontend/src/lib/components/Footer.svelte
Normal file
@@ -0,0 +1,323 @@
|
||||
<script lang="ts">
|
||||
import { getDBEntries } from "../../api"
|
||||
import { socialIcons } from "../../config"
|
||||
import { spaLink } from "../actions"
|
||||
import { login, navigationCache } from "../store"
|
||||
import CrinkledSection from "./CrinkledSection.svelte"
|
||||
import Input from "./pagebuilder/blocks/form/Input.svelte"
|
||||
import { submitNewsletter } from "../functions/CommerceAPIs/tibiEndpoints/actions"
|
||||
import { onChange } from "./pagebuilder/profile/helper"
|
||||
enum NavigationType {
|
||||
MainNavigation = 0,
|
||||
ServiceNavigation = 2,
|
||||
LegalNavigation = 1,
|
||||
}
|
||||
let emailIsSubscribed = false
|
||||
let navigationEntries: NavigationEntry[] = []
|
||||
|
||||
function elementsToCache(elements: NavigationElement[]) {
|
||||
elements.forEach((el) => {
|
||||
if (!el.external) {
|
||||
if (!$navigationCache[el.page]) $navigationCache[el.page] = el
|
||||
if (el.elements?.length > 0) elementsToCache(el.elements)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getDBEntries("navigation").then((navs) => {
|
||||
navigationEntries = navs.sort((a, b) => a.type - b.type)
|
||||
navigationEntries.forEach((nav) => elementsToCache(nav.elements))
|
||||
})
|
||||
export let className = ""
|
||||
let sections: {
|
||||
title: string
|
||||
links: NavigationElement[]
|
||||
}[] = []
|
||||
$: if (navigationEntries.length) sections = []
|
||||
|
||||
let email = ""
|
||||
let dataProt = false
|
||||
</script>
|
||||
|
||||
<CrinkledSection>
|
||||
<footer class="{'footer ' + className}">
|
||||
<section id="content-section">
|
||||
<section id="content-link-section">
|
||||
{#each sections as section}
|
||||
<section class="content">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<small class="service"><em>{section?.title}</em></small>
|
||||
<nav class="sub-points">
|
||||
<ul>
|
||||
{#each section?.links || [] as link}
|
||||
<li>
|
||||
<a
|
||||
class="footer-nav-point"
|
||||
use:spaLink
|
||||
href="{link.page}"><small>{link.name}</small></a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/each}
|
||||
</section>
|
||||
<section id="newsletter-section">
|
||||
<h4>Newsletter</h4>
|
||||
<form
|
||||
on:submit|preventDefault|stopPropagation="{() => {
|
||||
submitNewsletter(email, dataProt).then(() => {
|
||||
emailIsSubscribed = true
|
||||
})
|
||||
}}"
|
||||
>
|
||||
{#if emailIsSubscribed}
|
||||
<p>Du hast dich zum Newsletter angemeldet!</p>
|
||||
{:else}
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="E-Mail"
|
||||
bind:value="{email}"
|
||||
id="email"
|
||||
onChange="{onChange}"
|
||||
/>
|
||||
<div class="data-protection">
|
||||
<Input
|
||||
type="checkbox"
|
||||
bind:value="{dataProt}"
|
||||
id="dataprot"
|
||||
onChange="{onChange}"
|
||||
/>
|
||||
<p>
|
||||
<a
|
||||
href="/datenschutz"
|
||||
use:spaLink>Datenschutz</a
|
||||
> zum Newsletterversand akzeptieren
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="btn cta primary"
|
||||
type="submit"
|
||||
>
|
||||
Anmelden</button
|
||||
>
|
||||
{/if}
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
||||
<section id="icons-section">
|
||||
<div class="line">
|
||||
<div class="line-1"></div>
|
||||
<img
|
||||
alt="Symbol"
|
||||
class="symbol"
|
||||
src="../../../logo/logoShort.svg"
|
||||
/>
|
||||
<div class="line-2"></div>
|
||||
</div>
|
||||
<ul class="social">
|
||||
{#each Object.keys(socialIcons) as icon}
|
||||
<li>
|
||||
<a
|
||||
href="{socialIcons[icon]}"
|
||||
use:spaLink
|
||||
target="_blank"
|
||||
aria-label="{icon}"
|
||||
>
|
||||
<figure class="footer-icon">
|
||||
<img
|
||||
alt="{icon}"
|
||||
src="../../../media/{icon}.svg"
|
||||
/>
|
||||
</figure></a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
<section id="legal-section">
|
||||
<div class="wrapper">
|
||||
<small class="">© 2024 | BKDF - Bin Krass Du Fass | Alle Rechte vorbehalten.</small>
|
||||
<nav class="nav-points">
|
||||
<ul>
|
||||
{#each navigationEntries.length ? navigationEntries[NavigationType.LegalNavigation].elements : [] as link}
|
||||
<li>
|
||||
<a
|
||||
class="footer-nav-point-bottom"
|
||||
use:spaLink
|
||||
href="{link.page}"><small>{link.name}</small></a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
</footer>
|
||||
</CrinkledSection>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../lib/assets/css/variables.less";
|
||||
.footer {
|
||||
&,
|
||||
& * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
background: var(--neutral-white);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
gap: 1.5rem;
|
||||
#content-section {
|
||||
padding: 0px var(--horizontal-default-margin);
|
||||
max-width: var(--small-max-width);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.5rem;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
#content-link-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.5rem;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.125rem;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
width: 8rem;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
@media @mobile {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
#content-link-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
#newsletter-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
min-width: 300px;
|
||||
form {
|
||||
button {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
#icons-section {
|
||||
padding: 0px var(--horizontal-default-margin);
|
||||
max-width: var(--small-max-width);
|
||||
width: 100%;
|
||||
margin: 0 0 0 -0.0625rem;
|
||||
display: flex;
|
||||
gap: 0.625rem;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.payments,
|
||||
.social {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
figure {
|
||||
height: 1.2rem;
|
||||
img {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
.line {
|
||||
padding: 0rem 1.5rem 0rem 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.5rem;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.line-1,
|
||||
.line-2 {
|
||||
border-style: solid;
|
||||
border-color: var(--text-invers-100);
|
||||
border-width: 0.0625rem 0 0 0;
|
||||
flex: 1;
|
||||
height: 0rem;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
@media @mobile {
|
||||
flex-direction: column-reverse;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
.line {
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#legal-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
background: var(--bg-100);
|
||||
gap: 1.2rem;
|
||||
.wrapper {
|
||||
max-width: var(--small-max-width);
|
||||
|
||||
width: 100%;
|
||||
a,
|
||||
small {
|
||||
color: var(--text-100);
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
gap: 1.2rem;
|
||||
padding: 1.5rem var(--horizontal-default-margin);
|
||||
@media @mobile {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.nav-points {
|
||||
ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6rem;
|
||||
align-items: center;
|
||||
a {
|
||||
font-weight: 400;
|
||||
font-family: Outfit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
14
frontend/src/lib/components/MediaQueryWrapper.svelte
Normal file
14
frontend/src/lib/components/MediaQueryWrapper.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import MediaQuery from "./widgets/MediaQuery.svelte"
|
||||
|
||||
export let renderQueryObject: MediaQueryInputObject
|
||||
</script>
|
||||
|
||||
{#each Object.entries(renderQueryObject) as queryObject}
|
||||
<MediaQuery query="{queryObject[0]}">
|
||||
<svelte:component
|
||||
this="{queryObject[1][0]}"
|
||||
{...queryObject[1][1]}
|
||||
/>
|
||||
</MediaQuery>
|
||||
{/each}
|
||||
295
frontend/src/lib/components/Modal.svelte
Normal file
295
frontend/src/lib/components/Modal.svelte
Normal file
@@ -0,0 +1,295 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import { mdiCloseCircleOutline } from "@mdi/js"
|
||||
import Icon from "./widgets/Icon.svelte"
|
||||
import Notifications from "./widgets/Notifications.svelte"
|
||||
import { isMobile } from "../store"
|
||||
import { changeStateOfSite } from "./header/Desktop.svelte"
|
||||
|
||||
export let show: boolean = false,
|
||||
size: string = "md",
|
||||
cssClass: string = "",
|
||||
closeOnBackdrop: boolean = false
|
||||
|
||||
let dialog: HTMLDialogElement,
|
||||
dispatch = createEventDispatcher()
|
||||
|
||||
const onCancel = (e: any) => {
|
||||
show = false
|
||||
dialog.close()
|
||||
dispatch("close")
|
||||
},
|
||||
onDialogClick = (e: MouseEvent) => {
|
||||
if (closeOnBackdrop && (e.target as HTMLElement).nodeName === "DIALOG") {
|
||||
dialog.close()
|
||||
show = false
|
||||
dispatch("close")
|
||||
}
|
||||
}
|
||||
|
||||
$: if (dialog)
|
||||
if (show) {
|
||||
dialog.showModal()
|
||||
changeStateOfSite(true)
|
||||
dialog.classList.add("dialog-open")
|
||||
} else if (dialog.classList.contains("dialog-open")) {
|
||||
changeStateOfSite(false)
|
||||
dialog.classList.remove("dialog-open")
|
||||
dialog.close()
|
||||
dispatch("close")
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
return () => {
|
||||
dialog.classList.remove("dialog-open")
|
||||
changeStateOfSite(false)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<dialog
|
||||
class="modal {size ? 'modal-' + size : ''} {cssClass}"
|
||||
bind:this="{dialog}"
|
||||
on:cancel="{onCancel}"
|
||||
on:click|stopPropagation="{onDialogClick}"
|
||||
on:keypress
|
||||
data-cy="modal"
|
||||
>
|
||||
{#if $$slots.title}
|
||||
<div class="header">
|
||||
<div class="crinkle desktop-crinkle">
|
||||
<img
|
||||
src="/media/ModalCrinkle.svg"
|
||||
alt="crinkle"
|
||||
/>
|
||||
</div>
|
||||
<div class="crinkle mobile-crinkle">
|
||||
<img
|
||||
src="/media/ModalCrinkleMobile.svg"
|
||||
alt="crinkle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span class="bar">
|
||||
<h2>
|
||||
<slot name="title" />
|
||||
</h2>
|
||||
<button
|
||||
aria-label="Close"
|
||||
class="modal-close"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
dispatch('close')
|
||||
}}"
|
||||
on:keydown
|
||||
>
|
||||
<Icon
|
||||
path="{mdiCloseCircleOutline}"
|
||||
size="{$isMobile ? 24 : 32}px"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="modal-body">
|
||||
{#if $$slots.default}
|
||||
<section class="modal-content">
|
||||
<slot />
|
||||
</section>
|
||||
{/if}
|
||||
{#if $$slots.footer}
|
||||
<section class="modal-footer">
|
||||
<slot name="footer" />
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Notifications force="{true}" />
|
||||
</dialog>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
@import "../assets/css/variables.less";
|
||||
dialog {
|
||||
&.modal {
|
||||
.header {
|
||||
display: flex;
|
||||
height: 90px;
|
||||
min-height: 90px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: -1px;
|
||||
.crinkle {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
width: auto;
|
||||
margin-right: -2px;
|
||||
margin-left: -2px;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-crinkle {
|
||||
display: none;
|
||||
}
|
||||
@media @mobile {
|
||||
height: 70px;
|
||||
min-height: 70px;
|
||||
|
||||
.desktop-crinkle {
|
||||
display: none;
|
||||
}
|
||||
.mobile-crinkle {
|
||||
display: block;
|
||||
width: 38px !important;
|
||||
height: 70px !important;
|
||||
}
|
||||
}
|
||||
.bar {
|
||||
flex-grow: 1;
|
||||
background-color: var(--neutral-white);
|
||||
height: 100%;
|
||||
h2 {
|
||||
font-weight: 700;
|
||||
font-size: 2rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
border-radius: 4px 0px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
padding-right: 3.6rem;
|
||||
@media @mobile {
|
||||
align-items: center;
|
||||
padding-right: 2.4rem;
|
||||
}
|
||||
gap: 10px;
|
||||
button {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
}
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
gap: 2.4rem;
|
||||
padding: 48px 27px 48px 90px;
|
||||
@media @mobile {
|
||||
padding: 24px 27px 24px 38px;
|
||||
}
|
||||
background-color: var(--neutral-white);
|
||||
width: 100%;
|
||||
}
|
||||
cursor: default;
|
||||
background-color: transparent;
|
||||
border: 0px solid black;
|
||||
transition: all 0.4s ease-in-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
display: none;
|
||||
overflow: unset;
|
||||
z-index: 9998;
|
||||
inset: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
|
||||
&::backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
&.modal-xs {
|
||||
width: 30vw;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
&.modal-sm {
|
||||
width: 40vw;
|
||||
max-height: 90vh;
|
||||
|
||||
@media screen and (max-width: 1250px) {
|
||||
width: 50vw;
|
||||
}
|
||||
@media screen and (max-width: 1050px) {
|
||||
width: 60vw;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
width: 95vw;
|
||||
}
|
||||
}
|
||||
|
||||
&.modal-md {
|
||||
width: 50vw;
|
||||
max-height: 90vh;
|
||||
|
||||
@media screen and (max-width: 1250px) {
|
||||
width: 60vw;
|
||||
}
|
||||
@media screen and (max-width: 1050px) {
|
||||
width: 70vw;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
width: 95vw;
|
||||
}
|
||||
}
|
||||
|
||||
&.modal-lg {
|
||||
width: 60vw;
|
||||
max-height: 90vh;
|
||||
|
||||
@media screen and (max-width: 1250px) {
|
||||
width: 70vw;
|
||||
}
|
||||
@media screen and (max-width: 1050px) {
|
||||
width: 80vw;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
width: 95vw;
|
||||
}
|
||||
}
|
||||
|
||||
&.modal-xl {
|
||||
width: 80vw;
|
||||
max-height: 90vh;
|
||||
|
||||
@media screen and (max-width: 1050px) {
|
||||
width: 90vw;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
width: 95vw;
|
||||
}
|
||||
}
|
||||
|
||||
&.modal-full {
|
||||
width: 98vw;
|
||||
height: 98vw;
|
||||
max-height: 98vh;
|
||||
}
|
||||
|
||||
&[open] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
opacity: 1;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
0
frontend/src/lib/components/Review.svelte
Normal file
0
frontend/src/lib/components/Review.svelte
Normal file
18
frontend/src/lib/components/SSRSkip.svelte
Normal file
18
frontend/src/lib/components/SSRSkip.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { location } from "../store"
|
||||
|
||||
let oldPath: string
|
||||
$: if (typeof window !== "undefined" && oldPath !== $location.path) {
|
||||
const ref = oldPath ? document.location.protocol + "//" + document.location.host + oldPath : document.referrer
|
||||
oldPath = $location.path
|
||||
fetch(oldPath, {
|
||||
headers: {
|
||||
"x-ssr-skip": "204",
|
||||
"x-ssr-ref": ref,
|
||||
"x-ssr-res": `${window.innerWidth}x${window.innerHeight}`,
|
||||
// no cache
|
||||
"cache-control": "no-cache, no-store, must-revalidate",
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
428
frontend/src/lib/components/SidebarOverlay.svelte
Normal file
428
frontend/src/lib/components/SidebarOverlay.svelte
Normal file
@@ -0,0 +1,428 @@
|
||||
<script
|
||||
lang="ts"
|
||||
context="module"
|
||||
>
|
||||
import "simplebar"
|
||||
import "simplebar/dist/simplebar.css"
|
||||
import ResizeObserver from "resize-observer-polyfill"
|
||||
if (typeof window !== "undefined") window.ResizeObserver = ResizeObserver
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { mdiCloseCircleOutline } from "@mdi/js"
|
||||
import { isMobile, overlays } from "../store"
|
||||
import Icon from "./widgets/Icon.svelte"
|
||||
import { changeStateOfSite } from "./header/Desktop.svelte"
|
||||
$: changeStateOfSite(!!$overlays.length)
|
||||
|
||||
function checkIfNoOverlayIsActive(overlays: Array<Overlay>) {
|
||||
if (!overlays.length) return false
|
||||
return overlays.every((overlay) => !overlay?.active)
|
||||
}
|
||||
$: if (checkIfNoOverlayIsActive($overlays)) {
|
||||
if ($overlays.some((o) => o.hideTillDispatch)) {
|
||||
// make the one before it active
|
||||
const hidden = $overlays.findIndex((o) => o.hideTillDispatch)
|
||||
if (hidden > 0) {
|
||||
$overlays[hidden - 1].active = true
|
||||
} else {
|
||||
$overlays[1].active = true
|
||||
}
|
||||
} else {
|
||||
const lastIndex = $overlays.length - 1
|
||||
$overlays[lastIndex].active = true
|
||||
}
|
||||
}
|
||||
function closeModal(i: number) {
|
||||
const removedElements = $overlays.splice(i, $overlays.length - i)
|
||||
const hasActive = removedElements.some((element) => element.active === true)
|
||||
if (hasActive && $overlays.length > 0) {
|
||||
$overlays[$overlays.length - 1].active = true
|
||||
}
|
||||
$overlays = $overlays
|
||||
dispatched = dispatched.filter((dispatch, idx) => idx !== i)
|
||||
}
|
||||
let dispatched: boolean[] = []
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<div
|
||||
class:visible="{$overlays.length}"
|
||||
class="sidebarOverlay"
|
||||
id="overlay"
|
||||
role="dialog"
|
||||
aria-labelledby="overlayTitle"
|
||||
aria-label="Overlay"
|
||||
aria-modal="true"
|
||||
on:click="{() => {
|
||||
closeModal(0)
|
||||
}}"
|
||||
on:keydown="{(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
closeModal(0)
|
||||
}
|
||||
}}"
|
||||
>
|
||||
<section class="dark-side"></section>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<ul
|
||||
class="content-side"
|
||||
on:click|stopPropagation
|
||||
>
|
||||
{#each $overlays as overlay, i}
|
||||
<li
|
||||
class="content-block"
|
||||
class:active="{overlay.active}"
|
||||
class:dark="{(i + 1) % 2 == 0}"
|
||||
class:hideTillDispatch="{overlay.hideTillDispatch}"
|
||||
class:hideIt="{!dispatched[i]}"
|
||||
>
|
||||
<header
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click="{() => {
|
||||
$overlays = $overlays.map((o, index) => {
|
||||
o.active = index === i
|
||||
return o
|
||||
})
|
||||
}}"
|
||||
on:keydown="{(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
$overlays = $overlays.map((o, index) => {
|
||||
o.active = index === i
|
||||
return o
|
||||
})
|
||||
}
|
||||
}}"
|
||||
>
|
||||
<div
|
||||
class="upper-bar"
|
||||
class:noBg="{i === $overlays.length - 1 ||
|
||||
$overlays.filter((o) => o.hideTillDispatch).length > dispatched.filter(Boolean).length}"
|
||||
>
|
||||
<div class="crinkle desktop-crinkle">
|
||||
<img
|
||||
src="/media/ModalCrinkle{(i + 1) % 2 == 0 ? 'Dark' : ''}.svg"
|
||||
alt="crinkle"
|
||||
/>
|
||||
</div>
|
||||
<div class="crinkle mobile-crinkle">
|
||||
<img
|
||||
src="/media/ModalCrinkle{(i + 1) % 2 == 0 ? 'Dark' : ''}Mobile.svg"
|
||||
alt="crinkle"
|
||||
/>
|
||||
</div>
|
||||
{#if typeof overlay.title === "string"}
|
||||
<h2 id="overlayTitle">
|
||||
{overlay.title}
|
||||
</h2>
|
||||
{:else}
|
||||
<svelte:component this="{overlay.title}" />
|
||||
{/if}
|
||||
<div class="close">
|
||||
<button
|
||||
aria-label="Close"
|
||||
on:click="{() => {
|
||||
closeModal(i)
|
||||
}}"
|
||||
>
|
||||
<Icon
|
||||
path="{mdiCloseCircleOutline}"
|
||||
size="{$isMobile ? '24px' : '32px'}"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="lower-bar"></div>
|
||||
</header>
|
||||
<div
|
||||
class="scroll-container"
|
||||
class:active="{overlay.active}"
|
||||
data-simplebar
|
||||
>
|
||||
<div class="content-listing">
|
||||
{#key overlay.reload}
|
||||
<svelte:component
|
||||
this="{overlay.content}"
|
||||
on:showOverlay="{(e) => {
|
||||
dispatched[i] = true
|
||||
}}"
|
||||
on:removeOverlay="{() => {
|
||||
closeModal(i)
|
||||
}}"
|
||||
darkBg="{(i + 1) % 2 == 0}"
|
||||
{...overlay.properties}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
@import "../assets/css/variables.less";
|
||||
:global .sidebarOverlay {
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
right: 0;
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
overflow: hidden;
|
||||
z-index: 9999;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
bottom: -200%;
|
||||
top: 200%;
|
||||
&.visible {
|
||||
bottom: 0px;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
@media @desktop {
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
right: -100vw;
|
||||
left: 100vw;
|
||||
&.visible {
|
||||
right: 0px;
|
||||
}
|
||||
}
|
||||
.dark-side {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
.content-side {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-direction: column-reverse;
|
||||
@media @desktop {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
height: 100%;
|
||||
@media @mobile {
|
||||
height: 70%;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
&.hideTillDispatch {
|
||||
&.hideIt {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
header {
|
||||
.upper-bar {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 88px;
|
||||
background-color: var(--bg-100);
|
||||
|
||||
&.noBg {
|
||||
background-color: transparent;
|
||||
}
|
||||
.crinkle {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
margin-right: -1px;
|
||||
}
|
||||
|
||||
.mobile-crinkle {
|
||||
display: none;
|
||||
}
|
||||
@media @mobile {
|
||||
height: 70px;
|
||||
.desktop-crinkle {
|
||||
display: none;
|
||||
}
|
||||
.mobile-crinkle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
background-color: var(--neutral-white);
|
||||
flex-grow: 1;
|
||||
color: var(--text-invers-100);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
font-weight: 700;
|
||||
}
|
||||
.close {
|
||||
background-color: var(--neutral-white);
|
||||
padding-right: 27px;
|
||||
color: var(--text-invers-100);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
button {
|
||||
color: var(--text-invers-100);
|
||||
transform: translate(-50%, 25%);
|
||||
}
|
||||
}
|
||||
}
|
||||
.lower-bar {
|
||||
height: 1.2rem;
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
@media @mobile {
|
||||
height: unset;
|
||||
max-height: calc(100vh - 200px);
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
&:not(.active) {
|
||||
@media @mobile {
|
||||
height: unset;
|
||||
}
|
||||
}
|
||||
.simplebar-content {
|
||||
height: 100%;
|
||||
@media @mobile {
|
||||
padding-top: 12px !important;
|
||||
}
|
||||
}
|
||||
.simplebar-track {
|
||||
@media @mobile {
|
||||
margin-top: 12px !important;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
// if only one child, put it to 100%
|
||||
&:only-child {
|
||||
height: 100%;
|
||||
}
|
||||
@media @desktop {
|
||||
width: 50vw;
|
||||
border-left: 1px solid var(--bg-invers-100);
|
||||
}
|
||||
@media @large_desktop {
|
||||
width: max(30vw, 628px);
|
||||
}
|
||||
.scroll-container {
|
||||
height: calc(100% - 88px - 1.2rem);
|
||||
max-height: 100%;
|
||||
padding-right: 1.6rem;
|
||||
transition: height 0.3s;
|
||||
flex-grow: 1;
|
||||
background-color: var(--neutral-white);
|
||||
|
||||
padding-top: 2.4rem;
|
||||
@media @mobile {
|
||||
&:not(.active) {
|
||||
height: 0px;
|
||||
max-height: 0px;
|
||||
padding-top: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-track {
|
||||
background-color: rgba(13, 12, 12, 0.25);
|
||||
width: 7px;
|
||||
overflow: visible;
|
||||
margin-left: 5px;
|
||||
margin-right: 1.6rem;
|
||||
margin-top: 2.4rem;
|
||||
@media @mobile {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
.simplebar-scrollbar {
|
||||
transition-duration: 0ms !important;
|
||||
cursor: pointer;
|
||||
&::before {
|
||||
background-color: var(--bg-100);
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
opacity: 1;
|
||||
border-radius: 0;
|
||||
width: 11px;
|
||||
height: calc(100% + 2px);
|
||||
transition-delay: 0s;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content-listing {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
background-color: var(--neutral-white);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
header {
|
||||
.upper-bar {
|
||||
background-color: var(--neutral-white);
|
||||
&.noBg {
|
||||
background-color: transparent;
|
||||
}
|
||||
h2 {
|
||||
background-color: var(--bg-100);
|
||||
color: var(--text-100);
|
||||
}
|
||||
.close {
|
||||
background-color: var(--bg-100);
|
||||
button {
|
||||
color: var(--text-100) !important;
|
||||
transform: translate(25%, 25%);
|
||||
@media @mobile {
|
||||
transform: translate(-50%, 25%);
|
||||
}
|
||||
svg {
|
||||
fill: var(--text-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.lower-bar {
|
||||
background-color: var(--bg-100);
|
||||
}
|
||||
}
|
||||
.scroll-container {
|
||||
.simplebar-content {
|
||||
padding-left: 90px !important;
|
||||
padding-right: 2.8rem !important;
|
||||
@media @mobile {
|
||||
padding-left: 38px !important;
|
||||
}
|
||||
}
|
||||
background-color: var(--bg-100);
|
||||
.simplebar-track {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.simplebar-scrollbar {
|
||||
transition-duration: 0ms !important;
|
||||
cursor: pointer;
|
||||
&::before {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content-listing {
|
||||
background-color: var(--bg-100);
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
476
frontend/src/lib/components/header/Desktop.svelte
Normal file
476
frontend/src/lib/components/header/Desktop.svelte
Normal file
@@ -0,0 +1,476 @@
|
||||
<script
|
||||
context="module"
|
||||
lang="ts"
|
||||
>
|
||||
import { enableScrolling, stopScrolling } from "../../functions/utils"
|
||||
let scrollPosition = { top: 0, left: 0 }
|
||||
let whiteHeader = false
|
||||
export function changeStateOfSite(menuOn: boolean) {
|
||||
if (typeof window !== "undefined") {
|
||||
if (menuOn) {
|
||||
// make sure its not already in thsi state
|
||||
const state = document.body.classList.contains("no-scroll")
|
||||
if (state) {
|
||||
return
|
||||
}
|
||||
|
||||
stopScrolling(scrollPosition)
|
||||
} else {
|
||||
// make sure only if it is in this state
|
||||
const state = document.body.classList.contains("no-scroll")
|
||||
|
||||
if (!state) {
|
||||
return
|
||||
}
|
||||
// Remove the no-scroll class and reset the position
|
||||
enableScrolling(scrollPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { mdiAccountOutline, mdiHeartOutline, mdiMenu, mdiShoppingOutline } from "@mdi/js"
|
||||
import { spaLink } from "../../actions"
|
||||
import Icon from "../widgets/Icon.svelte"
|
||||
import { icons } from "../../../config"
|
||||
import MobileMenu from "./MobileMenu.svelte"
|
||||
import { overlays } from "../../store"
|
||||
import { showCartOverlay, showFavoriteOverlay } from "../../functions/helper/product"
|
||||
|
||||
export let elements: NavigationElement[],
|
||||
bannerVisible: boolean,
|
||||
scrolled: boolean,
|
||||
activeSubmenu = -1
|
||||
|
||||
async function changeSubmenu(index: string | number) {
|
||||
changeStateOfSite(true)
|
||||
const submenuContainers = document.getElementsByClassName("submenu-container")
|
||||
Array.from(submenuContainers).forEach((container) => container.classList.remove("shown"))
|
||||
const invisibleMenu = document.getElementById(`submenu-${index}`)
|
||||
invisibleMenu.classList.add("shown")
|
||||
activeSubmenu = Number(index)
|
||||
let headerContainer = document.getElementById("header-container")
|
||||
headerContainer.classList.add("scrolled")
|
||||
}
|
||||
|
||||
function closeSubmenu() {
|
||||
const submenuContainers = document.getElementsByClassName("submenu-container")
|
||||
Array.from(submenuContainers).forEach((container) => container.classList.remove("shown"))
|
||||
changeStateOfSite(false)
|
||||
activeSubmenu = -1
|
||||
if (window.scrollY < 100) {
|
||||
let headerContainer = document.getElementById("header-container")
|
||||
headerContainer.classList.remove("scrolled")
|
||||
}
|
||||
}
|
||||
let hoverTimeout: string | number | NodeJS.Timeout | undefined
|
||||
let hoverCloseTimeout: string | number | NodeJS.Timeout | undefined
|
||||
let isHoveringMenu = false
|
||||
function closeSubmenuWithTimeout() {
|
||||
hoverCloseTimeout = setTimeout(closeSubmenu, 200)
|
||||
}
|
||||
const openMenu = (i: number, page: NavigationElement) => {
|
||||
clearTimeout(hoverTimeout)
|
||||
if (page?.elements?.length) hoverTimeout = setTimeout(() => changeSubmenu(i), 500)
|
||||
else hoverTimeout = setTimeout(closeSubmenu, 500)
|
||||
}
|
||||
|
||||
$: if (isHoveringMenu) {
|
||||
clearTimeout(hoverCloseTimeout)
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav
|
||||
class="menu"
|
||||
class:scrolled="{scrolled || whiteHeader}"
|
||||
class:not-scrolled="{!scrolled && !whiteHeader}"
|
||||
class:active="{activeSubmenu !== -1}"
|
||||
on:mouseover="{() => (isHoveringMenu = true)}"
|
||||
on:focus="{() => (isHoveringMenu = true)}"
|
||||
on:mouseleave="{() => (isHoveringMenu = false)}"
|
||||
>
|
||||
<button
|
||||
aria-label="Open menu"
|
||||
id="burger-menu"
|
||||
class="mobile-burger"
|
||||
on:click="{() => {
|
||||
const overlay = {
|
||||
id: 'mobile-menu',
|
||||
title: 'Menu',
|
||||
active: true,
|
||||
content: MobileMenu,
|
||||
properties: {
|
||||
elements,
|
||||
bannerVisible,
|
||||
scrolled,
|
||||
},
|
||||
}
|
||||
$overlays = [overlay]
|
||||
}}"
|
||||
>
|
||||
<Icon
|
||||
path="{mdiMenu}"
|
||||
size="24px"
|
||||
color="{'#F3EED9'}"
|
||||
/>
|
||||
</button>
|
||||
<a
|
||||
href="/"
|
||||
use:spaLink
|
||||
class="logo-container"
|
||||
on:click="{closeSubmenu}"
|
||||
aria-label="Go to homepage"
|
||||
>
|
||||
<img
|
||||
src="../../../../logo/logo-white.svg"
|
||||
alt="logo"
|
||||
/>
|
||||
</a>
|
||||
<ul class="menuitem-container">
|
||||
{#each elements as page, i (i)}
|
||||
<li
|
||||
class="menu-item"
|
||||
class:active="{i == activeSubmenu}"
|
||||
on:mouseenter|stopPropagation="{() => openMenu(i, page)}"
|
||||
on:focus|stopPropagation="{() => openMenu(i, page)}"
|
||||
on:mouseleave|stopPropagation="{(e) => clearTimeout(hoverTimeout)}"
|
||||
>
|
||||
<div class="bar">
|
||||
<span></span>
|
||||
<img
|
||||
src="../../../../media/BottomLeftCrinkleRed.svg"
|
||||
alt="crinkle"
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
aria-label="{page.name}"
|
||||
href="{page.page}"
|
||||
style="display: flex; flex-direction: column; align-items: center"
|
||||
use:spaLink
|
||||
on:mousedown="{closeSubmenu}"
|
||||
><div style="padding: 0px; line-height: 100%;">{page.name}</div>
|
||||
{#if page.elements?.length}
|
||||
<Icon
|
||||
path="{icons.chevronDown}"
|
||||
color="{'#F3EED9'}"
|
||||
props="{{
|
||||
'fill-rule': 'evenodd',
|
||||
'clip-rule': 'evenodd',
|
||||
}}"
|
||||
/>
|
||||
{/if}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<ul class="icons-container">
|
||||
<li class="menu-item">
|
||||
<button
|
||||
on:click="{showFavoriteOverlay}"
|
||||
aria-label="Open favorites"
|
||||
><Icon
|
||||
color="{'#F3EED9'}"
|
||||
size="24px"
|
||||
path="{mdiHeartOutline}"
|
||||
/></button
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{#each elements as submenu, i (submenu.name)}
|
||||
{#if submenu.elements?.length}
|
||||
<section
|
||||
role="menu"
|
||||
tabindex="0"
|
||||
on:mouseleave="{() => !isHoveringMenu && closeSubmenuWithTimeout()}"
|
||||
on:focus|stopPropagation
|
||||
class="submenu-container"
|
||||
id="{`submenu-${i}`}"
|
||||
style="top: 86px"
|
||||
>
|
||||
<ul class="sub-menu-columns">
|
||||
{#each submenu?.elements.slice(0, 2) as submenu_title, i (i)}
|
||||
<li class="column">
|
||||
<a
|
||||
use:spaLink
|
||||
href="{submenu_title.page}"
|
||||
class="submenu-title"
|
||||
class:bold="{submenu_title?.elements?.length}"
|
||||
on:click="{closeSubmenu}">{submenu_title?.name}</a
|
||||
>
|
||||
{#if submenu_title?.elements?.length}
|
||||
<ul class="sub-menu-rows">
|
||||
{#each submenu_title?.elements || [] as submenu_point, j (j)}
|
||||
<li>
|
||||
<a
|
||||
use:spaLink
|
||||
href="{submenu_point.page}"
|
||||
class="submenu-endpoint"
|
||||
on:click="{closeSubmenu}">{submenu_point?.name}</a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
{#if submenu?.elements?.length > 2}
|
||||
<li class="column">
|
||||
<ul class="sub-menu-rows large-gap">
|
||||
{#each submenu.elements.slice(2) as submenu_title, k (k)}
|
||||
<li>
|
||||
<a
|
||||
use:spaLink
|
||||
href="{submenu_title.page}"
|
||||
class="submenu-title"
|
||||
class:bold="{submenu_title?.elements?.length}"
|
||||
on:click="{closeSubmenu}">{submenu_title?.name}</a
|
||||
>
|
||||
{#if submenu_title?.elements?.length}
|
||||
<ul class="sub-menu-rows">
|
||||
{#each submenu_title?.elements || [] as submenu_point, l (l)}
|
||||
<li>
|
||||
<a
|
||||
use:spaLink
|
||||
href="{submenu_point.page}"
|
||||
class="submenu-endpoint"
|
||||
on:click="{closeSubmenu}">{submenu_point?.name}</a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
<div class="img-placeholder"></div>
|
||||
<section class="image-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<img
|
||||
src="../../../../media/main-nav-pic.webp"
|
||||
alt="img"
|
||||
/>
|
||||
<svg
|
||||
width="10px"
|
||||
height="50px"
|
||||
viewBox="0 0 1 5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polygon
|
||||
points="0,0 1,0 0,5"
|
||||
fill="white"></polygon>
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<style lang="less">
|
||||
@import "../../assets/css/variables.less";
|
||||
.menu {
|
||||
max-width: var(--normal-max-width);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 86px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
&.not-scrolled a {
|
||||
color: var(--text-100);
|
||||
}
|
||||
a {
|
||||
color: var(--text-100);
|
||||
}
|
||||
.logo-container {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
img {
|
||||
height: 60px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
@media @desktop {
|
||||
#burger-menu {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.menuitem-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
width: fit-content;
|
||||
gap: 1.2rem;
|
||||
padding-top: 0.55rem;
|
||||
height: 100%;
|
||||
.menu-item {
|
||||
height: 50px;
|
||||
padding: 0px 1.2rem 0.6rem 1.2rem;
|
||||
position: relative;
|
||||
.bar {
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
|
||||
left: 0;
|
||||
right: 0;
|
||||
justify-content: flex-end;
|
||||
span {
|
||||
background-color: var(--primary-100);
|
||||
flex-grow: 1;
|
||||
margin-right: -2px;
|
||||
}
|
||||
img {
|
||||
width: 12.169px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
.bar {
|
||||
opacity: 1;
|
||||
}
|
||||
a {
|
||||
color: var(--text-100);
|
||||
}
|
||||
background-color: var(--primary-100);
|
||||
}
|
||||
}
|
||||
@media @max-tablet {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.icons-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 14px;
|
||||
gap: 1.2rem;
|
||||
@media @mobile {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.15rem;
|
||||
.menu-item {
|
||||
&:nth-of-type(1) {
|
||||
margin-right: 0.15rem;
|
||||
}
|
||||
&:nth-of-type(2) {
|
||||
margin-left: 0.15rem;
|
||||
}
|
||||
}
|
||||
.menu-item:nth-child(3) {
|
||||
grid-column: span 2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submenu-container {
|
||||
min-height: 22.3rem;
|
||||
min-width: 100vw;
|
||||
border-top: 4px solid var(--primary-100);
|
||||
height: 0px;
|
||||
z-index: 1000;
|
||||
max-width: var(--normal-max-width);
|
||||
background-color: var(--neutral-white);
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
left: -100vw;
|
||||
transition: left 300ms, opacity 300ms;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
overflow-y: hidden;
|
||||
|
||||
:global &.shown {
|
||||
opacity: 1 !important;
|
||||
left: 0px !important;
|
||||
right: 0px;
|
||||
height: fit-content;
|
||||
padding-left: calc((100vw - var(--normal-max-width)) / 2);
|
||||
@media (max-width: 1920px) {
|
||||
padding-left: var(--horizontal-default-margin);
|
||||
}
|
||||
}
|
||||
|
||||
.sub-menu-columns {
|
||||
padding: 2.4rem 0rem;
|
||||
|
||||
display: flex;
|
||||
align-items: start;
|
||||
gap: 2.4rem;
|
||||
flex-wrap: wrap;
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 180px;
|
||||
gap: 1.2rem;
|
||||
.submenu-title {
|
||||
&.bold {
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
font-style: Outfit-bold, sans-serif;
|
||||
}
|
||||
}
|
||||
.sub-menu-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.9rem;
|
||||
&.large-gap {
|
||||
gap: 2.4rem;
|
||||
}
|
||||
li {
|
||||
.submenu-endpoint {
|
||||
font-weight: 400 !important;
|
||||
font-style: Outfit-normal, sans-serif !important;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.img-placeholder {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 55%;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
max-width: 50%;
|
||||
height: 100%;
|
||||
|
||||
.inner-wrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
height: 100%;
|
||||
width: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
125
frontend/src/lib/components/header/Header.svelte
Normal file
125
frontend/src/lib/components/header/Header.svelte
Normal file
@@ -0,0 +1,125 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
import { getDBEntry } from "../../../api"
|
||||
import { navigationCache, location, categories, overlays } from "../../store"
|
||||
import DesktopHeader from "./Desktop.svelte"
|
||||
import {
|
||||
getBCGraphCategories,
|
||||
mapBigcommerceCategoriesToNavigation,
|
||||
} from "../../functions/CommerceAPIs/bigCommerce/categories"
|
||||
import Banner from "../widgets/Banner.svelte"
|
||||
|
||||
let navigationElements: NavigationElement[] = [],
|
||||
navOpen = false,
|
||||
subNavOpen: { [key: number]: boolean } = {},
|
||||
windowWidth: number
|
||||
|
||||
function elementsToCache(elements: NavigationElement[]) {
|
||||
elements.forEach((el) => {
|
||||
if (!el.external) {
|
||||
if (!$navigationCache[el.page]) $navigationCache[el.page] = el
|
||||
if (el.elements?.length > 0) elementsToCache(el.elements)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getDBEntry("navigation", {
|
||||
tree: 0,
|
||||
}).then((nav) => {
|
||||
navigationElements.push(...nav.elements)
|
||||
navigationElements = navigationElements
|
||||
})
|
||||
|
||||
$: if (!navOpen) subNavOpen = {}
|
||||
$: if ($location) navOpen = false
|
||||
|
||||
let scrolled: boolean = false,
|
||||
isHomepage: boolean = false,
|
||||
bannerVisible = false
|
||||
$: isHomepage = !$location.path || $location.path === "/"
|
||||
|
||||
function checkScroll() {
|
||||
scrolled = window.scrollY >= 100
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
checkScroll()
|
||||
window.addEventListener("scroll", checkScroll)
|
||||
return () => {
|
||||
window.removeEventListener("scroll", checkScroll)
|
||||
}
|
||||
}
|
||||
})
|
||||
$: {
|
||||
if (typeof window !== "undefined" && $location) {
|
||||
checkScroll()
|
||||
}
|
||||
}
|
||||
|
||||
let activeSubmenu = -1
|
||||
$: darkBG = isHomepage ? (scrolled ? true : activeSubmenu >= 0 || $overlays?.length) : false
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth="{windowWidth}" />
|
||||
|
||||
<Banner bind:isVisible="{bannerVisible}" />
|
||||
<header
|
||||
class="headercontainer"
|
||||
id="{'header-container'}"
|
||||
class:scrolled="{darkBG}"
|
||||
class:homepageHeader="{isHomepage}"
|
||||
class:bannerVisible="{bannerVisible}"
|
||||
role="dialog"
|
||||
aria-label="Hauptnavigation"
|
||||
on:focus
|
||||
>
|
||||
<div class="padding">
|
||||
{#key [bannerVisible]}
|
||||
<DesktopHeader
|
||||
bind:activeSubmenu="{activeSubmenu}"
|
||||
elements="{navigationElements}"
|
||||
bannerVisible="{bannerVisible}"
|
||||
scrolled="{darkBG}"
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../assets/css/variables.less";
|
||||
|
||||
@desktop: ~"only screen and (min-width: 1440px)";
|
||||
|
||||
.headercontainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@media @mobile {
|
||||
overflow: hidden;
|
||||
}
|
||||
position: sticky;
|
||||
z-index: 5500;
|
||||
top: 0px;
|
||||
|
||||
justify-content: space-between;
|
||||
background-color: #0d0c0c;
|
||||
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
.padding {
|
||||
width: 100%;
|
||||
padding: 0px var(--horizontal-default-margin);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
&.homepageHeader {
|
||||
background-color: transparent;
|
||||
}
|
||||
&.scrolled&.homepageHeader {
|
||||
box-shadow: 0px 10px 20px 0px rgba(0, 0, 0, 0.2);
|
||||
background-color: var(--bg-100);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
261
frontend/src/lib/components/header/MobileMenu.svelte
Normal file
261
frontend/src/lib/components/header/MobileMenu.svelte
Normal file
@@ -0,0 +1,261 @@
|
||||
<script
|
||||
context="module"
|
||||
lang="ts"
|
||||
>
|
||||
import { overlays } from "../../store"
|
||||
import MobileMenuSubCategory from "./MobileMenuSubCategory.svelte"
|
||||
|
||||
export function openSubmodule(element: NavigationElement) {
|
||||
overlays.update((o) => {
|
||||
const openedAtLvl = o.findIndex((overlay) => overlay.active)
|
||||
let overlays = o.map((overlay) => {
|
||||
overlay.active = false
|
||||
return overlay
|
||||
})
|
||||
overlays.splice(openedAtLvl + 1, overlays.length - openedAtLvl - 1)
|
||||
const subMenuOverlay: Overlay = {
|
||||
id: element.name,
|
||||
title: element.name,
|
||||
active: true,
|
||||
content: MobileMenuSubCategory,
|
||||
properties: {
|
||||
element,
|
||||
},
|
||||
}
|
||||
return [...overlays, subMenuOverlay]
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { mdiChevronRight } from "@mdi/js"
|
||||
import { spaLink, spaNavigate } from "../../actions"
|
||||
import Icon from "../widgets/Icon.svelte"
|
||||
import { socialIcons } from "../../../config"
|
||||
|
||||
export let elements: NavigationElement[]
|
||||
|
||||
const mainCategories = elements.filter((el) => el.type === "bigcommerce")
|
||||
let selectedCategory = mainCategories[0]
|
||||
const contentCategories = elements.filter((el) => el.type === "content")
|
||||
|
||||
//menu übereinander stacken
|
||||
</script>
|
||||
|
||||
<section id="mobileMenu">
|
||||
<section id="commerceCategories">
|
||||
<ul id="mainCategories">
|
||||
{#each mainCategories as mainCategory}
|
||||
<li>
|
||||
<button
|
||||
class="cta"
|
||||
class:active="{selectedCategory.name === mainCategory.name}"
|
||||
on:click="{() => {
|
||||
selectedCategory = mainCategory
|
||||
}}"
|
||||
>
|
||||
{mainCategory.name}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<ul id="subCategories">
|
||||
{#each selectedCategory.elements as subCategory}
|
||||
<li>
|
||||
<button
|
||||
class="cta"
|
||||
on:click="{() => {
|
||||
openSubmodule(subCategory)
|
||||
}}"
|
||||
>
|
||||
<p>{subCategory.name}</p>
|
||||
<Icon path="{mdiChevronRight}" />
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="contentCategories">
|
||||
<h4>Mehr Entdecken</h4>
|
||||
<ul>
|
||||
{#each contentCategories as contentCategory}
|
||||
{#if !!contentCategory.name}
|
||||
<li>
|
||||
<button
|
||||
class="cta"
|
||||
on:click="{() => {
|
||||
if (contentCategory.elements?.length) openSubmodule(contentCategory)
|
||||
else spaNavigate(contentCategory.page)
|
||||
}}"
|
||||
>
|
||||
<p>{contentCategory.name}</p>
|
||||
</button>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
<section class="additionalLinks">
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
use:spaLink
|
||||
href="/profile">Account</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
use:spaLink
|
||||
href="/widerrufsbelehrung">Retoure & Rückerstattung</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="socialMediaLinks">
|
||||
<ul class="social">
|
||||
{#each Object.keys(socialIcons) as icon}
|
||||
<li>
|
||||
<a
|
||||
href="{socialIcons[icon]}"
|
||||
use:spaLink
|
||||
target="_blank"
|
||||
aria-label="{icon}"
|
||||
>
|
||||
<figure class="footer-icon">
|
||||
<img
|
||||
alt="{icon}"
|
||||
src="../../../media/{icon}.svg"
|
||||
/>
|
||||
</figure></a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../assets/css/variables.less";
|
||||
#mobileMenu {
|
||||
width: 100%;
|
||||
padding: 24px 1rem 24px 90px;
|
||||
@media @mobile {
|
||||
padding: 24px 2rem 24px 38px;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
|
||||
button {
|
||||
color: var(--text-invers-100);
|
||||
width: 100%;
|
||||
text-transform: uppercase;
|
||||
font-family: "Outfit-Bold", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
}
|
||||
#commerceCategories {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
#mainCategories {
|
||||
display: flex;
|
||||
gap: 0px;
|
||||
width: 100%;
|
||||
li {
|
||||
width: 0px;
|
||||
flex-grow: 1;
|
||||
button {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--bg-100);
|
||||
|
||||
&.active {
|
||||
background-color: var(--primary-100);
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
&:nth-child(1) {
|
||||
button {
|
||||
border-right: 0px solid black;
|
||||
}
|
||||
}
|
||||
&:nth-child(2) {
|
||||
button {
|
||||
border-left: 0px solid black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#subCategories {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
li {
|
||||
button {
|
||||
background-color: var(--bg-300);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#contentCategories {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
h4 {
|
||||
font-family: "Outfit-Bold", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
li {
|
||||
button {
|
||||
background-color: var(--bg-300);
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.additionalLinks {
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 12px;
|
||||
|
||||
li {
|
||||
a {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.socialMediaLinks {
|
||||
ul {
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
justify-content: center;
|
||||
li {
|
||||
a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script lang="ts">
|
||||
import { mdiChevronRight } from "@mdi/js"
|
||||
import { spaNavigate } from "../../actions"
|
||||
import { openSubmodule } from "./MobileMenu.svelte"
|
||||
import Icon from "../widgets/Icon.svelte"
|
||||
import { overlays } from "../../store"
|
||||
export let element: NavigationElement, darkBg: boolean
|
||||
</script>
|
||||
|
||||
<ul
|
||||
class="subCategory"
|
||||
class:darkBg="{darkBg}"
|
||||
>
|
||||
{#each element.elements as subCategory}
|
||||
<li>
|
||||
<button
|
||||
class="cta"
|
||||
on:click="{() => {
|
||||
if (subCategory.elements?.length) openSubmodule(subCategory)
|
||||
else {
|
||||
$overlays = []
|
||||
spaNavigate(subCategory.page)
|
||||
}
|
||||
}}"
|
||||
>
|
||||
<p>{subCategory.name}</p>
|
||||
{#if subCategory.elements?.length}
|
||||
<Icon path="{mdiChevronRight}" />
|
||||
{/if}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style lang="less">
|
||||
.subCategory {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
li {
|
||||
button {
|
||||
background-color: var(--bg-300);
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
&.darkBg {
|
||||
background-color: transparent;
|
||||
button {
|
||||
background: var(--bg-200);
|
||||
color: var(--text-100);
|
||||
p {
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
122
frontend/src/lib/components/krasskraft/Blog.svelte
Normal file
122
frontend/src/lib/components/krasskraft/Blog.svelte
Normal file
@@ -0,0 +1,122 @@
|
||||
<script lang="ts">
|
||||
import { getDBEntry } from "../../../api"
|
||||
import CrinkledSection from "../CrinkledSection.svelte"
|
||||
import ContentBlock from "../pagebuilder/ContentBlock.svelte"
|
||||
import MedialibImage from "../widgets/MedialibImage.svelte"
|
||||
|
||||
export let entryId: string
|
||||
export let thumbnail: string
|
||||
export let sources: Source[]
|
||||
let entry: ContentEntry
|
||||
getDBEntry("content", { _id: entryId }).then((res) => {
|
||||
entry = res
|
||||
console.log("entry", entry)
|
||||
})
|
||||
</script>
|
||||
|
||||
<section class="blog-entry">
|
||||
<section class="thumbnail-image">
|
||||
<img
|
||||
src="../../../../media/topRightCrinkleDarkAndUnedged.svg"
|
||||
class="crinkle"
|
||||
alt="crinkle"
|
||||
/>
|
||||
{#if entry}
|
||||
<MedialibImage id="{thumbnail}" />
|
||||
{/if}
|
||||
</section>
|
||||
<section class="content-wrapper">
|
||||
<CrinkledSection
|
||||
activated="{true}"
|
||||
border="{false}"
|
||||
brightBackground="{false}"
|
||||
bigVersion="{true}"
|
||||
icon="bottomLeftDark"
|
||||
>
|
||||
<section class="content">
|
||||
{#if entry}
|
||||
{#each entry.blocks as block}
|
||||
<ContentBlock block="{block}" />
|
||||
{/each}
|
||||
{/if}
|
||||
</section>
|
||||
<section class="sources">
|
||||
<h4>Quellen:</h4>
|
||||
<ul>
|
||||
{#each sources as { source, url }}
|
||||
<li>
|
||||
<a
|
||||
href="{url}"
|
||||
target="_blank"
|
||||
>
|
||||
{source}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
</CrinkledSection>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.blog-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: var(--small-max-width);
|
||||
width: 100%;
|
||||
.thumbnail-image {
|
||||
width: 100%;
|
||||
height: 520px;
|
||||
position: relative;
|
||||
margin-botom: -3.6rem;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.crinkle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
.content-wrapper {
|
||||
padding: 0px 2.4rem;
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
background: var(--bg-100);
|
||||
width: 100%;
|
||||
* {
|
||||
color: var(--text-100) !important;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
.sources {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
color: var(--text-100);
|
||||
padding: 1.2rem 2.4rem;
|
||||
border-top: 1px solid var(--neutral-white);
|
||||
margin-top: 1.2rem;
|
||||
h4,
|
||||
a {
|
||||
padding: 0.6rem 0px;
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
133
frontend/src/lib/components/krasskraft/ChallengeDetailed.svelte
Normal file
133
frontend/src/lib/components/krasskraft/ChallengeDetailed.svelte
Normal file
@@ -0,0 +1,133 @@
|
||||
<script lang="ts">
|
||||
import { getCachedEntry } from "../../../api"
|
||||
import { getCalendarWeekFromDate } from "../../utils"
|
||||
import Steps from "../pagebuilder/blocks/Steps.svelte"
|
||||
import BlogTabSwitch from "../widgets/BlogTabSwitch.svelte"
|
||||
import MedialibImage from "../widgets/MedialibImage.svelte"
|
||||
import ChallengeBlog from "./Blog.svelte"
|
||||
|
||||
export let slug: string
|
||||
let challenge: BKDFChallenge
|
||||
getCachedEntry("selfImprovementChallenge", {
|
||||
slug: slug,
|
||||
}).then((res) => {
|
||||
challenge = res
|
||||
})
|
||||
let activeTab = 1
|
||||
</script>
|
||||
|
||||
<div class="rows-detailed-challenge">
|
||||
<section class="challenge-detailed-preview">
|
||||
{#if challenge}
|
||||
<h2>Weekly Challenge #{getCalendarWeekFromDate(challenge.activeAt)}</h2>
|
||||
|
||||
<div class="preview-img-wrapper">
|
||||
<MedialibImage id="{challenge.images.preview}" />
|
||||
</div>
|
||||
|
||||
<h1>{challenge.title}</h1>
|
||||
{/if}
|
||||
</section>
|
||||
<BlogTabSwitch
|
||||
tabs="{['Einleitung', 'Informationen']}"
|
||||
bind:selectedTab="{activeTab}"
|
||||
/>
|
||||
{#if activeTab == 0}
|
||||
<section class="introduction">
|
||||
{#if challenge}
|
||||
{#each challenge.introduction as intro}
|
||||
<p>{@html intro}</p>
|
||||
{/each}
|
||||
{/if}
|
||||
</section>
|
||||
{#if challenge}
|
||||
<section class="invitation">
|
||||
{challenge?.howItWorks?.invitation}
|
||||
</section>
|
||||
<Steps block="{challenge?.howItWorks}" />
|
||||
{/if}
|
||||
{:else if challenge}
|
||||
<ChallengeBlog
|
||||
entryId="{challenge.blog.blogId}"
|
||||
thumbnail="{challenge.blog.thumbnail}"
|
||||
sources="{challenge.blog.sources}"
|
||||
/>
|
||||
{/if}
|
||||
<div style="padding: 3.6rem 0px;"></div>
|
||||
</div>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.rows-detailed-challenge {
|
||||
max-width: var(--normal-max-width);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2.4rem;
|
||||
.challenge-detailed-preview {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
max-width: var(--normal-max-width);
|
||||
|
||||
h2 {
|
||||
color: transparent;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
-webkit-text-stroke: 1px var(--krass-kraft-primary);
|
||||
padding: 1.2rem 0px;
|
||||
line-height: 4.8rem;
|
||||
font-size: 4.8rem;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.preview-img-wrapper {
|
||||
width: 100%;
|
||||
height: 518px;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
span {
|
||||
color: var(--text-300);
|
||||
}
|
||||
h1 {
|
||||
font-size: 3.6rem;
|
||||
line-height: 3.6rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
.introduction {
|
||||
display: flex;
|
||||
|
||||
& > p {
|
||||
padding: 0px 1.2rem;
|
||||
color: var(--text-100);
|
||||
border-right: 1px solid white;
|
||||
* {
|
||||
color: var(--text-100);
|
||||
}
|
||||
&:first-child {
|
||||
padding-left: 0px;
|
||||
}
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
padding-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.invitation {
|
||||
padding: 2.4rem;
|
||||
color: white;
|
||||
border: 1px solid white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import { spaLink } from "../../actions"
|
||||
import MedialibImage from "../widgets/MedialibImage.svelte"
|
||||
|
||||
export let challenge: BKDFChallenge
|
||||
</script>
|
||||
|
||||
<li class="challenge-preview-item">
|
||||
<a
|
||||
class="thumbnail-wrapper"
|
||||
href="/selfimprovement/krasskraft/challenge/{challenge.slug}"
|
||||
use:spaLink
|
||||
>
|
||||
<MedialibImage id="{challenge.images.preview}" />
|
||||
</a>
|
||||
<div class="challenge-footer">
|
||||
<!-- <span>
|
||||
KW {getCalendarWeekFromDate(challenge.activeAt)}
|
||||
</span>-->
|
||||
<h3>
|
||||
{challenge.title}
|
||||
</h3>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
@import "../../assets/css/variables.less";
|
||||
li.challenge-preview-item {
|
||||
max-width: 27rem;
|
||||
width: 100%;
|
||||
@media @mobile {
|
||||
width: calc(100vw - 2 * var(--horizontal-default-margin) - 2px);
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
a.thumbnail-wrapper {
|
||||
width: 100%;
|
||||
aspect-ratio: 1.2 / 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
.challenge-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
padding-bottom: 1.2rem;
|
||||
border-bottom: 1px solid var(--krass-kraft-primary);
|
||||
span {
|
||||
color: var(--text-300);
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<script
|
||||
lang="ts"
|
||||
context="module"
|
||||
>
|
||||
import "simplebar"
|
||||
import "simplebar/dist/simplebar.css"
|
||||
import ResizeObserver from "resize-observer-polyfill"
|
||||
if (typeof window !== "undefined") window.ResizeObserver = ResizeObserver
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import ChallengePreview from "./ChallengePreview.svelte"
|
||||
export let challenges: BKDFChallenge[] = []
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="product-preview-list verticalScrollbar"
|
||||
data-simplebar
|
||||
>
|
||||
<ul class="inner-wrapper">
|
||||
{#each challenges as challenge}
|
||||
<ChallengePreview challenge="{challenge}" />
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.product-preview-list {
|
||||
width: 100%;
|
||||
max-width: var(--normal-max-width);
|
||||
|
||||
.simplebar-wrapper {
|
||||
padding-bottom: 1.2rem;
|
||||
.simplebar-content {
|
||||
max-width: var(--normal-max-width);
|
||||
& > .inner-wrapper {
|
||||
display: flex;
|
||||
gap: 2.2rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.simplebar-track {
|
||||
background-color: rgba(13, 12, 12, 0.25);
|
||||
height: 7px;
|
||||
overflow: visible;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.simplebar-scrollbar {
|
||||
transition-duration: 0ms !important;
|
||||
cursor: pointer;
|
||||
&::before {
|
||||
background-color: var(--bg-100);
|
||||
top: -2px;
|
||||
opacity: 1;
|
||||
border-radius: 0;
|
||||
height: 11px;
|
||||
left: 0px;
|
||||
|
||||
transition-delay: 0s;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
import { getDBEntries } from "../../../api"
|
||||
import { spaLink } from "../../actions"
|
||||
import Columns from "../pagebuilder/blocks/Columns.svelte"
|
||||
import ChallengePreviewList from "./ChallengePreviewList.svelte"
|
||||
|
||||
export let chapter: SelfImprovementChapter
|
||||
let challenges: BKDFChallenge[] = []
|
||||
getDBEntries("selfImprovementChallenge", {
|
||||
type: 1,
|
||||
}).then((res) => {
|
||||
challenges = res
|
||||
console.log(challenges)
|
||||
})
|
||||
</script>
|
||||
|
||||
<section class="row hp-row">
|
||||
<Columns
|
||||
block="{{
|
||||
type: 'columns',
|
||||
columns: [
|
||||
{
|
||||
type: 'chapterDescription',
|
||||
verticalAlign: 'middle',
|
||||
chapterDescription: {
|
||||
title: chapter.title,
|
||||
description: chapter.description,
|
||||
type: chapter.type,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
images: [chapter.previewImage],
|
||||
verticalAlign: 'middle',
|
||||
imageMobileBackground: true,
|
||||
},
|
||||
],
|
||||
}}"
|
||||
/>
|
||||
</section>
|
||||
<section class="challenges-row">
|
||||
<div class="headline">
|
||||
<div class="headline-col">
|
||||
<h2>Challenges</h2>
|
||||
</div>
|
||||
|
||||
<button class="">
|
||||
<a
|
||||
href="/selfimprovement/krasskraft/challenges"
|
||||
use:spaLink
|
||||
>
|
||||
Alle Anzeigen
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
<ChallengePreviewList challenges="{challenges}" />
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
max-width: var(--normal-max-width);
|
||||
}
|
||||
.row {
|
||||
flex-direction: row;
|
||||
}
|
||||
.hp-row {
|
||||
height: calc(38rem + 145px + 96px + 96px);
|
||||
max-height: 97vh;
|
||||
}
|
||||
.headline {
|
||||
width: 100%;
|
||||
margin-bottom: 1.2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
|
||||
h2 {
|
||||
font-size: 4.8rem;
|
||||
font-weight: 500;
|
||||
color: transparent;
|
||||
font-family: sans-serif;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
-webkit-text-stroke: 2px var(--krass-kraft-primary);
|
||||
}
|
||||
button {
|
||||
a {
|
||||
color: var(--krass-kraft-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
.challenges-row {
|
||||
margin-bottom: 3.6rem;
|
||||
}
|
||||
</style>
|
||||
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
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { deleteCartItem, updateCartItem } from "../../../functions/CommerceAPIs/bigCommerce/cart"
|
||||
import { deleteCookie } from "../../../functions/utils"
|
||||
import ProductInCartPreview from "../product/ProductInCartPreview.svelte"
|
||||
import LoadingWrapper from "../../widgets/LoadingWrapper.svelte"
|
||||
const dispatcher = createEventDispatcher()
|
||||
async function removeItem(id: string) {
|
||||
if (cart.lines.length == 1) {
|
||||
deleteCookie("cartId")
|
||||
await deleteCartItem(cart.id, id, true)
|
||||
dispatcher("removeCart")
|
||||
} else cart = await deleteCartItem(cart.id, id)
|
||||
}
|
||||
interface UpdateItemQuantity {
|
||||
merchandiseId: string
|
||||
productId: string
|
||||
quantity: number
|
||||
entityId: string
|
||||
}
|
||||
async function updateItemQuantity(line: UpdateItemQuantity) {
|
||||
cart = await updateCartItem(cart.id, line)
|
||||
}
|
||||
export let cart: BKDFCart,
|
||||
hideActions = false,
|
||||
showQuantity: boolean = false
|
||||
let loading = -1
|
||||
</script>
|
||||
|
||||
<ul class="products">
|
||||
{#each cart.lines as item, i (item?.id)}
|
||||
<li class="product">
|
||||
<LoadingWrapper
|
||||
active="{loading == i}"
|
||||
styles=" display: flex; width: 100%;flex-direction: column; gap: 24px;"
|
||||
>
|
||||
<ProductInCartPreview
|
||||
item="{item}"
|
||||
hideActions="{hideActions}"
|
||||
showQuantity="{showQuantity}"
|
||||
on:updateQuantity="{(e) => {
|
||||
loading = i
|
||||
updateItemQuantity({
|
||||
merchandiseId: item.merchandise.id,
|
||||
quantity: e.detail.quantity,
|
||||
productId: item.merchandise.product.id,
|
||||
entityId: item.id,
|
||||
}).finally(() => (loading = -1))
|
||||
}}"
|
||||
on:remove="{() => {
|
||||
loading = i
|
||||
removeItem(item.id).finally(() => (loading = -1))
|
||||
}}"
|
||||
/></LoadingWrapper
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style lang="less">
|
||||
.products {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
.product {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
200
frontend/src/lib/components/pagebuilder/cart/OverlayCart.svelte
Normal file
200
frontend/src/lib/components/pagebuilder/cart/OverlayCart.svelte
Normal file
@@ -0,0 +1,200 @@
|
||||
<script
|
||||
lang="ts"
|
||||
context="module"
|
||||
>
|
||||
import "simplebar"
|
||||
import "simplebar/dist/simplebar.css"
|
||||
import ResizeObserver from "resize-observer-polyfill"
|
||||
if (typeof window !== "undefined") window.ResizeObserver = ResizeObserver
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { mdiCartCheck } from "@mdi/js"
|
||||
import { getCart } from "../../../functions/CommerceAPIs/bigCommerce/cart"
|
||||
import { deleteCookie, getCookie } from "../../../functions/utils"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import CartProducts from "./CartProducts.svelte"
|
||||
import Loader from "../Loader.svelte"
|
||||
import { newNotification } from "../../../store"
|
||||
import { minimumForFreeShipping } from "../../../../config"
|
||||
|
||||
const cartId = getCookie("cartId")
|
||||
export let cart: BKDFCart | null = null,
|
||||
hideActions = false,
|
||||
showQuantity: boolean = false,
|
||||
shippingIncludedInTotal = false
|
||||
|
||||
let loading = false
|
||||
async function setCart(id: string) {
|
||||
loading = true
|
||||
cart = await getCart(id).catch(() => {
|
||||
loading = false
|
||||
newNotification({
|
||||
class: "error",
|
||||
html: `Fehler beim Laden des Warenkorbs. Bitte laden Sie die Seite neu.`,
|
||||
})
|
||||
deleteCookie("cartId")
|
||||
})
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function refetchCartAndGoToCheckout() {
|
||||
loading = true
|
||||
cart = await getCart(cartId).catch(() => {
|
||||
loading = false
|
||||
newNotification({
|
||||
class: "error",
|
||||
html: `Fehler beim Laden des Warenkorbs. Bitte laden Sie die Seite neu.`,
|
||||
})
|
||||
deleteCookie("cartId")
|
||||
})
|
||||
loading = false
|
||||
if (cart) {
|
||||
window.location.href = cart.checkoutUrl
|
||||
}
|
||||
}
|
||||
if (cartId && !cart) setCart(cartId)
|
||||
</script>
|
||||
|
||||
{#if cart}
|
||||
<div
|
||||
class="product-listing-wrapper"
|
||||
data-simplebar
|
||||
>
|
||||
<section class="product-listing-inner">
|
||||
<CartProducts
|
||||
bind:cart="{cart}"
|
||||
on:removeCart="{() => (cart = null)}"
|
||||
hideActions="{hideActions}"
|
||||
showQuantity="{showQuantity}"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
<section
|
||||
class="cart-summary"
|
||||
style="{cart.checkoutUrl ? '' : 'height: unset'} "
|
||||
>
|
||||
{#if Number(cart.cost.discountedAmount.amount) > 0}
|
||||
<div class="discount">
|
||||
<div>Rabatt</div>
|
||||
<div>-{cart.cost.discountedAmount.amount} €</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if Number(cart?.cost?.couponDiscount?.amount || 0) > 0}
|
||||
<div class="discount">
|
||||
<div>Rabatt Codes</div>
|
||||
<div>-{cart.cost.couponDiscount.amount} €</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="discount">
|
||||
<div>Versand</div>
|
||||
<div>
|
||||
{#if Number(cart.cost.amount.amount) >= minimumForFreeShipping}
|
||||
<span>Kostenfrei</span>
|
||||
{:else}
|
||||
<span>5,00 €</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="total">
|
||||
<em>Gesamt</em>
|
||||
<div>
|
||||
{(
|
||||
Number(cart.cost.amount.amount) +
|
||||
(Number(cart.cost.amount.amount) >= minimumForFreeShipping || shippingIncludedInTotal ? 0 : 5)
|
||||
).toFixed(2)} €
|
||||
</div>
|
||||
</div>
|
||||
{#if cart.checkoutUrl}
|
||||
<button
|
||||
class="checkout"
|
||||
on:click="{() => refetchCartAndGoToCheckout()}"
|
||||
><Icon path="{mdiCartCheck}" /> <span>Zur Kasse</span></button
|
||||
>
|
||||
{/if}
|
||||
</section>
|
||||
{:else if loading}
|
||||
<Loader size="3" />
|
||||
{:else}
|
||||
<p class="no-products">Keine Produkte im Warenkorb</p>
|
||||
{/if}
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
.no-products {
|
||||
padding-left: 90px;
|
||||
@media @mobile {
|
||||
padding-left: 38px;
|
||||
}
|
||||
}
|
||||
.product-listing-wrapper {
|
||||
width: 100%;
|
||||
max-height: calc(100% - 255px);
|
||||
@media @mobile {
|
||||
max-height: calc(100% - 200px);
|
||||
}
|
||||
flex-grow: 1;
|
||||
.product-listing-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding: 24px 1rem 24px 90px;
|
||||
@media @mobile {
|
||||
padding: 24px 2.8rem 24px 38px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cart-summary {
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
padding: 24px 1rem 24px 90px;
|
||||
@media @mobile {
|
||||
padding: 24px 2rem 24px 38px;
|
||||
}
|
||||
border-top: 1px solid var(--text-300);
|
||||
height: 255px;
|
||||
@media @mobile {
|
||||
height: 200px;
|
||||
padding-top: 1.2rem;
|
||||
padding-bottom: 1.2rem;
|
||||
}
|
||||
.discount,
|
||||
.total {
|
||||
padding: 0.6rem 1.2rem;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.discount {
|
||||
border-bottom: 1px solid var(--text-300);
|
||||
}
|
||||
.total {
|
||||
background-color: var(--bg-100);
|
||||
color: var(--neutral-white);
|
||||
em {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
line-height: 0.7rem;
|
||||
}
|
||||
}
|
||||
.checkout {
|
||||
width: 100%;
|
||||
padding: 0.6rem 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--neutral-white);
|
||||
font-size: 1rem;
|
||||
line-height: 0.7rem;
|
||||
background-color: var(--primary-100);
|
||||
box-shadow: 0px -2px 0px 0px rgba(0, 0, 0, 0.25);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 35px 0px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { getCart } from "../../../functions/CommerceAPIs/bigCommerce/cart"
|
||||
import { getBCGraphProductsByCategory } from "../../../functions/CommerceAPIs/bigCommerce/product"
|
||||
import { getCookie } from "../../../functions/utils"
|
||||
import ProductPreview from "../product/ProductPreview.svelte"
|
||||
const dispatch = createEventDispatcher()
|
||||
const cartId = getCookie("cartId")
|
||||
let products: BKDFProduct[] = []
|
||||
async function loadProductRecommendations() {
|
||||
const cart = await getCart(cartId).catch((error) => {
|
||||
console.log(error)
|
||||
})
|
||||
if (cart) {
|
||||
const categories = cart.lines.map((l) => {
|
||||
return l.merchandise.product.categories[0].id
|
||||
})
|
||||
const productsOfCategories: BKDFProduct[] = []
|
||||
const promises = categories.map(async (category) => {
|
||||
const products = await getBCGraphProductsByCategory(category)
|
||||
if (products.length > 0) {
|
||||
products.forEach((p) => {
|
||||
if (!productsOfCategories.find((product) => product.id === p.id)) {
|
||||
productsOfCategories.push(p)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
if (productsOfCategories.length == 0) {
|
||||
dispatch("removeOverlay")
|
||||
return
|
||||
}
|
||||
// take 5 random indices of products not already in cart and every index only once and make sure at
|
||||
let i = 0
|
||||
const randomIndizes: number[] = []
|
||||
while (randomIndizes.length < 5) {
|
||||
i++
|
||||
if (i > 100) {
|
||||
break
|
||||
}
|
||||
const randomIndex = Math.floor(Math.random() * productsOfCategories.length)
|
||||
if (
|
||||
!randomIndizes.includes(randomIndex) &&
|
||||
!cart.lines.find((line) => line.merchandise.product.id === productsOfCategories[randomIndex].id)
|
||||
) {
|
||||
randomIndizes.push(randomIndex)
|
||||
}
|
||||
}
|
||||
|
||||
products = randomIndizes.map((index) => productsOfCategories[index])
|
||||
dispatch("showOverlay")
|
||||
} else dispatch("removeOverlay")
|
||||
}
|
||||
loadProductRecommendations()
|
||||
</script>
|
||||
|
||||
{#if products.length > 0}
|
||||
<div class="product-overlay-listing-wrapper">
|
||||
<ul>
|
||||
{#each products as product}
|
||||
<ProductPreview
|
||||
product="{product}"
|
||||
brightVersion="{true}"
|
||||
/>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.product-overlay-listing-wrapper {
|
||||
li.product-preview {
|
||||
width: 100% !important;
|
||||
}
|
||||
& > ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { getBCGraphProductsByIds } from "../../../functions/CommerceAPIs/bigCommerce/product"
|
||||
import { wishlist } from "../../../store"
|
||||
import Loader from "../Loader.svelte"
|
||||
import ProductInCartPreview from "../product/ProductInCartPreview.svelte"
|
||||
async function loadProducts(): Promise<Partial<BKDFCartItem[]>> {
|
||||
const products = await getBCGraphProductsByIds($wishlist.items.map((p) => String(p.product_id)))
|
||||
const productsFormated: Partial<BKDFCartItem[]> = products.map((p) => {
|
||||
const variant = p.variants.find(
|
||||
(v) =>
|
||||
Number(v.id) ==
|
||||
Number($wishlist.items.find((w) => String(w.product_id) === String(p.id))?.variant_id)
|
||||
)
|
||||
return {
|
||||
previewImage: p.featuredImage,
|
||||
merchandise: {
|
||||
id: variant.id,
|
||||
selectedOptions: variant?.selectedOptions,
|
||||
product: p,
|
||||
},
|
||||
}
|
||||
})
|
||||
return productsFormated
|
||||
}
|
||||
let reload = false
|
||||
</script>
|
||||
|
||||
{#key $wishlist?.items?.length}
|
||||
{#if !$wishlist?.items?.length}
|
||||
<p>Keine Produkte in der Wunschliste</p>
|
||||
{:else}
|
||||
{#await loadProducts()}
|
||||
<Loader size="3" />
|
||||
{:then products}
|
||||
{#if products.length == 0}
|
||||
<p>Keine Produkte in der Wunschliste</p>
|
||||
{:else}
|
||||
<ul>
|
||||
{#each products as product}
|
||||
<li>
|
||||
<ProductInCartPreview
|
||||
item="{product}"
|
||||
on:removedFavorite="{() => (reload = !reload)}"
|
||||
hideActions="{true}"
|
||||
showFavoriteActionButtons="{true}"
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>{/if}
|
||||
{/await}
|
||||
{/if}
|
||||
{/key}
|
||||
|
||||
<style lang="less">
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { login } from "../../../store"
|
||||
import Button from "../../widgets/Button.svelte"
|
||||
import FavoriteListItems from "./FavoriteListItems.svelte"
|
||||
</script>
|
||||
|
||||
<section class="wishlistWrapper">
|
||||
{#if $login}
|
||||
<FavoriteListItems />
|
||||
{:else}
|
||||
<p>Um deine Favoriten zu sehen, musst du eingeloggt sein.</p>
|
||||
<div>
|
||||
<Button
|
||||
button="{{
|
||||
ctaType: 0,
|
||||
page: '/profile/login',
|
||||
buttonTarget: '_self',
|
||||
buttonText: 'Einloggen',
|
||||
}}"
|
||||
/>
|
||||
<Button
|
||||
button="{{
|
||||
ctaType: 1,
|
||||
page: '/profile/register',
|
||||
buttonTarget: '_self',
|
||||
buttonText: 'Registrieren',
|
||||
}}"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
.wishlistWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
padding: 24px 1rem 24px 90px;
|
||||
@media @mobile {
|
||||
padding: 24px 2rem 24px 38px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<script lang="ts">
|
||||
import { getCachedEntries } from "../../../../api"
|
||||
import { spaLink } from "../../../actions"
|
||||
import MedialibImage from "../../widgets/MedialibImage.svelte"
|
||||
import Loader from "../Loader.svelte"
|
||||
import CardWrapper from "../profile/CardWrapper.svelte"
|
||||
export let chapter: HelpCenterChapter
|
||||
let questions: ContentEntry[]
|
||||
let loading = true
|
||||
getCachedEntries("content", { type: "helpcenterQuestion", path: { $in: chapter.questions.map((q) => q.page) } })
|
||||
.then((res) => {
|
||||
questions = res
|
||||
})
|
||||
.finally(() => (loading = false))
|
||||
</script>
|
||||
|
||||
<CardWrapper>
|
||||
<div
|
||||
class="iconCardWrapper"
|
||||
slot="header"
|
||||
>
|
||||
<div class="inner-wrapper">
|
||||
<div class="blackBox"></div>
|
||||
<MedialibImage id="{chapter.brightIcon}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="innerWrapper">
|
||||
<h3>{chapter.title}</h3>
|
||||
<ul class="questions">
|
||||
{#if loading}
|
||||
<Loader size="3" />
|
||||
{:else if questions}
|
||||
{#each questions as question, i}
|
||||
{#if i < 3}
|
||||
<li class="question">
|
||||
<a
|
||||
href="/helpCenter/{chapter.slug}{question.path}"
|
||||
use:spaLink
|
||||
>
|
||||
{question.question}
|
||||
</a>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<p>Es wurden keine Fragen gefunden.</p>
|
||||
{/if}
|
||||
</ul>
|
||||
<div class="action-row">
|
||||
<button class="cta secondary">
|
||||
<a
|
||||
href="/helpCenter/{chapter.slug}"
|
||||
use:spaLink>weitere Fragen</a
|
||||
></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</CardWrapper>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.iconCardWrapper {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
width: 4.8rem;
|
||||
height: 4.8rem;
|
||||
.inner-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.blackBox {
|
||||
position: absolute;
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
background-color: var(--bg-100);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
img {
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
position: relative;
|
||||
object-fit: contain;
|
||||
z-index: 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
.innerWrapper {
|
||||
padding: 0px 2.4rem 2.4rem 2.4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 1.8rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
h3 {
|
||||
font-size: 1.6rem;
|
||||
line-height: 1.6rem;
|
||||
font-weight: 700;
|
||||
border-bottom: 2px solid var(--bg-100);
|
||||
padding-bottom: 0.6rem;
|
||||
}
|
||||
ul {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
flex-grow: 1;
|
||||
gap: 0.6rem !important;
|
||||
width: 100%;
|
||||
li {
|
||||
padding: 0.6rem 1.2rem;
|
||||
border-radius: 2px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--bg-100);
|
||||
a {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
font-family: Outfit;
|
||||
}
|
||||
}
|
||||
}
|
||||
.action-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,182 @@
|
||||
<script lang="ts">
|
||||
import { mdiArrowLeft } from "@mdi/js"
|
||||
import { spaLink } from "../../../actions"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import { getHelpCenterChapters } from "../../../functions/CommerceAPIs/tibiEndpoints/helpCenter"
|
||||
import Loader from "../Loader.svelte"
|
||||
import MedialibImage from "../../widgets/MedialibImage.svelte"
|
||||
import ChapterQuestionPreview from "./ChapterQuestionPreview.svelte"
|
||||
import ChapterQuestionDetailed from "./ChapterQuestionDetailed.svelte"
|
||||
|
||||
export let location: LocationStore
|
||||
$: pathBefore = location.path.split("/").slice(0, -1).join("/")
|
||||
let chapters: HelpCenterChapter[]
|
||||
getHelpCenterChapters().then((res) => {
|
||||
chapters = res
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="chapterDetailed">
|
||||
<div class="return">
|
||||
<a
|
||||
use:spaLink
|
||||
href="{pathBefore}"><Icon path="{mdiArrowLeft}" /> <span>Zurück</span></a
|
||||
>
|
||||
</div>
|
||||
<div class="main">
|
||||
<ul class="nav">
|
||||
{#if !Array.isArray(chapters)}
|
||||
<Loader size="3" />
|
||||
{:else}
|
||||
{#each chapters as chapter}
|
||||
<li class:active="{location.path.includes(`/helpCenter/${chapter.slug}`)}">
|
||||
<a
|
||||
href="/helpCenter/{chapter.slug}"
|
||||
use:spaLink
|
||||
>
|
||||
<span class="icon">
|
||||
<MedialibImage
|
||||
id="{location.path.includes(`/helpCenter/${chapter.slug}`)
|
||||
? chapter.brightIcon
|
||||
: chapter.darkIcon}"
|
||||
/>
|
||||
</span>
|
||||
{chapter.title}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
{#if pathBefore == "/helpCenter" || pathBefore == "/helpCenter/"}
|
||||
<ul class="questions">
|
||||
{#if !Array.isArray(chapters)}
|
||||
<Loader size="3" />
|
||||
{:else}
|
||||
{#each chapters as chapter}
|
||||
<ChapterQuestionPreview
|
||||
chapter="{chapter}"
|
||||
location="{location}"
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
{:else}
|
||||
<ChapterQuestionDetailed location="{location}" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
@import "../../../../lib/assets/css/variables.less";
|
||||
.chapterDetailed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
.return {
|
||||
a {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
font-weight: 400;
|
||||
font-family: Outfit;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
gap: 2.4rem;
|
||||
@media @mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
.nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 360px;
|
||||
|
||||
@media @mobile {
|
||||
min-width: 100%;
|
||||
}
|
||||
li {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-100);
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
background-color: var(--neutral-white);
|
||||
a {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0.6rem 1.2rem;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
.icon {
|
||||
min-height: 1.2rem;
|
||||
min-width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
width: 1.2rem;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
color: var(--bg-100);
|
||||
}
|
||||
&.active {
|
||||
background: var(--bg-100);
|
||||
a {
|
||||
color: var(--neutral-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.questions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
flex-grow: 1;
|
||||
.question {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
padding: 0.6rem 0px;
|
||||
border-bottom: 1px solid var(--bg-100);
|
||||
cursor: pointer;
|
||||
a {
|
||||
color: var(--bg-100);
|
||||
font-weight: 400;
|
||||
font-family: Outfit;
|
||||
}
|
||||
button {
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
min-width: 1.6rem;
|
||||
min-height: 1.6rem;
|
||||
border: 2px solid var(--bg-100);
|
||||
background-color: var(--neutral-white);
|
||||
transform: rotate(45deg);
|
||||
color: var(--bg-100);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
&:hover {
|
||||
button {
|
||||
transform: rotate(0);
|
||||
background-color: var(--bg-100);
|
||||
color: var(--neutral-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { getCachedEntry } from "../../../../api"
|
||||
import NotFound from "../../../../routes/NotFound.svelte"
|
||||
import ContentBlock from "../ContentBlock.svelte"
|
||||
import Loader from "../Loader.svelte"
|
||||
import Index from "../SEO/Index.svelte"
|
||||
export let location: LocationStore
|
||||
let question: ContentEntry
|
||||
let loading = true
|
||||
getCachedEntry("content", {
|
||||
type: "helpcenterQuestion",
|
||||
path: `/${location.path.split("/").filter(Boolean).pop()}`,
|
||||
})
|
||||
.then((res) => {
|
||||
question = res
|
||||
})
|
||||
.finally(() => {
|
||||
loading = false
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
<Loader size="3" />
|
||||
{:else if question}
|
||||
<Index
|
||||
title="{question.question} - BinKrassDuFass"
|
||||
keywords="Hilfe, FAQ, Fragen, Antworten, BinKrassDuFass"
|
||||
metaDescription="Antwort auf die Frage {question.question}."
|
||||
article="{true}"
|
||||
/>
|
||||
<div class="blocks">
|
||||
{#each question.blocks || [] as block, idx}
|
||||
<ContentBlock
|
||||
block="{block}"
|
||||
noHorizontalMargin="{true}"
|
||||
verticalPadding="{idx !== 0}"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<NotFound />
|
||||
{/if}
|
||||
|
||||
<style lang="less">
|
||||
.blocks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { mdiLinkVariant } from "@mdi/js"
|
||||
import { getCachedEntries } from "../../../../api"
|
||||
import { spaLink, spaNavigate } from "../../../actions"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import Loader from "../Loader.svelte"
|
||||
import Index from "../SEO/Index.svelte"
|
||||
|
||||
export let chapter: HelpCenterChapter, location: LocationStore
|
||||
getCachedEntries("content", {
|
||||
type: "helpcenterQuestion",
|
||||
path: { $in: chapter.questions.map((q) => q.page) },
|
||||
}).then((res) => {
|
||||
questions = res
|
||||
})
|
||||
let questions: ContentEntry[]
|
||||
</script>
|
||||
|
||||
{#if location.path.includes(`/helpCenter/${chapter.slug}`)}
|
||||
<Index
|
||||
title="{chapter.title} - BinKrassDuFass"
|
||||
keywords="Hilfe, FAQ, Fragen, Antworten, BinKrassDuFass"
|
||||
metaDescription="Häufig gestellte Fragen und Antworten zu BinKrassDuFass im Themengebiet {chapter.title}."
|
||||
/>
|
||||
{#if !Array.isArray(questions)}
|
||||
<Loader size="3" />
|
||||
{:else}
|
||||
{#each questions as question}
|
||||
<li class="question">
|
||||
<a
|
||||
href="/helpCenter/{chapter.slug}{question.path}"
|
||||
use:spaLink
|
||||
>
|
||||
{question.question}
|
||||
</a>
|
||||
<button
|
||||
aria-label="Link zur Frage"
|
||||
on:click="{() => {
|
||||
spaNavigate(`/helpCenter/${chapter.slug}${question.path}`)
|
||||
}}"
|
||||
>
|
||||
<Icon path="{mdiLinkVariant}" />
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts">
|
||||
import { icons } from "../../../../config"
|
||||
import { addProductToCart } from "../../../functions/helper/product"
|
||||
import { newNotification } from "../../../store"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import ToggleFavorite from "./ToggleFavorite.svelte"
|
||||
import ProductQuantity from "./widgets/ProductQuantity.svelte"
|
||||
export let variant: BKDFProductVariant,
|
||||
possibleVariants: BKDFProductVariant[],
|
||||
mobileFormat = false
|
||||
let quantity = 1
|
||||
|
||||
function noVariantError() {
|
||||
newNotification({
|
||||
class: "error",
|
||||
html: `Bitte wähle eine eindeutige Variante aus. Du musst sowohl eine Farbe als auch eine Größe auswählen.`,
|
||||
})
|
||||
}
|
||||
|
||||
function addToCart(variant: BKDFProductVariant, quantity: number) {
|
||||
loading = true
|
||||
addProductToCart(variant, quantity)
|
||||
.then(() => {
|
||||
loading = false
|
||||
})
|
||||
.catch(() => {
|
||||
loading = false
|
||||
})
|
||||
}
|
||||
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
<div
|
||||
id="actionRow"
|
||||
class="actionRow"
|
||||
class:mobileFormat="{mobileFormat}"
|
||||
>
|
||||
<ProductQuantity bind:quantity="{quantity}" />
|
||||
<button
|
||||
disabled="{loading}"
|
||||
id="addToCart"
|
||||
class="cta primary"
|
||||
on:click="{() => {
|
||||
if (!variant) return noVariantError()
|
||||
addToCart(variant, quantity)
|
||||
}}"
|
||||
>
|
||||
<Icon
|
||||
path="{icons.shoppingBag}"
|
||||
color="#F3EED9"
|
||||
width="{24}px"
|
||||
height="{24}px"
|
||||
props="{{ 'fill-rule': 'evenodd', 'clip-rule': 'evenodd' }}"
|
||||
/>
|
||||
IN DIE TASCHE</button
|
||||
>
|
||||
<ToggleFavorite
|
||||
productId="{Number(!variant ? possibleVariants[0]?.parentId : variant?.parentId)}"
|
||||
variantid="{Number(!variant ? possibleVariants[0]?.id : variant?.id)}"
|
||||
bigFormat="{mobileFormat}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
.actionRow {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-top: 1.2rem;
|
||||
grid-template-columns: 1fr 8fr 1fr;
|
||||
&.mobileFormat {
|
||||
grid-template-columns: 1fr 3fr;
|
||||
gap: 0;
|
||||
// give thrid item full width not just 1 fr
|
||||
}
|
||||
#addToCart {
|
||||
border-radius: 2px;
|
||||
padding: 0.6rem 1.2rem 0.6rem 1.2rem;
|
||||
flex-grow: 1;
|
||||
font-weight: 700;
|
||||
font-family: Outfit-Bold, sans-serif;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
|
||||
import { icons } from "../../../../config"
|
||||
import { addProductToCart } from "../../../functions/helper/product"
|
||||
|
||||
export let productId, variantId: number
|
||||
</script>
|
||||
|
||||
<button
|
||||
aria-label="In den Warenkorb legen"
|
||||
on:click="{() => {
|
||||
addProductToCart(
|
||||
{
|
||||
id: variantId,
|
||||
parentId: productId,
|
||||
},
|
||||
1
|
||||
)
|
||||
}}"
|
||||
>
|
||||
<Icon
|
||||
path="{icons.shoppingBag}"
|
||||
color="#2f4858"
|
||||
width="{24}px"
|
||||
height="{24}px"
|
||||
props="{{ 'fill-rule': 'evenodd', 'clip-rule': 'evenodd' }}"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<style lang="less">
|
||||
button {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
<script lang="ts">
|
||||
export let CL: CompleteYourLook
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { getCachedEntries } from "../../../../api"
|
||||
import ContentBlock from "../ContentBlock.svelte"
|
||||
export let bigCommerceProductId: number
|
||||
getCachedEntries("content", {
|
||||
products: bigCommerceProductId,
|
||||
type: "product",
|
||||
}).then((res) => {
|
||||
contentEntries = res
|
||||
})
|
||||
let contentEntries: ContentEntry
|
||||
</script>
|
||||
|
||||
<div class="dRows">
|
||||
{#each contentEntries || [] as contentEntry}
|
||||
{#each contentEntry.blocks || [] as block, idx}
|
||||
<ContentBlock
|
||||
block="{block}"
|
||||
noHorizontalMargin="{true}"
|
||||
/>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.dRows {
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import { spaNavigate } from "../../../actions"
|
||||
import { categories } from "../../../store"
|
||||
import FilterBlock from "./widgets/FilterBlock.svelte"
|
||||
function findCategoryByPath(categories: Category[], path: string): Category {
|
||||
for (const category of categories) {
|
||||
if (category.path === path) {
|
||||
return category
|
||||
}
|
||||
const foundInChildren = findCategoryByPath(category.children, path)
|
||||
if (foundInChildren) {
|
||||
return foundInChildren
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
function getDepthOfCategory(categories: Category[], path: string): number {
|
||||
for (const category of categories) {
|
||||
if (category.path === path) {
|
||||
return 0
|
||||
}
|
||||
const foundInChildren = getDepthOfCategory(category.children, path)
|
||||
if (foundInChildren !== undefined) {
|
||||
return foundInChildren + 1
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
export let path: string
|
||||
const category = findCategoryByPath($categories, path)
|
||||
</script>
|
||||
|
||||
{#if category}
|
||||
<div class="filter-wrapper">
|
||||
<FilterBlock
|
||||
title="{category.name}"
|
||||
deletable="{getDepthOfCategory($categories, path) > 0}"
|
||||
active="{true}"
|
||||
on:click="{() => {
|
||||
if (getDepthOfCategory($categories, path) > 0) {
|
||||
const newPath = path.endsWith('/') ? path.slice(0, -1) : path
|
||||
spaNavigate(`/collections/${newPath.split('/').slice(0, -1).join('/')}`)
|
||||
}
|
||||
}}"
|
||||
/>
|
||||
{#each category.children as subcategory}
|
||||
<FilterBlock
|
||||
title="{subcategory.name}"
|
||||
active="{false}"
|
||||
on:click="{() => {
|
||||
spaNavigate(`/collections${subcategory.path}`)
|
||||
}}"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="less">
|
||||
.filter-wrapper {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
gap: 12px;
|
||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none; /* Safari and Chrome */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,141 @@
|
||||
<script lang="ts">
|
||||
import { backgroundImages } from "../../../store"
|
||||
import MedialibImage from "../../widgets/MedialibImage.svelte"
|
||||
|
||||
export let src: string, alt: string
|
||||
|
||||
let shouldZoom = false,
|
||||
imagePosition: HTMLButtonElement,
|
||||
zoomFactor = 3, // Example zoom factor, adjust for stronger or weaker zoom
|
||||
initialClickPosition = { x: 0, y: 0 },
|
||||
currentPosition = { x: 0, y: 0 },
|
||||
coordX = 0,
|
||||
coordY = 0,
|
||||
lastTouchMoveEvent: number = 0
|
||||
|
||||
function handleMousemove(event: MouseEvent) {
|
||||
if (!shouldZoom) return
|
||||
updatePosition(event.clientX, event.clientY)
|
||||
}
|
||||
|
||||
function handleTouchMove(event: TouchEvent) {
|
||||
if (!shouldZoom) return
|
||||
event.preventDefault()
|
||||
const now = Date.now()
|
||||
if (now - lastTouchMoveEvent < 10) return
|
||||
lastTouchMoveEvent = now
|
||||
updatePosition(event.touches[0].clientX, event.touches[0].clientY)
|
||||
}
|
||||
|
||||
function handleClick(event: MouseEvent | TouchEvent) {
|
||||
shouldZoom = !shouldZoom
|
||||
if (shouldZoom) {
|
||||
if (event instanceof MouseEvent) handleMousemove(event)
|
||||
else if (event instanceof TouchEvent) handleTouchMove(event)
|
||||
} else resetPosition()
|
||||
}
|
||||
|
||||
function updatePosition(clientX: number, clientY: number) {
|
||||
var rect = imagePosition.getBoundingClientRect()
|
||||
currentPosition.x = clientX - rect.left
|
||||
currentPosition.y = clientY - rect.top
|
||||
coordX = currentPosition.x * -1 * zoomFactor
|
||||
coordY = currentPosition.y * -1 * zoomFactor
|
||||
}
|
||||
|
||||
function resetPosition() {
|
||||
initialClickPosition.x = 0
|
||||
initialClickPosition.y = 0
|
||||
coordX = 0
|
||||
coordY = 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="imageholder">
|
||||
<button
|
||||
class="image"
|
||||
aria-label="zoom in image"
|
||||
class:allowZoom="{shouldZoom}"
|
||||
on:click="{handleClick}"
|
||||
on:mousemove="{handleMousemove}"
|
||||
on:touchmove="{handleTouchMove}"
|
||||
bind:this="{imagePosition}"
|
||||
>
|
||||
<img
|
||||
src="{src.replace('2000w', '800w')}"
|
||||
alt="{alt}"
|
||||
/>
|
||||
<div class="zoomimg">
|
||||
<img
|
||||
src="{src}"
|
||||
alt="{alt}"
|
||||
style="left: {coordX}px; top: {coordY}px;"
|
||||
/>
|
||||
<div class="background-img-product">
|
||||
<MedialibImage
|
||||
id="{$backgroundImages['standard']}"
|
||||
filter="l"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="background-img-product">
|
||||
<MedialibImage
|
||||
id="{$backgroundImages['standard']}"
|
||||
filter="l"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
.imageholder {
|
||||
height: 100%;
|
||||
width: fit-content;
|
||||
flex-shrink: 0;
|
||||
.image {
|
||||
background: var(--bg-300);
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.zoomimg {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
background: var(--bg-300);
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0px;
|
||||
bottom: 0;
|
||||
right: 0px;
|
||||
img {
|
||||
position: absolute;
|
||||
width: 400%;
|
||||
height: auto;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition: top 0s ease, left 0s ease;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
cursor: cell;
|
||||
}
|
||||
&.allowZoom:hover {
|
||||
cursor: crosshair;
|
||||
> .zoomimg {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script
|
||||
context="module"
|
||||
lang="ts"
|
||||
>
|
||||
import { getDBEntries } from "../../../../api"
|
||||
|
||||
const slideInterval = 5000
|
||||
let productBenefits: productBenefit[] = []
|
||||
getDBEntries("productBenefit").then((data) => {
|
||||
productBenefits = data
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
|
||||
let shownSlide = 0,
|
||||
slideIntervalId: NodeJS.Timeout
|
||||
|
||||
function startSlideInterval() {
|
||||
slideIntervalId = setInterval(() => {
|
||||
shownSlide = (shownSlide + 1) % productBenefits.length
|
||||
}, slideInterval)
|
||||
}
|
||||
|
||||
function stopSlideInterval() {
|
||||
clearInterval(slideIntervalId)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
startSlideInterval()
|
||||
return () => stopSlideInterval()
|
||||
})
|
||||
</script>
|
||||
|
||||
<section class="slider">
|
||||
{#each productBenefits as benefit, index}
|
||||
<button
|
||||
class="slide-button {index === shownSlide ? 'selected' : ''}"
|
||||
on:click="{() => (shownSlide = index)}"
|
||||
on:mouseover="{stopSlideInterval}"
|
||||
on:focus="{stopSlideInterval}"
|
||||
on:mouseout="{startSlideInterval}"
|
||||
on:blur="{startSlideInterval}"
|
||||
>
|
||||
<em>{benefit.title}</em>
|
||||
<p>{benefit.description}</p>
|
||||
</button>
|
||||
{/each}
|
||||
<section class="slider-lines">
|
||||
{#each productBenefits as _, index}
|
||||
<div class="slider-line {index === shownSlide ? 'selected' : ''}"></div>
|
||||
{/each}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
.slider {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.slide-button {
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
padding: 5px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
transition: background-color 0.5s ease-in-out;
|
||||
&.selected {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
.slider-lines {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
.slider-line {
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background-color: grey;
|
||||
margin: 0 2px;
|
||||
transition: background-color 0.5s ease-in-out;
|
||||
&.selected {
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { getCurrentVariant } from "../../../functions/helper/product"
|
||||
import MedialibImage from "../../widgets/MedialibImage.svelte"
|
||||
import { backgroundImages } from "../../../store"
|
||||
|
||||
export let variants: BKDFProductVariant[],
|
||||
backgroundColor: string = "black",
|
||||
selectedColor: BKDFProductSelectedOption,
|
||||
selectedSize: BKDFProductSelectedOption,
|
||||
product: BKDFProduct,
|
||||
variantImageMappedObject: any
|
||||
|
||||
const dispatcher = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<ul class="productVariants {backgroundColor}">
|
||||
{#each variants.map((v) => v.selectedOptions.find((o) => o.name === "Farbe").value) as value, i}
|
||||
<li>
|
||||
<button
|
||||
class:active="{value === selectedColor?.value}"
|
||||
class:available="{selectedSize
|
||||
? getCurrentVariant(product, [{ name: 'Farbe', value }, selectedSize])?.availableForSale
|
||||
? true
|
||||
: false
|
||||
: true}"
|
||||
on:click="{() => {
|
||||
dispatcher('colorChange', {
|
||||
color: value,
|
||||
})
|
||||
}}"
|
||||
aria-label="Produkt Variante auswählen"
|
||||
>
|
||||
<div class="background-img-product">
|
||||
<MedialibImage
|
||||
id="{$backgroundImages['standard']}"
|
||||
filter="l"
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
src="{variantImageMappedObject[variants?.[i]?.id]?.[0]?.url?.replace('2000w', '64w')}"
|
||||
alt="{variants[i].defaultImage?.altText}"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style lang="less">
|
||||
.productVariants {
|
||||
width: 100%;
|
||||
height: 3.4rem;
|
||||
padding: 2px 0px;
|
||||
overflow: scroll;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
&.black {
|
||||
background-color: black;
|
||||
}
|
||||
&.white {
|
||||
background-attachment: white;
|
||||
}
|
||||
display: flex;
|
||||
button {
|
||||
cursor: pointer;
|
||||
border: 2px solid black;
|
||||
border-radius: 2px;
|
||||
width: 3.4rem;
|
||||
height: 100%;
|
||||
border: 2px solid black;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
position: relative;
|
||||
&.available {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
}
|
||||
&.active {
|
||||
border: 2px solid var(--primary-200);
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
&.white {
|
||||
button {
|
||||
border: 2px solid white;
|
||||
&.active {
|
||||
border: 2px solid var(--primary-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script
|
||||
context="module"
|
||||
lang="ts"
|
||||
>
|
||||
import {
|
||||
getCurrentVariant,
|
||||
getProductVariantsThatDifferInOptions,
|
||||
getProductVariantsThatDifferInOptionsGivenSelectedOption,
|
||||
mapVariantImages,
|
||||
} from "../../../functions/helper/product"
|
||||
|
||||
const optionTypes: { name: OptionNames; inputName: string }[] = [
|
||||
{ name: "Farbe", inputName: "color" },
|
||||
{ name: "Größe", inputName: "size" },
|
||||
]
|
||||
|
||||
function updateSelectedOptions(
|
||||
selectedOptions: BKDFProductSelectedOption[],
|
||||
optionName: OptionNames,
|
||||
value: string
|
||||
): BKDFProductSelectedOption[] {
|
||||
const newOptions = [...selectedOptions]
|
||||
const index = newOptions.findIndex((option) => option.name === optionName)
|
||||
if (index !== -1) newOptions[index].value = value
|
||||
else newOptions.push({ name: optionName, value })
|
||||
|
||||
return newOptions
|
||||
}
|
||||
|
||||
function updateColorVariants(
|
||||
product: BKDFProduct,
|
||||
selectedOptions: BKDFProductSelectedOption[]
|
||||
): { possibleColorVariants: BKDFProductVariant[]; colorVariantImageMappedObject: { [key: string]: Image[] } } {
|
||||
const selectedSizeOption = selectedOptions.find((option) => option.name === "Größe")
|
||||
const possibleColorVariants = getProductVariantsThatDifferInOptionsGivenSelectedOption(
|
||||
selectedSizeOption,
|
||||
product.variants
|
||||
)
|
||||
const colorVariantImageMappedObject = mapVariantImages(product, possibleColorVariants)
|
||||
return { possibleColorVariants, colorVariantImageMappedObject }
|
||||
}
|
||||
|
||||
function updateSizeVariants(
|
||||
product: BKDFProduct,
|
||||
selectedOptions: BKDFProductSelectedOption[]
|
||||
): BKDFProductVariant[] {
|
||||
const selectedColorOption = selectedOptions.find((option) => option.name === "Farbe")
|
||||
return getProductVariantsThatDifferInOptionsGivenSelectedOption(selectedColorOption, product.variants)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import ActionRow from "./ActionRow.svelte"
|
||||
import ProductColorPreviewList from "./ProductColorPreviewList.svelte"
|
||||
import ProductSizesPreviewList from "./ProductSizesPreviewList.svelte"
|
||||
|
||||
export let product: BKDFProduct,
|
||||
mobileFormat = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let possibleColorVariants: BKDFProductVariant[] = [],
|
||||
colorVariantImageMappedObject: { [key: string]: Image[] } = {},
|
||||
possibleSizeVariants: BKDFProductVariant[] = [],
|
||||
selectedVariant: BKDFProductVariant,
|
||||
selectedOptions: BKDFProductSelectedOption[] = []
|
||||
|
||||
function handleOptionChange(optionName: OptionNames, value: string) {
|
||||
selectedOptions = updateSelectedOptions(selectedOptions, optionName, value)
|
||||
const currentVariant = getCurrentVariant(product, selectedOptions)
|
||||
if (!!currentVariant) {
|
||||
;({ possibleColorVariants, colorVariantImageMappedObject } = updateColorVariants(
|
||||
product,
|
||||
selectedOptions.filter((option) => option?.name === "Größe")
|
||||
))
|
||||
handleVariantChange(currentVariant)
|
||||
}
|
||||
}
|
||||
|
||||
function handleVariantChange(variant: BKDFProductVariant) {
|
||||
selectedVariant = variant
|
||||
dispatch("variantChange", {
|
||||
variant: selectedVariant,
|
||||
previewImages: colorVariantImageMappedObject[selectedVariant.id],
|
||||
})
|
||||
}
|
||||
|
||||
const reactOnSizingChange = () => {
|
||||
;({ possibleColorVariants, colorVariantImageMappedObject } = updateColorVariants(
|
||||
product,
|
||||
selectedOptions.filter((option) => option?.name === "Größe")
|
||||
))
|
||||
}
|
||||
|
||||
function reactOnColorChange() {
|
||||
possibleSizeVariants = updateSizeVariants(
|
||||
product,
|
||||
selectedOptions.filter((option) => option?.name === "Farbe")
|
||||
)
|
||||
if (!selectedVariant)
|
||||
dispatch("colorChange", {
|
||||
previewImages:
|
||||
colorVariantImageMappedObject[Object.keys(mapVariantImages(product, possibleSizeVariants))[0]],
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
possibleColorVariants = getProductVariantsThatDifferInOptions(["Farbe"], product.variants)
|
||||
possibleSizeVariants = getProductVariantsThatDifferInOptions(["Größe"], product.variants)
|
||||
colorVariantImageMappedObject = mapVariantImages(product, possibleColorVariants)
|
||||
if (!selectedVariant)
|
||||
dispatch("colorChange", {
|
||||
previewImages: colorVariantImageMappedObject[Object.keys(colorVariantImageMappedObject)[0]],
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<section id="configurator">
|
||||
{#each optionTypes as { name }}
|
||||
{#if name == "Farbe"}
|
||||
<ProductColorPreviewList
|
||||
variants="{possibleColorVariants}"
|
||||
selectedSize="{selectedOptions.find((o) => o.name === 'Größe')}"
|
||||
selectedColor="{selectedOptions.find((o) => o.name === 'Farbe')}"
|
||||
product="{product}"
|
||||
variantImageMappedObject="{colorVariantImageMappedObject}"
|
||||
backgroundColor="white"
|
||||
on:colorChange="{(event) => {
|
||||
handleOptionChange(name, event.detail.color)
|
||||
reactOnColorChange()
|
||||
}}"
|
||||
/>
|
||||
{:else if name == "Größe"}
|
||||
<ProductSizesPreviewList
|
||||
possibleSizeVariants="{possibleSizeVariants}"
|
||||
selectedSize="{selectedOptions.find((o) => o.name === 'Größe')}"
|
||||
product="{product}"
|
||||
selectedColor="{selectedOptions.find((o) => o.name === 'Farbe')}"
|
||||
on:sizeChange="{(e) => {
|
||||
handleOptionChange(name, e.detail.size)
|
||||
reactOnSizingChange()
|
||||
}}"
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<ActionRow
|
||||
variant="{selectedVariant}"
|
||||
possibleVariants="{possibleSizeVariants}"
|
||||
mobileFormat="{mobileFormat}"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
#configurator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
margin-top: 1.2rem;
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,202 @@
|
||||
<script lang="ts">
|
||||
import { spaLink } from "../../../actions"
|
||||
|
||||
export let product: BKDFProduct
|
||||
let shownIndex: number = -1
|
||||
</script>
|
||||
|
||||
<section id="ExtendableProductDetails">
|
||||
<div
|
||||
class="box"
|
||||
class:opened="{shownIndex === 0}"
|
||||
>
|
||||
<div
|
||||
class="upper"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:keydown
|
||||
on:click="{() => {
|
||||
if (shownIndex === 0) shownIndex = -1
|
||||
else shownIndex = 0
|
||||
}}"
|
||||
>
|
||||
<h4>Beschreibung</h4>
|
||||
<div>
|
||||
<img
|
||||
src="../../../../../media/add-diamond.svg"
|
||||
alt="diamond"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
class:closed="{shownIndex !== 0}"
|
||||
>
|
||||
{@html product.descriptionHtml}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="box"
|
||||
class:opened="{shownIndex === 2}"
|
||||
>
|
||||
<div
|
||||
class="upper"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:keydown
|
||||
on:click="{() => {
|
||||
if (shownIndex === 2) shownIndex = -1
|
||||
else shownIndex = 2
|
||||
}}"
|
||||
>
|
||||
<h4>Lieferung & Rücksendung</h4>
|
||||
<div>
|
||||
<img
|
||||
src="../../../../../media/add-diamond.svg"
|
||||
alt="diamond"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
class:closed="{shownIndex !== 2}"
|
||||
>
|
||||
<ul>
|
||||
<li>
|
||||
<h4>Versandkosten ab 100 Euro gratis</h4>
|
||||
<p>
|
||||
Ab einem Bestellwert von 100 Euro liefern wir deine Bestellung versandkostenfrei. Die Lieferzeit
|
||||
beträgt in der Regel 7 Werktage, sodass du deine neuen Sportklamotten schnell in den Händen
|
||||
hältst.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>Rücksendung</h4>
|
||||
<p>
|
||||
Du kannst deine Bestellung innerhalb von 30 Tagen nach Erhalt der Ware zurücksenden. Die Kosten
|
||||
für die Rücksendung übernehmen wir nicht. Dir wird ein Versandschein zur Verfügung gestellt,
|
||||
wobei die Kosten dafür von dem zurückerstatteten Betrag abgezogen werden. Die Rücksendung kann
|
||||
ganz bequem über unsere Retourenfunktion in deinem persönlichen Konto abgewickelt werden.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>Mehr Details</h4>
|
||||
<p>
|
||||
Weitere Informationen zur Rücksendung findest du in unserer ausführlichen <a
|
||||
href="/widerrufsbelehrung"
|
||||
use:spaLink
|
||||
>
|
||||
Widerrufsbelehrung
|
||||
</a>
|
||||
sowie in unserem
|
||||
<a
|
||||
href="/helpcenter"
|
||||
use:spaLink>HelpCenter</a
|
||||
>. Bei Fragen stehen wir dir jederzeit zur Verfügung.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="box"
|
||||
class:opened="{shownIndex === 1}"
|
||||
>
|
||||
<div
|
||||
class="upper"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:keydown
|
||||
on:click="{() => {
|
||||
if (shownIndex === 1) shownIndex = -1
|
||||
else shownIndex = 1
|
||||
}}"
|
||||
>
|
||||
<h4>GPSR-bezogene Produktinformationen der EU</h4>
|
||||
<div>
|
||||
<img
|
||||
src="../../../../../media/add-diamond.svg"
|
||||
alt="diamond"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
class:closed="{shownIndex !== 1}"
|
||||
>
|
||||
<ul>
|
||||
<li>
|
||||
Hersteller-Kontaktinformationen
|
||||
<ul>
|
||||
<li>Name: Robin Grenzdörfer</li>
|
||||
<li>Email-Adresse: support@binkrassdufass.de</li>
|
||||
<li>Postanschrift: Robin Grenzdörfer; Schultheißstraße 39; 85049 Ingolstadt; Deutschland</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>EU-Garantie: 2 Jahre</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
#ExtendableProductDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 72px;
|
||||
.box {
|
||||
border-bottom: 2px solid var(--text-invers-100);
|
||||
display: flex;
|
||||
transition: padding-bottom 1s;
|
||||
padding-bottom: 0px;
|
||||
flex-direction: column;
|
||||
padding-top: 20px;
|
||||
gap: 20px;
|
||||
&.opened {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.upper {
|
||||
cursor: pointer;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
div {
|
||||
transition: transform 1s;
|
||||
img {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
}
|
||||
}
|
||||
div {
|
||||
}
|
||||
}
|
||||
&.opened {
|
||||
.upper {
|
||||
div {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
max-height: 1000px;
|
||||
overflow: hidden;
|
||||
transition: max-height 1s ease-in;
|
||||
ul {
|
||||
list-style-type: none;
|
||||
li {
|
||||
margin-bottom: 1.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.closed {
|
||||
max-height: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
import MagnifyableProductImage from "./MagnifyableProductImage.svelte"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import { mdiMagnifyPlusOutline } from "@mdi/js"
|
||||
import { tooltip } from "../../../functions/utils"
|
||||
|
||||
export let previewImages: Image[] = [],
|
||||
mobileView = false
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
const firstImageInSlider = document.querySelector("#productPreviewSlider .wrapper .imageholder")
|
||||
|
||||
if (firstImageInSlider) {
|
||||
const firstImageWidth = firstImageInSlider.clientWidth
|
||||
const wrapper = document.querySelector("#productPreviewSlider .wrapper")
|
||||
if (wrapper) {
|
||||
const viewportWidth = window.innerWidth
|
||||
if (firstImageWidth > viewportWidth) {
|
||||
// Scroll to the center of the first image with smooth transition
|
||||
wrapper.scrollTo({
|
||||
left: (firstImageWidth - viewportWidth) / 2,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
</script>
|
||||
|
||||
<section
|
||||
id="productPreviewSlider"
|
||||
class:mobileView="{mobileView}"
|
||||
>
|
||||
<div class="wrapper">
|
||||
{#each previewImages as previewImage}
|
||||
<MagnifyableProductImage
|
||||
src="{previewImage?.url}"
|
||||
alt="{previewImage?.altText}"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="icon"
|
||||
use:tooltip="{{
|
||||
content: 'Clicke auf das Bild um zu zoomen',
|
||||
}}"
|
||||
>
|
||||
<Icon
|
||||
path="{mdiMagnifyPlusOutline}"
|
||||
size="2.4rem"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
#productPreviewSlider {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: min(65vh, 38.25rem);
|
||||
position: relative;
|
||||
&.mobileView {
|
||||
height: calc(90vh - 86px);
|
||||
}
|
||||
.icon {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
z-index: 2;
|
||||
bottom: 1rem;
|
||||
height: 2.4rem;
|
||||
width: 2.4rem;
|
||||
}
|
||||
.wrapper {
|
||||
display: flex;
|
||||
gap: 0px;
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
position: relative;
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--primary-100);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: black;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,155 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
import ProductQuantity from "./widgets/ProductQuantity.svelte"
|
||||
import { mdiDeleteCircle, mdiOpenInApp } from "@mdi/js"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import ProductPricetags from "./widgets/ProductPricetags.svelte"
|
||||
import ToggleFavorite from "./ToggleFavorite.svelte"
|
||||
import { spaLink } from "../../../actions"
|
||||
import MedialibImage from "../../widgets/MedialibImage.svelte"
|
||||
import { backgroundImages } from "../../../store"
|
||||
export let item: BKDFCartItem,
|
||||
hideActions: boolean = false,
|
||||
showQuantity: boolean = false,
|
||||
showFavoriteActionButtons: boolean = false
|
||||
const product = item.merchandise.product
|
||||
const selectedColor = item.merchandise.selectedOptions?.find((o) => o.name === "Farbe")?.value
|
||||
const previewImage = product.images.find((i) => JSON.parse(i.altText).Farbe === selectedColor)
|
||||
const dispatcher = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<section class="productCartPreview">
|
||||
<div class="imageContainer">
|
||||
<div class="background-img-product">
|
||||
<MedialibImage
|
||||
id="{$backgroundImages['standard']}"
|
||||
filter="m"
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
src="{previewImage?.url?.replace('2000w', '120w')}"
|
||||
alt="{previewImage?.altText}"
|
||||
/>
|
||||
</div>
|
||||
<div class="informations">
|
||||
<section class="details">
|
||||
<h5>{product.title}</h5>
|
||||
{#if item.merchandise.selectedOptions}
|
||||
<small>{item.merchandise.selectedOptions?.map((o) => o.value).join(" | ")}</small>
|
||||
{/if}
|
||||
<slot />
|
||||
<div class="">
|
||||
<ProductPricetags
|
||||
product="{product}"
|
||||
smallVersion="{true}"
|
||||
/>
|
||||
|
||||
{#if !hideActions}
|
||||
<div class="actions">
|
||||
<ProductQuantity
|
||||
quantity="{item.quantity}"
|
||||
on:updateQuantity="{(e) => dispatcher('updateQuantity', e.detail)}"
|
||||
/>
|
||||
<button
|
||||
on:click="{() => dispatcher('remove')}"
|
||||
aria-label="Entfernen"
|
||||
><Icon
|
||||
path="{mdiDeleteCircle}"
|
||||
size="18px"
|
||||
/></button
|
||||
>
|
||||
</div>
|
||||
{:else if showQuantity}
|
||||
<div class="actions">
|
||||
{item.quantity} Stk.
|
||||
</div>
|
||||
{:else if showFavoriteActionButtons}
|
||||
<div class="actions">
|
||||
<ToggleFavorite
|
||||
on:removedFavorite
|
||||
productId="{Number(product.id)}"
|
||||
variantid="{Number(item.merchandise.id)}"
|
||||
/>
|
||||
<button aria-label="Produktseite öffnen"
|
||||
><a
|
||||
href="/product{item.merchandise.product.handle}"
|
||||
use:spaLink
|
||||
><Icon
|
||||
path="{mdiOpenInApp}"
|
||||
size="24px"
|
||||
/></a
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
.productCartPreview {
|
||||
display: flex;
|
||||
gap: 25px;
|
||||
width: 100%;
|
||||
padding-bottom: 1.2rem;
|
||||
border-bottom: 1px solid var(--bg-300);
|
||||
align-items: center;
|
||||
|
||||
.imageContainer {
|
||||
max-height: 6rem;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: auto;
|
||||
img {
|
||||
height: 100%;
|
||||
max-height: 6rem;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
}
|
||||
}
|
||||
.informations {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-grow: 1;
|
||||
.details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
small {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
& > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
button {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
& > div {
|
||||
gap: 12px;
|
||||
flex-direction: column-reverse;
|
||||
max-width: 150px;
|
||||
.actions {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,142 @@
|
||||
<script lang="ts">
|
||||
import { spaLink } from "../../../actions"
|
||||
import {
|
||||
getBCGraphBestsellingProducts,
|
||||
getBCGraphFeaturedProducts,
|
||||
getBCGraphNewestProducts,
|
||||
getBCGraphProductsByCategory,
|
||||
getBCGraphProductsByIds,
|
||||
} from "../../../functions/CommerceAPIs/bigCommerce/product"
|
||||
import Loader from "../Loader.svelte"
|
||||
import ProductPreviewList from "./ProductPreviewList.svelte"
|
||||
|
||||
export let block: ContentBlock<"productSlider">
|
||||
const productSlider = block.productSlider
|
||||
let loadProducts: any
|
||||
async function setProductsByIds() {
|
||||
if ("productIds" in productSlider) {
|
||||
const products = await getBCGraphProductsByIds(productSlider.productIds)
|
||||
const orderedProducts = productSlider.productIds.map((id) => products.find((product) => product.id == id))
|
||||
return orderedProducts.filter((product) => product !== undefined)
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
if (productSlider.productSource == "manual") {
|
||||
loadProducts = setProductsByIds
|
||||
} else if (productSlider.productSource == "category") {
|
||||
async function setProductsByCategory() {
|
||||
if ("categoryId" in productSlider) {
|
||||
return getBCGraphProductsByCategory(Number(productSlider.categoryId))
|
||||
}
|
||||
}
|
||||
loadProducts = setProductsByCategory
|
||||
} else if (productSlider.productSource == "bestseller") {
|
||||
async function setProductsByBestseller() {
|
||||
return getBCGraphBestsellingProducts()
|
||||
}
|
||||
loadProducts = setProductsByBestseller
|
||||
} else if (productSlider.productSource == "newProducts") {
|
||||
async function setNewProducts() {
|
||||
return getBCGraphNewestProducts()
|
||||
}
|
||||
loadProducts = setNewProducts
|
||||
} else if (productSlider.productSource == "featured") {
|
||||
async function setFeaturedProducts() {
|
||||
return getBCGraphFeaturedProducts()
|
||||
}
|
||||
loadProducts = setFeaturedProducts
|
||||
} else {
|
||||
loadProducts = setProductsByIds
|
||||
}
|
||||
let products: any[]
|
||||
let loading = true
|
||||
let error
|
||||
loadProducts()
|
||||
.then((res) => {
|
||||
products = res
|
||||
|
||||
if (productSlider.maxProducts && products.length > productSlider.maxProducts) {
|
||||
const randomIndizes: number[] = []
|
||||
while (randomIndizes.length < productSlider.maxProducts) {
|
||||
const randomIndex = Math.floor(Math.random() * products.length)
|
||||
if (!randomIndizes.includes(randomIndex)) {
|
||||
randomIndizes.push(randomIndex)
|
||||
}
|
||||
}
|
||||
products = randomIndizes.map((i) => products[i])
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
error = e
|
||||
})
|
||||
.finally(() => {
|
||||
loading = false
|
||||
})
|
||||
const cta = productSlider.callToActionButtons[0]
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
<Loader
|
||||
size="4"
|
||||
type="bar"
|
||||
/>
|
||||
{:else if products}
|
||||
<div class="product-preview-list-wrapper">
|
||||
{#if productSlider.headline}
|
||||
<div class="headline">
|
||||
<div class="headline-col">
|
||||
{#if productSlider.topLine}
|
||||
<small>
|
||||
{productSlider.topLine}
|
||||
</small>
|
||||
{/if}
|
||||
|
||||
<h2>{productSlider.headline}</h2>
|
||||
</div>
|
||||
{#if cta}
|
||||
<button class="">
|
||||
<a
|
||||
href="{cta.page}"
|
||||
use:spaLink>{cta.buttonText}</a
|
||||
>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<ProductPreviewList products="{products}" />
|
||||
</div>
|
||||
{:else}
|
||||
<p>Es wurden keine Produkte gefunden.</p>
|
||||
{/if}
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
@import "../../../assets/css/variables.less";
|
||||
.product-preview-list-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
width: 100%;
|
||||
.headline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 1.6rem;
|
||||
.headline-col {
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
color: var(--primary-100);
|
||||
}
|
||||
|
||||
small {
|
||||
font-weight: 700;
|
||||
font-family: Outfit-Bold, sans-serif;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<script lang="ts">
|
||||
import TopRightCrinkle from "../../widgets/TopRightCrinkle.svelte"
|
||||
|
||||
import ProductConfigurator from "./ProductConfigurator.svelte"
|
||||
import ProductExtendableBoxes from "./ProductExtendableBoxes.svelte"
|
||||
import ProductImagePreview from "./ProductImagePreview.svelte"
|
||||
import ProductSummary from "./ProductSummary.svelte"
|
||||
import RecommendedProducts from "./RecommendedProducts.svelte"
|
||||
import ProductBenefits from "./widgets/ProductBenefits.svelte"
|
||||
import ProductRatings from "./widgets/ProductRatings.svelte"
|
||||
import DynamicRows from "./DynamicRows.svelte"
|
||||
|
||||
export let product: BKDFProduct
|
||||
let previewImages: Image[] = [],
|
||||
selectedVariant: BKDFProductVariant | null = null
|
||||
</script>
|
||||
|
||||
<section id="productPage">
|
||||
<div class="product-page-wrapper">
|
||||
<section class="lefternSide">
|
||||
<div class="crinkle">
|
||||
<TopRightCrinkle
|
||||
edges="{false}"
|
||||
brightColor="{true}"
|
||||
/>
|
||||
</div>
|
||||
<div class="outerWrapper">
|
||||
<div class="inner-container">
|
||||
{#if previewImages?.length}
|
||||
<ProductImagePreview previewImages="{previewImages}" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if product}
|
||||
<DynamicRows bigCommerceProductId="{Number(product.id)}" />
|
||||
{/if}
|
||||
|
||||
{#if product}
|
||||
<ProductExtendableBoxes product="{product}" />
|
||||
{/if}
|
||||
{#if product}
|
||||
<ProductRatings bigCommerceProductId="{Number(product.id)}" />
|
||||
<RecommendedProducts product="{product}" />
|
||||
{/if}
|
||||
</section>
|
||||
<section class="righternSide">
|
||||
{#if product}
|
||||
<ProductSummary
|
||||
product="{product}"
|
||||
productTitleAsHeading="{true}"
|
||||
/>
|
||||
<ProductConfigurator
|
||||
product="{product}"
|
||||
on:colorChange="{(e) => {
|
||||
previewImages = e.detail.previewImages
|
||||
}}"
|
||||
on:variantChange="{(e) => {
|
||||
selectedVariant = e.detail.variant
|
||||
previewImages = e.detail.previewImages
|
||||
}}"
|
||||
/>
|
||||
{/if}
|
||||
<ProductBenefits />
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
#productPage {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding-bottom: 3rem;
|
||||
|
||||
justify-content: center;
|
||||
.product-page-wrapper {
|
||||
margin: 0 var(--horizontal-default-margin);
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: var(--normal-max-width);
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
|
||||
.lefternSide {
|
||||
min-height: 100%;
|
||||
width: 0px;
|
||||
flex-grow: 2;
|
||||
margin-bottom: 2.4rem;
|
||||
position: relative;
|
||||
overflow-x: visible;
|
||||
.icon {
|
||||
}
|
||||
.crinkle {
|
||||
position: sticky;
|
||||
width: calc(100% + 5px);
|
||||
height: 0px;
|
||||
overflow: visible;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
top: 146px;
|
||||
|
||||
z-index: 100;
|
||||
}
|
||||
.outerWrapper {
|
||||
height: min(65vh, 38.25rem);
|
||||
width: 100%;
|
||||
.inner-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
@media (min-width: 1770px) {
|
||||
min-width: calc(100% + (100vw - var(--normal-max-width)) / 2);
|
||||
left: calc(-1 * (100vw - var(--normal-max-width)) / 2);
|
||||
}
|
||||
@media (max-width: 1769px) {
|
||||
left: calc(-1 * var(--horizontal-default-margin));
|
||||
min-width: calc(100% + var(--horizontal-default-margin));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.righternSide {
|
||||
width: 0px;
|
||||
flex-grow: 1;
|
||||
|
||||
padding-left: 3.6rem;
|
||||
position: sticky;
|
||||
top: 140px;
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,106 @@
|
||||
<script lang="ts">
|
||||
import TopRightCrinkle from "../../widgets/TopRightCrinkle.svelte"
|
||||
import DynamicRows from "./DynamicRows.svelte"
|
||||
import ProductConfigurator from "./ProductConfigurator.svelte"
|
||||
import ProductExtendableBoxes from "./ProductExtendableBoxes.svelte"
|
||||
import ProductImagePreview from "./ProductImagePreview.svelte"
|
||||
import ProductSummary from "./ProductSummary.svelte"
|
||||
import RecommendedProducts from "./RecommendedProducts.svelte"
|
||||
import ProductRatings from "./widgets/ProductRatings.svelte"
|
||||
|
||||
export let product: BKDFProduct
|
||||
let previewImages: Image[] = [],
|
||||
selectedVariant: BKDFProductVariant | null = null
|
||||
</script>
|
||||
|
||||
<div class="crinkle">
|
||||
<TopRightCrinkle
|
||||
edges="{false}"
|
||||
brightColor="{false}"
|
||||
bigVersion="{true}"
|
||||
>
|
||||
<svg
|
||||
slot="icon"
|
||||
width="72"
|
||||
height="71"
|
||||
viewBox="0 0 72 71"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
id="Vector 3"
|
||||
d="M72 0V71L0 0H72Z"
|
||||
fill="#0d0c0c"></path>
|
||||
</svg>
|
||||
</TopRightCrinkle>
|
||||
</div>
|
||||
<section id="productPage">
|
||||
{#if previewImages?.length}
|
||||
<ProductImagePreview
|
||||
previewImages="{previewImages}"
|
||||
mobileView="{true}"
|
||||
/>
|
||||
{/if}
|
||||
<div class="content-wrapper">
|
||||
<ProductSummary
|
||||
product="{product}"
|
||||
productTitleAsHeading="{true}"
|
||||
/>
|
||||
<ProductConfigurator
|
||||
product="{product}"
|
||||
on:colorChange="{(e) => {
|
||||
previewImages = e.detail.previewImages
|
||||
}}"
|
||||
on:variantChange="{(e) => {
|
||||
selectedVariant = e.detail.variant
|
||||
previewImages = e.detail.previewImages
|
||||
}}"
|
||||
mobileFormat="{true}"
|
||||
/>
|
||||
{#if product}
|
||||
<DynamicRows bigCommerceProductId="{Number(product.id)}" />
|
||||
{/if}
|
||||
<ProductExtendableBoxes product="{product}" />
|
||||
{#if product}
|
||||
<ProductRatings bigCommerceProductId="{Number(product.id)}" />
|
||||
<RecommendedProducts product="{product}" />
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
.crinkle {
|
||||
width: calc(100% + 5px);
|
||||
height: 0px;
|
||||
overflow: visible;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
top: 84px;
|
||||
width: 100%;
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
z-index: 1000;
|
||||
}
|
||||
#productPage {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 3rem;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.content-wrapper {
|
||||
padding: 0 var(--horizontal-default-margin);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
max-width: var(--normal-max-width);
|
||||
position: relative;
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,326 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
addProductToCart,
|
||||
getOptionValues,
|
||||
getProductVariantsThatDifferInOptions,
|
||||
mapVariantImages,
|
||||
} from "../../../functions/helper/product"
|
||||
import ProductColorPreviewList from "./ProductColorPreviewList.svelte"
|
||||
import ToggleFavorite from "./ToggleFavorite.svelte"
|
||||
import ProductSummary from "./ProductSummary.svelte"
|
||||
import { spaLink } from "../../../actions"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import { icons } from "../../../../config"
|
||||
import LoadingWrapper from "../../widgets/LoadingWrapper.svelte"
|
||||
import MedialibImage from "../../widgets/MedialibImage.svelte"
|
||||
import { backgroundImages, overlays } from "../../../store"
|
||||
|
||||
export let product: BKDFProduct,
|
||||
shownImage = product.featuredImage,
|
||||
brightVersion = false
|
||||
|
||||
const variants = getProductVariantsThatDifferInOptions(["Farbe"], product.variants),
|
||||
variantImageMappedObject = mapVariantImages(product, variants)
|
||||
let currentVariant = variants[0],
|
||||
hover = false
|
||||
|
||||
function handleMouseOver() {
|
||||
hover = true
|
||||
shownImage = variantImageMappedObject[currentVariant.id][1] || variantImageMappedObject[currentVariant.id][0]
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
hover = false
|
||||
shownImage = variantImageMappedObject[currentVariant.id][0]
|
||||
}
|
||||
|
||||
function handleColorChange(event: CustomEvent) {
|
||||
currentVariant = variants.find(
|
||||
(v) => v.selectedOptions.find((o) => o.name === "Farbe")?.value === event.detail.color
|
||||
)
|
||||
shownImage = variantImageMappedObject[currentVariant.id][0]
|
||||
}
|
||||
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
<li
|
||||
class="product-preview"
|
||||
class:brightVersion="{brightVersion}"
|
||||
>
|
||||
<section
|
||||
class="productImage"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:focus
|
||||
on:mouseover="{handleMouseOver}"
|
||||
on:mouseleave="{handleMouseLeave}"
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
class:active="{hover}"
|
||||
aria-label="Produktinformationen"
|
||||
>
|
||||
<div class="favorite">
|
||||
<button
|
||||
class="favorite-toggle"
|
||||
on:click|stopPropagation
|
||||
on:mouseover|stopPropagation
|
||||
on:focus|stopPropagation
|
||||
aria-label="Favorisieren"
|
||||
>
|
||||
<ToggleFavorite
|
||||
productId="{product.id}"
|
||||
variantid="{currentVariant.id}"
|
||||
brightVersion="{brightVersion}"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="cart-btn"
|
||||
aria-label="In den Warenkorb legen"
|
||||
on:click|preventDefault|stopPropagation="{() => {
|
||||
handleMouseOver()
|
||||
}}"
|
||||
>
|
||||
<Icon
|
||||
path="{icons.shoppingBag}"
|
||||
color="{brightVersion ? '#FFF' : '#2F4858'}"
|
||||
width="1.2rem"
|
||||
height="1.2rem"
|
||||
props="{{ 'fill-rule': 'evenodd', 'clip-rule': 'evenodd' }}"
|
||||
/>
|
||||
</button>
|
||||
<img
|
||||
src="../../../../media/topRightCrinkle{brightVersion ? 'Dark' : 'Bright'}AndUnedged.svg"
|
||||
alt="crinkle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="sizing">
|
||||
<em>CHOOSE YOUR STYLE</em>
|
||||
<ul>
|
||||
{#each getOptionValues(product, "Größe") as size}
|
||||
<li>
|
||||
<button
|
||||
aria-label="Größe wählen"
|
||||
on:click="{() => {
|
||||
let loading = true
|
||||
const selectedColor = currentVariant.selectedOptions.find(
|
||||
(o) => o.name === 'Farbe'
|
||||
)?.value
|
||||
const selectedVariant = product.variants.find(
|
||||
(v) =>
|
||||
v.selectedOptions.find((o) => o.name === 'Farbe')?.value ===
|
||||
selectedColor &&
|
||||
v.selectedOptions.find((o) => o.name === 'Größe')?.value === size
|
||||
)
|
||||
addProductToCart(selectedVariant, 1).then(() => {
|
||||
loading = false
|
||||
if ($overlays.length > 0) {
|
||||
$overlays[0].reload = !$overlays[0].reload
|
||||
}
|
||||
})
|
||||
}}">{size}</button
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{#key loading}
|
||||
<div>
|
||||
<LoadingWrapper active="{loading}">
|
||||
<a
|
||||
href="/product{product.handle}"
|
||||
use:spaLink
|
||||
on:focus
|
||||
aria-label="Produktseite öffnen"
|
||||
>
|
||||
<div class="background-img-product">
|
||||
<MedialibImage
|
||||
id="{$backgroundImages['standard']}"
|
||||
filter="l"
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
src="{shownImage?.url.replace('2000w', '540w')}"
|
||||
alt="{shownImage?.altText}"
|
||||
/>
|
||||
</a>
|
||||
</LoadingWrapper>
|
||||
</div>
|
||||
{/key}
|
||||
</section>
|
||||
|
||||
<ProductColorPreviewList
|
||||
variants="{variants}"
|
||||
bind:currentVariant="{currentVariant}"
|
||||
backgroundColor="black"
|
||||
on:colorChange="{handleColorChange}"
|
||||
variantImageMappedObject="{variantImageMappedObject}"
|
||||
/>
|
||||
|
||||
<ProductSummary
|
||||
product="{product}"
|
||||
brightVersion="{brightVersion}"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
:global .background-img-product {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
.product-preview {
|
||||
overflow-y: hidden;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1.2rem;
|
||||
max-width: 27rem;
|
||||
width: 100%;
|
||||
@media @mobile {
|
||||
width: calc(100vw - 2 * var(--horizontal-default-margin) - 2px);
|
||||
}
|
||||
.productImage {
|
||||
position: relative;
|
||||
|
||||
a {
|
||||
width: 100%;
|
||||
aspect-ratio: 1.2 / 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
div[role="dialog"] {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
height: 3.6rem;
|
||||
width: 3.6rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
img {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
}
|
||||
|
||||
.favorite {
|
||||
height: 3.6rem;
|
||||
position: relative;
|
||||
width: 3.6rem;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-direction: column;
|
||||
|
||||
z-index: 100;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
right: 0px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.cart-btn {
|
||||
display: none;
|
||||
width: 100% !important;
|
||||
z-index: 999;
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
.favorite-toggle {
|
||||
margin-top: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
@media @mobile {
|
||||
justify-content: flex-end;
|
||||
height: 4rem;
|
||||
width: 4rem;
|
||||
flex-direction: column-reverse;
|
||||
.cart-btn {
|
||||
display: block;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.favorite-toggle {
|
||||
margin-top: 10px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
img {
|
||||
top: -3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.sizing {
|
||||
background: rgba(13, 12, 12, 0.5);
|
||||
padding: 0.6rem 0rem 0.6rem 0rem;
|
||||
display: flex;
|
||||
z-index: 99;
|
||||
width: 100%;
|
||||
right: -100%;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
overflow: hidden;
|
||||
em {
|
||||
color: var(--text-100);
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-size: 0.7rem;
|
||||
line-height: 0.7rem;
|
||||
font-family: Outfit-Bold, sans-serif;
|
||||
}
|
||||
ul {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
li {
|
||||
button {
|
||||
padding: 0.3rem 0.6rem 0.3rem 0.6rem;
|
||||
border-right: 1px solid var(--text-100);
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
color: var(--text-100);
|
||||
&:hover {
|
||||
color: var(--text-invers-100);
|
||||
background-color: var(--text-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
width: 100%;
|
||||
.sizing {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<script
|
||||
lang="ts"
|
||||
context="module"
|
||||
>
|
||||
import "simplebar"
|
||||
import "simplebar/dist/simplebar.css"
|
||||
import ResizeObserver from "resize-observer-polyfill"
|
||||
if (typeof window !== "undefined") window.ResizeObserver = ResizeObserver
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import ProductPreview from "./ProductPreview.svelte"
|
||||
export let products: BKDFProduct[] = []
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="product-preview-list verticalScrollbar"
|
||||
data-simplebar
|
||||
>
|
||||
<ul class="inner-wrapper">
|
||||
{#each products as product (product.id)}
|
||||
<ProductPreview product="{product}" />
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.product-preview-list {
|
||||
width: 100%;
|
||||
max-width: var(--normal-max-width);
|
||||
|
||||
.simplebar-wrapper {
|
||||
padding-bottom: 1.2rem;
|
||||
.simplebar-content {
|
||||
max-width: var(--normal-max-width);
|
||||
& > .inner-wrapper {
|
||||
display: flex;
|
||||
gap: 2.2rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.simplebar-track {
|
||||
background-color: rgba(13, 12, 12, 0.25);
|
||||
height: 7px;
|
||||
overflow: visible;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.simplebar-scrollbar {
|
||||
transition-duration: 0ms !important;
|
||||
cursor: pointer;
|
||||
&::before {
|
||||
background-color: var(--bg-100);
|
||||
top: -2px;
|
||||
opacity: 1;
|
||||
border-radius: 0;
|
||||
height: 11px;
|
||||
left: 0px;
|
||||
|
||||
transition-delay: 0s;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,183 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { getCurrentVariant } from "../../../functions/helper/product"
|
||||
import Modal from "../../Modal.svelte"
|
||||
import SizeChart from "./widgets/SizeChart.svelte"
|
||||
import { modules } from "../../../store"
|
||||
import { getTibiProduct } from "../../../functions/CommerceAPIs/tibiEndpoints/product"
|
||||
|
||||
export let possibleSizeVariants: BKDFProductVariant[] = [],
|
||||
selectedColor: BKDFProductSelectedOption,
|
||||
selectedSize: BKDFProductSelectedOption,
|
||||
product: BKDFProduct
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let showSizingChart = false
|
||||
let forcedWarning = ""
|
||||
async function loadSizeChart() {
|
||||
loading = true
|
||||
const tibiProduct = await getTibiProduct(Number(product.id))
|
||||
const sizingChart = tibiProduct.sizingChart || ({} as TSizeChart)
|
||||
if (tibiProduct.forcedWarning && tibiProduct.forcedWarning.trim().length > 0) {
|
||||
forcedWarning = tibiProduct.forcedWarning
|
||||
}
|
||||
if (sizingChart && "columns" in sizingChart)
|
||||
sizingChart.columns = (sizingChart?.columns || []).map((column) => {
|
||||
if ($modules?.length)
|
||||
column.germanLabelTranslation =
|
||||
$modules.find((module) => module.label === column.label)?.germanLabelTranslation || column.label
|
||||
return column
|
||||
})
|
||||
return sizingChart
|
||||
}
|
||||
let sizingChart: TSizeChart
|
||||
let loading = false
|
||||
$: if ($modules?.length && !sizingChart && !loading)
|
||||
loadSizeChart()
|
||||
.then((res) => {
|
||||
// @ts-ignore
|
||||
sizingChart = res
|
||||
})
|
||||
.finally(() => {
|
||||
loading = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<section id="sizing">
|
||||
<div class="heading">
|
||||
<em><small>CHOOSE SIZE</small></em>
|
||||
<button
|
||||
on:click|preventDefault|stopPropagation="{() => {
|
||||
showSizingChart = true
|
||||
}}">Größentabelle</button
|
||||
>
|
||||
</div>
|
||||
|
||||
<ul class="sizes">
|
||||
{#each possibleSizeVariants.map((v) => v.selectedOptions.find((o) => o.name === "Größe").value) as value}
|
||||
<li
|
||||
class:active="{selectedSize?.value === value}"
|
||||
class="{selectedColor
|
||||
? getCurrentVariant(product, [{ name: 'Größe', value }, selectedColor])?.availableForSale
|
||||
? 'forSale'
|
||||
: 'notForSale'
|
||||
: 'forSale'}"
|
||||
>
|
||||
<button
|
||||
on:click="{() => {
|
||||
dispatch('sizeChange', { size: value })
|
||||
}}"
|
||||
>
|
||||
{value}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<div class="top-heading">
|
||||
{#if forcedWarning}
|
||||
<strong class="hint">Heads Up</strong>
|
||||
<p class="warning">{@html forcedWarning}</p>
|
||||
{:else if sizingChart && sizingChart.generalDescription?.includes("fällt klein aus")}
|
||||
<strong class="hint">Heads Up</strong>
|
||||
<p class="warning">
|
||||
Dieser Artikel fällt <strong>klein</strong> aus. Daher empfehlen wir, eine Nummer größer zu bestellen.
|
||||
</p>
|
||||
{:else if sizingChart && sizingChart.generalDescription?.includes("fällt groß aus")}
|
||||
<strong class="hint">Heads Up</strong>
|
||||
<p class="warning">
|
||||
Dieser Artikel fällt <strong>groß</strong> aus. Daher empfehlen wir, eine Nummer kleiner zu bestellen.
|
||||
</p>
|
||||
{:else if sizingChart && sizingChart.generalDescription?.includes("wenn deine Maße zwischen den Größen liegen")}
|
||||
<strong class="hint">Heads Up</strong>
|
||||
<p class="warning">
|
||||
Solltest du unsicher sein, welche Größe du bestellen sollstest, empfehlen wir, die größere Größe zu
|
||||
wählen.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
{#if showSizingChart}
|
||||
<Modal
|
||||
show="{true}"
|
||||
on:close="{() => {
|
||||
showSizingChart = false
|
||||
}}"
|
||||
>
|
||||
<svelte:fragment slot="title">Größen</svelte:fragment>
|
||||
|
||||
<div class="sizeChart">
|
||||
{#if !loading}
|
||||
<SizeChart sizingChart="{sizingChart}" />
|
||||
{/if}
|
||||
</div>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style lang="less">
|
||||
h2 {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
#sizing {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
.heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
button {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
}
|
||||
.top-heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
.hint {
|
||||
font-size: 0.7rem;
|
||||
color: var(--primary-100);
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
}
|
||||
.warning {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
.sizes {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
li {
|
||||
flex-grow: 1;
|
||||
height: 2rem;
|
||||
|
||||
min-width: 4rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-300);
|
||||
&.notForSale {
|
||||
button {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
&.active {
|
||||
border: 1px solid var(--primary-100);
|
||||
}
|
||||
button {
|
||||
padding: 0rem 1.2rem 0rem 1.2rem;
|
||||
cursor: pointer !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,104 @@
|
||||
<script lang="ts">
|
||||
import { mdiInformationOutline } from "@mdi/js"
|
||||
|
||||
import ProductPricetags from "./widgets/ProductPricetags.svelte"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import { tooltip } from "../../../functions/utils"
|
||||
|
||||
export let product: BKDFProduct,
|
||||
productTitleAsHeading: boolean = false,
|
||||
brightVersion: boolean = false
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="productDetails1"
|
||||
class:brightVersion="{brightVersion}"
|
||||
>
|
||||
{#if productTitleAsHeading}
|
||||
<h1>{product.title}</h1>
|
||||
{:else}
|
||||
<em>{product.title}</em>
|
||||
{/if}
|
||||
{#if product.title.includes("QR")}
|
||||
<div
|
||||
use:tooltip="{{
|
||||
content: `Der QR-Code auf diesem Produkt leitet dich zu deinem persönlichen Gym-Profil auf BinKrassDuFass.de weiter. Hier kannst du deine Trainingsergebnisse und Rekorde hochladen. Dein Profil kannst du in den Accounteinstellungen verwalten.`,
|
||||
}}"
|
||||
class="helperText"
|
||||
>
|
||||
<Icon path="{mdiInformationOutline}" />
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="productDetails2"
|
||||
class:brightVersion="{brightVersion}"
|
||||
>
|
||||
<ProductPricetags product="{product}" />
|
||||
{#if product.reviews.avgRating}
|
||||
<div class="avgRating">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M21.9842 10.7231L17.7561 14.4131L19.0226 19.9069C19.0897 20.1941 19.0705 20.4946 18.9677 20.771C18.8648 21.0474 18.6827 21.2874 18.4442 21.4608C18.2057 21.6343 17.9214 21.7336 17.6267 21.7464C17.3321 21.7591 17.0402 21.6847 16.7876 21.5325L11.9961 18.6262L7.21483 21.5325C6.96223 21.6847 6.67038 21.7591 6.37574 21.7464C6.0811 21.7336 5.79676 21.6343 5.55826 21.4608C5.31976 21.2874 5.13769 21.0474 5.03481 20.771C4.93193 20.4946 4.9128 20.1941 4.97983 19.9069L6.24451 14.4187L2.01545 10.7231C1.79177 10.5302 1.63003 10.2755 1.5505 9.99107C1.47098 9.7066 1.47721 9.40498 1.56842 9.12404C1.65964 8.84309 1.83176 8.59533 2.06322 8.41182C2.29468 8.22832 2.57517 8.11723 2.86951 8.0925L8.44389 7.60968L10.6198 2.41968C10.7335 2.14735 10.9251 1.91473 11.1707 1.75111C11.4163 1.58749 11.7047 1.50018 11.9998 1.50018C12.2949 1.50018 12.5834 1.58749 12.829 1.75111C13.0745 1.91473 13.2662 2.14735 13.3798 2.41968L15.5623 7.60968L21.1348 8.0925C21.4292 8.11723 21.7097 8.22832 21.9411 8.41182C22.1726 8.59533 22.3447 8.84309 22.4359 9.12404C22.5271 9.40498 22.5334 9.7066 22.4538 9.99107C22.3743 10.2755 22.2126 10.5302 21.9889 10.7231H21.9842Z"
|
||||
fill="#2F4858"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
{product.reviews.avgRating.toFixed(1)}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
.productDetails1 {
|
||||
margin-top: 1.2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
button {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
em,
|
||||
h1 {
|
||||
color: var(--text-invers-100);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-style: Outfit;
|
||||
}
|
||||
&.brightVersion {
|
||||
em,
|
||||
h1 {
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
.productDetails2 {
|
||||
margin-top: 1.2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.avgRating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
color: var(--text-invers-100);
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-family: Outfit-Bold, sans-serif;
|
||||
}
|
||||
&.brightVersion {
|
||||
.avgRating {
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { spaLink } from "../../../actions"
|
||||
import { getBCGraphProductsByCategory } from "../../../functions/CommerceAPIs/bigCommerce/product"
|
||||
import ProductPreviewList from "./ProductPreviewList.svelte"
|
||||
export let product: BKDFProduct
|
||||
let products: BKDFProduct[] = []
|
||||
|
||||
getBCGraphProductsByCategory(product.categories[0]?.id).then((data) => {
|
||||
products = data.filter((el) => el.id !== product.id).slice(0, 5)
|
||||
})
|
||||
let innerWidth = window.innerWidth
|
||||
let isMobile = innerWidth < 768
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth="{innerWidth}" />
|
||||
|
||||
{#if products.length}
|
||||
<div class="product-preview-list-wrapper">
|
||||
<div class="headline">
|
||||
<div class="headline-col">
|
||||
<h2>
|
||||
<!-- svelte-ignore empty-block -->
|
||||
Weitere Produkte {#if isMobile}{:else}aus dieser Kategorie{/if}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<button class="">
|
||||
<a
|
||||
href="/collections{product.categories[0]?.path}"
|
||||
use:spaLink>Alle Anzeigen</a
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ProductPreviewList products="{products}" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="less">
|
||||
.product-preview-list-wrapper {
|
||||
margin-top: 2.4rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { mdiHeart, mdiHeartOutline } from "@mdi/js"
|
||||
import Icon from "../../widgets/Icon.svelte"
|
||||
import { login, newNotification, wishlist } from "../../../store"
|
||||
import { addWishlistEntry, removeWishlistEntry } from "../../../functions/CommerceAPIs/tibiEndpoints/wishlist"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
export let productId: number,
|
||||
variantid: number,
|
||||
bigFormat = false,
|
||||
brightVersion: boolean = false
|
||||
function checkIfIsOnWishlist(productId: number, wishlist: RestWishlist) {
|
||||
if (!wishlist || !wishlist.items || !wishlist.items.length) return false
|
||||
return wishlist?.items.some((p) => p.product_id == productId)
|
||||
}
|
||||
const dispatch = createEventDispatcher()
|
||||
let isOnWishlist = false
|
||||
$: isOnWishlist = checkIfIsOnWishlist(productId, $wishlist)
|
||||
</script>
|
||||
|
||||
<button
|
||||
class:isOnWishlist="{isOnWishlist}"
|
||||
class:bigFormat="{bigFormat}"
|
||||
class="{bigFormat ? 'cta tertiary' : ''}"
|
||||
class:brightVersion="{brightVersion}"
|
||||
on:click="{() => {
|
||||
if (!$login) {
|
||||
newNotification({
|
||||
html: 'Bitte logge dich ein, um Produkte zur Wunschliste hinzuzufügen',
|
||||
class: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
if (isOnWishlist) {
|
||||
const variantid = $wishlist.items.find((p) => p.product_id == productId).variant_id
|
||||
removeWishlistEntry(productId, variantid).then(() => {
|
||||
newNotification({
|
||||
html: 'Produkt wurde von der Wunschliste entfernt',
|
||||
class: 'success',
|
||||
})
|
||||
dispatch('removedFavorite')
|
||||
})
|
||||
} else {
|
||||
addWishlistEntry(productId, variantid).then(() => {
|
||||
newNotification({
|
||||
html: 'Produkt wurde zur Wunschliste hinzugefügt',
|
||||
class: 'success',
|
||||
})
|
||||
})
|
||||
}
|
||||
}}"
|
||||
>
|
||||
<Icon
|
||||
path="{isOnWishlist ? mdiHeart : mdiHeartOutline}"
|
||||
size="1.2rem"
|
||||
/>
|
||||
{#if bigFormat} zu Favoriten hinzufügen {/if}
|
||||
</button>
|
||||
|
||||
<style lang="less">
|
||||
button {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
color: var(--text-invers-100);
|
||||
&.bigFormat {
|
||||
grid-column: span 2;
|
||||
margin-top: 1.2rem;
|
||||
border: 1px solid var(--bg-100);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
&.brightVersion {
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,123 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Icon from "../../../widgets/Icon.svelte"
|
||||
import { mdiCloseCircleOutline, mdiFolderOutline } from "@mdi/js"
|
||||
|
||||
export let title,
|
||||
active,
|
||||
deletable = true
|
||||
const dispatcher = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<button
|
||||
class:active="{active}"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
dispatcher('click')
|
||||
}}"
|
||||
aria-label="filter {title}"
|
||||
>
|
||||
<div class="title">
|
||||
<span> {title}</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="46"
|
||||
height="38"
|
||||
viewBox="0 0 46 38"
|
||||
fill="none"
|
||||
>
|
||||
<g transform="rotate(180 23 19)">
|
||||
<path
|
||||
d="M44.5352 0V-1H43.5352H5H2.56171L4.29786 0.712034L42.8331 38.712L44.5352 40.3905V38V0Z"
|
||||
fill="#0D0C0C"
|
||||
stroke-width="2"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="right">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="39"
|
||||
height="38"
|
||||
viewBox="0 0 39 38"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M38.5352 0V38L0 0H38.5352Z"
|
||||
fill="#0D0C0C"></path>
|
||||
</svg>
|
||||
<span>
|
||||
{#if active}
|
||||
{#if deletable}
|
||||
<Icon
|
||||
path="{mdiCloseCircleOutline}"
|
||||
size="24px"
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<Icon
|
||||
path="{mdiFolderOutline}"
|
||||
size="24px"
|
||||
/>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<style lang="less">
|
||||
button {
|
||||
display: flex;
|
||||
border-radius: 4px;
|
||||
background: var(--bg-300);
|
||||
|
||||
.title {
|
||||
padding: 9.5px 12px;
|
||||
margin-right: 45px;
|
||||
line-height: 0.7rem;
|
||||
height: 100%;
|
||||
font-size: 1rem;
|
||||
color: var(--text-invers-100);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: var(--bg-300);
|
||||
position: relative;
|
||||
span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
svg {
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
position: absolute;
|
||||
display: none;
|
||||
transform: translateX(calc(100% - 2px));
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
.title {
|
||||
background-color: var(--bg-100);
|
||||
color: var(--neutral-white);
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
position: relative;
|
||||
.right {
|
||||
display: flex;
|
||||
background-color: var(--bg-100);
|
||||
height: 100%;
|
||||
padding: 9.5px 12px;
|
||||
svg {
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
position: absolute;
|
||||
transform: translateX(calc(-100% - 11.5px));
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
import { getDBEntries } from "../../../../../api"
|
||||
|
||||
let benefits: ProductBenefit[] = []
|
||||
getDBEntries("productBenefit").then((res) => {
|
||||
benefits = res
|
||||
setTimeout(() => {
|
||||
const progressBars = document.querySelectorAll(".progress-bar")
|
||||
progressBars.forEach((progressBar, i) => {
|
||||
if (i === activeBenefit) progressBar.classList.add("active")
|
||||
else progressBar.classList.remove("active")
|
||||
})
|
||||
}, 10)
|
||||
})
|
||||
|
||||
let activeBenefit = 0
|
||||
|
||||
onMount(() => {
|
||||
let interval = setInterval(() => {
|
||||
activeBenefit = (activeBenefit + 1) % benefits.length
|
||||
// give corresponding progressbar class active and remove it form the other ones
|
||||
const progressBars = document.querySelectorAll(".progress-bar")
|
||||
progressBars.forEach((progressBar, i) => {
|
||||
if (i === activeBenefit) {
|
||||
progressBar.classList.add("active")
|
||||
} else {
|
||||
progressBar.classList.remove("active")
|
||||
}
|
||||
})
|
||||
}, 4500)
|
||||
return () => clearInterval(interval)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="benefits-wrapper">
|
||||
<ul class="benefits">
|
||||
{#each benefits as benefit, i}
|
||||
<li class="benefit {i === activeBenefit ? 'active' : ''}">
|
||||
<h4>{benefit.title}</h4>
|
||||
<p>{@html benefit.description}</p>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<div class="currentActive">
|
||||
{#each benefits as _}
|
||||
<div class="wrapper">
|
||||
<div class="progress-bar"></div>
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.benefits-wrapper {
|
||||
overflow: hidden;
|
||||
padding: 1.2rem 0px;
|
||||
margin-top: 1.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.9rem;
|
||||
|
||||
.benefits {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 60px;
|
||||
.benefit {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transition: left 0.3s linear, opacity 0.3s linear;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: -500px;
|
||||
h4 {
|
||||
font-family: Outfit-Bold;
|
||||
}
|
||||
p {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
&.active {
|
||||
opacity: 1;
|
||||
left: 0px;
|
||||
pointer-events: auto;
|
||||
transition-delay: 0.3s;
|
||||
transition: opacity 0.3s linear, left 0s linear;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.currentActive {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
.wrapper {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
max-width: 2.4rem;
|
||||
.dot {
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
background: var(--bg-300);
|
||||
}
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
left: 0px;
|
||||
width: 0px;
|
||||
transition: width 4.5s linear;
|
||||
&.active {
|
||||
height: 100%;
|
||||
background: var(--primary-100);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import Pricetag from "../../../widgets/Pricetag.svelte"
|
||||
|
||||
export let product: BKDFProduct
|
||||
export let smallVersion: boolean = false
|
||||
const retailPrice = Number(product.retailPrice.amount),
|
||||
salePrice = Number(product.salePrice.amount),
|
||||
hasDiscount = !isNaN(salePrice) && salePrice < retailPrice
|
||||
</script>
|
||||
|
||||
<div class="pricetag">
|
||||
{#if hasDiscount}
|
||||
<Pricetag
|
||||
oldPrice="{false}"
|
||||
discount="{true}"
|
||||
price="{product.salePrice}"
|
||||
smallVersion="{smallVersion}"
|
||||
/>
|
||||
{/if}
|
||||
<Pricetag
|
||||
oldPrice="{hasDiscount}"
|
||||
discount="{false}"
|
||||
price="{product.retailPrice}"
|
||||
smallVersion="{smallVersion}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
.pricetag {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-left: 7px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { mdiMinusCircleOutline, mdiPlusCircleOutline } from "@mdi/js"
|
||||
import Icon from "../../../widgets/Icon.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
const dispatch = createEventDispatcher()
|
||||
export let quantity = 1,
|
||||
lowerBound = 1
|
||||
</script>
|
||||
|
||||
<div class="quantity">
|
||||
<button
|
||||
aria-label="weniger"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
quantity = Math.max(lowerBound, quantity - 1)
|
||||
dispatch('updateQuantity', { quantity })
|
||||
}}"><Icon path="{mdiMinusCircleOutline}" /></button
|
||||
>
|
||||
<div class="wrapper">
|
||||
<small> {quantity}</small>
|
||||
</div>
|
||||
<button
|
||||
aria-label="mehr"
|
||||
on:click|stopPropagation|preventDefault="{() => {
|
||||
quantity = Math.max(lowerBound, quantity + 1)
|
||||
dispatch('updateQuantity', { quantity })
|
||||
}}"><Icon path="{mdiPlusCircleOutline}" /></button
|
||||
>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
.quantity {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 6px;
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 25px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
small {
|
||||
height: fit-content;
|
||||
border-bottom: 1px solid var(--bg-200);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding-bottom: 6px;
|
||||
padding-top: 6px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
button {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,254 @@
|
||||
<script lang="ts">
|
||||
import { getTibiProduct } from "../../../../functions/CommerceAPIs/tibiEndpoints/product"
|
||||
import Loader from "../../Loader.svelte"
|
||||
|
||||
export let bigCommerceProductId: number
|
||||
let ratingDetails: any
|
||||
|
||||
async function setRating() {
|
||||
getTibiProduct(bigCommerceProductId).then((LocalProduct) => {
|
||||
// fill rating details
|
||||
const ratings = LocalProduct.ratings
|
||||
ratingDetails = {
|
||||
ratings,
|
||||
overview: {
|
||||
fiveStar: {
|
||||
count: ratings.filter((r) => r.rating.overall === 5).length,
|
||||
percentage:
|
||||
ratings.length > 0
|
||||
? (ratings.filter((r) => r.rating.overall === 5).length / ratings.length) * 100
|
||||
: 0,
|
||||
},
|
||||
fourStar: {
|
||||
count: ratings.filter((r) => r.rating.overall === 4).length,
|
||||
percentage:
|
||||
ratings.length > 0
|
||||
? (ratings.filter((r) => r.rating.overall === 4).length / ratings.length) * 100
|
||||
: 0,
|
||||
},
|
||||
threeStar: {
|
||||
count: ratings.filter((r) => r.rating.overall === 3).length,
|
||||
percentage:
|
||||
ratings.length > 0
|
||||
? (ratings.filter((r) => r.rating.overall === 3).length / ratings.length) * 100
|
||||
: 0,
|
||||
},
|
||||
twoStar: {
|
||||
count: ratings.filter((r) => r.rating.overall === 2).length,
|
||||
percentage:
|
||||
ratings.length > 0
|
||||
? (ratings.filter((r) => r.rating.overall === 2).length / ratings.length) * 100
|
||||
: 0,
|
||||
},
|
||||
oneStar: {
|
||||
count: ratings.filter((r) => r.rating.overall === 1).length,
|
||||
percentage:
|
||||
ratings.length > 0
|
||||
? (ratings.filter((r) => r.rating.overall === 1).length / ratings.length) * 100
|
||||
: 0,
|
||||
},
|
||||
},
|
||||
detailed: {
|
||||
// percentage is relative to how close it is to a 5/5 so 1 would be 20% and 5 would be 100%, count is null
|
||||
|
||||
quality: {
|
||||
label: "Qualität",
|
||||
bad: "schlecht",
|
||||
good: "Perfekt",
|
||||
percentage:
|
||||
(ratings.reduce((acc, r) => acc + r.rating.quality, 0) / (ratings.length * 5)) * 100,
|
||||
},
|
||||
priceQualityRatio: {
|
||||
label: "Preis/Leistung",
|
||||
bad: "schlecht",
|
||||
good: "Perfekt",
|
||||
percentage:
|
||||
(ratings.reduce((acc, r) => acc + r.rating.priceQualityRatio, 0) / (ratings.length * 5)) *
|
||||
100,
|
||||
},
|
||||
comfort: {
|
||||
label: "Tragekomfort",
|
||||
bad: "unbequem",
|
||||
good: "Perfekt",
|
||||
percentage:
|
||||
(ratings.reduce((acc, r) => acc + r.rating.comfort, 0) / (ratings.length * 5)) * 100,
|
||||
},
|
||||
overall: {
|
||||
label: "Gesamt",
|
||||
bad: "schlecht",
|
||||
good: "Perfekt",
|
||||
percentage:
|
||||
(ratings.reduce((acc, r) => acc + r.rating.overall, 0) / (ratings.length * 5)) * 100,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
return ratingDetails
|
||||
}
|
||||
setRating()
|
||||
</script>
|
||||
|
||||
<section class="ratings">
|
||||
<div>
|
||||
<h2>Bewertungen</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
{#if !ratingDetails}
|
||||
<Loader size="3" />
|
||||
{:else if ratingDetails.ratings.length === 0}
|
||||
<p>Es gibt noch keine Bewertungen für dieses Produkt.</p>
|
||||
{:else}
|
||||
<div>
|
||||
<h3>Überblick</h3>
|
||||
<ul>
|
||||
{#each Object.keys(ratingDetails.overview) as key, i}
|
||||
<li>
|
||||
<div class="star-lvl">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M21.9842 10.7232L17.7561 14.4132L19.0226 19.9069C19.0897 20.1941 19.0705 20.4947 18.9677 20.7711C18.8648 21.0475 18.6827 21.2874 18.4442 21.4609C18.2057 21.6344 17.9214 21.7337 17.6267 21.7464C17.3321 21.7592 17.0402 21.6848 16.7876 21.5326L11.9961 18.6263L7.21483 21.5326C6.96223 21.6848 6.67038 21.7592 6.37574 21.7464C6.0811 21.7337 5.79676 21.6344 5.55826 21.4609C5.31976 21.2874 5.13769 21.0475 5.03481 20.7711C4.93193 20.4947 4.9128 20.1941 4.97983 19.9069L6.24451 14.4188L2.01545 10.7232C1.79177 10.5303 1.63003 10.2756 1.5505 9.99113C1.47098 9.70666 1.47721 9.40504 1.56842 9.1241C1.65964 8.84315 1.83176 8.59539 2.06322 8.41188C2.29468 8.22838 2.57517 8.11729 2.86951 8.09256L8.44389 7.60974L10.6198 2.41974C10.7335 2.14742 10.9251 1.9148 11.1707 1.75117C11.4163 1.58755 11.7047 1.50024 11.9998 1.50024C12.2949 1.50024 12.5834 1.58755 12.829 1.75117C13.0745 1.9148 13.2662 2.14742 13.3798 2.41974L15.5623 7.60974L21.1348 8.09256C21.4292 8.11729 21.7097 8.22838 21.9411 8.41188C22.1726 8.59539 22.3447 8.84315 22.4359 9.1241C22.5271 9.40504 22.5334 9.70666 22.4538 9.99113C22.3743 10.2756 22.2126 10.5303 21.9889 10.7232H21.9842Z"
|
||||
fill="#2F4858"></path>
|
||||
</svg>
|
||||
<span>{i + 1}</span>
|
||||
</div>
|
||||
<div class="percentage-bar">
|
||||
<div
|
||||
style="width: {ratingDetails.overview[key].percentage == 100
|
||||
? 'calc(100% + 4px)'
|
||||
: ratingDetails.overview[key].percentage + '%'}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="count">{ratingDetails.overview[key].count}</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Ø Bewertungen</h3>
|
||||
<ul style="gap: 0.9rem;">
|
||||
<!--detailed props-->
|
||||
|
||||
{#each Object.keys(ratingDetails.detailed) as key}
|
||||
<li>
|
||||
<div class="label">
|
||||
{ratingDetails.detailed[key].label}
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="percentage-bar">
|
||||
<div
|
||||
style="width: {ratingDetails.detailed[key].percentage == 100
|
||||
? 'calc(100% + 4px)'
|
||||
: `${ratingDetails.detailed[key].percentage}%`};"
|
||||
></div>
|
||||
</div>
|
||||
<div class="labels">
|
||||
<span class="leftern">
|
||||
{ratingDetails.detailed[key].bad}
|
||||
</span>
|
||||
<span class="rightern">
|
||||
{ratingDetails.detailed[key].good}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
.ratings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
width: 100%;
|
||||
h2,
|
||||
h3 {
|
||||
color: var(--text-invers-100);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
|
||||
gap: 2.4rem;
|
||||
width: 100%;
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
flex-grow: 1;
|
||||
width: 0px;
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
width: 100%;
|
||||
li {
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
.star-lvl {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 2.4rem;
|
||||
font-weight: 700;
|
||||
font-family: Outfit-Bold;
|
||||
}
|
||||
.label {
|
||||
width: 120px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.count {
|
||||
width: 1.2rem;
|
||||
text-align: center;
|
||||
}
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
.percentage-bar {
|
||||
flex-grow: 1;
|
||||
border: 2px solid white;
|
||||
background-color: var(--bg-300);
|
||||
height: 0.6rem;
|
||||
position: relative;
|
||||
& > div {
|
||||
position: absolute;
|
||||
left: -2px;
|
||||
top: -2px;
|
||||
bottom: -2px;
|
||||
background-color: var(--primary-100);
|
||||
}
|
||||
}
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
flex-grow: 1;
|
||||
.percentage-bar {
|
||||
width: 100%;
|
||||
}
|
||||
.labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 1680px) {
|
||||
flex-direction: column;
|
||||
& > div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
<script lang="ts">
|
||||
// share this page on social media
|
||||
</script>
|
||||
@@ -0,0 +1,90 @@
|
||||
<script lang="ts">
|
||||
export let sizingChart: SizeChart
|
||||
</script>
|
||||
|
||||
{#if sizingChart && sizingChart.columns && sizingChart.columns.length}
|
||||
<div class="size-chart-wrapper">
|
||||
<h3>Messe dich selbst</h3>
|
||||
<p>
|
||||
{#if sizingChart.generalDescription}
|
||||
{@html sizingChart.generalDescription?.replace(
|
||||
"Die Maße werden von den Lieferanten zur Verfügung gestellt.",
|
||||
""
|
||||
)}
|
||||
{/if}
|
||||
</p>
|
||||
<div class="img-wrapper">
|
||||
<img
|
||||
src="{sizingChart.imageURL}"
|
||||
alt="größentabelle"
|
||||
/>
|
||||
<p>
|
||||
{#if sizingChart.imageDescription}
|
||||
{@html sizingChart.imageDescription
|
||||
.replace("Eine Brust", "A Brust")
|
||||
.replace("Eine Länge", "A Länge")
|
||||
.replace("Eine Taille", "A Taille")
|
||||
.replace("Eine Hüfte", "A Hüfte")
|
||||
.replace("Eine Schulter", "A Schulter")}{/if}
|
||||
</p>
|
||||
</div>
|
||||
<table class="size-chart">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Größe</th>
|
||||
{#each sizingChart.columns || [] as column}
|
||||
<th>{column.germanLabelTranslation || column.label}</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each sizingChart.availableSizes || [] as size, index}
|
||||
<tr>
|
||||
<td>{size}</td>
|
||||
{#each sizingChart.columns as column}
|
||||
<td>{column.sizes[index]} cm</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{:else}
|
||||
<p>Keine Größentabelle verfügbar</p>
|
||||
{/if}
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.size-chart-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
h3 {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
.img-wrapper {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
p {
|
||||
& > p {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1rem;
|
||||
@media (max-width: 768px) {
|
||||
margin-bottom: 0.6rem;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import { getCustomer } from "../../../functions/CommerceAPIs/tibiEndpoints/customer"
|
||||
import { login } from "../../../store"
|
||||
import Loader from "../Loader.svelte"
|
||||
import Addresses from "./account/Addresses.svelte"
|
||||
import IndentifyingData from "./account/IndentifyingData.svelte"
|
||||
import PersonalData from "./account/PersonalData.svelte"
|
||||
let reload = false
|
||||
</script>
|
||||
|
||||
<section class="wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<h2>Deine Daten</h2>
|
||||
{#key reload}
|
||||
{#await getCustomer($login.tokenData.tibiId)}
|
||||
<Loader
|
||||
size="4"
|
||||
type="bar"
|
||||
/>
|
||||
{:then customer}
|
||||
<div class="grid">
|
||||
<div class="item1">
|
||||
<PersonalData customer="{customer}" />
|
||||
</div>
|
||||
<div class="item2">
|
||||
<Addresses customer="{customer}" />
|
||||
</div>
|
||||
<div class="item3">
|
||||
<IndentifyingData
|
||||
customer="{customer}"
|
||||
on:update="{() => {
|
||||
reload = !reload
|
||||
}}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/await}{/key}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
.wrapper {
|
||||
flex-grow: 1;
|
||||
margin: 0px var(--horizontal-default-margin);
|
||||
margin-bottom: var(--vertical-default-margin);
|
||||
.inner-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
.grid {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
@media (min-width: 1100px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
column-gap: 2.4rem;
|
||||
row-gap: 2.4rem;
|
||||
.item1 {
|
||||
width: 100%;
|
||||
grid-column: 1;
|
||||
}
|
||||
.item2 {
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 3;
|
||||
grid-column: 2;
|
||||
@media (max-width: 1099px) {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.item3 {
|
||||
grid-column: 1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import { germanyStates } from "../../../../config"
|
||||
import Input from "../blocks/form/Input.svelte"
|
||||
import Select from "../blocks/form/Select.svelte"
|
||||
import { onChange } from "./helper"
|
||||
|
||||
export let address: BigCommerceAddress
|
||||
const stateOptions = germanyStates.map((state) => ({
|
||||
label: state.name,
|
||||
value: state.name,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{address.first_name}"
|
||||
placeholder="Vorname*"
|
||||
id="firstName"
|
||||
/>
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{address.last_name}"
|
||||
placeholder="Nachname*"
|
||||
id="lastName"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{address.address1}"
|
||||
placeholder="Straße*"
|
||||
id="address1"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{address.address2}"
|
||||
placeholder="Zusatz"
|
||||
id="address2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{address.postal_code}"
|
||||
placeholder="PLZ*"
|
||||
id="postalCode"
|
||||
/>
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{address.city}"
|
||||
placeholder="Stadt*"
|
||||
id="city"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<Select
|
||||
bind:value="{address.state_or_province}"
|
||||
options="{stateOptions}"
|
||||
placeholder="Bundesland*"
|
||||
selectedOption="{stateOptions[0]}"
|
||||
/>
|
||||
<Select
|
||||
bind:value="{address.country_code}"
|
||||
options="{[{ label: 'Deutschland', value: 'DE' }]}"
|
||||
selectedOption="{{ label: 'Deutschland', value: 'DE' }}"
|
||||
placeholder="Land*"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Input
|
||||
onChange="{onChange}"
|
||||
bind:value="{address.phone}"
|
||||
placeholder="Telefonnummer"
|
||||
id="phone"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 1.2rem;
|
||||
@media @mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user