Finished initial site

This commit is contained in:
Hug Tao 2025-02-02 19:06:14 +01:00
parent d82ac04c81
commit f4a925ace3
25 changed files with 2480 additions and 289 deletions

View file

@ -1,48 +0,0 @@
# Astro Starter Kit: Basics
```sh
npm create astro@latest -- --template basics
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
│ └── favicon.svg
├── src/
│ ├── layouts/
│ │ └── Layout.astro
│ └── pages/
│ └── index.astro
└── package.json
```
To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/).
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

View file

@ -1,5 +1,19 @@
// @ts-check
import { defineConfig } from 'astro/config';
import alpinejs from '@astrojs/alpinejs';
import tailwind from '@astrojs/tailwind';
import svelte from '@astrojs/svelte';
//import node from '@astrojs/node';
// https://astro.build/config
export default defineConfig({});
export default defineConfig({
integrations: [alpinejs({ entrypoint: '/src/entrypoint' }), tailwind(), svelte()],
experimental: {
responsiveImages: true,
},
});

View file

@ -9,6 +9,30 @@
"astro": "astro"
},
"dependencies": {
"astro": "^5.1.7"
"@alpinejs/collapse": "^3.14.8",
"@astrojs/alpinejs": "^0.4.2",
"@astrojs/node": "^9.0.2",
"@astrojs/svelte": "^7.0.4",
"@astrojs/tailwind": "^5.1.4",
"@fontsource-variable/inter": "^5.1.1",
"@fontsource-variable/sora": "^5.1.1",
"@fontsource/inter": "^5.1.1",
"@phosphor-icons/web": "^2.1.1",
"@theatre/core": "^0.7.2",
"@theatre/studio": "^0.7.2",
"@threlte/core": "^8.0.0",
"@threlte/extras": "^9.0.0",
"@threlte/theatre": "^3.0.0",
"@types/alpinejs": "^3.13.11",
"@types/three": "^0.172.0",
"alpinejs": "^3.14.8",
"astro": "^5.1.7",
"nodemailer": "^6.10.0",
"simplex-noise": "^4.0.3",
"svelte": "^5.19.1",
"tailwindcss": "^3.4.17",
"three": "^0.172.0",
"typescript": "^5.7.3",
"zod": "^3.24.1"
}
}

File diff suppressed because it is too large Load diff

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

Before

Width:  |  Height:  |  Size: 749 B

BIN
public/me.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/noise.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,34 @@
uniform float uTime;
uniform vec3 uColor1;
uniform vec3 uColor2;
uniform vec3 uColor3;
uniform vec3 uWhite;
uniform float uWhitenStrength;
uniform float uScatterStrength;
uniform sampler2D uNoiseTexture;
uniform float uNoiseScale;
uniform float uAmplitude;
varying float vDepth;
varying vec3 vPos;
void main() {
// Animated noise movement over time
vec2 noiseUV = vPos.xy * uNoiseScale*0.1 + vec2(uTime * 0.002, uTime * 0.0015);
float noiseValue = texture2D(uNoiseTexture, noiseUV).r; // Sampled noise height
// Normalize noise value and scale it between 0 and 1
float t = clamp(noiseValue, 0.0, 1.0);
// Blend between uColor1 and uColor2 based on noise value
vec3 baseColor = mix(uColor2, uColor3, t);
// Introduce uColor3 dynamically based on noise variation
baseColor = mix(baseColor, uColor3, smoothstep(0.3, 0.7, t));
// Fog-like whiten effect (applies at both near and far)
float whitenFactor = smoothstep(0.0, uWhitenStrength, abs(vDepth - 24.0));
baseColor = mix(baseColor, uWhite, whitenFactor);
gl_FragColor = vec4(baseColor, 1.0);
}

View file

@ -0,0 +1,41 @@
uniform float uTime;
uniform sampler2D uNoiseTexture;
uniform float uNoiseScale;
uniform float uAmplitude;
uniform float uScatterStrength; // Strength of outward scattering
varying float vDepth;
varying vec3 vPos;
void main() {
vec3 pos = position;
// Sample noise texture for height displacement
vec2 noiseUV = pos.xy * uNoiseScale + uTime / 512.0;
float noise = texture2D(uNoiseTexture, noiseUV).r;
// Apply vertical displacement from noise
pos.z += noise * uAmplitude;
// Compute outward movement vector (normalized direction from origin)
vec3 direction = normalize(pos); // Unit vector pointing away from (0,0,0)
// Compute scatter amount based on strength and time
float scatterAmount = uScatterStrength * 0.05; // Scale scatter movement
// Apply outward movement and random offset
pos += (direction * scatterAmount);
// Pass depth and position info
vDepth = length(cameraPosition - pos);
vPos = pos;
// Final position transformation
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
// Adjust point size based on distance
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
gl_PointSize = 12.0 * (5.0 / -mvPosition.z);
}

View file

@ -0,0 +1,253 @@
<script lang="ts">
import { fade } from "svelte/transition";
import { contactSchema } from "../schema/contact";
let formData = $state({
name: "",
email: "",
phone: "",
service: "",
message: "",
honeypot: "",
});
let inputHadFocus = $state({
name: false,
email: false,
phone: false,
service: false,
message: false,
});
let errors = $derived.by(() => {
// Validate form data using Zod
const result = contactSchema.safeParse(formData);
// If validation fails, update errors
if (!result.success) {
let errors: any = {};
result.error.errors.forEach((err: any) => {
errors[err.path[0]] = err.message;
});
console.log(errors);
return errors;
}
// Reset errors
return {};
});
let isFormOK = $derived.by(() => {
return Object.keys(errors).length === 0;
});
//$inspect(errors, isFormOK);
async function postData() {
if (!isFormOK) return;
try {
const response = await fetch("./api/contact", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
if (!response.ok) {
alert("Form submission failed");
throw new Error("Form submission failed");
}
alert("Daten wurden erfolgreich übertragen!");
formData = {
name: "",
email: "",
phone: "",
service: "",
message: "",
honeypot: "",
};
} catch (error) {
alert("Something went wrong, please try again later");
}
}
</script>
<div class="rounded-lg p-6 md:p-8 col-span-6">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Kontakt aufnehmen</h2>
<p class="text-gray-600 mb-6">
Neugierig, wie Deine Kampagne noch besser werden kann? Wähle Dein
gewünschtes Anliegen und sende mir eine Nachricht.
</p>
<form class="space-y-6 text-md relative z-50">
<!-- Name -->
<div>
<label class="block mb-1 font-semibold text-xs" for="name"
>Dein Name*</label
>
<input
type="text"
id="name"
name="name"
bind:value={formData.name}
class="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-orange-500 focus:outline-none"
placeholder="John Marketing"
onblur={() => (inputHadFocus.name = true)}
onfocus={() => (inputHadFocus.name = false)}
/>
{#if errors.name && inputHadFocus.name === true}
<p
transition:fade={{ duration: 100 }}
class="text-red-500 text-xs absolute r-0 t-0"
>
{errors.name}
</p>
{/if}
</div>
<!-- Email -->
<div>
<label class="block font-semibold text-xs mb-1" for="email">E-Mail*</label
>
<input
type="email"
id="email"
name="email"
bind:value={formData.email}
class="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-orange-500 focus:outline-none"
placeholder="your@mail.com"
onblur={() => (inputHadFocus.email = true)}
onfocus={() => (inputHadFocus.email = false)}
/>
{#if errors.email && inputHadFocus.email === true}
<p
transition:fade={{ duration: 100 }}
class="text-red-500 text-xs absolute r-0 t-0"
>
{errors.email}
</p>
{/if}
</div>
<!-- Phone -->
<div>
<label class="block font-semibold text-xs mb-1" for="phone"
>Telefonnummer</label
>
<input
type="tel"
id="phone"
name="phone"
bind:value={formData.phone}
class="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-orange-500 focus:outline-none"
placeholder="+49 123 456 789"
onblur={() => (inputHadFocus.phone = true)}
onfocus={() => (inputHadFocus.phone = false)}
/>
{#if errors.phone && inputHadFocus.phone === true}
<p
transition:fade={{ duration: 100 }}
class="text-red-500 text-xs absolute r-0 t-0"
>
{errors.phone}
</p>
{/if}
</div>
<!-- Service Selection -->
<div class="relative">
<label class="block font-semibold text-xs mb-1" for="service"
>Deine Anfrage*</label
>
<select
id="service"
name="service"
bind:value={formData.service}
class="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-orange-500 focus:outline-none"
onblur={() => (inputHadFocus.service = true)}
onfocus={() => (inputHadFocus.service = false)}
>
<option value="" disabled selected>Bitte wählen...</option>
<option value="Technische Beratung">Technische Beratung</option>
<option value="Digitale Werbemittel">Digitale Werbemittel</option>
<option value="Website Development">Website Development</option>
<option value="Datenvisualisierung">Datenvisualisierung</option>
<option value="Marketing-Tools Integration"
>Marketing-Tools Integration</option
>
<option value="Conversion-Optimierung">Conversion-Optimierung</option>
<option value="Plugin Entwicklung">Plugin Entwicklung</option>
<option value="Tooling">Tooling</option>
<option value="Allgemeine Anfrage">Etc.</option>
</select>
{#if errors.service && inputHadFocus.service === true}
<p
transition:fade={{ duration: 100 }}
class="text-red-500 text-xs absolute r-0 t-0"
>
{errors.service}
</p>
{/if}
</div>
<!-- Message -->
<div>
<label class="block font-semibold text-xs mb-1" for="message"
>Nachricht* {formData.message.length} / 500</label
>
<textarea
id="message"
name="message"
bind:value={formData.message}
rows="5"
class="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-orange-500 focus:outline-none"
placeholder="Schreibe deine Nachricht hier..."
onblur={() => (inputHadFocus.message = true)}
onfocus={() => (inputHadFocus.message = false)}
></textarea>
{#if errors.message && inputHadFocus.message === true}
<p
transition:fade={{ duration: 100 }}
class="text-red-500 text-xs absolute r-0 t-0"
>
{errors.message}
</p>
{/if}
</div>
<!-- Honeypot Field -->
<div style="display: none;">
<input
type="text"
name="honeypot"
bind:value={formData.honeypot}
class="honeypot-field"
autocomplete="off"
/>
</div>
<!-- Submit Button -->
<div>
<button
type="submit"
disabled={!isFormOK}
onclick={postData}
class="disabled:border-slate-400 disabled:text-slate-400 border-2 w-full border-slate-800 font-semibold px-6 py-3 rounded-md text-lg transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-lg"
>
<span>Anfrage senden</span>
<i class="ph ph-chat-text font-extrabold text-xl pl-2"></i>
</button>
</div>
</form>
</div>
<style>
.honeypot-field {
display: none;
}
</style>

190
src/components/Hero.astro Normal file
View file

@ -0,0 +1,190 @@
<script>
import {
Scene,
PerspectiveCamera,
WebGLRenderer,
BufferGeometry,
BufferAttribute,
TextureLoader,
ShaderMaterial,
Color,
Points,
RepeatWrapping,
} from "three";
import vertexShader from "../assets/shaders/vertex.glsl?raw";
import fragmentShader from "../assets/shaders/fragment.glsl?raw";
window.addEventListener("load", () => {
const gridX = 256; // Number of points per row/column
const gridY = 256; // Number of points per row/column
const spacing = 0.06; // Distance between points
const numPoints = gridX * gridY;
const scene = new Scene();
const camera = new PerspectiveCamera(
15,
window.innerWidth / window.innerHeight,
0.1,
1000,
);
camera.position.z = 24;
camera.position.x = 0;
camera.position.y = -2.0;
const renderer = new WebGLRenderer({
antialias: true,
preserveDrawingBuffer: true,
alpha: true,
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById("three-container").appendChild(renderer.domElement);
// Create a grid of points
const geometry = new BufferGeometry();
const positions = new Float32Array(numPoints * 3);
let index = 0;
for (let y = 0; y < gridY; y++) {
const yOffset = (y - gridY / 2) * spacing; // Y position
for (let x = 0; x < gridX; x++) {
const xOffset = (x - gridX / 2) * spacing;
const xPosition = xOffset + (y % 2 === 0 ? 0 : spacing / 2); // ✅ Offset every second row
positions[index++] = xPosition; // X position
positions[index++] = yOffset; // Y position
positions[index++] = 0; // Z position (flat initially)
}
}
geometry.setAttribute("position", new BufferAttribute(positions, 3));
// Load the noise texture asynchronously
const loader = new TextureLoader();
const noiseTex = loader.load("./noise.png", () => {
// Texture has finished loading, now we can safely start the scene rendering
noiseTex.wrapS = RepeatWrapping;
noiseTex.wrapT = RepeatWrapping;
// Create shader material after texture is loaded
const material = new ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
uTime: { value: Math.random() * 128 },
uNoiseTexture: { value: noiseTex },
uNoiseScale: { value: 0.01 },
uAmplitude: { value: 3.0 },
uColor1: { value: new Color("#ffb3ba") }, // Pastel Pink
uColor2: { value: new Color("#9EDEF2") }, // Pastel Blue
uColor3: { value: new Color("#E5FFE0") }, // Pastel Green
uWhite: { value: new Color("#FFFFFF") },
uWhitenStrength: { value: 1 },
uScatterStrength: { value: 1 },
},
});
const points = new Points(geometry, material);
scene.add(points);
// Function to update the grid scale based on camera
function updateGridScale() {
// Get visible width at the grid's depth
const fovRad = (camera.fov * Math.PI) / 180; // Convert FOV to radians
const visibleHeight = 2 * Math.tan(fovRad / 2) * camera.position.z; // Frustum height at depth
const visibleWidth = visibleHeight * camera.aspect; // Frustum width at depth
// Calculate original grid width
const originalGridWidth = (gridX - 1) * spacing - (4.0 * getScaleFactor() );
// Compute scale factor to match camera width
const scaleFactor = visibleWidth / originalGridWidth;
// Apply uniform scaling
points.scale.set(scaleFactor, scaleFactor, 1);
}
const breakpoints = {
"sm": 640, // Small screens
"md": 768, // Medium screens
"lg": 1024, // Large screens
"xl": 1280, // Extra large screens
"2xl": 1536, // 2XL screens
};
function getScaleFactor() {
let scaleFactor = 1;
if (window.innerWidth < breakpoints.sm) {
scaleFactor = 2.5; // Scale down for very small screens
} else if (window.innerWidth < breakpoints.md) {
scaleFactor = 2.5; // Default scale for small screens
} else if (window.innerWidth < breakpoints.lg) {
scaleFactor = 1.5;
} else if (window.innerWidth < breakpoints.xl) {
scaleFactor = 1.5;
} else if (window.innerWidth < breakpoints["2xl"]) {
scaleFactor = 1;
} else {
scaleFactor = 1.0; // Scale up for extra large screens
}
console.log(scaleFactor)
return scaleFactor
}
// Call function initially to set scale properly
updateGridScale();
// Animation loop
function animate() {
material.uniforms.uTime.value += 0.05;
material.uniforms.uScatterStrength.value =
(window.scrollY /
(document.documentElement.scrollHeight - window.innerHeight)) *
100;
renderer.render(scene, camera);
points.rotation.x =
(window.scrollY /
(document.documentElement.scrollHeight - window.innerHeight)) *
Math.PI *
0.02 +
Math.PI / 1.8;
requestAnimationFrame(animate);
}
animate();
// Handle window resizing
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
updateGridScale();
});
});
});
</script>
<div id="three-container"></div>
<style is:global>
canvas,
#three-container {
position: fixed;
top: 0;
left: 0;
display: block;
width: 100vw;
height: 100vh;
z-index: -1;
overflow: hidden;
}
</style>

View file

@ -0,0 +1,16 @@
---
const { title ="Title", description = "This is your description", tags=["Example"] } = Astro.props;
---
<div class="xl:p-3 lg:p-2 md:p-4 p-4 rounded-lg">
<a :href="item.link" class="block">
<h2 class="text-xl font-semibold">{title}</h2>
<p class="mt-2">{description}</p>
</a>
<div class="mt-4 flex gap-2 flex-wrap ">
{tags.map((item: string) => (
<div class="bg-gray-200 px-2 py-1 rounded text-ellipsis whitespace-nowrap shadow-xs text-xs font-bold">{item}</div>
))}
</div>
</div>

View file

@ -1,209 +0,0 @@
---
import astroLogo from '../assets/astro.svg';
import background from '../assets/background.svg';
---
<div id="container">
<img id="background" src={background.src} alt="" fetchpriority="high" />
<main>
<section id="hero">
<a href="https://astro.build"
><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
>
<h1>
To get started, open the <code><pre>src/pages</pre></code> directory in your project.
</h1>
<section id="links">
<a class="button" href="https://docs.astro.build">Read our docs</a>
<a href="https://astro.build/chat"
>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
><path
fill="currentColor"
d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
></path></svg
>
</a>
</section>
</section>
</main>
<a href="https://astro.build/blog/astro-5/" id="news" class="box">
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
><path
d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
fill="#111827"></path></svg
>
<h2>What's New in Astro 5.0?</h2>
<p>
From content layers to server islands, click to learn more about the new features and
improvements in Astro 5.0
</p>
</a>
</div>
<style>
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
filter: blur(100px);
}
#container {
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
height: 100%;
}
main {
height: 100%;
display: flex;
justify-content: center;
}
#hero {
display: flex;
align-items: start;
flex-direction: column;
justify-content: center;
padding: 16px;
}
h1 {
font-size: 22px;
margin-top: 0.25em;
}
#links {
display: flex;
gap: 16px;
}
#links a {
display: flex;
align-items: center;
padding: 10px 12px;
color: #111827;
text-decoration: none;
transition: color 0.2s;
}
#links a:hover {
color: rgb(78, 80, 86);
}
#links a svg {
height: 1em;
margin-left: 8px;
}
#links a.button {
color: white;
background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
inset 0 -2px 0 rgba(0, 0, 0, 0.24);
border-radius: 10px;
}
#links a.button:hover {
color: rgb(230, 230, 230);
box-shadow: none;
}
pre {
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas,
'DejaVu Sans Mono', monospace;
font-weight: normal;
background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
}
h2 {
margin: 0 0 1em;
font-weight: normal;
color: #111827;
font-size: 20px;
}
p {
color: #4b5563;
font-size: 16px;
line-height: 24px;
letter-spacing: -0.006em;
margin: 0;
}
code {
display: inline-block;
background:
linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
border-radius: 8px;
border: 1px solid transparent;
padding: 6px 8px;
}
.box {
padding: 16px;
background: rgba(255, 255, 255, 1);
border-radius: 16px;
border: 1px solid white;
}
#news {
position: absolute;
bottom: 16px;
right: 16px;
max-width: 300px;
text-decoration: none;
transition: background 0.2s;
backdrop-filter: blur(50px);
}
#news:hover {
background: rgba(255, 255, 255, 0.55);
}
@media screen and (max-height: 368px) {
#news {
display: none;
}
}
@media screen and (max-width: 768px) {
#container {
display: flex;
flex-direction: column;
}
#hero {
display: block;
padding-top: 10%;
}
#links {
flex-wrap: wrap;
}
#links a.button {
padding: 14px 18px;
}
#news {
right: 16px;
left: 16px;
bottom: 2.5rem;
max-width: 100%;
}
h1 {
line-height: 1.5;
}
}
</style>

6
src/entrypoint.ts Normal file
View file

@ -0,0 +1,6 @@
import type { Alpine } from 'alpinejs'
import collapse from '@alpinejs/collapse'
export default (Alpine: Alpine) => {
Alpine.plugin(collapse)
}

View file

@ -1,11 +1,20 @@
---
import '@fontsource-variable/sora';
import "@phosphor-icons/web/regular";
import "@phosphor-icons/web/bold";
const isProd = import.meta.env.PROD;
---
<!doctype html>
<html lang="en">
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
<meta name="generator" content={Astro.generator} />
<title>Astro Basics</title>
{isProd && <script defer src="https://umami.merli.one/script.js" data-website-id="418105c9-7c18-4ee4-b268-00167070754a"></script>}
<title>Oskar Wolnarek</title>
</head>
<body>
<slot />
@ -13,6 +22,7 @@
</html>
<style>
html,
body {
margin: 0;

32
src/pages/404.astro Normal file
View file

@ -0,0 +1,32 @@
---
import Layout from "../layouts/Layout.astro";
---
<Layout>
<div class="min-h-screen flex flex-col justify-center items-center text-slate-700">
<div class="text-center">
<!-- Error Code -->
<h1 class="text-9xl font-black bg-gradient-to-r from-red-500 to-orange-500 bg-clip-text text-transparent">
404
</h1>
<!-- Error Message -->
<h2 class="text-3xl font-bold mt-4">Oops! Seite nicht gefunden.</h2>
<p class="mt-2 text-lg ">
Die Seite, nach der du suchst, existiert nicht oder wurde verschoben.
</p>
<!-- Call-to-Action Button -->
<div class="pt-12">
<a
href="/"
class="bg-gradient-to-r from-red-500 to-orange-500 text-white font-bold px-8 py-4 rounded-md hover:shadow-lg transition-all duration-300 hover:scale-105"
>
Zurück zur Startseite
</a>
</div>
</div>
</div>
</Layout>

106
src/pages/api/contact.ts Normal file
View file

@ -0,0 +1,106 @@
export const prerender = true;
import type { APIRoute } from "astro";
import {contactSchema} from "../../schema/contact";
import nodemailer from "nodemailer";
// MXRoute SMTP configuration
const smtpConfig = {
host: "pixel.mxrouting.net", // MXRoute SMTP server
port: 465, // Port for SSL
secure: true, // Use SSL
auth: {
user: "system@merlione.com",
pass: "RrZw9aqZjpJ9SBsrQxS2",
},
};
// Create a Nodemailer transporter
const transporter = nodemailer.createTransport(smtpConfig);
// Function to send "Hello World" email
async function sendEmail(data) {
try {
const info = await transporter.sendMail({
from: {
name: 'wolnarek.de',
address: 'system@merlione.com'
},
to: "hello@wolnarek.de",
subject: "Neue Anfrage von: " + data.name + "über " + data.service,
html:
`<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Anfrage wolnarek.de</title>
</head>
<body>
<h1>Neue Anfrage von wolnarek.de</h1>
<p>Du hast eine neue Anfrage mit den folgenden Details erhalten:</p>
<ul>
<li><strong>Name:</strong> ${data.name}</li>
<li><strong>Email:</strong> ${data.email}</li>
<li><strong>Phone:</strong> ${data.phone}</li>
<li><strong>Service:</strong> ${data.service}</li>
</ul>
<h2>
Nachricht:
</h2>
<p>${data.message}</p>
<p>Thank you!</p>
</body>
</html>`
});
} catch (error) {
console.error("Error sending email:", error);
}
}
export const POST: APIRoute = async ({ request }) => {
if (request.headers.get("Content-Type") !== "application/json") return;
const body = await request.json();
let formData = {
name: body.name,
email: body.email,
phone: body.phone,
service: body.service,
message: body.message,
honeypot: body.honeypot,
}
const parsedResult = contactSchema.safeParse(formData);
if (parsedResult.success) {
// Do something with the data, then return a success response
//let response = await sendEmail(formData)
//console.log(response)
return new Response(
JSON.stringify({
message: "Nachricht wurde erfolgreich zugestellt!"
}),
{ status: 200 }
);
} else {
return new Response(
JSON.stringify({
message: "Es gab Probleme bei der Verarbeitung Ihrer Anfrage. Versuchen Sie es bitte später noch einmal.",
}),
{ status: 400 }
);
}
};

View file

@ -1,11 +1,341 @@
---
import Welcome from '../components/Welcome.astro';
import Layout from '../layouts/Layout.astro';
import Layout from "../layouts/Layout.astro";
import Services from "../components/Services.astro";
// import Contact from "../components/Contact.svelte"
import Hero from "../components/Hero.astro";
// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build
// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
---
<Layout>
<Welcome />
<header id="header" class="bg-slate-800 shadow-xl text-white hidden">
<div class="container mx-auto px-4">
<div
class="flex flex-row gap-8 justify-center items-center p-1 font-semibold"
>
<h2 class="text-sm hidden">Sag Hallo!</h2>
<!-- Phone -->
<div class="flex items-center space-x-3">
<a
class="text-sm flex items-center space-x-2 p-1 sm:p-0"
href="tel:+4915679601116"
>
<i class="ph ph-phone sm:text-xl text-2xl"></i>
<span class="sm:block hidden">+49 15679 601116</span>
</a>
</div>
<!-- Email -->
<div class="flex items-center space-x-3">
<a
class="text-sm flex items-center space-x-2 p-1 sm:p-0"
href="mailto:hello@wolnarek.de"
>
<i class="ph ph-envelope sm:text-xl text-2xl"></i>
<span class="sm:block hidden">hello@wolnarek.de</span>
</a>
</div>
<!-- WhatsApp -->
<div class="flex items-center space-x-3">
<a
class="text-sm flex items-center space-x-2 p-1 sm:p-0"
href="https://wa.me/4915679601116?text=Guten%20Tag%2C%20ich%20bin%20interessiert%20an%20einer%20Zusammenarbeit%20und%20würde%20mich%20freuen%2C%20mehr%20darüber%20zu%20erfahren."
target="_blank"
>
<i class="ph ph-whatsapp-logo sm:text-xl text-2xl"></i>
<span class="sm:block hidden">WhatsApp</span>
</a>
</div>
<!-- LinkedIn -->
<div class="flex items-center space-x-3">
<a
class="text-sm flex items-center space-x-2 p-1 sm:p-0"
href="https://www.linkedin.com/in/wolnarek"
target="_blank"
>
<i class="ph ph-linkedin-logo sm:text-xl text-2xl"></i>
<span class="sm:block hidden">LinkedIn</span>
</a>
</div>
</div>
</div>
</header>
<div
id="wrapper"
class="min-h-screen pt-0 sm:pt-12 z-50 text-gray-800 subpixel-antialiased"
>
<div class="container mx-auto px-4 mb-24">
<div class="flex justify-center items-center">
<div class="w-full md:w-8/12 text-center mt-24">
<div class="mb-12">
<h1 class="md:text-4xl text-xl font-black">
Hi, ich bin <span
class="bg-gradient-to-r from-rose-500 via-purple-500 to-rose-500 bg-clip-text text-transparent"
>
Oskar Wolnarek
</span> Consultant für Ad Tech & Digitale
Kampagnen.
</h1>
<p
class="sm:text-2xl text-lg mt-12 leading-relaxed font-normal"
>
<span
class="bg-white/50 px-2 box-decoration-clone rounded-md"
>
In der heutigen Welt entscheiden immer mehr
technologische Ansätze über den Erfolg von
Werbekampagnen. Ich helfe Unternehmen und Marken
dabei, das Beste aus ihrer digitalen Kampagne
herauszuholen mit maßgeschneiderten
technischen Lösungen und datengetriebenen
Strategien.
</span>
</p>
</div>
<div
class="mt-8 transform transition-all duration-300 ease-in-out hover:scale-105 flex items-center justify-center"
>
<a
href="#Contact"
class="bg-gradient-to-r transition-all from-rose-500 to-red-500 text-white font-bold px-8 py-4 rounded-md hover:shadow-lg flex flex-row items-center"
>
<span>Kontaktieren & Durchstarten!</span>
<i
class="ph ph-chat-text font-extrabold text-xl pl-2"
></i>
</a>
</div>
</div>
</div>
</div>
<div class="container mx-auto px-4 mt-12 w-full md:w-8/12">
<h1
class="md:text-4xl text-xl font-black text-left lg:text-center lg:p-0 pl-4 mb-2"
>
Meine Services
</h1>
<div
class="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6"
>
<Services
title="Technische Beratung & Automatisierung"
description="Zusammen identifizieren wir manuelle Prozesse und implementieren maßgeschneiderte Automatisierungen für mehr Zeit, die wirklich zählt."
tags={["Systemintegration", "Google Apps Script"]}
/>
<Services
title="Dynamische & interaktive Werbemittel"
description="Werbemittel, die im Gedächtnis bleiben: Mit performanten Animationen und präzisem Tracking für aussagekräftige Kampagnen-Analysen."
tags={["GSAP", "CreateJS", "Rich Media Ads"]}
/>
<Services
title="Frontend-Entwicklung & Web Performance"
description="Gemeinsam entwickeln wir schnelle, schlanke Websites, die Nutzer lieben von der ersten Idee bis zum Lighthouse-Score von 90+"
tags={["React", "Vue.js", "Svelte", "Lighthouse", "SEO"]}
/>
<Services
title="Datenvisualisierung & Performance Dashboards"
description="Verwandele deine Kampagnendaten in klare Handlungsempfehlungen: Interaktive Dashboards, die Teamentscheidungen datenbasiert und nachvollziehbar machen."
tags={["Google Data Studio", "Tableau", "Power BI"]}
/>
<Services
title="Integrationen in CRM- & Marketing-Tools"
description="Kombiniere Tracking und CRM-Systeme ohne Medienbrüche: Nahtlose Integrationen zwischen Analytics, CRM und deinen Kampagnen-Tools."
tags={["Google Analytics", "HubSpot", "Salesforce"]}
/>
<Services
title="Mobile First Design & Entwicklung"
description="Kampagnen, die auf jedem Gerät überzeugen: Mobile-Erlebnisse, die Conversions bringen nicht nur Auflösungen anpassen"
tags={["AMP", "PWA", "UX/UI-Design", "Cross-Platform"]}
/>
<Services
title="Datenmanagement & Data-Driven Campaigns"
description="Struktur für deine Datenflut: Wir schaffen die Grundlage für Kampagnen, die auf echten Insights statt auf Bauchgefühl basieren."
tags={["Data Warehousing", "Data-Driven Marketing"]}
/>
<Services
title="API-basierte Marketing-Tools"
description="Automatisierte Workflows statt manueller Übertragungen: Verbinde deine Marketing-Tools über APIs und moderne Scripting Schnittstellen."
tags={["RESTful APIs", "Webhooks", "Zapier"]}
/>
<Services
title="Ad-Server & Programmatic Advertising"
description="Effizienz durch Automatisierung: Beratung und Umsetzung für präzise Targeting-Strategien in der programmatischen Werbung."
tags={["Google DoubleClick", "AdRoll", "OpenRTB"]}
/>
<Services
title="Conversion-starke Landing Pages"
description="Mehr Conversions durch klare User Journeys: Sorgfältig optimierte Landing Pages, die Besucher Schritt für Schritt überzeugen."
tags={["PageSpeed Insights", "CRO", "Lighthouse"]}
/>
<Services
title="Social Media Automation"
description="Planungshilfe für Content-Teams: Automatisierte Publishing-Workflows, die Raum für kreative Kampagnen-Entwicklung schaffen."
tags={["Hootsuite", "Buffer", "Sprout Social"]}
/>
<Services
title="UXP Tools für Adobe Creative Cloud"
description="Effizienz-Tools für Designer: Individuelle Plugins, die repetitive Tasks in Photoshop & After Effects reduzieren und dir wertvolle Zeit sparen."
tags={["Adobe UXP", "AE Expressions", "Plugins"]}
/>
</div>
</div>
<div id="Contact" class="container mx-auto py-16 p-1 sm:px-6 my-36 relative w-full 2xl:w-7/12 xl:w-9/12 md:11/12 max-w-5xl">
<!-- Grid Layout for 2 Columns -->
<div class=" gap-8 items-center relative">
<!-- About Me Section -->
<div
class="relative bg-white border-slate-300 border rounded-lg shadow-lg shadow-slate-400/50 md:p-16 p-8 pt-24"
>
<!-- Image Section -->
<div class="flex justify-center items-center absolute -top-24 left-1/2 transform -translate-x-1/2">
<img
src="me.jpg"
alt="Portrait of Oskar Wolnarek"
class="rounded-full w-48 h-48 object-cover shadow-lg"
/>
</div>
<h1 class="text-3xl font-black mb-6">Das bin Ich</h1>
<div class="text-left font-medium">
<p class="text-lg leading-relaxed">
Ich bin Oskar Wolnarek, ein erfahrener Senior
Developer mit mehr als 8 Jahren Expertise in der
Media- und Digitalmarketing-Branche.
</p>
<p class="text-lg leading-relaxed mt-4">
Während dieser Zeit habe ich sowohl lokale und
internationale Kampagnen erfolgreich umgesetzt,
komplexe Ad-Tech-Integrationen realisiert und
namhafte Kunden wie das BMVg, Lidl und BMW betreut.
Mein Fokus liegt auf der Schnittstelle zwischen
Technologie, Design und datengetriebenem Marketing
von performanten Frontend-Architekturen bis hin zu
automatisierten Workflows für digitale Werbesysteme.
</p>
<p class="text-lg leading-relaxed mt-4">
In meiner Freizeit genieße ich das Wandern und
Reisen, engagiere mich für den Erhalt alter, lokaler
Nutzpflanzen in meinem Garten und entwickle
leidenschaftlich Spiele mit <a
class="underline"
href="https://godotengine.org/"
target="_blank">Godot</a
>, einer Open Source Game Engine.
</p>
</div>
<!-- Contact Information in a Row -->
<div class="mt-8 flex flex-col lg:flex-row gap-6 lg:gap-12">
<!-- Phone -->
<div class="flex flex-col items-start">
<h3 class="font-bold text-sm mb-2 flex items-center space-x-2">
<i class="ph ph-phone text-xl"></i>
<span>Per Telefon</span>
</h3>
<a
class="text-lg p-1 sm:p-0 underline"
href="tel:+4915679601116"
>
+49 15679 601116
</a>
</div>
<!-- Mail -->
<div class="flex flex-col items-start">
<h3 class="font-bold text-sm mb-2 flex items-center space-x-2">
<i class="ph ph-envelope text-xl"></i>
<span>Per Mail</span>
</h3>
<a
class="text-lg p-1 sm:p-0 underline"
href="mailto:hello@wolnarek.de"
>
hello@wolnarek.de
</a>
</div>
<!-- WhatsApp -->
<div class="flex flex-col items-start">
<h3 class="font-bold text-sm mb-2 flex items-center space-x-2">
<i class="ph ph-whatsapp-logo text-xl"></i>
<span>WhatsApp</span>
</h3>
<a
class="text-lg p-1 sm:p-0 underline"
href="https://wa.me/4915679601116?text=Guten%20Tag%2C%20ich%20bin%20interessiert%20an%20einer%20Zusammenarbeit%20und%20würde%20mich%20freuen%2C%20mehr%20darüber%20zu%20erfahren."
target="_blank"
>
Nachricht schreiben
</a>
</div>
<!-- LinkedIn -->
<div class="flex flex-col items-start">
<h3 class="font-bold text-sm mb-2 flex items-center space-x-2">
<i class="ph ph-linkedin-logo text-xl"></i>
<span>LinkedIn</span>
</h3>
<a
class="text-lg p-1 sm:p-0 underline"
href="https://www.linkedin.com/in/wolnarek"
target="_blank"
>
Connect
</a>
</div>
</div>
</div>
</div>
</div>
<footer
class="bg-gradient-to-tl from-slate-900 to-slate-800 text-white py-6"
>
<div
class="container mx-auto px-4 flex flex-wrap flex-col lg:flex-row justify-center lg:items-center lg:text-sm items-start text-md gap-2 lg:gap-4"
>
<div class="flex items-center space-x-2">
<span class="font-black">Oskar Wolnarek</span>
</div>
<div class="flex items-center space-x-2">
<i class="ph ph-map-pin"></i>
<span>Nordstraße 5, 42489 Wülfrath, Deutschland</span>
</div>
<div class="flex items-center space-x-2">
<i class="ph ph-phone"></i>
<span>+49 15679 601116</span>
</div>
<div class="flex items-center space-x-2">
<i class="ph ph-envelope"></i>
<span>hello@wolnarek.de</span>
</div>
<div class="flex items-center space-x-2">
<i class="ph ph-globe"></i>
<a href="https://www.wolnarek.de" class="hover:underline"
>www.wolnarek.de</a
>
</div>
<div class="flex items-center space-x-2 hidden">
<i class="ph ph-briefcase"></i>
<span>USt-IdNr: DE123456789</span>
</div>
</div>
</footer>
</div>
<div class="fixed top-0 left-0 w-screen h-screen -z-10">
<div
class="fixed top-0 h-screen w-screen bg-gradient-to-t from-white via-white/25 to-transparent"
>
</div>
<Hero />
</div>
</Layout>

12
src/schema/contact.ts Normal file
View file

@ -0,0 +1,12 @@
import { z } from 'zod';
// Define schema for validation using Zod
export let contactSchema = z.object({
name: z.string().min(1, 'Name ist erforderlich.').max(128, 'Name ist zu lang.'),
email: z.string().email('Invalid email format.').min(1, 'Email darf nicht leer sein.').max(255, 'Email ist zu lang.'),
phone: z.string().regex(new RegExp(/^\+?[0-9 ]+$/), 'Ungültige Zeichen. Erlaubt sind nur Ziffern und Plus- und Leerzeichen.').max(32, 'Telefonnummer ist zu lang.').optional().or(z.literal('')),
service: z.string().min(1, 'Wähle bitte einen Service aus.'),
message: z.string().min(1, 'Nachricht darf nicht leer sein.').max(500, 'Nachricht ist zu lang.'),
// Honeypot field to detect bots
honeypot: z.string().optional(),
});

5
svelte.config.js Normal file
View file

@ -0,0 +1,5 @@
import { vitePreprocess } from '@astrojs/svelte';
export default {
preprocess: vitePreprocess(),
}

15
tailwind.config.mjs Normal file
View file

@ -0,0 +1,15 @@
/** @type {import('tailwindcss').Config} */
import defaultTheme from 'tailwindcss/defaultTheme'
export default {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
extend: {
fontFamily: {
sans: ['Sora', ...defaultTheme.fontFamily.sans],
},
},
},
plugins: [],
}

View file

@ -1,5 +1,7 @@
{
"baseUrl": "src",
"paths": { "@/*": ["*"] },
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}
}