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 // @ts-check
import { defineConfig } from 'astro/config'; 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 // 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" "astro": "astro"
}, },
"dependencies": { "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> <!doctype html>
<html lang="en"> <html lang="de">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" /> <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} /> <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> </head>
<body> <body>
<slot /> <slot />
@ -13,6 +22,7 @@
</html> </html>
<style> <style>
html, html,
body { body {
margin: 0; 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> <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> </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,4 +1,6 @@
{ {
"baseUrl": "src",
"paths": { "@/*": ["*"] },
"extends": "astro/tsconfigs/strict", "extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"], "include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"] "exclude": ["dist"]