2 Commits

Author SHA1 Message Date
Timo Knuth
65fe18a718 dynamisch barcode 2026-04-14 19:46:14 +02:00
Timo Knuth
6b73ac5c50 feat: implement pricing strategy, subscription tiers, and core infrastructure for QR code management 2026-04-14 19:34:47 +02:00
20 changed files with 3727 additions and 1344 deletions

234
.agents/pricing-strategy.md Normal file
View File

@@ -0,0 +1,234 @@
# QR Master — Pricing Strategy
*Erstellt: April 2026 | Basiert auf Marktforschung, Competitor-Scraping & SaaS-Benchmarks*
---
## 1. Marktkontext
### QR-Code-Markt 2025/2026
| Metrik | Wert |
|--------|------|
| Globale Marktgröße | $15,3 Mrd. (2025) |
| CAGR bis 2030 | 16,1% |
| US-Smartphone-User die QR scannen | 100+ Mio. monatlich |
| Business-Adoption | 50% der Unternehmen nutzen QR-Codes aktiv |
**Fazit:** Wachstumsmarkt mit noch großem Potenzial, besonders im KMU-Segment.
---
## 2. Wettbewerbs-Pricing-Map
### Vollständige Marktübersicht (aus Firecrawl-Recherche)
| Tool | Preis/Monat | Analytics | Dynamic | Bulk | Branding | Positionierung |
|------|------------|-----------|---------|------|----------|----------------|
| TQRCG | $5 | ✅ | ✅ | ❌ | ✅ | Value-Leader |
| QRStuff | $5 | ❌ | ❌ | ✅ | ❌ | Budget |
| ViralQR | $1,49 | ✅ | ✅ | ❌ | ✅ | Ultra-Budget |
| Beaconstac | $524 | ✅ | ✅ | ✅ | ✅ | SMBEnterprise |
| Bitly QR | $10 | ✅ | ✅ | ❌ | ✅ | Mid-Market |
| Unitag | $10 | ❌ | ✅ | ✅ | ✅ | Mid-Market |
| ZebraQR | $9 | ✅ | ✅ | ❌ | ✅ | Hospitality-Nische |
| QR Tiger | $1215 | ✅ | ✅ | ✅ | ✅ | Mid-Market+ |
| Hovercode | $15 | ✅ | ✅ | ✅ | ✅ | Growth-Fokus |
| Flowcode | $1015 | ✅ | ✅ | ❌ | ✅ | Design-Fokus |
| Scanova | $20 | ✅ | ✅ | ✅ | ✅ | Premium |
| QR Code Chimp | $20 | ✅ | ✅ | ✅ | ✅ | Premium-Design |
| Uniqode | $1030 | ✅ | ✅ | ✅ | ✅ | Enterprise |
| QRFY | $25 | ✅ | ✅ | ✅ | ✅ | Premium-Flat |
| QR Code Generator Pro | $1529 | ✅ | ✅ | ✅ | ✅ | Agency |
### Marktlücke für QR Master
> **Kein einziges Tool unter $12 bietet Analytics + Bulk + Custom Branding + DSGVO gleichzeitig.**
> Das ist exakt QR Masters Sweet Spot.
---
## 3. SaaS-Benchmark-Daten (Industrie)
| Metrik | Benchmark | Quelle |
|--------|-----------|--------|
| Median Entry-Level Preis (SaaS) | $29/mo | Monetizely 2025 |
| Free-to-Paid Conversion | ~5% | RevenueCat 2026 |
| Anteil Jahres-Abos (vs. Monatlich) | 68% annual / 32% monthly | RevenueCat 2026 |
| ARPU (Subscription Apps) | ~$30 | RevenueCat 2026 |
| Freemium-Anteil unter SaaS | 38% der Unternehmen | Monetizely 2025 |
| Hybrid-Pricing-Adoption | 61% | Monetizely 2025 |
| SaaS Churn (SMB) | 35%/Monat | Benchmark |
**Key Insight:** 68% der Subscriber wählen Jahrestarife. Das ist der wichtigste Hebel für Cashflow und Churn-Reduktion.
---
## 4. Value Metric Empfehlung
### Aktuelles Modell: Anzahl dynamischer QR-Codes
**Bewertung: Gut, aber optimierbar.**
Die Anzahl dynamischer Codes skaliert mit dem wahrgenommenen Wert (mehr Codes = mehr Kampagnen = mehr Wert). Jedoch:
- Limit von 8 FREE / 50 PRO / 500 BUSINESS ist nicht intuitiv kommuniziert
- Kunden denken in "Projekten" oder "Kampagnen", nicht in "Codes"
### Empfehlung: Hybrid-Metric einführen
Primär-Metric behalten (Dynamic Codes), aber mit Sekundär-Metriken ergänzen:
| Tier | Primär-Metric | Sekundär-Metriken |
|------|--------------|-------------------|
| FREE | 8 Dynamic Codes | 1 User, Basic Analytics, 30 Tage History |
| PRO | 50 Dynamic Codes | 13 User, Full Analytics, 1 Jahr History, Custom Domain |
| BUSINESS | 500 Dynamic Codes | Unlimitierte User, Advanced Analytics, Bulk, API |
---
## 5. Empfohlene Pricing-Struktur
### Tier-Empfehlung (Monatlich / Jährlich)
#### FREE — Kostenlos, für immer
- **8 dynamische QR-Codes** (klar kommuniziert als "8 Kampagnen")
- Unlimitierte statische Codes
- Basis-Analytics (Scans, Datum)
- QR Master Branding (nicht entfernbar)
- **Ziel:** Acquisition, Habit-Building, Virality durch Branding
#### PRO — €9/Monat (monatlich) | **€7/Monat (jährlich = €84/Jahr)**
*Empfohlen für: Restaurants, lokale Unternehmen, Marketing-Einsteiger*
- **50 dynamische QR-Codes**
- Custom Branding (kein QR Master Logo)
- Vollständige Analytics (Device, Location, OS, UTM)
- 1 Jahr Analytics-History
- Custom Domain für Redirects
- Prioritäts-Support
- **Rationale:** $79 liegt im bewiesenen Sweet Spot ($5$10) für diese Zielgruppe. Beaconstac Starter bei $5 hat nur 100 Scans — wir haben keine Scan-Limits.
#### BUSINESS — €24/Monat (monatlich) | **€19/Monat (jährlich = €228/Jahr)**
*Empfohlen für: Agenturen, Retail-Chains, Event-Organisatoren*
- **500 dynamische QR-Codes**
- Bulk-Upload (Excel/CSV bis 1.000 Zeilen)
- API-Zugriff
- Team-Management (bis 5 User)
- Erweiterte Analytics + Export (CSV, PDF)
- White-Label Option
- DSGVO-Compliance-Report
- **Rationale:** $1924 ist der Bereich wo Scanova ($20), Hovercode ($15) und QR Code Chimp ($20) spielen — aber keiner hat DSGVO + Bulk + Analytics zusammen.
#### ENTERPRISE — Auf Anfrage (ab €99/Monat)
*Für: Corporations, Franchise-Ketten*
- Unlimitierte Codes
- Dedizierter Account Manager
- Custom SLA
- SSO / SAML
- On-Premise Option (optional)
---
## 6. Psychologische Preisgestaltung
### Anchoring-Strategie
Reihenfolge auf Pricing-Page: **BUSINESS → PRO → FREE** (von teuer nach günstig)
→ PRO wirkt dadurch als "vernünftiger Kompromiss"
### Decoy-Effekt
PRO muss der offensichtliche "Best Deal" sein:
- BUSINESS ist 2,7× teurer als PRO aber hat 10× mehr Codes → Nur für Power-User
- FREE hat 6× weniger Codes als PRO → Upgrade liegt nahe
### Jahres-Pricing-Push
- Monatlich: €9 / €24
- Jährlich: €7 / €19 (sparst 22% / 21%)
- **Wichtig:** Jahrespreis prominent anzeigen mit "Spare 2 Monate" statt Prozent
- Default-Toggle: **Jährlich** (da 68% aller Subscriber Jahrestarife wählen)
### Charm vs. Round Pricing
- PRO: **€9** (nicht €10) → Charm Pricing für Conversion
- BUSINESS: **€24** (nicht €25) → Knapp unter psychologischer Grenze
- Jahrestarife: **€84/Jahr** und **€228/Jahr** (rund → Premium-Signal)
---
## 7. Jahres-Discount-Strategie
| Tier | Monatlich | Jährlich | Ersparnis |
|------|-----------|----------|-----------|
| PRO | €9/Mo | €84/Jahr (€7/Mo) | 22% / 2 Monate gratis |
| BUSINESS | €24/Mo | €228/Jahr (€19/Mo) | 21% / 2,5 Monate gratis |
**Kommunikation:** "2 Monate kostenlos bei jährlicher Zahlung" schlägt "20% Rabatt" in A/B-Tests regelmäßig.
---
## 8. Free-Tier-Optimierung
### Ziel des Free-Tiers
Nicht monetarisieren — **qualifizieren und konvertieren**.
### Empfohlene Trigger für Upgrade-Prompts
1. **Code-Limit erreicht** → "Du hast 8/8 Codes verwendet. Upgrade auf PRO für 50 Codes."
2. **Analytics-Feature geklickt** → "Detaillierte Location-Analytics nur in PRO."
3. **Custom Branding versucht** → "Entferne das QR Master Logo — upgrade auf PRO."
4. **Bulk-Upload versucht** → "Bulk-Upload ist nur in BUSINESS verfügbar."
5. **Nach 7 Tagen aktive Nutzung** → In-App Prompt: "Du nutzt QR Master aktiv — hole mehr raus."
### Virality-Mechanismus
- FREE-Codes enthalten subtiles "Made with QR Master" in Metadaten
- QR-Code-Landing-Pages (bei Dynamic Redirects) zeigen "Powered by QR Master" Footer
- Jeder Scan ist eine potenzielle Akquisition
---
## 9. Positioning Statement je Tier
**FREE:**
> "Starte kostenlos mit 8 professionellen QR-Codes — keine Kreditkarte erforderlich."
**PRO:**
> "Für Restaurants, lokale Geschäfte und Marketer: Unbegrenzte Änderungen, echte Analytics, dein Branding — für weniger als ein Mittagessen pro Monat."
**BUSINESS:**
> "Für Agenturen und Retail-Chains: Erstelle 500 Codes auf einmal, per Excel-Upload — DSGVO-konform, skalierbar, professionell."
---
## 10. Pricing Page Struktur (Empfehlung)
### Elemente above the fold
1. **Toggle: Monatlich / Jährlich** (Default: Jährlich)
2. **3 Tier-Karten** in Reihenfolge: FREE → PRO (highlighted "Beliebteste Wahl") → BUSINESS
3. **CTA je Tier:** "Kostenlos starten" / "14 Tage gratis testen" / "Jetzt upgraden"
4. **Trust-Signal:** "Keine Kreditkarte für Free • DSGVO-konform • Jederzeit kündbar"
### Weitere Sektionen
- Feature-Vergleichstabelle (vollständig)
- ROI-Rechner: "Wie viel sparst du durch dynamische QR-Codes vs. Neudruck?"
- FAQ (Objections aus Product-Marketing-Context)
- Testimonials-Sektion (Platzhalter für spätere echte Reviews)
- Enterprise-CTA am Ende
---
## 11. Kurzfristige Maßnahmen (Quick Wins)
| Priorität | Maßnahme | Impact |
|-----------|----------|--------|
| 🔴 Hoch | Jahrestarif als Default auf Pricing-Page setzen | +2030% ARPU sofort |
| 🔴 Hoch | "2 Monate gratis" Kommunikation (statt %) | +Conversion |
| 🟡 Mittel | Upgrade-Prompts bei Feature-Gates einbauen | +Free-to-Paid |
| 🟡 Mittel | 14-Tage PRO Trial (kreditkartenlos) | +Trial Signups |
| 🟢 Niedrig | BUSINESS Jahrespreis auf €228 festlegen | Cashflow |
| 🟢 Niedrig | Enterprise-Kontaktformular ergänzen | Upmarket |
---
## 12. Risiken & Gegenmaßnahmen
| Risiko | Wahrscheinlichkeit | Gegenmaßnahme |
|--------|-------------------|---------------|
| ViralQR mit $1,49 unterbietbar | Mittel | Auf Analytics + DSGVO differenzieren, nicht Preis |
| FREE-User konvertieren nicht | Hoch | Smarte Feature-Gates + E-Mail-Nurturing |
| BUSINESS-Preis zu hoch für KMU | Mittel | Jährlich-Preis betonen: €19/mo fühlt sich zugänglich an |
| Konkurrenten senken Preise | Niedrig | Value-Story stärken, nicht mitziehen |
---
*Datenbasis: Firecrawl-Scraping von 5+ Competitor-Seiten, QR Marktstatistiken 2026, RevenueCat State of Subscription Apps 2026, Monetizely SaaS Benchmark 2025, Product Marketing Context QR Master.*

View File

@@ -0,0 +1,60 @@
---
name: awesome-design-md
description: Use VoltAgent's awesome-design-md collection when the user wants UI inspired by a specific brand or asks for a DESIGN.md reference, visual system, or brand-style implementation such as Stripe, Linear, Vercel, Claude, or Supabase. Resolve the brand slug from the installed `design-md/` folder, fetch the matching `getdesign.md` design document for that slug, and apply it as the design-system reference for implementation.
---
# Awesome DESIGN.md
Use this skill to turn the installed `awesome-design-md` collection into a practical design reference workflow.
The local `design-md/` directory is the index of supported brand slugs. Its per-brand `README.md` files are only pointers. The actual design-system document lives at:
```text
https://getdesign.md/<slug>/design-md
```
## Workflow
1. Identify the target brand or closest visual reference.
2. Resolve the brand slug from the local `design-md/` folder.
3. Prefer exact folder names for dotted brands such as `linear.app`, `mistral.ai`, `opencode.ai`, `together.ai`, and `x.ai`.
4. Fetch `https://getdesign.md/<slug>/design-md`.
5. Use the fetched document in one of two ways:
- write or update the project's root `DESIGN.md`
- keep it as an external design reference while implementing UI
6. Preserve the user's product semantics and content model. Borrow visual language, spacing, typography, motion, and component patterns, not product-specific copy.
## Local Source Of Truth
Use the installed folder below to confirm which slugs exist before fetching:
```text
C:\Users\a931627\.claude\skills\awesome-design-md\design-md
```
If needed, list the available slugs with:
```powershell
Get-ChildItem -Name C:\Users\a931627\.claude\skills\awesome-design-md\design-md
```
## Practical Rules
- Treat `DESIGN.md` as a visual system reference, not as code to mirror verbatim.
- If the user asks for "something like X, but lighter, warmer, or more minimal", adapt the reference instead of cloning it literally.
- If multiple brands fit, choose the closest one and state the choice.
- If a slug is missing locally or the remote fetch fails, pick the nearest available brand or ask the user for a replacement target.
- When working inside an existing design system, merge the borrowed visual cues with the established component structure instead of replacing everything.
## Common Slug Examples
- `stripe`
- `vercel`
- `claude`
- `cursor`
- `supabase`
- `linear.app`
- `mistral.ai`
- `opencode.ai`
- `together.ai`
- `x.ai`

322
DESIGN.md Normal file
View File

@@ -0,0 +1,322 @@
# Design System Inspired by Stripe
## 1. Visual Theme & Atmosphere
Stripe's website is the gold standard of fintech design -- a system that manages to feel simultaneously technical and luxurious, precise and warm. The page opens on a clean white canvas (`#ffffff`) with deep navy headings (`#061b31`) and a signature purple (`#533afd`) that functions as both brand anchor and interactive accent. This isn't the cold, clinical purple of enterprise software; it's a rich, saturated violet that reads as confident and premium. The overall impression is of a financial institution redesigned by a world-class type foundry.
The custom `sohne-var` variable font is the defining element of Stripe's visual identity. Every text element enables the OpenType `"ss01"` stylistic set, which modifies character shapes for a distinctly geometric, modern feel. At display sizes (48px-56px), sohne-var runs at weight 300 -- an extraordinarily light weight for headlines that creates an ethereal, almost whispered authority. This is the opposite of the "bold hero headline" convention; Stripe's headlines feel like they don't need to shout. The negative letter-spacing (-1.4px at 56px, -0.96px at 48px) tightens the text into dense, engineered blocks. At smaller sizes, the system also uses weight 300 with proportionally reduced tracking, and tabular numerals via `"tnum"` for financial data display.
What truly distinguishes Stripe is its shadow system. Rather than the flat or single-layer approach of most sites, Stripe uses multi-layer, blue-tinted shadows: the signature `rgba(50,50,93,0.25)` combined with `rgba(0,0,0,0.1)` creates shadows with a cool, almost atmospheric depth -- like elements are floating in a twilight sky. The blue-gray undertone of the primary shadow color (50,50,93) ties directly to the navy-purple brand palette, making even elevation feel on-brand.
**Key Characteristics:**
- sohne-var with OpenType `"ss01"` on all text -- a custom stylistic set that defines the brand's letterforms
- Weight 300 as the signature headline weight -- light, confident, anti-convention
- Negative letter-spacing at display sizes (-1.4px at 56px, progressive relaxation downward)
- Blue-tinted multi-layer shadows using `rgba(50,50,93,0.25)` -- elevation that feels brand-colored
- Deep navy (`#061b31`) headings instead of black -- warm, premium, financial-grade
- Conservative border-radius (4px-8px) -- nothing pill-shaped, nothing harsh
- Ruby (`#ea2261`) and magenta (`#f96bee`) accents for gradient and decorative elements
- `SourceCodePro` as the monospace companion for code and technical labels
## 2. Color Palette & Roles
### Primary
- **Stripe Purple** (`#533afd`): Primary brand color, CTA backgrounds, link text, interactive highlights. A saturated blue-violet that anchors the entire system.
- **Deep Navy** (`#061b31`): `--hds-color-heading-solid`. Primary heading color. Not black, not gray -- a very dark blue that adds warmth and depth to text.
- **Pure White** (`#ffffff`): Page background, card surfaces, button text on dark backgrounds.
### Brand & Dark
- **Brand Dark** (`#1c1e54`): `--hds-color-util-brand-900`. Deep indigo for dark sections, footer backgrounds, and immersive brand moments.
- **Dark Navy** (`#0d253d`): `--hds-color-core-neutral-975`. The darkest neutral -- almost-black with a blue undertone for maximum depth without harshness.
### Accent Colors
- **Ruby** (`#ea2261`): `--hds-color-accentColorMode-ruby-icon-solid`. Warm red-pink for icons, alerts, and accent elements.
- **Magenta** (`#f96bee`): `--hds-color-accentColorMode-magenta-icon-gradientMiddle`. Vivid pink-purple for gradients and decorative highlights.
- **Magenta Light** (`#ffd7ef`): `--hds-color-util-accent-magenta-100`. Tinted surface for magenta-themed cards and badges.
### Interactive
- **Primary Purple** (`#533afd`): Primary link color, active states, selected elements.
- **Purple Hover** (`#4434d4`): Darker purple for hover states on primary elements.
- **Purple Deep** (`#2e2b8c`): `--hds-color-button-ui-iconHover`. Dark purple for icon hover states.
- **Purple Light** (`#b9b9f9`): `--hds-color-action-bg-subduedHover`. Soft lavender for subdued hover backgrounds.
- **Purple Mid** (`#665efd`): `--hds-color-input-selector-text-range`. Range selector and input highlight color.
### Neutral Scale
- **Heading** (`#061b31`): Primary headings, nav text, strong labels.
- **Label** (`#273951`): `--hds-color-input-text-label`. Form labels, secondary headings.
- **Body** (`#64748d`): Secondary text, descriptions, captions.
- **Success Green** (`#15be53`): Status badges, success indicators (with 0.2-0.4 alpha for backgrounds/borders).
- **Success Text** (`#108c3d`): Success badge text color.
- **Lemon** (`#9b6829`): `--hds-color-core-lemon-500`. Warning and highlight accent.
### Surface & Borders
- **Border Default** (`#e5edf5`): Standard border color for cards, dividers, and containers.
- **Border Purple** (`#b9b9f9`): Active/selected state borders on buttons and inputs.
- **Border Soft Purple** (`#d6d9fc`): Subtle purple-tinted borders for secondary elements.
- **Border Magenta** (`#ffd7ef`): Pink-tinted borders for magenta-themed elements.
- **Border Dashed** (`#362baa`): Dashed borders for drop zones and placeholder elements.
### Shadow Colors
- **Shadow Blue** (`rgba(50,50,93,0.25)`): The signature -- blue-tinted primary shadow color.
- **Shadow Dark Blue** (`rgba(3,3,39,0.25)`): Deeper blue shadow for elevated elements.
- **Shadow Black** (`rgba(0,0,0,0.1)`): Secondary shadow layer for depth reinforcement.
- **Shadow Ambient** (`rgba(23,23,23,0.08)`): Soft ambient shadow for subtle elevation.
- **Shadow Soft** (`rgba(23,23,23,0.06)`): Minimal ambient shadow for light lift.
## 3. Typography Rules
### Font Family
- **Primary**: `sohne-var`, with fallback: `SF Pro Display`
- **Monospace**: `SourceCodePro`, with fallback: `SFMono-Regular`
- **OpenType Features**: `"ss01"` enabled globally on all sohne-var text; `"tnum"` for tabular numbers on financial data and captions.
### Hierarchy
| Role | Font | Size | Weight | Line Height | Letter Spacing | Features | Notes |
|------|------|------|--------|-------------|----------------|----------|-------|
| Display Hero | sohne-var | 56px (3.50rem) | 300 | 1.03 (tight) | -1.4px | ss01 | Maximum size, whisper-weight authority |
| Display Large | sohne-var | 48px (3.00rem) | 300 | 1.15 (tight) | -0.96px | ss01 | Secondary hero headlines |
| Section Heading | sohne-var | 32px (2.00rem) | 300 | 1.10 (tight) | -0.64px | ss01 | Feature section titles |
| Sub-heading Large | sohne-var | 26px (1.63rem) | 300 | 1.12 (tight) | -0.26px | ss01 | Card headings, sub-sections |
| Sub-heading | sohne-var | 22px (1.38rem) | 300 | 1.10 (tight) | -0.22px | ss01 | Smaller section heads |
| Body Large | sohne-var | 18px (1.13rem) | 300 | 1.40 | normal | ss01 | Feature descriptions, intro text |
| Body | sohne-var | 16px (1.00rem) | 300-400 | 1.40 | normal | ss01 | Standard reading text |
| Button | sohne-var | 16px (1.00rem) | 400 | 1.00 (tight) | normal | ss01 | Primary button text |
| Button Small | sohne-var | 14px (0.88rem) | 400 | 1.00 (tight) | normal | ss01 | Secondary/compact buttons |
| Link | sohne-var | 14px (0.88rem) | 400 | 1.00 (tight) | normal | ss01 | Navigation links |
| Caption | sohne-var | 13px (0.81rem) | 400 | normal | normal | ss01 | Small labels, metadata |
| Caption Small | sohne-var | 12px (0.75rem) | 300-400 | 1.33-1.45 | normal | ss01 | Fine print, timestamps |
| Caption Tabular | sohne-var | 12px (0.75rem) | 300-400 | 1.33 | -0.36px | tnum | Financial data, numbers |
| Micro | sohne-var | 10px (0.63rem) | 300 | 1.15 (tight) | 0.1px | ss01 | Tiny labels, axis markers |
| Micro Tabular | sohne-var | 10px (0.63rem) | 300 | 1.15 (tight) | -0.3px | tnum | Chart data, small numbers |
| Nano | sohne-var | 8px (0.50rem) | 300 | 1.07 (tight) | normal | ss01 | Smallest labels |
| Code Body | SourceCodePro | 12px (0.75rem) | 500 | 2.00 (relaxed) | normal | -- | Code blocks, syntax |
| Code Bold | SourceCodePro | 12px (0.75rem) | 700 | 2.00 (relaxed) | normal | -- | Bold code, keywords |
| Code Label | SourceCodePro | 12px (0.75rem) | 500 | 2.00 (relaxed) | normal | uppercase | Technical labels |
| Code Micro | SourceCodePro | 9px (0.56rem) | 500 | 1.00 (tight) | normal | ss01 | Tiny code annotations |
### Principles
- **Light weight as signature**: Weight 300 at display sizes is Stripe's most distinctive typographic choice. Where others use 600-700 to command attention, Stripe uses lightness as luxury -- the text is so confident it doesn't need weight to be authoritative.
- **ss01 everywhere**: The `"ss01"` stylistic set is non-negotiable. It modifies specific glyphs (likely alternate `a`, `g`, `l` forms) to create a more geometric, contemporary feel across all sohne-var text.
- **Two OpenType modes**: `"ss01"` for display/body text, `"tnum"` for tabular numerals in financial data. These never overlap -- a number in a paragraph uses ss01, a number in a data table uses tnum.
- **Progressive tracking**: Letter-spacing tightens proportionally with size: -1.4px at 56px, -0.96px at 48px, -0.64px at 32px, -0.26px at 26px, normal at 16px and below.
- **Two-weight simplicity**: Primarily 300 (body and headings) and 400 (UI/buttons). No bold (700) in the primary font -- SourceCodePro uses 500/700 for code contrast.
## 4. Component Stylings
### Buttons
**Primary Purple**
- Background: `#533afd`
- Text: `#ffffff`
- Padding: 8px 16px
- Radius: 4px
- Font: 16px sohne-var weight 400, `"ss01"`
- Hover: `#4434d4` background
- Use: Primary CTA ("Start now", "Contact sales")
**Ghost / Outlined**
- Background: transparent
- Text: `#533afd`
- Padding: 8px 16px
- Radius: 4px
- Border: `1px solid #b9b9f9`
- Font: 16px sohne-var weight 400, `"ss01"`
- Hover: background shifts to `rgba(83,58,253,0.05)`
- Use: Secondary actions
**Transparent Info**
- Background: transparent
- Text: `#2874ad`
- Padding: 8px 16px
- Radius: 4px
- Border: `1px solid rgba(43,145,223,0.2)`
- Use: Tertiary/info-level actions
**Neutral Ghost**
- Background: transparent (`rgba(255,255,255,0)`)
- Text: `rgba(16,16,16,0.3)`
- Padding: 8px 16px
- Radius: 4px
- Outline: `1px solid rgb(212,222,233)`
- Use: Disabled or muted actions
### Cards & Containers
- Background: `#ffffff`
- Border: `1px solid #e5edf5` (standard) or `1px solid #061b31` (dark accent)
- Radius: 4px (tight), 5px (standard), 6px (comfortable), 8px (featured)
- Shadow (standard): `rgba(50,50,93,0.25) 0px 30px 45px -30px, rgba(0,0,0,0.1) 0px 18px 36px -18px`
- Shadow (ambient): `rgba(23,23,23,0.08) 0px 15px 35px 0px`
- Hover: shadow intensifies, often adding the blue-tinted layer
### Badges / Tags / Pills
**Neutral Pill**
- Background: `#ffffff`
- Text: `#000000`
- Padding: 0px 6px
- Radius: 4px
- Border: `1px solid #f6f9fc`
- Font: 11px weight 400
**Success Badge**
- Background: `rgba(21,190,83,0.2)`
- Text: `#108c3d`
- Padding: 1px 6px
- Radius: 4px
- Border: `1px solid rgba(21,190,83,0.4)`
- Font: 10px weight 300
### Inputs & Forms
- Border: `1px solid #e5edf5`
- Radius: 4px
- Focus: `1px solid #533afd` or purple ring
- Label: `#273951`, 14px sohne-var
- Text: `#061b31`
- Placeholder: `#64748d`
### Navigation
- Clean horizontal nav on white, sticky with blur backdrop
- Brand logotype left-aligned
- Links: sohne-var 14px weight 400, `#061b31` text with `"ss01"`
- Radius: 6px on nav container
- CTA: purple button right-aligned ("Sign in", "Start now")
- Mobile: hamburger toggle with 6px radius
### Decorative Elements
**Dashed Borders**
- `1px dashed #362baa` (purple) for placeholder/drop zones
- `1px dashed #ffd7ef` (magenta) for magenta-themed decorative borders
**Gradient Accents**
- Ruby-to-magenta gradients (`#ea2261` to `#f96bee`) for hero decorations
- Brand dark sections use `#1c1e54` backgrounds with white text
## 5. Layout Principles
### Spacing System
- Base unit: 8px
- Scale: 1px, 2px, 4px, 6px, 8px, 10px, 11px, 12px, 14px, 16px, 18px, 20px
- Notable: The scale is dense at the small end (every 2px from 4-12), reflecting Stripe's precision-oriented UI for financial data
### Grid & Container
- Max content width: approximately 1080px
- Hero: centered single-column with generous padding, lightweight headlines
- Feature sections: 2-3 column grids for feature cards
- Full-width dark sections with `#1c1e54` background for brand immersion
- Code/dashboard previews as contained cards with blue-tinted shadows
### Whitespace Philosophy
- **Precision spacing**: Unlike the vast emptiness of minimalist systems, Stripe uses measured, purposeful whitespace. Every gap is a deliberate typographic choice.
- **Dense data, generous chrome**: Financial data displays (tables, charts) are tightly packed, but the UI chrome around them is generously spaced. This creates a sense of controlled density -- like a well-organized spreadsheet in a beautiful frame.
- **Section rhythm**: White sections alternate with dark brand sections (`#1c1e54`), creating a dramatic light/dark cadence that prevents monotony without introducing arbitrary color.
### Border Radius Scale
- Micro (1px): Fine-grained elements, subtle rounding
- Standard (4px): Buttons, inputs, badges, cards -- the workhorse
- Comfortable (5px): Standard card containers
- Relaxed (6px): Navigation, larger interactive elements
- Large (8px): Featured cards, hero elements
- Compound: `0px 0px 6px 6px` for bottom-rounded containers (tab panels, dropdown footers)
## 6. Depth & Elevation
| Level | Treatment | Use |
|-------|-----------|-----|
| Flat (Level 0) | No shadow | Page background, inline text |
| Ambient (Level 1) | `rgba(23,23,23,0.06) 0px 3px 6px` | Subtle card lift, hover hints |
| Standard (Level 2) | `rgba(23,23,23,0.08) 0px 15px 35px` | Standard cards, content panels |
| Elevated (Level 3) | `rgba(50,50,93,0.25) 0px 30px 45px -30px, rgba(0,0,0,0.1) 0px 18px 36px -18px` | Featured cards, dropdowns, popovers |
| Deep (Level 4) | `rgba(3,3,39,0.25) 0px 14px 21px -14px, rgba(0,0,0,0.1) 0px 8px 17px -8px` | Modals, floating panels |
| Ring (Accessibility) | `2px solid #533afd` outline | Keyboard focus ring |
**Shadow Philosophy**: Stripe's shadow system is built on a principle of chromatic depth. Where most design systems use neutral gray or black shadows, Stripe's primary shadow color (`rgba(50,50,93,0.25)`) is a deep blue-gray that echoes the brand's navy palette. This creates shadows that don't just add depth -- they add brand atmosphere. The multi-layer approach pairs this blue-tinted shadow with a pure black secondary layer (`rgba(0,0,0,0.1)`) at a different offset, creating a parallax-like depth where the branded shadow sits farther from the element and the neutral shadow sits closer. The negative spread values (-30px, -18px) ensure shadows don't extend beyond the element's footprint horizontally, keeping elevation vertical and controlled.
### Decorative Depth
- Dark brand sections (`#1c1e54`) create immersive depth through background color contrast
- Gradient overlays with ruby-to-magenta transitions for hero decorations
- Shadow color `rgba(0,55,112,0.08)` (`--hds-color-shadow-sm-top`) for top-edge shadows on sticky elements
## 7. Do's and Don'ts
### Do
- Use sohne-var with `"ss01"` on every text element -- the stylistic set IS the brand
- Use weight 300 for all headlines and body text -- lightness is the signature
- Apply blue-tinted shadows (`rgba(50,50,93,0.25)`) for all elevated elements
- Use `#061b31` (deep navy) for headings instead of `#000000` -- the warmth matters
- Keep border-radius between 4px-8px -- conservative rounding is intentional
- Use `"tnum"` for any tabular/financial number display
- Layer shadows: blue-tinted far + neutral close for depth parallax
- Use `#533afd` purple as the primary interactive/CTA color
### Don't
- Don't use weight 600-700 for sohne-var headlines -- weight 300 is the brand voice
- Don't use large border-radius (12px+, pill shapes) on cards or buttons -- Stripe is conservative
- Don't use neutral gray shadows -- always tint with blue (`rgba(50,50,93,...)`)
- Don't skip `"ss01"` on any sohne-var text -- the alternate glyphs define the personality
- Don't use pure black (`#000000`) for headings -- always `#061b31` deep navy
- Don't use warm accent colors (orange, yellow) for interactive elements -- purple is primary
- Don't apply positive letter-spacing at display sizes -- Stripe tracks tight
- Don't use the magenta/ruby accents for buttons or links -- they're decorative/gradient only
## 8. Responsive Behavior
### Breakpoints
| Name | Width | Key Changes |
|------|-------|-------------|
| Mobile | <640px | Single column, reduced heading sizes, stacked cards |
| Tablet | 640-1024px | 2-column grids, moderate padding |
| Desktop | 1024-1280px | Full layout, 3-column feature grids |
| Large Desktop | >1280px | Centered content with generous margins |
### Touch Targets
- Buttons use comfortable padding (8px-16px vertical)
- Navigation links at 14px with adequate spacing
- Badges have 6px horizontal padding minimum for tap targets
- Mobile nav toggle with 6px radius button
### Collapsing Strategy
- Hero: 56px display -> 32px on mobile, weight 300 maintained
- Navigation: horizontal links + CTAs -> hamburger toggle
- Feature cards: 3-column -> 2-column -> single column stacked
- Dark brand sections: maintain full-width treatment, reduce internal padding
- Financial data tables: horizontal scroll on mobile
- Section spacing: 64px+ -> 40px on mobile
- Typography scale compresses: 56px -> 48px -> 32px hero sizes across breakpoints
### Image Behavior
- Dashboard/product screenshots maintain blue-tinted shadow at all sizes
- Hero gradient decorations simplify on mobile
- Code blocks maintain `SourceCodePro` treatment, may horizontally scroll
- Card images maintain consistent 4px-6px border-radius
## 9. Agent Prompt Guide
### Quick Color Reference
- Primary CTA: Stripe Purple (`#533afd`)
- CTA Hover: Purple Dark (`#4434d4`)
- Background: Pure White (`#ffffff`)
- Heading text: Deep Navy (`#061b31`)
- Body text: Slate (`#64748d`)
- Label text: Dark Slate (`#273951`)
- Border: Soft Blue (`#e5edf5`)
- Link: Stripe Purple (`#533afd`)
- Dark section: Brand Dark (`#1c1e54`)
- Success: Green (`#15be53`)
- Accent decorative: Ruby (`#ea2261`), Magenta (`#f96bee`)
### Example Component Prompts
- "Create a hero section on white background. Headline at 48px sohne-var weight 300, line-height 1.15, letter-spacing -0.96px, color #061b31, font-feature-settings 'ss01'. Subtitle at 18px weight 300, line-height 1.40, color #64748d. Purple CTA button (#533afd, 4px radius, 8px 16px padding, white text) and ghost button (transparent, 1px solid #b9b9f9, #533afd text, 4px radius)."
- "Design a card: white background, 1px solid #e5edf5 border, 6px radius. Shadow: rgba(50,50,93,0.25) 0px 30px 45px -30px, rgba(0,0,0,0.1) 0px 18px 36px -18px. Title at 22px sohne-var weight 300, letter-spacing -0.22px, color #061b31, 'ss01'. Body at 16px weight 300, #64748d."
- "Build a success badge: rgba(21,190,83,0.2) background, #108c3d text, 4px radius, 1px 6px padding, 10px sohne-var weight 300, border 1px solid rgba(21,190,83,0.4)."
- "Create navigation: white sticky header with backdrop-filter blur(12px). sohne-var 14px weight 400 for links, #061b31 text, 'ss01'. Purple CTA 'Start now' right-aligned (#533afd bg, white text, 4px radius). Nav container 6px radius."
- "Design a dark brand section: #1c1e54 background, white text. Headline 32px sohne-var weight 300, letter-spacing -0.64px, 'ss01'. Body 16px weight 300, rgba(255,255,255,0.7). Cards inside use rgba(255,255,255,0.1) border with 6px radius."
### Iteration Guide
1. Always enable `font-feature-settings: "ss01"` on sohne-var text -- this is the brand's typographic DNA
2. Weight 300 is the default; use 400 only for buttons/links/navigation
3. Shadow formula: `rgba(50,50,93,0.25) 0px Y1 B1 -S1, rgba(0,0,0,0.1) 0px Y2 B2 -S2` where Y1/B1 are larger (far shadow) and Y2/B2 are smaller (near shadow)
4. Heading color is `#061b31` (deep navy), body is `#64748d` (slate), labels are `#273951` (dark slate)
5. Border-radius stays in the 4px-8px range -- never use pill shapes or large rounding
6. Use `"tnum"` for any numbers in tables, charts, or financial displays
7. Dark sections use `#1c1e54` -- not black, not gray, but a deep branded indigo
8. SourceCodePro for code at 12px/500 with 2.00 line-height (very generous for readability)

View File

@@ -121,6 +121,7 @@ enum ContentType {
APP
COUPON
FEEDBACK
BARCODE
}
enum QRStatus {

View File

@@ -22,8 +22,10 @@ interface BulkQRData {
interface GeneratedQR {
title: string;
content: string; // Original URL
svg: string; // SVG markup
content: string;
svg: string;
slug?: string;
redirectUrl?: string;
}
export default function BulkCreationPage() {
@@ -35,16 +37,25 @@ export default function BulkCreationPage() {
const [loading, setLoading] = useState(false);
const [generatedQRs, setGeneratedQRs] = useState<GeneratedQR[]>([]);
const [userPlan, setUserPlan] = useState<string>('FREE');
const [isDynamic, setIsDynamic] = useState(false);
const [remainingDynamic, setRemainingDynamic] = useState(0);
// Check user plan on mount
// Check user plan and dynamic quota on mount
React.useEffect(() => {
const checkPlan = async () => {
try {
const response = await fetch('/api/user/plan');
if (response.ok) {
const data = await response.json();
const [planRes, statsRes] = await Promise.all([
fetch('/api/user/plan'),
fetch('/api/user/stats'),
]);
if (planRes.ok) {
const data = await planRes.json();
setUserPlan(data.plan || 'FREE');
}
if (statsRes.ok) {
const stats = await statsRes.json();
setRemainingDynamic((stats.dynamicLimit || 0) - (stats.dynamicUsed || 0));
}
} catch (error) {
console.error('Error checking plan:', error);
}
@@ -196,6 +207,58 @@ export default function BulkCreationPage() {
}
};
const generateDynamicQRCodes = async () => {
setLoading(true);
const toProcess = remainingDynamic > 0 ? data.slice(0, remainingDynamic) : [];
if (toProcess.length === 0) {
showToast('Du hast keine dynamischen QR-Codes mehr übrig. Bitte upgrade deinen Plan.', 'error');
setLoading(false);
return;
}
if (data.length > remainingDynamic) {
showToast(`Nur ${remainingDynamic} dynamische Codes verfügbar. Es werden nur die ersten ${remainingDynamic} Zeilen verarbeitet.`, 'warning');
}
try {
const QRCode = require('qrcode');
const results: GeneratedQR[] = [];
for (const row of toProcess) {
const title = String(row[mapping.title as keyof typeof row] || 'Untitled');
const url = String(row[mapping.content as keyof typeof row] || 'https://example.com');
const res = await fetchWithCsrf('/api/qrs', {
method: 'POST',
body: JSON.stringify({
title,
contentType: 'URL',
content: { url },
isStatic: false,
}),
});
if (res.ok) {
const qr = await res.json();
const redirectUrl = `${window.location.origin}/r/${qr.slug}`;
const svg = await QRCode.toString(redirectUrl, { type: 'svg', width: 300, margin: 2 });
results.push({ title, content: url, svg, slug: qr.slug, redirectUrl });
}
}
setGeneratedQRs(results);
setRemainingDynamic(prev => Math.max(0, prev - results.length));
setStep('complete');
showToast(`${results.length} dynamische QR-Codes erstellt!`, 'success');
} catch (error) {
console.error('Dynamic QR generation error:', error);
showToast('Fehler beim Erstellen der dynamischen QR-Codes', 'error');
} finally {
setLoading(false);
}
};
const downloadAllQRCodes = async () => {
const zip = new JSZip();
@@ -204,6 +267,18 @@ export default function BulkCreationPage() {
zip.file(fileName, qr.svg);
});
// Add metadata CSV for dynamic QR codes
const hasDynamic = generatedQRs.some(qr => qr.slug);
if (hasDynamic) {
const csvRows = ['title,original_url,redirect_url,slug'];
generatedQRs.forEach(qr => {
if (qr.slug) {
csvRows.push(`"${qr.title}","${qr.content}","${qr.redirectUrl}","${qr.slug}"`);
}
});
zip.file('metadata.csv', csvRows.join('\n'));
}
const blob = await zip.generateAsync({ type: 'blob' });
saveAs(blob, 'qr-codes-bulk.zip');
showToast('Download started!', 'success');
@@ -274,8 +349,8 @@ export default function BulkCreationPage() {
URL.revokeObjectURL(url);
};
// Show upgrade prompt if not Business plan
if (userPlan !== 'BUSINESS') {
// Show upgrade prompt if not Business or Enterprise plan
if (userPlan !== 'BUSINESS' && userPlan !== 'ENTERPRISE') {
return (
<div className="max-w-4xl mx-auto">
<Card className="mt-12">
@@ -309,6 +384,39 @@ export default function BulkCreationPage() {
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">{t('bulk.title')}</h1>
<p className="text-gray-600 mt-2">{t('bulk.subtitle')}</p>
{/* Static / Dynamic Toggle */}
<div className="mt-4 flex items-center gap-4 p-4 bg-gray-50 rounded-xl border border-gray-200">
<span className="text-sm font-medium text-gray-700">QR Code Type:</span>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
checked={!isDynamic}
onChange={() => setIsDynamic(false)}
className="accent-primary-600"
/>
<span className="text-sm font-medium">Static</span>
<span className="text-xs text-gray-500">(download only, no tracking)</span>
</label>
<label className={`flex items-center gap-2 ${userPlan === 'BUSINESS' || userPlan === 'ENTERPRISE' ? 'cursor-pointer' : 'opacity-50 cursor-not-allowed'}`}>
<input
type="radio"
checked={isDynamic}
onChange={() => setIsDynamic(true)}
disabled={userPlan !== 'BUSINESS' && userPlan !== 'ENTERPRISE'}
className="accent-primary-600"
/>
<span className="text-sm font-medium">Dynamic</span>
{isDynamic && remainingDynamic > 0 && (
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full">
{remainingDynamic} verbleibend
</span>
)}
{(userPlan !== 'BUSINESS' && userPlan !== 'ENTERPRISE') && (
<span className="text-xs text-amber-600">(Business Plan erforderlich)</span>
)}
</label>
</div>
</div>
{/* Template Warning Banner */}
@@ -641,8 +749,13 @@ export default function BulkCreationPage() {
<Button variant="outline" onClick={() => setStep('upload')}>
Back
</Button>
<Button onClick={generateStaticQRCodes} loading={loading}>
Generate {data.length} Static QR Codes
<Button
onClick={isDynamic ? generateDynamicQRCodes : generateStaticQRCodes}
loading={loading}
>
{isDynamic
? `Generate ${Math.min(data.length, remainingDynamic)} Dynamic QR Codes`
: `Generate ${data.length} Static QR Codes`}
</Button>
</div>
</CardContent>

View File

@@ -15,8 +15,9 @@ import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf';
import { showToast } from '@/components/ui/Toast';
import {
Globe, User, MapPin, Phone, FileText, Smartphone, Ticket, Star, HelpCircle, Upload
Globe, User, MapPin, Phone, FileText, Smartphone, Ticket, Star, HelpCircle, Upload, Barcode as BarcodeIcon
} from 'lucide-react';
import Barcode from 'react-barcode';
// Tooltip component for form field help
const Tooltip = ({ text }: { text: string }) => (
@@ -140,6 +141,7 @@ export default function CreatePage() {
{ value: 'APP', label: 'App Download', icon: Smartphone },
{ value: 'COUPON', label: 'Coupon / Discount', icon: Ticket },
{ value: 'FEEDBACK', label: 'Feedback / Review', icon: Star },
{ value: 'BARCODE', label: 'Barcode', icon: BarcodeIcon },
];
// Get QR content based on content type
@@ -170,6 +172,8 @@ export default function CreatePage() {
return `Coupon: ${content.code || 'SAVE20'} - ${content.discount || '20% OFF'}`;
case 'FEEDBACK':
return content.feedbackUrl || 'https://example.com/feedback';
case 'BARCODE':
return content.value || '123456789';
default:
return 'https://example.com';
}
@@ -642,6 +646,68 @@ export default function CreatePage() {
/>
</>
);
case 'BARCODE':
return (
<>
{isDynamic ? (
<>
<div className="rounded-lg bg-blue-50 border border-blue-200 p-3 text-sm text-blue-800">
<strong>How dynamic barcodes work:</strong> The barcode encodes a short redirect URL
(e.g. <span className="font-mono text-xs">qrmaster.net/r/</span>). When scanned with a
smartphone camera, it opens the browser and redirects to your destination which you
can update anytime. Works with smartphone cameras, not POS laser scanners.
</div>
<Input
label="Destination URL"
value={content.url || ''}
onChange={(e) => setContent({ ...content, url: e.target.value })}
placeholder="https://example.com"
required
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Barcode Format</label>
<select
value={['CODE128', 'CODE39'].includes(content.format) ? content.format : 'CODE128'}
onChange={(e) => setContent({ ...content, format: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
>
<option value="CODE128">CODE128 General purpose (recommended)</option>
<option value="CODE39">CODE39 Industrial / logistics</option>
</select>
<p className="text-xs text-gray-500 mt-1">
Only URL-capable formats available. EAN-13, UPC, and ITF-14 encode numbers only and cannot embed a redirect URL.
</p>
</div>
</>
) : (
<>
<Input
label="Barcode Value"
value={content.value || ''}
onChange={(e) => setContent({ ...content, value: e.target.value })}
placeholder="123456789012"
required
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Barcode Format</label>
<select
value={content.format || 'CODE128'}
onChange={(e) => setContent({ ...content, format: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
>
<option value="CODE128">CODE128 General purpose (recommended)</option>
<option value="EAN13">EAN-13 Retail products (international)</option>
<option value="UPC">UPC Retail products (USA/Canada)</option>
<option value="CODE39">CODE39 Industrial / logistics</option>
<option value="ITF14">ITF-14 Shipping containers</option>
<option value="MSI">MSI Shelf labeling / inventory</option>
<option value="pharmacode">Pharmacode Pharmaceutical packaging</option>
</select>
</div>
</>
)}
</>
);
default:
return null;
}
@@ -992,7 +1058,25 @@ export default function CreatePage() {
</div>
)}
{qrContent ? (
{contentType === 'BARCODE' ? (
qrContent ? (
<div className="p-2 bg-white">
<Barcode
value={qrContent}
format={content.format || 'CODE128'}
lineColor={foregroundColor}
background={backgroundColor}
width={2}
height={100}
displayValue={true}
/>
</div>
) : (
<div className="w-[200px] h-[200px] bg-gray-100 rounded flex items-center justify-center text-gray-500">
Enter barcode value
</div>
)
) : qrContent ? (
<div className={cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}>
<QRCodeSVG
value={qrContent}

View File

@@ -165,6 +165,8 @@ export default function SettingsPage() {
return { dynamic: 50, price: '€9', period: 'per month' };
case 'BUSINESS':
return { dynamic: 500, price: '€29', period: 'per month' };
case 'ENTERPRISE':
return { dynamic: 99999, price: 'Custom', period: 'per month' };
default:
return { dynamic: 3, price: '€0', period: 'forever' };
}

View File

@@ -9,11 +9,11 @@ import { Button } from '@/components/ui/Button';
import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf';
type LoginClientProps = {
showPageHeading?: boolean;
};
export default function LoginClient({ showPageHeading = true }: LoginClientProps) {
type LoginClientProps = {
showPageHeading?: boolean;
};
export default function LoginClient({ showPageHeading = true }: LoginClientProps) {
const router = useRouter();
const searchParams = useSearchParams();
const { t } = useTranslation();
@@ -22,6 +22,7 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -79,15 +80,15 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<Link href="/" className="inline-flex items-center space-x-2 mb-6">
<img src="/favicon1.png" alt="QR Master" className="w-10 h-10 rounded-full object-cover" />
<span className="text-2xl font-bold text-gray-900">QR Master</span>
</Link>
{showPageHeading ? (
<h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1>
) : (
<h2 className="text-3xl font-bold text-gray-900">Welcome Back</h2>
)}
<Link href="/" className="inline-flex items-center space-x-2 mb-6">
<img src="/favicon1.png" alt="QR Master" className="w-10 h-10 rounded-full object-cover" />
<span className="text-2xl font-bold text-gray-900">QR Master</span>
</Link>
{showPageHeading ? (
<h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1>
) : (
<h2 className="text-3xl font-bold text-gray-900">Welcome Back</h2>
)}
<p className="text-gray-600 mt-2">Sign in to your account</p>
<Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors">
Back to Home
@@ -112,14 +113,37 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
required
/>
<Input
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
<div className="space-y-1">
<label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
<div className="relative">
<input
id="password"
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="flex h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
tabIndex={-1}
>
{showPassword ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</button>
</div>
</div>
<div className="flex items-center justify-between">
<label className="flex items-center">

View File

@@ -19,6 +19,8 @@ export default function SignupClient() {
const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -88,10 +90,10 @@ export default function SignupClient() {
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<Link href="/" className="inline-flex items-center space-x-2 mb-6">
<img src="/favicon1.png" alt="QR Master" className="w-10 h-10 rounded-full object-cover" />
<span className="text-2xl font-bold text-gray-900">QR Master</span>
</Link>
<Link href="/" className="inline-flex items-center space-x-2 mb-6">
<img src="/favicon1.png" alt="QR Master" className="w-10 h-10 rounded-full object-cover" />
<span className="text-2xl font-bold text-gray-900">QR Master</span>
</Link>
<h1 className="text-3xl font-bold text-gray-900">Create Account</h1>
<p className="text-gray-600 mt-2">Start creating QR codes in seconds</p>
<Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors">
@@ -126,23 +128,69 @@ export default function SignupClient() {
required
/>
<Input
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
<div className="space-y-1">
<label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
<div className="relative">
<input
id="password"
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="flex h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
tabIndex={-1}
>
{showPassword ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</button>
</div>
</div>
<Input
label="Confirm Password"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
required
/>
<div className="space-y-1">
<label htmlFor="confirm-password" className="block text-sm font-medium text-gray-700">Confirm Password</label>
<div className="relative">
<input
id="confirm-password"
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
required
className="flex h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2"
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
tabIndex={-1}
>
{showConfirmPassword ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</button>
</div>
</div>
<Button type="submit" className="w-full" loading={loading}>
Create Account

File diff suppressed because it is too large Load Diff

View File

@@ -1,269 +1,315 @@
'use client';
import React, { useState, useEffect } from 'react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { showToast } from '@/components/ui/Toast';
import { useRouter } from 'next/navigation';
import { BillingToggle } from '@/components/ui/BillingToggle';
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
export default function PricingPage() {
const router = useRouter();
const [loading, setLoading] = useState<string | null>(null);
const [currentPlan, setCurrentPlan] = useState<string>('FREE');
const [currentInterval, setCurrentInterval] = useState<'month' | 'year' | null>(null);
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
useEffect(() => {
// Fetch current user plan
const fetchUserPlan = async () => {
try {
const response = await fetch('/api/user/plan');
if (response.ok) {
const data = await response.json();
setCurrentPlan(data.plan || 'FREE');
setCurrentInterval(data.interval || null);
}
} catch (error) {
console.error('Error fetching user plan:', error);
}
};
fetchUserPlan();
}, []);
const handleUpgrade = async (plan: 'PRO' | 'BUSINESS') => {
setLoading(plan);
try {
const response = await fetch('/api/stripe/create-checkout-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
plan,
billingInterval: billingPeriod === 'month' ? 'month' : 'year',
}),
});
if (!response.ok) {
throw new Error('Failed to create checkout session');
}
const { url } = await response.json();
window.location.href = url;
} catch (error) {
console.error('Error creating checkout session:', error);
showToast('Failed to start checkout. Please try again.', 'error');
setLoading(null);
}
};
const handleDowngrade = async () => {
// Show confirmation dialog
const confirmed = window.confirm(
'Are you sure you want to downgrade to the Free plan? Your subscription will be canceled immediately and you will lose access to premium features.'
);
if (!confirmed) {
return;
}
setLoading('FREE');
try {
const response = await fetch('/api/stripe/cancel-subscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to cancel subscription');
}
showToast('Successfully downgraded to Free plan', 'success');
// Refresh to update the plan
setTimeout(() => {
window.location.reload();
}, 1500);
} catch (error: any) {
console.error('Error canceling subscription:', error);
showToast(error.message || 'Failed to downgrade. Please try again.', 'error');
setLoading(null);
}
};
// Helper function to check if this is the user's exact current plan (plan + interval)
const isCurrentPlanWithInterval = (planType: string, interval: 'month' | 'year') => {
return currentPlan === planType && currentInterval === interval;
};
// Helper function to check if user has this plan but different interval
const hasPlanDifferentInterval = (planType: string) => {
return currentPlan === planType && currentInterval && currentInterval !== billingPeriod;
};
const selectedInterval = billingPeriod === 'month' ? 'month' : 'year';
const plans = [
{
key: 'free',
name: 'Free',
price: '€0',
period: 'forever',
showDiscount: false,
features: [
'3 active dynamic QR codes (8 types available)',
'Unlimited static QR codes',
'Basic scan tracking',
'Standard QR design templates',
'Download as SVG/PNG',
],
buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Downgrade to Free',
buttonVariant: 'outline' as const,
disabled: currentPlan === 'FREE',
popular: false,
onDowngrade: handleDowngrade,
},
{
key: 'pro',
name: 'Pro',
price: billingPeriod === 'month' ? '€9' : '€90',
period: billingPeriod === 'month' ? 'per month' : 'per year',
showDiscount: billingPeriod === 'year',
features: [
'50 dynamic QR codes',
'Unlimited static QR codes',
'Advanced analytics (scans, devices, locations)',
'Custom branding (colors & logos)',
],
buttonText: isCurrentPlanWithInterval('PRO', selectedInterval)
? 'Current Plan'
: hasPlanDifferentInterval('PRO')
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
: 'Upgrade to Pro',
buttonVariant: 'primary' as const,
disabled: isCurrentPlanWithInterval('PRO', selectedInterval),
popular: true,
onUpgrade: () => handleUpgrade('PRO'),
},
{
key: 'business',
name: 'Business',
price: billingPeriod === 'month' ? '€29' : '€290',
period: billingPeriod === 'month' ? 'per month' : 'per year',
showDiscount: billingPeriod === 'year',
features: [
'500 dynamic QR codes',
'Unlimited static QR codes',
'Everything from Pro',
'Bulk QR Creation (up to 1,000)',
'Priority email support',
'Advanced tracking & insights',
],
buttonText: isCurrentPlanWithInterval('BUSINESS', selectedInterval)
? 'Current Plan'
: hasPlanDifferentInterval('BUSINESS')
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
: 'Upgrade to Business',
buttonVariant: 'primary' as const,
disabled: isCurrentPlanWithInterval('BUSINESS', selectedInterval),
popular: false,
onUpgrade: () => handleUpgrade('BUSINESS'),
},
];
return (
<div className="container mx-auto px-4 py-12">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Choose Your Plan
</h1>
<p className="text-xl text-gray-600">
Select the perfect plan for your QR code needs
</p>
</div>
<div className="flex justify-center mb-8">
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
</div>
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto">
{plans.map((plan) => (
<Card
key={plan.key}
className={plan.popular ? 'border-primary-500 shadow-xl relative' : ''}
>
{plan.popular && (
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
<Badge variant="info" className="px-3 py-1">
Most Popular
</Badge>
</div>
)}
<CardHeader className="text-center pb-8">
<CardTitle className="text-2xl mb-4">
{plan.name}
</CardTitle>
<div className="flex flex-col items-center">
<div className="flex items-baseline justify-center">
<span className="text-4xl font-bold">
{plan.price}
</span>
<span className="text-gray-600 ml-2">
{plan.period}
</span>
</div>
{plan.showDiscount && (
<Badge variant="success" className="mt-2">
Save 16%
</Badge>
)}
</div>
</CardHeader>
<CardContent className="space-y-6">
<ul className="space-y-3">
{plan.features.map((feature: string, index: number) => (
<li key={index} className="flex items-start space-x-3">
<svg className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
<span className="text-gray-700">{feature}</span>
</li>
))}
</ul>
<Button
variant={plan.buttonVariant}
className="w-full"
size="lg"
disabled={plan.disabled || loading === plan.key.toUpperCase()}
onClick={plan.key === 'free' ? (plan as any).onDowngrade : (plan as any).onUpgrade}
>
{loading === plan.key.toUpperCase() ? 'Processing...' : plan.buttonText}
</Button>
</CardContent>
</Card>
))}
</div>
<div className="text-center mt-12">
<p className="text-gray-600">
All plans include unlimited static QR codes and basic customization.
</p>
<p className="text-gray-600 mt-2">
Need help choosing? <ObfuscatedMailto email="support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</ObfuscatedMailto>
</p>
</div>
</div>
);
}
'use client';
import React, { useState, useEffect } from 'react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { showToast } from '@/components/ui/Toast';
import { useRouter } from 'next/navigation';
import { BillingToggle } from '@/components/ui/BillingToggle';
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
export default function PricingPage() {
const router = useRouter();
const [loading, setLoading] = useState<string | null>(null);
const [currentPlan, setCurrentPlan] = useState<string>('FREE');
const [currentInterval, setCurrentInterval] = useState<
'month' | 'year' | null
>(null);
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
useEffect(() => {
// Fetch current user plan
const fetchUserPlan = async () => {
try {
const response = await fetch('/api/user/plan');
if (response.ok) {
const data = await response.json();
setCurrentPlan(data.plan || 'FREE');
setCurrentInterval(data.interval || null);
}
} catch (error) {
console.error('Error fetching user plan:', error);
}
};
fetchUserPlan();
}, []);
const handleUpgrade = async (plan: 'PRO' | 'BUSINESS') => {
setLoading(plan);
try {
const response = await fetch('/api/stripe/create-checkout-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
plan,
billingInterval: billingPeriod === 'month' ? 'month' : 'year',
}),
});
if (!response.ok) {
throw new Error('Failed to create checkout session');
}
const { url } = await response.json();
window.location.href = url;
} catch (error) {
console.error('Error creating checkout session:', error);
showToast('Failed to start checkout. Please try again.', 'error');
setLoading(null);
}
};
const handleDowngrade = async () => {
// Show confirmation dialog
const confirmed = window.confirm(
'Are you sure you want to downgrade to the Free plan? Your subscription will be canceled immediately and you will lose access to premium features.'
);
if (!confirmed) {
return;
}
setLoading('FREE');
try {
const response = await fetch('/api/stripe/cancel-subscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to cancel subscription');
}
showToast('Successfully downgraded to Free plan', 'success');
// Refresh to update the plan
setTimeout(() => {
window.location.reload();
}, 1500);
} catch (error: any) {
console.error('Error canceling subscription:', error);
showToast(
error.message || 'Failed to downgrade. Please try again.',
'error'
);
setLoading(null);
}
};
// Helper function to check if this is the user's exact current plan (plan + interval)
const isCurrentPlanWithInterval = (
planType: string,
interval: 'month' | 'year'
) => {
return currentPlan === planType && currentInterval === interval;
};
// Helper function to check if user has this plan but different interval
const hasPlanDifferentInterval = (planType: string) => {
return (
currentPlan === planType &&
currentInterval &&
currentInterval !== billingPeriod
);
};
const selectedInterval = billingPeriod === 'month' ? 'month' : 'year';
const plans = [
{
key: 'free',
name: 'Free',
price: '€0',
period: 'forever',
showDiscount: false,
features: [
'3 active dynamic QR codes (8 types available)',
'Unlimited static QR codes',
'Basic scan tracking',
'Standard QR design templates',
'Download as SVG/PNG',
],
buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Downgrade to Free',
buttonVariant: 'outline' as const,
disabled: currentPlan === 'FREE',
popular: false,
onDowngrade: handleDowngrade,
},
{
key: 'pro',
name: 'Pro',
price: billingPeriod === 'month' ? '€9' : '€90',
period: billingPeriod === 'month' ? 'per month' : 'per year',
showDiscount: billingPeriod === 'year',
features: [
'50 dynamic QR codes',
'Unlimited static QR codes',
'Advanced analytics (scans, devices, locations)',
'Custom branding (colors & logos)',
],
buttonText: isCurrentPlanWithInterval('PRO', selectedInterval)
? 'Current Plan'
: hasPlanDifferentInterval('PRO')
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
: 'Upgrade to Pro',
buttonVariant: 'primary' as const,
disabled: isCurrentPlanWithInterval('PRO', selectedInterval),
popular: true,
onUpgrade: () => handleUpgrade('PRO'),
},
{
key: 'business',
name: 'Business',
price: billingPeriod === 'month' ? '€29' : '€290',
period: billingPeriod === 'month' ? 'per month' : 'per year',
showDiscount: billingPeriod === 'year',
features: [
'500 dynamic QR codes',
'Unlimited static QR codes',
'Everything from Pro',
'Bulk QR Creation (up to 1,000)',
'Priority email support',
'Advanced tracking & insights',
],
buttonText: isCurrentPlanWithInterval('BUSINESS', selectedInterval)
? 'Current Plan'
: hasPlanDifferentInterval('BUSINESS')
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
: 'Upgrade to Business',
buttonVariant: 'primary' as const,
disabled: isCurrentPlanWithInterval('BUSINESS', selectedInterval),
popular: false,
onUpgrade: () => handleUpgrade('BUSINESS'),
},
{
key: 'enterprise',
name: 'Enterprise',
price: 'Custom',
period: '',
showDiscount: false,
features: [
'∞ dynamic QR codes',
'Unlimited static QR codes',
'Everything from Business',
'Dedicated Account Manager',
],
buttonText: 'Contact Us',
buttonVariant: 'outline' as const,
disabled: false,
popular: false,
onUpgrade: () => (window.location.href = 'mailto:timo@qrmaster.net'),
},
];
return (
<div className="container mx-auto px-4 py-12">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Choose Your Plan
</h1>
<p className="text-xl text-gray-600">
Select the perfect plan for your QR code needs
</p>
</div>
<div className="flex justify-center mb-8">
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8 max-w-7xl mx-auto">
{plans.map((plan) => (
<Card
key={plan.key}
className={
plan.popular ? 'border-primary-500 shadow-xl relative' : ''
}
>
{plan.popular && (
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
<Badge variant="info" className="px-3 py-1">
Most Popular
</Badge>
</div>
)}
<CardHeader className="text-center pb-8">
<CardTitle className="text-2xl mb-4">{plan.name}</CardTitle>
<div className="flex flex-col items-center">
<div className="flex items-baseline justify-center">
<span className="text-4xl font-bold">{plan.price}</span>
<span className="text-gray-600 ml-2">{plan.period}</span>
</div>
{plan.showDiscount && (
<Badge variant="success" className="mt-2">
Save 16%
</Badge>
)}
</div>
</CardHeader>
<CardContent className="space-y-6">
<ul className="space-y-3">
{plan.features.map((feature: string, index: number) => (
<li key={index} className="flex items-start space-x-3">
<svg
className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-gray-700">{feature}</span>
</li>
))}
</ul>
<Button
variant={plan.buttonVariant}
className="w-full"
size="lg"
disabled={plan.disabled || loading === plan.key.toUpperCase()}
onClick={
plan.key === 'free'
? (plan as any).onDowngrade
: (plan as any).onUpgrade
}
>
{loading === plan.key.toUpperCase()
? 'Processing...'
: plan.buttonText}
</Button>
</CardContent>
</Card>
))}
</div>
<div className="text-center mt-12">
<p className="text-gray-600">
All plans include unlimited static QR codes and basic customization.
</p>
<p className="text-gray-600 mt-2">
Need help choosing?{' '}
<ObfuscatedMailto
email="support@qrmaster.net"
className="text-primary-600 hover:text-primary-700 underline"
>
Contact our team
</ObfuscatedMailto>
</p>
</div>
</div>
);
}

View File

@@ -51,6 +51,7 @@ const PLAN_LIMITS = {
FREE: 3,
PRO: 50,
BUSINESS: 500,
ENTERPRISE: 99999,
};
// POST /api/qrs - Create a new QR code

View File

@@ -1,62 +1,64 @@
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { db } from '@/lib/db';
export const dynamic = 'force-dynamic';
export async function GET(request: NextRequest) {
try {
const userId = cookies().get('userId')?.value;
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Get user with plan info
const user = await db.user.findUnique({
where: { id: userId },
select: {
plan: true,
},
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
// Count dynamic QR codes
const dynamicQRCount = await db.qRCode.count({
where: {
userId,
type: 'DYNAMIC',
},
});
// Count static QR codes
const staticQRCount = await db.qRCode.count({
where: {
userId,
type: 'STATIC',
},
});
// Determine limits based on plan
let dynamicLimit = 3; // FREE plan default
if (user.plan === 'PRO') {
dynamicLimit = 50;
} else if (user.plan === 'BUSINESS') {
dynamicLimit = 500;
}
return NextResponse.json({
dynamicUsed: dynamicQRCount,
dynamicLimit,
staticUsed: staticQRCount,
});
} catch (error) {
console.error('Error fetching user stats:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { db } from '@/lib/db';
export const dynamic = 'force-dynamic';
export async function GET(request: NextRequest) {
try {
const userId = cookies().get('userId')?.value;
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Get user with plan info
const user = await db.user.findUnique({
where: { id: userId },
select: {
plan: true,
},
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
// Count dynamic QR codes
const dynamicQRCount = await db.qRCode.count({
where: {
userId,
type: 'DYNAMIC',
},
});
// Count static QR codes
const staticQRCount = await db.qRCode.count({
where: {
userId,
type: 'STATIC',
},
});
// Determine limits based on plan
let dynamicLimit = 3; // FREE plan default
if (user.plan === 'PRO') {
dynamicLimit = 50;
} else if (user.plan === 'BUSINESS') {
dynamicLimit = 500;
} else if ((user.plan as string) === 'ENTERPRISE') {
dynamicLimit = 99999;
}
return NextResponse.json({
dynamicUsed: dynamicQRCount,
dynamicLimit,
staticUsed: staticQRCount,
});
} catch (error) {
console.error('Error fetching user stats:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -87,6 +87,10 @@ export async function GET(
const baseUrlFeedback = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050';
destination = `${baseUrlFeedback}/feedback/${slug}`;
break;
case 'BARCODE':
// Dynamic barcode redirects to its stored URL
destination = ensureAbsoluteUrl(content.url || content.value || 'https://example.com');
break;
default:
destination = 'https://example.com';
}

View File

@@ -206,6 +206,12 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/dynamic-barcode-generator`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/bulk-qr-code-generator`,
lastModified: new Date(),

View File

@@ -1,141 +1,164 @@
'use client';
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { BillingToggle } from '@/components/ui/BillingToggle';
interface PricingProps {
t: any; // i18n translation function
}
export const Pricing: React.FC<PricingProps> = ({ t }) => {
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
const plans = [
{
key: 'free',
popular: false,
},
{
key: 'pro',
popular: true,
},
{
key: 'business',
popular: false,
},
];
return (
<section id="pricing" className="py-16">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="text-center mb-12"
>
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
{t.pricing.title}
</h2>
<p className="text-xl text-gray-600">
{t.pricing.subtitle}
</p>
</motion.div>
<div className="flex justify-center mb-8">
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
</div>
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
{plans.map((plan, index) => (
<motion.div
key={plan.key}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="h-full"
>
<Card
className={`h-full flex flex-col ${plan.popular
? 'border-primary-500 shadow-xl relative scale-105 z-10'
: 'border-gray-200 hover:border-gray-300 hover:shadow-lg transition-all'
}`}
>
{plan.popular && (
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2 w-full text-center">
<Badge variant="info" className="px-4 py-1.5 shadow-sm">
{t.pricing[plan.key].badge}
</Badge>
</div>
)}
<CardHeader className="text-center pb-8">
<CardTitle className="text-2xl mb-4">
{t.pricing[plan.key].title}
</CardTitle>
<div className="flex flex-col items-center">
<div className="flex items-baseline justify-center">
<span className="text-4xl font-bold">
{plan.key === 'free'
? t.pricing[plan.key].price
: billingPeriod === 'month'
? t.pricing[plan.key].price
: plan.key === 'pro'
? '€90'
: '€290'}
</span>
<span className="text-gray-600 ml-2">
{plan.key === 'free'
? t.pricing[plan.key].period
: billingPeriod === 'month'
? t.pricing[plan.key].period
: 'per year'}
</span>
</div>
{billingPeriod === 'year' && plan.key !== 'free' && (
<Badge variant="success" className="mt-2">
Save 16%
</Badge>
)}
</div>
</CardHeader>
<CardContent className="space-y-8 flex-1 flex flex-col">
<ul className="space-y-3 flex-1">
{t.pricing[plan.key].features.map((feature: string, index: number) => (
<li key={index} className="flex items-start space-x-3">
<svg className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
<span className="text-gray-700">{feature}</span>
</li>
))}
</ul>
<div className="mt-8 pt-8 border-t border-gray-100">
<Link href="/signup">
<Button
variant={plan.popular ? 'primary' : 'outline'}
className="w-full"
size="lg"
>
Get Started
</Button>
</Link>
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
);
};
'use client';
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { BillingToggle } from '@/components/ui/BillingToggle';
interface PricingProps {
t: any; // i18n translation function
}
export const Pricing: React.FC<PricingProps> = ({ t }) => {
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
const plans = [
{
key: 'free',
popular: false,
},
{
key: 'pro',
popular: true,
},
{
key: 'business',
popular: false,
},
{
key: 'enterprise',
popular: false,
},
];
return (
<section id="pricing" className="py-16">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="text-center mb-12"
>
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
{t.pricing.title}
</h2>
<p className="text-xl text-gray-600">{t.pricing.subtitle}</p>
</motion.div>
<div className="flex justify-center mb-8">
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8 max-w-7xl mx-auto">
{plans.map((plan, index) => (
<motion.div
key={plan.key}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="h-full"
>
<Card
className={`h-full flex flex-col ${
plan.popular
? 'border-primary-500 shadow-xl relative scale-105 z-10'
: 'border-gray-200 hover:border-gray-300 hover:shadow-lg transition-all'
}`}
>
{plan.popular && (
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2 w-full text-center">
<Badge variant="info" className="px-4 py-1.5 shadow-sm">
{t.pricing[plan.key].badge}
</Badge>
</div>
)}
<CardHeader className="text-center pb-8">
<CardTitle className="text-2xl mb-4">
{t.pricing[plan.key].title}
</CardTitle>
<div className="flex flex-col items-center">
<div className="flex items-baseline justify-center">
<span className="text-4xl font-bold">
{plan.key === 'free' || plan.key === 'enterprise'
? t.pricing[plan.key].price
: billingPeriod === 'month'
? t.pricing[plan.key].price
: plan.key === 'pro'
? '€90'
: '€290'}
</span>
<span className="text-gray-600 ml-2">
{plan.key === 'free' || plan.key === 'enterprise'
? t.pricing[plan.key].period
: billingPeriod === 'month'
? t.pricing[plan.key].period
: 'per year'}
</span>
</div>
{billingPeriod === 'year' &&
plan.key !== 'free' &&
plan.key !== 'enterprise' && (
<Badge variant="success" className="mt-2">
Save 16%
</Badge>
)}
</div>
</CardHeader>
<CardContent className="space-y-8 flex-1 flex flex-col">
<ul className="space-y-3 flex-1">
{t.pricing[plan.key].features.map(
(feature: string, index: number) => (
<li key={index} className="flex items-start space-x-3">
<svg
className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-gray-700">{feature}</span>
</li>
)
)}
</ul>
<div className="mt-8 pt-8 border-t border-gray-100">
{plan.key === 'enterprise' ? (
<Link href="mailto:timo@qrmaster.net">
<Button variant="outline" className="w-full" size="lg">
{t.pricing[plan.key].contact || 'Contact Us'}
</Button>
</Link>
) : (
<Link href="/signup">
<Button
variant={plan.popular ? 'primary' : 'outline'}
className="w-full"
size="lg"
>
Get Started
</Button>
</Link>
)}
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
);
};

View File

@@ -19,10 +19,10 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-8">
<div>
<Link href="/" className="flex items-center space-x-2 mb-4 hover:opacity-80 transition-opacity">
<img src="/favicon1.png" alt="QR Master Logo" className="w-[68px] h-[68px] rounded-full object-cover" />
<span className={`text-xl font-bold ${isDashboard ? 'text-gray-900' : ''}`}>QR Master</span>
</Link>
<Link href="/" className="flex items-center space-x-2 mb-4 hover:opacity-80 transition-opacity">
<img src="/favicon1.png" alt="QR Master Logo" className="w-[68px] h-[68px] rounded-full object-cover" />
<span className={`text-xl font-bold ${isDashboard ? 'text-gray-900' : ''}`}>QR Master</span>
</Link>
<p className={isDashboard ? 'text-gray-500' : 'text-gray-400'}>
{translations.tagline}
</p>
@@ -64,6 +64,7 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
<li><Link href="/faq" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.all_questions}</Link></li>
<li><Link href="/blog" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.all_articles}</Link></li>
<li><Link href="/bulk-qr-code-generator" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Bulk QR Generator</Link></li>
<li><Link href="/dynamic-barcode-generator" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Dynamic Barcode Generator</Link></li>
<li>
<Link href="/qr-code-for" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>
@@ -100,14 +101,14 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
</ul>
</div>
<div>
<h3 className={`font-semibold mb-4 ${isDashboard ? 'text-gray-900' : ''}`}>Compare</h3>
<ul className={`space-y-2 ${isDashboard ? 'text-gray-500' : 'text-gray-400'}`}>
<li><Link href="/alternatives" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>All Alternatives</Link></li>
<li><Link href="/vs" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>All Comparisons</Link></li>
<li><Link href="/alternatives/qr-code-generator" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>QR-Code-Generator Alt.</Link></li>
<li><Link href="/alternatives/flowcode" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Flowcode Alternative</Link></li>
<li><Link href="/alternatives/beaconstac" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Beaconstac Alternative</Link></li>
<div>
<h3 className={`font-semibold mb-4 ${isDashboard ? 'text-gray-900' : ''}`}>Compare</h3>
<ul className={`space-y-2 ${isDashboard ? 'text-gray-500' : 'text-gray-400'}`}>
<li><Link href="/alternatives" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>All Alternatives</Link></li>
<li><Link href="/vs" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>All Comparisons</Link></li>
<li><Link href="/alternatives/qr-code-generator" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>QR-Code-Generator Alt.</Link></li>
<li><Link href="/alternatives/flowcode" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Flowcode Alternative</Link></li>
<li><Link href="/alternatives/beaconstac" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Beaconstac Alternative</Link></li>
<li><Link href="/alternatives/bitly" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Bitly Alternative</Link></li>
<li><Link href="/vs/beaconstac" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>QR Master vs Uniqode</Link></li>
</ul>
@@ -136,7 +137,7 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
) : (
<div></div>
)}
<p>&copy; 2026 {translations.rights_reserved}</p>
<p>&copy; 2026 {translations.rights_reserved}</p>
<div className="w-12"></div>
</div>
</div>

View File

@@ -1,408 +1,421 @@
{
"nav": {
"features": "Funktionen",
"pricing": "Preise",
"faq": "FAQ",
"blog": "Blog",
"login": "Anmelden",
"dashboard": "Dashboard",
"about": "Über uns",
"contact": "Kontakt",
"signup": "Registrieren",
"learn": "Lernen",
"create_qr": "QR erstellen",
"bulk_creation": "Massen-Erstellung",
"analytics": "Analytik",
"settings": "Einstellungen",
"cta": "Kostenlos starten",
{
"nav": {
"features": "Funktionen",
"pricing": "Preise",
"faq": "FAQ",
"blog": "Blog",
"login": "Anmelden",
"dashboard": "Dashboard",
"about": "Über uns",
"contact": "Kontakt",
"signup": "Registrieren",
"learn": "Lernen",
"create_qr": "QR erstellen",
"bulk_creation": "Massen-Erstellung",
"analytics": "Analytik",
"settings": "Einstellungen",
"cta": "Kostenlos starten",
"tools": "Kostenlose Tools",
"all_free": "Alle Generatoren sind 100% kostenlos",
"resources": "Ressourcen",
"all_industries": "Branchen"
},
"hero": {
"badge": "Kostenloser QR-Code-Generator",
"title": "Erstellen Sie QR-Codes, die überall funktionieren",
"subtitle": "Generieren Sie statische und dynamische QR-Codes mit Tracking, individuellem Branding und Massen-Erstellung. Kostenlos für immer.",
"features": [
"Keine Kreditkarte zum Starten erforderlich",
"QR-Codes für immer kostenlos erstellen",
"Erweiterte Verfolgung und Analytik",
"Individuelle Farben und Stile"
],
"cta_primary": "QR-Code kostenlos erstellen",
"cta_secondary": "Preise ansehen",
"engagement_badge": "Kostenlos für immer",
"get_started": "Loslegen",
"view_full_pricing": "Alle Preisdetails ansehen →"
},
"trust": {
"users": "Aktive Nutzer",
"codes": "QR-Codes erstellt",
"scans": "Scans verfolgt",
"countries": "Länder"
},
"industries": {
"restaurant": "Restaurant-Kette",
"tech": "Tech-Startup",
"realestate": "Immobilien",
"events": "Event-Agentur",
"retail": "Einzelhandel",
"healthcare": "Gesundheitswesen"
},
"templates": {
"title": "Mit einer Vorlage beginnen",
"restaurant": "Restaurant-Menü",
"business": "Visitenkarte",
"vcard": "Kontaktkarte",
"event": "Event-Ticket",
"use_template": "Vorlage verwenden →"
},
"generator": {
"title": "Sofortiger QR-Code-Generator",
"url_placeholder": "Geben Sie hier Ihre URL ein...",
"foreground": "Vordergrund",
"background": "Hintergrund",
"corners": "Ecken",
"size": "Größe",
"contrast_good": "Guter Kontrast",
"download_svg": "SVG herunterladen",
"download_png": "PNG herunterladen",
"save_track": "Speichern & Verfolgen",
"live_preview": "Live-Vorschau",
"demo_note": "Dies ist ein Demo-QR-Code"
},
"static_vs_dynamic": {
"title": "Warum dynamische QR-Codes Ihnen Geld sparen",
"description": "Hören Sie auf, Materialien neu zu drucken. Wechseln Sie Ziele sofort und verfolgen Sie jeden Scan.",
"static": {
"title": "Statische QR-Codes",
"subtitle": "Immer kostenlos",
"description": "Perfekt für permanente Inhalte, die sich nie ändern",
"features": [
"Inhalt kann nicht bearbeitet werden",
"Keine Scan-Verfolgung",
"Funktioniert für immer",
"Kein Konto erforderlich"
]
},
"dynamic": {
"title": "Dynamische QR-Codes",
"subtitle": "Empfohlen",
"description": "Volle Kontrolle mit Tracking- und Bearbeitungsfunktionen",
"features": [
"Inhalt jederzeit bearbeiten",
"Erweiterte Analytik",
"Individuelles Branding",
"Bulk-Operationen"
]
}
},
"features": {
"title": "Alles was Sie brauchen, um professionelle QR-Codes zu erstellen",
"analytics": {
"title": "Erweiterte Analytik",
"description": "Verfolgen Sie Scans, Standorte, Geräte und Nutzerverhalten mit detaillierten Einblicken."
},
"customization": {
"title": "Vollständige Anpassung",
"description": "Branden Sie Ihre QR-Codes mit individuellen Farben, Logos und Styling-Optionen."
},
"unlimited": {
"title": "Unbegrenzte statische QR-Codes",
"description": "Erstellen Sie so viele statische QR-Codes wie Sie benötigen. Kostenlos für immer, ohne Limits."
},
"bulk": {
"title": "Bulk-Operationen",
"description": "Erstellen Sie hunderte von QR-Codes auf einmal mit CSV-Import und Batch-Verarbeitung."
},
"integrations": {
"title": "Integrationen",
"description": "Verbinden Sie sich mit Zapier, Airtable, Google Sheets und weiteren beliebten Tools."
},
"api": {
"title": "Entwickler-API",
"description": "Integrieren Sie QR-Code-Generierung in Ihre Anwendungen mit unserer REST-API."
},
"support": {
"title": "24/7 Support",
"description": "Erhalten Sie Hilfe, wenn Sie sie brauchen, mit unserem dedizierten Kundensupport-Team."
}
},
"pricing": {
"title": "Wählen Sie Ihren Plan",
"subtitle": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
"choose_plan": "Wählen Sie Ihren Plan",
"select_plan": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
"current_plan": "Aktueller Plan",
"upgrade_to": "Upgrade auf",
"downgrade_to_free": "Zu Kostenlos zurückstufen",
"most_popular": "Beliebteste",
"all_plans_note": "Alle Pläne beinhalten unbegrenzte statische QR-Codes und Basis-Anpassung.",
"free": {
"title": "Kostenlos",
"name": "Free",
"price": "€0",
"period": "für immer",
"features": [
"3 aktive dynamische QR-Codes (8 Typen verfügbar)",
"Unbegrenzte statische QR-Codes",
"Basis-Scan-Tracking",
"Standard QR-Design-Vorlagen"
]
},
"pro": {
"title": "Pro",
"name": "Pro",
"price": "€9",
"period": "pro Monat",
"badge": "Beliebteste",
"features": [
"50 dynamische QR-Codes",
"Unbegrenzte statische QR-Codes",
"Erweiterte Analytik (Scans, Geräte, Standorte)",
"Individuelles Branding (Farben)",
"Download als SVG/PNG"
]
},
"business": {
"title": "Business",
"name": "Business",
"price": "€29",
"period": "pro Monat",
"features": [
"500 dynamische QR-Codes",
"Unbegrenzte statische QR-Codes",
"Alles aus Pro",
"Massen-QR-Erstellung (bis zu 1.000)",
"Prioritäts-E-Mail-Support",
"Erweiterte Tracking & Insights"
]
}
},
"faq": {
"title": "Häufig gestellte Fragen",
"questions": {
"account": {
"question": "Benötige ich ein Konto, um QR-Codes zu erstellen?",
"answer": "Für statische QR-Codes ist kein Konto erforderlich. Dynamische QR-Codes mit Tracking- und Bearbeitungsfunktionen erfordern jedoch ein kostenloses Konto."
},
"static_vs_dynamic": {
"question": "Was ist der Unterschied zwischen statischen und dynamischen QR-Codes?",
"answer": "Statische QR-Codes enthalten feste Inhalte, die nicht geändert werden können. Dynamische QR-Codes können jederzeit bearbeitet werden und bieten detaillierte Analytik."
},
"forever": {
"question": "Funktionieren meine QR-Codes für immer?",
"answer": "Statische QR-Codes funktionieren für immer, da der Inhalt direkt eingebettet ist. Dynamische QR-Codes funktionieren, solange Ihr Konto aktiv ist."
},
"file_type": {
"question": "Welchen Dateityp sollte ich zum Drucken verwenden?",
"answer": "Für Druckmaterialien empfehlen wir das SVG-Format für Skalierbarkeit oder hochauflösendes PNG (300+ DPI) für beste Qualität."
},
"password": {
"question": "Kann ich einen QR-Code mit einem Passwort schützen?",
"answer": "Ja, Pro- und Business-Pläne beinhalten Passwortschutz und Zugriffskontrollfunktionen für Ihre QR-Codes."
},
"analytics": {
"question": "Wie funktioniert die Analytik?",
"answer": "Wir verfolgen Scans, Standorte, Geräte und Referrer unter Beachtung der Privatsphäre der Nutzer. Keine persönlichen Daten werden gespeichert."
},
"privacy": {
"question": "Verfolgen Sie persönliche Daten?",
"answer": "Wir respektieren die Privatsphäre und sammeln nur anonyme Nutzungsdaten. IP-Adressen werden gehasht und wir respektieren Do-Not-Track-Header."
},
"bulk": {
"question": "Kann ich Codes in großen Mengen mit meinen eigenen Daten erstellen?",
"answer": "Ja, Sie können CSV- oder Excel-Dateien hochladen, um mehrere QR-Codes auf einmal mit individueller Datenzuordnung zu erstellen."
}
}
},
"dashboard": {
"title": "Dashboard",
"subtitle": "Verwalten Sie Ihre QR-Codes und verfolgen Sie Ihre Performance",
"stats": {
"total_scans": "Gesamte Scans",
"active_codes": "Aktive QR-Codes",
"conversion_rate": "Konversionsrate"
},
"recent_codes": "Aktuelle QR-Codes",
"blog_resources": "Blog & Ressourcen",
"menu": {
"edit": "Bearbeiten",
"duplicate": "Duplizieren",
"pause": "Pausieren",
"delete": "Löschen"
}
},
"create": {
"title": "QR-Code erstellen",
"subtitle": "Generieren Sie dynamische und statische QR-Codes mit individuellem Branding",
"content": "Inhalt",
"type": "QR-Code-Typ",
"style": "Stil & Branding",
"preview": "Live-Vorschau",
"title_label": "Titel",
"title_placeholder": "Mein QR-Code",
"content_type": "Inhaltstyp",
"url_label": "URL",
"url_placeholder": "https://beispiel.de",
"tags_label": "Tags (durch Komma getrennt)",
"tags_placeholder": "marketing, kampagne, 2025",
"qr_code_type": "QR-Code-Typ",
"dynamic": "Dynamisch",
"static": "Statisch",
"recommended": "Empfohlen",
"dynamic_description": "Dynamisch: Scans verfolgen, URL später bearbeiten, Analytik ansehen. QR enthält Tracking-Link.",
"static_description": "Statisch: Direkt zum Inhalt, kein Tracking, nicht bearbeitbar. QR enthält tatsächlichen Inhalt.",
"foreground_color": "Vordergrundfarbe",
"background_color": "Hintergrundfarbe",
"corner_style": "Eckenstil",
"size": "Größe",
"good_contrast": "Guter Kontrast",
"contrast_ratio": "Kontrastverhältnis",
"download_svg": "SVG herunterladen",
"download_png": "PNG herunterladen",
"save_qr_code": "QR-Code speichern"
},
"analytics": {
"title": "Analytik",
"subtitle": "Verfolgen und analysieren Sie die Performance Ihrer QR-Codes",
"export_report": "Bericht exportieren",
"from_last_period": "vom letzten Zeitraum",
"no_mobile_scans": "Keine mobilen Scans",
"of_total": "der Gesamtmenge",
"ranges": {
"7d": "7 Tage",
"30d": "30 Tage",
"90d": "90 Tage"
},
"kpis": {
"total_scans": "Gesamte Scans",
"avg_scans": "Ø Scans/QR",
"mobile_usage": "Mobile Nutzung",
"top_country": "Top Land"
},
"charts": {
"scans_over_time": "Scans über Zeit",
"device_types": "Gerätetypen",
"top_countries": "Top Länder"
},
"table": {
"qr_code": "QR-Code",
"type": "Typ",
"total_scans": "Gesamte Scans",
"unique_scans": "Einzigartige Scans",
"conversion": "Konversion",
"trend": "Trend",
"scans": "Scans",
"percentage": "Prozent",
"country": "Land",
"performance": "Performance",
"created": "Erstellt",
"status": "Status"
},
"performance_title": "QR-Code-Performance"
},
"bulk": {
"title": "Massen-Erstellung",
"subtitle": "Erstellen Sie mehrere QR-Codes gleichzeitig aus CSV- oder Excel-Dateien",
"template_warning_title": "Bitte folgen Sie dem Vorlagenformat",
"template_warning_text": "Laden Sie die Vorlage unten herunter und folgen Sie dem Format genau. Ihre CSV muss Spalten für Titel und Inhalt (URL) enthalten.",
"static_only_title": "Nur statische QR-Codes",
"static_only_text": "Massen-Erstellung generiert statische QR-Codes, die nach der Erstellung nicht bearbeitet werden können. Diese QR-Codes beinhalten kein Tracking oder Analytik. Perfekt für Druckmaterialien und Offline-Nutzung.",
"download_template": "Vorlage herunterladen",
"no_file_selected": "Keine ausgewählt",
"simple_format": "Einfaches Format",
"just_title_url": "Nur Titel & URL",
"static_qr_codes": "Statische QR-Codes",
"no_tracking": "Kein Tracking enthalten",
"instant_download": "Sofortiger Download",
"get_zip": "ZIP mit allen SVGs erhalten",
"max_rows": "max 1.000 Zeilen",
"steps": {
"upload": "Datei hochladen",
"preview": "Vorschau & Zuordnung",
"download": "Herunterladen"
},
"drag_drop": "Datei hier hinziehen",
"or_click": "oder klicken zum Durchsuchen",
"supported_formats": "Unterstützt CSV, XLS, XLSX (max 1.000 Zeilen)"
},
"integrations": {
"title": "Integrationen",
"metrics": {
"total_codes": "QR-Codes Gesamt",
"active_integrations": "Aktive Integrationen",
"sync_status": "Sync-Status",
"available_services": "Verfügbare Services"
},
"zapier": {
"title": "Zapier",
"description": "Automatisieren Sie QR-Code-Erstellung mit 5000+ Apps",
"features": [
"Trigger bei neuen QR-Codes",
"Codes aus anderen Apps erstellen",
"Scan-Daten synchronisieren"
]
},
"airtable": {
"title": "Airtable",
"description": "Synchronisieren Sie QR-Codes mit Ihren Airtable-Basen",
"features": [
"Bidirektionale Synchronisation",
"Individuelle Feldzuordnung",
"Echtzeit-Updates"
]
},
"sheets": {
"title": "Google Sheets",
"description": "Exportieren Sie Daten automatisch zu Google Sheets",
"features": [
"Automatisierte Exporte",
"Individuelle Vorlagen",
"Geplante Updates"
]
},
"activate": "Aktivieren & Konfigurieren"
},
"settings": {
"title": "Einstellungen",
"subtitle": "Verwalten Sie Ihre Kontoeinstellungen und Präferenzen",
"tabs": {
"profile": "Profil",
"billing": "Abrechnung",
"team": "Team & Rollen",
"api": "API-Schlüssel",
"workspace": "Arbeitsbereich"
}
},
"common": {
"save": "Speichern",
"cancel": "Abbrechen",
"delete": "Löschen",
"edit": "Bearbeiten",
"create": "Erstellen",
"loading": "Lädt...",
"error": "Ein Fehler ist aufgetreten",
"success": "Erfolgreich!"
},
"footer": {
"product": "Produkt",
"features": "Funktionen",
"pricing": "Preise",
"faq": "FAQ",
"blog": "Blog",
"resources": "Ressourcen",
"full_pricing": "Alle Preise",
"all_questions": "Alle Fragen",
"all_articles": "Alle Artikel",
"learn": "Lernen",
"hero": {
"badge": "Kostenloser QR-Code-Generator",
"title": "Erstellen Sie QR-Codes, die überall funktionieren",
"subtitle": "Generieren Sie statische und dynamische QR-Codes mit Tracking, individuellem Branding und Massen-Erstellung. Kostenlos für immer.",
"features": [
"Keine Kreditkarte zum Starten erforderlich",
"QR-Codes für immer kostenlos erstellen",
"Erweiterte Verfolgung und Analytik",
"Individuelle Farben und Stile"
],
"cta_primary": "QR-Code kostenlos erstellen",
"cta_secondary": "Preise ansehen",
"engagement_badge": "Kostenlos für immer",
"get_started": "Loslegen",
"view_full_pricing": "Alle Preisdetails ansehen →"
},
"trust": {
"users": "Aktive Nutzer",
"codes": "QR-Codes erstellt",
"scans": "Scans verfolgt",
"countries": "Länder"
},
"industries": {
"restaurant": "Restaurant-Kette",
"tech": "Tech-Startup",
"realestate": "Immobilien",
"events": "Event-Agentur",
"retail": "Einzelhandel",
"healthcare": "Gesundheitswesen"
},
"templates": {
"title": "Mit einer Vorlage beginnen",
"restaurant": "Restaurant-Menü",
"business": "Visitenkarte",
"vcard": "Kontaktkarte",
"event": "Event-Ticket",
"use_template": "Vorlage verwenden →"
},
"generator": {
"title": "Sofortiger QR-Code-Generator",
"url_placeholder": "Geben Sie hier Ihre URL ein...",
"foreground": "Vordergrund",
"background": "Hintergrund",
"corners": "Ecken",
"size": "Größe",
"contrast_good": "Guter Kontrast",
"download_svg": "SVG herunterladen",
"download_png": "PNG herunterladen",
"save_track": "Speichern & Verfolgen",
"live_preview": "Live-Vorschau",
"demo_note": "Dies ist ein Demo-QR-Code"
},
"static_vs_dynamic": {
"title": "Warum dynamische QR-Codes Ihnen Geld sparen",
"description": "Hören Sie auf, Materialien neu zu drucken. Wechseln Sie Ziele sofort und verfolgen Sie jeden Scan.",
"static": {
"title": "Statische QR-Codes",
"subtitle": "Immer kostenlos",
"description": "Perfekt für permanente Inhalte, die sich nie ändern",
"features": [
"Inhalt kann nicht bearbeitet werden",
"Keine Scan-Verfolgung",
"Funktioniert für immer",
"Kein Konto erforderlich"
]
},
"dynamic": {
"title": "Dynamische QR-Codes",
"subtitle": "Empfohlen",
"description": "Volle Kontrolle mit Tracking- und Bearbeitungsfunktionen",
"features": [
"Inhalt jederzeit bearbeiten",
"Erweiterte Analytik",
"Individuelles Branding",
"Bulk-Operationen"
]
}
},
"features": {
"title": "Alles was Sie brauchen, um professionelle QR-Codes zu erstellen",
"analytics": {
"title": "Erweiterte Analytik",
"description": "Verfolgen Sie Scans, Standorte, Geräte und Nutzerverhalten mit detaillierten Einblicken."
},
"customization": {
"title": "Vollständige Anpassung",
"description": "Branden Sie Ihre QR-Codes mit individuellen Farben, Logos und Styling-Optionen."
},
"unlimited": {
"title": "Unbegrenzte statische QR-Codes",
"description": "Erstellen Sie so viele statische QR-Codes wie Sie benötigen. Kostenlos für immer, ohne Limits."
},
"bulk": {
"title": "Bulk-Operationen",
"description": "Erstellen Sie hunderte von QR-Codes auf einmal mit CSV-Import und Batch-Verarbeitung."
},
"integrations": {
"title": "Integrationen",
"description": "Verbinden Sie sich mit Zapier, Airtable, Google Sheets und weiteren beliebten Tools."
},
"api": {
"title": "Entwickler-API",
"description": "Integrieren Sie QR-Code-Generierung in Ihre Anwendungen mit unserer REST-API."
},
"support": {
"title": "24/7 Support",
"description": "Erhalten Sie Hilfe, wenn Sie sie brauchen, mit unserem dedizierten Kundensupport-Team."
}
},
"pricing": {
"title": "Wählen Sie Ihren Plan",
"subtitle": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
"choose_plan": "Wählen Sie Ihren Plan",
"select_plan": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
"current_plan": "Aktueller Plan",
"upgrade_to": "Upgrade auf",
"downgrade_to_free": "Zu Kostenlos zurückstufen",
"most_popular": "Beliebteste",
"all_plans_note": "Alle Pläne beinhalten unbegrenzte statische QR-Codes und Basis-Anpassung.",
"free": {
"title": "Kostenlos",
"name": "Free",
"price": "€0",
"period": "für immer",
"features": [
"3 aktive dynamische QR-Codes (8 Typen verfügbar)",
"Unbegrenzte statische QR-Codes",
"Basis-Scan-Tracking",
"Standard QR-Design-Vorlagen"
]
},
"pro": {
"title": "Pro",
"name": "Pro",
"price": "€9",
"period": "pro Monat",
"badge": "Beliebteste",
"features": [
"50 dynamische QR-Codes",
"Unbegrenzte statische QR-Codes",
"Erweiterte Analytik (Scans, Geräte, Standorte)",
"Individuelles Branding (Farben)",
"Download als SVG/PNG"
]
},
"business": {
"title": "Business",
"name": "Business",
"price": "€29",
"period": "pro Monat",
"features": [
"500 dynamische QR-Codes",
"Unbegrenzte statische QR-Codes",
"Alles aus Pro",
"Massen-QR-Erstellung (bis zu 1.000)",
"Prioritäts-E-Mail-Support",
"Erweiterte Tracking & Insights"
]
},
"enterprise": {
"title": "Enterprise",
"name": "Enterprise",
"price": "Individuell",
"period": "",
"features": [
"∞ dynamische QR-Codes",
"Unbegrenzte statische QR-Codes",
"Alles aus Business",
"Eigener Account Manager"
],
"contact": "Kontakt aufnehmen"
}
},
"faq": {
"title": "Häufig gestellte Fragen",
"questions": {
"account": {
"question": "Benötige ich ein Konto, um QR-Codes zu erstellen?",
"answer": "Für statische QR-Codes ist kein Konto erforderlich. Dynamische QR-Codes mit Tracking- und Bearbeitungsfunktionen erfordern jedoch ein kostenloses Konto."
},
"static_vs_dynamic": {
"question": "Was ist der Unterschied zwischen statischen und dynamischen QR-Codes?",
"answer": "Statische QR-Codes enthalten feste Inhalte, die nicht geändert werden können. Dynamische QR-Codes können jederzeit bearbeitet werden und bieten detaillierte Analytik."
},
"forever": {
"question": "Funktionieren meine QR-Codes für immer?",
"answer": "Statische QR-Codes funktionieren für immer, da der Inhalt direkt eingebettet ist. Dynamische QR-Codes funktionieren, solange Ihr Konto aktiv ist."
},
"file_type": {
"question": "Welchen Dateityp sollte ich zum Drucken verwenden?",
"answer": "Für Druckmaterialien empfehlen wir das SVG-Format für Skalierbarkeit oder hochauflösendes PNG (300+ DPI) für beste Qualität."
},
"password": {
"question": "Kann ich einen QR-Code mit einem Passwort schützen?",
"answer": "Ja, Pro- und Business-Pläne beinhalten Passwortschutz und Zugriffskontrollfunktionen für Ihre QR-Codes."
},
"analytics": {
"question": "Wie funktioniert die Analytik?",
"answer": "Wir verfolgen Scans, Standorte, Geräte und Referrer unter Beachtung der Privatsphäre der Nutzer. Keine persönlichen Daten werden gespeichert."
},
"privacy": {
"question": "Verfolgen Sie persönliche Daten?",
"answer": "Wir respektieren die Privatsphäre und sammeln nur anonyme Nutzungsdaten. IP-Adressen werden gehasht und wir respektieren Do-Not-Track-Header."
},
"bulk": {
"question": "Kann ich Codes in großen Mengen mit meinen eigenen Daten erstellen?",
"answer": "Ja, Sie können CSV- oder Excel-Dateien hochladen, um mehrere QR-Codes auf einmal mit individueller Datenzuordnung zu erstellen."
}
}
},
"dashboard": {
"title": "Dashboard",
"subtitle": "Verwalten Sie Ihre QR-Codes und verfolgen Sie Ihre Performance",
"stats": {
"total_scans": "Gesamte Scans",
"active_codes": "Aktive QR-Codes",
"conversion_rate": "Konversionsrate"
},
"recent_codes": "Aktuelle QR-Codes",
"blog_resources": "Blog & Ressourcen",
"menu": {
"edit": "Bearbeiten",
"duplicate": "Duplizieren",
"pause": "Pausieren",
"delete": "Löschen"
}
},
"create": {
"title": "QR-Code erstellen",
"subtitle": "Generieren Sie dynamische und statische QR-Codes mit individuellem Branding",
"content": "Inhalt",
"type": "QR-Code-Typ",
"style": "Stil & Branding",
"preview": "Live-Vorschau",
"title_label": "Titel",
"title_placeholder": "Mein QR-Code",
"content_type": "Inhaltstyp",
"url_label": "URL",
"url_placeholder": "https://beispiel.de",
"tags_label": "Tags (durch Komma getrennt)",
"tags_placeholder": "marketing, kampagne, 2025",
"qr_code_type": "QR-Code-Typ",
"dynamic": "Dynamisch",
"static": "Statisch",
"recommended": "Empfohlen",
"dynamic_description": "Dynamisch: Scans verfolgen, URL später bearbeiten, Analytik ansehen. QR enthält Tracking-Link.",
"static_description": "Statisch: Direkt zum Inhalt, kein Tracking, nicht bearbeitbar. QR enthält tatsächlichen Inhalt.",
"foreground_color": "Vordergrundfarbe",
"background_color": "Hintergrundfarbe",
"corner_style": "Eckenstil",
"size": "Größe",
"good_contrast": "Guter Kontrast",
"contrast_ratio": "Kontrastverhältnis",
"download_svg": "SVG herunterladen",
"download_png": "PNG herunterladen",
"save_qr_code": "QR-Code speichern"
},
"analytics": {
"title": "Analytik",
"subtitle": "Verfolgen und analysieren Sie die Performance Ihrer QR-Codes",
"export_report": "Bericht exportieren",
"from_last_period": "vom letzten Zeitraum",
"no_mobile_scans": "Keine mobilen Scans",
"of_total": "der Gesamtmenge",
"ranges": {
"7d": "7 Tage",
"30d": "30 Tage",
"90d": "90 Tage"
},
"kpis": {
"total_scans": "Gesamte Scans",
"avg_scans": "Ø Scans/QR",
"mobile_usage": "Mobile Nutzung",
"top_country": "Top Land"
},
"charts": {
"scans_over_time": "Scans über Zeit",
"device_types": "Gerätetypen",
"top_countries": "Top Länder"
},
"table": {
"qr_code": "QR-Code",
"type": "Typ",
"total_scans": "Gesamte Scans",
"unique_scans": "Einzigartige Scans",
"conversion": "Konversion",
"trend": "Trend",
"scans": "Scans",
"percentage": "Prozent",
"country": "Land",
"performance": "Performance",
"created": "Erstellt",
"status": "Status"
},
"performance_title": "QR-Code-Performance"
},
"bulk": {
"title": "Massen-Erstellung",
"subtitle": "Erstellen Sie mehrere QR-Codes gleichzeitig aus CSV- oder Excel-Dateien",
"template_warning_title": "Bitte folgen Sie dem Vorlagenformat",
"template_warning_text": "Laden Sie die Vorlage unten herunter und folgen Sie dem Format genau. Ihre CSV muss Spalten für Titel und Inhalt (URL) enthalten.",
"static_only_title": "Nur statische QR-Codes",
"static_only_text": "Massen-Erstellung generiert statische QR-Codes, die nach der Erstellung nicht bearbeitet werden können. Diese QR-Codes beinhalten kein Tracking oder Analytik. Perfekt für Druckmaterialien und Offline-Nutzung.",
"download_template": "Vorlage herunterladen",
"no_file_selected": "Keine ausgewählt",
"simple_format": "Einfaches Format",
"just_title_url": "Nur Titel & URL",
"static_qr_codes": "Statische QR-Codes",
"no_tracking": "Kein Tracking enthalten",
"instant_download": "Sofortiger Download",
"get_zip": "ZIP mit allen SVGs erhalten",
"max_rows": "max 1.000 Zeilen",
"steps": {
"upload": "Datei hochladen",
"preview": "Vorschau & Zuordnung",
"download": "Herunterladen"
},
"drag_drop": "Datei hier hinziehen",
"or_click": "oder klicken zum Durchsuchen",
"supported_formats": "Unterstützt CSV, XLS, XLSX (max 1.000 Zeilen)"
},
"integrations": {
"title": "Integrationen",
"metrics": {
"total_codes": "QR-Codes Gesamt",
"active_integrations": "Aktive Integrationen",
"sync_status": "Sync-Status",
"available_services": "Verfügbare Services"
},
"zapier": {
"title": "Zapier",
"description": "Automatisieren Sie QR-Code-Erstellung mit 5000+ Apps",
"features": [
"Trigger bei neuen QR-Codes",
"Codes aus anderen Apps erstellen",
"Scan-Daten synchronisieren"
]
},
"airtable": {
"title": "Airtable",
"description": "Synchronisieren Sie QR-Codes mit Ihren Airtable-Basen",
"features": [
"Bidirektionale Synchronisation",
"Individuelle Feldzuordnung",
"Echtzeit-Updates"
]
},
"sheets": {
"title": "Google Sheets",
"description": "Exportieren Sie Daten automatisch zu Google Sheets",
"features": [
"Automatisierte Exporte",
"Individuelle Vorlagen",
"Geplante Updates"
]
},
"activate": "Aktivieren & Konfigurieren"
},
"settings": {
"title": "Einstellungen",
"subtitle": "Verwalten Sie Ihre Kontoeinstellungen und Präferenzen",
"tabs": {
"profile": "Profil",
"billing": "Abrechnung",
"team": "Team & Rollen",
"api": "API-Schlüssel",
"workspace": "Arbeitsbereich"
}
},
"common": {
"save": "Speichern",
"cancel": "Abbrechen",
"delete": "Löschen",
"edit": "Bearbeiten",
"create": "Erstellen",
"loading": "Lädt...",
"error": "Ein Fehler ist aufgetreten",
"success": "Erfolgreich!"
},
"footer": {
"product": "Produkt",
"features": "Funktionen",
"pricing": "Preise",
"faq": "FAQ",
"blog": "Blog",
"resources": "Ressourcen",
"full_pricing": "Alle Preise",
"all_questions": "Alle Fragen",
"all_articles": "Alle Artikel",
"learn": "Lernen",
"get_started": "Loslegen",
"legal": "Rechtliches",
"industries": "Branchen",
"privacy_policy": "Datenschutzerklärung",
"tagline": "Erstellen Sie benutzerdefinierte QR-Codes in Sekunden mit erweitertem Tracking und Analysen.",
"newsletter": "Newsletter-Anmeldung",
"rights_reserved": "QR Master. Alle Rechte vorbehalten."
}
"privacy_policy": "Datenschutzerklärung",
"tagline": "Erstellen Sie benutzerdefinierte QR-Codes in Sekunden mit erweitertem Tracking und Analysen.",
"newsletter": "Newsletter-Anmeldung",
"rights_reserved": "QR Master. Alle Rechte vorbehalten."
}
}

View File

@@ -1,406 +1,411 @@
{
"nav": {
"features": "Features",
"pricing": "Pricing",
"faq": "FAQ",
"blog": "Blog",
"login": "Login",
"dashboard": "Dashboard",
"about": "About",
"contact": "Contact",
"signup": "Sign Up",
"learn": "Learn",
"create_qr": "Create QR",
"bulk_creation": "Bulk Creation",
"analytics": "Analytics",
"settings": "Settings",
"cta": "Get Started Free",
"tools": "Free Tools",
"all_free": "All generators are 100% free",
"resources": "Resources",
"all_industries": "Industries"
},
"hero": {
"badge": "Free QR Code Generator",
"title": "Create QR Codes That Work Everywhere",
"subtitle": "Generate static and dynamic QR codes with tracking, custom branding, and bulk generation. Free forever.",
"features": [
"No credit card required to start",
"Create QR codes free forever",
"Advanced tracking and analytics",
"Custom colors and styles"
],
"cta_primary": "Make a QR Code Free",
"cta_secondary": "View Pricing",
"engagement_badge": "Free Forever"
},
"trust": {
"users": "Happy Users",
"codes": "Active QR Codes",
"scans": "Total Scans",
"countries": "Countries"
},
"industries": {
"restaurant": "Restaurant Chain",
"tech": "Tech Startup",
"realestate": "Real Estate",
"events": "Event Agency",
"retail": "Retail Store",
"healthcare": "Healthcare"
},
"templates": {
"title": "Start with a Template",
"restaurant": "Restaurant Menu",
"business": "Business Card",
"vcard": "Contact Card",
"event": "Event Ticket",
"use_template": "Use template →"
},
"generator": {
"title": "Instant QR Code Generator",
"url_placeholder": "Enter your URL here...",
"foreground": "Foreground",
"background": "Background",
"corners": "Corners",
"size": "Size",
"contrast_good": "Good contrast",
"download_svg": "Download SVG",
"download_png": "Download PNG",
"save_track": "Save & Track",
"live_preview": "Live Preview",
"demo_note": "This is a demo QR code"
},
"static_vs_dynamic": {
"title": "Why Dynamic QR Codes Save You Money",
"description": "Stop re-printing materials. Switch destinations instantly and track every scan.",
"static": {
"title": "Static QR Codes",
"subtitle": "Always Free",
"description": "Perfect for permanent content that never changes",
"features": [
"Content cannot be edited",
"No scan tracking",
"Works forever",
"No account required"
]
},
"dynamic": {
"title": "Dynamic QR Codes",
"subtitle": "Recommended",
"description": "Full control with tracking and editing capabilities",
"features": [
"Edit content anytime",
"Advanced analytics",
"Custom branding",
"Bulk operations"
]
}
},
"features": {
"title": "Everything you need to create professional QR codes",
"analytics": {
"title": "Advanced Analytics",
"description": "Track scans, locations, devices, and user behavior with detailed insights."
},
"customization": {
"title": "Full Customization",
"description": "Brand your QR codes with custom colors and styling options."
},
"unlimited": {
"title": "Unlimited Static QR Codes",
"description": "Create as many static QR codes as you need. Free forever, no limits."
},
"bulk": {
"title": "Bulk Operations",
"description": "Create hundreds of QR codes at once with CSV import and batch processing."
},
"integrations": {
"title": "Integrations",
"description": "Connect with Zapier, Airtable, Google Sheets, and more popular tools."
},
"api": {
"title": "Developer API",
"description": "Integrate QR code generation into your applications with our REST API."
},
"support": {
"title": "24/7 Support",
"description": "Get help when you need it with our dedicated customer support team."
}
},
"pricing": {
"title": "Choose Your Plan",
"subtitle": "Select the perfect plan for your QR code needs",
"choose_plan": "Choose Your Plan",
"select_plan": "Select the perfect plan for your QR code needs",
"current_plan": "Current Plan",
"upgrade_to": "Upgrade to",
"downgrade_to_free": "Downgrade to Free",
"most_popular": "Most Popular",
"all_plans_note": "All plans include unlimited static QR codes and basic customization.",
"free": {
"title": "Free",
"name": "Free",
"price": "€0",
"period": "forever",
"features": [
"3 active dynamic QR codes (8 types available)",
"Unlimited static QR codes",
"Basic scan tracking",
"Standard QR design templates",
"Download as SVG/PNG"
]
},
"pro": {
"title": "Pro",
"name": "Pro",
"price": "€9",
"period": "per month",
"badge": "Most Popular",
"features": [
"50 dynamic QR codes",
"Unlimited static QR codes",
"Advanced analytics (scans, devices, locations)",
"Custom branding (colors & logos)"
]
},
"business": {
"title": "Business",
"name": "Business",
"price": "€29",
"period": "per month",
"features": [
"500 dynamic QR codes",
"Unlimited static QR codes",
"Everything from Pro",
"Bulk QR Creation (up to 1,000)",
"Priority email support",
"Advanced tracking & insights"
]
}
},
"faq": {
"title": "Frequently Asked Questions",
"questions": {
"account": {
"question": "Do I need an account to create QR codes?",
"answer": "No account is required for static QR codes. However, dynamic QR codes with tracking and editing capabilities require a free account."
},
"static_vs_dynamic": {
"question": "What's the difference between static and dynamic QR codes?",
"answer": "Static QR codes contain fixed content that cannot be changed. Dynamic QR codes can be edited anytime and provide detailed analytics."
},
"forever": {
"question": "Will my QR codes work forever?",
"answer": "Static QR codes work forever as the content is embedded directly. Dynamic QR codes work as long as your account is active."
},
"file_type": {
"question": "What file type should I use for printing?",
"answer": "For print materials, we recommend SVG format for scalability or high-resolution PNG (300+ DPI) for best quality."
},
"password": {
"question": "Can I password-protect a QR code?",
"answer": "Yes, Pro and Business plans include password protection and access control features for your QR codes."
},
"analytics": {
"question": "How do analytics work?",
"answer": "We track scans, locations, devices, and referrers while respecting user privacy. No personal data is stored."
},
"privacy": {
"question": "Do you track personal data?",
"answer": "We respect privacy and only collect anonymous usage data. IP addresses are hashed and we honor Do Not Track headers."
},
"bulk": {
"question": "Can I bulk-create codes with my own data?",
"answer": "Yes, you can upload CSV or Excel files to create multiple QR codes at once with custom data mapping."
}
}
},
"dashboard": {
"title": "Dashboard",
"subtitle": "Manage your QR codes and track performance",
"stats": {
"total_scans": "Total Scans",
"active_codes": "Active QR Codes",
"conversion_rate": "Conversion Rate"
},
"recent_codes": "Recent QR Codes",
"blog_resources": "Blog & Resources",
"menu": {
"edit": "Edit",
"duplicate": "Duplicate",
"pause": "Pause",
"delete": "Delete"
}
},
"create": {
"title": "Create QR Code",
"subtitle": "Generate dynamic and static QR codes with custom branding",
"content": "Content",
"type": "QR Code Type",
"style": "Style & Branding",
"preview": "Live Preview",
"title_label": "Title",
"title_placeholder": "My QR Code",
"content_type": "Content Type",
"url_label": "URL",
"url_placeholder": "https://example.com",
"tags_label": "Tags (comma-separated)",
"tags_placeholder": "marketing, campaign, 2025",
"qr_code_type": "QR Code Type",
"dynamic": "Dynamic",
"static": "Static",
"recommended": "Recommended",
"dynamic_description": "Dynamic: Track scans, edit URL later, view analytics. QR contains tracking link.",
"static_description": "Static: Direct to content, no tracking, cannot edit. QR contains actual content.",
"foreground_color": "Foreground Color",
"background_color": "Background Color",
"corner_style": "Corner Style",
"size": "Size",
"good_contrast": "Good contrast",
"contrast_ratio": "Contrast ratio",
"download_svg": "Download SVG",
"download_png": "Download PNG",
"save_qr_code": "Save QR Code"
},
"analytics": {
"title": "Analytics",
"subtitle": "Track and analyze your QR code performance",
"export_report": "Export Report",
"from_last_period": "from last period",
"no_mobile_scans": "No mobile scans",
"of_total": "of total",
"ranges": {
"7d": "7 Days",
"30d": "30 Days",
"90d": "90 Days"
},
"kpis": {
"total_scans": "Total Scans",
"avg_scans": "Avg Scans/QR",
"mobile_usage": "Mobile Usage",
"top_country": "Top Country"
},
"charts": {
"scans_over_time": "Scans Over Time",
"device_types": "Device Types",
"top_countries": "Top Countries"
},
"table": {
"qr_code": "QR Code",
"type": "Type",
"total_scans": "Total Scans",
"unique_scans": "Unique Scans",
"conversion": "Conversion",
"trend": "Trend",
"scans": "Scans",
"percentage": "Percentage",
"country": "Country",
"performance": "Performance",
"created": "Created",
"status": "Status"
},
"performance_title": "QR Code Performance"
},
"bulk": {
"title": "Bulk Creation",
"subtitle": "Create multiple QR codes at once from CSV or Excel files",
"template_warning_title": "Please Follow the Template Format",
"template_warning_text": "Download the template below and follow the format exactly. Your CSV must include columns for title and content (URL).",
"static_only_title": "Static QR Codes Only",
"static_only_text": "Bulk creation generates static QR codes that cannot be edited after creation. These QR codes do not include tracking or analytics. Perfect for print materials and offline use.",
"download_template": "Download Template",
"no_file_selected": "No file selected",
"simple_format": "Simple Format",
"just_title_url": "Just title & URL",
"static_qr_codes": "Static QR Codes",
"no_tracking": "No tracking included",
"instant_download": "Instant Download",
"get_zip": "Get ZIP with all SVGs",
"max_rows": "max 1,000 rows",
"steps": {
"upload": "Upload File",
"preview": "Preview & Map",
"download": "Download"
},
"drag_drop": "Drag & drop your file here",
"or_click": "or click to browse",
"supported_formats": "Supports CSV, XLS, XLSX (max 1,000 rows)"
},
"integrations": {
"title": "Integrations",
"metrics": {
"total_codes": "QR Codes Total",
"active_integrations": "Active Integrations",
"sync_status": "Sync Status",
"available_services": "Available Services"
},
"zapier": {
"title": "Zapier",
"description": "Automate QR code creation with 5000+ apps",
"features": [
"Trigger on new QR codes",
"Create codes from other apps",
"Sync scan data"
]
},
"airtable": {
"title": "Airtable",
"description": "Sync QR codes with your Airtable bases",
"features": [
"Two-way sync",
"Custom field mapping",
"Real-time updates"
]
},
"sheets": {
"title": "Google Sheets",
"description": "Export data to Google Sheets automatically",
"features": [
"Automated exports",
"Custom templates",
"Scheduled updates"
]
},
"activate": "Activate & Configure"
},
"settings": {
"title": "Settings",
"subtitle": "Manage your account settings and preferences",
"tabs": {
"profile": "Profile",
"billing": "Billing",
"team": "Team & Roles",
"api": "API Keys",
"workspace": "Workspace"
}
},
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"create": "Create",
"loading": "Loading...",
"error": "An error occurred",
"success": "Success!"
},
"footer": {
"product": "Product",
"features": "Features",
"pricing": "Pricing",
"faq": "FAQ",
"blog": "Blog",
"resources": "Resources",
"full_pricing": "Full Pricing",
"all_questions": "All Questions",
"all_articles": "All Articles",
"learn": "Learn",
"get_started": "Get Started",
"legal": "Legal",
"industries": "Industries",
"privacy_policy": "Privacy Policy",
"tagline": "Create custom QR codes in seconds with advanced tracking and analytics.",
"newsletter": "Newsletter signup",
"rights_reserved": "QR Master. All rights reserved."
}
}
{
"nav": {
"features": "Features",
"pricing": "Pricing",
"faq": "FAQ",
"blog": "Blog",
"login": "Login",
"dashboard": "Dashboard",
"about": "About",
"contact": "Contact",
"signup": "Sign Up",
"learn": "Learn",
"create_qr": "Create QR",
"bulk_creation": "Bulk Creation",
"analytics": "Analytics",
"settings": "Settings",
"cta": "Get Started Free",
"tools": "Free Tools",
"all_free": "All generators are 100% free",
"resources": "Resources",
"all_industries": "Industries"
},
"hero": {
"badge": "Free QR Code Generator",
"title": "Create QR Codes That Work Everywhere",
"subtitle": "Generate static and dynamic QR codes with tracking, custom branding, and bulk generation. Free forever.",
"features": [
"No credit card required to start",
"Create QR codes free forever",
"Advanced tracking and analytics",
"Custom colors and styles"
],
"cta_primary": "Make a QR Code Free",
"cta_secondary": "View Pricing",
"engagement_badge": "Free Forever"
},
"trust": {
"users": "Happy Users",
"codes": "Active QR Codes",
"scans": "Total Scans",
"countries": "Countries"
},
"industries": {
"restaurant": "Restaurant Chain",
"tech": "Tech Startup",
"realestate": "Real Estate",
"events": "Event Agency",
"retail": "Retail Store",
"healthcare": "Healthcare"
},
"templates": {
"title": "Start with a Template",
"restaurant": "Restaurant Menu",
"business": "Business Card",
"vcard": "Contact Card",
"event": "Event Ticket",
"use_template": "Use template →"
},
"generator": {
"title": "Instant QR Code Generator",
"url_placeholder": "Enter your URL here...",
"foreground": "Foreground",
"background": "Background",
"corners": "Corners",
"size": "Size",
"contrast_good": "Good contrast",
"download_svg": "Download SVG",
"download_png": "Download PNG",
"save_track": "Save & Track",
"live_preview": "Live Preview",
"demo_note": "This is a demo QR code"
},
"static_vs_dynamic": {
"title": "Why Dynamic QR Codes Save You Money",
"description": "Stop re-printing materials. Switch destinations instantly and track every scan.",
"static": {
"title": "Static QR Codes",
"subtitle": "Always Free",
"description": "Perfect for permanent content that never changes",
"features": [
"Content cannot be edited",
"No scan tracking",
"Works forever",
"No account required"
]
},
"dynamic": {
"title": "Dynamic QR Codes",
"subtitle": "Recommended",
"description": "Full control with tracking and editing capabilities",
"features": [
"Edit content anytime",
"Advanced analytics",
"Custom branding",
"Bulk operations"
]
}
},
"features": {
"title": "Everything you need to create professional QR codes",
"analytics": {
"title": "Advanced Analytics",
"description": "Track scans, locations, devices, and user behavior with detailed insights."
},
"customization": {
"title": "Full Customization",
"description": "Brand your QR codes with custom colors and styling options."
},
"unlimited": {
"title": "Unlimited Static QR Codes",
"description": "Create as many static QR codes as you need. Free forever, no limits."
},
"bulk": {
"title": "Bulk Operations",
"description": "Create hundreds of QR codes at once with CSV import and batch processing."
},
"integrations": {
"title": "Integrations",
"description": "Connect with Zapier, Airtable, Google Sheets, and more popular tools."
},
"api": {
"title": "Developer API",
"description": "Integrate QR code generation into your applications with our REST API."
},
"support": {
"title": "24/7 Support",
"description": "Get help when you need it with our dedicated customer support team."
}
},
"pricing": {
"title": "Choose Your Plan",
"subtitle": "Select the perfect plan for your QR code needs",
"choose_plan": "Choose Your Plan",
"select_plan": "Select the perfect plan for your QR code needs",
"current_plan": "Current Plan",
"upgrade_to": "Upgrade to",
"downgrade_to_free": "Downgrade to Free",
"most_popular": "Most Popular",
"all_plans_note": "All plans include unlimited static QR codes and basic customization.",
"free": {
"title": "Free",
"name": "Free",
"price": "€0",
"period": "forever",
"features": [
"3 active dynamic QR codes (8 types available)",
"Unlimited static QR codes",
"Basic scan tracking",
"Standard QR design templates",
"Download as SVG/PNG"
]
},
"pro": {
"title": "Pro",
"name": "Pro",
"price": "€9",
"period": "per month",
"badge": "Most Popular",
"features": [
"50 dynamic QR codes",
"Unlimited static QR codes",
"Advanced analytics (scans, devices, locations)",
"Custom branding (colors & logos)"
]
},
"business": {
"title": "Business",
"name": "Business",
"price": "€29",
"period": "per month",
"features": [
"500 dynamic QR codes",
"Unlimited static QR codes",
"Everything from Pro",
"Bulk QR Creation (up to 1,000)",
"Priority email support",
"Advanced tracking & insights"
]
},
"enterprise": {
"title": "Enterprise",
"name": "Enterprise",
"price": "Custom",
"period": "",
"features": [
"∞ dynamic QR codes",
"Unlimited static QR codes",
"Everything from Business",
"Dedicated Account Manager"
],
"contact": "Contact Us"
}
},
"faq": {
"title": "Frequently Asked Questions",
"questions": {
"account": {
"question": "Do I need an account to create QR codes?",
"answer": "No account is required for static QR codes. However, dynamic QR codes with tracking and editing capabilities require a free account."
},
"static_vs_dynamic": {
"question": "What's the difference between static and dynamic QR codes?",
"answer": "Static QR codes contain fixed content that cannot be changed. Dynamic QR codes can be edited anytime and provide detailed analytics."
},
"forever": {
"question": "Will my QR codes work forever?",
"answer": "Static QR codes work forever as the content is embedded directly. Dynamic QR codes work as long as your account is active."
},
"file_type": {
"question": "What file type should I use for printing?",
"answer": "For print materials, we recommend SVG format for scalability or high-resolution PNG (300+ DPI) for best quality."
},
"password": {
"question": "Can I password-protect a QR code?",
"answer": "Yes, Pro and Business plans include password protection and access control features for your QR codes."
},
"analytics": {
"question": "How do analytics work?",
"answer": "We track scans, locations, devices, and referrers while respecting user privacy. No personal data is stored."
},
"privacy": {
"question": "Do you track personal data?",
"answer": "We respect privacy and only collect anonymous usage data. IP addresses are hashed and we honor Do Not Track headers."
},
"bulk": {
"question": "Can I bulk-create codes with my own data?",
"answer": "Yes, you can upload CSV or Excel files to create multiple QR codes at once with custom data mapping."
}
}
},
"dashboard": {
"title": "Dashboard",
"subtitle": "Manage your QR codes and track performance",
"stats": {
"total_scans": "Total Scans",
"active_codes": "Active QR Codes",
"conversion_rate": "Conversion Rate"
},
"recent_codes": "Recent QR Codes",
"blog_resources": "Blog & Resources",
"menu": {
"edit": "Edit",
"duplicate": "Duplicate",
"pause": "Pause",
"delete": "Delete"
}
},
"create": {
"title": "Create QR Code",
"subtitle": "Generate dynamic and static QR codes with custom branding",
"content": "Content",
"type": "QR Code Type",
"style": "Style & Branding",
"preview": "Live Preview",
"title_label": "Title",
"title_placeholder": "My QR Code",
"content_type": "Content Type",
"url_label": "URL",
"url_placeholder": "https://example.com",
"tags_label": "Tags (comma-separated)",
"tags_placeholder": "marketing, campaign, 2025",
"qr_code_type": "QR Code Type",
"dynamic": "Dynamic",
"static": "Static",
"recommended": "Recommended",
"dynamic_description": "Dynamic: Track scans, edit URL later, view analytics. QR contains tracking link.",
"static_description": "Static: Direct to content, no tracking, cannot edit. QR contains actual content.",
"foreground_color": "Foreground Color",
"background_color": "Background Color",
"corner_style": "Corner Style",
"size": "Size",
"good_contrast": "Good contrast",
"contrast_ratio": "Contrast ratio",
"download_svg": "Download SVG",
"download_png": "Download PNG",
"save_qr_code": "Save QR Code"
},
"analytics": {
"title": "Analytics",
"subtitle": "Track and analyze your QR code performance",
"export_report": "Export Report",
"from_last_period": "from last period",
"no_mobile_scans": "No mobile scans",
"of_total": "of total",
"ranges": {
"7d": "7 Days",
"30d": "30 Days",
"90d": "90 Days"
},
"kpis": {
"total_scans": "Total Scans",
"avg_scans": "Avg Scans/QR",
"mobile_usage": "Mobile Usage",
"top_country": "Top Country"
},
"charts": {
"scans_over_time": "Scans Over Time",
"device_types": "Device Types",
"top_countries": "Top Countries"
},
"table": {
"qr_code": "QR Code",
"type": "Type",
"total_scans": "Total Scans",
"unique_scans": "Unique Scans",
"conversion": "Conversion",
"trend": "Trend",
"scans": "Scans",
"percentage": "Percentage",
"country": "Country",
"performance": "Performance",
"created": "Created",
"status": "Status"
},
"performance_title": "QR Code Performance"
},
"bulk": {
"title": "Bulk Creation",
"subtitle": "Create multiple QR codes at once from CSV or Excel files",
"template_warning_title": "Please Follow the Template Format",
"template_warning_text": "Download the template below and follow the format exactly. Your CSV must include columns for title and content (URL).",
"static_only_title": "Static QR Codes Only",
"static_only_text": "Bulk creation generates static QR codes that cannot be edited after creation. These QR codes do not include tracking or analytics. Perfect for print materials and offline use.",
"download_template": "Download Template",
"no_file_selected": "No file selected",
"simple_format": "Simple Format",
"just_title_url": "Just title & URL",
"static_qr_codes": "Static QR Codes",
"no_tracking": "No tracking included",
"instant_download": "Instant Download",
"get_zip": "Get ZIP with all SVGs",
"max_rows": "max 1,000 rows",
"steps": {
"upload": "Upload File",
"preview": "Preview & Map",
"download": "Download"
},
"drag_drop": "Drag & drop your file here",
"or_click": "or click to browse",
"supported_formats": "Supports CSV, XLS, XLSX (max 1,000 rows)"
},
"integrations": {
"title": "Integrations",
"metrics": {
"total_codes": "QR Codes Total",
"active_integrations": "Active Integrations",
"sync_status": "Sync Status",
"available_services": "Available Services"
},
"zapier": {
"title": "Zapier",
"description": "Automate QR code creation with 5000+ apps",
"features": [
"Trigger on new QR codes",
"Create codes from other apps",
"Sync scan data"
]
},
"airtable": {
"title": "Airtable",
"description": "Sync QR codes with your Airtable bases",
"features": ["Two-way sync", "Custom field mapping", "Real-time updates"]
},
"sheets": {
"title": "Google Sheets",
"description": "Export data to Google Sheets automatically",
"features": ["Automated exports", "Custom templates", "Scheduled updates"]
},
"activate": "Activate & Configure"
},
"settings": {
"title": "Settings",
"subtitle": "Manage your account settings and preferences",
"tabs": {
"profile": "Profile",
"billing": "Billing",
"team": "Team & Roles",
"api": "API Keys",
"workspace": "Workspace"
}
},
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"create": "Create",
"loading": "Loading...",
"error": "An error occurred",
"success": "Success!"
},
"footer": {
"product": "Product",
"features": "Features",
"pricing": "Pricing",
"faq": "FAQ",
"blog": "Blog",
"resources": "Resources",
"full_pricing": "Full Pricing",
"all_questions": "All Questions",
"all_articles": "All Articles",
"learn": "Learn",
"get_started": "Get Started",
"legal": "Legal",
"industries": "Industries",
"privacy_policy": "Privacy Policy",
"tagline": "Create custom QR codes in seconds with advanced tracking and analytics.",
"newsletter": "Newsletter signup",
"rights_reserved": "QR Master. All rights reserved."
}
}

View File

@@ -33,6 +33,7 @@ export function middleware(req: NextRequest) {
// '/guide', // Redirected to /learn/*
'/qr-code-erstellen',
'/dynamic-qr-code-generator',
'/dynamic-barcode-generator',
'/bulk-qr-code-generator',
'/qr-code-tracking',
'/qr-code-analytics',