zwischenstand
This commit is contained in:
@@ -1,109 +1,92 @@
|
||||
<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 { onMount } from "svelte"
|
||||
import Header from "./lib/components/header/Header.svelte"
|
||||
import Footer from "./lib/components/Footer.svelte"
|
||||
import Content from "./routes/Content.svelte"
|
||||
import Notifications from "./lib/components/widgets/Notifications.svelte"
|
||||
import { 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 HelpCenter from "./routes/HelpCenter.svelte"
|
||||
import RedirectToPublicProfile from "./routes/RedirectToPublicProfile.svelte"
|
||||
import { onMount } from "svelte"
|
||||
import ActionApproval from "./lib/components/widgets/ActionApproval.svelte"
|
||||
import KrassKraftChapter from "./routes/KrassKraftChapter.svelte"
|
||||
import { baseURL } from "./config"
|
||||
import { isMobile, location, openModal } from "./lib/store"
|
||||
|
||||
export let url = ""
|
||||
|
||||
if (url) {
|
||||
// ssr
|
||||
let l = url.split("?")
|
||||
$location ={
|
||||
path: l[0],
|
||||
search: l.length > 1 ? decodeURIComponent(`?${l[1]}`) : "",
|
||||
const [rawPath, rawQuery = ""] = url.split("?")
|
||||
const normalizedPath = rawPath.startsWith("/") ? rawPath : `/${rawPath}`
|
||||
const query = rawQuery ? decodeURIComponent(`?${rawQuery}`) : ""
|
||||
$location = {
|
||||
path: normalizedPath,
|
||||
search: query,
|
||||
hash: "",
|
||||
push: false,
|
||||
pop: false,
|
||||
url: `${baseURL}/${l[0]}?${l[1]}`,
|
||||
url: `${baseURL}${normalizedPath}${rawQuery ? `?${rawQuery}` : ""}`,
|
||||
}
|
||||
}
|
||||
|
||||
let oldPath: string
|
||||
location.subscribe((l) => {
|
||||
if (l.push && oldPath != l.path) window.scrollTo(0, 0)
|
||||
oldPath = l.path
|
||||
})
|
||||
|
||||
api(`module`, {
|
||||
method: "GET",
|
||||
}).then((res) => {
|
||||
$modules = res.data
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const updateModalState = () => {
|
||||
$openModal = document.getElementsByClassName("dialog-open").length > 0
|
||||
}
|
||||
const interval = setInterval(updateModalState, 100)
|
||||
return () => clearInterval(interval)
|
||||
})
|
||||
|
||||
let googleCookiesAllowed = false
|
||||
let googleCookieName = "googleAnalytics"
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("ccAccept", (e) => {
|
||||
// @ts-ignore
|
||||
if (e.detail.includes(googleCookieName)) googleCookiesAllowed = 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)
|
||||
}
|
||||
|
||||
let innerWidth = 0
|
||||
$: $isMobile = innerWidth < 1700
|
||||
$: $isMobile = innerWidth <= 1024
|
||||
|
||||
let googleCookiesAllowed = false
|
||||
const googleCookieName = "googleAnalytics"
|
||||
|
||||
const syncModalState = () => {
|
||||
if (typeof document === "undefined") return
|
||||
$openModal = document.querySelectorAll(".dialog-open").length > 0
|
||||
}
|
||||
|
||||
let previousPath = ""
|
||||
const unsubscribe = location.subscribe((value) => {
|
||||
if (typeof window !== "undefined" && value.push && previousPath !== value.path) {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" })
|
||||
}
|
||||
previousPath = value.path
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(syncModalState, 150)
|
||||
let consentHandler: EventListener | null = null
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const evaluateCookieConsent = () => {
|
||||
googleCookiesAllowed = document.cookie
|
||||
.split(";")
|
||||
.map((cookie) => cookie.trim())
|
||||
.some((cookie) => cookie.startsWith("ccTags") && cookie.includes(googleCookieName))
|
||||
}
|
||||
|
||||
const onConsent = (event: CustomEvent<string[]>) => {
|
||||
const values = event.detail || []
|
||||
if (Array.isArray(values) && values.includes(googleCookieName)) {
|
||||
googleCookiesAllowed = true
|
||||
}
|
||||
}
|
||||
|
||||
evaluateCookieConsent()
|
||||
consentHandler = onConsent as EventListener
|
||||
window.addEventListener("ccAccept", consentHandler)
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
if (typeof window !== "undefined" && consentHandler) {
|
||||
window.removeEventListener("ccAccept", consentHandler)
|
||||
}
|
||||
unsubscribe()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth={innerWidth} />
|
||||
|
||||
<svelte:head>
|
||||
{#if googleCookiesAllowed}
|
||||
<!-- 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)
|
||||
@@ -114,33 +97,21 @@
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<div>
|
||||
<SidebarOverlay />
|
||||
<div class="app-shell">
|
||||
<Header />
|
||||
<main>
|
||||
<Header />
|
||||
|
||||
<Content location={$location} />
|
||||
|
||||
<div class="crossGap"></div>
|
||||
<Footer />
|
||||
<Content location={$location} />
|
||||
</main>
|
||||
|
||||
<SSRSkip />
|
||||
<Notifications />
|
||||
<Footer />
|
||||
</div>
|
||||
<ActionApproval />
|
||||
<DateModal />
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
<Notifications />
|
||||
<DateModal />
|
||||
<SSRSkip />
|
||||
|
||||
<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";
|
||||
|
||||
@@ -148,25 +119,33 @@
|
||||
body {
|
||||
font-family: "Outfit", sans-serif;
|
||||
background-color: var(--bg-100);
|
||||
min-height: 100vh;
|
||||
|
||||
button {
|
||||
font-family: "Outfit", sans-serif;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
@media @mobile {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media @min-tablet {
|
||||
font-size: 20px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
.app-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--neutral-white);
|
||||
min-height: 100vh;
|
||||
background-color: var(--neutral-white);
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.crossGap {
|
||||
flex-grow: 1;
|
||||
}
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,305 +1,90 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
import { apiBaseOverride } from "./lib/store"
|
||||
import { apiBaseURL } from "./config"
|
||||
|
||||
interface RequestProgressState {
|
||||
downLoaded: number
|
||||
downTotal: number
|
||||
upLoaded: number
|
||||
upTotal: number
|
||||
}
|
||||
const cache = new Map<string, { expire: number; data: unknown }>()
|
||||
|
||||
const inProgess = new Map<object, RequestProgressState>()
|
||||
const cacheKey = (value: unknown) => JSON.stringify(value, Object.keys(value as object).sort())
|
||||
|
||||
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)
|
||||
}
|
||||
const buildQuery = (options: ApiOptions) => {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
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.filter) params.set("filter", JSON.stringify(options.filter))
|
||||
if (options.sort) params.set("sort", options.sort)
|
||||
if (typeof options.limit === "number") params.set("limit", String(options.limit))
|
||||
if (typeof options.offset === "number") params.set("offset", String(options.offset))
|
||||
if (options.lookup) params.set("lookup", options.lookup)
|
||||
if (options.projection) params.set("projection", options.projection)
|
||||
|
||||
if (options.params) {
|
||||
Object.keys(options.params).forEach((p) => {
|
||||
xhrParams[p] = encodeURIComponent(options.params[p])
|
||||
Object.entries(options.params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
params.set(key, value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let method = options.method || "GET"
|
||||
let url = (endpoint.startsWith("/") ? "" : apiClientBaseURL) + endpoint
|
||||
let queryString = new URLSearchParams(xhrParams).toString()
|
||||
if (queryString) url += `?${queryString}`
|
||||
return params
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
const toAbsoluteEndpoint = (endpoint: string) => {
|
||||
const base = get(apiBaseOverride) ?? apiBaseURL
|
||||
if (endpoint.startsWith("/")) {
|
||||
return `${base}${endpoint.slice(1)}`
|
||||
}
|
||||
return `${base}${endpoint}`
|
||||
}
|
||||
|
||||
xhr.open(method, url, true)
|
||||
const handleResponse = async <T>(response: Response): Promise<ApiResult<T>> => {
|
||||
const text = await response.text()
|
||||
const data = text ? JSON.parse(text) : null
|
||||
|
||||
Object.keys(options.headers).forEach((key) => {
|
||||
xhr.setRequestHeader(key, options.headers[key])
|
||||
})
|
||||
if (!response.ok) {
|
||||
const error = new Error(response.statusText)
|
||||
Object.assign(error, { status: response.status, data })
|
||||
throw error
|
||||
}
|
||||
|
||||
xhr.upload.onprogress = (event) => {
|
||||
if (showProgress) {
|
||||
updateProgressMap(ref, true, "up", event.loaded, event.total)
|
||||
if (options.onUploadProgress) {
|
||||
options.onUploadProgress(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
const countHeader = response.headers.get("x-results-count")
|
||||
const count = countHeader ? parseInt(countHeader, 10) : Array.isArray(data) ? data.length : 0
|
||||
|
||||
xhr.onprogress = (event) => {
|
||||
if (showProgress) {
|
||||
updateProgressMap(ref, true, "down", event.loaded, event.total)
|
||||
if (options.onDownloadProgress) {
|
||||
options.onDownloadProgress(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
data,
|
||||
count,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
export async function api<T>(endpoint: string, options: ApiOptions = {}, body?: unknown): Promise<ApiResult<T>> {
|
||||
const method = (options.method || "GET").toUpperCase()
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "application/json",
|
||||
...options.headers,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
const url = new URL(toAbsoluteEndpoint(endpoint), "http://dummy.base")
|
||||
const query = buildQuery(options)
|
||||
query.forEach((value, key) => {
|
||||
url.searchParams.set(key, value)
|
||||
})
|
||||
}
|
||||
|
||||
// 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)
|
||||
const init: RequestInit = {
|
||||
method,
|
||||
headers,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if (method === "POST" || method === "PUT" || method === "PATCH") {
|
||||
headers["Content-Type"] = "application/json"
|
||||
init.body = JSON.stringify(body ?? {})
|
||||
}
|
||||
|
||||
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
|
||||
if (method === "DELETE") {
|
||||
init.body = body ? JSON.stringify(body) : undefined
|
||||
if (init.body) headers["Content-Type"] = "application/json"
|
||||
}
|
||||
} = {}
|
||||
|
||||
const response = await fetch(url.toString().replace("http://dummy.base", ""), init)
|
||||
return handleResponse<T>(response)
|
||||
}
|
||||
|
||||
type EntryTypeSwitch<T> = T extends "medialib"
|
||||
? MedialibEntry
|
||||
@@ -307,7 +92,7 @@ type EntryTypeSwitch<T> = T extends "medialib"
|
||||
? ContentEntry
|
||||
: T extends "navigation"
|
||||
? NavigationEntry
|
||||
: never
|
||||
: any
|
||||
|
||||
export async function getDBEntries<T extends CollectionName>(
|
||||
collectionName: T,
|
||||
@@ -317,8 +102,8 @@ export async function getDBEntries<T extends CollectionName>(
|
||||
offset?: number,
|
||||
projection?: string
|
||||
): Promise<EntryTypeSwitch<T>[]> {
|
||||
const c = await api<EntryTypeSwitch<T>[]>(collectionName, { filter, sort, limit, offset, projection })
|
||||
return c.data
|
||||
const response = await api<EntryTypeSwitch<T>[]>(collectionName, { filter, sort, limit, offset, projection })
|
||||
return (response.data as EntryTypeSwitch<T>[]) ?? []
|
||||
}
|
||||
|
||||
export async function getCachedEntries<T extends CollectionName>(
|
||||
@@ -329,35 +114,39 @@ export async function getCachedEntries<T extends CollectionName>(
|
||||
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 key = cacheKey({ collectionName, filter, sort, limit, offset, projection })
|
||||
const cached = cache.get(key)
|
||||
if (cached && cached.expire > Date.now()) {
|
||||
return cached.data as EntryTypeSwitch<T>[]
|
||||
}
|
||||
const entries = await getDBEntries<T>(collectionName, filter, sort, limit, offset, projection)
|
||||
const inOneHour = Date.now() + 1000 * 60 * 60
|
||||
cache[filterStr] = { expire: inOneHour, data: entries }
|
||||
|
||||
const entries = await getDBEntries(collectionName, filter, sort, limit, offset, projection)
|
||||
cache.set(key, { data: entries, expire: Date.now() + 1000 * 60 * 5 })
|
||||
return entries
|
||||
}
|
||||
|
||||
export async function getDBEntry<T extends CollectionName>(collectionName: T, filter: { [key: string]: any }) {
|
||||
return (await getDBEntries<T>(collectionName, filter, "_id"))?.[0]
|
||||
const entries = await getDBEntries(collectionName, filter, "_id", 1)
|
||||
return entries?.[0]
|
||||
}
|
||||
|
||||
export async function getCachedEntry<T extends CollectionName>(collectionName: T, filter: { [key: string]: any }) {
|
||||
return (await getCachedEntries<T>(collectionName, filter, "_id"))?.[0]
|
||||
const entries = await getCachedEntries(collectionName, filter, "_id", 1)
|
||||
return entries?.[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 putDBEntry<T extends CollectionName>(
|
||||
collectionName: T,
|
||||
entry: Partial<EntryTypeSwitch<T>> & { id: string }
|
||||
) {
|
||||
const { id, ...payload } = entry
|
||||
return api<EntryTypeSwitch<T>>(`${collectionName}/${id}`, { method: "PUT" }, payload)
|
||||
}
|
||||
|
||||
export async function deleteDBEntry<T extends CollectionName>(collectionName: T, id: string) {
|
||||
return api<EntryTypeSwitch<T>>(collectionName + "/" + id, {
|
||||
method: "DELETE",
|
||||
useJwt: true,
|
||||
})
|
||||
return api(`${collectionName}/${id}`, { method: "DELETE" })
|
||||
}
|
||||
|
||||
@@ -1,249 +1,19 @@
|
||||
import configClient from "../../api/hooks/config-client"
|
||||
import * as sentry from "./sentry"
|
||||
const protocol = typeof window !== "undefined" ? window.location.protocol : "https:"
|
||||
export const baseDomain = typeof window !== "undefined" ? window.location.hostname : "kontextwerk.de"
|
||||
export const baseURL = `${protocol}//${baseDomain}`
|
||||
export const apiBaseURL = "/api/"
|
||||
|
||||
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)
|
||||
export const websiteName = "Kontextwerk"
|
||||
export const companyName = "Kontextwerk GmbH"
|
||||
export const email = "hello@kontextwerk.de"
|
||||
export const streetAddress = "Gertrudenstraße 3"
|
||||
export const localityAddress = "Hamburg"
|
||||
export const regionAddress = "Hamburg"
|
||||
export const zipCode = "20095"
|
||||
export const countryAddress = "DE"
|
||||
|
||||
// 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",
|
||||
instagram: "https://www.instagram.com/kontextwerk",
|
||||
linkedin: "https://www.linkedin.com/company/kontextwerk",
|
||||
youtube: "https://www.youtube.com/@kontextwerk",
|
||||
}
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import type { Action } from "svelte/action"
|
||||
import { overlays } from "./store"
|
||||
|
||||
export const spaLink: Action<HTMLAnchorElement> = (node) => {
|
||||
const onClick = (event: MouseEvent) => {
|
||||
if (node.dataset.spaPrevent && node.dataset.spaPrevent !== "false") {
|
||||
return
|
||||
}
|
||||
|
||||
const anchor = event.currentTarget as HTMLAnchorElement
|
||||
if ((anchor.target === "" || anchor.target === "_self") && anchor.href.startsWith(window.location.origin)) {
|
||||
const isSelfTarget = anchor.target === "" || anchor.target === "_self"
|
||||
|
||||
if (isSelfTarget && anchor.href.startsWith(window.location.origin)) {
|
||||
event.preventDefault()
|
||||
const nextUrl = anchor.pathname + anchor.search + anchor.hash
|
||||
const currentUrl = window.location.pathname + window.location.search + window.location.hash
|
||||
const newUrl = anchor.pathname + anchor.search + anchor.hash
|
||||
if (currentUrl === newUrl) {
|
||||
window.scrollTo(0, 0)
|
||||
|
||||
if (nextUrl === currentUrl) {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" })
|
||||
return
|
||||
}
|
||||
spaNavigate(anchor.pathname + anchor.search + anchor.hash)
|
||||
|
||||
spaNavigate(nextUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,26 +33,15 @@ export const spaLink: Action<HTMLAnchorElement> = (node) => {
|
||||
}
|
||||
|
||||
export const spaNavigate = (to: string, options?: { replace?: boolean }) => {
|
||||
overlays.update((current) => [])
|
||||
window.scrollTo(0, 0)
|
||||
//scroll to top of page
|
||||
setTimeout(
|
||||
() => {
|
||||
window.scrollTo(0, 0)
|
||||
},
|
||||
|
||||
100
|
||||
)
|
||||
|
||||
if (options?.replace) {
|
||||
window.history.replaceState(null, "", to)
|
||||
} else {
|
||||
window.history.pushState(null, "", to)
|
||||
}
|
||||
|
||||
window.scrollTo({ top: 0, behavior: "smooth" })
|
||||
}
|
||||
|
||||
export const spaBack = () => {
|
||||
window.history.back()
|
||||
}
|
||||
|
||||
// TODO: spaLinks container for containing {@html ...}
|
||||
|
||||
@@ -1,323 +1,219 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
import { getDBEntries } from "../../api"
|
||||
import { socialIcons } from "../../config"
|
||||
import { socialIcons, companyName, email, streetAddress, zipCode, localityAddress } from "../../config"
|
||||
import { spaLink } from "../actions"
|
||||
import { login, navigationCache } from "../store"
|
||||
import CrinkledSection from "./CrinkledSection.svelte"
|
||||
import Input from "./pagebuilder/blocks/form/Input.svelte"
|
||||
import { submitNewsletter } from "../functions/CommerceAPIs/tibiEndpoints/actions"
|
||||
import { onChange } from "./pagebuilder/profile/helper"
|
||||
enum NavigationType {
|
||||
MainNavigation = 0,
|
||||
ServiceNavigation = 2,
|
||||
LegalNavigation = 1,
|
||||
}
|
||||
let emailIsSubscribed = false
|
||||
let navigationEntries: NavigationEntry[] = []
|
||||
|
||||
function elementsToCache(elements: NavigationElement[]) {
|
||||
elements.forEach((el) => {
|
||||
if (!el.external) {
|
||||
if (!$navigationCache[el.page]) $navigationCache[el.page] = el
|
||||
if (el.elements?.length > 0) elementsToCache(el.elements)
|
||||
}
|
||||
})
|
||||
const NAVIGATION_TYPE = {
|
||||
Main: 0,
|
||||
Service: 1,
|
||||
Legal: 2,
|
||||
} as const
|
||||
|
||||
let legalLinks: NavigationElement[] = []
|
||||
let serviceLinks: NavigationElement[] = []
|
||||
let loadingNavigation = true
|
||||
|
||||
const resolveHref = (link: NavigationElement) => {
|
||||
const base = link.page || "/"
|
||||
const hash = link.hash ? (link.hash.startsWith("#") ? link.hash : `#${link.hash}`) : ""
|
||||
return `${base}${hash}`
|
||||
}
|
||||
|
||||
getDBEntries("navigation").then((navs) => {
|
||||
navigationEntries = navs.sort((a, b) => a.type - b.type)
|
||||
navigationEntries.forEach((nav) => elementsToCache(nav.elements))
|
||||
onMount(async () => {
|
||||
try {
|
||||
const entries = await getDBEntries("navigation")
|
||||
legalLinks = entries.find((entry) => Number(entry.type) === NAVIGATION_TYPE.Legal)?.elements ?? []
|
||||
serviceLinks = entries.find((entry) => Number(entry.type) === NAVIGATION_TYPE.Service)?.elements ?? []
|
||||
} catch (error) {
|
||||
console.error("Unable to load footer navigation", error)
|
||||
} finally {
|
||||
loadingNavigation = false
|
||||
}
|
||||
})
|
||||
export let className = ""
|
||||
let sections: {
|
||||
title: string
|
||||
links: NavigationElement[]
|
||||
}[] = []
|
||||
$: if (navigationEntries.length) sections = []
|
||||
|
||||
let email = ""
|
||||
let dataProt = false
|
||||
const currentYear = new Date().getFullYear()
|
||||
</script>
|
||||
|
||||
<CrinkledSection>
|
||||
<footer class={'footer ' + className}>
|
||||
<section id="content-section">
|
||||
<section id="content-link-section">
|
||||
{#each sections as section}
|
||||
<section class="content">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<small class="service"><em>{section?.title}</em></small>
|
||||
<nav class="sub-points">
|
||||
<ul>
|
||||
{#each section?.links || [] as link}
|
||||
<li>
|
||||
<a
|
||||
class="footer-nav-point"
|
||||
use:spaLink
|
||||
href={link.page}><small>{link.name}</small></a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/each}
|
||||
</section>
|
||||
<section id="newsletter-section">
|
||||
<h4>Newsletter</h4>
|
||||
<form
|
||||
on:submit|preventDefault|stopPropagation={() => {
|
||||
submitNewsletter(email, dataProt).then(() => {
|
||||
emailIsSubscribed = true
|
||||
})
|
||||
}}
|
||||
>
|
||||
{#if emailIsSubscribed}
|
||||
<p>Du hast dich zum Newsletter angemeldet!</p>
|
||||
{:else}
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="E-Mail"
|
||||
bind:value={email}
|
||||
id="email"
|
||||
onChange={onChange}
|
||||
/>
|
||||
<div class="data-protection">
|
||||
<Input
|
||||
type="checkbox"
|
||||
bind:value={dataProt}
|
||||
id="dataprot"
|
||||
onChange={onChange}
|
||||
/>
|
||||
<p>
|
||||
<a
|
||||
href="/datenschutz"
|
||||
use:spaLink>Datenschutz</a
|
||||
> zum Newsletterversand akzeptieren
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="btn cta primary"
|
||||
type="submit"
|
||||
>
|
||||
Anmelden</button
|
||||
>
|
||||
{/if}
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
||||
<section id="icons-section">
|
||||
<div class="line">
|
||||
<div class="line-1"></div>
|
||||
<img
|
||||
alt="Symbol"
|
||||
class="symbol"
|
||||
src="../../../logo/logoShort.svg"
|
||||
/>
|
||||
<div class="line-2"></div>
|
||||
</div>
|
||||
<ul class="social">
|
||||
{#each Object.keys(socialIcons) as icon}
|
||||
<footer class="site-footer">
|
||||
<div class="footer-inner">
|
||||
<section class="footer-column">
|
||||
<h2>{companyName}</h2>
|
||||
<p>{streetAddress}, {zipCode} {localityAddress}</p>
|
||||
<a
|
||||
class="footer-link"
|
||||
href={`mailto:${email}`}
|
||||
>
|
||||
{email}
|
||||
</a>
|
||||
<ul class="footer-social">
|
||||
{#each Object.entries(socialIcons) as [name, url]}
|
||||
<li>
|
||||
<a
|
||||
href={socialIcons[icon]}
|
||||
use:spaLink
|
||||
href={url}
|
||||
target="_blank"
|
||||
aria-label={icon}
|
||||
>
|
||||
<figure class="footer-icon">
|
||||
<img
|
||||
alt={icon}
|
||||
src="../../../media/{icon}.svg"
|
||||
/>
|
||||
</figure></a
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`Öffne ${name} in einem neuen Tab`}
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
<section id="legal-section">
|
||||
<div class="wrapper">
|
||||
<small class="">© 2024 | BKDF - Bin Krass Du Fass | Alle Rechte vorbehalten.</small>
|
||||
<nav class="nav-points">
|
||||
<ul>
|
||||
{#each navigationEntries.length ? navigationEntries[NavigationType.LegalNavigation].elements : [] as link}
|
||||
<li>
|
||||
<section class="footer-column">
|
||||
<h3>Service</h3>
|
||||
{#if loadingNavigation}
|
||||
<span class="footer-placeholder">Links werden geladen …</span>
|
||||
{:else if serviceLinks.length}
|
||||
<ul>
|
||||
{#each serviceLinks as link (link.name)}
|
||||
<li>
|
||||
{#if link.external && link.externalUrl}
|
||||
<a
|
||||
class="footer-nav-point-bottom"
|
||||
use:spaLink
|
||||
href={link.page}><small>{link.name}</small></a
|
||||
href={link.externalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{link.name}
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href={resolveHref(link)}
|
||||
use:spaLink
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<span class="footer-placeholder">Aktuell keine Service-Links</span>
|
||||
{/if}
|
||||
</section>
|
||||
</footer>
|
||||
</CrinkledSection>
|
||||
<section class="footer-column">
|
||||
<h3>Rechtliches</h3>
|
||||
{#if loadingNavigation}
|
||||
<span class="footer-placeholder">Links werden geladen …</span>
|
||||
{:else if legalLinks.length}
|
||||
<ul>
|
||||
{#each legalLinks as link (link.name)}
|
||||
<li>
|
||||
{#if link.external && link.externalUrl}
|
||||
<a
|
||||
href={link.externalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href={resolveHref(link)}
|
||||
use:spaLink
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<span class="footer-placeholder">Aktuell keine rechtlichen Hinweise</span>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
<div class="footer-meta">© {currentYear} {companyName}. Alle Rechte vorbehalten.</div>
|
||||
</footer>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../lib/assets/css/variables.less";
|
||||
.footer {
|
||||
&,
|
||||
& * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
background: var(--neutral-white);
|
||||
|
||||
.site-footer {
|
||||
background-color: var(--bg-200, #0d0d0d);
|
||||
color: var(--neutral-white);
|
||||
padding: 3rem 1.5rem 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 2rem;
|
||||
width: 100%;
|
||||
gap: 1.5rem;
|
||||
#content-section {
|
||||
padding: 0px var(--horizontal-default-margin);
|
||||
max-width: var(--small-max-width);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.5rem;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
#content-link-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.5rem;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.125rem;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
width: 8rem;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
@media @mobile {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
#content-link-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
max-width: var(--body-maxwidth);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.footer-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
|
||||
h2,
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
#newsletter-section {
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
min-width: 300px;
|
||||
form {
|
||||
button {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
#icons-section {
|
||||
padding: 0px var(--horizontal-default-margin);
|
||||
max-width: var(--small-max-width);
|
||||
width: 100%;
|
||||
margin: 0 0 0 -0.0625rem;
|
||||
display: flex;
|
||||
gap: 0.625rem;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.payments,
|
||||
.social {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
figure {
|
||||
height: 1.2rem;
|
||||
img {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
.line {
|
||||
padding: 0rem 1.5rem 0rem 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.5rem;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.line-1,
|
||||
.line-2 {
|
||||
border-style: solid;
|
||||
border-color: var(--text-invers-100);
|
||||
border-width: 0.0625rem 0 0 0;
|
||||
flex: 1;
|
||||
height: 0rem;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
@media @mobile {
|
||||
flex-direction: column-reverse;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
.line {
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
#legal-section {
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
background: var(--bg-100);
|
||||
gap: 1.2rem;
|
||||
.wrapper {
|
||||
max-width: var(--small-max-width);
|
||||
}
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
a,
|
||||
small {
|
||||
color: var(--text-100);
|
||||
}
|
||||
.footer-link,
|
||||
.footer-column a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
gap: 1.2rem;
|
||||
padding: 1.5rem var(--horizontal-default-margin);
|
||||
@media @mobile {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-points {
|
||||
ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6rem;
|
||||
align-items: center;
|
||||
a {
|
||||
font-weight: 400;
|
||||
font-family: Outfit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.footer-social {
|
||||
flex-direction: row;
|
||||
gap: 0.75rem;
|
||||
|
||||
a {
|
||||
text-transform: capitalize;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-placeholder {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.footer-meta {
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.site-footer {
|
||||
padding: 2.4rem 1.2rem 1.2rem;
|
||||
}
|
||||
|
||||
.footer-social {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import Icon from "./widgets/Icon.svelte"
|
||||
import Notifications from "./widgets/Notifications.svelte"
|
||||
import { isMobile } from "../store"
|
||||
import { changeStateOfSite } from "./header/Desktop.svelte"
|
||||
import { enableScrolling, stopScrolling } from "../functions/utils"
|
||||
|
||||
export let show: boolean = false,
|
||||
size: string = "md",
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
let dialog: HTMLDialogElement,
|
||||
dispatch = createEventDispatcher()
|
||||
const scrollPosition = { top: 0, left: 0 }
|
||||
|
||||
const onCancel = (e: any) => {
|
||||
show = false
|
||||
@@ -30,19 +31,19 @@
|
||||
$: if (dialog)
|
||||
if (show) {
|
||||
dialog.showModal()
|
||||
changeStateOfSite(true)
|
||||
dialog.classList.add("dialog-open")
|
||||
stopScrolling(scrollPosition)
|
||||
} else if (dialog.classList.contains("dialog-open")) {
|
||||
changeStateOfSite(false)
|
||||
dialog.classList.remove("dialog-open")
|
||||
dialog.close()
|
||||
enableScrolling(scrollPosition)
|
||||
dispatch("close")
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
return () => {
|
||||
dialog.classList.remove("dialog-open")
|
||||
changeStateOfSite(false)
|
||||
enableScrolling(scrollPosition)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -53,7 +54,6 @@
|
||||
bind:this={dialog}
|
||||
on:cancel={onCancel}
|
||||
on:click|stopPropagation={onDialogClick}
|
||||
on:keypress
|
||||
data-cy="modal"
|
||||
>
|
||||
{#if $$slots.title}
|
||||
@@ -85,7 +85,7 @@
|
||||
>
|
||||
<Icon
|
||||
path={mdiCloseCircleOutline}
|
||||
size={$isMobile ? 24 : 32}px"
|
||||
size={$isMobile ? "24px" : "32px"}
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
|
||||
@@ -1,428 +0,0 @@
|
||||
<script
|
||||
lang="ts"
|
||||
context="module"
|
||||
>
|
||||
import "simplebar"
|
||||
import "simplebar/dist/simplebar.css"
|
||||
import ResizeObserver from "resize-observer-polyfill"
|
||||
if (typeof window !== "undefined") window.ResizeObserver = ResizeObserver
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { mdiCloseCircleOutline } from "@mdi/js"
|
||||
import { isMobile, overlays } from "../store"
|
||||
import Icon from "./widgets/Icon.svelte"
|
||||
import { changeStateOfSite } from "./header/Desktop.svelte"
|
||||
$: changeStateOfSite(!!$overlays.length)
|
||||
|
||||
function checkIfNoOverlayIsActive(overlays: Array<Overlay>) {
|
||||
if (!overlays.length) return false
|
||||
return overlays.every((overlay) => !overlay?.active)
|
||||
}
|
||||
$: if (checkIfNoOverlayIsActive($overlays)) {
|
||||
if ($overlays.some((o) => o.hideTillDispatch)) {
|
||||
// make the one before it active
|
||||
const hidden = $overlays.findIndex((o) => o.hideTillDispatch)
|
||||
if (hidden > 0) {
|
||||
$overlays[hidden - 1].active = true
|
||||
} else {
|
||||
$overlays[1].active = true
|
||||
}
|
||||
} else {
|
||||
const lastIndex = $overlays.length - 1
|
||||
$overlays[lastIndex].active = true
|
||||
}
|
||||
}
|
||||
function closeModal(i: number) {
|
||||
const removedElements = $overlays.splice(i, $overlays.length - i)
|
||||
const hasActive = removedElements.some((element) => element.active === true)
|
||||
if (hasActive && $overlays.length > 0) {
|
||||
$overlays[$overlays.length - 1].active = true
|
||||
}
|
||||
$overlays = $overlays
|
||||
dispatched = dispatched.filter((dispatch, idx) => idx !== i)
|
||||
}
|
||||
let dispatched: boolean[] = []
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<div
|
||||
class:visible={$overlays.length}
|
||||
class="sidebarOverlay"
|
||||
id="overlay"
|
||||
role="dialog"
|
||||
aria-labelledby="overlayTitle"
|
||||
aria-label="Overlay"
|
||||
aria-modal="true"
|
||||
on:click={() => {
|
||||
closeModal(0)
|
||||
}}
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
closeModal(0)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<section class="dark-side"></section>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<ul
|
||||
class="content-side"
|
||||
on:click|stopPropagation
|
||||
>
|
||||
{#each $overlays as overlay, i}
|
||||
<li
|
||||
class="content-block"
|
||||
class:active={overlay.active}
|
||||
class:dark={(i + 1) % 2 == 0}
|
||||
class:hideTillDispatch={overlay.hideTillDispatch}
|
||||
class:hideIt={!dispatched[i]}
|
||||
>
|
||||
<header
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click={() => {
|
||||
$overlays = $overlays.map((o, index) => {
|
||||
o.active = index === i
|
||||
return o
|
||||
})
|
||||
}}
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
$overlays = $overlays.map((o, index) => {
|
||||
o.active = index === i
|
||||
return o
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="upper-bar"
|
||||
class:noBg={i === $overlays.length - 1 ||
|
||||
$overlays.filter((o) => o.hideTillDispatch).length > dispatched.filter(Boolean).length}
|
||||
>
|
||||
<div class="crinkle desktop-crinkle">
|
||||
<img
|
||||
src="/media/ModalCrinkle{(i + 1) % 2 == 0 ? 'Dark' : ''}.svg"
|
||||
alt="crinkle"
|
||||
/>
|
||||
</div>
|
||||
<div class="crinkle mobile-crinkle">
|
||||
<img
|
||||
src="/media/ModalCrinkle{(i + 1) % 2 == 0 ? 'Dark' : ''}Mobile.svg"
|
||||
alt="crinkle"
|
||||
/>
|
||||
</div>
|
||||
{#if typeof overlay.title === "string"}
|
||||
<h2 id="overlayTitle">
|
||||
{overlay.title}
|
||||
</h2>
|
||||
{:else}
|
||||
<svelte:component this={overlay.title} />
|
||||
{/if}
|
||||
<div class="close">
|
||||
<button
|
||||
aria-label="Close"
|
||||
on:click={() => {
|
||||
closeModal(i)
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
path={mdiCloseCircleOutline}
|
||||
size={$isMobile ? '24px' : '32px'}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="lower-bar"></div>
|
||||
</header>
|
||||
<div
|
||||
class="scroll-container"
|
||||
class:active={overlay.active}
|
||||
data-simplebar
|
||||
>
|
||||
<div class="content-listing">
|
||||
{#key overlay.reload}
|
||||
<svelte:component
|
||||
this={overlay.content}
|
||||
on:showOverlay={(e) => {
|
||||
dispatched[i] = true
|
||||
}}
|
||||
on:removeOverlay={() => {
|
||||
closeModal(i)
|
||||
}}
|
||||
darkBg={(i + 1) % 2 == 0}
|
||||
{...overlay.properties}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
@import "../assets/css/variables.less";
|
||||
:global .sidebarOverlay {
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
right: 0;
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
overflow: hidden;
|
||||
z-index: 9999;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
bottom: -200%;
|
||||
top: 200%;
|
||||
&.visible {
|
||||
bottom: 0px;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
@media @desktop {
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
right: -100vw;
|
||||
left: 100vw;
|
||||
&.visible {
|
||||
right: 0px;
|
||||
}
|
||||
}
|
||||
.dark-side {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
.content-side {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-direction: column-reverse;
|
||||
@media @desktop {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
height: 100%;
|
||||
@media @mobile {
|
||||
height: 70%;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
&.hideTillDispatch {
|
||||
&.hideIt {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
header {
|
||||
.upper-bar {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 88px;
|
||||
background-color: var(--bg-100);
|
||||
|
||||
&.noBg {
|
||||
background-color: transparent;
|
||||
}
|
||||
.crinkle {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
margin-right: -1px;
|
||||
}
|
||||
|
||||
.mobile-crinkle {
|
||||
display: none;
|
||||
}
|
||||
@media @mobile {
|
||||
height: 70px;
|
||||
.desktop-crinkle {
|
||||
display: none;
|
||||
}
|
||||
.mobile-crinkle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
background-color: var(--neutral-white);
|
||||
flex-grow: 1;
|
||||
color: var(--text-invers-100);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
font-weight: 700;
|
||||
}
|
||||
.close {
|
||||
background-color: var(--neutral-white);
|
||||
padding-right: 27px;
|
||||
color: var(--text-invers-100);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
button {
|
||||
color: var(--text-invers-100);
|
||||
transform: translate(-50%, 25%);
|
||||
}
|
||||
}
|
||||
}
|
||||
.lower-bar {
|
||||
height: 1.2rem;
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
@media @mobile {
|
||||
height: unset;
|
||||
max-height: calc(100vh - 200px);
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
&:not(.active) {
|
||||
@media @mobile {
|
||||
height: unset;
|
||||
}
|
||||
}
|
||||
.simplebar-content {
|
||||
height: 100%;
|
||||
@media @mobile {
|
||||
padding-top: 12px !important;
|
||||
}
|
||||
}
|
||||
.simplebar-track {
|
||||
@media @mobile {
|
||||
margin-top: 12px !important;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
// if only one child, put it to 100%
|
||||
&:only-child {
|
||||
height: 100%;
|
||||
}
|
||||
@media @desktop {
|
||||
width: 50vw;
|
||||
border-left: 1px solid var(--bg-invers-100);
|
||||
}
|
||||
@media @large_desktop {
|
||||
width: max(30vw, 628px);
|
||||
}
|
||||
.scroll-container {
|
||||
height: calc(100% - 88px - 1.2rem);
|
||||
max-height: 100%;
|
||||
padding-right: 1.6rem;
|
||||
transition: height 0.3s;
|
||||
flex-grow: 1;
|
||||
background-color: var(--neutral-white);
|
||||
|
||||
padding-top: 2.4rem;
|
||||
@media @mobile {
|
||||
&:not(.active) {
|
||||
height: 0px;
|
||||
max-height: 0px;
|
||||
padding-top: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-track {
|
||||
background-color: rgba(13, 12, 12, 0.25);
|
||||
width: 7px;
|
||||
overflow: visible;
|
||||
margin-left: 5px;
|
||||
margin-right: 1.6rem;
|
||||
margin-top: 2.4rem;
|
||||
@media @mobile {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
.simplebar-scrollbar {
|
||||
transition-duration: 0ms !important;
|
||||
cursor: pointer;
|
||||
&::before {
|
||||
background-color: var(--bg-100);
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
opacity: 1;
|
||||
border-radius: 0;
|
||||
width: 11px;
|
||||
height: calc(100% + 2px);
|
||||
transition-delay: 0s;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content-listing {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
background-color: var(--neutral-white);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
header {
|
||||
.upper-bar {
|
||||
background-color: var(--neutral-white);
|
||||
&.noBg {
|
||||
background-color: transparent;
|
||||
}
|
||||
h2 {
|
||||
background-color: var(--bg-100);
|
||||
color: var(--text-100);
|
||||
}
|
||||
.close {
|
||||
background-color: var(--bg-100);
|
||||
button {
|
||||
color: var(--text-100) !important;
|
||||
transform: translate(25%, 25%);
|
||||
@media @mobile {
|
||||
transform: translate(-50%, 25%);
|
||||
}
|
||||
svg {
|
||||
fill: var(--text-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.lower-bar {
|
||||
background-color: var(--bg-100);
|
||||
}
|
||||
}
|
||||
.scroll-container {
|
||||
.simplebar-content {
|
||||
padding-left: 90px !important;
|
||||
padding-right: 2.8rem !important;
|
||||
@media @mobile {
|
||||
padding-left: 38px !important;
|
||||
}
|
||||
}
|
||||
background-color: var(--bg-100);
|
||||
.simplebar-track {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.simplebar-scrollbar {
|
||||
transition-duration: 0ms !important;
|
||||
cursor: pointer;
|
||||
&::before {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content-listing {
|
||||
background-color: var(--bg-100);
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,476 +0,0 @@
|
||||
<script
|
||||
context="module"
|
||||
lang="ts"
|
||||
>
|
||||
import { enableScrolling, stopScrolling } from "../../functions/utils"
|
||||
let scrollPosition ={ top: 0, left: 0 }
|
||||
let whiteHeader = false
|
||||
export function changeStateOfSite(menuOn: boolean) {
|
||||
if (typeof window !== "undefined") {
|
||||
if (menuOn) {
|
||||
// make sure its not already in thsi state
|
||||
const state = document.body.classList.contains("no-scroll")
|
||||
if (state) {
|
||||
return
|
||||
}
|
||||
|
||||
stopScrolling(scrollPosition)
|
||||
} else {
|
||||
// make sure only if it is in this state
|
||||
const state = document.body.classList.contains("no-scroll")
|
||||
|
||||
if (!state) {
|
||||
return
|
||||
}
|
||||
// Remove the no-scroll class and reset the position
|
||||
enableScrolling(scrollPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { mdiAccountOutline, mdiHeartOutline, mdiMenu, mdiShoppingOutline } from "@mdi/js"
|
||||
import { spaLink } from "../../actions"
|
||||
import Icon from "../widgets/Icon.svelte"
|
||||
import { icons } from "../../../config"
|
||||
import MobileMenu from "./MobileMenu.svelte"
|
||||
import { overlays } from "../../store"
|
||||
import { showCartOverlay, showFavoriteOverlay } from "../../functions/helper/product"
|
||||
|
||||
export let elements: NavigationElement[],
|
||||
bannerVisible: boolean,
|
||||
scrolled: boolean,
|
||||
activeSubmenu = -1
|
||||
|
||||
async function changeSubmenu(index: string | number) {
|
||||
changeStateOfSite(true)
|
||||
const submenuContainers = document.getElementsByClassName("submenu-container")
|
||||
Array.from(submenuContainers).forEach((container) => container.classList.remove("shown"))
|
||||
const invisibleMenu = document.getElementById(`submenu-${index}`)
|
||||
invisibleMenu.classList.add("shown")
|
||||
activeSubmenu = Number(index)
|
||||
let headerContainer = document.getElementById("header-container")
|
||||
headerContainer.classList.add("scrolled")
|
||||
}
|
||||
|
||||
function closeSubmenu() {
|
||||
const submenuContainers = document.getElementsByClassName("submenu-container")
|
||||
Array.from(submenuContainers).forEach((container) => container.classList.remove("shown"))
|
||||
changeStateOfSite(false)
|
||||
activeSubmenu = -1
|
||||
if (window.scrollY < 100) {
|
||||
let headerContainer = document.getElementById("header-container")
|
||||
headerContainer.classList.remove("scrolled")
|
||||
}
|
||||
}
|
||||
let hoverTimeout: string | number | NodeJS.Timeout | undefined
|
||||
let hoverCloseTimeout: string | number | NodeJS.Timeout | undefined
|
||||
let isHoveringMenu = false
|
||||
function closeSubmenuWithTimeout() {
|
||||
hoverCloseTimeout = setTimeout(closeSubmenu, 200)
|
||||
}
|
||||
const openMenu = (i: number, page: NavigationElement) => {
|
||||
clearTimeout(hoverTimeout)
|
||||
if (page?.elements?.length) hoverTimeout = setTimeout(() => changeSubmenu(i), 500)
|
||||
else hoverTimeout = setTimeout(closeSubmenu, 500)
|
||||
}
|
||||
|
||||
$: if (isHoveringMenu) {
|
||||
clearTimeout(hoverCloseTimeout)
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav
|
||||
class="menu"
|
||||
class:scrolled={scrolled || whiteHeader}
|
||||
class:not-scrolled={!scrolled && !whiteHeader}
|
||||
class:active={activeSubmenu !== -1}
|
||||
on:mouseover={() => (isHoveringMenu = true)}
|
||||
on:focus={() => (isHoveringMenu = true)}
|
||||
on:mouseleave={() => (isHoveringMenu = false)}
|
||||
>
|
||||
<button
|
||||
aria-label="Open menu"
|
||||
id="burger-menu"
|
||||
class="mobile-burger"
|
||||
on:click={() => {
|
||||
const overlay ={
|
||||
id: 'mobile-menu',
|
||||
title: 'Menu',
|
||||
active: true,
|
||||
content: MobileMenu,
|
||||
properties: {
|
||||
elements,
|
||||
bannerVisible,
|
||||
scrolled,
|
||||
},
|
||||
}
|
||||
$overlays = [overlay]
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
path={mdiMenu}
|
||||
size="24px"
|
||||
color={'#F3EED9'}
|
||||
/>
|
||||
</button>
|
||||
<a
|
||||
href="/"
|
||||
use:spaLink
|
||||
class="logo-container"
|
||||
on:click={closeSubmenu}
|
||||
aria-label="Go to homepage"
|
||||
>
|
||||
<img
|
||||
src="../../../../logo/logo-white.svg"
|
||||
alt="logo"
|
||||
/>
|
||||
</a>
|
||||
<ul class="menuitem-container">
|
||||
{#each elements as page, i (i)}
|
||||
<li
|
||||
class="menu-item"
|
||||
class:active={i == activeSubmenu}
|
||||
on:mouseenter|stopPropagation={() => openMenu(i, page)}
|
||||
on:focus|stopPropagation={() => openMenu(i, page)}
|
||||
on:mouseleave|stopPropagation={(e) => clearTimeout(hoverTimeout)}
|
||||
>
|
||||
<div class="bar">
|
||||
<span></span>
|
||||
<img
|
||||
src="../../../../media/BottomLeftCrinkleRed.svg"
|
||||
alt="crinkle"
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
aria-label={page.name}
|
||||
href={page.page}
|
||||
style="display: flex; flex-direction: column; align-items: center"
|
||||
use:spaLink
|
||||
on:mousedown={closeSubmenu}
|
||||
><div style="padding: 0px; line-height: 100%;">{page.name}</div>
|
||||
{#if page.elements?.length}
|
||||
<Icon
|
||||
path={icons.chevronDown}
|
||||
color={'#F3EED9'}
|
||||
props={{
|
||||
'fill-rule': 'evenodd',
|
||||
'clip-rule': 'evenodd',
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<ul class="icons-container">
|
||||
<li class="menu-item">
|
||||
<button
|
||||
on:click={showFavoriteOverlay}
|
||||
aria-label="Open favorites"
|
||||
><Icon
|
||||
color={'#F3EED9'}
|
||||
size="24px"
|
||||
path={mdiHeartOutline}
|
||||
/></button
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{#each elements as submenu, i (submenu.name)}
|
||||
{#if submenu.elements?.length}
|
||||
<section
|
||||
role="menu"
|
||||
tabindex="0"
|
||||
on:mouseleave={() => !isHoveringMenu && closeSubmenuWithTimeout()}
|
||||
on:focus|stopPropagation
|
||||
class="submenu-container"
|
||||
id={`submenu-${i}`}
|
||||
style="top: 86px"
|
||||
>
|
||||
<ul class="sub-menu-columns">
|
||||
{#each submenu?.elements.slice(0, 2) as submenu_title, i (i)}
|
||||
<li class="column">
|
||||
<a
|
||||
use:spaLink
|
||||
href={submenu_title.page}
|
||||
class="submenu-title"
|
||||
class:bold={submenu_title?.elements?.length}
|
||||
on:click={closeSubmenu}>{submenu_title?.name}</a
|
||||
>
|
||||
{#if submenu_title?.elements?.length}
|
||||
<ul class="sub-menu-rows">
|
||||
{#each submenu_title?.elements || [] as submenu_point, j (j)}
|
||||
<li>
|
||||
<a
|
||||
use:spaLink
|
||||
href={submenu_point.page}
|
||||
class="submenu-endpoint"
|
||||
on:click={closeSubmenu}>{submenu_point?.name}</a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
{#if submenu?.elements?.length > 2}
|
||||
<li class="column">
|
||||
<ul class="sub-menu-rows large-gap">
|
||||
{#each submenu.elements.slice(2) as submenu_title, k (k)}
|
||||
<li>
|
||||
<a
|
||||
use:spaLink
|
||||
href={submenu_title.page}
|
||||
class="submenu-title"
|
||||
class:bold={submenu_title?.elements?.length}
|
||||
on:click={closeSubmenu}>{submenu_title?.name}</a
|
||||
>
|
||||
{#if submenu_title?.elements?.length}
|
||||
<ul class="sub-menu-rows">
|
||||
{#each submenu_title?.elements || [] as submenu_point, l (l)}
|
||||
<li>
|
||||
<a
|
||||
use:spaLink
|
||||
href={submenu_point.page}
|
||||
class="submenu-endpoint"
|
||||
on:click={closeSubmenu}>{submenu_point?.name}</a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
<div class="img-placeholder"></div>
|
||||
<section class="image-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<img
|
||||
src="../../../../media/main-nav-pic.webp"
|
||||
alt="img"
|
||||
/>
|
||||
<svg
|
||||
width="10px"
|
||||
height="50px"
|
||||
viewBox="0 0 1 5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polygon
|
||||
points="0,0 1,0 0,5"
|
||||
fill="white"></polygon>
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<style lang="less">
|
||||
@import "../../assets/css/variables.less";
|
||||
.menu {
|
||||
max-width: var(--normal-max-width);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 86px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
&.not-scrolled a {
|
||||
color: var(--text-100);
|
||||
}
|
||||
a {
|
||||
color: var(--text-100);
|
||||
}
|
||||
.logo-container {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
img {
|
||||
height: 60px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
@media @desktop {
|
||||
#burger-menu {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.menuitem-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
width: fit-content;
|
||||
gap: 1.2rem;
|
||||
padding-top: 0.55rem;
|
||||
height: 100%;
|
||||
.menu-item {
|
||||
height: 50px;
|
||||
padding: 0px 1.2rem 0.6rem 1.2rem;
|
||||
position: relative;
|
||||
.bar {
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
|
||||
left: 0;
|
||||
right: 0;
|
||||
justify-content: flex-end;
|
||||
span {
|
||||
background-color: var(--primary-100);
|
||||
flex-grow: 1;
|
||||
margin-right: -2px;
|
||||
}
|
||||
img {
|
||||
width: 12.169px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
.bar {
|
||||
opacity: 1;
|
||||
}
|
||||
a {
|
||||
color: var(--text-100);
|
||||
}
|
||||
background-color: var(--primary-100);
|
||||
}
|
||||
}
|
||||
@media @max-tablet {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.icons-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 14px;
|
||||
gap: 1.2rem;
|
||||
@media @mobile {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.15rem;
|
||||
.menu-item {
|
||||
&:nth-of-type(1) {
|
||||
margin-right: 0.15rem;
|
||||
}
|
||||
&:nth-of-type(2) {
|
||||
margin-left: 0.15rem;
|
||||
}
|
||||
}
|
||||
.menu-item:nth-child(3) {
|
||||
grid-column: span 2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submenu-container {
|
||||
min-height: 22.3rem;
|
||||
min-width: 100vw;
|
||||
border-top: 4px solid var(--primary-100);
|
||||
height: 0px;
|
||||
z-index: 1000;
|
||||
max-width: var(--normal-max-width);
|
||||
background-color: var(--neutral-white);
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
left: -100vw;
|
||||
transition: left 300ms, opacity 300ms;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
overflow-y: hidden;
|
||||
|
||||
:global &.shown {
|
||||
opacity: 1 !important;
|
||||
left: 0px !important;
|
||||
right: 0px;
|
||||
height: fit-content;
|
||||
padding-left: calc((100vw - var(--normal-max-width)) / 2);
|
||||
@media (max-width: 1920px) {
|
||||
padding-left: var(--horizontal-default-margin);
|
||||
}
|
||||
}
|
||||
|
||||
.sub-menu-columns {
|
||||
padding: 2.4rem 0rem;
|
||||
|
||||
display: flex;
|
||||
align-items: start;
|
||||
gap: 2.4rem;
|
||||
flex-wrap: wrap;
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 180px;
|
||||
gap: 1.2rem;
|
||||
.submenu-title {
|
||||
&.bold {
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
font-style: Outfit-bold, sans-serif;
|
||||
}
|
||||
}
|
||||
.sub-menu-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.9rem;
|
||||
&.large-gap {
|
||||
gap: 2.4rem;
|
||||
}
|
||||
li {
|
||||
.submenu-endpoint {
|
||||
font-weight: 400 !important;
|
||||
font-style: Outfit-normal, sans-serif !important;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.img-placeholder {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 55%;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
max-width: 50%;
|
||||
height: 100%;
|
||||
|
||||
.inner-wrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
height: 100%;
|
||||
width: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,125 +1,247 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
import { getDBEntry } from "../../../api"
|
||||
import { navigationCache, location, categories, overlays } from "../../store"
|
||||
import DesktopHeader from "./Desktop.svelte"
|
||||
import {
|
||||
getBCGraphCategories,
|
||||
mapBigcommerceCategoriesToNavigation,
|
||||
} from "../../functions/CommerceAPIs/bigCommerce/categories"
|
||||
import Banner from "../widgets/Banner.svelte"
|
||||
import { getDBEntries } from "../../../api"
|
||||
import { spaLink } from "../../actions"
|
||||
import { location } from "../../store"
|
||||
|
||||
let navigationElements: NavigationElement[] = [],
|
||||
navOpen = false,
|
||||
subNavOpen: { [key: number]: boolean } ={},
|
||||
windowWidth: number
|
||||
const NAVIGATION_TYPE = {
|
||||
Main: 0,
|
||||
Service: 1,
|
||||
Legal: 2,
|
||||
} as const
|
||||
|
||||
function elementsToCache(elements: NavigationElement[]) {
|
||||
elements.forEach((el) => {
|
||||
if (!el.external) {
|
||||
if (!$navigationCache[el.page]) $navigationCache[el.page] = el
|
||||
if (el.elements?.length > 0) elementsToCache(el.elements)
|
||||
}
|
||||
})
|
||||
let navigationEntries: NavigationEntry[] = []
|
||||
let isMenuOpen = false
|
||||
let loadingNavigation = true
|
||||
|
||||
const resolveHref = (item: NavigationElement) => {
|
||||
const base = item.page || "/"
|
||||
const hash = item.hash ? (item.hash.startsWith("#") ? item.hash : `#${item.hash}`) : ""
|
||||
return `${base}${hash}`
|
||||
}
|
||||
|
||||
getDBEntry("navigation", {
|
||||
tree: 0,
|
||||
}).then((nav) => {
|
||||
navigationElements.push(...nav.elements)
|
||||
navigationElements = navigationElements
|
||||
})
|
||||
|
||||
$: if (!navOpen) subNavOpen ={}
|
||||
$: if ($location) navOpen = false
|
||||
|
||||
let scrolled: boolean = false,
|
||||
isHomepage: boolean = false,
|
||||
bannerVisible = false
|
||||
$: isHomepage = !$location.path || $location.path === "/"
|
||||
|
||||
function checkScroll() {
|
||||
scrolled = window.scrollY >= 100
|
||||
const isActive = (item: NavigationElement) => {
|
||||
const target = resolveHref(item)
|
||||
const [path] = target.split("#")
|
||||
return path === $location.path
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
checkScroll()
|
||||
window.addEventListener("scroll", checkScroll)
|
||||
return () => {
|
||||
window.removeEventListener("scroll", checkScroll)
|
||||
}
|
||||
const closeMenu = () => {
|
||||
isMenuOpen = false
|
||||
}
|
||||
|
||||
const toggleMenu = () => {
|
||||
isMenuOpen = !isMenuOpen
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const entries = await getDBEntries("navigation")
|
||||
navigationEntries = entries ?? []
|
||||
} catch (error) {
|
||||
console.error("Unable to load navigation", error)
|
||||
} finally {
|
||||
loadingNavigation = false
|
||||
}
|
||||
})
|
||||
$: {
|
||||
if (typeof window !== "undefined" && $location) {
|
||||
checkScroll()
|
||||
}
|
||||
}
|
||||
|
||||
let activeSubmenu = -1
|
||||
$: darkBG = isHomepage ? (scrolled ? true : activeSubmenu >= 0 || $overlays?.length) : false
|
||||
$: mainNavigation = navigationEntries.find((entry) => Number(entry.type) === NAVIGATION_TYPE.Main)
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth={windowWidth} />
|
||||
|
||||
<Banner bind:isVisible={bannerVisible} />
|
||||
<header
|
||||
class="headercontainer"
|
||||
id={'header-container'}
|
||||
class:scrolled={darkBG}
|
||||
class:homepageHeader={isHomepage}
|
||||
class:bannerVisible={bannerVisible}
|
||||
role="dialog"
|
||||
aria-label="Hauptnavigation"
|
||||
on:focus
|
||||
>
|
||||
<div class="padding">
|
||||
{#key [bannerVisible]}
|
||||
<DesktopHeader
|
||||
bind:activeSubmenu={activeSubmenu}
|
||||
elements={navigationElements}
|
||||
bannerVisible={bannerVisible}
|
||||
scrolled={darkBG}
|
||||
/>
|
||||
{/key}
|
||||
<header class="site-header" aria-label="Primäre Navigation">
|
||||
<div class="header-inner">
|
||||
<a
|
||||
class="brand"
|
||||
href="/"
|
||||
use:spaLink
|
||||
on:click={closeMenu}
|
||||
>
|
||||
Kontextwerk
|
||||
</a>
|
||||
<button
|
||||
class="menu-toggle"
|
||||
aria-expanded={isMenuOpen}
|
||||
aria-controls="primary-navigation"
|
||||
on:click={toggleMenu}
|
||||
>
|
||||
<span class="sr-only">Navigation umschalten</span>
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
</button>
|
||||
<nav
|
||||
id="primary-navigation"
|
||||
class:open={isMenuOpen}
|
||||
>
|
||||
{#if loadingNavigation}
|
||||
<span class="nav-placeholder">Navigation wird geladen …</span>
|
||||
{:else if mainNavigation?.elements?.length}
|
||||
<ul>
|
||||
{#each mainNavigation.elements as item (item.name)}
|
||||
<li class:active={isActive(item)}>
|
||||
{#if item.external && item.externalUrl}
|
||||
<a
|
||||
href={item.externalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
on:click={closeMenu}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href={resolveHref(item)}
|
||||
use:spaLink
|
||||
on:click={closeMenu}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<span class="nav-placeholder">Keine Navigationspunkte verfügbar</span>
|
||||
{/if}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../assets/css/variables.less";
|
||||
|
||||
@desktop: ~"only screen and (min-width: 1440px)";
|
||||
|
||||
.headercontainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@media @mobile {
|
||||
overflow: hidden;
|
||||
}
|
||||
.site-header {
|
||||
position: sticky;
|
||||
z-index: 5500;
|
||||
top: 0px;
|
||||
|
||||
justify-content: space-between;
|
||||
background-color: #0d0c0c;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
background-color: var(--neutral-white);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
max-width: var(--body-maxwidth);
|
||||
margin: 0 auto;
|
||||
padding: 1.2rem 1.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
.padding {
|
||||
width: 100%;
|
||||
padding: 0px var(--horizontal-default-margin);
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-invers-100);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.6rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
|
||||
.bar {
|
||||
width: 1.6rem;
|
||||
height: 2px;
|
||||
background-color: var(--text-invers-100);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 1.2rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
&.homepageHeader {
|
||||
background-color: transparent;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
|
||||
&.active a::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
&.scrolled&.homepageHeader {
|
||||
box-shadow: 0px 10px 20px 0px rgba(0, 0, 0, 0.2);
|
||||
background-color: var(--bg-100);
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
color: var(--text-100);
|
||||
padding: 0.3rem 0;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -0.3rem;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: var(--accent-100, #c4102d);
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-placeholder {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-60);
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.menu-toggle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
nav {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: var(--neutral-white);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
transform: translateY(-110%);
|
||||
transition: transform 0.2s ease;
|
||||
padding: 1.2rem 1.6rem;
|
||||
|
||||
&.open {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
ul {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,261 +0,0 @@
|
||||
<script
|
||||
context="module"
|
||||
lang="ts"
|
||||
>
|
||||
import { overlays } from "../../store"
|
||||
import MobileMenuSubCategory from "./MobileMenuSubCategory.svelte"
|
||||
|
||||
export function openSubmodule(element: NavigationElement) {
|
||||
overlays.update((o) => {
|
||||
const openedAtLvl = o.findIndex((overlay) => overlay.active)
|
||||
let overlays = o.map((overlay) => {
|
||||
overlay.active = false
|
||||
return overlay
|
||||
})
|
||||
overlays.splice(openedAtLvl + 1, overlays.length - openedAtLvl - 1)
|
||||
const subMenuOverlay: Overlay ={
|
||||
id: element.name,
|
||||
title: element.name,
|
||||
active: true,
|
||||
content: MobileMenuSubCategory,
|
||||
properties: {
|
||||
element,
|
||||
},
|
||||
}
|
||||
return [...overlays, subMenuOverlay]
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { mdiChevronRight } from "@mdi/js"
|
||||
import { spaLink, spaNavigate } from "../../actions"
|
||||
import Icon from "../widgets/Icon.svelte"
|
||||
import { socialIcons } from "../../../config"
|
||||
|
||||
export let elements: NavigationElement[]
|
||||
|
||||
const mainCategories = elements.filter((el) => el.type === "bigcommerce")
|
||||
let selectedCategory = mainCategories[0]
|
||||
const contentCategories = elements.filter((el) => el.type === "content")
|
||||
|
||||
//menu übereinander stacken
|
||||
</script>
|
||||
|
||||
<section id="mobileMenu">
|
||||
<section id="commerceCategories">
|
||||
<ul id="mainCategories">
|
||||
{#each mainCategories as mainCategory}
|
||||
<li>
|
||||
<button
|
||||
class="cta"
|
||||
class:active={selectedCategory.name === mainCategory.name}
|
||||
on:click={() => {
|
||||
selectedCategory = mainCategory
|
||||
}}
|
||||
>
|
||||
{mainCategory.name}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<ul id="subCategories">
|
||||
{#each selectedCategory.elements as subCategory}
|
||||
<li>
|
||||
<button
|
||||
class="cta"
|
||||
on:click={() => {
|
||||
openSubmodule(subCategory)
|
||||
}}
|
||||
>
|
||||
<p>{subCategory.name}</p>
|
||||
<Icon path={mdiChevronRight} />
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="contentCategories">
|
||||
<h4>Mehr Entdecken</h4>
|
||||
<ul>
|
||||
{#each contentCategories as contentCategory}
|
||||
{#if !!contentCategory.name}
|
||||
<li>
|
||||
<button
|
||||
class="cta"
|
||||
on:click={() => {
|
||||
if (contentCategory.elements?.length) openSubmodule(contentCategory)
|
||||
else spaNavigate(contentCategory.page)
|
||||
}}
|
||||
>
|
||||
<p>{contentCategory.name}</p>
|
||||
</button>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
<section class="additionalLinks">
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
use:spaLink
|
||||
href="/profile">Account</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
use:spaLink
|
||||
href="/widerrufsbelehrung">Retoure & Rückerstattung</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="socialMediaLinks">
|
||||
<ul class="social">
|
||||
{#each Object.keys(socialIcons) as icon}
|
||||
<li>
|
||||
<a
|
||||
href={socialIcons[icon]}
|
||||
use:spaLink
|
||||
target="_blank"
|
||||
aria-label={icon}
|
||||
>
|
||||
<figure class="footer-icon">
|
||||
<img
|
||||
alt={icon}
|
||||
src="../../../media/{icon}.svg"
|
||||
/>
|
||||
</figure></a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../assets/css/variables.less";
|
||||
#mobileMenu {
|
||||
width: 100%;
|
||||
padding: 24px 1rem 24px 90px;
|
||||
@media @mobile {
|
||||
padding: 24px 2rem 24px 38px;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
|
||||
button {
|
||||
color: var(--text-invers-100);
|
||||
width: 100%;
|
||||
text-transform: uppercase;
|
||||
font-family: "Outfit-Bold", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
}
|
||||
#commerceCategories {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
#mainCategories {
|
||||
display: flex;
|
||||
gap: 0px;
|
||||
width: 100%;
|
||||
li {
|
||||
width: 0px;
|
||||
flex-grow: 1;
|
||||
button {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--bg-100);
|
||||
|
||||
&.active {
|
||||
background-color: var(--primary-100);
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
&:nth-child(1) {
|
||||
button {
|
||||
border-right: 0px solid black;
|
||||
}
|
||||
}
|
||||
&:nth-child(2) {
|
||||
button {
|
||||
border-left: 0px solid black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#subCategories {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
li {
|
||||
button {
|
||||
background-color: var(--bg-300);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#contentCategories {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
h4 {
|
||||
font-family: "Outfit-Bold", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
li {
|
||||
button {
|
||||
background-color: var(--bg-300);
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.additionalLinks {
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 12px;
|
||||
|
||||
li {
|
||||
a {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.socialMediaLinks {
|
||||
ul {
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
justify-content: center;
|
||||
li {
|
||||
a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,61 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiChevronRight } from "@mdi/js"
|
||||
import { spaNavigate } from "../../actions"
|
||||
import { openSubmodule } from "./MobileMenu.svelte"
|
||||
import Icon from "../widgets/Icon.svelte"
|
||||
import { overlays } from "../../store"
|
||||
export let element: NavigationElement, darkBg: boolean
|
||||
</script>
|
||||
|
||||
<ul
|
||||
class="subCategory"
|
||||
class:darkBg={darkBg}
|
||||
>
|
||||
{#each element.elements as subCategory}
|
||||
<li>
|
||||
<button
|
||||
class="cta"
|
||||
on:click={() => {
|
||||
if (subCategory.elements?.length) openSubmodule(subCategory)
|
||||
else {
|
||||
$overlays = []
|
||||
spaNavigate(subCategory.page)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p>{subCategory.name}</p>
|
||||
{#if subCategory.elements?.length}
|
||||
<Icon path={mdiChevronRight} />
|
||||
{/if}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style lang="less">
|
||||
.subCategory {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
li {
|
||||
button {
|
||||
background-color: var(--bg-300);
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
&.darkBg {
|
||||
background-color: transparent;
|
||||
button {
|
||||
background: var(--bg-200);
|
||||
color: var(--text-100);
|
||||
p {
|
||||
color: var(--text-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,36 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { spaLink } from "../../actions"
|
||||
import { navigationCache } from "../../store"
|
||||
|
||||
export let location: LocationStore = undefined
|
||||
|
||||
let paths: string[] = []
|
||||
|
||||
$: if (location?.path?.match(/\/[^\/]+\//)) {
|
||||
let _paths: string[] = []
|
||||
let _p = location.path
|
||||
while (_p || _p.includes("/")) {
|
||||
_paths.push(_p)
|
||||
// remove last part
|
||||
_p = _p.replace(/\/[^\/]+\/?$/, "")
|
||||
}
|
||||
paths = _paths.reverse()
|
||||
} else {
|
||||
paths = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- only for paths with more levels -->
|
||||
{#if paths.length}
|
||||
<nav aria-label="Breadcrumbs" class="breadcrumbs">
|
||||
<div class="container vp-m">
|
||||
<ol>
|
||||
{#each paths as path}
|
||||
{#if $navigationCache[path]}
|
||||
<li><a use:spaLink href={path}>{$navigationCache[path].name}</a></li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ol>
|
||||
</div>
|
||||
</nav>
|
||||
{/if}
|
||||
@@ -1,301 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { apiBaseOverride, location } from "../../store"
|
||||
import blocks from "./blocks"
|
||||
import CrinkledSection from "../CrinkledSection.svelte"
|
||||
import MedialibImage from "../widgets/MedialibImage.svelte"
|
||||
import Button from "../widgets/Button.svelte"
|
||||
|
||||
export let block: ContentBlock,
|
||||
apiBase: string = null,
|
||||
verticalPadding = true,
|
||||
noHorizontalMargin = false
|
||||
|
||||
if (apiBase) $apiBaseOverride = apiBase
|
||||
$: blockComponent = blocks[block.type] || blocks.default
|
||||
let scrollTargetContainer: HTMLDivElement
|
||||
let lastAnchor: string
|
||||
$: if (block.anchorId && typeof window !== "undefined") {
|
||||
const targetAnchor = $location.hash
|
||||
if (targetAnchor !== lastAnchor && scrollTargetContainer) {
|
||||
lastAnchor = targetAnchor
|
||||
if (targetAnchor == "#" + block.anchorId) {
|
||||
setTimeout(() => {
|
||||
scrollTargetContainer.scrollIntoView({ behavior: "instant", block: "start", inline: "nearest" })
|
||||
setTimeout(() => {
|
||||
scrollTargetContainer.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" })
|
||||
}, 600)
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<CrinkledSection activated={block.crinkledSection || false}>
|
||||
{#if block.anchorId}
|
||||
<!-- position 100px above -->
|
||||
<div style="position:relative;">
|
||||
<div
|
||||
bind:this={scrollTargetContainer}
|
||||
style="position: absolute; top: -100px"
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
<section
|
||||
class:additionalHeightAtBottom={block.additionalHeightBottom}
|
||||
class:headerHeightUp={block?.background?.headerHeightUp}
|
||||
data-type={block.type}
|
||||
class="content-section minheight-{block?.background?.minHeight} {blockComponent.sectionClass} {block.background
|
||||
?.color
|
||||
? block?.background?.color + '-bg'
|
||||
: ''}
|
||||
>
|
||||
{#if block.background?.image}
|
||||
<div class="background-image">
|
||||
<figure>
|
||||
<MedialibImage id={block?.background?.image} />
|
||||
{#if block?.background?.overlay}
|
||||
<div class="overlay"></div>
|
||||
{/if}
|
||||
</figure>
|
||||
</div>
|
||||
{/if}
|
||||
<section
|
||||
class="content"
|
||||
class:narrowWidth={block.contentWidth == 1}
|
||||
class:normalWidth={block.contentWidth == 2}
|
||||
class:fullWidth={block.contentWidth == 0}
|
||||
>
|
||||
<div
|
||||
class="wrapper"
|
||||
class:noHorizontalMargin={noHorizontalMargin}
|
||||
class:noMobileHorizontalMargin={block?.background?.noHorizontalMargin}
|
||||
class:verticalPadding={verticalPadding && !block?.background?.noVerticalPadding}
|
||||
>
|
||||
{#if block.headline || block.subline}
|
||||
<div class="headline-row">
|
||||
<div
|
||||
class="container bp-xl"
|
||||
class:darkColor={!block?.background?.image && block?.background?.color === 'white'}
|
||||
>
|
||||
<!-- Überschrift Element -->
|
||||
{#if block.headline}
|
||||
{#if block.headlineH1}
|
||||
<h1 class="h2">{block.headline}</h1>
|
||||
{:else}
|
||||
<h2 class:doublyLined={block.doublyLined}>{block.headline}</h2>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if block.subline}
|
||||
<h3>{block.subline}</h3>
|
||||
{/if}
|
||||
</div>
|
||||
{#if block?.headlineLink}
|
||||
<button class="">
|
||||
<a
|
||||
href={block.headlineLink}
|
||||
class="headline-link"
|
||||
>
|
||||
{block.headlineLinkText}
|
||||
</a>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<svelte:component
|
||||
this={blockComponent.component}
|
||||
block={block}
|
||||
/>
|
||||
<div class="buttons">
|
||||
{#each block.callToActionButtons || [] as button}
|
||||
<Button button={button} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</CrinkledSection>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../lib/assets/css/variables.less";
|
||||
.content-section {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&.additionalHeightAtBottom {
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
&.headerHeightUp {
|
||||
margin-top: -86px;
|
||||
}
|
||||
&.minheight-normal {
|
||||
height: 38rem;
|
||||
min-height: 75vh;
|
||||
}
|
||||
|
||||
&.minheight-extended {
|
||||
padding-top: 145px;
|
||||
height: calc(38rem + 145px);
|
||||
min-height: 75vh;
|
||||
}
|
||||
&.homepageRow {
|
||||
height: calc(38rem + 145px + 96px + 96px);
|
||||
max-height: 97vh;
|
||||
}
|
||||
&.chapterPreview {
|
||||
.content {
|
||||
& > .wrapper {
|
||||
margin: 0px !important;
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.ImproveYourselfDescription {
|
||||
height: unset;
|
||||
.content {
|
||||
.wrapper {
|
||||
display: unset !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
padding-top: 2.4rem;
|
||||
}
|
||||
& .content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.headline-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
& > button {
|
||||
width: fit-content;
|
||||
white-space: nowrap;
|
||||
a {
|
||||
color: var(--white-100);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.8rem;
|
||||
|
||||
h2,
|
||||
h1 {
|
||||
width: 100%;
|
||||
}
|
||||
h2.doublyLined,
|
||||
h3.doublyLined {
|
||||
border-top: 1px solid white;
|
||||
border-bottom: 1px solid white;
|
||||
padding: 1.2rem 3.6rem;
|
||||
margin: 1.2rem -3.6rem;
|
||||
margin-top: 0px;
|
||||
width: calc(100% + 7.2rem);
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
&.darkColor {
|
||||
h2,
|
||||
h1,
|
||||
h3 {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
h2.doublyLined,
|
||||
h3.doublyLined {
|
||||
border-top: 1px solid var(--bg-100);
|
||||
border-bottom: 1px solid var(--bg-100);
|
||||
padding: 1.2rem 3.6rem;
|
||||
margin: 1.2rem -3.6rem;
|
||||
margin-top: 0px;
|
||||
width: calc(100% + 7.2rem);
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
}
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
}
|
||||
.wrapper {
|
||||
margin: 0px var(--horizontal-default-margin);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: calc(100% - var(--horizontal-default-margin));
|
||||
max-width: calc(100% - var(--horizontal-default-margin));
|
||||
height: 100%;
|
||||
&.verticalPadding {
|
||||
padding: var(--vertical-default-margin) 0px;
|
||||
}
|
||||
&.noHorizontalMargin {
|
||||
margin: 0px;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
&.noMobileHorizontalMargin {
|
||||
@media @mobile {
|
||||
margin: 0px;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.normalWidth {
|
||||
.wrapper {
|
||||
max-width: var(--normal-max-width);
|
||||
}
|
||||
}
|
||||
&.narrowWidth {
|
||||
.wrapper {
|
||||
max-width: var(--small-max-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
.background-image {
|
||||
z-index: 1;
|
||||
top: 0px;
|
||||
overflow: hidden;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
figure {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
.overlay {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
background-color: rgba(32, 28, 28, 0.3);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:global .background-image > figure > img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
:global .black-bg {
|
||||
background-color: var(--bg-100);
|
||||
}
|
||||
</style>
|
||||
@@ -1,126 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
import { register } from "swiper/element/bundle"
|
||||
import MedialibImage from "../widgets/MedialibImage.svelte"
|
||||
export let images: string[]
|
||||
export let imageHoverEffect: boolean
|
||||
export let forceFullHeight: boolean
|
||||
register(false)
|
||||
let swiper: any
|
||||
onMount(async () => {
|
||||
if (swiper !== undefined) {
|
||||
const response = await fetch("/dist/index.css"),
|
||||
cssText = await response.text(),
|
||||
params ={
|
||||
injectStyles: [cssText],
|
||||
}
|
||||
Object.assign(swiper, params)
|
||||
swiper.initialize()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if images.length > 1}
|
||||
<div class="default-swiper">
|
||||
<swiper-container
|
||||
bind:this={swiper}
|
||||
slides-per-view="1"
|
||||
loop={true}
|
||||
direction="horizontal"
|
||||
effect="slide"
|
||||
autoplay-delay="2500"
|
||||
mousewheel={true}
|
||||
navigation={true}
|
||||
init={false}
|
||||
speed="600"
|
||||
class="relative"
|
||||
>
|
||||
{#each images as image (image)}
|
||||
<swiper-slide class="relative">
|
||||
<div
|
||||
class:imageHoverEffect={imageHoverEffect}
|
||||
class="image-container"
|
||||
>
|
||||
<MedialibImage
|
||||
id={image}
|
||||
filter={typeof window !== 'undefined' && window.innerWidth > 500 ? 'xl' : 'm'}
|
||||
/>
|
||||
</div>
|
||||
</swiper-slide>
|
||||
{/each}
|
||||
</swiper-container>
|
||||
</div>
|
||||
{:else if images[0]}
|
||||
<div
|
||||
class="image-container single flex"
|
||||
class:forceFullHeight={forceFullHeight}
|
||||
class:imageHoverEffect={imageHoverEffect}
|
||||
>
|
||||
<MedialibImage
|
||||
id={images[0]}
|
||||
filter={typeof window !== 'undefined' && window.innerWidth > 500 ? 'xl' : 'm'}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="less">
|
||||
@import "../../assets/css/variables.less";
|
||||
|
||||
:global .default-swiper {
|
||||
flex: 2 !important;
|
||||
.swiper-button-prev,
|
||||
.swiper-button-next {
|
||||
transform-origin: left;
|
||||
color: #333;
|
||||
transform: scale(0.3);
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
top: 50%;
|
||||
padding: 10px;
|
||||
height: 70px;
|
||||
width: 70px;
|
||||
border-radius: 50px;
|
||||
}
|
||||
.swiper-button-prev {
|
||||
left: 6%;
|
||||
}
|
||||
.swiper-button-next {
|
||||
right: 6%;
|
||||
transform-origin: right;
|
||||
}
|
||||
|
||||
swiper-container {
|
||||
width: 100%;
|
||||
swiper-slide {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-container {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
overflow: visible;
|
||||
max-width: @body-maxwidth;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
:global &.imageHoverEffect:hover img {
|
||||
transform: scale(1.05);
|
||||
transform-origin: center;
|
||||
}
|
||||
:global & > img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
:global &.forceFullHeight img {
|
||||
width: unset;
|
||||
}
|
||||
@media @mobile {
|
||||
:global & > img {
|
||||
height: 90% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,36 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { BarLoader, Circle } from "svelte-loading-spinners"
|
||||
export let size: string,
|
||||
type: "bar" | "circle" = "circle"
|
||||
</script>
|
||||
|
||||
<div class={type}>
|
||||
{#if type == "bar"}
|
||||
<BarLoader
|
||||
size={size}
|
||||
color="#741e20"
|
||||
unit="rem"
|
||||
/>
|
||||
{:else}
|
||||
<Circle
|
||||
size={size}
|
||||
color="#741e20"
|
||||
unit="rem"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
div {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 2.4rem 0px;
|
||||
&.circle {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,84 +0,0 @@
|
||||
<script lang="ts">
|
||||
import OpenGraph from "./OpenGraph.svelte"
|
||||
import Product from "./Product.svelte"
|
||||
import SchemaOrg from "./SchemaORG.svelte"
|
||||
import { websiteName } from "../../../../config"
|
||||
|
||||
export let article: boolean = false,
|
||||
createdAt: Date = null,
|
||||
updatedAt: Date = null,
|
||||
keywords: string = "",
|
||||
metaDescription: string = "",
|
||||
title: string = "",
|
||||
product: BKDFProduct = null,
|
||||
noIndex = false,
|
||||
active = true,
|
||||
FAQ = false,
|
||||
FAQDetails: {
|
||||
question: string
|
||||
answer: string
|
||||
}[] = []
|
||||
|
||||
let pageTitle: string
|
||||
keywords += ", BinKrassDuFass, BKDF, BinKrassDuFass.de"
|
||||
|
||||
if (title) pageTitle = `${title} - ${websiteName}`
|
||||
else pageTitle = `${websiteName}`
|
||||
|
||||
const openGraphProps ={
|
||||
article,
|
||||
datePublished: createdAt,
|
||||
lastUpdated: updatedAt,
|
||||
metaDescription,
|
||||
pageTitle,
|
||||
product,
|
||||
}
|
||||
const schemaOrgProps ={
|
||||
createdAt,
|
||||
updatedAt,
|
||||
article,
|
||||
description: metaDescription,
|
||||
title: pageTitle,
|
||||
product,
|
||||
FAQ,
|
||||
FAQDetails,
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{pageTitle}</title>
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content={metaDescription}
|
||||
/>
|
||||
|
||||
<meta
|
||||
name="keywords"
|
||||
content={keywords}
|
||||
/>
|
||||
|
||||
<meta
|
||||
property="og:site_name"
|
||||
content={websiteName}
|
||||
/>
|
||||
|
||||
{#if noIndex || active === false}
|
||||
<meta
|
||||
name="robots"
|
||||
content="noindex, nofollow"
|
||||
/>
|
||||
{:else}
|
||||
<meta
|
||||
name="robots"
|
||||
content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1"
|
||||
/>
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
{#if product}
|
||||
<Product product={product} />
|
||||
{/if}
|
||||
<OpenGraph {...openGraphProps} />
|
||||
|
||||
<SchemaOrg {...schemaOrgProps} />
|
||||
@@ -1,130 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { baseURL, companyName } from "../../../../config"
|
||||
import { location } from "../../../store"
|
||||
export let article: boolean = false
|
||||
export let datePublished: Date | null
|
||||
export let lastUpdated: Date | null
|
||||
export let metaDescription: string | null
|
||||
export let pageTitle: string | null
|
||||
export let product: BKDFProduct
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<meta
|
||||
property="og:locale"
|
||||
content={'de_DE'}
|
||||
/>
|
||||
<meta
|
||||
property="og:url"
|
||||
content={$location.url}
|
||||
/>
|
||||
<meta
|
||||
property="og:type"
|
||||
content={article ? 'article' : 'website'}
|
||||
/>
|
||||
<meta
|
||||
property="og:title"
|
||||
content={pageTitle}
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content={metaDescription}
|
||||
/>
|
||||
{#if product}
|
||||
<meta
|
||||
property="og:image"
|
||||
content={product.featuredImage?.url}
|
||||
/>
|
||||
<meta
|
||||
property="og:image:width"
|
||||
content={String(product.featuredImage?.width)}
|
||||
/>
|
||||
<meta
|
||||
property="og:image:height"
|
||||
content={String(product.featuredImage?.height)}
|
||||
/>
|
||||
<meta
|
||||
property="og:image:alt"
|
||||
content={product.title}
|
||||
/>
|
||||
<!-- Twitter Card Tags for Product -->
|
||||
<meta
|
||||
name="twitter:card"
|
||||
content="summary_large_image"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content={pageTitle}
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content={metaDescription}
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content={product.featuredImage?.url}
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image:alt"
|
||||
content={product.title}
|
||||
/>
|
||||
{:else}
|
||||
<meta
|
||||
property="og:image"
|
||||
content={baseURL}/api/_/assets/logo/logo-blue.svg"
|
||||
/>
|
||||
<meta
|
||||
property="og:image:width"
|
||||
content="576"
|
||||
/>
|
||||
<meta
|
||||
property="og:image:height"
|
||||
content="158"
|
||||
/>
|
||||
<meta
|
||||
property="og:image:alt"
|
||||
content="BinKrassDuFass Logo"
|
||||
/>
|
||||
|
||||
<!-- Twitter Card Tags for Default -->
|
||||
<meta
|
||||
name="twitter:card"
|
||||
content="summary_large_image"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content={pageTitle}
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content={metaDescription}
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content={baseURL}/api/_/assets/logo/logo-blue.svg"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image:alt"
|
||||
content="BinKrassDuFass Logo"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if article}
|
||||
<meta
|
||||
property="article:publisher"
|
||||
content={companyName}
|
||||
/>
|
||||
<meta
|
||||
property="article:author"
|
||||
content={companyName}
|
||||
/>
|
||||
<meta
|
||||
property="article:published_time"
|
||||
content={datePublished?.toISOString()}
|
||||
/>
|
||||
<meta
|
||||
property="article:modified_time"
|
||||
content={lastUpdated?.toISOString()}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:head>
|
||||
@@ -1,16 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { location } from "../../../store"
|
||||
export let product: BKDFProduct
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="canonical" href={$location.url} />
|
||||
|
||||
<meta property="product:brand" content="BinKrassDuFass" />
|
||||
|
||||
<meta property="product:name" content={product.title} />
|
||||
|
||||
<meta property="product:price:amount" content={String(product.priceRange.minVariantPrice.amount)} />
|
||||
|
||||
<meta property="product:price:currency" content={product.priceRange.minVariantPrice.currencyCode} />
|
||||
</svelte:head>
|
||||
@@ -1,224 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
baseURL,
|
||||
socialIcons,
|
||||
websiteName,
|
||||
streetAddress,
|
||||
regionAddress,
|
||||
zipCode,
|
||||
countryAddress,
|
||||
localityAddress,
|
||||
email,
|
||||
} from "../../../../config"
|
||||
import { location } from "../../../store"
|
||||
export let createdAt: Date | string = new Date(),
|
||||
updatedAt: Date | string = new Date(),
|
||||
article = false,
|
||||
blog = false,
|
||||
FAQ = false,
|
||||
FAQDetails: {
|
||||
question: string
|
||||
answer: string
|
||||
}[] = [],
|
||||
metaDescription = "",
|
||||
title = "",
|
||||
product: BKDFProduct
|
||||
|
||||
createdAt = new Date(createdAt).toLocaleDateString("de-DE", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
})
|
||||
updatedAt = new Date(updatedAt).toLocaleDateString("de-DE", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
})
|
||||
|
||||
const defaultProps ={
|
||||
author: {
|
||||
"@id": `${baseURL}/#company`,
|
||||
},
|
||||
publisher: {
|
||||
"@id": `${baseURL}/#company`,
|
||||
},
|
||||
inLanguage: {
|
||||
"@type": "Language",
|
||||
name: "German",
|
||||
},
|
||||
datePublished: createdAt,
|
||||
dateModified: updatedAt,
|
||||
isPartOf: {
|
||||
"@id": `${baseURL}/#website`,
|
||||
},
|
||||
}
|
||||
|
||||
const schemaOrgEntity ={
|
||||
"@type": ["Store", "Organization"],
|
||||
"@id": `${baseURL}/#company`,
|
||||
name: websiteName,
|
||||
url: baseURL,
|
||||
email,
|
||||
address: {
|
||||
"@type": "PostalAddress",
|
||||
streetAddress,
|
||||
addressLocality: localityAddress,
|
||||
addressRegion: regionAddress,
|
||||
postalCode: zipCode,
|
||||
addressCountry: countryAddress,
|
||||
},
|
||||
image: {
|
||||
"@type": "ImageObject",
|
||||
"@id": `${baseURL}/#image`,
|
||||
inLanguage: {
|
||||
"@type": "Language",
|
||||
name: "German",
|
||||
},
|
||||
contentUrl: `${baseURL}/api/_/assets/logo/logo-blue.svg`,
|
||||
width: 576,
|
||||
height: 158,
|
||||
caption: "BinKrassDuFass Logo",
|
||||
},
|
||||
logo: {
|
||||
"@type": "ImageObject",
|
||||
"@id": `${baseURL}/#logo`,
|
||||
url: baseURL,
|
||||
contentUrl: `${baseURL}/api/_/assets/logo/logo-blue.svg`,
|
||||
caption: "BinKrassDuFass Logo",
|
||||
inLanguage: {
|
||||
"@type": "Language",
|
||||
name: "German",
|
||||
},
|
||||
},
|
||||
priceRange: "$$",
|
||||
location: {
|
||||
"@id": location,
|
||||
},
|
||||
sameAs: [Object.values(socialIcons)],
|
||||
}
|
||||
|
||||
const schemaOrgWebsite ={
|
||||
"@type": "WebSite",
|
||||
"@id": `${baseURL}/#website`,
|
||||
url: baseURL,
|
||||
name: "BKDF",
|
||||
description: "BinKrassDuFass Online Store",
|
||||
...defaultProps,
|
||||
}
|
||||
|
||||
const schemaOrgWebPage ={
|
||||
"@type": "WebPage",
|
||||
"@id": `${$location.url}#webpage`,
|
||||
url: $location.url,
|
||||
name: title,
|
||||
...defaultProps,
|
||||
description: metaDescription,
|
||||
|
||||
potentialAction: [
|
||||
{
|
||||
"@type": "ReadAction",
|
||||
target: [$location.url],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
let schemaOrgArticle = null
|
||||
if (article || blog) {
|
||||
schemaOrgArticle ={
|
||||
"@type": "Article",
|
||||
"@id": `${$location.url}#article`,
|
||||
headline: title,
|
||||
description: metaDescription,
|
||||
...defaultProps,
|
||||
articleSection: ["blog"],
|
||||
}
|
||||
}
|
||||
|
||||
let schemaOrgBlog = null
|
||||
if (blog) {
|
||||
schemaOrgBlog ={
|
||||
"@type": "BlogPosting",
|
||||
"@id": `${$location.url}#blog`,
|
||||
headline: title,
|
||||
description: metaDescription,
|
||||
...defaultProps,
|
||||
}
|
||||
}
|
||||
|
||||
let schemaOrgFAQ = null
|
||||
if (FAQ) {
|
||||
schemaOrgFAQ ={
|
||||
"@type": "FAQPage",
|
||||
"@id": `${$location.url}#faq`,
|
||||
...defaultProps,
|
||||
mainEntity: FAQDetails.map((faq) => ({
|
||||
"@type": "Question",
|
||||
name: faq.question,
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: faq.answer,
|
||||
},
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
let schemaOrgProduct = null
|
||||
if (product)
|
||||
schemaOrgProduct ={
|
||||
"@context": "http://schema.org/",
|
||||
"@type": "Product",
|
||||
"@id": `${$location.url}/#product`,
|
||||
...defaultProps,
|
||||
image: {
|
||||
url: product.images[0]?.url,
|
||||
caption: product.images[0]?.altText,
|
||||
},
|
||||
name: product.title,
|
||||
description: product.description,
|
||||
sku: product.sku,
|
||||
brand: "BinKrassDuFass",
|
||||
category: product.categories[0]?.name,
|
||||
mainEntityOfPage: {
|
||||
"@id": `${$location.url}#webpage`,
|
||||
},
|
||||
releaseDate: createdAt,
|
||||
dateModified: updatedAt,
|
||||
url: $location.url,
|
||||
offers: {
|
||||
"@type": "Offer",
|
||||
availability: "http://schema.org/InStock",
|
||||
url: $location.url,
|
||||
price: product.priceRange.minVariantPrice.amount,
|
||||
priceCurrency: product.priceRange.minVariantPrice.currencyCode,
|
||||
itemCondition: "NewCondition",
|
||||
seller: {
|
||||
"@type": "Organization",
|
||||
"@id": `${baseURL}/#company`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const schemaOrgArray = [
|
||||
schemaOrgEntity,
|
||||
schemaOrgWebsite,
|
||||
schemaOrgWebPage,
|
||||
schemaOrgProduct,
|
||||
schemaOrgFAQ,
|
||||
schemaOrgArticle,
|
||||
schemaOrgBlog,
|
||||
].filter(Boolean)
|
||||
const schemaOrgObject ={
|
||||
"@context": "https://schema.org",
|
||||
"@graph": schemaOrgArray,
|
||||
}
|
||||
let jsonLdString = JSON.stringify(schemaOrgObject)
|
||||
let jsonLdScript = `
|
||||
<script type="application/ld+json">
|
||||
${jsonLdString}
|
||||
${"<"}/script>
|
||||
`
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{@html jsonLdScript}
|
||||
</svelte:head>
|
||||
@@ -1,30 +0,0 @@
|
||||
<script lang="ts">
|
||||
import ColumnsColumn from "./ColumnsColumn.svelte"
|
||||
export let block: ContentBlock<"columns">
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#each block.columns || [] as column}
|
||||
<ColumnsColumn column={column} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../../lib/assets/css/variables.less";
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 1.1rem;
|
||||
max-width: var(--normal-max-width);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
@media @mobile {
|
||||
flex-direction: column-reverse;
|
||||
:global & > section {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,72 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { apiBaseOverride } from "../../../store"
|
||||
import DefaultImage from "../DefaultImage.svelte"
|
||||
import ChapterDescription from "./columns/ChapterDescription.svelte"
|
||||
import Cta from "./columns/CTA.svelte"
|
||||
import Text from "./columns/Text.svelte"
|
||||
|
||||
export let column: BlockColumn
|
||||
|
||||
export let apiBase: string = null
|
||||
if (apiBase) $apiBaseOverride = apiBase
|
||||
</script>
|
||||
|
||||
<section
|
||||
class:imageMobileBackground={column.imageMobileBackground}
|
||||
class={column.colWidth > 0 ? 'col-md-' + column.colWidth + ' col-12' : 'col-md-auto col-12'}
|
||||
class:align-self-start={column.verticalAlign == 'top'}
|
||||
class:align-self-center={column.verticalAlign == 'middle'}
|
||||
class:align-self-end={column.verticalAlign == 'bottom'}
|
||||
>
|
||||
{#if column.type == "text"}
|
||||
<Text column={column} />
|
||||
{:else if column.type == "image"}
|
||||
<DefaultImage
|
||||
images={column.images}
|
||||
imageHoverEffect={column.imageHoverEffect}
|
||||
forceFullHeight={column.forceFullHeight}
|
||||
/>
|
||||
{:else if column.type == "cta"}
|
||||
<Cta column={column} />
|
||||
{:else if column.type == "chapterDescription"}
|
||||
<ChapterDescription
|
||||
title={column.chapterDescription.title}
|
||||
type={column.chapterDescription.type}
|
||||
description={column.chapterDescription.description}
|
||||
/>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../../lib/assets/css/variables.less";
|
||||
section {
|
||||
width: 0px;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
@media @mobile {
|
||||
&.imageMobileBackground {
|
||||
position: absolute;
|
||||
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
&.align-self-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
&.align-self-center {
|
||||
justify-content: center;
|
||||
}
|
||||
&.align-self-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +0,0 @@
|
||||
<script lang="ts">
|
||||
import CookieSet from "../../widgets/CookieSet.svelte"
|
||||
const setHeight = (element: HTMLElement) => {
|
||||
element.style.height = (element.offsetWidth / 16) * 9 + "px"
|
||||
}
|
||||
</script>
|
||||
|
||||
<CookieSet cookieName={'googleMaps'} textPosition={'unten'} background={'rgba(44, 44, 44, 0.4)'}>
|
||||
<iframe
|
||||
title="Google Maps"
|
||||
use:setHeight
|
||||
id="iframe"
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2512.077224708092!2d11.023318016007138!3d50.97776317955154!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x47a4729650047377%3A0xca0e03f621729448!2sWebmakers%20GmbH!5e0!3m2!1sde!2sde!4v1571866765252!5m2!1sde!2sde"
|
||||
style="border:0;"
|
||||
allowfullscreen={true}
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer-when-downgrade"></iframe>
|
||||
</CookieSet>
|
||||
|
||||
<style>
|
||||
iframe {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,92 +0,0 @@
|
||||
<script lang="ts">
|
||||
import MedialibImage from "../../widgets/MedialibImage.svelte"
|
||||
import Cta from "./columns/CTA.svelte"
|
||||
|
||||
export let block: ContentBlock<"homepage">
|
||||
const hp = block.mainHomepage
|
||||
</script>
|
||||
|
||||
<section class="homepage">
|
||||
<div class="placeholder"></div>
|
||||
<div class="left">
|
||||
<Cta
|
||||
column={{
|
||||
type: 'cta',
|
||||
cta: hp.cta,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="hp-media">
|
||||
<MedialibImage
|
||||
id={hp.image}
|
||||
filter={typeof window !== 'undefined' && window.innerWidth > 500 ? 'xl' : 'm'}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
@import "../../../assets/css/variables.less";
|
||||
|
||||
.homepage {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: --color 1s ease;
|
||||
margin: -96px 0;
|
||||
|
||||
.placeholder {
|
||||
}
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
transform: translateY(96px);
|
||||
width: 100%;
|
||||
@media @mobile {
|
||||
padding: 0px var(--horizontal-default-margin);
|
||||
}
|
||||
@media @mobile {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
.hp-media {
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: flex-end;
|
||||
flex-grow: 1;
|
||||
@media @mobile {
|
||||
&::before {
|
||||
//shadow
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.9) 100%);
|
||||
}
|
||||
}
|
||||
img {
|
||||
height: calc(100%);
|
||||
object-fit: contain;
|
||||
}
|
||||
@media @mobile {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@media @mobile {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,184 +0,0 @@
|
||||
<script lang="ts">
|
||||
import MedialibImage from "../../widgets/MedialibImage.svelte"
|
||||
|
||||
export let block: ContentBlock<"improveYourselfDescription">
|
||||
const des = block.improveYourselfDescription
|
||||
</script>
|
||||
|
||||
<section class="improveYourselfDescription">
|
||||
<div class="img">
|
||||
<MedialibImage
|
||||
id={des.image}
|
||||
filter="xxl"
|
||||
/>
|
||||
<div class="shadow"></div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2 class="h1">Improve Yourself</h2>
|
||||
<p>{@html des.upperDescription}</p>
|
||||
<ul>
|
||||
<li>
|
||||
<span
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="44"
|
||||
height="32"
|
||||
viewBox="0 0 44 32"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M42.5934 9.61759L21.9008 31.2762L1.20832 9.61759L10.829 0.5H32.9727L42.5934 9.61759Z"
|
||||
stroke="#EB5757"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span style="color: #EB5757;">Fitness</span>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="44"
|
||||
height="32"
|
||||
viewBox="0 0 44 32"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M23.0956 25.0301C25.4532 25.0301 27.6755 24.4466 29.625 23.4161C26.9937 28.2334 21.8812 31.5 16.0069 31.5C7.44266 31.5 0.5 24.5574 0.5 15.9931C0.5 8.2062 6.23975 1.75968 13.719 0.653784C10.8856 3.21373 9.10412 6.9181 9.10412 11.0386C9.10412 18.7659 15.3684 25.0301 23.0956 25.0301Z"
|
||||
stroke="#56F2B0"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span style="color: #56F2B0;">Entspannung</span>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="44"
|
||||
height="32"
|
||||
viewBox="0 0 44 32"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M18.4213 6.89342L18.8989 8.42622L19.3761 6.89329C20.5237 3.20667 23.8663 0.5 27.7914 0.5C32.6671 0.5 36.6326 4.44843 36.6326 9.48563C36.6326 11.9912 35.6486 13.7553 33.9735 15.7311L18.9071 31.2817L3.82337 15.7311C2.15062 13.7562 1.16675 11.9768 1.16675 9.4684C1.16675 4.43715 5.12025 0.599827 10.0049 0.599827C13.9382 0.599827 17.2753 3.21536 18.4213 6.89342Z"
|
||||
stroke="#C0F256"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span style="color: #C0F256;">Ernährung</span>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="44"
|
||||
height="32"
|
||||
viewBox="0 0 44 32"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M16 1.16101L20.157 9.87264L20.2738 10.1176L20.5429 10.153L30.1127 11.4145L23.112 18.0601L22.9152 18.2469L22.9646 18.5137L24.7221 28.005L16.2385 23.4006L16 23.2711L15.7615 23.4006L7.27786 28.005L9.03536 18.5137L9.08477 18.2469L8.88795 18.0601L1.88728 11.4145L11.4571 10.153L11.7262 10.1176L11.843 9.87264L16 1.16101Z"
|
||||
stroke="#56F2F2"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span style="color: #56F2F2;">Weiterbildung</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p>{@html des.lowerDescription}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
@import "../../../assets/css/variables.less";
|
||||
.improveYourselfDescription {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2.4rem;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.img {
|
||||
width: 400px;
|
||||
min-width: 400px;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
.shadow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 2.4rem;
|
||||
z-index: 99;
|
||||
p {
|
||||
color: var(--text-100);
|
||||
}
|
||||
ul {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
span {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media @mobile {
|
||||
flex-direction: column;
|
||||
padding: 0px;
|
||||
.content {
|
||||
ul {
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
li {
|
||||
flex-direction: row;
|
||||
span:first-child {
|
||||
width: 100px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.img {
|
||||
max-width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
.shadow {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(13, 12, 12, 0.5);
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
color: var(--primary-200);
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because one or more lines are too long
@@ -1,12 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Content from "../../../../routes/Content.svelte"
|
||||
|
||||
export let block: ContentBlock<"predefinedBlock">
|
||||
</script>
|
||||
|
||||
{#if block.predefinedBlock?.id}
|
||||
<Content
|
||||
id={block.predefinedBlock.id}
|
||||
allwaysInView
|
||||
/>
|
||||
{/if}
|
||||
@@ -1,203 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
import { getDBEntries } from "../../../../api"
|
||||
import { getVariableNameForChapter } from "../../../utils"
|
||||
|
||||
export let block: ContentBlock<"splittedHomepage">
|
||||
let selfImprovementChapters: SelfImprovementChapter[] = []
|
||||
let interval: NodeJS.Timeout
|
||||
let selectedChapter = -1
|
||||
let currentColor = "#741e20" // Initial color
|
||||
|
||||
getDBEntries("selfImprovementChapter").then((res) => {
|
||||
selfImprovementChapters = res
|
||||
if (selfImprovementChapters.length > 0) {
|
||||
selectedChapter = -1
|
||||
|
||||
setTimeout(() => {
|
||||
// set width and height of placeholder to elements width and height
|
||||
const placeholder = document.querySelector(".placeholder")
|
||||
const elements = document.querySelector(".elements")
|
||||
if (placeholder && elements) {
|
||||
const { width, height } = elements.getBoundingClientRect()
|
||||
placeholder.style.width = `${width}px`
|
||||
placeholder.style.height = `${height}px`
|
||||
}
|
||||
}, 10)
|
||||
}
|
||||
})
|
||||
|
||||
function startInterval() {
|
||||
interval = setInterval(() => {
|
||||
selectedChapter = (selectedChapter + 1) % selfImprovementChapters.length
|
||||
updateShadowColor(selfImprovementChapters[selectedChapter]?.color || "#000")
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
function stopInterval() {
|
||||
clearInterval(interval)
|
||||
}
|
||||
|
||||
function updateShadowColor(newColor: string) {
|
||||
const filter = document.getElementById("redShadow")
|
||||
const feDropShadow = filter?.querySelector("feDropShadow")
|
||||
if (feDropShadow) {
|
||||
feDropShadow.style.transition = "flood-color 1s ease"
|
||||
feDropShadow.setAttribute("flood-color", newColor)
|
||||
currentColor = newColor
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
startInterval()
|
||||
return () => clearInterval(interval)
|
||||
})
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="splittedHomepage"
|
||||
style="--color: {currentColor}
|
||||
>
|
||||
<div class="placeholder"></div>
|
||||
<ul class="elements">
|
||||
{#each selfImprovementChapters as chapter, i}
|
||||
<li
|
||||
class:selected={i == selectedChapter}
|
||||
on:mouseenter={() => {
|
||||
stopInterval()
|
||||
selectedChapter = i
|
||||
updateShadowColor(chapter.color)
|
||||
}}
|
||||
on:mouseleave={startInterval}
|
||||
>
|
||||
{#if i == selectedChapter}
|
||||
<h2
|
||||
class={i % 2 ? '' : 'transparent-heading'}
|
||||
style={i % 2
|
||||
? `color: var(${getVariableNameForChapter(chapter.type)})`
|
||||
: `-webkit-text-stroke: 1px var(${getVariableNameForChapter(chapter.type)})`}
|
||||
>
|
||||
{chapter.title}
|
||||
</h2>
|
||||
{:else}
|
||||
<h2 class={i % 2 ? 'white-heading' : 'transparent-heading'}>{chapter.alias}</h2>
|
||||
{/if}
|
||||
|
||||
<p style="color: var({getVariableNameForChapter(chapter.type)} !important;">
|
||||
{@html chapter.shortDescription}
|
||||
</p>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<div class="media">
|
||||
<svg
|
||||
viewBox="-100 -100 1200 1275"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<filter
|
||||
id="redShadow"
|
||||
x="-50%"
|
||||
y="-50%"
|
||||
width="200%"
|
||||
height="200%"
|
||||
>
|
||||
<feDropShadow
|
||||
dx="-10"
|
||||
dy="0"
|
||||
stdDeviation="20"
|
||||
flood-color={currentColor}></feDropShadow>
|
||||
</filter>
|
||||
</defs>
|
||||
<path
|
||||
d="M877.847 697.627H631.968L582.45 612.133H1000L904.042 446.139H481.362L605.704 231.311L680.46 360.44H854.319L685.658 69.1471L565.762 0L0 977.156L168.593 1074.55H738.253L794.132 977.772L728.062 863.552H973.941L877.847 697.627ZM204.637 924.082L396.895 592.025L589.153 924.082H204.637Z"
|
||||
fill="black"
|
||||
filter="url(#redShadow)"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../../assets/css/variables.less";
|
||||
|
||||
.splittedHomepage {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: --color 1s ease;
|
||||
.placeholder {
|
||||
}
|
||||
.elements {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
@media @mobile {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
li {
|
||||
transition: max-height 1.5s ease;
|
||||
|
||||
max-height: 5rem;
|
||||
max-width: 900px;
|
||||
h2 {
|
||||
font-size: 4.5rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
line-height: 4.5rem;
|
||||
font-family: sans-serif;
|
||||
@media @mobile {
|
||||
font-size: 3rem;
|
||||
line-height: 3rem;
|
||||
}
|
||||
&.transparent-heading {
|
||||
@media @mobile {
|
||||
font-size: 3rem;
|
||||
}
|
||||
font-weight: 700;
|
||||
color: transparent;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
-webkit-text-stroke: 1px white;
|
||||
}
|
||||
}
|
||||
height: fit-content;
|
||||
max-height: 10rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
p {
|
||||
opacity: 0;
|
||||
transition: opacity 1s;
|
||||
}
|
||||
&.selected {
|
||||
p {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.media {
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
@media @mobile {
|
||||
width: 100%;
|
||||
}
|
||||
svg {
|
||||
width: 100%;
|
||||
transition: fill 1s ease;
|
||||
}
|
||||
}
|
||||
@media @mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,48 +0,0 @@
|
||||
<script lang="ts">
|
||||
import MedialibImage from "../../widgets/MedialibImage.svelte"
|
||||
|
||||
export let item: {
|
||||
image: string
|
||||
title: string
|
||||
descriptions: string[]
|
||||
}
|
||||
export let i: number
|
||||
export let isMobile: boolean
|
||||
</script>
|
||||
|
||||
<li>
|
||||
<div class="image-wrapper">
|
||||
<MedialibImage id={item.image} />
|
||||
<h3>
|
||||
{i + 1}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="title-row">
|
||||
<h4>
|
||||
{item.title}
|
||||
</h4>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={isMobile ? 58 : 72}
|
||||
height={isMobile ? 58 : 72}
|
||||
viewBox="0 0 {isMobile ? 58 : 72} {isMobile ? 58 : 72}
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M0 {isMobile ? 58 : 72}V0L{isMobile ? 58 : 72} {isMobile ? 58 : 72}H0Z"
|
||||
fill="#EB5757"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="descriptions">
|
||||
<ul>
|
||||
{#each item.descriptions as description}
|
||||
<li>
|
||||
{description}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -1,172 +0,0 @@
|
||||
<script
|
||||
lang="ts"
|
||||
context="module"
|
||||
>
|
||||
import "simplebar"
|
||||
import "simplebar/dist/simplebar.css"
|
||||
import ResizeObserver from "resize-observer-polyfill"
|
||||
if (typeof window !== "undefined") window.ResizeObserver = ResizeObserver
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import MedialibImage from "../../widgets/MedialibImage.svelte"
|
||||
import Step from "./Step.svelte"
|
||||
|
||||
export let block: ContentBlock<"steps">
|
||||
console.log("block", block)
|
||||
let innerWidth = 0
|
||||
$: isMobile = innerWidth < 968
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth={innerWidth} />
|
||||
|
||||
{#if block.steps?.horizontal}
|
||||
<div
|
||||
class=" product-preview-list verticalScrollbar"
|
||||
data-simplebar
|
||||
>
|
||||
<ul class="step-blocks horizontal">
|
||||
{#each block.steps.items as item, i}
|
||||
<Step
|
||||
item={item}
|
||||
i={i}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{:else}
|
||||
<ul class="step-blocks">
|
||||
{#each block.steps.items as item, i}
|
||||
<Step
|
||||
item={item}
|
||||
i={i}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
@import "../../../assets/css/variables.less";
|
||||
.step-blocks {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(560px, 1fr));
|
||||
@media @mobile {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
&.horizontal {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
gap: 2.4rem;
|
||||
padding-bottom: 2.4rem;
|
||||
|
||||
& > li {
|
||||
min-width: 500px;
|
||||
flex-grow: 1;
|
||||
height: unset !important;
|
||||
@media @mobile {
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
@media @mobile {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
gap: 2.4rem;
|
||||
& > li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
.image-wrapper {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
aspect-ratio: 1 / 1;
|
||||
h3 {
|
||||
position: absolute;
|
||||
right: 3.6rem;
|
||||
text-align: center;
|
||||
font-size: 5.5rem;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 100%; /* 128px */
|
||||
text-transform: uppercase;
|
||||
font-family: sans-serif;
|
||||
z-index: 4;
|
||||
bottom: 2.4rem;
|
||||
|
||||
@media @mobile {
|
||||
font-size: 3rem;
|
||||
bottom: 2rem;
|
||||
right: 2.4rem;
|
||||
}
|
||||
font-weight: 700;
|
||||
color: transparent;
|
||||
|
||||
display: inline-block;
|
||||
-webkit-text-stroke: 2px #eb5757;
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
margin-top: -3.6rem;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 3;
|
||||
.title-row {
|
||||
height: 3.6rem;
|
||||
max-height: 3.6rem;
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
h4 {
|
||||
background-color: #eb5757;
|
||||
padding: 1.2rem 2.4rem;
|
||||
font-size: 1.6rem;
|
||||
max-height: 100%;
|
||||
width: calc(100% - 3 * 3.6rem - 2rem);
|
||||
color: var(--bg-100);
|
||||
@media @mobile {
|
||||
font-size: 1.2rem;
|
||||
width: calc(100% - 3 * 2.4rem - 2rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
.descriptions {
|
||||
background-color: #eb5757;
|
||||
padding: 0px;
|
||||
flex-grow: 1;
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
li {
|
||||
padding: 2.4rem;
|
||||
|
||||
width: 100%;
|
||||
color: var(--bg-100);
|
||||
border-bottom: 2px solid var(--bg-100);
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,52 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Button from "../../../widgets/Button.svelte"
|
||||
|
||||
export let column: BlockColumn<"cta">
|
||||
const cta = column.cta
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="cta"
|
||||
id="cta"
|
||||
>
|
||||
{#if cta.upperHeadline}
|
||||
<small>
|
||||
{cta.upperHeadline}
|
||||
</small>
|
||||
{/if}
|
||||
|
||||
<h1>
|
||||
{cta.whiteHeadline}
|
||||
{#if cta.headlineArrangement !== "row"} <br />{/if} <em class="red">{cta.redHeadline}</em>
|
||||
</h1>
|
||||
|
||||
<p>{cta.description}</p>
|
||||
<div class="buttons">
|
||||
{#each cta.callToActionButtons as button}
|
||||
<Button button={button} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
#cta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
|
||||
small {
|
||||
font-weight: 700;
|
||||
font-family: Outfit-Bold, sans-serif;
|
||||
color: var(--text-100);
|
||||
}
|
||||
p {
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-100);
|
||||
}
|
||||
.buttons {
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,27 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getVariableNameForChapter } from "../../../../utils"
|
||||
|
||||
export let title: string, type: number, description: string
|
||||
|
||||
//
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h1 style="color: var({getVariableNameForChapter(type)});">
|
||||
{title}
|
||||
</h1>
|
||||
<p style="color: var({getVariableNameForChapter(type)});">
|
||||
{@html description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
h1 {
|
||||
font-size: 4.8rem;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
p {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +0,0 @@
|
||||
<script lang="ts">
|
||||
import LinkList from "../../../widgets/LinkList.svelte"
|
||||
|
||||
export let column: BlockColumn<"text">
|
||||
</script>
|
||||
|
||||
{@html column.text}
|
||||
{#if column.links?.length}
|
||||
<div class="button-wrap gap-m">
|
||||
<!-- Buttons sitzen im Wrapper, sollten vermutlich auch flexibel / repeatable sein, mal nur einen, mal zwei, mal keiner -->
|
||||
<LinkList links={column.links} />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,193 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
import Icon from "../../../widgets/Icon.svelte"
|
||||
|
||||
import { mdiCalendarAccount, mdiCalendarRemove } from "@mdi/js"
|
||||
|
||||
export let dateValue: Date,
|
||||
placeholder: string,
|
||||
id: string,
|
||||
editMode = true
|
||||
let Datepicker: any
|
||||
onMount(() => {
|
||||
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
||||
import("vanillajs-datepicker")
|
||||
.then((module) => {
|
||||
Datepicker = module.Datepicker
|
||||
})
|
||||
.catch((err) => console.error("Failed to load datepicker:", err))
|
||||
}
|
||||
})
|
||||
let datepicker: any
|
||||
let wrapper: HTMLLabelElement
|
||||
function getElementPosition(
|
||||
element: HTMLElement,
|
||||
options: {
|
||||
minSpace: number
|
||||
offsetAbove: number
|
||||
} ={ minSpace: 150, offsetAbove: 75 }
|
||||
): {
|
||||
x: string
|
||||
y: string
|
||||
} {
|
||||
const rect = element.getBoundingClientRect()
|
||||
const scrollTop = window.scrollY || window.pageYOffset
|
||||
const windowHeight = window.innerHeight
|
||||
const topBody = Number(document.body.style.top.split("px")[0])
|
||||
let top = rect.bottom + scrollTop + 5 - topBody
|
||||
|
||||
// Check if there's the specified minimum visible space below the element
|
||||
if (windowHeight - rect.bottom < options.minSpace) {
|
||||
// Place the element above the current element with the specified offset
|
||||
top = rect.top + scrollTop - element.offsetHeight - options.offsetAbove - topBody + 100
|
||||
}
|
||||
return {
|
||||
y: top + "px",
|
||||
x: window.innerWidth - rect.right - 20 + "px",
|
||||
}
|
||||
}
|
||||
|
||||
function respawnElement(element: HTMLElement, deepCopy: boolean = true) {
|
||||
var newElement = element.cloneNode(deepCopy)
|
||||
element.parentNode.replaceChild(newElement, element)
|
||||
return newElement
|
||||
}
|
||||
|
||||
function hideAllModals() {
|
||||
const dateModal = document.getElementById("dateModal")
|
||||
dateModal.style.display = "none"
|
||||
respawnElement(dateModal, false)
|
||||
}
|
||||
|
||||
function outSideDateModalClickListener(e: MouseEvent) {
|
||||
const dateModal = document.getElementById("dateModal")
|
||||
if (
|
||||
!dateModal.contains(e.target as Node) &&
|
||||
e.target !== document &&
|
||||
!(e.target as Element).classList.contains("datepicker-cell")
|
||||
) {
|
||||
hideDatepicker()
|
||||
}
|
||||
}
|
||||
function dateChangeListener(e: any) {
|
||||
dateValue = new Date(e.detail.date)
|
||||
// element has data-for="error-message-birthday" attribute, get it by that
|
||||
const el = document.querySelector(`[data-for="error-message-${id}]`)
|
||||
if (el) {
|
||||
el.remove()
|
||||
}
|
||||
hideDatepicker()
|
||||
}
|
||||
|
||||
function hideDatepicker() {
|
||||
const dateModal = document.getElementById("dateModal")
|
||||
dateModal.style.display = "none"
|
||||
datepicker?.destroy()
|
||||
|
||||
dateModal.removeEventListener("changeDate", dateChangeListener)
|
||||
document.removeEventListener("click", outSideDateModalClickListener)
|
||||
}
|
||||
|
||||
function showDatepicker() {
|
||||
let dateModal = document.getElementById("dateModal")
|
||||
hideAllModals()
|
||||
if (datepicker) hideDatepicker()
|
||||
dateModal = document.getElementById("dateModal")
|
||||
dateModal.style.display = "block"
|
||||
dateModal.style.top = getElementPosition(wrapper, { minSpace: 300, offsetAbove: 300 }).y
|
||||
dateModal.style.right = getElementPosition(wrapper, { minSpace: 300, offsetAbove: 300 }).x
|
||||
datepicker = new Datepicker(dateModal, {})
|
||||
if (dateValue) datepicker.setDate(dateValue)
|
||||
dateModal.addEventListener("changeDate", dateChangeListener)
|
||||
document.addEventListener("click", outSideDateModalClickListener)
|
||||
}
|
||||
</script>
|
||||
|
||||
<label
|
||||
bind:this={wrapper}
|
||||
id={id}
|
||||
class="custom-date-picker"
|
||||
>
|
||||
<span class:hasValue={!!dateValue || !editMode}>{placeholder}</span>
|
||||
{#if editMode}
|
||||
<div class="wrapper">
|
||||
{#if dateValue}
|
||||
<button
|
||||
data-cy="custom-date-picker-text"
|
||||
class="btn text transparent"
|
||||
on:click|preventDefault|stopPropagation={showDatepicker}
|
||||
>
|
||||
{new Date(dateValue).toLocaleDateString("de-DE")}
|
||||
</button>
|
||||
{:else}
|
||||
<div style="opacity: 0.75;">{placeholder}</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
data-cy="custom-date-picker-icon"
|
||||
aria-label="Toggle datepicker"
|
||||
class="btn transparent icon"
|
||||
on:click|preventDefault|stopPropagation={() => {
|
||||
hideAllModals()
|
||||
if (dateValue) dateValue = null
|
||||
else showDatepicker()
|
||||
}}
|
||||
>
|
||||
{#if !dateValue}
|
||||
<Icon
|
||||
path={mdiCalendarAccount}
|
||||
width="24px"
|
||||
height="24px"
|
||||
/>
|
||||
{:else}
|
||||
<Icon
|
||||
path={mdiCalendarRemove}
|
||||
width="24px"
|
||||
height="24px"
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
</div>{:else if !editMode}
|
||||
{#if dateValue}
|
||||
<div class="no-input">
|
||||
{new Date(dateValue).toLocaleDateString("de-DE")}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="no-input">-</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<style lang="less">
|
||||
@import "vanillajs-datepicker/css/datepicker.css";
|
||||
:global .hiddenscript {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
}
|
||||
.custom-date-picker:global {
|
||||
min-height: 60px;
|
||||
flex-grow: 1;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
justify-content: space-between;
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.btn.icon,
|
||||
.btn.text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-invers-100);
|
||||
|
||||
&.text {
|
||||
color: var(--text-invers-100);
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,342 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiCameraOutline, mdiInformationOutline, mdiMovieOpenOutline } from "@mdi/js"
|
||||
import { apiBaseURL } from "../../../../../config"
|
||||
import Icon from "../../../widgets/Icon.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { newNotification } from "../../../../store"
|
||||
import MedialibImage from "../../../widgets/MedialibImage.svelte"
|
||||
import MedialibFile from "../../../widgets/MedialibFile.svelte"
|
||||
import Modal from "../../../Modal.svelte"
|
||||
import { tooltip } from "../../../../functions/utils"
|
||||
const dispatch = createEventDispatcher()
|
||||
export let id,
|
||||
classList: string = "",
|
||||
placeholder: string = "",
|
||||
disabled: boolean = false,
|
||||
helperText: string = "",
|
||||
value: FileField = null,
|
||||
collectionName: string,
|
||||
entryId: string,
|
||||
type: "image" | "video" = "image",
|
||||
noDelete: boolean = false,
|
||||
imgIsData = false
|
||||
let file: File, fileInput: HTMLInputElement
|
||||
let showApproveModal = false
|
||||
function onChange(forceUpload = false) {
|
||||
file = fileInput?.files[0]
|
||||
const maxFileSize = 150 * 1024 * 1024 // 150 MB in Bytes
|
||||
const supportedMimeTypes = [
|
||||
// Images
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/bmp",
|
||||
"image/tiff",
|
||||
"image/svg+xml",
|
||||
"video/mp4",
|
||||
"video/webm",
|
||||
"video/ogg",
|
||||
"video/quicktime",
|
||||
]
|
||||
|
||||
const supportedFormats = `
|
||||
Unterstützte Bildformate: JPEG, PNG, GIF, WebP, BMP, TIFF, SVG.
|
||||
Unterstützte Videoformate: MP4, WebM, Ogg.
|
||||
`
|
||||
|
||||
const excludedMimeTypes = ["video/quicktime"]
|
||||
const excludedExtensions = ["mov"]
|
||||
|
||||
if (file) {
|
||||
const fileExtension = file?.name?.split(".")?.pop()?.toLowerCase()
|
||||
|
||||
if (
|
||||
(excludedMimeTypes.includes(file.type) || excludedExtensions.includes(fileExtension)) &&
|
||||
forceUpload !== true
|
||||
) {
|
||||
showApproveModal = true
|
||||
return
|
||||
}
|
||||
if (!supportedMimeTypes.includes(file.type)) {
|
||||
newNotification({
|
||||
class: "error",
|
||||
html: `Der Dateityp - "${file.type} wird nicht unterstützt. Bitte wähle eine unterstützte Datei aus. ${supportedFormats}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const isSupported = supportedMimeTypes.includes(file.type)
|
||||
|
||||
if (!isSupported) {
|
||||
newNotification({
|
||||
class: "error",
|
||||
html: `Der Dateityp "${file.type} wird nicht unterstützt. Bitte wähle eine unterstützte Datei aus. ${supportedFormats}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
const reader = new FileReader()
|
||||
if (file.size > maxFileSize) {
|
||||
newNotification({
|
||||
class: "error",
|
||||
html: "Die Datei ist zu groß. Bitte wähle eine Datei unter 150 MB aus bzw. komprimiere die ausgewählte Datei.",
|
||||
})
|
||||
return
|
||||
}
|
||||
reader.addEventListener("load", function () {
|
||||
imgIsData = true
|
||||
const objectURL = URL.createObjectURL(file)
|
||||
value ={
|
||||
path: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
src: objectURL,
|
||||
}
|
||||
rerender = !rerender
|
||||
dispatch("change")
|
||||
})
|
||||
// @ts-ignore
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
}
|
||||
|
||||
let highlight = false
|
||||
let focused = false
|
||||
let rerender = false
|
||||
</script>
|
||||
|
||||
<label class="file file-input-label">
|
||||
<div class="headline">
|
||||
<span class:hasValue={true}>
|
||||
{placeholder}
|
||||
|
||||
{#if helperText}
|
||||
<div
|
||||
use:tooltip={{
|
||||
content: helperText,
|
||||
}}
|
||||
class="helperText"
|
||||
>
|
||||
<Icon path={mdiInformationOutline} />
|
||||
</div>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<Icon
|
||||
path={type == 'image' ? mdiCameraOutline : mdiMovieOpenOutline}
|
||||
size="24px"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="file-input"
|
||||
class:focused={focused}
|
||||
class:highlight-container={highlight}
|
||||
role="button"
|
||||
aria-label="File upload"
|
||||
tabindex={0}
|
||||
on:dragenter|preventDefault={() => {
|
||||
highlight = true
|
||||
}}
|
||||
on:dragover|preventDefault={() => {
|
||||
highlight = true
|
||||
}}
|
||||
on:dragleave|preventDefault={() => {
|
||||
highlight = false
|
||||
}}
|
||||
on:drop|preventDefault={(e) => {
|
||||
highlight = false
|
||||
fileInput = e.dataTransfer
|
||||
onChange()
|
||||
}}
|
||||
>
|
||||
<div class="fileContainer">
|
||||
<input
|
||||
type="file"
|
||||
bind:this={fileInput}
|
||||
on:change|stopPropagation={onChange}
|
||||
readonly={disabled}
|
||||
id={id}
|
||||
accept={type === 'image' ? 'image/*' : 'video/*'}
|
||||
on:click|stopPropagation
|
||||
/>
|
||||
|
||||
<div
|
||||
class="filePreview"
|
||||
class:hasChildElement={!!value}
|
||||
>
|
||||
{#key rerender}
|
||||
{#if value}
|
||||
{#if type == "image"}
|
||||
{#if typeof value === "string"}
|
||||
<MedialibImage
|
||||
id={value}
|
||||
filter="l"
|
||||
/>
|
||||
{:else if imgIsData}
|
||||
<img
|
||||
src={value.src}
|
||||
alt={value.path}
|
||||
/>
|
||||
{:else if typeof value.src === "string" && value.src.includes(";base64,")}
|
||||
<img
|
||||
src={value.src}
|
||||
alt={value.path}
|
||||
/>
|
||||
{:else if typeof value.src === "string" && !value.src.match(/^https?:\/\//)}
|
||||
<img
|
||||
src={`${apiBaseURL}${collectionName}/${entryId}/${value.src}`}
|
||||
alt={value.path}
|
||||
/>
|
||||
{/if}
|
||||
{:else if type == "video"}
|
||||
{#if typeof value == "string"}
|
||||
<MedialibFile
|
||||
id={value}
|
||||
let:entry
|
||||
let:src
|
||||
>
|
||||
<video>
|
||||
<track kind="captions" />
|
||||
<source
|
||||
src={src}#t=0.1"
|
||||
type="video/mp4"
|
||||
/>
|
||||
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</MedialibFile>
|
||||
{:else}
|
||||
<video
|
||||
controls
|
||||
preload="metadata"
|
||||
>
|
||||
<track kind="captions" />
|
||||
<source
|
||||
src={value.src}#t=0.1"
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
{/if}
|
||||
{/if}
|
||||
<div>
|
||||
{#if noDelete}
|
||||
<button
|
||||
aria-label="Datei hochladen"
|
||||
class="delete cta primary"
|
||||
on:click|stopPropagation|preventDefault={() => {
|
||||
// change file by clicking input
|
||||
fileInput.click()
|
||||
}}
|
||||
>
|
||||
Austauschen
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
aria-label="Datei löschen"
|
||||
class="delete cta primary"
|
||||
on:click|stopPropagation|preventDefault={() => {
|
||||
value = null
|
||||
dispatch('change')
|
||||
}}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
{#if showApproveModal}
|
||||
<Modal
|
||||
show={true}
|
||||
on:close={() => {
|
||||
showApproveModal = false
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="title">Dateiformat Warnung</svelte:fragment>
|
||||
<p>
|
||||
Deine Datei wird von einigen Browsern, insbesondere von Google Chrome, nur eingeschränkt unterstützt. Wenn
|
||||
du die Datei dennoch hochladen möchtest, klicke auf "Fortfahren". Andernfalls empfehlen wir, das Video
|
||||
zunächst in eine MP4-Datei zu konvertieren. Dafür stehen zahlreiche kostenlose Online-Dienste zur Verfügung.
|
||||
</p>
|
||||
<div
|
||||
slot="footer"
|
||||
class="file-warning-footer"
|
||||
>
|
||||
<button
|
||||
class="btn cta primary"
|
||||
on:click={() => {
|
||||
showApproveModal = false
|
||||
}}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
class="btn cta secondary"
|
||||
on:click={() => {
|
||||
showApproveModal = false
|
||||
onChange(true)
|
||||
}}>Fortfahren</button
|
||||
>
|
||||
</div>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style
|
||||
lang="less"
|
||||
global
|
||||
>
|
||||
.file-input-label {
|
||||
.headline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
span {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 0px !important;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
.helperText {
|
||||
color: var(--bg-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fileContainer {
|
||||
height: fit-content;
|
||||
padding: 0.6rem 0px !important;
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
.filePreview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
min-height: 15px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
&.hasChildElement {
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
img,
|
||||
video {
|
||||
height: 180px;
|
||||
max-height: 180px;
|
||||
width: 100% !important;
|
||||
background-color: var(--bg-100);
|
||||
object-fit: contain !important;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,143 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { mdiInformationOutline } from "@mdi/js"
|
||||
import { tooltip } from "../../../../functions/utils"
|
||||
import Icon from "../../../widgets/Icon.svelte"
|
||||
|
||||
export let value: any,
|
||||
id,
|
||||
classList: string = "",
|
||||
onChange: (e: Event) => void,
|
||||
type: "password" | "text" | "number" | "checkbox" | "noInput" | "textarea" | "select" = "text",
|
||||
placeholder: string = "",
|
||||
disabled: boolean = false,
|
||||
name = "",
|
||||
options: { name: string; value: string }[] = [],
|
||||
selectedOptionIndex = 0,
|
||||
helperText = ""
|
||||
|
||||
$: hasValue = Boolean(value)
|
||||
|
||||
const attributes ={
|
||||
id,
|
||||
class: classList,
|
||||
placeholder,
|
||||
name: name || id,
|
||||
disabled: !!disabled,
|
||||
}
|
||||
</script>
|
||||
|
||||
<label
|
||||
style=""
|
||||
class:textarea={type == 'textarea'}
|
||||
class:checkbox={type == 'checkbox'}
|
||||
>
|
||||
{#if type !== "checkbox"}
|
||||
<span class:hasValue={hasValue || type === 'noInput'}>{placeholder}</span>
|
||||
{/if}
|
||||
{#if type == "checkbox"}
|
||||
<input
|
||||
type="checkbox"
|
||||
{...attributes}
|
||||
on:change={onChange}
|
||||
bind:checked={value}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="checkit-span"
|
||||
aria-label="Toggle checkbox"
|
||||
tabindex={0}
|
||||
on:click={() => {
|
||||
value = !value
|
||||
setTimeout(() => {
|
||||
const event = new Event('change', { bubbles: true })
|
||||
document.getElementById(id)?.dispatchEvent(event)
|
||||
}, 10)
|
||||
}}
|
||||
on:keydown={(e) => {}}></button>
|
||||
{/if}
|
||||
{#if type == "password"}
|
||||
<input
|
||||
{...attributes}
|
||||
on:blur={onChange}
|
||||
bind:value={value}
|
||||
on:change={onChange}
|
||||
type="password"
|
||||
class="sentry-mask"
|
||||
/>
|
||||
{/if}
|
||||
{#if type == "text"}
|
||||
<input
|
||||
on:blur={onChange}
|
||||
{...attributes}
|
||||
bind:value={value}
|
||||
on:change={onChange}
|
||||
/>
|
||||
{/if}
|
||||
{#if type == "textarea"}
|
||||
<textarea
|
||||
on:blur={onChange}
|
||||
{...attributes}
|
||||
bind:value={value}
|
||||
on:change={onChange}></textarea>
|
||||
{/if}
|
||||
{#if type == "number"}
|
||||
<input
|
||||
on:blur={onChange}
|
||||
type="number"
|
||||
{...attributes}
|
||||
bind:value={value}
|
||||
on:change={onChange}
|
||||
/>
|
||||
{/if}
|
||||
{#if type == "noInput"}
|
||||
<div class="no-input">
|
||||
{#if id.includes("pass")}
|
||||
************
|
||||
{:else}
|
||||
{Boolean(value) ? value : "-"}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if type == "select"}
|
||||
<select
|
||||
on:change={onChange}
|
||||
{...attributes}
|
||||
bind:value={value}
|
||||
>
|
||||
{#each options as option, index}
|
||||
<option
|
||||
value={option.value}
|
||||
selected={index === selectedOptionIndex}
|
||||
>
|
||||
{option.name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
{#if type !== "checkbox"}
|
||||
<span class="underline"></span>
|
||||
{/if}
|
||||
{#if helperText}
|
||||
<div
|
||||
use:tooltip={{
|
||||
content: helperText,
|
||||
}}
|
||||
class="helperText"
|
||||
>
|
||||
<Icon path={mdiInformationOutline} />
|
||||
</div>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<style lang="less">
|
||||
.checkbox {
|
||||
width: 1.2rem !important;
|
||||
}
|
||||
.helperText {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
color: var(--bg-100);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
</style>
|
||||
@@ -1,102 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Select from "svelte-select/Select.svelte"
|
||||
export let options: { label: string; value: string }[] = [],
|
||||
selectedOption: { label: string; value: string } = null,
|
||||
value = "",
|
||||
placeholder: string,
|
||||
clearable = false,
|
||||
editMode = true
|
||||
|
||||
$: value = selectedOption?.value
|
||||
$: hasValue = Boolean(value)
|
||||
let focused = false
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-to-interactive-role -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click|stopPropagation={() => {
|
||||
focused = true
|
||||
}}
|
||||
>
|
||||
<span class:hasValue={hasValue || !editMode}>{placeholder}</span>
|
||||
{#if editMode}
|
||||
<Select
|
||||
bind:items={options}
|
||||
inputAttributes={{ autocomplete: 'on' }}
|
||||
placeholder={placeholder}
|
||||
showChevron={true}
|
||||
bind:focused={focused}
|
||||
clearable={clearable}
|
||||
hideEmptyState={true}
|
||||
searchable={true}
|
||||
bind:value={selectedOption}
|
||||
on:clear={() => (selectedOption = null)}
|
||||
/>
|
||||
{:else if !editMode}
|
||||
<div class="no-input">{selectedOption?.label || "-"}</div>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<style lang="less">
|
||||
:global .svelte-select {
|
||||
background-color: var(--neutral-white) !important;
|
||||
resize: none !important;
|
||||
.value-container {
|
||||
input {
|
||||
padding-top: 20px;
|
||||
}
|
||||
.selected-item {
|
||||
padding-top: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-item {
|
||||
background-color: rgba(0, 0, 0, 0.1) !important;
|
||||
border-radius: 3px !important;
|
||||
}
|
||||
|
||||
.selected-item {
|
||||
margin-left: 3px;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 100%;
|
||||
font-size: 1rem !important;
|
||||
border: 0px solid black !important;
|
||||
}
|
||||
* {
|
||||
border-left: 0px solid black !important;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
.item {
|
||||
&.active {
|
||||
background-color: transparent !important;
|
||||
color: black !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.indicators {
|
||||
color: #ccc;
|
||||
|
||||
& > * {
|
||||
border-left: 1px solid #ccc !important;
|
||||
height: 40px !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.clear-select {
|
||||
color: var(--primary-200) !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,66 +0,0 @@
|
||||
import type { SvelteComponent } from "svelte"
|
||||
import PredefinedBlock from "./PredefinedBlock.svelte"
|
||||
import Columns from "./Columns.svelte"
|
||||
import ProductListPreviewWidget from "../product/ProductListPreviewWidget.svelte"
|
||||
import NewsletterRow from "./NewsletterRow.svelte"
|
||||
import SplittedHomepage from "./SplittedHomepage.svelte"
|
||||
import ImproveYourselfDescription from "./ImproveYourselfDescription.svelte"
|
||||
import HomepageRow from "./HomepageRow.svelte"
|
||||
import ChapterPreview from "./ChapterPreview/ChapterPreview.svelte"
|
||||
import PreviewCard from "./RatingPreview/PreviewCard.svelte"
|
||||
import Steps from "./Steps.svelte"
|
||||
|
||||
const blocks: {
|
||||
[key: string]: {
|
||||
sectionClass: string
|
||||
component: typeof SvelteComponent<{ block?: ContentBlock }>
|
||||
}
|
||||
} = {
|
||||
default: {
|
||||
sectionClass: "",
|
||||
component: Columns,
|
||||
},
|
||||
columns: {
|
||||
sectionClass: "",
|
||||
component: Columns,
|
||||
},
|
||||
|
||||
predefinedBlock: {
|
||||
sectionClass: "predefined-block",
|
||||
component: PredefinedBlock,
|
||||
},
|
||||
productSlider: {
|
||||
sectionClass: "",
|
||||
component: ProductListPreviewWidget,
|
||||
},
|
||||
NewsletterRow: {
|
||||
sectionClass: "newsletter-row",
|
||||
component: NewsletterRow,
|
||||
},
|
||||
splittedHomepage: {
|
||||
sectionClass: "",
|
||||
component: SplittedHomepage,
|
||||
},
|
||||
improveYourselfDescription: {
|
||||
sectionClass: "ImproveYourselfDescription",
|
||||
component: ImproveYourselfDescription,
|
||||
},
|
||||
homepage: {
|
||||
sectionClass: "homepageRow",
|
||||
component: HomepageRow,
|
||||
},
|
||||
selfImprovementChapterPreview: {
|
||||
sectionClass: "chapterPreview",
|
||||
component: ChapterPreview,
|
||||
},
|
||||
ratingPreview: {
|
||||
sectionClass: "ratingPreview",
|
||||
component: PreviewCard,
|
||||
},
|
||||
stepNr: {
|
||||
sectionClass: "stepNr",
|
||||
component: Steps,
|
||||
},
|
||||
}
|
||||
|
||||
export default blocks
|
||||
@@ -7,7 +7,7 @@
|
||||
<Modal
|
||||
show={true}
|
||||
size="sm"
|
||||
on:close={(e) => {
|
||||
on:close={() => {
|
||||
actionApproval.set(null)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="cta {button.ctaType == 0 ? 'primary' : 'secondary'}
|
||||
class={`cta ${button.ctaType === 0 ? 'primary' : 'secondary'}`}
|
||||
aria-label={button.buttonText}
|
||||
>
|
||||
<a
|
||||
@@ -13,8 +13,8 @@
|
||||
href={button.page}
|
||||
target={button.buttonTarget}
|
||||
>
|
||||
{button.buttonText}</a
|
||||
>
|
||||
{button.buttonText}
|
||||
</a>
|
||||
</button>
|
||||
|
||||
<style lang="less">
|
||||
|
||||
@@ -41,17 +41,17 @@
|
||||
<slot />
|
||||
{:else}
|
||||
<div
|
||||
style="display: flex;
|
||||
style={{`display: flex;
|
||||
justify-content: center;
|
||||
align-items: {positions[textPosition]};
|
||||
background-image: url({backgroundUrl});
|
||||
align-items: ${positions[textPosition]};
|
||||
background-image: url(${backgroundUrl});
|
||||
background-size: cover;
|
||||
width: 100%;
|
||||
align-self: stretch;
|
||||
flex-grow: 1;
|
||||
"
|
||||
`}}
|
||||
>
|
||||
<div style="background-color: rgba(255,255,255,0.7); padding: 20px; background: {background}; width: 100%;">
|
||||
<div style={{`background-color: rgba(255,255,255,0.7); padding: 20px; background: ${background}; width: 100%;`}}>
|
||||
<p>Cookie ist nicht aktiviert. Bitte aktiviere ihn.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,52 +1,105 @@
|
||||
<script lang="ts">
|
||||
import { spaLink } from "../../actions"
|
||||
import { createContactRequest } from "../../functions/CommerceAPIs/tibiEndpoints/helpCenter"
|
||||
import { newNotification } from "../../store"
|
||||
import { cryptchaSolutionId, initCryptcha, cryptchaSolution } from "../../utils"
|
||||
import Input from "../pagebuilder/blocks/form/Input.svelte"
|
||||
import { onChange, validateEmail, validateField, validateInput } from "../pagebuilder/profile/helper"
|
||||
export let email = "",
|
||||
name = "",
|
||||
description = "",
|
||||
title = "Es sind noch Fragen offen?"
|
||||
import { apiBaseURL } from "../../../config"
|
||||
|
||||
export let email = ""
|
||||
export let name = ""
|
||||
export let description = ""
|
||||
export let title = "Es sind noch Fragen offen?"
|
||||
|
||||
let contactRequestSent = false
|
||||
const contactRequest ={
|
||||
email: email,
|
||||
name: name,
|
||||
description: description,
|
||||
usageTerms: false,
|
||||
_s: "",
|
||||
_sId: "",
|
||||
}
|
||||
function validateContactRequest(): boolean {
|
||||
const validations = [
|
||||
{ id: "email", value: contactRequest.email, validator: validateEmail },
|
||||
{ id: "name", value: contactRequest.name, validator: validateInput },
|
||||
{ id: "description", value: contactRequest.description, validator: validateInput },
|
||||
]
|
||||
let isValid = true
|
||||
|
||||
for (const { id, value, validator } of validations) {
|
||||
// @ts-ignore
|
||||
if (!validateField(id, value, validator)) isValid = false
|
||||
}
|
||||
if (!contactRequest.usageTerms) {
|
||||
newNotification({
|
||||
class: "error",
|
||||
html: "Bitte akzeptiere die Datenschutzerklärung.",
|
||||
})
|
||||
isValid = false
|
||||
}
|
||||
return isValid
|
||||
}
|
||||
let submitting = false
|
||||
let cryptchaEl: HTMLDivElement
|
||||
|
||||
let cryptchaInitialized = false
|
||||
|
||||
const contactRequest = {
|
||||
email,
|
||||
name,
|
||||
description,
|
||||
usageTerms: false,
|
||||
}
|
||||
|
||||
const emailPattern = /^(?:[a-zA-Z0-9_'^&%+\-])+(?:\.(?:[a-zA-Z0-9_'^&%+\-])+)*@(?:(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|\[(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\]$/
|
||||
|
||||
const validate = () => {
|
||||
if (!emailPattern.test(contactRequest.email || "")) {
|
||||
newNotification({ class: "error", html: "Bitte gib eine gültige E-Mail-Adresse an." })
|
||||
return false
|
||||
}
|
||||
|
||||
if (!contactRequest.name?.trim()) {
|
||||
newNotification({ class: "error", html: "Bitte gib deinen Namen an." })
|
||||
return false
|
||||
}
|
||||
|
||||
if (!contactRequest.description?.trim()) {
|
||||
newNotification({ class: "error", html: "Bitte beschreibe dein Anliegen." })
|
||||
return false
|
||||
}
|
||||
|
||||
if (!contactRequest.usageTerms) {
|
||||
newNotification({ class: "error", html: "Bitte akzeptiere die Datenschutzerklärung." })
|
||||
return false
|
||||
}
|
||||
|
||||
if (!$cryptchaSolution || !$cryptchaSolutionId) {
|
||||
newNotification({ class: "error", html: "Bitte löse die Sicherheitsabfrage." })
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const updateField = (field: keyof typeof contactRequest) => (event: Event) => {
|
||||
const target = event.currentTarget as HTMLInputElement | HTMLTextAreaElement
|
||||
if (field === "usageTerms") {
|
||||
contactRequest[field] = (target as HTMLInputElement).checked as never
|
||||
} else {
|
||||
contactRequest[field] = target.value as never
|
||||
}
|
||||
}
|
||||
|
||||
$: if (cryptchaEl && !cryptchaInitialized && contactRequest.name && contactRequest.email) {
|
||||
initCryptcha(cryptchaEl)
|
||||
cryptchaInitialized = true
|
||||
}
|
||||
|
||||
const submitContactRequest = async () => {
|
||||
if (!validate() || submitting) return
|
||||
|
||||
submitting = true
|
||||
try {
|
||||
const response = await fetch(`${apiBaseURL}support-requests`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...contactRequest,
|
||||
solution: $cryptchaSolution,
|
||||
solutionId: $cryptchaSolutionId,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed with status ${response.status}`)
|
||||
}
|
||||
|
||||
newNotification({ class: "success", html: "Deine Anfrage wurde erfolgreich versendet!" })
|
||||
contactRequestSent = true
|
||||
} catch (error) {
|
||||
console.error("Failed to submit contact request", error)
|
||||
newNotification({
|
||||
class: "error",
|
||||
html: "Die Anfrage konnte nicht gesendet werden. Bitte versuche es später erneut.",
|
||||
})
|
||||
} finally {
|
||||
submitting = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="small-wrapper contact">
|
||||
@@ -55,91 +108,61 @@
|
||||
<small>Kundenservice</small>
|
||||
<h2>{title}</h2>
|
||||
<p>
|
||||
Konnte unser <a
|
||||
href="/helpCenter"
|
||||
use:spaLink>FAQ</a
|
||||
> dir nicht weiterhelfen? Kein Problem! Kontaktiere uns direkt, und unser Kundenservice steht dir gerne zur
|
||||
Verfügung.
|
||||
Konnte unser <a href="/helpCenter" use:spaLink>FAQ</a> dir nicht weiterhelfen? Kein Problem! Kontaktiere uns direkt, und unser Kundenservice steht dir gerne zur Verfügung.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="contactForm"
|
||||
on:submit|preventDefault|stopPropagation={() => {
|
||||
if (validateContactRequest()) {
|
||||
createContactRequest(contactRequest, $cryptchaSolutionId, $cryptchaSolution).then(() => {
|
||||
newNotification({
|
||||
class: 'success',
|
||||
html: 'Deine Anfrage wurde erfolgreich versendet!',
|
||||
})
|
||||
contactRequestSent = true
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<form class="contactForm" on:submit|preventDefault|stopPropagation={submitContactRequest}>
|
||||
{#if contactRequestSent}
|
||||
<section class="row">
|
||||
<p>
|
||||
Deine Anfrage wurde erfolgreich versendet! Üblicherweise werden wir uns in den nächsten 5
|
||||
Werktagen bei dir melden!
|
||||
</p>
|
||||
<p>Danke! Wir melden uns innerhalb der nächsten Werktage bei dir.</p>
|
||||
</section>
|
||||
{:else}
|
||||
<section class="row">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="E-Mail"
|
||||
bind:value={contactRequest.email}
|
||||
id="email"
|
||||
onChange={onChange}
|
||||
helperText="Wir benötigen deine E-Mail Adresse, um dir antworten zu können."
|
||||
<label for="support-email">E-Mail</label>
|
||||
<input
|
||||
id="support-email"
|
||||
type="email"
|
||||
value={contactRequest.email}
|
||||
on:input={updateField("email")}
|
||||
required
|
||||
/>
|
||||
</section>
|
||||
<section class="row">
|
||||
<Input
|
||||
<label for="support-name">Name</label>
|
||||
<input
|
||||
id="support-name"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
bind:value={contactRequest.name}
|
||||
id="name"
|
||||
onChange={onChange}
|
||||
value={contactRequest.name}
|
||||
on:input={updateField("name")}
|
||||
required
|
||||
/>
|
||||
</section>
|
||||
<section class="row">
|
||||
<Input
|
||||
type="textarea"
|
||||
placeholder="Beschreibung"
|
||||
bind:value={contactRequest.description}
|
||||
id="description"
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label for="support-description">Beschreibung</label>
|
||||
<textarea
|
||||
id="support-description"
|
||||
rows={4}
|
||||
value={contactRequest.description}
|
||||
on:input={updateField("description")}
|
||||
required
|
||||
></textarea>
|
||||
</section>
|
||||
<section
|
||||
class="row"
|
||||
style="flex-direction: row !important;"
|
||||
>
|
||||
<Input
|
||||
<section class="row checkbox-row">
|
||||
<input
|
||||
id="support-usageTerms"
|
||||
type="checkbox"
|
||||
bind:value={contactRequest.usageTerms}
|
||||
onChange={onChange}
|
||||
id="usageTerms"
|
||||
checked={contactRequest.usageTerms}
|
||||
on:change={updateField("usageTerms")}
|
||||
required
|
||||
/>
|
||||
<p>
|
||||
Ich habe die <a
|
||||
use:spaLink
|
||||
href="/datenschutz">Datenschutzerklärung</a
|
||||
> gelesen und akzeptiere Sie.
|
||||
Ich habe die <a use:spaLink href="/datenschutz">Datenschutzerklärung</a> gelesen und akzeptiere sie.
|
||||
</p>
|
||||
</section>
|
||||
<section
|
||||
class="row"
|
||||
style="flex-direction: row !important;"
|
||||
>
|
||||
<button
|
||||
class="cta primary"
|
||||
type="submit"
|
||||
disabled={!$cryptchaSolution}
|
||||
>
|
||||
Absenden
|
||||
<section class="row">
|
||||
<button class="cta primary" type="submit" disabled={submitting}>
|
||||
{submitting ? "Wird gesendet…" : "Absenden"}
|
||||
</button>
|
||||
</section>
|
||||
<section class="row">
|
||||
@@ -152,48 +175,56 @@
|
||||
|
||||
<style lang="less">
|
||||
@import "../../assets/css/variables.less";
|
||||
|
||||
.small-wrapper.contact {
|
||||
display: flex;
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
.inner-small-wrapper {
|
||||
display: grid;
|
||||
overflow: visible;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2.4rem;
|
||||
@media @mobile {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.leftern {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
small {
|
||||
font-family: Outfit-Bold;
|
||||
font-size: 0.7rem;
|
||||
line-height: 0.7rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
p {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
}
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
.row {
|
||||
padding: 0px !important;
|
||||
}
|
||||
button {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
.inner-small-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 2.4rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.leftern {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.checkbox-row {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
|
||||
input {
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
padding: 0.6rem 0.8rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
border-radius: 6px;
|
||||
font: inherit;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,18 +2,33 @@
|
||||
import { spaLink } from "../../actions"
|
||||
import MedialibFile from "./MedialibFile.svelte"
|
||||
|
||||
export let links: BlockLink[]
|
||||
export let links: BlockLink[] = []
|
||||
|
||||
const resolveClass = (style?: string) => {
|
||||
if (!style) return "cta"
|
||||
return `cta btn-${style}`
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each links || [] as link}
|
||||
{#if link.url || !link.file}
|
||||
<a use:spaLink href={link.url} class="cta btn-{link.style} {link.style} target={link.target || undefined}
|
||||
>{link.text}</a
|
||||
{#each links || [] as link (link.text)}
|
||||
{#if link.url}
|
||||
<a
|
||||
use:spaLink
|
||||
href={link.url}
|
||||
class={resolveClass(link.style)}
|
||||
target={link.target || "_self"}
|
||||
>
|
||||
{:else}
|
||||
{link.text}
|
||||
</a>
|
||||
{:else if link.file}
|
||||
<MedialibFile id={link.file} let:src>
|
||||
<a href={src} class="cta btn-{link.style} {link.style} target={link.target || undefined}>{link.text}</a
|
||||
<a
|
||||
href={src}
|
||||
class={resolveClass(link.style)}
|
||||
target={link.target || "_blank"}
|
||||
>
|
||||
{link.text}
|
||||
</a>
|
||||
</MedialibFile>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { Circle } from "svelte-loading-spinners"
|
||||
import { loadingStore } from "../../store"
|
||||
|
||||
export let active = false,
|
||||
styles: string = "",
|
||||
progress = false
|
||||
export let active = false
|
||||
export let styles: string = ""
|
||||
export let progress = false
|
||||
export let loaded = 0
|
||||
export let total = 0
|
||||
|
||||
$: percentage = total > 0 ? Math.round((loaded / total) * 100) : 0
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -20,7 +23,7 @@
|
||||
/>
|
||||
|
||||
{#if progress}
|
||||
<span>{Math.round(($loadingStore.loaded / $loadingStore.total) * 100) || 0} %</span>
|
||||
<span>{percentage} %</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
{#if !$openModal || ($openModal && force)}
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-to-interactive-role -->
|
||||
<li
|
||||
class="notification {n.class}
|
||||
class={`notification ${n.class}`}
|
||||
use:animateIt
|
||||
on:click={() => {
|
||||
removeNotification(n)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { get, writable } from "svelte/store"
|
||||
import { setUser } from "../sentry"
|
||||
import { baseURL } from "../config"
|
||||
/*********** location **************************/
|
||||
|
||||
@@ -126,18 +125,5 @@ location.subscribe((l) => {
|
||||
})
|
||||
})
|
||||
export const openModal = writable<boolean>(false)
|
||||
export const overlays = writable<Overlay[]>([])
|
||||
export const categories = writable<BigCommerceCategory[]>([])
|
||||
export const login = writable<Login | null>(null)
|
||||
login.subscribe((l) => {
|
||||
setUser({
|
||||
username: l?.customer?.username || l?.tokenData?.tibiId || "anonymous",
|
||||
email: l?.customer?.email || l?.tokenData?.email || "anonymous",
|
||||
})
|
||||
})
|
||||
export const modules = writable<Module[]>([])
|
||||
|
||||
|
||||
export const backgroundImages = writable<{ standard?: string }>({})
|
||||
export const isMobile = writable<boolean>(false)
|
||||
export const actionApproval = writable<ActionApproval | null>(null)
|
||||
|
||||
@@ -1,62 +1,110 @@
|
||||
<script lang="ts">
|
||||
import { getCachedEntry } from "../api"
|
||||
import NotFound from "./NotFound.svelte"
|
||||
import Breadcrumbs from "../lib/components/pagebuilder/Breadcrumbs.svelte"
|
||||
import ContentBlock from "../lib/components/pagebuilder/ContentBlock.svelte"
|
||||
import Loader from "../lib/components/pagebuilder/Loader.svelte"
|
||||
import Index from "../lib/components/pagebuilder/SEO/Index.svelte"
|
||||
|
||||
export let location: LocationStore = undefined,
|
||||
id: string = undefined
|
||||
export let location: LocationStore | undefined
|
||||
export let id: string | undefined
|
||||
|
||||
let loading = true
|
||||
let contentEntry: ContentEntry
|
||||
async function loadContent() {
|
||||
let contentEntry: ContentEntry | null = null
|
||||
let errorMessage = ""
|
||||
|
||||
const loadContent = async () => {
|
||||
loading = true
|
||||
errorMessage = ""
|
||||
contentEntry = null
|
||||
|
||||
try {
|
||||
contentEntry = await getCachedEntry(
|
||||
"content",
|
||||
location ? { $or: [{ path: location.path }, { "alternativePaths.path": location.path }] } : { _id: id }
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
if (id) {
|
||||
contentEntry = await getCachedEntry("content", { _id: id })
|
||||
} else if (location?.path) {
|
||||
contentEntry = await getCachedEntry("content", {
|
||||
$or: [{ path: location.path }, { "alternativePaths.path": location.path }],
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load content", error)
|
||||
errorMessage = "Beim Laden der Inhalte ist ein Fehler aufgetreten."
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
|
||||
// only load new content if location.path or id changes
|
||||
let _location: typeof location
|
||||
let _id: typeof id
|
||||
$: if (location?.path != _location?.path || id != _id) {
|
||||
_id = id
|
||||
_location = location
|
||||
$: if (location?.path || id) {
|
||||
loadContent()
|
||||
}
|
||||
|
||||
$: breadCrumbPosition = contentEntry?.blocks?.length && contentEntry.blocks[0].type?.startsWith("hero") ? 1 : 0
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
<Loader size="3" />
|
||||
<section class="content-loading">
|
||||
<p>Inhalt wird geladen …</p>
|
||||
</section>
|
||||
{:else if errorMessage}
|
||||
<section class="content-error">
|
||||
<p>{errorMessage}</p>
|
||||
</section>
|
||||
{:else if contentEntry}
|
||||
<Index
|
||||
createdAt={new Date(contentEntry.insertTime)}
|
||||
updatedAt={new Date(contentEntry.updateTime)}
|
||||
metaDescription={contentEntry.meta?.description}
|
||||
title={contentEntry.meta?.title}
|
||||
keywords={contentEntry.meta?.keywords}
|
||||
article={contentEntry.meta.isArticle}
|
||||
active={contentEntry.active}
|
||||
FAQ={contentEntry.meta.hasFAQ}
|
||||
FAQDetails={contentEntry.meta.FAQ}
|
||||
/>
|
||||
{#each contentEntry.blocks || [] as block, idx}
|
||||
{#if idx === breadCrumbPosition}
|
||||
<Breadcrumbs location={location} />
|
||||
<article class="content-article">
|
||||
<header>
|
||||
<h1>{contentEntry.meta?.title ?? contentEntry.name}</h1>
|
||||
{#if contentEntry.meta?.description}
|
||||
<p class="lead">{contentEntry.meta.description}</p>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
{#if contentEntry.blocks?.length}
|
||||
<section class="content-debug">
|
||||
<h2>Struktur</h2>
|
||||
<pre>{JSON.stringify(contentEntry.blocks, null, 2)}</pre>
|
||||
</section>
|
||||
{:else}
|
||||
<p>Für diesen Inhalt sind noch keine Bausteine definiert.</p>
|
||||
{/if}
|
||||
<ContentBlock block={block} />
|
||||
{/each}
|
||||
</article>
|
||||
{:else}
|
||||
<NotFound />
|
||||
{/if}
|
||||
|
||||
<style lang="less">
|
||||
.content-loading,
|
||||
.content-error,
|
||||
.content-article {
|
||||
max-width: 920px;
|
||||
margin: 4rem auto;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.content-article {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.2rem, 4vw, 3.2rem);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-size: 1.1rem;
|
||||
color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.content-debug {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,69 +1,22 @@
|
||||
<script>
|
||||
import Index from "../lib/components/pagebuilder/SEO/Index.svelte"
|
||||
|
||||
// set 404 for ssr
|
||||
if (typeof window === "undefined") {
|
||||
// @ts-ignore
|
||||
if (context) context.is404 = true
|
||||
}
|
||||
export let disableIndex = false
|
||||
</script>
|
||||
|
||||
{#if disableIndex}
|
||||
<Index
|
||||
title="404 - Nicht Gefunden"
|
||||
metaDescription="Die Seite konnte nicht gefunden werden."
|
||||
keywords="404, Nicht Gefunden, Seite"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="not-found">
|
||||
<div class="content">
|
||||
<h1>404</h1>
|
||||
<h2>Nicht Gefunden</h2>
|
||||
<p></p>
|
||||
<a
|
||||
href="/"
|
||||
class="back-home">Zurück</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<section class="not-found">
|
||||
<h1>Seite nicht gefunden</h1>
|
||||
<p>Der angeforderte Inhalt ist nicht verfügbar.</p>
|
||||
</section>
|
||||
|
||||
<style lang="less">
|
||||
@import "../lib/assets/css/variables.less";
|
||||
|
||||
.not-found {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
max-width: 720px;
|
||||
margin: 4rem auto;
|
||||
padding: 0 1.5rem;
|
||||
text-align: center;
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
h1 {
|
||||
font-size: clamp(2.4rem, 5vw, 3.4rem);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 6rem;
|
||||
color: var(--text-invers-200);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.back-home {
|
||||
text-decoration: none;
|
||||
border: 1px solid black;
|
||||
padding: 0.5rem 1rem;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
p {
|
||||
margin-top: 1rem;
|
||||
color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,41 +2,50 @@ import * as Sentry from "@sentry/svelte"
|
||||
|
||||
let initialized = false
|
||||
|
||||
export const init = (dsn: string, tracingOrigins: (string | RegExp)[], environment: string, release: string) => {
|
||||
if (typeof window !== "undefined") {
|
||||
Sentry.init({
|
||||
dsn: dsn,
|
||||
tunnel: "/_s",
|
||||
integrations: [
|
||||
new Sentry.BrowserTracing({
|
||||
tracingOrigins: tracingOrigins,
|
||||
traceFetch: false,
|
||||
traceXHR: false,
|
||||
}),
|
||||
new Sentry.Replay({
|
||||
maskAllText: false,
|
||||
maskAllInputs: false,
|
||||
blockAllMedia: false,
|
||||
networkDetailAllowUrls: [/\/api\//, /\/tibiapi\//],
|
||||
}),
|
||||
],
|
||||
environment: environment,
|
||||
tracesSampleRate: 1.0,
|
||||
debug: false,
|
||||
release: release,
|
||||
replaysSessionSampleRate: 1.0,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
})
|
||||
console.log("Sentry initialized")
|
||||
initialized = true
|
||||
export const init = (
|
||||
dsn: string | undefined,
|
||||
_tracingOrigins: (string | RegExp)[] = [],
|
||||
environment?: string,
|
||||
release?: string
|
||||
) => {
|
||||
if (!dsn || typeof window === "undefined" || initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
Sentry.init({
|
||||
dsn,
|
||||
tunnel: "/_s",
|
||||
environment,
|
||||
release,
|
||||
integrations: [
|
||||
Sentry.browserTracingIntegration(),
|
||||
Sentry.replayIntegration({
|
||||
maskAllText: false,
|
||||
maskAllInputs: false,
|
||||
blockAllMedia: false,
|
||||
}),
|
||||
],
|
||||
tracesSampleRate: 1.0,
|
||||
replaysSessionSampleRate: 1.0,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
})
|
||||
|
||||
initialized = true
|
||||
}
|
||||
|
||||
export const currentTransaction = () => Sentry.getCurrentHub().getScope().getTransaction()
|
||||
export const currentTransaction = () => {
|
||||
try {
|
||||
const hub = (Sentry as any).getCurrentHub?.()
|
||||
return hub?.getScope?.()?.getTransaction?.() ?? null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const setUser = (user: Sentry.User) => {
|
||||
if (typeof window !== "undefined" && initialized) {
|
||||
user.ip_address = "{{auto}}"
|
||||
Sentry.setUser(user)
|
||||
if (!initialized || typeof window === "undefined") {
|
||||
return
|
||||
}
|
||||
|
||||
Sentry.setUser({ ...user, ip_address: "{{auto}}" })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user