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:
2026-02-26 03:54:07 +00:00
parent e8fd38e98a
commit 40ffa8207e
27 changed files with 2009 additions and 98 deletions
+147
View File
@@ -0,0 +1,147 @@
<script lang="ts">
import { _ } from "../lib/i18n/index"
import { reveal } from "../lib/actions/reveal"
import { addToast } from "../lib/toast"
import Form from "../widgets/Form.svelte"
import Input from "../widgets/Input.svelte"
import Select from "../widgets/Select.svelte"
import Button from "../widgets/Button.svelte"
let { block }: { block: ContentBlockEntry } = $props()
const paddingTop = $derived(
block.padding?.top === "lg"
? "pt-20"
: block.padding?.top === "md"
? "pt-12"
: block.padding?.top === "sm"
? "pt-8"
: "pt-4"
)
const paddingBottom = $derived(
block.padding?.bottom === "lg"
? "pb-20"
: block.padding?.bottom === "md"
? "pb-12"
: block.padding?.bottom === "sm"
? "pb-8"
: "pb-4"
)
let name = $state("")
let email = $state("")
let subject = $state("")
let message = $state("")
let sending = $state(false)
let formRef = $state<{ validate: () => Promise<boolean> } | null>(null)
const subjectOptions = $derived([
{ value: "", label: $_("form.selectSubject") },
{ value: "general", label: $_("form.subjects.general") },
{ value: "project", label: $_("form.subjects.project") },
{ value: "support", label: $_("form.subjects.support") },
{ value: "feedback", label: $_("form.subjects.feedback") },
])
async function handleSubmit(e: SubmitEvent) {
e.preventDefault()
if (!formRef) return
const isValid = await formRef.validate()
if (!isValid) return
sending = true
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1200))
addToast($_("form.success"), "success", 5000)
// Reset form
name = ""
email = ""
subject = ""
message = ""
sending = false
}
</script>
<section
data-block="contact-form"
class="contact-form-section {paddingTop} {paddingBottom}"
id={block.anchorId || undefined}
>
<div class="max-w-2xl mx-auto px-6">
{#if block.headline}
<div use:reveal>
<h2 class="text-3xl font-display font-bold text-gray-900 mb-8">
{block.headline}
</h2>
</div>
{/if}
<div use:reveal={{ delay: 150 }}>
<Form bind:this={formRef} onsubmit={handleSubmit} class="space-y-6">
<div class="grid sm:grid-cols-2 gap-6">
<Input
label={$_("form.name")}
hideLabel={false}
bind:value={name}
placeholder={$_("form.name")}
required
name="name"
messages={{ valueMissing: $_("form.validation.required") }}
/>
<Input
label={$_("form.email")}
hideLabel={false}
bind:value={email}
placeholder={$_("form.email")}
type="email"
required
name="email"
messages={{
valueMissing: $_("form.validation.required"),
typeMismatch: $_("form.validation.email"),
}}
/>
</div>
<Select
label={$_("form.subject")}
hideLabel={false}
bind:value={subject}
options={subjectOptions}
required
name="subject"
/>
<div>
<label for="contact-message" class="block text-sm font-medium text-gray-700 mb-1">
{$_("form.message")}
</label>
<textarea
id="contact-message"
bind:value={message}
rows="5"
required
name="message"
placeholder={$_("form.message")}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 shadow-sm focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20 focus:outline-none transition-colors resize-y"
></textarea>
</div>
<div class="flex justify-end">
<Button
type="submit"
variant="primary"
size="lg"
text={sending ? $_("loading") : $_("form.send")}
disabled={sending}
class="bg-brand-600! hover:bg-brand-700! rounded-xl!"
/>
</div>
</Form>
</div>
</div>
</section>