diff --git a/apps/contacts/apps/landing/astro.config.mjs b/apps/contacts/apps/landing/astro.config.mjs
new file mode 100644
index 000000000..83c9c822a
--- /dev/null
+++ b/apps/contacts/apps/landing/astro.config.mjs
@@ -0,0 +1,19 @@
+import { defineConfig } from 'astro/config';
+import tailwind from '@astrojs/tailwind';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [tailwind()],
+ output: 'static',
+ build: {
+ inlineStylesheets: 'auto',
+ },
+ vite: {
+ resolve: {
+ alias: {
+ '@components': '/src/components',
+ '@layouts': '/src/layouts',
+ },
+ },
+ },
+});
diff --git a/apps/contacts/apps/landing/package.json b/apps/contacts/apps/landing/package.json
new file mode 100644
index 000000000..99f05f455
--- /dev/null
+++ b/apps/contacts/apps/landing/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "@contacts/landing",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "astro dev --port 4321",
+ "start": "astro dev",
+ "build": "astro check && astro build",
+ "preview": "astro preview",
+ "astro": "astro",
+ "type-check": "astro check",
+ "format": "prettier --write .",
+ "clean": "rm -rf dist .astro node_modules"
+ },
+ "dependencies": {
+ "@astrojs/check": "^0.9.0",
+ "@manacore/shared-landing-ui": "workspace:*",
+ "astro": "^5.16.0",
+ "typescript": "^5.9.2"
+ },
+ "devDependencies": {
+ "@astrojs/tailwind": "^6.0.2",
+ "@tailwindcss/typography": "^0.5.18",
+ "@types/node": "^20.0.0",
+ "eslint": "^9.0.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-astro": "^1.0.0",
+ "prettier": "^3.6.2",
+ "prettier-plugin-astro": "^0.14.1",
+ "prettier-plugin-tailwindcss": "^0.6.14",
+ "tailwindcss": "^3.4.0"
+ }
+}
diff --git a/apps/contacts/apps/landing/src/components/CTA.astro b/apps/contacts/apps/landing/src/components/CTA.astro
new file mode 100644
index 000000000..78f3418ba
--- /dev/null
+++ b/apps/contacts/apps/landing/src/components/CTA.astro
@@ -0,0 +1,60 @@
+---
+// Call to Action section
+---
+
+
+
+
+
+
+
+
+
+ Bereit, deine Kontakte zu organisieren?
+
+
+ Starte kostenlos und importiere deine bestehenden Kontakte in Sekunden. Keine Kreditkarte
+ erforderlich.
+
+
+
+
+
+
+
+
+
Kostenlos starten
+
+
+
+
Google & vCard Import
+
+
+
+
Offline verfügbar
+
+
+
+
+
diff --git a/apps/contacts/apps/landing/src/components/Features.astro b/apps/contacts/apps/landing/src/components/Features.astro
new file mode 100644
index 000000000..d15d78460
--- /dev/null
+++ b/apps/contacts/apps/landing/src/components/Features.astro
@@ -0,0 +1,85 @@
+---
+// Features section for Contacts landing page
+
+const features = [
+ {
+ icon: ``,
+ title: 'Tags & Gruppen',
+ description:
+ 'Organisiere deine Kontakte mit farbigen Tags und Gruppen. Filtere und finde Kontakte blitzschnell.',
+ },
+ {
+ icon: ``,
+ title: 'Import & Export',
+ description:
+ 'Importiere Kontakte aus Google, CSV oder vCard. Exportiere jederzeit in gängige Formate.',
+ },
+ {
+ icon: ``,
+ title: 'Duplikaterkennung',
+ description:
+ 'Finde und merge doppelte Kontakte automatisch. Halte dein Adressbuch sauber und aktuell.',
+ },
+ {
+ icon: ``,
+ title: 'Notizen & Aktivitäten',
+ description:
+ 'Füge Notizen zu Kontakten hinzu und verfolge Aktivitäten. Vergiss nie wieder ein wichtiges Detail.',
+ },
+ {
+ icon: ``,
+ title: 'Kontaktfotos',
+ description:
+ 'Lade Profilfotos hoch oder synchronisiere sie automatisch. Erkenne Kontakte auf einen Blick.',
+ },
+ {
+ icon: ``,
+ title: 'Offline-First',
+ description:
+ 'Arbeite auch ohne Internet. Deine Kontakte werden lokal gespeichert und automatisch synchronisiert.',
+ },
+];
+---
+
+
+
+
+
+
+ Funktionen
+
+
Kontaktverwaltung neu gedacht
+
+ ManaContacts bietet alles, was du brauchst, um deine Kontakte effizient zu verwalten.
+
+
+
+
+
+ {
+ features.map((feature) => (
+
+
+
+
+
{feature.title}
+
{feature.description}
+
+ ))
+ }
+
+
+
diff --git a/apps/contacts/apps/landing/src/components/Footer.astro b/apps/contacts/apps/landing/src/components/Footer.astro
new file mode 100644
index 000000000..2fc0c9b68
--- /dev/null
+++ b/apps/contacts/apps/landing/src/components/Footer.astro
@@ -0,0 +1,109 @@
+---
+// Footer component
+
+const currentYear = new Date().getFullYear();
+
+const links = {
+ product: [
+ { name: 'Funktionen', href: '#features' },
+ { name: 'Preise', href: '#pricing' },
+ { name: 'Changelog', href: '/changelog' },
+ { name: 'Roadmap', href: '/roadmap' },
+ ],
+ legal: [
+ { name: 'Impressum', href: '/impressum' },
+ { name: 'Datenschutz', href: '/datenschutz' },
+ { name: 'AGB', href: '/agb' },
+ ],
+ support: [
+ { name: 'FAQ', href: '/faq' },
+ { name: 'Kontakt', href: '/kontakt' },
+ { name: 'Status', href: '/status' },
+ ],
+};
+---
+
+
diff --git a/apps/contacts/apps/landing/src/components/Hero.astro b/apps/contacts/apps/landing/src/components/Hero.astro
new file mode 100644
index 000000000..e324dd2fe
--- /dev/null
+++ b/apps/contacts/apps/landing/src/components/Hero.astro
@@ -0,0 +1,156 @@
+---
+// Hero section for Contacts landing page
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Smart Contact Management
+
+
+
+
+ Alle Kontakte an
+ einem Ort
+
+
+
+
+ Verwalte deine Kontakte mit Tags, Gruppen und Notizen. Importiere aus Google, CSV oder vCard
+ und finde Duplikate automatisch.
+
+
+
+
+
+
+
+
+ {
+ [1, 2, 3, 4, 5].map(() => (
+
+ ))
+ }
+
+
+ 500+ Nutzer vertrauen ManaContacts
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Kontakte
+
+
+
+
+
+
+ {
+ [
+ {
+ name: 'Anna Müller',
+ email: 'anna@example.com',
+ tags: ['Arbeit', 'Design'],
+ },
+ {
+ name: 'Max Schmidt',
+ email: 'max.schmidt@firma.de',
+ tags: ['Kunde'],
+ },
+ {
+ name: 'Sarah Weber',
+ email: 'sarah.w@startup.io',
+ tags: ['Arbeit', 'Dev'],
+ },
+ {
+ name: 'Tom Fischer',
+ email: 'tom@creative.de',
+ tags: ['Freelancer'],
+ },
+ {
+ name: 'Lisa Braun',
+ email: 'lisa.braun@uni.edu',
+ tags: ['Privat'],
+ },
+ ].map((contact) => (
+
+
+ {contact.name
+ .split(' ')
+ .map((n) => n[0])
+ .join('')}
+
+
+ {contact.name}
+ {contact.email}
+
+
+ {contact.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+ ))
+ }
+
+
+
+
+
+
+
diff --git a/apps/contacts/apps/landing/src/layouts/Layout.astro b/apps/contacts/apps/landing/src/layouts/Layout.astro
new file mode 100644
index 000000000..29b66e5c6
--- /dev/null
+++ b/apps/contacts/apps/landing/src/layouts/Layout.astro
@@ -0,0 +1,58 @@
+---
+import '../styles/global.css';
+import Analytics from '@manacore/shared-landing-ui/atoms/Analytics.astro';
+
+interface Props {
+ title?: string;
+ description?: string;
+}
+
+const {
+ title = 'ManaContacts - Smart Contact Management',
+ description = 'Verwalte deine Kontakte intelligent. Gruppen, Tags, Import/Export, Duplikaterkennung und mehr. Kostenlos starten.',
+} = Astro.props;
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ import.meta.env.PUBLIC_UMAMI_WEBSITE_ID && (
+
+ )
+ }
+
+ {title}
+
+
+
+
+
+
diff --git a/apps/contacts/apps/landing/src/pages/index.astro b/apps/contacts/apps/landing/src/pages/index.astro
new file mode 100644
index 000000000..4c28d0e91
--- /dev/null
+++ b/apps/contacts/apps/landing/src/pages/index.astro
@@ -0,0 +1,252 @@
+---
+import Layout from '@layouts/Layout.astro';
+import Hero from '@components/Hero.astro';
+import Features from '@components/Features.astro';
+import CTA from '@components/CTA.astro';
+import Footer from '@components/Footer.astro';
+
+// Try to import shared components if available
+let StepsSection: any = null;
+let PricingSection: any = null;
+try {
+ const shared = await import('@manacore/shared-landing-ui/sections/StepsSection.astro');
+ StepsSection = shared.default;
+} catch {
+ // Shared component not available
+}
+try {
+ const shared = await import('@manacore/shared-landing-ui/sections/PricingSection.astro');
+ PricingSection = shared.default;
+} catch {
+ // Shared component not available
+}
+
+// Steps data
+const steps = [
+ {
+ number: '1',
+ title: 'Kontakte importieren',
+ description:
+ 'Importiere deine bestehenden Kontakte aus Google, CSV oder vCard - oder starte von Null.',
+ image: '/screenshots/import.png',
+ },
+ {
+ number: '2',
+ title: 'Organisieren & Taggen',
+ description:
+ 'Ordne Kontakte mit Tags und Gruppen. Finde Duplikate automatisch und halte alles sauber.',
+ image: '/screenshots/organize.png',
+ },
+ {
+ number: '3',
+ title: 'Immer griffbereit',
+ description: 'Greife offline auf alle Kontakte zu. Synchronisiere nahtlos zwischen Geräten.',
+ image: '/screenshots/access.png',
+ },
+];
+
+// Pricing data
+const pricingPlans = [
+ {
+ name: 'Free',
+ price: '0',
+ period: '/Monat',
+ description: 'Perfekt zum Einstieg',
+ features: [
+ { text: 'Bis zu 100 Kontakte', included: true },
+ { text: 'Tags & Gruppen', included: true },
+ { text: 'vCard Import/Export', included: true },
+ { text: 'Web-App Zugang', included: true },
+ { text: 'Google Import', included: false },
+ { text: 'Duplikaterkennung', included: false },
+ ],
+ cta: {
+ text: 'Kostenlos starten',
+ href: '#',
+ },
+ },
+ {
+ name: 'Pro',
+ price: '4,99',
+ period: '/Monat',
+ description: 'Für alle deine Kontakte',
+ features: [
+ { text: 'Unbegrenzte Kontakte', included: true },
+ { text: 'Google Import', included: true },
+ { text: 'Duplikaterkennung', included: true },
+ { text: 'Kontaktfotos', included: true },
+ { text: 'Aktivitäten & Notizen', included: true },
+ { text: 'Priority Support', included: true },
+ ],
+ cta: {
+ text: 'Pro starten',
+ href: '#',
+ },
+ highlighted: true,
+ badge: 'Beliebt',
+ },
+ {
+ name: 'Team',
+ price: '9,99',
+ period: '/Monat',
+ description: 'Für Teams & Unternehmen',
+ features: [
+ { text: 'Alles aus Pro', included: true },
+ { text: 'Geteilte Kontakte', included: true },
+ { text: 'Team-Verwaltung', included: true },
+ { text: 'API-Zugang', included: true },
+ { text: 'Admin-Dashboard', included: true },
+ { text: 'SLA Garantie', included: true },
+ ],
+ cta: {
+ text: 'Team erstellen',
+ href: '#',
+ },
+ },
+];
+---
+
+
+
+
+
+ {
+ StepsSection && (
+
+ )
+ }
+
+ {
+ !StepsSection && (
+
+
+
+
+ So funktioniert's
+
+
So einfach geht's
+
In drei Schritten zu organisierten Kontakten
+
+
+
+ {steps.map((step) => (
+
+
+ {step.number}
+
+
{step.title}
+
{step.description}
+
+ ))}
+
+
+
+ )
+ }
+
+ {
+ PricingSection && (
+
+ )
+ }
+
+ {
+ !PricingSection && (
+
+
+
+
+ Preise
+
+
Einfache, transparente Preise
+
Starte kostenlos, upgrade wenn du mehr brauchst
+
+
+
+ {pricingPlans.map((plan) => (
+
+ {plan.badge && (
+
+ {plan.badge}
+
+ )}
+
{plan.name}
+
{plan.description}
+
+ {plan.price}€
+ {plan.period}
+
+
+ {plan.features.map((feature) => (
+ -
+ {feature.included ? (
+
+ ) : (
+
+ )}
+ {feature.text}
+
+ ))}
+
+
+ {plan.cta.text}
+
+
+ ))}
+
+
+
+ )
+ }
+
+
+
+
diff --git a/apps/contacts/apps/landing/src/styles/global.css b/apps/contacts/apps/landing/src/styles/global.css
new file mode 100644
index 000000000..350042fc5
--- /dev/null
+++ b/apps/contacts/apps/landing/src/styles/global.css
@@ -0,0 +1,78 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ --color-background-page: #0a0a0a;
+ --color-background-card: #1a1a1a;
+ --color-background-card-hover: #242424;
+ --color-text-primary: #ffffff;
+ --color-text-secondary: #d1d5db;
+ --color-text-muted: #9ca3af;
+ --color-border: #262626;
+ --color-border-hover: #3f3f3f;
+ --color-primary: #3b82f6;
+ --color-primary-hover: #2563eb;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ @apply bg-dark-bg text-white;
+ margin: 0;
+ padding: 0;
+ overflow-x: hidden;
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
+}
+
+/* Custom scrollbar */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ @apply bg-dark-bg;
+}
+
+::-webkit-scrollbar-thumb {
+ @apply bg-dark-border rounded-full;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ @apply bg-gray-600;
+}
+
+/* Section padding */
+section {
+ @apply py-16 md:py-24;
+}
+
+/* Container */
+.container {
+ @apply mx-auto max-w-7xl px-4 sm:px-6 lg:px-8;
+}
+
+/* Gradient text */
+.gradient-text {
+ @apply bg-gradient-to-r from-primary-400 to-primary-600 bg-clip-text text-transparent;
+}
+
+/* Button styles */
+.btn {
+ @apply inline-flex items-center justify-center rounded-lg px-6 py-3 font-medium transition-all duration-200;
+}
+
+.btn-primary {
+ @apply bg-primary-500 text-white hover:bg-primary-600;
+}
+
+.btn-secondary {
+ @apply border border-dark-border bg-dark-card text-white hover:bg-dark-surface;
+}
diff --git a/apps/contacts/apps/landing/tailwind.config.mjs b/apps/contacts/apps/landing/tailwind.config.mjs
new file mode 100644
index 000000000..258311ba9
--- /dev/null
+++ b/apps/contacts/apps/landing/tailwind.config.mjs
@@ -0,0 +1,53 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ './src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}',
+ '../../packages/shared-landing-ui/src/**/*.{astro,html,js,jsx,ts,tsx}',
+ ],
+ theme: {
+ extend: {
+ colors: {
+ // Contacts app theme - blue
+ primary: {
+ DEFAULT: '#3b82f6',
+ 50: '#eff6ff',
+ 100: '#dbeafe',
+ 200: '#bfdbfe',
+ 300: '#93c5fd',
+ 400: '#60a5fa',
+ 500: '#3b82f6',
+ 600: '#2563eb',
+ 700: '#1d4ed8',
+ 800: '#1e40af',
+ 900: '#1e3a8a',
+ 950: '#172554',
+ },
+ dark: {
+ bg: '#0a0a0a',
+ surface: '#111111',
+ card: '#1a1a1a',
+ border: '#262626',
+ },
+ // CSS variable mappings for shared-landing-ui compatibility
+ background: {
+ page: 'var(--color-background-page, #0a0a0a)',
+ card: 'var(--color-background-card, #1a1a1a)',
+ 'card-hover': 'var(--color-background-card-hover, #242424)',
+ },
+ text: {
+ primary: 'var(--color-text-primary, #ffffff)',
+ secondary: 'var(--color-text-secondary, #d1d5db)',
+ muted: 'var(--color-text-muted, #9ca3af)',
+ },
+ border: {
+ DEFAULT: 'var(--color-border, #262626)',
+ hover: 'var(--color-border-hover, #3f3f3f)',
+ },
+ },
+ fontFamily: {
+ sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
+ },
+ },
+ },
+ plugins: [require('@tailwindcss/typography')],
+};
diff --git a/apps/contacts/apps/landing/tsconfig.json b/apps/contacts/apps/landing/tsconfig.json
new file mode 100644
index 000000000..4b0f22d55
--- /dev/null
+++ b/apps/contacts/apps/landing/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@components/*": ["src/components/*"],
+ "@layouts/*": ["src/layouts/*"]
+ }
+ }
+}
diff --git a/apps/contacts/apps/landing/wrangler.toml b/apps/contacts/apps/landing/wrangler.toml
new file mode 100644
index 000000000..ecd3ed1d2
--- /dev/null
+++ b/apps/contacts/apps/landing/wrangler.toml
@@ -0,0 +1,3 @@
+name = "contacts-landing"
+compatibility_date = "2024-12-01"
+pages_build_output_dir = "dist"
diff --git a/apps/contacts/apps/server/src/index.ts b/apps/contacts/apps/server/src/index.ts
index 54d3e97ef..8ce038e3a 100644
--- a/apps/contacts/apps/server/src/index.ts
+++ b/apps/contacts/apps/server/src/index.ts
@@ -6,17 +6,32 @@
import { Hono } from 'hono';
import { cors } from 'hono/cors';
-import { authMiddleware, healthRoute, errorHandler, notFoundHandler } from '@manacore/shared-hono';
+import {
+ authMiddleware,
+ healthRoute,
+ errorHandler,
+ notFoundHandler,
+ rateLimitMiddleware,
+} from '@manacore/shared-hono';
const PORT = parseInt(process.env.PORT || '3004', 10);
const CORS_ORIGINS = (process.env.CORS_ORIGINS || 'http://localhost:5173').split(',');
+const ALLOWED_AVATAR_TYPES = new Set([
+ 'image/jpeg',
+ 'image/png',
+ 'image/gif',
+ 'image/webp',
+ 'image/svg+xml',
+]);
+
const app = new Hono();
app.onError(errorHandler);
app.notFound(notFoundHandler);
app.use('*', cors({ origin: CORS_ORIGINS, credentials: true }));
app.route('/health', healthRoute('contacts-server'));
+app.use('/api/*', rateLimitMiddleware({ max: 100, windowMs: 60_000 }));
app.use('/api/*', authMiddleware());
// ─── Avatar Upload (server-only: S3) ─────────────────────────
@@ -28,6 +43,9 @@ app.post('/api/v1/contacts/:id/avatar', async (c) => {
if (!file) return c.json({ error: 'No file' }, 400);
if (file.size > 5 * 1024 * 1024) return c.json({ error: 'Max 5MB' }, 400);
+ if (!ALLOWED_AVATAR_TYPES.has(file.type)) {
+ return c.json({ error: 'Invalid file type. Allowed: JPEG, PNG, GIF, WebP, SVG' }, 400);
+ }
try {
const { createContactsStorage, generateUserFileKey, getContentType } = await import(
diff --git a/apps/contacts/apps/web/src/app.html b/apps/contacts/apps/web/src/app.html
index 32f3f1e01..8cb6e08b3 100644
--- a/apps/contacts/apps/web/src/app.html
+++ b/apps/contacts/apps/web/src/app.html
@@ -1,10 +1,21 @@
-
+
- Kontakte
+
+
+
+
+
+
+
+
+
+
+
+ ManaContacts
%sveltekit.head%