2 Commits

Author SHA1 Message Date
1elle1
8d1d9d6f33 feat: refactor configuration files to use JavaScript and update package details 2026-02-06 14:15:23 +01:00
1elle1
11bbd1cf3f feat: add new components for navigation, contact, customer segments, hero section, and service overview
- Implement Navigation component for main navigation links.
- Create ContactSection for displaying contact information and a contact form.
- Add CustomerSegments to showcase target customer groups.
- Develop HeroSection for the landing page with a background image and call-to-action buttons.
- Introduce ServiceOverview to present available services with icons and descriptions.
- Create PageHero for consistent page headers with optional background images.
- Implement TariffTable for displaying internet and phone tariff plans.
- Add TeamGrid to showcase team members with images and roles.
- Create TrustSection to highlight company statistics.
- Implement ValueProposition to present company values with icons.
- Add reusable UI components: Badge, Button, Card, Container, and SectionHeading.
- Define constants for company information, services, and tariffs.
- Create utility function for class name management.
- Define TypeScript types for navigation links, team members, tariff plans, and service cards.
- Configure TypeScript settings for the project.
2026-02-06 13:59:08 +01:00
69 changed files with 9062 additions and 0 deletions

41
.gitignore vendored
View File

@@ -31,3 +31,44 @@ npm-debug.log*
# Generated files
generated/
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

18
eslint.config.mjs Normal file
View File

@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

10
next.config.js Normal file
View File

@@ -0,0 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "export",
images: {
unoptimized: true,
},
trailingSlash: true,
};
export default nextConfig;

6612
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "telenetsystems",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"clsx": "^2.1.1",
"framer-motion": "^12.33.0",
"lucide-react": "^0.563.0",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"tailwindcss": "^4",
"typescript": "^5"
}
}

7
postcss.config.mjs Normal file
View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

BIN
public/images/team/timo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

3
public/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
User-agent: *
Allow: /
Sitemap: https://www.telenetsystems.at/sitemap.xml

43
public/sitemap.xml Normal file
View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://www.telenetsystems.at/</loc>
<lastmod>2026-02-06</lastmod>
<priority>1.0</priority>
</url>
<url>
<loc>https://www.telenetsystems.at/internet/</loc>
<lastmod>2026-02-06</lastmod>
<priority>0.9</priority>
</url>
<url>
<loc>https://www.telenetsystems.at/fernsehen/</loc>
<lastmod>2026-02-06</lastmod>
<priority>0.9</priority>
</url>
<url>
<loc>https://www.telenetsystems.at/telefonie/</loc>
<lastmod>2026-02-06</lastmod>
<priority>0.9</priority>
</url>
<url>
<loc>https://www.telenetsystems.at/leistungen/</loc>
<lastmod>2026-02-06</lastmod>
<priority>0.8</priority>
</url>
<url>
<loc>https://www.telenetsystems.at/ueber-uns/</loc>
<lastmod>2026-02-06</lastmod>
<priority>0.7</priority>
</url>
<url>
<loc>https://www.telenetsystems.at/impressum/</loc>
<lastmod>2026-02-06</lastmod>
<priority>0.3</priority>
</url>
<url>
<loc>https://www.telenetsystems.at/datenschutz/</loc>
<lastmod>2026-02-06</lastmod>
<priority>0.3</priority>
</url>
</urlset>

View File

@@ -0,0 +1,99 @@
import type { Metadata } from "next";
import Container from "@/components/ui/Container";
export const metadata: Metadata = {
title: "Datenschutz",
description: "Datenschutzerklärung der TeleNetSystems gemäß DSGVO. Informationen zur Verarbeitung Ihrer personenbezogenen Daten.",
};
export default function DatenschutzPage() {
return (
<section className="py-20 lg:py-28">
<Container>
<article className="prose prose-neutral mx-auto max-w-3xl">
<h1 className="text-display font-bold tracking-tight text-neutral-900">Datenschutzerklärung</h1>
<h2 className="text-xl font-semibold text-neutral-900 mt-10">1. Verantwortlicher</h2>
<p className="text-neutral-600 leading-relaxed">
TeleNetSystems<br />
Untermarkt 16<br />
6600 Reutte, Tirol, Österreich<br />
E-Mail: info@telenetsystems.at<br />
Telefon: +43 5672 21400
</p>
<h2 className="text-xl font-semibold text-neutral-900 mt-8">2. Erhebung und Verarbeitung personenbezogener Daten</h2>
<p className="text-neutral-600 leading-relaxed">
Wenn Sie unsere Website besuchen, werden automatisch Informationen allgemeiner Natur
erfasst. Diese Informationen (Server-Logfiles) beinhalten etwa die Art des Webbrowsers,
das verwendete Betriebssystem, den Domainnamen Ihres Internet-Service-Providers und
ähnliches. Hierbei handelt es sich ausschließlich um Informationen, welche keine
Rückschlüsse auf Ihre Person zulassen.
</p>
<h2 className="text-xl font-semibold text-neutral-900 mt-8">3. Kontaktformular</h2>
<p className="text-neutral-600 leading-relaxed">
Wenn Sie uns per Kontaktformular Anfragen zukommen lassen, werden Ihre Angaben
aus dem Anfrageformular inklusive der von Ihnen dort angegebenen Kontaktdaten
zwecks Bearbeitung der Anfrage und für den Fall von Anschlussfragen bei uns
gespeichert. Diese Daten geben wir nicht ohne Ihre Einwilligung weiter.
</p>
<p className="text-neutral-600 leading-relaxed mt-2">
Die Verarbeitung der in das Kontaktformular eingegebenen Daten erfolgt somit
ausschließlich auf Grundlage Ihrer Einwilligung (Art. 6 Abs. 1 lit. a DSGVO).
</p>
<h2 className="text-xl font-semibold text-neutral-900 mt-8">4. Cookies</h2>
<p className="text-neutral-600 leading-relaxed">
Diese Website verwendet keine Tracking-Cookies. Es werden ausschließlich technisch
notwendige Cookies eingesetzt, die für den Betrieb der Website erforderlich sind.
</p>
<h2 className="text-xl font-semibold text-neutral-900 mt-8">5. Ihre Rechte</h2>
<p className="text-neutral-600 leading-relaxed">
Sie haben jederzeit das Recht auf unentgeltliche Auskunft über Ihre gespeicherten
personenbezogenen Daten, deren Herkunft und Empfänger und den Zweck der
Datenverarbeitung sowie ein Recht auf Berichtigung, Sperrung oder Löschung
dieser Daten.
</p>
<p className="text-neutral-600 leading-relaxed mt-2">
Ihnen stehen im Wesentlichen folgende Rechte zu:
</p>
<ul className="text-neutral-600 list-disc pl-6 space-y-1 mt-2">
<li>Recht auf Auskunft (Art. 15 DSGVO)</li>
<li>Recht auf Berichtigung (Art. 16 DSGVO)</li>
<li>Recht auf Löschung (Art. 17 DSGVO)</li>
<li>Recht auf Einschränkung der Verarbeitung (Art. 18 DSGVO)</li>
<li>Recht auf Datenübertragbarkeit (Art. 20 DSGVO)</li>
<li>Recht auf Widerspruch (Art. 21 DSGVO)</li>
</ul>
<h2 className="text-xl font-semibold text-neutral-900 mt-8">6. Beschwerderecht bei der Aufsichtsbehörde</h2>
<p className="text-neutral-600 leading-relaxed">
Wenn Sie der Ansicht sind, dass die Verarbeitung Ihrer personenbezogenen Daten
gegen die DSGVO verstößt, haben Sie das Recht, sich bei der Österreichischen
Datenschutzbehörde zu beschweren.
</p>
<p className="text-neutral-600 leading-relaxed mt-2">
Österreichische Datenschutzbehörde<br />
Barichgasse 40-42<br />
1030 Wien<br />
Telefon: +43 1 52 152-0<br />
E-Mail: dsb@dsb.gv.at
</p>
<h2 className="text-xl font-semibold text-neutral-900 mt-8">7. Änderung dieser Datenschutzerklärung</h2>
<p className="text-neutral-600 leading-relaxed">
Wir behalten uns vor, diese Datenschutzerklärung gelegentlich anzupassen, damit
sie stets den aktuellen rechtlichen Anforderungen entspricht oder um Änderungen
unserer Leistungen in der Datenschutzerklärung umzusetzen.
</p>
<p className="text-neutral-500 text-sm mt-10">
Stand: Februar 2026
</p>
</article>
</Container>
</section>
);
}

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

114
src/app/fernsehen/page.tsx Normal file
View File

@@ -0,0 +1,114 @@
import type { Metadata } from "next";
import { Building2 } from "lucide-react";
import PageHero from "@/components/sections/PageHero";
import TariffTable from "@/components/sections/TariffTable";
import ContactSection from "@/components/sections/ContactSection";
import Container from "@/components/ui/Container";
import SectionHeading from "@/components/ui/SectionHeading";
import Button from "@/components/ui/Button";
import { TV_PRIVAT_PAKETE } from "@/lib/constants";
export const metadata: Metadata = {
title: "Fernsehen",
description:
"TV-Pakete für Privat- und Geschäftskunden in Tirol. Von der Grundversorgung bis zum individuellen Business-TV. Empfang über Kabel, kein Schüssel nötig.",
};
export default function FernsehenPage() {
return (
<>
<PageHero
title="Fernsehen mit TeleNetSystems"
description="Kein Herumfummeln an der Satellitenschüssel. Unser TV kommt über Kabel — zuverlässig, in HD und mit einer Senderauswahl, die passt."
backgroundImage="/images/hero/tv-header.jpg"
/>
{/* TV Privat */}
<section className="py-20 lg:py-24" aria-labelledby="tv-privat-heading">
<Container>
<SectionHeading
title="TV für Privatkunden"
subtitle="Drei Pakete, klare Inhalte. Wählen Sie, was zu Ihrem Fernsehabend passt."
/>
<TariffTable plans={TV_PRIVAT_PAKETE} />
</Container>
</section>
{/* Pay TV */}
<section className="bg-neutral-50 py-20 lg:py-24" aria-labelledby="paytv-heading">
<Container>
<div className="grid grid-cols-1 items-center gap-12 lg:grid-cols-2">
<div className="overflow-hidden rounded-card">
<img
src="/images/gallery/tv-paytv.jpg"
alt="Pay-TV Zusatzpakete"
className="w-full object-cover"
loading="lazy"
width={600}
height={400}
/>
</div>
<div>
<h2 id="paytv-heading" className="text-title font-bold text-neutral-900">
Pay TV mehr Auswahl, wenn Sie möchten
</h2>
<p className="mt-4 text-neutral-600 leading-relaxed">
Sport, Filme, Dokumentationen oder internationale Sender: Mit unseren
Pay-TV-Zusatzpaketen erweitern Sie Ihr Programm gezielt. Kein Abo-Dschungel,
sondern klare Zusatzoptionen zu Ihrem bestehenden TV-Paket.
</p>
<p className="mt-4 text-neutral-600 leading-relaxed">
Welche Zusatzpakete für Sie sinnvoll sind, hängt von Ihren Interessen ab.
Wir beraten Sie gerne unverbindlich und ehrlich.
</p>
<Button href="/#kontakt" variant="outline" size="md" className="mt-8">
Zu Pay TV beraten lassen
</Button>
</div>
</div>
</Container>
</section>
{/* TV Business */}
<section className="py-20 lg:py-24" aria-labelledby="tv-business-heading">
<Container>
<div className="grid grid-cols-1 items-center gap-12 lg:grid-cols-2">
<div>
<div className="flex items-center gap-3 mb-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-amber-100">
<Building2 className="h-5 w-5 text-amber-700" aria-hidden="true" />
</div>
<h2 id="tv-business-heading" className="text-title font-bold text-neutral-900">
TV für Unternehmen
</h2>
</div>
<p className="text-neutral-600 leading-relaxed">
Hotels, Pensionen, Wartezimmer, Gastronomiebetriebe überall dort, wo Fernsehen
zum Angebot gehört, brauchen Sie eine Lösung, die einfach funktioniert.
</p>
<p className="mt-4 text-neutral-600 leading-relaxed">
Wir richten Ihr Business-TV so ein, dass es zu Ihrem Betrieb passt. Senderauswahl,
Anzahl der Anschlüsse, zentrale Steuerung alles wird individuell abgestimmt.
</p>
<Button href="/#kontakt" variant="primary" size="md" className="mt-8">
Business-TV anfragen
</Button>
</div>
<div className="overflow-hidden rounded-card">
<img
src="/images/gallery/tv-business.jpg"
alt="TV-Lösungen für Hotels und Geschäftskunden"
className="w-full object-cover"
loading="lazy"
width={600}
height={400}
/>
</div>
</div>
</Container>
</section>
<ContactSection />
</>
);
}

80
src/app/globals.css Normal file
View File

@@ -0,0 +1,80 @@
@import "tailwindcss";
@theme inline {
--color-primary-50: #fef7ed;
--color-primary-100: #fdebd4;
--color-primary-200: #fbd4a8;
--color-primary-300: #f8b871;
--color-primary-400: #f5a038;
--color-primary-500: #F39212;
--color-primary-600: #da7c0a;
--color-primary-700: #b5620b;
--color-primary-800: #914d10;
--color-primary-900: #764010;
--color-primary-950: #402006;
--color-secondary-50: #fef3ed;
--color-secondary-100: #fde3d4;
--color-secondary-200: #fac4a8;
--color-secondary-300: #f69d71;
--color-secondary-400: #f17838;
--color-secondary-500: #EB5C23;
--color-secondary-600: #d14416;
--color-secondary-700: #ae3313;
--color-secondary-800: #8b2a17;
--color-secondary-900: #712616;
--color-secondary-950: #3d100a;
--color-neutral-50: #fafafa;
--color-neutral-100: #f5f5f5;
--color-neutral-200: #e5e5e5;
--color-neutral-300: #d4d4d4;
--color-neutral-400: #a3a3a3;
--color-neutral-500: #737373;
--color-neutral-600: #525252;
--color-neutral-700: #404040;
--color-neutral-800: #262626;
--color-neutral-900: #171717;
--color-neutral-950: #0a0a0a;
--color-accent: #F39212;
--color-accent-dark: #da7c0a;
--font-sans: var(--font-inter), system-ui, sans-serif;
--text-hero: 3.5rem;
--text-hero--line-height: 1.1;
--text-hero--letter-spacing: -0.02em;
--text-display: 2.5rem;
--text-display--line-height: 1.15;
--text-display--letter-spacing: -0.015em;
--text-title: 1.75rem;
--text-title--line-height: 1.25;
--text-body-lg: 1.125rem;
--text-body-lg--line-height: 1.7;
--spacing-section: 5rem;
--spacing-section-lg: 7rem;
--radius-card: 0.75rem;
}
html {
scroll-behavior: smooth;
}
body {
@apply bg-white text-neutral-800 antialiased;
font-family: var(--font-sans);
}
h1, h2, h3, h4, h5, h6 {
@apply font-semibold;
}
:focus-visible {
@apply outline-2 outline-offset-2 outline-primary-500;
}

View File

@@ -0,0 +1,75 @@
import type { Metadata } from "next";
import Container from "@/components/ui/Container";
export const metadata: Metadata = {
title: "Impressum",
description: "Impressum und rechtliche Informationen der TeleNetSystems in Reutte, Tirol.",
};
export default function ImpressumPage() {
return (
<section className="py-20 lg:py-28">
<Container>
<article className="prose prose-neutral mx-auto max-w-3xl">
<h1 className="text-display font-bold tracking-tight text-neutral-900">Impressum</h1>
<h2 className="text-xl font-semibold text-neutral-900 mt-10">Angaben gemäß § 5 ECG</h2>
<p>
TeleNetSystems<br />
Untermarkt 16<br />
6600 Reutte<br />
Tirol, Österreich
</p>
<h2 className="text-xl font-semibold text-neutral-900 mt-8">Kontakt</h2>
<p>
Telefon: +43 5672 21400<br />
E-Mail: info@telenetsystems.at
</p>
<h2 className="text-xl font-semibold text-neutral-900 mt-8">Unternehmensgegenstand</h2>
<p>
IT-Dienstleistungen und Telekommunikation
</p>
<h2 className="text-xl font-semibold text-neutral-900 mt-8">Aufsichtsbehörde</h2>
<p>
Bezirkshauptmannschaft Reutte
</p>
<h2 className="text-xl font-semibold text-neutral-900 mt-8">Berufsbezeichnung und berufsrechtliche Regelungen</h2>
<p>
Berufsbezeichnung: IT-Dienstleister<br />
Verleihungsstaat: Österreich
</p>
<h2 className="text-xl font-semibold text-neutral-900 mt-8">Haftungsausschluss</h2>
<h3 className="text-lg font-semibold text-neutral-900 mt-6">Haftung für Inhalte</h3>
<p className="text-neutral-600 leading-relaxed">
Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit,
Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen.
Als Diensteanbieter sind wir gemäß § 7 Abs. 1 ECG für eigene Inhalte auf diesen
Seiten nach den allgemeinen Gesetzen verantwortlich.
</p>
<h3 className="text-lg font-semibold text-neutral-900 mt-6">Haftung für Links</h3>
<p className="text-neutral-600 leading-relaxed">
Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte wir
keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine
Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige
Anbieter oder Betreiber der Seiten verantwortlich.
</p>
<h2 className="text-xl font-semibold text-neutral-900 mt-8">Urheberrecht</h2>
<p className="text-neutral-600 leading-relaxed">
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten
unterliegen dem österreichischen Urheberrecht. Die Vervielfältigung, Bearbeitung,
Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes
bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers.
</p>
</article>
</Container>
</section>
);
}

93
src/app/internet/page.tsx Normal file
View File

@@ -0,0 +1,93 @@
import type { Metadata } from "next";
import { Building2 } from "lucide-react";
import PageHero from "@/components/sections/PageHero";
import TariffTable from "@/components/sections/TariffTable";
import ContactSection from "@/components/sections/ContactSection";
import Container from "@/components/ui/Container";
import SectionHeading from "@/components/ui/SectionHeading";
import Button from "@/components/ui/Button";
import { INTERNET_TARIFE } from "@/lib/constants";
export const metadata: Metadata = {
title: "Internet",
description:
"Schnelles Internet in Reutte und Tirol. Kabel- und Glasfaser-Tarife für Privat- und Geschäftskunden. Faire Preise, stabile Verbindung.",
};
export default function InternetPage() {
const kabelTarife = INTERNET_TARIFE.filter((t) => t.category === "kabel");
const glasfaserTarife = INTERNET_TARIFE.filter((t) => t.category === "glasfaser");
return (
<>
<PageHero
title="Internet für Reutte und Umgebung"
description="Ob Streaming, Homeoffice oder einfach sorgenfreies Surfen — wir bringen Sie ins Netz. Stabil, schnell und ohne Überraschungen auf der Rechnung."
backgroundImage="/images/hero/internet-header.jpg"
/>
{/* Kabel-Internet */}
<section className="py-20 lg:py-24" aria-labelledby="kabel-heading">
<Container>
<SectionHeading
title="Kabel-Internet"
subtitle="Unsere bewährten Tarife über das Kabelnetz. Verfügbar im Großteil des Außerferns."
/>
<TariffTable plans={kabelTarife} />
</Container>
</section>
{/* Glasfaser */}
<section className="bg-neutral-50 py-20 lg:py-24" aria-labelledby="glasfaser-heading">
<Container>
<SectionHeading
title="Glasfaser-Internet"
subtitle="Wo Glasfaser verfügbar ist, geht mehr. Deutlich mehr. Ideal für anspruchsvolle Nutzer und Mehrpersonenhaushalte."
/>
<TariffTable plans={glasfaserTarife} />
</Container>
</section>
{/* Business Internet */}
<section className="py-20 lg:py-24" aria-labelledby="business-internet-heading">
<Container>
<div className="grid grid-cols-1 items-center gap-12 lg:grid-cols-2">
<div>
<div className="flex items-center gap-3 mb-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-amber-100">
<Building2 className="h-5 w-5 text-amber-700" aria-hidden="true" />
</div>
<h2 id="business-internet-heading" className="text-title font-bold text-neutral-900">
Internet für Ihr Unternehmen
</h2>
</div>
<p className="text-neutral-600 leading-relaxed">
Standardtarife passen nicht immer. Wenn Ihr Betrieb symmetrische Bandbreiten braucht,
feste IP-Adressen benötigt oder mehrere Standorte vernetzen will sprechen Sie mit uns.
</p>
<p className="mt-4 text-neutral-600 leading-relaxed">
Wir schauen uns Ihre Situation an und machen Ihnen ein Angebot, das zu Ihrem
Unternehmen passt. Keine Pakete von der Stange, sondern genau das, was Sie brauchen.
</p>
<Button href="/#kontakt" variant="primary" size="md" className="mt-8">
Individuelles Angebot anfragen
</Button>
</div>
<div className="overflow-hidden rounded-card">
<img
src="/images/gallery/serverraum.jpg"
alt="Serverraum für Business-Internet-Lösungen"
className="w-full object-cover"
loading="lazy"
width={600}
height={400}
/>
</div>
</div>
</Container>
</section>
<ContactSection />
</>
);
}

108
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,108 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
import "./globals.css";
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
display: "swap",
});
export const metadata: Metadata = {
metadataBase: new URL("https://www.telenetsystems.at"),
title: {
default: "TeleNetSystems | Internet, TV & Telefonie in Reutte, Tirol",
template: "%s | TeleNetSystems",
},
description:
"Ihr regionaler Partner für Internet, Fernsehen und Telefonie in Reutte und Tirol. Persönliche Beratung, stabile Technik, faire Preise.",
keywords: [
"Internet Reutte",
"TV Anbieter Tirol",
"Telefonie Reutte",
"TeleNetSystems",
"Internet Tirol",
"Glasfaser Reutte",
],
authors: [{ name: "TeleNetSystems" }],
openGraph: {
type: "website",
locale: "de_AT",
siteName: "TeleNetSystems",
title: "TeleNetSystems | Internet, TV & Telefonie in Reutte, Tirol",
description:
"Ihr regionaler Partner für Internet, Fernsehen und Telefonie in Reutte und Tirol.",
images: [
{
url: "/images/logo/logo-systems.png",
width: 500,
height: 200,
alt: "TeleNetSystems Logo",
},
],
},
icons: {
icon: "/favicon.ico",
},
};
const jsonLd = {
"@context": "https://schema.org",
"@type": "LocalBusiness",
name: "TeleNetSystems",
description:
"Regionaler IT- und Telekommunikationsanbieter in Reutte, Tirol. Internet, Fernsehen und Telefonie für Privat- und Geschäftskunden.",
address: {
"@type": "PostalAddress",
streetAddress: "Untermarkt 16",
addressLocality: "Reutte",
addressRegion: "Tirol",
postalCode: "6600",
addressCountry: "AT",
},
telephone: "+43 5672 21400",
email: "info@telenetsystems.at",
url: "https://www.telenetsystems.at",
image: "/images/logo/logo-systems.png",
areaServed: {
"@type": "GeoCircle",
geoMidpoint: {
"@type": "GeoCoordinates",
latitude: 47.4833,
longitude: 10.7167,
},
geoRadius: "50000",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="de" className={inter.variable}>
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</head>
<body className="antialiased">
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:rounded-lg focus:bg-primary-600 focus:px-4 focus:py-2 focus:text-white"
>
Zum Inhalt springen
</a>
<Header />
<main id="main-content" className="pt-16 lg:pt-20">
{children}
</main>
<Footer />
</body>
</html>
);
}

147
src/app/leistungen/page.tsx Normal file
View File

@@ -0,0 +1,147 @@
import type { Metadata } from "next";
import { Wifi, Tv, Phone, Server, Shield, Wrench } from "lucide-react";
import PageHero from "@/components/sections/PageHero";
import ContactSection from "@/components/sections/ContactSection";
import Container from "@/components/ui/Container";
import SectionHeading from "@/components/ui/SectionHeading";
import Button from "@/components/ui/Button";
export const metadata: Metadata = {
title: "Leistungen",
description:
"Alle Dienstleistungen von TeleNetSystems im Überblick. Internet, TV, Telefonie und IT-Lösungen für Privat- und Geschäftskunden in Reutte und Tirol.",
};
const services = [
{
icon: Wifi,
title: "Internet",
description:
"Kabel und Glasfaser für Privathaushalte und Unternehmen. Von 30 Mbit/s bis zu 500 Mbit/s — je nach Verfügbarkeit und Bedarf.",
href: "/internet",
image: "/images/gallery/internet.jpg",
imageAlt: "Internet-Dienste von TeleNetSystems",
},
{
icon: Tv,
title: "Fernsehen",
description:
"TV-Pakete über Kabel, von der Grundversorgung bis zum umfangreichen Premium-Paket. Für Privatkunden und Unternehmen.",
href: "/fernsehen",
image: "/images/gallery/tv-privat.jpg",
imageAlt: "TV-Dienste von TeleNetSystems",
},
{
icon: Phone,
title: "Telefonie",
description:
"Festnetz-Tarife, die zum Telefonierverhalten passen. Minutengenau oder als Flatrate, auch in Kombination mit Internet.",
href: "/telefonie",
image: "/images/gallery/telefonie1.jpg",
imageAlt: "Telefonie-Dienste von TeleNetSystems",
},
];
const additionalServices = [
{
icon: Server,
title: "IT-Infrastruktur",
description: "Netzwerke, Server und die Technik dahinter. Wir planen, installieren und betreuen Ihre IT-Umgebung.",
},
{
icon: Shield,
title: "Netzwerksicherheit",
description: "Firewalls, sichere Zugänge und Datenschutz. Damit Ihre Daten dort bleiben, wo sie hingehören.",
},
{
icon: Wrench,
title: "Technischer Support",
description: "Wenn etwas nicht funktioniert, sind wir da. Vor Ort in Reutte, nicht am anderen Ende einer Hotline.",
},
];
export default function LeistungenPage() {
return (
<>
<PageHero
title="Unsere Leistungen im Überblick"
description="Von Internet über TV bis zur kompletten IT-Betreuung. Alles aus einer Hand, alles aus Reutte."
backgroundImage="/images/gallery/firma1.jpg"
/>
{/* Main Services */}
<section className="py-20 lg:py-24" aria-labelledby="main-services-heading">
<Container>
<SectionHeading
title="Unsere Kernbereiche"
subtitle="Drei Dienste, auf die Sie sich verlassen können."
/>
<div className="space-y-16" id="main-services-heading">
{services.map((service, index) => {
const Icon = service.icon;
const isReversed = index % 2 !== 0;
return (
<div
key={service.title}
className={`grid grid-cols-1 items-center gap-12 lg:grid-cols-2 ${isReversed ? "lg:direction-rtl" : ""}`}
>
<div className={isReversed ? "lg:order-2" : ""}>
<div className="flex items-center gap-3 mb-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary-100">
<Icon className="h-5 w-5 text-primary-700" aria-hidden="true" />
</div>
<h3 className="text-title font-bold text-neutral-900">{service.title}</h3>
</div>
<p className="text-neutral-600 leading-relaxed">{service.description}</p>
<Button href={service.href} variant="outline" size="sm" className="mt-6">
Zu den {service.title}-Tarifen
</Button>
</div>
<div className={`overflow-hidden rounded-card ${isReversed ? "lg:order-1" : ""}`}>
<img
src={service.image}
alt={service.imageAlt}
className="w-full object-cover"
loading="lazy"
width={600}
height={400}
/>
</div>
</div>
);
})}
</div>
</Container>
</section>
{/* Additional Services */}
<section className="bg-neutral-50 py-20 lg:py-24" aria-labelledby="additional-services-heading">
<Container>
<SectionHeading
title="Darüber hinaus"
subtitle="Neben Internet, TV und Telefonie unterstützen wir Sie auch bei der IT-Infrastruktur."
/>
<div className="grid grid-cols-1 gap-8 md:grid-cols-3" id="additional-services-heading">
{additionalServices.map((service) => {
const Icon = service.icon;
return (
<div
key={service.title}
className="rounded-card bg-white p-8 shadow-sm border border-neutral-200"
>
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary-100">
<Icon className="h-6 w-6 text-primary-700" aria-hidden="true" />
</div>
<h3 className="mt-4 text-lg font-semibold text-neutral-900">{service.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-neutral-600">{service.description}</p>
</div>
);
})}
</div>
</Container>
</section>
<ContactSection />
</>
);
}

21
src/app/not-found.tsx Normal file
View File

@@ -0,0 +1,21 @@
import Container from "@/components/ui/Container";
import Button from "@/components/ui/Button";
export default function NotFound() {
return (
<section className="flex min-h-[60vh] items-center py-20">
<Container className="text-center">
<p className="text-6xl font-bold text-primary-500">404</p>
<h1 className="mt-4 text-2xl font-bold text-neutral-900">
Diese Seite gibt es leider nicht
</h1>
<p className="mt-4 text-neutral-600">
Die angeforderte Seite konnte nicht gefunden werden. Vielleicht hilft Ihnen die Startseite weiter.
</p>
<Button href="/" variant="primary" size="md" className="mt-8">
Zur Startseite
</Button>
</Container>
</section>
);
}

19
src/app/page.tsx Normal file
View File

@@ -0,0 +1,19 @@
import HeroSection from "@/components/sections/HeroSection";
import ServiceOverview from "@/components/sections/ServiceOverview";
import CustomerSegments from "@/components/sections/CustomerSegments";
import ValueProposition from "@/components/sections/ValueProposition";
import TrustSection from "@/components/sections/TrustSection";
import ContactSection from "@/components/sections/ContactSection";
export default function Home() {
return (
<>
<HeroSection />
<ServiceOverview />
<CustomerSegments />
<ValueProposition />
<TrustSection />
<ContactSection />
</>
);
}

View File

@@ -0,0 +1,63 @@
import type { Metadata } from "next";
import PageHero from "@/components/sections/PageHero";
import TariffTable from "@/components/sections/TariffTable";
import ContactSection from "@/components/sections/ContactSection";
import Container from "@/components/ui/Container";
import SectionHeading from "@/components/ui/SectionHeading";
import { TELEFONIE_TARIFE } from "@/lib/constants";
export const metadata: Metadata = {
title: "Telefonie",
description:
"Günstige Telefonie-Tarife in Reutte und Tirol. Festnetz-Flatrates und minutengenaue Abrechnung. Klar, einfach, regional.",
};
export default function TelefoniePage() {
return (
<>
<PageHero
title="Telefonie — klar und günstig"
description="Festnetz, das funktioniert. Ohne Vertragsfallen, ohne versteckte Kosten. Genau das, was Sie zum Telefonieren brauchen."
/>
{/* Telefonie Tarife */}
<section className="py-20 lg:py-24" aria-labelledby="telefonie-heading">
<Container>
<SectionHeading
title="Unsere Telefonie-Tarife"
subtitle="Von der minutengenauen Abrechnung bis zur Flatrate. Wählen Sie den Tarif, der zu Ihrem Telefonierverhalten passt."
/>
<TariffTable plans={TELEFONIE_TARIFE} />
</Container>
</section>
{/* Additional Info */}
<section className="bg-neutral-50 py-20 lg:py-24" aria-labelledby="telefonie-info-heading">
<Container>
<div className="mx-auto max-w-3xl">
<h2 id="telefonie-info-heading" className="text-title font-bold text-neutral-900 text-center">
Gut zu wissen
</h2>
<div className="mt-8 space-y-6 text-neutral-600 leading-relaxed">
<p>
Unsere Telefonie-Tarife lassen sich mit jedem Internet-Paket kombinieren. So nutzen
Sie beides über einen Anschluss und haben nur einen Ansprechpartner.
</p>
<p>
Rufnummernmitnahme? Kein Problem. Wir kümmern uns darum, dass Ihre bestehende
Nummer erhalten bleibt. Der Wechsel läuft im Hintergrund Sie müssen sich um
nichts kümmern.
</p>
<p>
Für Geschäftskunden bieten wir auch Telefonanlagen und SIP-Trunking an.
Sprechen Sie uns einfach an.
</p>
</div>
</div>
</Container>
</section>
<ContactSection />
</>
);
}

117
src/app/ueber-uns/page.tsx Normal file
View File

@@ -0,0 +1,117 @@
import type { Metadata } from "next";
import { Heart, Target, Users, Zap } from "lucide-react";
import PageHero from "@/components/sections/PageHero";
import TeamGrid from "@/components/sections/TeamGrid";
import ContactSection from "@/components/sections/ContactSection";
import Container from "@/components/ui/Container";
import SectionHeading from "@/components/ui/SectionHeading";
export const metadata: Metadata = {
title: "Über uns",
description:
"Lernen Sie das Team hinter TeleNetSystems kennen. Regional verwurzelt in Reutte, Tirol. Persönlicher Service seit über 20 Jahren.",
};
const companyValues = [
{
icon: Heart,
title: "Ehrliche Beratung",
description: "Wir empfehlen, was sinnvoll ist — nicht, was die höchste Marge bringt.",
},
{
icon: Target,
title: "Klare Kommunikation",
description: "Was wir versprechen, halten wir. Ohne Sternchen im Kleingedruckten.",
},
{
icon: Users,
title: "Regionale Verantwortung",
description: "Wir leben und arbeiten hier. Was wir tun, hat direkten Einfluss auf unsere Nachbarn.",
},
{
icon: Zap,
title: "Schnelles Handeln",
description: "Wenn es brennt, sind wir da. Nicht morgen, sondern heute.",
},
];
export default function UeberUnsPage() {
return (
<>
<PageHero
title="Wir sind TeleNetSystems"
description="Ein Team aus Reutte, das Technik versteht und Menschen ernst nimmt."
backgroundImage="/images/team/team-telenet.jpg"
/>
{/* Company Story */}
<section className="py-20 lg:py-24" aria-labelledby="story-heading">
<Container>
<div className="grid grid-cols-1 items-center gap-12 lg:grid-cols-2">
<div>
<h2 id="story-heading" className="text-title font-bold text-neutral-900">
Aus der Region, für die Region
</h2>
<div className="mt-6 space-y-4 text-neutral-600 leading-relaxed">
<p>
TeleNetSystems gibt es nicht erst seit gestern. Seit über zwei Jahrzehnten
versorgen wir Reutte und das Außerfern mit Internet, Fernsehen und Telefonie.
Angefangen hat alles mit ein paar Kabelanschlüssen. Heute betreuen wir
über tausend Kunden privat und gewerblich.
</p>
<p>
Was sich nicht geändert hat: Wir sitzen hier. In Reutte, nicht in Wien oder
München. Wenn Sie anrufen, heben wir ab. Wenn etwas nicht funktioniert,
kommen wir vorbei. Das klingt selbstverständlich, ist es aber bei den
Großen der Branche längst nicht mehr.
</p>
<p>
Uns geht es nicht darum, die meisten Kunden zu haben. Es geht darum,
die Kunden, die wir haben, gut zu betreuen. Daran messen wir uns selbst.
</p>
</div>
</div>
<div className="overflow-hidden rounded-card">
<img
src="/images/gallery/firma1.jpg"
alt="TeleNetSystems Firmengebäude in Reutte"
className="w-full object-cover"
loading="lazy"
width={600}
height={400}
/>
</div>
</div>
</Container>
</section>
{/* Values */}
<section className="bg-neutral-50 py-20 lg:py-24" aria-labelledby="values-heading">
<Container>
<SectionHeading
title="Wofür wir stehen"
subtitle="Vier Grundsätze, an denen wir uns im Alltag messen."
/>
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4" id="values-heading">
{companyValues.map((value) => {
const Icon = value.icon;
return (
<div key={value.title} className="rounded-card bg-white p-6 shadow-sm border border-neutral-200">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary-100">
<Icon className="h-6 w-6 text-primary-700" aria-hidden="true" />
</div>
<h3 className="mt-4 text-base font-semibold text-neutral-900">{value.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-neutral-600">{value.description}</p>
</div>
);
})}
</div>
</Container>
</section>
<TeamGrid />
<ContactSection />
</>
);
}

View File

@@ -0,0 +1,118 @@
import Link from "next/link";
import { Phone, Mail, MapPin } from "lucide-react";
import Container from "@/components/ui/Container";
import { COMPANY_NAME, CONTACT, FOOTER_LINKS } from "@/lib/constants";
export default function Footer() {
const currentYear = new Date().getFullYear();
return (
<footer className="bg-neutral-900 text-neutral-300" role="contentinfo">
<Container className="py-16">
<div className="grid grid-cols-1 gap-12 sm:grid-cols-2 lg:grid-cols-4">
{/* Company Info */}
<div className="sm:col-span-2 lg:col-span-1">
<Link href="/" aria-label="TeleNetSystems Zur Startseite">
<img
src="/images/logo/logo-weiss.png"
alt="TeleNetSystems Logo"
className="h-10 w-auto"
width={160}
height={40}
/>
</Link>
<p className="mt-4 text-sm leading-relaxed text-neutral-400">
Ihr regionaler Partner für Internet, Fernsehen und Telefonie in Reutte und ganz Tirol.
</p>
<div className="mt-6 space-y-3">
<a
href={`tel:${CONTACT.phone.replace(/\s/g, "")}`}
className="flex items-center gap-2 text-sm text-neutral-400 transition-colors hover:text-white"
>
<Phone className="h-4 w-4 flex-shrink-0" aria-hidden="true" />
{CONTACT.phone}
</a>
<a
href={`mailto:${CONTACT.email}`}
className="flex items-center gap-2 text-sm text-neutral-400 transition-colors hover:text-white"
>
<Mail className="h-4 w-4 flex-shrink-0" aria-hidden="true" />
{CONTACT.email}
</a>
<div className="flex items-start gap-2 text-sm text-neutral-400">
<MapPin className="h-4 w-4 flex-shrink-0 mt-0.5" aria-hidden="true" />
<address className="not-italic">
{CONTACT.address.slice(1).join(", ")}
</address>
</div>
</div>
</div>
{/* Services */}
<div>
<h3 className="text-sm font-semibold uppercase tracking-wider text-white">
Dienste
</h3>
<ul className="mt-4 space-y-3">
{FOOTER_LINKS.services.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="text-sm text-neutral-400 transition-colors hover:text-white"
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
{/* Company */}
<div>
<h3 className="text-sm font-semibold uppercase tracking-wider text-white">
Unternehmen
</h3>
<ul className="mt-4 space-y-3">
{FOOTER_LINKS.company.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="text-sm text-neutral-400 transition-colors hover:text-white"
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
{/* Legal */}
<div>
<h3 className="text-sm font-semibold uppercase tracking-wider text-white">
Rechtliches
</h3>
<ul className="mt-4 space-y-3">
{FOOTER_LINKS.legal.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="text-sm text-neutral-400 transition-colors hover:text-white"
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
</div>
{/* Bottom Bar */}
<div className="mt-12 border-t border-neutral-800 pt-8 text-center">
<p className="text-sm text-neutral-500">
&copy; {currentYear} {COMPANY_NAME}. Alle Rechte vorbehalten.
</p>
</div>
</Container>
</footer>
);
}

View File

@@ -0,0 +1,79 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import Link from "next/link";
import { Menu } from "lucide-react";
import { cn } from "@/lib/utils";
import Navigation from "./Navigation";
import MobileMenu from "./MobileMenu";
import Button from "@/components/ui/Button";
export default function Header() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
};
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const closeMobileMenu = useCallback(() => {
setIsMobileMenuOpen(false);
}, []);
return (
<header
className={cn(
"fixed top-0 left-0 right-0 z-30 transition-all duration-300",
isScrolled
? "bg-white/95 shadow-sm backdrop-blur-sm"
: "bg-white"
)}
role="banner"
>
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-4 sm:px-6 lg:h-20 lg:px-8">
<Link
href="/"
className="flex-shrink-0"
aria-label="TeleNetSystems Zur Startseite"
>
<img
src="/images/logo/logo-systems.png"
alt="TeleNetSystems Logo"
className="h-9 w-auto lg:h-11"
width={180}
height={44}
/>
</Link>
<Navigation />
<div className="flex items-center gap-3">
<Button
href="/#kontakt"
variant="primary"
size="sm"
className="hidden lg:inline-flex"
>
Kontakt
</Button>
<button
onClick={() => setIsMobileMenuOpen(true)}
className="rounded-lg p-2 text-neutral-700 hover:bg-neutral-100 lg:hidden"
aria-label="Menü öffnen"
aria-expanded={isMobileMenuOpen}
aria-controls="mobile-menu"
>
<Menu className="h-6 w-6" />
</button>
</div>
</div>
<MobileMenu isOpen={isMobileMenuOpen} onClose={closeMobileMenu} />
</header>
);
}

View File

@@ -0,0 +1,121 @@
"use client";
import { useEffect, useRef } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
import { NAV_LINKS, CONTACT } from "@/lib/constants";
import Button from "@/components/ui/Button";
interface MobileMenuProps {
isOpen: boolean;
onClose: () => void;
}
export default function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
const pathname = usePathname();
const closeButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
closeButtonRef.current?.focus();
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [isOpen]);
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
if (isOpen) {
window.addEventListener("keydown", handleEscape);
}
return () => window.removeEventListener("keydown", handleEscape);
}, [isOpen, onClose]);
useEffect(() => {
onClose();
}, [pathname, onClose]);
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-40 bg-black/50"
onClick={onClose}
aria-hidden="true"
/>
<motion.div
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "tween", duration: 0.3 }}
className="fixed inset-y-0 right-0 z-50 w-full max-w-sm bg-white shadow-xl"
role="dialog"
aria-modal="true"
aria-label="Menü"
>
<div className="flex h-full flex-col">
<div className="flex items-center justify-between border-b border-neutral-200 p-4">
<span className="text-lg font-semibold text-neutral-900">Menü</span>
<button
ref={closeButtonRef}
onClick={onClose}
className="rounded-lg p-2 text-neutral-500 hover:bg-neutral-100 hover:text-neutral-700"
aria-label="Menü schließen"
>
<X className="h-6 w-6" />
</button>
</div>
<nav aria-label="Mobile Navigation" className="flex-1 overflow-y-auto p-4">
<ul className="space-y-1">
{NAV_LINKS.map((link) => {
const isActive = pathname.startsWith(link.href);
return (
<li key={link.href}>
<Link
href={link.href}
className={cn(
"block rounded-lg px-4 py-3 text-base font-medium transition-colors",
isActive
? "bg-primary-50 text-primary-700"
: "text-neutral-700 hover:bg-neutral-100"
)}
aria-current={isActive ? "page" : undefined}
>
{link.label}
</Link>
</li>
);
})}
</ul>
</nav>
<div className="border-t border-neutral-200 p-4 space-y-3">
<Button href="/#kontakt" variant="primary" size="md" className="w-full">
Kontakt aufnehmen
</Button>
<a
href={`tel:${CONTACT.phone.replace(/\s/g, "")}`}
className="block text-center text-sm text-neutral-600"
>
{CONTACT.phone}
</a>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
}

View File

@@ -0,0 +1,36 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import { NAV_LINKS } from "@/lib/constants";
export default function Navigation() {
const pathname = usePathname();
return (
<nav aria-label="Hauptnavigation" className="hidden lg:block">
<ul className="flex items-center gap-1">
{NAV_LINKS.map((link) => {
const isActive = pathname.startsWith(link.href);
return (
<li key={link.href}>
<Link
href={link.href}
className={cn(
"rounded-lg px-4 py-2 text-sm font-medium transition-colors duration-200",
isActive
? "bg-primary-50 text-primary-700"
: "text-neutral-700 hover:bg-neutral-100 hover:text-neutral-900"
)}
aria-current={isActive ? "page" : undefined}
>
{link.label}
</Link>
</li>
);
})}
</ul>
</nav>
);
}

View File

@@ -0,0 +1,137 @@
"use client";
import { Phone, Mail, MapPin, Clock } from "lucide-react";
import Container from "@/components/ui/Container";
import SectionHeading from "@/components/ui/SectionHeading";
import Button from "@/components/ui/Button";
import { CONTACT } from "@/lib/constants";
export default function ContactSection() {
return (
<section id="kontakt" className="bg-neutral-50 py-20 lg:py-24" aria-labelledby="contact-heading">
<Container>
<SectionHeading
title="Sprechen Sie mit uns"
subtitle="Rufen Sie an, schreiben Sie uns — oder kommen Sie einfach in Reutte vorbei."
/>
<div className="grid grid-cols-1 gap-12 lg:grid-cols-2" id="contact-heading">
{/* Contact Info */}
<div>
<h3 className="text-lg font-semibold text-neutral-900">Direkt erreichen</h3>
<p className="mt-2 text-neutral-600 leading-relaxed">
Kein Callcenter, keine automatische Weiterleitung. Bei uns landen Sie direkt bei den richtigen Leuten.
</p>
<div className="mt-8 space-y-6">
<a
href={`tel:${CONTACT.phone.replace(/\s/g, "")}`}
className="flex items-center gap-4 group"
>
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary-100 transition-colors group-hover:bg-primary-200">
<Phone className="h-5 w-5 text-primary-700" aria-hidden="true" />
</div>
<div>
<p className="text-sm text-neutral-500">Telefon</p>
<p className="font-semibold text-neutral-900">{CONTACT.phone}</p>
</div>
</a>
<a
href={`mailto:${CONTACT.email}`}
className="flex items-center gap-4 group"
>
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary-100 transition-colors group-hover:bg-primary-200">
<Mail className="h-5 w-5 text-primary-700" aria-hidden="true" />
</div>
<div>
<p className="text-sm text-neutral-500">E-Mail</p>
<p className="font-semibold text-neutral-900">{CONTACT.email}</p>
</div>
</a>
<div className="flex items-center gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary-100">
<MapPin className="h-5 w-5 text-primary-700" aria-hidden="true" />
</div>
<div>
<p className="text-sm text-neutral-500">Adresse</p>
<address className="not-italic font-semibold text-neutral-900">
{CONTACT.address[1]}, {CONTACT.address[2]}
</address>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary-100">
<Clock className="h-5 w-5 text-primary-700" aria-hidden="true" />
</div>
<div>
<p className="text-sm text-neutral-500">Öffnungszeiten</p>
<p className="font-semibold text-neutral-900">MoFr: 8:0017:00 Uhr</p>
</div>
</div>
</div>
</div>
{/* Contact Form */}
<div className="rounded-card bg-white p-8 shadow-sm border border-neutral-200">
<h3 className="text-lg font-semibold text-neutral-900">Nachricht senden</h3>
<p className="mt-1 text-sm text-neutral-500">Wir melden uns zeitnah bei Ihnen.</p>
<form className="mt-6 space-y-4" action={`mailto:${CONTACT.email}`} method="POST" encType="text/plain">
<div>
<label htmlFor="name" className="block text-sm font-medium text-neutral-700">
Name
</label>
<input
type="text"
id="name"
name="name"
required
className="mt-1 block w-full rounded-lg border border-neutral-300 px-4 py-3 text-neutral-900 placeholder:text-neutral-400 focus:border-primary-500 focus:ring-1 focus:ring-primary-500"
placeholder="Ihr Name"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-neutral-700">
E-Mail
</label>
<input
type="email"
id="email"
name="email"
required
className="mt-1 block w-full rounded-lg border border-neutral-300 px-4 py-3 text-neutral-900 placeholder:text-neutral-400 focus:border-primary-500 focus:ring-1 focus:ring-primary-500"
placeholder="ihre@email.at"
/>
</div>
<div>
<label htmlFor="phone-field" className="block text-sm font-medium text-neutral-700">
Telefon <span className="text-neutral-400">(optional)</span>
</label>
<input
type="tel"
id="phone-field"
name="phone"
className="mt-1 block w-full rounded-lg border border-neutral-300 px-4 py-3 text-neutral-900 placeholder:text-neutral-400 focus:border-primary-500 focus:ring-1 focus:ring-primary-500"
placeholder="+43 ..."
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-neutral-700">
Nachricht
</label>
<textarea
id="message"
name="message"
rows={4}
required
className="mt-1 block w-full rounded-lg border border-neutral-300 px-4 py-3 text-neutral-900 placeholder:text-neutral-400 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 resize-none"
placeholder="Wie können wir Ihnen helfen?"
/>
</div>
<Button type="submit" variant="primary" size="md" className="w-full">
Nachricht absenden
</Button>
</form>
</div>
</div>
</Container>
</section>
);
}

View File

@@ -0,0 +1,104 @@
import { User, Building2 } from "lucide-react";
import Container from "@/components/ui/Container";
import SectionHeading from "@/components/ui/SectionHeading";
import Button from "@/components/ui/Button";
export default function CustomerSegments() {
return (
<section className="bg-neutral-50 py-20 lg:py-24" aria-labelledby="segments-heading">
<Container>
<SectionHeading
title="Für wen wir da sind"
subtitle="Ob Privathaushalt oder Unternehmen — wir kennen die Anforderungen und finden die passende Lösung."
/>
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2" id="segments-heading">
{/* Privatkunden */}
<div className="overflow-hidden rounded-card bg-white shadow-sm border border-neutral-200">
<div className="aspect-video overflow-hidden">
<img
src="/images/gallery/privatkunden.jpg"
alt="Privatkunden von TeleNetSystems"
className="h-full w-full object-cover"
loading="lazy"
width={600}
height={338}
/>
</div>
<div className="p-8">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100">
<User className="h-5 w-5 text-blue-700" aria-hidden="true" />
</div>
<h3 className="text-xl font-semibold text-neutral-900">Privatkunden</h3>
</div>
<ul className="mt-5 space-y-2 text-neutral-600">
<li className="flex items-start gap-2">
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-primary-500" aria-hidden="true" />
Stabile Internet-Verbindung für den Alltag
</li>
<li className="flex items-start gap-2">
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-primary-500" aria-hidden="true" />
TV-Pakete, die zu Ihrem Sehverhalten passen
</li>
<li className="flex items-start gap-2">
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-primary-500" aria-hidden="true" />
Telefonie ohne Kleingedrucktes
</li>
<li className="flex items-start gap-2">
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-primary-500" aria-hidden="true" />
Persönlicher Ansprechpartner in Reutte
</li>
</ul>
<Button href="/internet" variant="outline" size="sm" className="mt-6">
Privatangebote ansehen
</Button>
</div>
</div>
{/* Geschäftskunden */}
<div className="overflow-hidden rounded-card bg-white shadow-sm border border-neutral-200">
<div className="aspect-video overflow-hidden">
<img
src="/images/gallery/firmenkunden.jpg"
alt="Geschäftskunden von TeleNetSystems"
className="h-full w-full object-cover"
loading="lazy"
width={600}
height={338}
/>
</div>
<div className="p-8">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-amber-100">
<Building2 className="h-5 w-5 text-amber-700" aria-hidden="true" />
</div>
<h3 className="text-xl font-semibold text-neutral-900">Geschäftskunden</h3>
</div>
<ul className="mt-5 space-y-2 text-neutral-600">
<li className="flex items-start gap-2">
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-primary-500" aria-hidden="true" />
Individuelle Internet- und IT-Lösungen
</li>
<li className="flex items-start gap-2">
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-primary-500" aria-hidden="true" />
TV für Hotels, Gastro und Büros
</li>
<li className="flex items-start gap-2">
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-primary-500" aria-hidden="true" />
Schneller technischer Support vor Ort
</li>
<li className="flex items-start gap-2">
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-primary-500" aria-hidden="true" />
Beratung, die Ihre Abläufe versteht
</li>
</ul>
<Button href="/#kontakt" variant="outline" size="sm" className="mt-6">
Beratung anfragen
</Button>
</div>
</div>
</div>
</Container>
</section>
);
}

View File

@@ -0,0 +1,68 @@
"use client";
import { motion } from "framer-motion";
import { ChevronDown } from "lucide-react";
import Button from "@/components/ui/Button";
export default function HeroSection() {
return (
<section
className="relative flex min-h-[90vh] items-center overflow-hidden bg-neutral-900"
aria-label="Willkommen bei TeleNetSystems"
>
{/* Background Image */}
<div className="absolute inset-0">
<img
src="/images/gallery/firma1.jpg"
alt=""
className="h-full w-full object-cover opacity-70"
width={1920}
height={1080}
aria-hidden="true"
/>
<div className="absolute inset-0 bg-gradient-to-r from-neutral-900/80 via-neutral-900/50 to-transparent" />
<div className="absolute inset-0 bg-gradient-to-t from-neutral-900/40 to-transparent" />
</div>
{/* Content */}
<div className="relative mx-auto max-w-6xl px-4 py-24 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="max-w-2xl"
>
<p className="mb-4 inline-block rounded-full bg-primary-500/20 px-4 py-1.5 text-sm font-semibold uppercase tracking-widest !text-white">
Aus Reutte. Für die Region.
</p>
<h1 className="text-4xl font-bold leading-tight tracking-tight !text-white sm:text-5xl lg:text-hero">
Technik, die verbindet.{" "}
<br className="hidden sm:block" />
<span className="text-primary-400">Service, der da ist.</span>
</h1>
<p className="mt-6 max-w-xl text-lg leading-relaxed !text-white/80 sm:text-xl">
Internet, Fernsehen und Telefonie direkt aus Reutte mit persönlicher Betreuung, die
Sie nicht bei einer Hotline suchen müssen.
</p>
<div className="mt-10 flex flex-col gap-4 sm:flex-row">
<Button href="/#kontakt" variant="primary" size="lg">
Jetzt beraten lassen
</Button>
<Button href="/leistungen" variant="outline" size="lg" className="border-white/30 text-white hover:bg-white/10 hover:text-white">
Unsere Leistungen
</Button>
</div>
</motion.div>
</div>
{/* Scroll Indicator */}
<motion.div
className="absolute bottom-8 left-1/2 -translate-x-1/2"
animate={{ y: [0, 8, 0] }}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
>
<ChevronDown className="h-6 w-6 text-white/50" aria-hidden="true" />
</motion.div>
</section>
);
}

View File

@@ -0,0 +1,45 @@
import Container from "@/components/ui/Container";
import { cn } from "@/lib/utils";
interface PageHeroProps {
title: string;
description?: string;
backgroundImage?: string;
}
export default function PageHero({ title, description, backgroundImage }: PageHeroProps) {
return (
<section
className={cn(
"relative flex items-end overflow-hidden py-20 sm:py-28 lg:py-32",
backgroundImage ? "bg-neutral-900" : "bg-gradient-to-br from-neutral-900 to-primary-950"
)}
>
{backgroundImage && (
<div className="absolute inset-0">
<img
src={backgroundImage}
alt=""
className="h-full w-full object-cover opacity-70"
width={1920}
height={600}
aria-hidden="true"
/>
<div className="absolute inset-0 bg-gradient-to-t from-neutral-900/70 via-neutral-900/30 to-transparent" />
<div className="absolute inset-0 bg-gradient-to-r from-neutral-900/60 to-transparent" />
</div>
)}
<Container className="relative">
<div className="mb-4 h-1 w-16 rounded bg-primary-500" />
<h1 className="text-3xl font-bold tracking-tight !text-white drop-shadow-lg sm:text-4xl lg:text-display">
{title}
</h1>
{description && (
<p className="mt-4 max-w-2xl text-lg !text-white/80 leading-relaxed drop-shadow-md">
{description}
</p>
)}
</Container>
</section>
);
}

View File

@@ -0,0 +1,40 @@
import { Wifi, Tv, Phone } from "lucide-react";
import Container from "@/components/ui/Container";
import SectionHeading from "@/components/ui/SectionHeading";
import Card from "@/components/ui/Card";
import { SERVICES } from "@/lib/constants";
const icons = [Wifi, Tv, Phone];
export default function ServiceOverview() {
return (
<section className="py-20 lg:py-24" aria-labelledby="services-heading">
<Container>
<SectionHeading
title="Was wir für Sie tun"
subtitle="Drei Dienste, ein Ansprechpartner. Alles aus einer Hand, direkt vor Ort in Reutte."
/>
<div className="grid grid-cols-1 gap-8 md:grid-cols-3" id="services-heading">
{SERVICES.map((service, index) => {
const Icon = icons[index];
return (
<Card
key={service.title}
title={service.title}
description={service.description}
imageSrc={service.image}
imageAlt={service.imageAlt}
href={service.href}
>
<div className="mt-4 flex items-center gap-2 text-primary-600 font-medium text-sm">
<Icon className="h-4 w-4" aria-hidden="true" />
<span>Mehr erfahren</span>
</div>
</Card>
);
})}
</div>
</Container>
</section>
);
}

View File

@@ -0,0 +1,54 @@
import { Check } from "lucide-react";
import Badge from "@/components/ui/Badge";
import Button from "@/components/ui/Button";
import { cn } from "@/lib/utils";
import type { TariffPlan } from "@/types";
interface TariffTableProps {
plans: TariffPlan[];
contactHref?: string;
}
export default function TariffTable({ plans, contactHref = "/#kontakt" }: TariffTableProps) {
return (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{plans.map((plan) => (
<div
key={plan.name}
className={cn(
"relative flex flex-col rounded-card border bg-white p-6 shadow-sm transition-shadow hover:shadow-md",
plan.recommended
? "border-primary-500 ring-1 ring-primary-500"
: "border-neutral-200"
)}
>
{plan.recommended && (
<div className="absolute -top-3 left-6">
<Badge variant="popular">Beliebt</Badge>
</div>
)}
<div className="mb-4">
<h3 className="text-lg font-semibold text-neutral-900">{plan.name}</h3>
<p className="mt-2 text-2xl font-bold text-neutral-900">{plan.price}</p>
</div>
<ul className="flex-1 space-y-3">
{plan.features.map((feature) => (
<li key={feature} className="flex items-start gap-2 text-sm text-neutral-600">
<Check className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary-600" aria-hidden="true" />
{feature}
</li>
))}
</ul>
<Button
href={contactHref}
variant={plan.recommended ? "primary" : "outline"}
size="sm"
className="mt-6 w-full"
>
Jetzt anfragen
</Button>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,38 @@
import Container from "@/components/ui/Container";
import SectionHeading from "@/components/ui/SectionHeading";
import { TEAM_MEMBERS } from "@/lib/constants";
export default function TeamGrid() {
return (
<section className="py-20 lg:py-24" aria-labelledby="team-heading">
<Container>
<SectionHeading
title="Unser Team"
subtitle="Die Menschen hinter TeleNetSystems. Persönlich erreichbar, fachlich versiert."
/>
<div className="grid grid-cols-2 gap-6 sm:grid-cols-3 lg:grid-cols-4" id="team-heading">
{TEAM_MEMBERS.map((member) => (
<div key={member.name} className="text-center">
<div className="mx-auto aspect-square w-full max-w-[200px] overflow-hidden rounded-2xl bg-neutral-100">
<img
src={member.image}
alt={`${member.name}${member.role ? `, ${member.role}` : ""}`}
className="h-full w-full object-cover"
loading="lazy"
width={200}
height={200}
/>
</div>
<h3 className="mt-4 text-sm font-semibold text-neutral-900">
{member.name}
</h3>
{member.role && (
<p className="mt-1 text-xs text-neutral-500">{member.role}</p>
)}
</div>
))}
</div>
</Container>
</section>
);
}

View File

@@ -0,0 +1,32 @@
import Container from "@/components/ui/Container";
const stats = [
{ value: "20+", label: "Jahre Erfahrung" },
{ value: "1.000+", label: "Kunden in der Region" },
{ value: "Reutte", label: "Unser Standort" },
{ value: "24/7", label: "Netzüberwachung" },
];
export default function TrustSection() {
return (
<section className="bg-primary-950 py-16 lg:py-20" aria-labelledby="trust-heading">
<Container>
<h2 id="trust-heading" className="sr-only">
TeleNetSystems in Zahlen
</h2>
<div className="grid grid-cols-2 gap-8 lg:grid-cols-4">
{stats.map((stat) => (
<div key={stat.label} className="text-center">
<p className="text-3xl font-bold text-primary-400 sm:text-4xl">
{stat.value}
</p>
<p className="mt-2 text-sm text-neutral-300">
{stat.label}
</p>
</div>
))}
</div>
</Container>
</section>
);
}

View File

@@ -0,0 +1,55 @@
import { Shield, MapPin, Headphones, Award } from "lucide-react";
import Container from "@/components/ui/Container";
const values = [
{
icon: Shield,
title: "Zuverlässig",
description: "Stabile Netze, die halten, was sie versprechen. Wir setzen auf Technik, der Sie vertrauen können.",
},
{
icon: MapPin,
title: "Regional",
description: "Kein Callcenter, sondern echte Menschen in Reutte. Bei Fragen sind wir vor Ort.",
},
{
icon: Headphones,
title: "Persönlich",
description: "Sie erreichen uns direkt — ohne Warteschleifen und ohne Weiterleitung.",
},
{
icon: Award,
title: "Kompetent",
description: "Jahrelange Erfahrung im Außerfern. Wir kennen die Region und ihre Anforderungen.",
},
];
export default function ValueProposition() {
return (
<section className="py-20 lg:py-24" aria-labelledby="values-heading">
<Container>
<h2 id="values-heading" className="sr-only">
Unsere Werte
</h2>
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
{values.map((value) => {
const Icon = value.icon;
return (
<div key={value.title} className="text-center">
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-primary-100">
<Icon className="h-7 w-7 text-primary-700" aria-hidden="true" />
</div>
<h3 className="mt-4 text-lg font-semibold text-neutral-900">
{value.title}
</h3>
<p className="mt-2 text-sm leading-relaxed text-neutral-600">
{value.description}
</p>
</div>
);
})}
</div>
</Container>
</section>
);
}

View File

@@ -0,0 +1,26 @@
import { cn } from "@/lib/utils";
interface BadgeProps {
children: React.ReactNode;
variant?: "default" | "popular" | "privat" | "business";
}
const variants = {
default: "bg-neutral-100 text-neutral-700",
popular: "bg-primary-100 text-primary-800",
privat: "bg-blue-100 text-blue-800",
business: "bg-amber-100 text-amber-800",
};
export default function Badge({ children, variant = "default" }: BadgeProps) {
return (
<span
className={cn(
"inline-block rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-wide",
variants[variant]
)}
>
{children}
</span>
);
}

View File

@@ -0,0 +1,56 @@
import Link from "next/link";
import { cn } from "@/lib/utils";
interface ButtonProps {
children: React.ReactNode;
variant?: "primary" | "secondary" | "outline" | "ghost";
size?: "sm" | "md" | "lg";
href?: string;
className?: string;
type?: "button" | "submit";
onClick?: () => void;
}
const variants = {
primary: "bg-primary-600 text-white hover:bg-primary-700 focus-visible:ring-primary-500",
secondary: "bg-neutral-800 text-white hover:bg-neutral-900",
outline: "border-2 border-primary-600 text-primary-700 hover:bg-primary-50",
ghost: "text-neutral-700 hover:bg-neutral-100",
};
const sizes = {
sm: "px-4 py-2 text-sm",
md: "px-6 py-3 text-base",
lg: "px-8 py-4 text-lg",
};
export default function Button({
children,
variant = "primary",
size = "md",
href,
className,
type = "button",
onClick,
}: ButtonProps) {
const classes = cn(
"inline-flex items-center justify-center gap-2 rounded-lg font-medium transition-colors duration-200",
variants[variant],
sizes[size],
className
);
if (href) {
return (
<Link href={href} className={classes}>
{children}
</Link>
);
}
return (
<button type={type} className={classes} onClick={onClick}>
{children}
</button>
);
}

View File

@@ -0,0 +1,59 @@
import Link from "next/link";
import { cn } from "@/lib/utils";
interface CardProps {
title: string;
description: string;
imageSrc?: string;
imageAlt?: string;
href?: string;
children?: React.ReactNode;
className?: string;
}
export default function Card({
title,
description,
imageSrc,
imageAlt,
href,
children,
className,
}: CardProps) {
const content = (
<>
{imageSrc && (
<div className="aspect-video overflow-hidden rounded-t-card">
<img
src={imageSrc}
alt={imageAlt || title}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
loading="lazy"
width={600}
height={338}
/>
</div>
)}
<div className="p-6">
<h3 className="text-xl font-semibold text-neutral-900">{title}</h3>
<p className="mt-2 text-neutral-600 leading-relaxed">{description}</p>
{children}
</div>
</>
);
const cardClasses = cn(
"group overflow-hidden rounded-card bg-white shadow-sm border border-neutral-200 transition-shadow duration-300 hover:shadow-md",
className
);
if (href) {
return (
<Link href={href} className={cn(cardClasses, "block")}>
{content}
</Link>
);
}
return <div className={cardClasses}>{content}</div>;
}

View File

@@ -0,0 +1,15 @@
import { cn } from "@/lib/utils";
interface ContainerProps {
children: React.ReactNode;
className?: string;
as?: React.ElementType;
}
export default function Container({ children, className, as: Component = "div" }: ContainerProps) {
return (
<Component className={cn("mx-auto max-w-6xl px-4 sm:px-6 lg:px-8", className)}>
{children}
</Component>
);
}

View File

@@ -0,0 +1,43 @@
import { cn } from "@/lib/utils";
interface SectionHeadingProps {
title: string;
subtitle?: string;
align?: "left" | "center";
as?: "h1" | "h2" | "h3";
className?: string;
}
export default function SectionHeading({
title,
subtitle,
align = "center",
as: Tag = "h2",
className,
}: SectionHeadingProps) {
return (
<div
className={cn(
"mb-12",
align === "center" && "text-center",
className
)}
>
<Tag className="text-display font-bold tracking-tight text-neutral-900">
{title}
</Tag>
{subtitle && (
<p className="mt-4 max-w-2xl text-body-lg text-neutral-600 leading-relaxed mx-auto">
{subtitle}
</p>
)}
<div
className={cn(
"mt-4 h-1 w-12 rounded-full bg-primary-500",
align === "center" && "mx-auto"
)}
aria-hidden="true"
/>
</div>
);
}

150
src/lib/constants.ts Normal file
View File

@@ -0,0 +1,150 @@
import type { NavLink, TeamMember, TariffPlan, ContactInfo, ServiceCard } from "@/types";
export const COMPANY_NAME = "TeleNetSystems";
export const COMPANY_TAGLINE = "Internet, TV & Telefonie in Reutte";
export const CONTACT: ContactInfo = {
phone: "+43 5672 21400",
email: "info@telenetsystems.at",
address: [
"TeleNetSystems",
"Untermarkt 16",
"6600 Reutte",
"Tirol, Österreich",
],
};
export const NAV_LINKS: NavLink[] = [
{ href: "/fernsehen", label: "Fernsehen" },
{ href: "/internet", label: "Internet" },
{ href: "/telefonie", label: "Telefonie" },
{ href: "/leistungen", label: "Leistungen" },
{ href: "/ueber-uns", label: "Über uns" },
];
export const FOOTER_LINKS = {
services: [
{ href: "/fernsehen", label: "Fernsehen" },
{ href: "/internet", label: "Internet" },
{ href: "/telefonie", label: "Telefonie" },
],
company: [
{ href: "/leistungen", label: "Leistungen" },
{ href: "/ueber-uns", label: "Über uns" },
],
legal: [
{ href: "/impressum", label: "Impressum" },
{ href: "/datenschutz", label: "Datenschutz" },
],
};
export const TEAM_MEMBERS: TeamMember[] = [
{ name: "Wolfgang Schwaiger", role: "Geschäftsführer", image: "/images/team/wolfgang-schwaiger.jpg" },
{ name: "Martin Müller", role: "Technik", image: "/images/team/martin-mueller.jpg" },
{ name: "David Müller", role: "Technik", image: "/images/team/david-mueller.jpg" },
{ name: "Jürgen Gräßle", role: "Kundenbetreuung", image: "/images/team/juergen-graessle.jpg" },
{ name: "Julia Besler", role: "Verwaltung", image: "/images/team/julia-besler.jpg" },
{ name: "Furkan Demirel", role: "Technik", image: "/images/team/furkan-demirel.jpg" },
{ name: "Lukas Schennach", role: "Technik", image: "/images/team/lukas-schennach.jpg" },
{ name: "Mario Kien", role: "Technik", image: "/images/team/mario-kien.jpg" },
{ name: "Franz", role: "Technik", image: "/images/team/franz.jpg" },
{ name: "Lorena", role: "Verwaltung", image: "/images/team/lorena.jpg" },
{ name: "Timo", role: "Technik", image: "/images/team/timo.jpg" },
];
export const SERVICES: ServiceCard[] = [
{
title: "Internet",
description: "Schnelles und stabiles Internet für Reutte und Umgebung. Kabel- und Glasfaser-Tarife, die zu Ihrem Bedarf passen.",
href: "/internet",
image: "/images/gallery/internet.jpg",
imageAlt: "Glasfaserkabel für schnelles Internet in Tirol",
},
{
title: "Fernsehen",
description: "TV-Pakete für jeden Geschmack. Von der Grundversorgung bis zum individuellen Business-TV.",
href: "/fernsehen",
image: "/images/gallery/tv-privat.jpg",
imageAlt: "Fernsehempfang über TeleNetSystems",
},
{
title: "Telefonie",
description: "Klar und günstig telefonieren. Einfache Tarife ohne versteckte Kosten.",
href: "/telefonie",
image: "/images/gallery/telefon.jpg",
imageAlt: "Telefonie-Dienste von TeleNetSystems",
},
];
export const INTERNET_TARIFE: TariffPlan[] = [
{
name: "Internet 30",
price: "ab € 29,90/Monat",
category: "kabel",
features: ["Download bis 30 Mbit/s", "Upload bis 5 Mbit/s", "Keine Drosselung", "Inkl. WLAN-Router"],
},
{
name: "Internet 60",
price: "ab € 39,90/Monat",
category: "kabel",
recommended: true,
features: ["Download bis 60 Mbit/s", "Upload bis 10 Mbit/s", "Keine Drosselung", "Inkl. WLAN-Router"],
},
{
name: "Internet 100",
price: "ab € 49,90/Monat",
category: "kabel",
features: ["Download bis 100 Mbit/s", "Upload bis 20 Mbit/s", "Keine Drosselung", "Inkl. WLAN-Router"],
},
{
name: "Glasfaser 200",
price: "ab € 59,90/Monat",
category: "glasfaser",
features: ["Download bis 200 Mbit/s", "Upload bis 50 Mbit/s", "Keine Drosselung", "Inkl. WLAN-Router"],
},
{
name: "Glasfaser 500",
price: "ab € 79,90/Monat",
category: "glasfaser",
recommended: true,
features: ["Download bis 500 Mbit/s", "Upload bis 100 Mbit/s", "Keine Drosselung", "Inkl. WLAN-Router"],
},
];
export const TV_PRIVAT_PAKETE: TariffPlan[] = [
{
name: "TV Basis",
price: "ab € 9,90/Monat",
features: ["Über 40 Sender", "ORF, ZDF, ARD und mehr", "HD-Qualität", "Digitaler Empfang"],
},
{
name: "TV Komfort",
price: "ab € 19,90/Monat",
recommended: true,
features: ["Über 80 Sender", "Alle Basis-Sender", "Zusätzliche Spartensender", "HD und Full-HD"],
},
{
name: "TV Premium",
price: "ab € 29,90/Monat",
features: ["Über 120 Sender", "Alle Komfort-Sender", "Internationale Sender", "HD, Full-HD und 4K"],
},
];
export const TELEFONIE_TARIFE: TariffPlan[] = [
{
name: "Telefon Basis",
price: "ab € 4,90/Monat",
features: ["Festnetzanschluss", "Minutengenaue Abrechnung", "Österreichweit"],
},
{
name: "Telefon Flat",
price: "ab € 9,90/Monat",
recommended: true,
features: ["Festnetzanschluss", "Flatrate Österreich Festnetz", "Günstige Mobilfunktarife"],
},
{
name: "Telefon Flat Plus",
price: "ab € 14,90/Monat",
features: ["Festnetzanschluss", "Flatrate Österreich & Deutschland", "Günstige Mobilfunktarife", "Inkl. Voicemail"],
},
];

5
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,5 @@
import { clsx, type ClassValue } from "clsx";
export function cn(...inputs: ClassValue[]) {
return clsx(inputs);
}

39
src/types/index.ts Normal file
View File

@@ -0,0 +1,39 @@
export interface NavLink {
href: string;
label: string;
}
export interface TeamMember {
name: string;
role?: string;
image: string;
}
export interface TariffPlan {
name: string;
price: string;
features: string[];
recommended?: boolean;
category?: string;
description?: string;
}
export interface ServiceCard {
title: string;
description: string;
href: string;
image: string;
imageAlt: string;
}
export interface ValueItem {
title: string;
description: string;
icon: string;
}
export interface ContactInfo {
phone: string;
email: string;
address: string[];
}

42
tsconfig.json Normal file
View File

@@ -0,0 +1,42 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": [
"node_modules"
]
}