implements brevo form, webfonts sh

This commit is contained in:
saiminh 2026-01-16 17:43:23 +13:00
parent bad6faf555
commit 8e2a5ed8bf
14 changed files with 220 additions and 71 deletions

View file

@ -3,7 +3,10 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="stylesheet" href="https://use.typekit.net/ltu2cxf.css">
<link rel="preload" as="font" type="font/woff2" href="/fonts/Stratos-Regular.woff2" crossorigin>
<link rel="preload" as="font" type="font/woff2" href="/fonts/Stratos-BoldItalic.woff2" crossorigin>
<link rel="preload" as="font" type="font/woff2" href="/fonts/PPFormula-Medium.woff2" crossorigin>
<link rel="preload" as="font" type="font/woff2" href="/fonts/PPFormula-Extrabold.woff2" crossorigin>
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>

View file

@ -1,13 +1,9 @@
:root {
--spacing-outer: 5vw;
--spacing-nav: 5vw;
// --color-bg: hsl(202, 58%, 39%);
--color-bg: hsl(0, 60%, 53%);
--color-text: #ffead9;
--color-highlight: #1d0c12;
// --color-bg: #000000;
// --color-text: #FFFFFF;
// --color-highlight: #FF4D00;
--color-bg: rgb(207, 63, 63);
--color-text: rgb(255, 234, 217);
--color-highlight: rgb(29, 12, 18);
--aspect-ratio-heroes: 1.5;
--font-size-p: clamp(20px, 1.6vw, 1.6vw);
@ -17,6 +13,20 @@
}
}
@font-face {
font-family: 'Stratos';
src: url('/fonts/Stratos-Regular.woff2') format('woff2');
font-weight: normal;
font-display: swap;
}
@font-face {
font-family: 'Stratos';
src: url('/fonts/Stratos-BoldItalic.woff2') format('woff2');
font-weight: bold;
font-display: swap;
}
a:hover, input:hover, button:hover {
cursor: url('/pointer.svg'), auto;
}
@ -32,18 +42,6 @@ body {
max-width: 100vw;
overflow-x: hidden;
}
// body:after {
// content: '';
// display: block;
// position: fixed;
// top: 0;
// left: 0;
// width: calc(100vw - 00px);
// height: calc(100svh - 30px);
// border: 15px solid var(--color-text);
// z-index: 1;
// pointer-events: none;
// }
body * {
box-sizing: border-box;
}

View file

@ -1,7 +1,11 @@
import * as PIXI from 'pixi.js';
export default function createCanvasText( element: HTMLImageElement, stage: PIXI.Container ){
export default function createCanvasText(
element: HTMLImageElement,
stage: PIXI.Container,
spriteObject: PIXI.Sprite | null = null
){
const elem = element;
const elemSrc = elem.currentSrc || elem.src;
const elemPosition = elem.getBoundingClientRect();
@ -11,9 +15,22 @@ export default function createCanvasText( element: HTMLImageElement, stage: PIX
if (elemSrc.includes('.svg')) {
scalefactor = Number(elem.attributes.getNamedItem('data-svgscale')?.value);
const canvasImgTexture = PIXI.Texture.from(elemSrc, { resourceOptions: { scale: scalefactor } });
canvasImg = new PIXI.Sprite(canvasImgTexture);
// Use provided sprite object or create a new one
if (spriteObject) {
spriteObject.texture = canvasImgTexture;
canvasImg = spriteObject;
} else {
canvasImg = new PIXI.Sprite(canvasImgTexture);
}
} else {
canvasImg = PIXI.Sprite.from(elemSrc);
// Use provided sprite object or create a new one
if (spriteObject) {
spriteObject.texture = PIXI.Texture.from(elemSrc);
canvasImg = spriteObject;
} else {
canvasImg = PIXI.Sprite.from(elemSrc);
}
}
canvasImg.position.set(elemPosition.x, elemPosition.y);

View file

@ -1,8 +1,12 @@
import type * as PIXI from 'pixi.js';
import { Text } from 'pixi.js';
export default function createCanvasText( element: HTMLElement, stage: PIXI.Container ){
export default function createCanvasText(
element: HTMLElement,
stage: PIXI.Container,
textObject: PIXI.Text | null = null
){
const elem = element;
const elemStyles = window.getComputedStyle(elem);
const elemFontSize = elemStyles.getPropertyValue('font-size') || '16px';
@ -19,7 +23,8 @@ export default function createCanvasText( element: HTMLElement, stage: PIXI.Con
elem.textContent = elem.textContent?.toUpperCase() as string;
}
const canvasText = new Text(elem.textContent as string, {
// Use provided text object or create a new one
const canvasText = textObject || new Text(elem.textContent as string, {
fontFamily: elemFontFamily,
fontSize: elemFontSize,
fontWeight: elemFontWeight as PIXI.TextStyleFontWeight,
@ -30,6 +35,18 @@ export default function createCanvasText( element: HTMLElement, stage: PIXI.Con
padding: 20,
});
// Update existing text object properties
if (textObject) {
canvasText.text = elem.textContent as string;
canvasText.style.fontFamily = elemFontFamily;
canvasText.style.fontSize = parseInt(elemFontSize);
canvasText.style.fontWeight = elemFontWeight as PIXI.TextStyleFontWeight;
canvasText.style.fontStyle = elemFontStyle as PIXI.TextStyleFontStyle;
canvasText.style.letterSpacing = elemLetterSpacing;
canvasText.style.fill = elemColor;
canvasText.style.align = elemAlignment as PIXI.TextStyleAlign;
}
canvasText.on('added', () => {
elem.classList.add('canvas-text-added');
elem.style.visibility = 'hidden';

View file

@ -0,0 +1,79 @@
import type { Actions } from './$types';
import { BREVO_API_KEY } from '$env/static/private';
import { fail, redirect } from '@sveltejs/kit';
const BREVO_ENDPOINT = 'https://api.brevo.com/v3/smtp/email';
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export const prerender = false;
export const actions: Actions = {
default: async ({ request, fetch }) => {
const formData = await request.formData();
const name = (formData.get('name') ?? '').toString().trim();
const email = (formData.get('email') ?? '').toString().trim();
const message = (formData.get('contact') ?? '').toString().trim();
const fields = { name, email, contact: message };
if (!email || !message) {
return fail(400, { error: 'Please provide your email and message.', fields });
}
if (!emailRegex.test(email)) {
return fail(400, { error: 'Please provide a valid email address.', fields });
}
if (!BREVO_API_KEY) {
console.error('Missing BREVO_API_KEY in environment');
return fail(500, { error: 'Email service is not configured. Please try again later.', fields });
}
const subject = `New contact form submission from ${name || 'Website visitor'}`;
const textContent = `Name: ${name || 'N/A'}\nEmail: ${email}\n\nMessage:\n${message}`;
const htmlContent = `<p><strong>Name:</strong> ${name || 'N/A'}</p><p><strong>Email:</strong> ${email}</p><p><strong>Message:</strong></p><p>${message.replace(/\n/g, '<br>')}</p>`;
const payload = {
sender: {
name: 'Website Contact',
email: 'simon@floter.design'
},
to: [
{
email: 'simon@floter.design',
name: 'Simon Flöter'
}
],
replyTo: {
email,
name: name || 'Website visitor'
},
subject,
textContent,
htmlContent,
tags: ['contact-form']
};
try {
const response = await fetch(BREVO_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'api-key': BREVO_API_KEY
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text();
console.error(`Brevo API error ${response.status}: ${errorText}`);
return fail(502, { error: 'Sending failed. Please try again later.', fields });
}
throw redirect(303, '/success');
} catch (err) {
console.error('Brevo API request failed', err);
return fail(500, { error: 'Unexpected error. Please try again later.', fields });
}
}
};

View file

@ -5,6 +5,8 @@
import { SplitText } from 'gsap/SplitText';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
export let form: { error?: string; fields?: { name?: string; email?: string; contact?: string } } | undefined;
let canvasTexts: Array<HTMLElement> = [];
let contactFormVisible = false;
let contactFormClickHandler = (e: Event) => {
@ -88,25 +90,27 @@
</div>
<div class="formwrapper">
<span class="button button-back" on:click={contactFormClickHandler} on:keydown={contactFormClickHandler} role="button" tabindex="0">← Back</span>
<form name="contact" action="/success" method="POST" data-netlify="true">
<input type="hidden" name="form-name" value="contact">
<form name="contact" method="POST">
<div class="inputs-flex-row">
<label for="name">
<!-- <p>How would you like to be addressed?</p> -->
<input type="text" name="name" id="name" placeholder="Your name">
<input type="text" name="name" id="name" placeholder="Your name" value={form?.fields?.name ?? ''}>
</label>
<label for="email">
<!-- <p>For receiving a reply, add your Email address:</p> -->
<input type="email" name="email" id="email" placeholder="Your Email?*" required>
<input type="email" name="email" id="email" placeholder="Your Email?*" required value={form?.fields?.email ?? ''}>
</label>
</div>
<label for="contact">
<!-- <p>Please describe your plight in a few words</p> -->
<textarea rows="8" name="contact" id="contact" placeholder="Your business propositions, praise, complaints and/or threats" required></textarea>
<textarea rows="8" name="contact" id="contact" placeholder="Your business propositions, praise, complaints and/or threats" required>{form?.fields?.contact ?? ''}</textarea>
</label>
<div class="disclaimer">
<p>Disclaimer: I will only use the data you submit here (name, email, message) to respond. I will not pass it on to any third party. If I don't hear from you I will delete the data and keep no records of it.</p>
</div>
{#if form?.error}
<p class="form-error" role="alert" aria-live="polite">{form.error}</p>
{/if}
<div class="send">
<button class="button button--xl button--primary" type="submit">Send it!</button>
</div>
@ -266,6 +270,15 @@
margin: 1em auto;
font-size: 1rem;
}
.form-error {
max-width: var(--form-maxwidth);
margin: 0 auto 1em auto;
padding: .75em;
border: 1px solid var(--color-text);
border-radius: 4px;
font-size: .95em;
background: rgba(255, 255, 255, 0.08);
}
.send {
max-width: var(--form-maxwidth);
margin: auto;

View file

@ -1,4 +1,4 @@
export const prerender = true
export const prerender = false
export function load() {
return {
title: 'Contact me!',

View file

@ -5,23 +5,25 @@
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { onMount } from 'svelte';
let canvas: HTMLCanvasElement;
let introDone = false;
onMount( () => {
// Object pool for PIXI.Text objects
const textPool: PIXI.Text[] = [];
onMount(() => {
let highLightColor = window.getComputedStyle(document.body).getPropertyValue('--color-highlight');
let is_fine = window.matchMedia('(pointer:fine)').matches
let is_landscape = window.matchMedia('(orientation:landscape)').matches
gsap.registerPlugin(ScrollTrigger);
let app = new PIXI.Application({
resizeTo: window,
antialias: true,
autoDensity: true,
autoDensity: true,
resolution: 2,
backgroundColor: 'rgb(255, 255, 255)',
backgroundAlpha: 0,
@ -31,7 +33,7 @@
let textgroup = new PIXI.Container();
textgroup.pivot.set(0, 0);
textgroup.x = 0;
textgroup.y = - window.innerHeight;
textgroup.y = -window.innerHeight;
textgroup.height = window.innerHeight;
textgroup.width = window.innerWidth;
app.stage.addChild(textgroup);
@ -40,37 +42,50 @@
let fontSize = window.innerHeight / 3;
function createText(string: string){
let text = new PIXI.Text(string,
{ fontFamily: 'Stratos',
fontSize: fontSize,
fontWeight: '800',
fontStyle: 'italic',
lineHeight: 0,
letterSpacing: -10,
fill: highLightColor,
padding: 0
}
);
function createText(string: string): PIXI.Text {
let text = getTextFromPool();
text.text = string;
text.style = {
fontFamily: 'Stratos',
fontSize: fontSize,
fontWeight: '800',
fontStyle: 'italic',
lineHeight: 0,
letterSpacing: -10,
fill: highLightColor,
padding: 0
};
text.anchor.set(0);
textgroup.addChild(text);
return text
}
// Text object pool management
function getTextFromPool(): PIXI.Text {
if (textPool.length > 0) {
return textPool.pop() as PIXI.Text;
}
return new PIXI.Text();
}
function returnTextToPool(text: PIXI.Text) {
textPool.push(text);
}
let allTexts = [
createText('SERVICES SERVICES '), createText('SERVICES SERVICES '),
createText('CONSULTATION DESIGN WEB DEVELOPMENT '), createText('CONSULTATION DESIGN WEB DEVELOPMENT '),
createText('SERVICES SERVICES '), createText('SERVICES SERVICES '),
createText('CONSULTATION DESIGN WEB DEVELOPMENT '), createText('CONSULTATION DESIGN WEB DEVELOPMENT '),
createText('SERVICES SERVICES '), createText('SERVICES SERVICES '),
createText('CONSULTATION DESIGN WEB DEVELOPMENT '), createText('CONSULTATION DESIGN WEB DEVELOPMENT ')
]
let textRows = [
[allTexts[0], allTexts[1]],
[allTexts[2], allTexts[3]],
[allTexts[4], allTexts[5]],
[allTexts[6], allTexts[7]]
]
textRows.forEach( (row, index) => {
textRows.forEach((row, index) => {
if (index % 2 === 0) {
row[0].x = 0;
row[1].x = row[0].width;
@ -82,7 +97,7 @@
row[1].y = (fontSize * 0.8 * index);
})
let textRowsright = gsap.to([textRows[0], textRows[2]], {
let textRowsright = gsap.to([textRows[0], textRows[2]], {
x: '+=' + -textRows[0][0].width,
duration: 10,
ease: 'none',
@ -90,7 +105,7 @@
overwrite: true
})
tweens.push(textRowsright);
let textRowsLeft = gsap.to([textRows[1], textRows[3]], {
x: '+=' + textRows[1][0].width,
duration: 10,
@ -136,14 +151,20 @@
mouse.y = e.clientY / window.innerHeight;
}, { passive: true })
// Optimize ticker by using requestAnimationFrame
let animationFrameId: number;
let elapsed = 0;
app.ticker.add((delta) => {
elapsed += delta;
function animate() {
elapsed += app.ticker.elapsedMS / 16.6667; // Convert to delta time
bulgefilter.center = new PIXI.Point(mouse.x, 0.5);
})
animationFrameId = requestAnimationFrame(animate);
}
animate();
let scrollTriggerTweens: Array<GSAPTween> = [];
function createScrollTrigger() {
const tween1 = gsap.to([textRows[0], textRows[1]], {
y: '+=' + -textRows[0][0].height * 0.9,
@ -173,23 +194,23 @@
}
return () => {
cancelAnimationFrame(animationFrameId);
// IMPORTANT: Kill ScrollTrigger and GSAP animations FIRST,
// before destroying PixiJS objects they reference
scrollTriggerTweens.forEach( tween => {
scrollTriggerTweens.forEach(tween => {
if (tween.scrollTrigger) {
tween.scrollTrigger.kill();
}
tween.kill();
});
// Kill all other tweens
tweens.forEach( tween => tween.kill() );
tweens.forEach(tween => tween.kill());
// Return objects to pools
allTexts.forEach(text => returnTextToPool(text));
// Now safe to destroy PixiJS app
app.destroy(true, { children: true, texture: true, baseTexture: true });
}
})
</script>
<canvas bind:this={canvas} id="service-canvas"></canvas>
@ -204,4 +225,5 @@
z-index: 2;
pointer-events: none;
}
</style>
</style>

View file

@ -129,18 +129,18 @@
overwrite: true,
})
}
// Hover states or .work
// Hover states of .work
works.forEach((work, index) => {
work.addEventListener('mouseenter', (e) => {
if (isZoomed) return;
gsap.to(work, {
backgroundColor: 'var(--color-bg)',
duration: .5,
duration: .125,
ease: 'power1.inOut',
})
gsap.to(work.querySelector('.work-logo'), {
color: 'var(--color-highlight)',
duration: .5,
duration: .125,
ease: 'power1.inOut',
})
})
@ -148,12 +148,12 @@
if (isZoomed) return;
gsap.to(work, {
backgroundColor: 'var(--color-highlight)',
duration: .5,
duration: .125,
ease: 'power1.inOut',
})
gsap.to(work.querySelector('.work-logo'), {
color: 'var(--color-bg)',
duration: .5,
duration: .125,
ease: 'power1.inOut',
})
})

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.