diff --git a/backlog/tasks/task-5 - Add-webcam-less-fallback-to-the-toggle.md b/backlog/tasks/task-5 - Add-webcam-less-fallback-to-the-toggle.md
new file mode 100644
index 0000000..b77658a
--- /dev/null
+++ b/backlog/tasks/task-5 - Add-webcam-less-fallback-to-the-toggle.md
@@ -0,0 +1,52 @@
+---
+id: TASK-5
+title: Show the webcam toggle only when a webcam is available
+status: In Progress
+assignee: []
+created_date: '2026-05-06 20:11'
+updated_date: '2026-05-07 05:58'
+labels:
+ - frontend
+ - fallback
+dependencies: []
+priority: medium
+---
+
+## Description
+
+
+Detect whether the browser reports an available webcam and only render the hero live-raster switch for visitors who can actually use the camera effect.
+
+
+## Acceptance Criteria
+
+- [x] #1 The hero checks browser media devices for at least one video input without prompting for camera permission.
+- [x] #2 The webcam switch and helper copy are hidden when no video input is reported or media-device enumeration is unavailable.
+- [x] #3 The switch still starts and stops the existing webcam pixel grid for visitors with an available webcam.
+
+
+## Implementation Plan
+
+
+1. Inspect current hero toggle and WebcamPixelGrid error behavior
+2. Detect webcam availability with browser media-device enumeration
+3. Hide the helper copy and switch when no video input is available
+4. Preserve the existing webcam start/stop flow when a camera exists
+5. Verify with build and update acceptance criteria
+
+
+## Implementation Notes
+
+
+Implemented a hero fallback raster that activates when WebcamPixelGrid reports unavailable or denied camera access.
+
+Added an explicit mediaDevices/getUserMedia availability check before requesting camera access.
+
+Verified with npm run build; Astro built 3 static pages successfully. Dev server is running at http://127.0.0.1:4322/ for manual testing.
+
+Changed direction after feedback: removed the visual fallback and now hide the switch unless enumerateDevices reports a videoinput.
+
+Added a devicechange listener so the switch can appear or disappear if camera hardware availability changes during the session.
+
+Verified the revised behavior compiles with npm run build.
+
diff --git a/backlog/tasks/task-6 - Modularisiere-die-Landingpage-und-entferne-ungenutzte-Komponenten.md b/backlog/tasks/task-6 - Modularisiere-die-Landingpage-und-entferne-ungenutzte-Komponenten.md
new file mode 100644
index 0000000..588d2db
--- /dev/null
+++ b/backlog/tasks/task-6 - Modularisiere-die-Landingpage-und-entferne-ungenutzte-Komponenten.md
@@ -0,0 +1,71 @@
+---
+id: TASK-6
+title: Modularisiere die Landingpage und entferne ungenutzte Komponenten
+status: In Progress
+assignee: []
+created_date: '2026-05-07 06:21'
+updated_date: '2026-05-07 06:23'
+labels:
+ - refactor
+ - frontend
+dependencies: []
+modified_files:
+ - src/pages/index.astro
+ - tests/landing-content.test.mjs
+ - src/components/landing-page-sections.tsx
+ - src/components/landing/services-section.tsx
+ - src/components/landing/deliverables-section.tsx
+ - src/components/landing/packages-section.tsx
+ - src/components/landing/contact-section.tsx
+ - src/components/landing.tsx
+ - src/components/about19.tsx
+ - src/components/contact21.tsx
+ - src/components/cta.tsx
+ - src/components/faq7.tsx
+ - src/components/feature284.tsx
+ - src/components/hero235.tsx
+ - src/components/pricing4.tsx
+ - src/components/stats11.tsx
+ - src/components/ui/accordion.tsx
+ - src/components/ui/badge.tsx
+ - src/components/ui/button.tsx
+ - src/components/ui/field.tsx
+ - src/components/ui/glowing-effect.tsx
+ - src/components/ui/input.tsx
+ - src/components/ui/label.tsx
+ - src/components/ui/separator.tsx
+ - src/components/ui/tabs.tsx
+priority: medium
+---
+
+## Description
+
+
+Die bestehende Landingpage soll in klarere, kleinere Komponenten aufgeteilt werden. Nicht mehr referenzierte Komponenten sollen entfernt werden, ohne vorhandene Nutzer- oder laufende Änderungen zurückzudrehen.
+
+
+## Acceptance Criteria
+
+- [x] #1 Die Hauptseite nutzt klar benannte modulare Komponenten statt einer monolithischen LandingRest-Komponente.
+- [x] #2 Nicht benötigte Komponenten im Komponentenordner sind entfernt oder nicht mehr Teil der Codebasis.
+- [x] #3 Build und vorhandene Tests laufen nach der Änderung ohne Fehler.
+
+
+## Implementation Plan
+
+
+1. Bestehende Landingpage-Struktur und Import-Verwendung prüfen.
+2. LandingRest in klar benannte Sektionen extrahieren und Index-Import aktualisieren.
+3. Nicht referenzierte Template- und UI-Komponenten entfernen.
+4. Tests an neue Struktur anpassen und Build/Test ausführen.
+
+
+## Implementation Notes
+
+
+LandingRest wurde in ServicesSection, DeliverablesSection, PackagesSection und ContactSection extrahiert; index.astro nutzt nun LandingPageSections.
+
+Nicht referenzierte Template-Komponenten sowie deren ungenutzte UI-Hilfskomponenten wurden entfernt. Vorbestehende Änderungen an landing-hero-section.tsx und ui/webcam-pixel-grid.tsx blieben unangetastet.
+
+Verifikation: node --test tests/*.mjs und pnpm build laufen erfolgreich.
+
diff --git a/src/components/about19.tsx b/src/components/about19.tsx
deleted file mode 100644
index 0ccff2b..0000000
--- a/src/components/about19.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-
-import { cn } from "@/lib/utils";
-
-interface About19Props {
- className?: string;
-}
-
-const About19 = ({ className }: About19Props) => {
- return (
-
-
-
-
-
-
-
-
-
- Über die Zusammenarbeit
-
-
- Hallo, ich bin Matthias. Zurück in der Region und hier, um zu
- bleiben.
-
-
-
- Ich bin in der Region aufgewachsen, war durch die Bundeswehr
- viele Jahre weg und bin jetzt zurück.
-
-
- Seit über 15 Jahren beschäftige ich mich mit Webentwicklung und
- Software. Einen Großteil davon intern für die Bundeswehr:
- Projekte, die ich Ihnen leider nicht zeigen kann. Was ich Ihnen
- zeigen kann: wie ich arbeite. Zuverlässig, präzise und ohne
- unnötigen Schnickschnack.
-
-
- Neben Websites für regionale Unternehmen entwickle ich eigene
- Software und Apps. Wenn Ihre Anforderungen irgendwann über eine
- einfache Website hinausgehen, bleibt der Ansprechpartner also
- derselbe.
-
-
-
- Mein Ziel: Unternehmen aus der Region mit einer Website
- ausstatten, die funktioniert, gefunden wird und Ihnen keine
- Kopfschmerzen macht.
-
-
-
-
-
- );
-};
-
-export { About19 };
diff --git a/src/components/contact21.tsx b/src/components/contact21.tsx
deleted file mode 100644
index 913dd93..0000000
--- a/src/components/contact21.tsx
+++ /dev/null
@@ -1,219 +0,0 @@
-"use client";
-
-import { zodResolver } from "@hookform/resolvers/zod";
-import { CornerDownRight, LoaderIcon } from "lucide-react";
-import { useState } from "react";
-import { Controller, useForm } from "react-hook-form";
-import { z } from "zod";
-
-import { Button } from "@/components/ui/button";
-import {
- Field,
- FieldError,
- FieldGroup,
- FieldLabel,
-} from "@/components/ui/field";
-import { Input } from "@/components/ui/input";
-import { cn } from "@/lib/utils";
-
-const contactFormSchema = z.object({
- name: z.string().min(1, "Bitte geben Sie Ihren Namen ein"),
- email: z
- .string()
- .min(1, "Bitte geben Sie Ihre E-Mail ein")
- .email("Bitte geben Sie eine gültige E-Mail ein"),
- message: z.string().min(1, "Bitte beschreiben Sie kurz Ihr Anliegen"),
-});
-
-type ContactFormData = z.infer;
-
-interface Contact21Props {
- className?: string;
- onSubmit?: (data: ContactFormData) => Promise;
-}
-
-const Contact21 = ({ className, onSubmit }: Contact21Props) => {
- const [isSubmitted, setIsSubmitted] = useState(false);
- const [showSuccess, setShowSuccess] = useState(false);
-
- const form = useForm({
- resolver: zodResolver(contactFormSchema),
- mode: "onSubmit",
- reValidateMode: "onSubmit",
- defaultValues: {
- name: "",
- email: "",
- message: "",
- },
- });
-
- const handleFormSubmit = async (data: ContactFormData) => {
- try {
- if (onSubmit) {
- await onSubmit(data);
- } else {
- console.log("Form submitted:", data);
- await new Promise((resolve) => setTimeout(resolve, 1000));
- }
- setIsSubmitted(true);
- setShowSuccess(true);
- form.reset();
- setTimeout(() => setShowSuccess(false), 4500);
- setTimeout(() => setIsSubmitted(false), 5000);
- } catch {
- form.setError("root", {
- message: "Beim Senden ist etwas schiefgelaufen. Bitte versuchen Sie es erneut.",
- });
- }
- };
-
- return (
-
-
-
-
-
-
- Kontakt
-
-
- Erzählen Sie kurz, worum es geht.
-
-
- Ich melde mich innerhalb von 24 Stunden mit einer ersten
- Einschätzung und dem passenden nächsten Schritt.
-
-
-
-
- MM
-
-
-
- Matthias Meister
-
-
- Freelance Webdesigner
-
-
-
-
-
- {isSubmitted && (
-
-
- Vielen Dank! Ich melde mich in Kürze bei Ihnen.
-
-
- )}
-
-
-
-
-
-
- );
-};
-
-export { Contact21 };
diff --git a/src/components/cta.tsx b/src/components/cta.tsx
deleted file mode 100644
index 5be0efe..0000000
--- a/src/components/cta.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import { cn } from "@/lib/utils";
-
-const trustAnchors = [
- {
- title: "Direkter Kontakt",
- description:
- "Sie sprechen mit dem Menschen, der die Website auch plant und baut.",
- note: "Keine Vertriebsrunde, keine unklaren Übergänge.",
- },
- {
- title: "15+ Jahre Erfahrung",
- description:
- "Webentwicklung und Software mit Fokus auf robuste, wartbare Lösungen.",
- note: "Praxis statt Buzzwords und Technik nur dort, wo sie wirklich hilft.",
- },
- {
- title: "Hosting in Sachsen",
- description:
- "Deutsche Server, DSGVO-konform und passend für regionale Unternehmen.",
- note: "Greifbar, nachvollziehbar und ohne unnötiges Zusatztheater.",
- },
-];
-
-export default function CTASection() {
- return (
-
-
-
-
- Vor dem Angebot
-
-
- Erst verstehen, dann bauen.
-
-
- Die Zusammenarbeit ist bewusst direkt gehalten: ein Gespräch, eine
- klare Empfehlung und ein Vorschlag, der zu Ihrem Betrieb passt.
-
-
-
- {trustAnchors.map((item, index) => (
-
-
- {item.title}
-
-
-
- {item.description}
-
-
- {item.note}
-
-
-
- ))}
-
-
-
- );
-}
diff --git a/src/components/faq7.tsx b/src/components/faq7.tsx
deleted file mode 100644
index d99c0f1..0000000
--- a/src/components/faq7.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
-} from "@/components/ui/accordion";
-import { Button } from "@/components/ui/button";
-import { cn } from "@/lib/utils";
-
-const faqs = [
- {
- question: "Wie lange dauert es bis meine Website fertig ist?",
- answer:
- "In der Regel ist Ihre Website innerhalb von zwei Wochen fertig — vom ersten Gespräch bis zum Go-Live. Nach der Entwicklung bekommen Sie einen Vorschau-Link, damit Sie alles in Ruhe prüfen können. Erst wenn Sie zufrieden sind, geht die Seite online.",
- },
- {
- question: "Was passiert wenn ich das Hosting kündige?",
- answer:
- "Ihre Website und Ihre Domain gehören Ihnen — immer. Wenn Sie das Hosting kündigen, übertrage ich Ihnen alles ohne Wenn und Aber. Keine versteckten Abhängigkeiten, das ist vertraglich festgehalten.",
- },
- {
- question: "Ich habe schon eine Domain — was passiert damit?",
- answer:
- "Kein Problem. Wir zeigen Ihre bestehende Domain einfach auf die neue Website um. Falls Sie möchten, kann ich die Domain auch zu mir umziehen — das macht die Verwaltung einfacher, ist aber kein Muss.",
- },
- {
- question: "Brauche ich technisches Wissen?",
- answer:
- "Keins. Sie kümmern Sie um Ihr Geschäft, ich um alles Technische. Von der Domain über die E-Mails bis zu Updates — das liegt bei mir.",
- },
- {
- question: "Kümmern Sie sich auch um Impressum und Datenschutz?",
- answer:
- "Ja, jede Website die ich baue kommt mit einem rechtssicheren Impressum und einer DSGVO-konformen Datenschutzerklärung — inklusive übersichtlichem Cookie-Hinweis, wo nötig, statt Chaos und unnötigen Tracking-Dialogen.",
- },
-];
-
-interface Faq7Props {
- className?: string;
-}
-
-const Faq7 = ({ className }: Faq7Props) => {
- return (
-
-
-
-
-
- Häufige Fragen
-
-
- Vor dem Start soll nichts schwammig bleiben.
-
-
- Falls noch etwas offen ist, schreiben Sie mir gern über das
-
- Kontaktformular
-
- .
-
-
- Frage stellen
-
-
-
- {faqs.map((faq, index) => (
-
-
- {faq.question}
-
-
- {faq.answer}
-
-
- ))}
-
-
-
-
- );
-};
-
-export { Faq7 };
diff --git a/src/components/feature284.tsx b/src/components/feature284.tsx
deleted file mode 100644
index c5165c7..0000000
--- a/src/components/feature284.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import {
- Gauge,
- Handshake,
- MapPinned,
- Search,
- Smartphone,
-} from "lucide-react";
-
-import { cn } from "@/lib/utils";
-
-const featureData = [
- {
- desc: "Die Startseite sagt schnell, für wen Sie arbeiten, was Sie anbieten und wie Interessenten Kontakt aufnehmen.",
- title: "Klare Positionierung",
- badgeTitle: "01",
- icon: MapPinned,
- },
- {
- desc: "Gestaltung, Texte und Struktur wirken seriös, ohne den Charakter eines regionalen Betriebs glattzubügeln.",
- title: "Glaubwürdiger Auftritt",
- badgeTitle: "02",
- icon: Handshake,
- },
- {
- desc: "Telefonnummer, Formular und zentrale Informationen bleiben auf Smartphone und Desktop leicht erreichbar.",
- title: "Mobil sauber geführt",
- badgeTitle: "03",
- icon: Smartphone,
- },
- {
- desc: "Technik, Bilder und Inhalte werden so umgesetzt, dass die Seite schnell lädt und stabil bleibt.",
- title: "Schnell und robust",
- badgeTitle: "04",
- icon: Gauge,
- },
- {
- desc: "Google findet die richtigen Inhalte: Leistungen, Region, Kontakt und die wichtigsten Suchbegriffe.",
- title: "Für Suche vorbereitet",
- badgeTitle: "05",
- icon: Search,
- },
-];
-
-interface Feature284Props {
- className?: string;
-}
-
-const Feature284 = ({ className }: Feature284Props) => {
- return (
-
-
-
-
-
- Was die Seite leisten muss
-
-
- Professionell heißt hier: verständlich, erreichbar, belastbar.
-
-
-
- {featureData.map((feature, index) => (
-
-
-
- {feature.badgeTitle}
-
-
-
-
-
- {feature.title}
-
-
- {feature.desc}
-
-
-
- ))}
-
-
-
-
- );
-};
-
-export { Feature284 };
diff --git a/src/components/hero235.tsx b/src/components/hero235.tsx
deleted file mode 100644
index 806c80e..0000000
--- a/src/components/hero235.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import { ArrowRight, Mail, MapPin, Phone } from "lucide-react";
-
-import { Button } from "@/components/ui/button";
-import { cn } from "@/lib/utils";
-
-interface Hero235Props {
- className?: string;
-}
-
-const Hero235 = ({ className }: Hero235Props) => {
- return (
-
-
-
-
-
-
-
- Webdesign für regionale KMU
-
-
- Websites, die vor Ort Vertrauen schaffen.
-
-
- Für Handwerk, Praxen und kleine Betriebe: klar erklärt, schnell
- gebaut und so strukturiert, dass Besucher ohne Umwege verstehen,
- warum sie gerade Sie anfragen sollten.
-
-
-
- {[
- ["24h", "Rückmeldung"],
- ["2 Wochen", "typischer Go-Live"],
- ["Sachsen", "Hosting & Betrieb"],
- ].map(([value, label]) => (
-
-
- {value}
-
-
- {label}
-
-
- ))}
-
-
-
-
-
-
-
-
- Direkt mit dem Entwickler statt mit wechselnden Agenturrollen.
-
-
- Persönlich geplant
-
-
-
-
-
-
-
- );
-};
-
-export { Hero235 };
diff --git a/src/components/landing-hero-section.tsx b/src/components/landing-hero-section.tsx
index 10ee515..ea0e30d 100644
--- a/src/components/landing-hero-section.tsx
+++ b/src/components/landing-hero-section.tsx
@@ -1,7 +1,7 @@
"use client";
import { ArrowUpRight } from "lucide-react";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
import { WebcamPixelGrid } from "@/components/ui/webcam-pixel-grid";
@@ -11,6 +11,47 @@ const PRIMARY_HERO_BG = "#B54440";
const LandingHeroSection = () => {
const [liveRasterOn, setLiveRasterOn] = useState(false);
+ const [hasWebcam, setHasWebcam] = useState(false);
+
+ useEffect(() => {
+ const mediaDevices = navigator.mediaDevices;
+
+ if (!mediaDevices?.enumerateDevices) {
+ setHasWebcam(false);
+ return;
+ }
+
+ const updateWebcamAvailability = async () => {
+ try {
+ const devices = await mediaDevices.enumerateDevices();
+ const hasVideoInput = devices.some(
+ (device) => device.kind === "videoinput",
+ );
+
+ setHasWebcam(hasVideoInput);
+ if (!hasVideoInput) {
+ setLiveRasterOn(false);
+ }
+ } catch {
+ setHasWebcam(false);
+ setLiveRasterOn(false);
+ }
+ };
+
+ updateWebcamAvailability();
+ mediaDevices.addEventListener?.("devicechange", updateWebcamAvailability);
+
+ return () => {
+ mediaDevices.removeEventListener?.(
+ "devicechange",
+ updateWebcamAvailability,
+ );
+ };
+ }, []);
+
+ const handleToggleLiveRaster = () => {
+ setLiveRasterOn((isOn) => !isOn);
+ };
return (
@@ -70,6 +111,7 @@ const LandingHeroSection = () => {
borderColor="#ffffff"
borderOpacity={0}
quietWebcamErrors
+ onWebcamError={() => setLiveRasterOn(false)}
/>
) : null}
@@ -80,42 +122,44 @@ const LandingHeroSection = () => {
©2026
-
-
- {liveRasterOn
- ? "Kamera aus? Schalter zurück, fertig."
- : "Psst ... einmal wippen, dann lebt die Seite."}
-
-
setLiveRasterOn((v) => !v)}
- className={cn(
- "relative h-9 w-13 shrink-0 rounded-full border transition-colors duration-300 focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-primary-foreground",
- liveRasterOn
- ? "border-primary-foreground/50 bg-primary-foreground/20"
- : "border-primary-foreground/45 bg-primary-foreground/10 hover:border-primary-foreground/70",
- )}
- >
-
+
-
-
+ >
+ {liveRasterOn
+ ? "Kamera aus? Schalter zurück, fertig."
+ : "Psst ... einmal wippen, dann lebt die Seite."}
+
+
+
+
+
+ ) : null}
diff --git a/src/components/landing-page-sections.tsx b/src/components/landing-page-sections.tsx
new file mode 100644
index 0000000..df01120
--- /dev/null
+++ b/src/components/landing-page-sections.tsx
@@ -0,0 +1,17 @@
+import { ContactSection } from "@/components/landing/contact-section";
+import { DeliverablesSection } from "@/components/landing/deliverables-section";
+import { PackagesSection } from "@/components/landing/packages-section";
+import { ServicesSection } from "@/components/landing/services-section";
+
+const LandingPageSections = () => {
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export { LandingPageSections };
diff --git a/src/components/landing.tsx b/src/components/landing.tsx
deleted file mode 100644
index 4598e6c..0000000
--- a/src/components/landing.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-import {
- Check,
- CornerDownRight,
- Mail,
- MapPin,
- Phone,
-} from "lucide-react";
-
-const services = [
- {
- number: "01",
- title: "Website",
- text: "Eine klare Startseite oder ein kompletter Auftritt, der sofort zeigt, warum man Ihnen vertrauen kann.",
- },
- {
- number: "02",
- title: "Struktur",
- text: "Angebot, Referenzen, Ablauf und Kontakt werden so sortiert, dass Besucher nicht suchen müssen.",
- },
- {
- number: "03",
- title: "Technik",
- text: "Schnell, mobil sauber, DSGVO-sauber und so gebaut, dass spätere Änderungen nicht zum Projekt werden.",
- },
-];
-
-const deliverables = [
- "Strategie und Seitenstruktur",
- "Individuelles Screen-Design",
- "Moderne, schnelle Umsetzung",
- "Kontaktformular und Datenschutz",
- "Hosting, Wartung und Analytics",
-];
-
-const packages = [
- {
- name: "Basis",
- price: "799 EUR",
- detail: "Eine starke Seite für ein klares Angebot.",
- },
- {
- name: "Profi",
- price: "1.499 EUR",
- detail: "Mehrere Seiten für Betriebe mit mehr zu zeigen.",
- },
- {
- name: "Maßarbeit",
- price: "2.499 EUR+",
- detail: "Eigene Struktur, selbst pflegbar, für besondere Anforderungen.",
- },
-];
-
-const LandingRest = () => {
- return (
- <>
-
-
-
- Leistungen (02)
-
-
- Drei Schritte. Eine Website.
-
-
-
- {services.map((service) => (
-
-
- {service.number}
-
-
- {service.title}
-
-
- {service.text}
-
-
- ))}
-
-
-
-
-
-
- Ergebnis (03)
-
-
- Was am Ende steht
-
-
-
- {deliverables.map((item) => (
-
-
- {item}
-
- ))}
-
-
-
-
-
-
-
- Pakete (04)
-
-
- Kosten ohne Nebel
-
-
-
- {packages.map((item) => (
-
-
-
- {item.name}
-
-
- {item.price}
-
-
-
- {item.detail}
-
-
- ))}
-
-
-
-
-
-
-
- Kontakt (05)
-
-
- Erzählen Sie mir kurz von Ihrem Betrieb
-
-
- Ein paar Sätze reichen: Was bieten Sie an, was soll die Website
- für Sie tun, und wann soll sie online sein?
-
-
-
- Anfrage per Mail senden
-
-
-
-
-
- hallo@matthias-meister.com
-
-
-
-
Rückmeldung innerhalb von 24 Stunden
-
-
-
- Betriebe aus der Region
-
-
-
- >
- );
-};
-
-export { LandingRest };
diff --git a/src/components/landing/contact-section.tsx b/src/components/landing/contact-section.tsx
new file mode 100644
index 0000000..992a128
--- /dev/null
+++ b/src/components/landing/contact-section.tsx
@@ -0,0 +1,46 @@
+import { CornerDownRight, Mail, MapPin, Phone } from "lucide-react";
+
+const ContactSection = () => {
+ return (
+
+
+
+ Kontakt (05)
+
+
+ Erzählen Sie mir kurz von Ihrem Betrieb
+
+
+ Ein paar Sätze reichen: Was bieten Sie an, was soll die Website für
+ Sie tun, und wann soll sie online sein?
+
+
+
+ Anfrage per Mail senden
+
+
+
+
+
+ hallo@matthias-meister.com
+
+
+
+
Rückmeldung innerhalb von 24 Stunden
+
+
+
+ Betriebe aus der Region
+
+
+
+ );
+};
+
+export { ContactSection };
diff --git a/src/components/landing/deliverables-section.tsx b/src/components/landing/deliverables-section.tsx
new file mode 100644
index 0000000..24a3de4
--- /dev/null
+++ b/src/components/landing/deliverables-section.tsx
@@ -0,0 +1,37 @@
+import { Check } from "lucide-react";
+
+const deliverables = [
+ "Strategie und Seitenstruktur",
+ "Individuelles Screen-Design",
+ "Moderne, schnelle Umsetzung",
+ "Kontaktformular und Datenschutz",
+ "Hosting, Wartung und Analytics",
+];
+
+const DeliverablesSection = () => {
+ return (
+
+
+
+ Ergebnis (03)
+
+
+ Was am Ende steht
+
+
+
+ {deliverables.map((item) => (
+
+
+ {item}
+
+ ))}
+
+
+ );
+};
+
+export { DeliverablesSection };
diff --git a/src/components/landing/packages-section.tsx b/src/components/landing/packages-section.tsx
new file mode 100644
index 0000000..1545fc8
--- /dev/null
+++ b/src/components/landing/packages-section.tsx
@@ -0,0 +1,59 @@
+const packages = [
+ {
+ name: "Basis",
+ price: "799 EUR",
+ detail: "Eine starke Seite für ein klares Angebot.",
+ },
+ {
+ name: "Profi",
+ price: "1.499 EUR",
+ detail: "Mehrere Seiten für Betriebe mit mehr zu zeigen.",
+ },
+ {
+ name: "Maßarbeit",
+ price: "2.499 EUR+",
+ detail: "Eigene Struktur, selbst pflegbar, für besondere Anforderungen.",
+ },
+];
+
+const PackagesSection = () => {
+ return (
+
+
+
+
+ Pakete (04)
+
+
+ Kosten ohne Nebel
+
+
+
+ {packages.map((item) => (
+
+
+
+ {item.name}
+
+
+ {item.price}
+
+
+
+ {item.detail}
+
+
+ ))}
+
+
+
+ );
+};
+
+export { PackagesSection };
diff --git a/src/components/landing/services-section.tsx b/src/components/landing/services-section.tsx
new file mode 100644
index 0000000..2d9aebd
--- /dev/null
+++ b/src/components/landing/services-section.tsx
@@ -0,0 +1,55 @@
+const services = [
+ {
+ number: "01",
+ title: "Website",
+ text: "Eine klare Startseite oder ein kompletter Auftritt, der sofort zeigt, warum man Ihnen vertrauen kann.",
+ },
+ {
+ number: "02",
+ title: "Struktur",
+ text: "Angebot, Referenzen, Ablauf und Kontakt werden so sortiert, dass Besucher nicht suchen müssen.",
+ },
+ {
+ number: "03",
+ title: "Technik",
+ text: "Schnell, mobil sauber, DSGVO-sauber und so gebaut, dass spätere Änderungen nicht zum Projekt werden.",
+ },
+];
+
+const ServicesSection = () => {
+ return (
+
+
+
+ Leistungen (02)
+
+
+ Drei Schritte. Eine Website.
+
+
+
+ {services.map((service) => (
+
+
+ {service.number}
+
+
+ {service.title}
+
+
+ {service.text}
+
+
+ ))}
+
+
+ );
+};
+
+export { ServicesSection };
diff --git a/src/components/pricing4.tsx b/src/components/pricing4.tsx
deleted file mode 100644
index 8e2b688..0000000
--- a/src/components/pricing4.tsx
+++ /dev/null
@@ -1,219 +0,0 @@
-"use client";
-
-import { Check } from "lucide-react";
-import { useState } from "react";
-
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { cn } from "@/lib/utils";
-
-interface PricingPlan {
- name: string;
- price: string;
- period: string;
- description: string;
- features: string[];
- isPopular?: boolean;
-}
-
-interface Pricing4Props {
- title?: string;
- description?: string;
- className?: string;
-}
-
-const developmentPlans: PricingPlan[] = [
- {
- name: "Basis",
- price: "799 €",
- period: "Einmalpreis",
- description: "Für einen klaren Webauftritt mit den wichtigsten Inhalten.",
- features: [
- "Eine Seite mit fünf Sektionen",
- "Kontaktformular",
- "Impressum und Datenschutz",
- "Mobilfreundlich und für Google vorbereitet",
- "Cookiefreie Analytics ohne Banner",
- ],
- },
- {
- name: "Profi",
- price: "1.499 €",
- period: "Einmalpreis",
- description: "Für Betriebe, die mehrere Leistungen sauber erklären wollen.",
- features: [
- "Bis zu fünf Unterseiten",
- "Google Maps Integration",
- "SEO-Basis für lokale Auffindbarkeit",
- "Optionaler Blog",
- "Alles aus Basis inklusive",
- ],
- isPopular: true,
- },
- {
- name: "Maßarbeit",
- price: "2.499 €",
- period: "Einmalpreis",
- description: "Für individuelle Anforderungen, CMS und spätere Erweiterungen.",
- features: [
- "Individuelles Design nach Ihren Anforderungen",
- "CMS zur eigenen Inhaltspflege",
- "Erweiterbare Struktur",
- "Alles aus Profi inklusive",
- ],
- },
-];
-
-const servicePlans: PricingPlan[] = [
- {
- name: "Hosting",
- price: "19 €",
- period: "pro Monat",
- description: "Solide technische Basis für kleine Unternehmensseiten.",
- features: [
- "Hosting auf deutschen Servern in Sachsen",
- "SSL, Domain und tägliche Backups",
- "Monatlicher Einblick in Besucherzahlen",
- ],
- },
- {
- name: "Wartung",
- price: "39 €",
- period: "pro Monat",
- description: "Für Unternehmen, die Betrieb und Sicherheit abgeben möchten.",
- features: [
- "Alles aus Hosting inklusive",
- "Regelmäßige Updates und Sicherheitschecks",
- "1 Stunde Support pro Monat",
- "Monitoring bei technischen Problemen",
- ],
- isPopular: true,
- },
- {
- name: "Full Service",
- price: "69 €",
- period: "pro Monat",
- description: "Für laufende Änderungen ohne jedes Mal ein neues Projekt.",
- features: [
- "Alles aus Wartung inklusive",
- "Kleinere Inhaltsänderungen bis 2 Stunden pro Monat",
- "Häufigerer Einblick in Besucherzahlen",
- ],
- },
-];
-
-const Pricing4 = ({
- title = "Pakete mit klarer Kante.",
- description =
- "Die Preise sind bewusst nachvollziehbar gehalten. Im Gespräch klären wir, welches Paket passt und wo ein schlankerer Weg sinnvoller ist.",
- className,
-}: Pricing4Props) => {
- const [activeTab, setActiveTab] = useState<"development" | "service">(
- "development",
- );
- const plans = activeTab === "development" ? developmentPlans : servicePlans;
-
- return (
-
-
-
-
-
- Preise
-
-
- {title}
-
-
- {description}
-
-
-
- setActiveTab(value as "development" | "service")
- }
- className="w-fit lg:justify-self-end"
- aria-label="Paketart auswählen"
- >
-
-
- Entwicklung
-
-
- Hosting & Wartung
-
-
-
-
-
-
- {plans.map((plan) => (
-
-
-
-
- {plan.name}
-
-
- {plan.description}
-
-
- {plan.isPopular && (
-
- Empfohlen
-
- )}
-
-
-
-
- {plan.price}
-
-
- {plan.period}
-
-
-
-
- {plan.features.map((feature) => (
-
-
- {feature}
-
- ))}
-
-
-
- Kostenloses Angebot anfordern
-
-
- ))}
-
-
-
- );
-};
-
-export { Pricing4 };
diff --git a/src/components/stats11.tsx b/src/components/stats11.tsx
deleted file mode 100644
index b96e095..0000000
--- a/src/components/stats11.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { cn } from "@/lib/utils";
-
-interface Stats11Props {
- className?: string;
-}
-
-const Stats11 = ({ className }: Stats11Props) => {
- const stats = [
- ["SEO-ready", "Leistungsseiten, Region und Kontakt sauber strukturiert."],
- ["< 1 Sek.", "Auf schnelle Ladezeiten und klare Technik ausgelegt."],
- ["ab 799 €", "Transparente Einstiegspreise ohne Paketnebel."],
- ["2 Wochen", "Typischer Zeitraum vom Startgespräch bis zur Vorschau."],
- ];
-
- return (
-
-
-
-
-
- Messbare Grundlagen
-
-
- Gute Websites fühlen sich ruhig an, weil die Basis stimmt.
-
-
-
- {stats.map(([value, label]) => (
-
-
- {value}
-
-
- {label}
-
-
- ))}
-
-
-
-
- );
-};
-
-export { Stats11 };
diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx
deleted file mode 100644
index fcfee5c..0000000
--- a/src/components/ui/accordion.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import * as React from "react"
-import { Accordion as AccordionPrimitive } from "radix-ui"
-
-import { cn } from "@/lib/utils"
-import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"
-
-function Accordion({
- className,
- ...props
-}: React.ComponentProps
) {
- return (
-
- )
-}
-
-function AccordionItem({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AccordionTrigger({
- className,
- children,
- ...props
-}: React.ComponentProps) {
- return (
-
-
- {children}
-
-
-
-
- )
-}
-
-function AccordionContent({
- className,
- children,
- ...props
-}: React.ComponentProps) {
- return (
-
-
- {children}
-
-
- )
-}
-
-export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
deleted file mode 100644
index cacff11..0000000
--- a/src/components/ui/badge.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
-import { Slot } from "radix-ui"
-
-import { cn } from "@/lib/utils"
-
-const badgeVariants = cva(
- "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
- {
- variants: {
- variant: {
- default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
- secondary:
- "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
- destructive:
- "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
- outline:
- "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
- ghost:
- "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
- link: "text-primary underline-offset-4 hover:underline",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
-)
-
-function Badge({
- className,
- variant = "default",
- asChild = false,
- ...props
-}: React.ComponentProps<"span"> &
- VariantProps & { asChild?: boolean }) {
- const Comp = asChild ? Slot.Root : "span"
-
- return (
-
- )
-}
-
-export { Badge, badgeVariants }
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
deleted file mode 100644
index 6138844..0000000
--- a/src/components/ui/button.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
-import { Slot } from "radix-ui"
-
-import { cn } from "@/lib/utils"
-
-const buttonVariants = cva(
- "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
- {
- variants: {
- variant: {
- default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
- outline:
- "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
- secondary:
- "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
- ghost:
- "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
- destructive:
- "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
- link: "text-primary underline-offset-4 hover:underline",
- },
- size: {
- default:
- "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
- xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
- sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
- lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
- icon: "size-8",
- "icon-xs":
- "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
- "icon-sm":
- "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
- "icon-lg": "size-9",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- }
-)
-
-function Button({
- className,
- variant = "default",
- size = "default",
- asChild = false,
- ...props
-}: React.ComponentProps<"button"> &
- VariantProps & {
- asChild?: boolean
- }) {
- const Comp = asChild ? Slot.Root : "button"
-
- return (
-
- )
-}
-
-export { Button, buttonVariants }
diff --git a/src/components/ui/field.tsx b/src/components/ui/field.tsx
deleted file mode 100644
index 654b9b4..0000000
--- a/src/components/ui/field.tsx
+++ /dev/null
@@ -1,236 +0,0 @@
-import { useMemo } from "react"
-import { cva, type VariantProps } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
-import { Label } from "@/components/ui/label"
-import { Separator } from "@/components/ui/separator"
-
-function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
- return (
- [data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
- className
- )}
- {...props}
- />
- )
-}
-
-function FieldLegend({
- className,
- variant = "legend",
- ...props
-}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
- return (
-
- )
-}
-
-function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-const fieldVariants = cva(
- "group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
- {
- variants: {
- orientation: {
- vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
- horizontal:
- "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
- responsive:
- "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
- },
- },
- defaultVariants: {
- orientation: "vertical",
- },
- }
-)
-
-function Field({
- className,
- orientation = "vertical",
- ...props
-}: React.ComponentProps<"div"> & VariantProps) {
- return (
-
- )
-}
-
-function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function FieldLabel({
- className,
- ...props
-}: React.ComponentProps) {
- return (
- [data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10",
- "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
- className
- )}
- {...props}
- />
- )
-}
-
-function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
- return (
- a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
- className
- )}
- {...props}
- />
- )
-}
-
-function FieldSeparator({
- children,
- className,
- ...props
-}: React.ComponentProps<"div"> & {
- children?: React.ReactNode
-}) {
- return (
-
-
- {children && (
-
- {children}
-
- )}
-
- )
-}
-
-function FieldError({
- className,
- children,
- errors,
- ...props
-}: React.ComponentProps<"div"> & {
- errors?: Array<{ message?: string } | undefined>
-}) {
- const content = useMemo(() => {
- if (children) {
- return children
- }
-
- if (!errors?.length) {
- return null
- }
-
- const uniqueErrors = [
- ...new Map(errors.map((error) => [error?.message, error])).values(),
- ]
-
- if (uniqueErrors?.length == 1) {
- return uniqueErrors[0]?.message
- }
-
- return (
-
- {uniqueErrors.map(
- (error, index) =>
- error?.message && {error.message}
- )}
-
- )
- }, [children, errors])
-
- if (!content) {
- return null
- }
-
- return (
-
- {content}
-
- )
-}
-
-export {
- Field,
- FieldLabel,
- FieldDescription,
- FieldError,
- FieldGroup,
- FieldLegend,
- FieldSeparator,
- FieldSet,
- FieldContent,
- FieldTitle,
-}
diff --git a/src/components/ui/glowing-effect.tsx b/src/components/ui/glowing-effect.tsx
deleted file mode 100644
index 25c4c41..0000000
--- a/src/components/ui/glowing-effect.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-"use client";
-
-import { memo, useCallback, useEffect, useRef } from "react";
-import { cn } from "@/lib/utils";
-import { animate } from "motion/react";
-
-interface GlowingEffectProps {
- blur?: number;
- inactiveZone?: number;
- proximity?: number;
- spread?: number;
- variant?: "default" | "white";
- glow?: boolean;
- className?: string;
- disabled?: boolean;
- movementDuration?: number;
- borderWidth?: number;
-}
-const GlowingEffect = memo(
- ({
- blur = 0,
- inactiveZone = 0.7,
- proximity = 0,
- spread = 20,
- variant = "default",
- glow = false,
- className,
- movementDuration = 2,
- borderWidth = 1,
- disabled = true,
- }: GlowingEffectProps) => {
- const containerRef = useRef(null);
- const lastPosition = useRef({ x: 0, y: 0 });
- const animationFrameRef = useRef(0);
-
- const handleMove = useCallback(
- (e?: MouseEvent | { x: number; y: number }) => {
- if (!containerRef.current) return;
-
- if (animationFrameRef.current) {
- cancelAnimationFrame(animationFrameRef.current);
- }
-
- animationFrameRef.current = requestAnimationFrame(() => {
- const element = containerRef.current;
- if (!element) return;
-
- const { left, top, width, height } = element.getBoundingClientRect();
- const mouseX = e?.x ?? lastPosition.current.x;
- const mouseY = e?.y ?? lastPosition.current.y;
-
- if (e) {
- lastPosition.current = { x: mouseX, y: mouseY };
- }
-
- const center = [left + width * 0.5, top + height * 0.5];
- const distanceFromCenter = Math.hypot(
- mouseX - center[0],
- mouseY - center[1]
- );
- const inactiveRadius = 0.5 * Math.min(width, height) * inactiveZone;
-
- if (distanceFromCenter < inactiveRadius) {
- element.style.setProperty("--active", "0");
- return;
- }
-
- const isActive =
- mouseX > left - proximity &&
- mouseX < left + width + proximity &&
- mouseY > top - proximity &&
- mouseY < top + height + proximity;
-
- element.style.setProperty("--active", isActive ? "1" : "0");
-
- if (!isActive) return;
-
- const currentAngle =
- parseFloat(element.style.getPropertyValue("--start")) || 0;
- let targetAngle =
- (180 * Math.atan2(mouseY - center[1], mouseX - center[0])) /
- Math.PI +
- 90;
-
- const angleDiff = ((targetAngle - currentAngle + 180) % 360) - 180;
- const newAngle = currentAngle + angleDiff;
-
- animate(currentAngle, newAngle, {
- duration: movementDuration,
- ease: [0.16, 1, 0.3, 1],
- onUpdate: (value) => {
- element.style.setProperty("--start", String(value));
- },
- });
- });
- },
- [inactiveZone, proximity, movementDuration]
- );
-
- useEffect(() => {
- if (disabled) return;
-
- const handleScroll = () => handleMove();
- const handlePointerMove = (e: PointerEvent) => handleMove(e);
-
- window.addEventListener("scroll", handleScroll, { passive: true });
- document.body.addEventListener("pointermove", handlePointerMove, {
- passive: true,
- });
-
- return () => {
- if (animationFrameRef.current) {
- cancelAnimationFrame(animationFrameRef.current);
- }
- window.removeEventListener("scroll", handleScroll);
- document.body.removeEventListener("pointermove", handlePointerMove);
- };
- }, [handleMove, disabled]);
-
- return (
- <>
-
- 0 && "blur-[var(--blur)] ",
- className,
- disabled && "!hidden"
- )}
- >
-
-
- >
- );
- }
-);
-
-GlowingEffect.displayName = "GlowingEffect";
-
-export { GlowingEffect };
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
deleted file mode 100644
index d763cd9..0000000
--- a/src/components/ui/input.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import * as React from "react"
-
-import { cn } from "@/lib/utils"
-
-function Input({ className, type, ...props }: React.ComponentProps<"input">) {
- return (
-
- )
-}
-
-export { Input }
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
deleted file mode 100644
index f752f82..0000000
--- a/src/components/ui/label.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import * as React from "react"
-import { Label as LabelPrimitive } from "radix-ui"
-
-import { cn } from "@/lib/utils"
-
-function Label({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-export { Label }
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
deleted file mode 100644
index ca11501..0000000
--- a/src/components/ui/separator.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import * as React from "react"
-import { Separator as SeparatorPrimitive } from "radix-ui"
-
-import { cn } from "@/lib/utils"
-
-function Separator({
- className,
- orientation = "horizontal",
- decorative = true,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-export { Separator }
diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx
deleted file mode 100644
index 05f469f..0000000
--- a/src/components/ui/tabs.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
-import { Tabs as TabsPrimitive } from "radix-ui"
-
-import { cn } from "@/lib/utils"
-
-function Tabs({
- className,
- orientation = "horizontal",
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-const tabsListVariants = cva(
- "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
- {
- variants: {
- variant: {
- default: "bg-muted",
- line: "gap-1 bg-transparent",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
-)
-
-function TabsList({
- className,
- variant = "default",
- ...props
-}: React.ComponentProps &
- VariantProps) {
- return (
-
- )
-}
-
-function TabsTrigger({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function TabsContent({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
diff --git a/src/components/ui/webcam-pixel-grid.tsx b/src/components/ui/webcam-pixel-grid.tsx
index 4858aa9..9cef547 100644
--- a/src/components/ui/webcam-pixel-grid.tsx
+++ b/src/components/ui/webcam-pixel-grid.tsx
@@ -165,6 +165,10 @@ export const WebcamPixelGrid: React.FC = ({
// Request camera access
const requestCameraAccess = useCallback(async () => {
try {
+ if (!navigator.mediaDevices?.getUserMedia) {
+ throw new Error("Webcam access is not available in this browser");
+ }
+
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 640 },
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 880666a..6ac2efe 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,6 +1,6 @@
---
import { LandingHeroSection } from "@/components/landing-hero-section";
-import { LandingRest } from "@/components/landing";
+import { LandingPageSections } from "@/components/landing-page-sections";
import { Footer27 } from "@/components/footer27";
import "@/styles/global.css";
---
@@ -21,7 +21,7 @@ import "@/styles/global.css";
-
+