✨ feat: add new contact form, hero, features, and richtext blocks; implement scroll-reveal action and update styles
- Introduced ContactFormBlock, FeaturesBlock, HeroBlock, and RichtextBlock components. - Implemented a scroll-reveal action for animations on element visibility. - Enhanced CSS styles for better theming and prose formatting. - Added localization support for new components and updated existing translations. - Created e2e tests for demo pages including contact form validation and navigation. - Added a video tour showcasing the demo pages and interactions.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Scroll-reveal action — fades elements in when they enter the viewport.
|
||||
*
|
||||
* Usage: <div use:reveal>...</div>
|
||||
* <div use:reveal={{ delay: 200, threshold: 0.2 }}>...</div>
|
||||
*
|
||||
* The element gets the `.reveal` CSS class on mount and `.revealed` when
|
||||
* it enters the viewport. Animation is defined in style.css.
|
||||
*
|
||||
* SSR-safe: no-ops when IntersectionObserver is unavailable.
|
||||
*/
|
||||
|
||||
export interface RevealOptions {
|
||||
/** Delay in ms before the animation starts (default: 0) */
|
||||
delay?: number
|
||||
/** IntersectionObserver threshold 0–1 (default: 0.15) */
|
||||
threshold?: number
|
||||
/** Only animate once, then disconnect (default: true) */
|
||||
once?: boolean
|
||||
}
|
||||
|
||||
export function reveal(node: HTMLElement, options: RevealOptions = {}) {
|
||||
if (typeof IntersectionObserver === "undefined") return
|
||||
|
||||
const { delay = 0, threshold = 0.15, once = true } = options
|
||||
|
||||
node.classList.add("reveal")
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
if (delay > 0) {
|
||||
setTimeout(() => node.classList.add("revealed"), delay)
|
||||
} else {
|
||||
node.classList.add("revealed")
|
||||
}
|
||||
if (once) observer.unobserve(node)
|
||||
} else if (!once) {
|
||||
node.classList.remove("revealed")
|
||||
}
|
||||
}
|
||||
},
|
||||
{ threshold }
|
||||
)
|
||||
|
||||
observer.observe(node)
|
||||
|
||||
return {
|
||||
update(newOptions: RevealOptions) {
|
||||
// Options are static for simplicity — recreate if needed
|
||||
},
|
||||
destroy() {
|
||||
observer.unobserve(node)
|
||||
observer.disconnect()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@ export const LANGUAGE_LABELS: Record<SupportedLanguage, string> = {
|
||||
* Example: { about: { de: "ueber-uns", en: "about" } }
|
||||
*/
|
||||
export const ROUTE_TRANSLATIONS: Record<string, Record<SupportedLanguage, string>> = {
|
||||
// Add your route translations here:
|
||||
// about: { de: "ueber-uns", en: "about" },
|
||||
about: { de: "ueber-uns", en: "about" },
|
||||
contact: { de: "kontakt", en: "contact" },
|
||||
}
|
||||
|
||||
export const getLocalizedRoute = (canonicalRoute: string, lang: SupportedLanguage): string => {
|
||||
|
||||
@@ -16,8 +16,40 @@
|
||||
"contact": {
|
||||
"title": "Kontakt",
|
||||
"text": "Hier kannst du ein Kontaktformular oder Kontaktdaten anzeigen."
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Seite nicht gefunden",
|
||||
"text": "Die Seite, die du suchst, existiert leider nicht.",
|
||||
"backHome": "Zur Startseite"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"name": "Name",
|
||||
"email": "E-Mail",
|
||||
"subject": "Betreff",
|
||||
"message": "Nachricht",
|
||||
"send": "Nachricht senden",
|
||||
"success": "Vielen Dank! Deine Nachricht wurde gesendet.",
|
||||
"error": "Leider ist ein Fehler aufgetreten. Bitte versuche es erneut.",
|
||||
"selectSubject": "Bitte wählen…",
|
||||
"subjects": {
|
||||
"general": "Allgemeine Anfrage",
|
||||
"project": "Projektanfrage",
|
||||
"support": "Technischer Support",
|
||||
"feedback": "Feedback"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Dieses Feld ist erforderlich.",
|
||||
"email": "Bitte gib eine gültige E-Mail-Adresse ein."
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"builtWith": "Gebaut mit",
|
||||
"rights": "Alle Rechte vorbehalten.",
|
||||
"madeWith": "Gemacht mit ♥ und Svelte"
|
||||
},
|
||||
"welcome": "Willkommen",
|
||||
"language": "Sprache"
|
||||
"language": "Sprache",
|
||||
"scrollToTop": "Nach oben",
|
||||
"loading": "Laden…"
|
||||
}
|
||||
@@ -16,8 +16,40 @@
|
||||
"contact": {
|
||||
"title": "Contact",
|
||||
"text": "Use this page to display a contact form or contact details."
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Page not found",
|
||||
"text": "The page you're looking for doesn't exist.",
|
||||
"backHome": "Back to Home"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"name": "Name",
|
||||
"email": "Email",
|
||||
"subject": "Subject",
|
||||
"message": "Message",
|
||||
"send": "Send message",
|
||||
"success": "Thank you! Your message has been sent.",
|
||||
"error": "Sorry, something went wrong. Please try again.",
|
||||
"selectSubject": "Please select…",
|
||||
"subjects": {
|
||||
"general": "General inquiry",
|
||||
"project": "Project inquiry",
|
||||
"support": "Technical support",
|
||||
"feedback": "Feedback"
|
||||
},
|
||||
"validation": {
|
||||
"required": "This field is required.",
|
||||
"email": "Please enter a valid email address."
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"builtWith": "Built with",
|
||||
"rights": "All rights reserved.",
|
||||
"madeWith": "Made with ♥ and Svelte"
|
||||
},
|
||||
"welcome": "Welcome",
|
||||
"language": "Language"
|
||||
"language": "Language",
|
||||
"scrollToTop": "Scroll to top",
|
||||
"loading": "Loading…"
|
||||
}
|
||||
Reference in New Issue
Block a user