Finished initial site
This commit is contained in:
parent
d82ac04c81
commit
f4a925ace3
25 changed files with 2480 additions and 289 deletions
48
README.md
48
README.md
|
|
@ -1,48 +0,0 @@
|
|||
# Astro Starter Kit: Basics
|
||||
|
||||
```sh
|
||||
npm create astro@latest -- --template basics
|
||||
```
|
||||
|
||||
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
|
||||
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
|
||||
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||

|
||||
|
||||
## 🚀 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).
|
||||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
26
package.json
26
package.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
1379
pnpm-lock.yaml
1379
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
|
|
@ -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
BIN
public/me.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
6
public/merlione_logo_white.svg
Normal file
6
public/merlione_logo_white.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 11 KiB |
BIN
public/noise.png
Normal file
BIN
public/noise.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
6
src/assets/img/merlione_logo_white.svg
Normal file
6
src/assets/img/merlione_logo_white.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 11 KiB |
34
src/assets/shaders/fragment.glsl
Normal file
34
src/assets/shaders/fragment.glsl
Normal 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);
|
||||
}
|
||||
41
src/assets/shaders/vertex.glsl
Normal file
41
src/assets/shaders/vertex.glsl
Normal 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);
|
||||
}
|
||||
253
src/components/Contact.svelte
Normal file
253
src/components/Contact.svelte
Normal 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
190
src/components/Hero.astro
Normal 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>
|
||||
16
src/components/Services.astro
Normal file
16
src/components/Services.astro
Normal 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>
|
||||
|
|
@ -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
6
src/entrypoint.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import type { Alpine } from 'alpinejs'
|
||||
import collapse from '@alpinejs/collapse'
|
||||
|
||||
export default (Alpine: Alpine) => {
|
||||
Alpine.plugin(collapse)
|
||||
}
|
||||
|
|
@ -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
32
src/pages/404.astro
Normal 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
106
src/pages/api/contact.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -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
12
src/schema/contact.ts
Normal 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
5
svelte.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { vitePreprocess } from '@astrojs/svelte';
|
||||
|
||||
export default {
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
||||
15
tailwind.config.mjs
Normal file
15
tailwind.config.mjs
Normal 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: [],
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"baseUrl": "src",
|
||||
"paths": { "@/*": ["*"] },
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue