header & hero texts
This commit is contained in:
@@ -17,3 +17,16 @@ export const socialIcons = {
|
|||||||
linkedin: "https://www.linkedin.com/company/kontextwerk",
|
linkedin: "https://www.linkedin.com/company/kontextwerk",
|
||||||
youtube: "https://www.youtube.com/@kontextwerk",
|
youtube: "https://www.youtube.com/@kontextwerk",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HeaderLinkType = "link" | "button"
|
||||||
|
|
||||||
|
export type HeaderLink =
|
||||||
|
| ({ sectionId: string; href?: never } & { label: string; type?: HeaderLinkType })
|
||||||
|
| ({ href: string; sectionId?: never } & { label: string; type?: HeaderLinkType })
|
||||||
|
|
||||||
|
export const headerLinks: HeaderLink[] = [
|
||||||
|
{ label: "Überblick", sectionId: "leistungen" },
|
||||||
|
{ label: "Voicebot", sectionId: "voicebot-demo" },
|
||||||
|
{ label: "Chatbot", sectionId: "chatbot-demo" },
|
||||||
|
{ label: "Kontakt", sectionId: "kontakt", type: "button" },
|
||||||
|
]
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export const spaLink: Action<HTMLAnchorElement> = (node) => {
|
|||||||
const currentUrl = window.location.pathname + window.location.search + window.location.hash
|
const currentUrl = window.location.pathname + window.location.search + window.location.hash
|
||||||
|
|
||||||
if (nextUrl === currentUrl) {
|
if (nextUrl === currentUrl) {
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" })
|
const hashValue = anchor.hash ? anchor.hash.substring(1) : null
|
||||||
|
scrollToTarget(hashValue)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,14 +33,48 @@ export const spaLink: Action<HTMLAnchorElement> = (node) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scrollToTarget = (hash: string | null) => {
|
||||||
|
const header = document.getElementById("header-container")
|
||||||
|
const headerHeight = header?.getBoundingClientRect().height ?? 0
|
||||||
|
|
||||||
|
if (!hash) {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodedHash = decodeURIComponent(hash)
|
||||||
|
|
||||||
|
const tryScroll = (attempt = 0) => {
|
||||||
|
const target = document.getElementById(decodedHash)
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
const targetPosition = target.getBoundingClientRect().top + window.scrollY
|
||||||
|
const offset = Math.max(targetPosition - headerHeight - 16, 0)
|
||||||
|
window.scrollTo({ top: offset, behavior: "smooth" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt < 10) {
|
||||||
|
requestAnimationFrame(() => tryScroll(attempt + 1))
|
||||||
|
} else {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => tryScroll())
|
||||||
|
}
|
||||||
|
|
||||||
export const spaNavigate = (to: string, options?: { replace?: boolean }) => {
|
export const spaNavigate = (to: string, options?: { replace?: boolean }) => {
|
||||||
|
const hashIndex = to.indexOf("#")
|
||||||
|
const hash = hashIndex >= 0 ? to.slice(hashIndex + 1) : null
|
||||||
|
|
||||||
if (options?.replace) {
|
if (options?.replace) {
|
||||||
window.history.replaceState(null, "", to)
|
window.history.replaceState(null, "", to)
|
||||||
} else {
|
} else {
|
||||||
window.history.pushState(null, "", to)
|
window.history.pushState(null, "", to)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" })
|
scrollToTarget(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const spaBack = () => {
|
export const spaBack = () => {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { spaLink } from "../../actions"
|
import { spaLink } from "../../actions"
|
||||||
|
import { location } from "../../store"
|
||||||
|
import { headerLinks } from "../../../config"
|
||||||
|
import type { HeaderLink } from "../../../config"
|
||||||
|
|
||||||
let scrolled: boolean = false,
|
let scrolled: boolean = $state(false)
|
||||||
isHomepage: boolean = true
|
|
||||||
|
const links: HeaderLink[] = headerLinks
|
||||||
|
|
||||||
function checkScroll() {
|
function checkScroll() {
|
||||||
scrolled = window.scrollY >= 100
|
scrolled = window.scrollY >= 100
|
||||||
@@ -19,8 +23,13 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$: checkScroll()
|
$effect(() => {
|
||||||
$: darkBG = scrolled
|
checkScroll()
|
||||||
|
})
|
||||||
|
let isHomepage = $derived($location?.path === "/")
|
||||||
|
let darkBG = $derived(scrolled || !isHomepage)
|
||||||
|
|
||||||
|
const resolveHref = (link: HeaderLink) => ("sectionId" in link ? `/#${link.sectionId}` : link.href)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header
|
<header
|
||||||
@@ -44,6 +53,20 @@
|
|||||||
alt="logo"
|
alt="logo"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
<ul class="nav-links">
|
||||||
|
{#each links as link}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={resolveHref(link)}
|
||||||
|
use:spaLink
|
||||||
|
class="nav-link"
|
||||||
|
class:nav-link-button={link.type === "button"}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -92,6 +115,80 @@
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.nav-link {
|
||||||
|
color: var(--text-100);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
transition:
|
||||||
|
color 0.2s ease,
|
||||||
|
background-color 0.2s ease,
|
||||||
|
border-color 0.2s ease;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--primary-200);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: currentColor;
|
||||||
|
transform: scaleX(0);
|
||||||
|
transform-origin: left;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
color: var(--primary-200);
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-button {
|
||||||
|
border: 1px solid var(--primary-200);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.45rem 1.4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--neutral-white);
|
||||||
|
background: linear-gradient(135deg, var(--primary-200), var(--primary-100));
|
||||||
|
box-shadow: 0 0 12px rgba(116, 30, 32, 0.45);
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
transition:
|
||||||
|
background 0.3s ease,
|
||||||
|
color 0.3s ease;
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
color: var(--neutral-white);
|
||||||
|
background: linear-gradient(135deg, var(--primary-100), #ff5252);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.homepageHeader {
|
&.homepageHeader {
|
||||||
@@ -102,4 +199,35 @@
|
|||||||
background-color: var(--bg-100);
|
background-color: var(--bg-100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media @mobile {
|
||||||
|
.headercontainer {
|
||||||
|
.padding {
|
||||||
|
.menu {
|
||||||
|
flex-direction: column;
|
||||||
|
height: auto;
|
||||||
|
padding: 0.8rem 0;
|
||||||
|
gap: 0.8rem;
|
||||||
|
.logo-container {
|
||||||
|
height: 48px;
|
||||||
|
img {
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.nav-links {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.8rem 1.2rem;
|
||||||
|
.nav-link {
|
||||||
|
padding: 0.4rem 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.nav-link-button {
|
||||||
|
padding: 0.4rem 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
properties={voiceProperties}
|
properties={voiceProperties}
|
||||||
title="Chatbot Demo"
|
title="Chatbot Demo"
|
||||||
reverse={true}
|
reverse={true}
|
||||||
|
sectionId="chatbot-demo"
|
||||||
upperDescription="Unsere Voicebots sind rund um die Uhr für Ihre Kunden da und bieten maßgeschneiderte Lösungen, die perfekt auf Ihre Bedürfnisse abgestimmt sind."
|
upperDescription="Unsere Voicebots sind rund um die Uhr für Ihre Kunden da und bieten maßgeschneiderte Lösungen, die perfekt auf Ihre Bedürfnisse abgestimmt sind."
|
||||||
lowerDescription="Durch den Einsatz modernster KI-Technologien gewährleisten wir eine intelligente und effiziente Kommunikation, die den höchsten Datenschutzstandards entspricht."
|
lowerDescription="Durch den Einsatz modernster KI-Technologien gewährleisten wir eine intelligente und effiziente Kommunikation, die den höchsten Datenschutzstandards entspricht."
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -84,7 +84,10 @@
|
|||||||
|
|
||||||
<CrinkledSection>
|
<CrinkledSection>
|
||||||
{#snippet contentSnippet()}
|
{#snippet contentSnippet()}
|
||||||
<section class="small-wrapper contact-row">
|
<section
|
||||||
|
class="small-wrapper contact-row"
|
||||||
|
id="kontakt"
|
||||||
|
>
|
||||||
<div class="contact-wrapper">
|
<div class="contact-wrapper">
|
||||||
<div class="copy">
|
<div class="copy">
|
||||||
<small>Kontakt</small>
|
<small>Kontakt</small>
|
||||||
|
|||||||
@@ -46,28 +46,28 @@
|
|||||||
title: "Schneller",
|
title: "Schneller",
|
||||||
alias: "Schneller",
|
alias: "Schneller",
|
||||||
shortDescription:
|
shortDescription:
|
||||||
"Unser internes System sorgt für eine schnelle und effiziente Umsetzung Ihres Projekts. Dadurch ermöglichen wir, ihr Projekt in Wochen, statt Monaten zu realisieren!",
|
"Projektstart innerhalb von 3 Werktagen. Ein FAQ-Bot ist nach 3 Tagen live, komplexere Lösungen in Rekordzeit – möglich durch erprobte Prozesse und eine modulare Codebasis.",
|
||||||
color: "#ffffff",
|
color: "#ffffff",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Qualitativer",
|
title: "Zuverlässiger",
|
||||||
alias: "Qualitativer",
|
alias: "Zuverlässiger",
|
||||||
shortDescription:
|
shortDescription:
|
||||||
"Höhere Qualität durch spezialisierte Experten. Wir setzen auf ein Netzwerk aus erfahrenen Fachleuten, um Ihnen die bestmöglichen Lösungen mit State-of-the-Art-Technologien zu bieten.",
|
"Der Assistent greift ausschließlich auf freigegebenes Unternehmenswissen zu. Mehrstufige Qualitätsprüfungen und monatliche KPI-Berichte sichern dauerhaft verlässliche Ergebnisse.",
|
||||||
color: "#741e20",
|
color: "#741e20",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Entspannter",
|
title: "Sorglos",
|
||||||
alias: "Entspannter",
|
alias: "Sorglos",
|
||||||
shortDescription:
|
shortDescription:
|
||||||
"Wir bieten Ihnen einen Rundum-sorglos-Service. Von der Konzeption über die Umsetzung bis hin zur Nachbetreuung. Alles aus einer Hand.",
|
"Alles aus einer Hand: Kick-off-Workshop, Datenaufbereitung, Implementierung und Kanalintegration. Hosting im deutschen Rechenzentrum, 24/7-Monitoring, automatisierte Tests und kontinuierliche Optimierung sind für uns Standard.",
|
||||||
color: "#ffffff",
|
color: "#ffffff",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Autonomer",
|
title: "Unabhängiger",
|
||||||
alias: "Autonomer",
|
alias: "Unabhängiger",
|
||||||
shortDescription:
|
shortDescription:
|
||||||
"Sie entscheiden wo die Software gehostet wird. Ob bei uns in unseren deutschen Rechenzentren oder bei Ihnen, wir bieten beides an.",
|
"Betriebsmodell nach Wahl: Managed in deutschen Rechenzentren, Private Cloud oder On-Premise. Offene Schnittstellen und containerisierte Übergabe garantieren volle Kontrolle – auf Wunsch auch mit Ihrem eigenen Sprachmodell.",
|
||||||
color: "#741e20",
|
color: "#741e20",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -75,6 +75,7 @@
|
|||||||
|
|
||||||
<section
|
<section
|
||||||
class="splittedHomepage"
|
class="splittedHomepage"
|
||||||
|
id="leistungen"
|
||||||
style="--color: {currentColor}"
|
style="--color: {currentColor}"
|
||||||
>
|
>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
<ProductCategoryFrame
|
<ProductCategoryFrame
|
||||||
properties={voiceProperties}
|
properties={voiceProperties}
|
||||||
title="Voicebot Demo"
|
title="Voicebot Demo"
|
||||||
|
sectionId="voicebot-demo"
|
||||||
upperDescription="Unsere Voicebots sind rund um die Uhr für Ihre Kunden da und bieten maßgeschneiderte Lösungen, die perfekt auf Ihre Bedürfnisse abgestimmt sind."
|
upperDescription="Unsere Voicebots sind rund um die Uhr für Ihre Kunden da und bieten maßgeschneiderte Lösungen, die perfekt auf Ihre Bedürfnisse abgestimmt sind."
|
||||||
lowerDescription="Durch den Einsatz modernster KI-Technologien gewährleisten wir eine intelligente und effiziente Kommunikation, die den höchsten Datenschutzstandards entspricht."
|
lowerDescription="Durch den Einsatz modernster KI-Technologien gewährleisten wir eine intelligente und effiziente Kommunikation, die den höchsten Datenschutzstandards entspricht."
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
export let query: string
|
|
||||||
|
|
||||||
let mql: MediaQueryList
|
|
||||||
let mqlListener: {
|
|
||||||
(v: any): any
|
|
||||||
(this: MediaQueryList, ev: MediaQueryListEvent): any
|
|
||||||
(this: MediaQueryList, ev: MediaQueryListEvent): any
|
|
||||||
}
|
|
||||||
let wasMounted = false
|
|
||||||
let matches = false
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
wasMounted = true
|
|
||||||
return () => {
|
|
||||||
removeActiveListener()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
$: {
|
|
||||||
if (wasMounted) {
|
|
||||||
removeActiveListener()
|
|
||||||
addNewListener(query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addNewListener(query: string) {
|
|
||||||
if (!query) {
|
|
||||||
query = "(min-width:0px)"
|
|
||||||
console.warn(
|
|
||||||
"Missing view property 'meta.views.mediaQuery' in collection config. Query has been set to '(min-width:0px)'."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (typeof "window" == "undefined") return
|
|
||||||
mql = window.matchMedia(query)
|
|
||||||
mqlListener = (e: { matches: boolean }) => (matches = e.matches)
|
|
||||||
//mql.addListener(mqlListener)
|
|
||||||
mql.addEventListener("change", mqlListener)
|
|
||||||
matches = mql.matches
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeActiveListener() {
|
|
||||||
if (mql && mqlListener) {
|
|
||||||
// mql.removeListener(mqlListener)
|
|
||||||
mql.removeEventListener("change", mqlListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if matches}
|
|
||||||
<slot />
|
|
||||||
{/if}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
<script module lang="ts">
|
|
||||||
import pDebounce from "p-debounce"
|
|
||||||
|
|
||||||
import { apiBaseURL } from "../../configs/config"
|
|
||||||
import { getCachedEntries, getCachedEntry } from "../../api"
|
|
||||||
const medialibCache: { [id: string]: MedialibEntry } = {}
|
|
||||||
|
|
||||||
let loadQueue: string[] = []
|
|
||||||
export async function loadMedialibEntry(id: string): Promise<MedialibEntry> {
|
|
||||||
if (medialibCache[id]) return medialibCache[id]
|
|
||||||
loadQueue.push(id)
|
|
||||||
await processQueueDebounced()
|
|
||||||
return medialibCache[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
const processQueue = async () => {
|
|
||||||
if (loadQueue.length) {
|
|
||||||
const _ids = loadQueue
|
|
||||||
loadQueue = []
|
|
||||||
const entries = await getCachedEntries("medialib", { _id: { $in: _ids } })
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
medialibCache[entry.id] = entry
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const processQueueDebounced = pDebounce(processQueue, 50)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
let {
|
|
||||||
id,
|
|
||||||
noPlaceholder,
|
|
||||||
loading,
|
|
||||||
childrenSnippet,
|
|
||||||
loadingSnippet,
|
|
||||||
notFoundSnippet,
|
|
||||||
}: {
|
|
||||||
id: string
|
|
||||||
noPlaceholder?: boolean
|
|
||||||
loading?: boolean
|
|
||||||
loadingSnippet: ({ entry, src }: { entry: MedialibEntry; src: string }) => any
|
|
||||||
childrenSnippet: ({ entry, src }: { entry: MedialibEntry; src: string }) => any
|
|
||||||
notFoundSnippet: () => any
|
|
||||||
} = $props()
|
|
||||||
|
|
||||||
let entry: MedialibEntry = $state(),
|
|
||||||
fileSrc: string = $state()
|
|
||||||
|
|
||||||
async function loadFile() {
|
|
||||||
loading = true
|
|
||||||
entry = null
|
|
||||||
fileSrc = null
|
|
||||||
try {
|
|
||||||
entry =
|
|
||||||
typeof window !== "undefined"
|
|
||||||
? await loadMedialibEntry(id)
|
|
||||||
: await getCachedEntry("medialib", { _id: id })
|
|
||||||
if (entry?.file?.src) {
|
|
||||||
fileSrc = `${apiBaseURL}${window?.location.host.includes("tibi") ? "_/renz_shop_2024/" : ""}medialib/${id}/${entry.file.src}`
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
loading = false
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (id) loadFile()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if id}
|
|
||||||
{#if loading}
|
|
||||||
{#if !noPlaceholder}
|
|
||||||
{@render loadingSnippet({ entry, src: fileSrc })}
|
|
||||||
{/if}
|
|
||||||
{:else if entry}
|
|
||||||
{@render childrenSnippet({ entry, src: fileSrc })}
|
|
||||||
{:else if !noPlaceholder}
|
|
||||||
{@render notFoundSnippet()}
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { onDestroy } from "svelte"
|
|
||||||
import MedialibFile from "./MedialibFile.svelte"
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
id: string
|
|
||||||
filter?:
|
|
||||||
| null
|
|
||||||
| "xs"
|
|
||||||
| "s"
|
|
||||||
| "m"
|
|
||||||
| "l"
|
|
||||||
| "xl"
|
|
||||||
| "xxl"
|
|
||||||
| "xs-webp"
|
|
||||||
| "s-webp"
|
|
||||||
| "m-webp"
|
|
||||||
| "l-webp"
|
|
||||||
| "xl-webp"
|
|
||||||
| "xxl-webp"
|
|
||||||
minWidth?: number
|
|
||||||
noPlaceholder?: boolean
|
|
||||||
style?: string
|
|
||||||
widthMultiplier?: number
|
|
||||||
lazy?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
id,
|
|
||||||
filter,
|
|
||||||
minWidth = null,
|
|
||||||
noPlaceholder = false,
|
|
||||||
style,
|
|
||||||
widthMultiplier = 1,
|
|
||||||
lazy = false,
|
|
||||||
}: Props = $props()
|
|
||||||
|
|
||||||
let imgElement: HTMLImageElement = $state()
|
|
||||||
|
|
||||||
function getSrcWithFilter(_imgElement: HTMLImageElement, entry: MedialibEntry, src: string) {
|
|
||||||
if (typeof window === "undefined") {
|
|
||||||
return src + `?filter=${filter || "xs-webp"}`
|
|
||||||
}
|
|
||||||
let internalFilter = filter
|
|
||||||
if (imgElement) {
|
|
||||||
if (!entry.file?.type?.match(/^image\/(png|jpe?g|webp)/)) return src // no filter for svg
|
|
||||||
if (!internalFilter) {
|
|
||||||
let imgWidth = _imgElement.width
|
|
||||||
|
|
||||||
if (widthMultiplier) {
|
|
||||||
imgWidth *= widthMultiplier
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the width of the image element
|
|
||||||
const width = minWidth ? (imgWidth < minWidth ? minWidth : imgWidth) : imgWidth
|
|
||||||
switch (true) {
|
|
||||||
case width <= 90:
|
|
||||||
internalFilter = "xs-webp"
|
|
||||||
break
|
|
||||||
case width <= 300:
|
|
||||||
internalFilter = "s-webp"
|
|
||||||
break
|
|
||||||
case width <= 600:
|
|
||||||
internalFilter = "m-webp"
|
|
||||||
break
|
|
||||||
case width <= 1200:
|
|
||||||
internalFilter = "l-webp"
|
|
||||||
break
|
|
||||||
case width <= 2000:
|
|
||||||
internalFilter = "xl-webp"
|
|
||||||
break
|
|
||||||
case width <= 4000:
|
|
||||||
internalFilter = "xxl-webp"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return src + (internalFilter ? `?filter=${internalFilter}` : "")
|
|
||||||
} else {
|
|
||||||
// placeholder
|
|
||||||
return "/assets/img/placeholder-image.png"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let resizeObserver: ResizeObserver
|
|
||||||
|
|
||||||
function initResizeObserver(node: HTMLImageElement, { entry, src }: { entry: MedialibEntry; src: string }) {
|
|
||||||
if (!filter && typeof ResizeObserver !== "undefined") {
|
|
||||||
resizeObserver?.disconnect()
|
|
||||||
resizeObserver = new ResizeObserver(() => {
|
|
||||||
const oldWidth = parseInt(node.getAttribute("data-width")) || 0
|
|
||||||
const newWidth = node.width
|
|
||||||
|
|
||||||
if (oldWidth >= newWidth) return
|
|
||||||
|
|
||||||
const newSrc = getSrcWithFilter(imgElement, entry, src)
|
|
||||||
if (newSrc !== node.getAttribute("src")) {
|
|
||||||
node.setAttribute("src", newSrc)
|
|
||||||
}
|
|
||||||
node.setAttribute("data-width", newWidth.toString())
|
|
||||||
})
|
|
||||||
resizeObserver.observe(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
destroy() {
|
|
||||||
resizeObserver?.disconnect()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
resizeObserver?.disconnect()
|
|
||||||
})
|
|
||||||
let properties = {}
|
|
||||||
if (lazy) {
|
|
||||||
properties = { loading: "lazy" }
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if id}
|
|
||||||
<MedialibFile {id} {noPlaceholder}>
|
|
||||||
{#snippet childrenSnippet({ entry, src }: { entry: MedialibEntry; src: string })}
|
|
||||||
<img
|
|
||||||
{style}
|
|
||||||
bind:this={imgElement}
|
|
||||||
src={getSrcWithFilter(imgElement, entry, src)}
|
|
||||||
alt={entry.alt || "icon"}
|
|
||||||
data-entry-id={id}
|
|
||||||
use:initResizeObserver={{ entry, src }}
|
|
||||||
loading="lazy"
|
|
||||||
{...properties}
|
|
||||||
/>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet loadingSnippet({ entry, src }: { entry: MedialibEntry; src: string })}{/snippet}
|
|
||||||
|
|
||||||
{#snippet notFoundSnippet()}{/snippet}
|
|
||||||
</MedialibFile>
|
|
||||||
{/if}
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
upperDescription,
|
upperDescription,
|
||||||
lowerDescription,
|
lowerDescription,
|
||||||
reverse = false,
|
reverse = false,
|
||||||
|
sectionId,
|
||||||
primaryContent,
|
primaryContent,
|
||||||
}: {
|
}: {
|
||||||
properties: Array<{ title: string; icon: string; color: string }>
|
properties: Array<{ title: string; icon: string; color: string }>
|
||||||
@@ -15,12 +15,14 @@
|
|||||||
upperDescription: string
|
upperDescription: string
|
||||||
reverse?: boolean
|
reverse?: boolean
|
||||||
lowerDescription: string
|
lowerDescription: string
|
||||||
|
sectionId?: string
|
||||||
primaryContent: () => any
|
primaryContent: () => any
|
||||||
} = $props()
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
class="product-frame"
|
class="product-frame"
|
||||||
|
id={sectionId}
|
||||||
class:reverse
|
class:reverse
|
||||||
>
|
>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
|
|||||||
Reference in New Issue
Block a user