Merge branch 'main' into codex/add-3d-traceroute-visualization-app
This commit is contained in:
+1906
-332
File diff suppressed because it is too large
Load Diff
@@ -126,37 +126,16 @@ $positions = generatePositions($traceData);
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 0;
|
|
||||||
}
|
}
|
||||||
#canvas-container {
|
#canvas-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 420px;
|
|
||||||
background: radial-gradient(circle at 20% 20%, rgba(59, 130, 246, 0.15), transparent 55%),
|
|
||||||
radial-gradient(circle at 80% 30%, rgba(45, 212, 191, 0.12), transparent 45%),
|
|
||||||
#020617;
|
|
||||||
}
|
}
|
||||||
#scene {
|
#scene {
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
#canvas-overlay {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
background: rgba(15, 23, 42, 0.75);
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
font-size: 1.1rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
#panel {
|
#panel {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
background: rgba(15, 23, 42, 0.9);
|
background: rgba(15, 23, 42, 0.9);
|
||||||
@@ -264,12 +243,6 @@ $positions = generatePositions($traceData);
|
|||||||
<main>
|
<main>
|
||||||
<div id="canvas-container">
|
<div id="canvas-container">
|
||||||
<canvas id="scene"></canvas>
|
<canvas id="scene"></canvas>
|
||||||
<div id="canvas-overlay" hidden>
|
|
||||||
<div>
|
|
||||||
<strong>WebGL nicht verfügbar</strong>
|
|
||||||
<p>Der 3D-Traceroute benötigt WebGL-Unterstützung. Bitte verwenden Sie einen aktuellen Browser oder aktivieren Sie WebGL in den Einstellungen.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<aside id="panel">
|
<aside id="panel">
|
||||||
<form method="get">
|
<form method="get">
|
||||||
@@ -330,42 +303,23 @@ $positions = generatePositions($traceData);
|
|||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
const canvas = document.getElementById('scene');
|
const canvas = document.getElementById('scene');
|
||||||
const container = document.getElementById('canvas-container');
|
const renderer = new THREE.WebGLRenderer({canvas, antialias: true});
|
||||||
const overlay = document.getElementById('canvas-overlay');
|
|
||||||
|
|
||||||
let renderer;
|
|
||||||
try {
|
|
||||||
renderer = new THREE.WebGLRenderer({canvas, antialias: true, alpha: true});
|
|
||||||
} catch (error) {
|
|
||||||
overlay.hidden = false;
|
|
||||||
console.error('WebGLRenderer konnte nicht initialisiert werden', error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.setPixelRatio(window.devicePixelRatio);
|
renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
|
||||||
function setRendererSize() {
|
|
||||||
const width = container.clientWidth || window.innerWidth;
|
|
||||||
const height = container.clientHeight || window.innerHeight;
|
|
||||||
renderer.setSize(width, height, false);
|
|
||||||
camera.aspect = width / height;
|
|
||||||
camera.updateProjectionMatrix();
|
|
||||||
}
|
|
||||||
|
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
scene.background = new THREE.Color(0x020617);
|
scene.background = new THREE.Color(0x020617);
|
||||||
|
|
||||||
const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000);
|
const camera = new THREE.PerspectiveCamera(60, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);
|
||||||
camera.position.set(60, 60, 60);
|
camera.position.set(60, 60, 60);
|
||||||
|
|
||||||
const controls = new THREE.OrbitControls(camera, renderer.domElement);
|
const controls = new THREE.OrbitControls(camera, renderer.domElement);
|
||||||
controls.enableDamping = true;
|
controls.enableDamping = true;
|
||||||
|
|
||||||
const light = new THREE.PointLight(0xffffff, 1.1, 500, 2);
|
const light = new THREE.PointLight(0xffffff, 1.2);
|
||||||
light.position.set(50, 120, 60);
|
light.position.set(50, 50, 50);
|
||||||
scene.add(light);
|
scene.add(light);
|
||||||
scene.add(new THREE.AmbientLight(0x93c5fd, 0.35));
|
scene.add(new THREE.AmbientLight(0x4c566a, 0.6));
|
||||||
scene.add(new THREE.HemisphereLight(0x38bdf8, 0x0f172a, 0.4));
|
|
||||||
|
|
||||||
const nodeMaterial = new THREE.MeshStandardMaterial({color: 0x38bdf8, emissive: 0x0f172a});
|
const nodeMaterial = new THREE.MeshStandardMaterial({color: 0x38bdf8, emissive: 0x0f172a});
|
||||||
const targetMaterial = new THREE.MeshStandardMaterial({color: 0x22d3ee, emissive: 0x164e63});
|
const targetMaterial = new THREE.MeshStandardMaterial({color: 0x22d3ee, emissive: 0x164e63});
|
||||||
@@ -402,7 +356,7 @@ $positions = generatePositions($traceData);
|
|||||||
particlePositions[i * 3 + 2] = (Math.random() - 0.5) * 400;
|
particlePositions[i * 3 + 2] = (Math.random() - 0.5) * 400;
|
||||||
}
|
}
|
||||||
particleGeometry.setAttribute('position', new THREE.BufferAttribute(particlePositions, 3));
|
particleGeometry.setAttribute('position', new THREE.BufferAttribute(particlePositions, 3));
|
||||||
const particleMaterial = new THREE.PointsMaterial({color: 0x0f172a, size: 1.1});
|
const particleMaterial = new THREE.PointsMaterial({color: 0x1e293b, size: 1.2});
|
||||||
const particles = new THREE.Points(particleGeometry, particleMaterial);
|
const particles = new THREE.Points(particleGeometry, particleMaterial);
|
||||||
scene.add(particles);
|
scene.add(particles);
|
||||||
|
|
||||||
@@ -410,19 +364,25 @@ $positions = generatePositions($traceData);
|
|||||||
const pointer = new THREE.Vector2();
|
const pointer = new THREE.Vector2();
|
||||||
const hopList = document.getElementById('hop-list');
|
const hopList = document.getElementById('hop-list');
|
||||||
|
|
||||||
|
function resizeRendererToDisplaySize() {
|
||||||
|
const width = canvas.clientWidth;
|
||||||
|
const height = canvas.clientHeight;
|
||||||
|
const needResize = canvas.width !== width || canvas.height !== height;
|
||||||
|
if (needResize) {
|
||||||
|
renderer.setSize(width, height, false);
|
||||||
|
camera.aspect = width / height;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
return needResize;
|
||||||
|
}
|
||||||
|
|
||||||
function animate() {
|
function animate() {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
controls.update();
|
controls.update();
|
||||||
setRendererSize();
|
resizeRendererToDisplaySize();
|
||||||
nodes.forEach((node, index) => {
|
|
||||||
const pulse = Math.sin(Date.now() * 0.001 + index) * 0.15 + 1.05;
|
|
||||||
node.scale.setScalar(pulse);
|
|
||||||
});
|
|
||||||
particles.rotation.y += 0.0007;
|
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
}
|
}
|
||||||
|
|
||||||
setRendererSize();
|
|
||||||
animate();
|
animate();
|
||||||
|
|
||||||
const infoCards = Array.from(document.querySelectorAll('.hop-card'));
|
const infoCards = Array.from(document.querySelectorAll('.hop-card'));
|
||||||
@@ -441,7 +401,7 @@ $positions = generatePositions($traceData);
|
|||||||
setActiveHop(index);
|
setActiveHop(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', () => setRendererSize());
|
window.addEventListener('resize', () => resizeRendererToDisplaySize());
|
||||||
|
|
||||||
renderer.domElement.addEventListener('click', event => {
|
renderer.domElement.addEventListener('click', event => {
|
||||||
const rect = renderer.domElement.getBoundingClientRect();
|
const rect = renderer.domElement.getBoundingClientRect();
|
||||||
@@ -503,10 +463,6 @@ $positions = generatePositions($traceData);
|
|||||||
searchNodes();
|
searchNodes();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (traceData.length > 0) {
|
|
||||||
focusOnNode(0);
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -0,0 +1,454 @@
|
|||||||
|
<?php
|
||||||
|
$recipient = 'meinemail@mail.ch';
|
||||||
|
$errors = [];
|
||||||
|
$successMessage = '';
|
||||||
|
|
||||||
|
function ensureDirectory(string $path): void
|
||||||
|
{
|
||||||
|
if (!is_dir($path)) {
|
||||||
|
mkdir($path, 0775, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeFileName(string $name): string
|
||||||
|
{
|
||||||
|
$name = preg_replace('/[^A-Za-z0-9_.-]/', '_', $name);
|
||||||
|
return substr($name, 0, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpload(string $field, string $targetDir, array &$errors): ?array
|
||||||
|
{
|
||||||
|
if (!isset($_FILES[$field]) || $_FILES[$field]['error'] === UPLOAD_ERR_NO_FILE) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $_FILES[$field];
|
||||||
|
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
$errors[] = 'Beim Hochladen der Datei "' . htmlspecialchars($file['name']) . '" ist ein Fehler aufgetreten.';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||||
|
$mimeType = $finfo->file($file['tmp_name']);
|
||||||
|
$allowedTypes = [
|
||||||
|
'image/jpeg' => 'jpg',
|
||||||
|
'image/png' => 'png',
|
||||||
|
'image/webp' => 'webp',
|
||||||
|
'image/gif' => 'gif'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!array_key_exists($mimeType, $allowedTypes)) {
|
||||||
|
$errors[] = 'Die Datei "' . htmlspecialchars($file['name']) . '" ist kein unterstütztes Bildformat.';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureDirectory($targetDir);
|
||||||
|
$extension = $allowedTypes[$mimeType];
|
||||||
|
$filename = sanitizeFileName(pathinfo($file['name'], PATHINFO_FILENAME));
|
||||||
|
if ($filename === '') {
|
||||||
|
$filename = 'upload_' . time();
|
||||||
|
}
|
||||||
|
$destination = rtrim($targetDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename . '_' . uniqid() . '.' . $extension;
|
||||||
|
|
||||||
|
if (!move_uploaded_file($file['tmp_name'], $destination)) {
|
||||||
|
$errors[] = 'Die Datei "' . htmlspecialchars($file['name']) . '" konnte nicht gespeichert werden.';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'path' => $destination,
|
||||||
|
'original' => $file['name'],
|
||||||
|
'type' => $mimeType
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$company = trim($_POST['company'] ?? '');
|
||||||
|
$contactEmail = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL) ? $_POST['email'] : '';
|
||||||
|
$website = trim($_POST['website'] ?? '');
|
||||||
|
$message = trim($_POST['message'] ?? '');
|
||||||
|
$bannerLink = trim($_POST['banner_link'] ?? '');
|
||||||
|
|
||||||
|
if ($company === '') {
|
||||||
|
$errors[] = 'Bitte geben Sie den Namen Ihres Unternehmens oder Projekts an.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($contactEmail === '') {
|
||||||
|
$errors[] = 'Bitte geben Sie eine gültige E-Mail-Adresse an.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bannerLink === '') {
|
||||||
|
$errors[] = 'Bitte geben Sie den Link an, der Ihrem Banner zugeordnet werden soll.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploadedImage = handleUpload('project_image', __DIR__ . '/uploads/images', $errors);
|
||||||
|
$uploadedBanner = handleUpload('banner_image', __DIR__ . '/uploads/banners', $errors);
|
||||||
|
|
||||||
|
if (!$errors) {
|
||||||
|
$boundary = '=_TinyHome_' . md5((string) microtime(true));
|
||||||
|
$headers = [];
|
||||||
|
$fromAddress = $contactEmail ?: 'no-reply@tinyhome.local';
|
||||||
|
$headers[] = 'From: ' . $fromAddress;
|
||||||
|
$headers[] = 'Reply-To: ' . $fromAddress;
|
||||||
|
$headers[] = 'MIME-Version: 1.0';
|
||||||
|
$headers[] = 'Content-Type: multipart/mixed; boundary="' . $boundary . '"';
|
||||||
|
|
||||||
|
$bodyParts = [];
|
||||||
|
$text = "Neue TinyHome-Anfrage\n\n";
|
||||||
|
$text .= "Unternehmen/Projekt: $company\n";
|
||||||
|
$text .= "Kontakt-E-Mail: $contactEmail\n";
|
||||||
|
if ($website !== '') {
|
||||||
|
$text .= "Website: $website\n";
|
||||||
|
}
|
||||||
|
$text .= "Banner-Link: $bannerLink\n\n";
|
||||||
|
if ($message !== '') {
|
||||||
|
$text .= "Nachricht:\n$message\n\n";
|
||||||
|
}
|
||||||
|
if ($uploadedImage) {
|
||||||
|
$text .= 'Projektbild gespeichert unter: ' . $uploadedImage['path'] . "\n";
|
||||||
|
}
|
||||||
|
if ($uploadedBanner) {
|
||||||
|
$text .= 'Banner gespeichert unter: ' . $uploadedBanner['path'] . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$bodyParts[] = '--' . $boundary;
|
||||||
|
$bodyParts[] = 'Content-Type: text/plain; charset="UTF-8"';
|
||||||
|
$bodyParts[] = 'Content-Transfer-Encoding: 8bit';
|
||||||
|
$bodyParts[] = '';
|
||||||
|
$bodyParts[] = $text;
|
||||||
|
|
||||||
|
foreach ([$uploadedImage, $uploadedBanner] as $upload) {
|
||||||
|
if (!$upload) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$fileContent = file_get_contents($upload['path']);
|
||||||
|
if ($fileContent === false) {
|
||||||
|
$errors[] = 'Die Datei "' . htmlspecialchars($upload['original']) . '" konnte nicht für den Mailversand gelesen werden.';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$bodyParts[] = '--' . $boundary;
|
||||||
|
$bodyParts[] = 'Content-Type: ' . $upload['type'] . '; name="' . sanitizeFileName($upload['original']) . '"';
|
||||||
|
$bodyParts[] = 'Content-Transfer-Encoding: base64';
|
||||||
|
$bodyParts[] = 'Content-Disposition: attachment; filename="' . sanitizeFileName($upload['original']) . '"';
|
||||||
|
$bodyParts[] = '';
|
||||||
|
$bodyParts[] = chunk_split(base64_encode($fileContent));
|
||||||
|
}
|
||||||
|
|
||||||
|
$bodyParts[] = '--' . $boundary . '--';
|
||||||
|
$body = implode("\r\n", $bodyParts);
|
||||||
|
|
||||||
|
if (!$errors) {
|
||||||
|
$mailSent = mail(
|
||||||
|
$recipient,
|
||||||
|
'Neue TinyHome-Anfrage von ' . $company,
|
||||||
|
$body,
|
||||||
|
implode("\r\n", $headers)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($mailSent) {
|
||||||
|
$successMessage = 'Vielen Dank! Ihre Anfrage wurde erfolgreich übermittelt.';
|
||||||
|
} else {
|
||||||
|
$errors[] = 'Ihre Anfrage konnte leider nicht versendet werden. Bitte versuchen Sie es später erneut.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>TinyHome Plattform – Natürlich. Minimalistisch. Erschwinglich.</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #f5f5f1;
|
||||||
|
--accent: #3b755f;
|
||||||
|
--accent-dark: #2f5c4b;
|
||||||
|
--text: #2b2b28;
|
||||||
|
--muted: #6a6a66;
|
||||||
|
--card: rgba(255, 255, 255, 0.9);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
background: linear-gradient(135deg, rgba(59,117,95,0.9), rgba(143,170,119,0.85)), url('https://images.unsplash.com/photo-1523419409543-0c1df022bdd7?auto=format&fit=crop&w=1600&q=80') center/cover no-repeat;
|
||||||
|
color: #fff;
|
||||||
|
padding: 6rem 1.5rem;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
header::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(47,92,75,0.45);
|
||||||
|
}
|
||||||
|
.header-content {
|
||||||
|
position: relative;
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(2.5rem, 4vw, 3.5rem);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
max-width: 640px;
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
color: rgba(255,255,255,0.9);
|
||||||
|
}
|
||||||
|
.cta {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.9rem 1.6rem;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 999px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
.cta:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 22px rgba(0,0,0,0.18);
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: -4rem auto 0;
|
||||||
|
padding: 0 1.5rem 4rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2.5rem;
|
||||||
|
box-shadow: 0 20px 40px rgba(47,92,75,0.12);
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
.features {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
.feature {
|
||||||
|
background: rgba(255,255,255,0.8);
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 1.75rem;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255,255,255,0.4);
|
||||||
|
}
|
||||||
|
.feature h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent-dark);
|
||||||
|
}
|
||||||
|
input[type="text"],
|
||||||
|
input[type="email"],
|
||||||
|
input[type="url"],
|
||||||
|
textarea,
|
||||||
|
input[type="file"] {
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
border: 1px solid rgba(47,92,75,0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255,255,255,0.9);
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
input:focus,
|
||||||
|
textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px rgba(59,117,95,0.2);
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
min-height: 140px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
.submit-btn {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 0.9rem 2.2rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s ease, transform 0.3s ease;
|
||||||
|
}
|
||||||
|
.submit-btn:hover {
|
||||||
|
background: var(--accent-dark);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.notice {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(47,92,75,0.1);
|
||||||
|
color: var(--accent-dark);
|
||||||
|
}
|
||||||
|
.notice.error {
|
||||||
|
background: rgba(180,58,58,0.12);
|
||||||
|
color: #8e2a2a;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
background: #1f2e27;
|
||||||
|
color: rgba(255,255,255,0.85);
|
||||||
|
padding: 2.5rem 1.5rem;
|
||||||
|
}
|
||||||
|
.footer-content {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
.agb {
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
.agb h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0,1fr));
|
||||||
|
}
|
||||||
|
.form-grid .form-group.full-width {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="header-content">
|
||||||
|
<h1>TinyHome Plattform</h1>
|
||||||
|
<p class="subtitle">Minimalistisches Wohnen im Einklang mit der Natur – wir verbinden Hersteller, Planer:innen und Menschen, die erschwingliche, hochwertige Tiny Houses suchen.</p>
|
||||||
|
<a class="cta" href="#kontakt">Jetzt Projekt einreichen</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<section class="card">
|
||||||
|
<h2>Warum TinyHomes?</h2>
|
||||||
|
<div class="features">
|
||||||
|
<div class="feature">
|
||||||
|
<h3>Naturnah leben</h3>
|
||||||
|
<p>Unsere Plattform bündelt Anbieter, die nachhaltige Materialien und ökologische Bauweisen priorisieren.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<h3>Minimale Baukosten</h3>
|
||||||
|
<p>Smarter Grundriss, effiziente Energie, faire Preise: TinyHomes machen Wohnen wieder erschwinglich.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<h3>Flexibel kombinierbar</h3>
|
||||||
|
<p>Vom Wochenend-Retreat bis zum ganzjährigen Zuhause – konfigurieren Sie Ihr TinyHome passend zu Ihrem Lebensstil.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<h3>Direkter Kontakt</h3>
|
||||||
|
<p>Sie erhalten maßgeschneiderte Angebote von geprüften Hersteller:innen aus der DACH-Region.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card" id="kontakt">
|
||||||
|
<h2>Projekt vorstellen & Banner übermitteln</h2>
|
||||||
|
<p>Übermitteln Sie uns Ihr TinyHome-Konzept – inklusive Banner-Link, damit wir Bestellungen eindeutig zuordnen können. Wir melden uns persönlich bei Ihnen.</p>
|
||||||
|
|
||||||
|
<?php if ($successMessage !== ''): ?>
|
||||||
|
<div class="notice"><?php echo htmlspecialchars($successMessage, ENT_QUOTES, 'UTF-8'); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($errors): ?>
|
||||||
|
<div class="notice error">
|
||||||
|
<ul>
|
||||||
|
<?php foreach ($errors as $error): ?>
|
||||||
|
<li><?php echo htmlspecialchars($error, ENT_QUOTES, 'UTF-8'); ?></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data" class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="company">Unternehmen / Projekt*</label>
|
||||||
|
<input type="text" id="company" name="company" value="<?php echo htmlspecialchars($_POST['company'] ?? '', ENT_QUOTES, 'UTF-8'); ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">E-Mail*</label>
|
||||||
|
<input type="email" id="email" name="email" value="<?php echo htmlspecialchars($_POST['email'] ?? '', ENT_QUOTES, 'UTF-8'); ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="website">Website</label>
|
||||||
|
<input type="url" id="website" name="website" placeholder="https://" value="<?php echo htmlspecialchars($_POST['website'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="banner_link">Banner-Link (Tracking)*</label>
|
||||||
|
<input type="url" id="banner_link" name="banner_link" placeholder="https://ihr-tracking-link" value="<?php echo htmlspecialchars($_POST['banner_link'] ?? '', ENT_QUOTES, 'UTF-8'); ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="project_image">Projektbild (JPG, PNG, WEBP, GIF)</label>
|
||||||
|
<input type="file" id="project_image" name="project_image" accept="image/*">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="banner_image">Banner (JPG, PNG, WEBP, GIF)</label>
|
||||||
|
<input type="file" id="banner_image" name="banner_image" accept="image/*">
|
||||||
|
</div>
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="message">Botschaft an uns</label>
|
||||||
|
<textarea id="message" name="message" placeholder="Was macht Ihr TinyHome besonders?"><?php echo htmlspecialchars($_POST['message'] ?? '', ENT_QUOTES, 'UTF-8'); ?></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="submit-btn">Anfrage absenden</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<div class="footer-content">
|
||||||
|
<div>
|
||||||
|
<h3>Kontakt</h3>
|
||||||
|
<p>TinyHome Plattform<br>Für weitere Fragen: <a href="mailto:meinemail@mail.ch" style="color:#fff; text-decoration: underline;">meinemail@mail.ch</a></p>
|
||||||
|
</div>
|
||||||
|
<div class="agb">
|
||||||
|
<h3>AGB (Schweiz)</h3>
|
||||||
|
<p>1. Geltungsbereich: Diese Allgemeinen Geschäftsbedingungen gelten für alle Leistungen der TinyHome Plattform mit Sitz in der Schweiz. Maßgebend ist die jeweils aktuelle Version zum Zeitpunkt des Vertragsschlusses.</p>
|
||||||
|
<p>2. Leistungen: Wir vermitteln TinyHome-Hersteller:innen und Interessent:innen. Ein Vertrag über Planung oder Bau kommt ausschließlich zwischen Anbieter:in und Kund:in zustande. Preise verstehen sich in Schweizer Franken (CHF), sofern nicht anders vereinbart.</p>
|
||||||
|
<p>3. Haftung: Für Inhalte und Angebote der angeschlossenen Hersteller:innen übernehmen wir keine Haftung. Eigene Haftung wird auf vorsätzliches oder grob fahrlässiges Verhalten beschränkt. Die Plattform haftet nicht für indirekte Schäden, Mangelfolgeschäden oder entgangenen Gewinn.</p>
|
||||||
|
<p>4. Datenschutz: Personenbezogene Daten werden gemäß schweizerischem Datenschutzrecht (insbesondere DSG) verarbeitet. Daten werden nur an beteiligte Anbieter:innen weitergegeben, sofern dies für die Vermittlung erforderlich ist.</p>
|
||||||
|
<p>5. Zahlungsbedingungen: Vermittlungs- oder Servicegebühren werden gesondert vereinbart und sind binnen 30 Tagen zahlbar. Bei Zahlungsverzug fallen Verzugszinsen in gesetzlicher Höhe sowie Mahngebühren an.</p>
|
||||||
|
<p>6. Widerruf & Stornierung: Widerrufs- oder Rücktrittsrechte richten sich nach den individuellen Vereinbarungen zwischen Kund:in und Hersteller:in. Die Plattform bietet Unterstützung bei der Klärung von Streitfällen, übernimmt jedoch keine rechtliche Vertretung.</p>
|
||||||
|
<p>7. Gerichtsstand & Recht: Es gilt ausschließlich schweizerisches Recht. Gerichtsstand ist der Sitz der TinyHome Plattform, sofern zwingende gesetzliche Bestimmungen nichts anderes vorsehen.</p>
|
||||||
|
</div>
|
||||||
|
<p style="font-size:0.9rem; color: rgba(255,255,255,0.7);">© <?php echo date('Y'); ?> TinyHome Plattform. Alle Rechte vorbehalten.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user