implements brevo form, webfonts sh
This commit is contained in:
parent
bad6faf555
commit
8e2a5ed8bf
14 changed files with 220 additions and 71 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
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;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
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);
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
79
src/routes/contact/+page.server.ts
Normal file
79
src/routes/contact/+page.server.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export const prerender = true
|
||||
export const prerender = false
|
||||
export function load() {
|
||||
return {
|
||||
title: 'Contact me!',
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@
|
|||
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
|
||||
|
|
@ -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,23 +42,36 @@
|
|||
|
||||
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 '),
|
||||
|
|
@ -70,7 +85,7 @@
|
|||
[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',
|
||||
|
|
@ -136,11 +151,17 @@
|
|||
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> = [];
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -205,3 +226,4 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
})
|
||||
})
|
||||
|
|
|
|||
BIN
static/fonts/PPFormula-Extrabold.woff2
Normal file
BIN
static/fonts/PPFormula-Extrabold.woff2
Normal file
Binary file not shown.
BIN
static/fonts/PPFormula-Medium.woff2
Normal file
BIN
static/fonts/PPFormula-Medium.woff2
Normal file
Binary file not shown.
BIN
static/fonts/PPFormula-MediumItalic.woff2
Normal file
BIN
static/fonts/PPFormula-MediumItalic.woff2
Normal file
Binary file not shown.
BIN
static/fonts/Stratos-BoldItalic.woff2
Normal file
BIN
static/fonts/Stratos-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
static/fonts/Stratos-Regular.woff2
Normal file
BIN
static/fonts/Stratos-Regular.woff2
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue