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 APP
COUPON COUPON
FEEDBACK FEEDBACK
BARCODE
} }
enum QRStatus { enum QRStatus {

View File

@@ -22,8 +22,10 @@ interface BulkQRData {
interface GeneratedQR { interface GeneratedQR {
title: string; title: string;
content: string; // Original URL content: string;
svg: string; // SVG markup svg: string;
slug?: string;
redirectUrl?: string;
} }
export default function BulkCreationPage() { export default function BulkCreationPage() {
@@ -35,16 +37,25 @@ export default function BulkCreationPage() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [generatedQRs, setGeneratedQRs] = useState<GeneratedQR[]>([]); const [generatedQRs, setGeneratedQRs] = useState<GeneratedQR[]>([]);
const [userPlan, setUserPlan] = useState<string>('FREE'); 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(() => { React.useEffect(() => {
const checkPlan = async () => { const checkPlan = async () => {
try { try {
const response = await fetch('/api/user/plan'); const [planRes, statsRes] = await Promise.all([
if (response.ok) { fetch('/api/user/plan'),
const data = await response.json(); fetch('/api/user/stats'),
]);
if (planRes.ok) {
const data = await planRes.json();
setUserPlan(data.plan || 'FREE'); setUserPlan(data.plan || 'FREE');
} }
if (statsRes.ok) {
const stats = await statsRes.json();
setRemainingDynamic((stats.dynamicLimit || 0) - (stats.dynamicUsed || 0));
}
} catch (error) { } catch (error) {
console.error('Error checking plan:', 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 downloadAllQRCodes = async () => {
const zip = new JSZip(); const zip = new JSZip();
@@ -204,6 +267,18 @@ export default function BulkCreationPage() {
zip.file(fileName, qr.svg); 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' }); const blob = await zip.generateAsync({ type: 'blob' });
saveAs(blob, 'qr-codes-bulk.zip'); saveAs(blob, 'qr-codes-bulk.zip');
showToast('Download started!', 'success'); showToast('Download started!', 'success');
@@ -274,8 +349,8 @@ export default function BulkCreationPage() {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}; };
// Show upgrade prompt if not Business plan // Show upgrade prompt if not Business or Enterprise plan
if (userPlan !== 'BUSINESS') { if (userPlan !== 'BUSINESS' && userPlan !== 'ENTERPRISE') {
return ( return (
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<Card className="mt-12"> <Card className="mt-12">
@@ -309,6 +384,39 @@ export default function BulkCreationPage() {
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">{t('bulk.title')}</h1> <h1 className="text-3xl font-bold text-gray-900">{t('bulk.title')}</h1>
<p className="text-gray-600 mt-2">{t('bulk.subtitle')}</p> <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> </div>
{/* Template Warning Banner */} {/* Template Warning Banner */}
@@ -641,8 +749,13 @@ export default function BulkCreationPage() {
<Button variant="outline" onClick={() => setStep('upload')}> <Button variant="outline" onClick={() => setStep('upload')}>
Back Back
</Button> </Button>
<Button onClick={generateStaticQRCodes} loading={loading}> <Button
Generate {data.length} Static QR Codes onClick={isDynamic ? generateDynamicQRCodes : generateStaticQRCodes}
loading={loading}
>
{isDynamic
? `Generate ${Math.min(data.length, remainingDynamic)} Dynamic QR Codes`
: `Generate ${data.length} Static QR Codes`}
</Button> </Button>
</div> </div>
</CardContent> </CardContent>

View File

@@ -15,8 +15,9 @@ import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf'; import { useCsrf } from '@/hooks/useCsrf';
import { showToast } from '@/components/ui/Toast'; import { showToast } from '@/components/ui/Toast';
import { 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'; } from 'lucide-react';
import Barcode from 'react-barcode';
// Tooltip component for form field help // Tooltip component for form field help
const Tooltip = ({ text }: { text: string }) => ( const Tooltip = ({ text }: { text: string }) => (
@@ -140,6 +141,7 @@ export default function CreatePage() {
{ value: 'APP', label: 'App Download', icon: Smartphone }, { value: 'APP', label: 'App Download', icon: Smartphone },
{ value: 'COUPON', label: 'Coupon / Discount', icon: Ticket }, { value: 'COUPON', label: 'Coupon / Discount', icon: Ticket },
{ value: 'FEEDBACK', label: 'Feedback / Review', icon: Star }, { value: 'FEEDBACK', label: 'Feedback / Review', icon: Star },
{ value: 'BARCODE', label: 'Barcode', icon: BarcodeIcon },
]; ];
// Get QR content based on content type // Get QR content based on content type
@@ -170,6 +172,8 @@ export default function CreatePage() {
return `Coupon: ${content.code || 'SAVE20'} - ${content.discount || '20% OFF'}`; return `Coupon: ${content.code || 'SAVE20'} - ${content.discount || '20% OFF'}`;
case 'FEEDBACK': case 'FEEDBACK':
return content.feedbackUrl || 'https://example.com/feedback'; return content.feedbackUrl || 'https://example.com/feedback';
case 'BARCODE':
return content.value || '123456789';
default: default:
return 'https://example.com'; 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: default:
return null; return null;
} }
@@ -992,7 +1058,25 @@ export default function CreatePage() {
</div> </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' : ''}> <div className={cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}>
<QRCodeSVG <QRCodeSVG
value={qrContent} value={qrContent}

View File

@@ -165,6 +165,8 @@ export default function SettingsPage() {
return { dynamic: 50, price: '€9', period: 'per month' }; return { dynamic: 50, price: '€9', period: 'per month' };
case 'BUSINESS': case 'BUSINESS':
return { dynamic: 500, price: '€29', period: 'per month' }; return { dynamic: 500, price: '€29', period: 'per month' };
case 'ENTERPRISE':
return { dynamic: 99999, price: 'Custom', period: 'per month' };
default: default:
return { dynamic: 3, price: '€0', period: 'forever' }; 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 { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf'; import { useCsrf } from '@/hooks/useCsrf';
type LoginClientProps = { type LoginClientProps = {
showPageHeading?: boolean; showPageHeading?: boolean;
}; };
export default function LoginClient({ showPageHeading = true }: LoginClientProps) { export default function LoginClient({ showPageHeading = true }: LoginClientProps) {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -22,6 +22,7 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); 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="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="w-full max-w-md">
<div className="text-center mb-8"> <div className="text-center mb-8">
<Link href="/" className="inline-flex items-center space-x-2 mb-6"> <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" /> <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> <span className="text-2xl font-bold text-gray-900">QR Master</span>
</Link> </Link>
{showPageHeading ? ( {showPageHeading ? (
<h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1> <h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1>
) : ( ) : (
<h2 className="text-3xl font-bold text-gray-900">Welcome Back</h2> <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> <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"> <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 Back to Home
@@ -112,14 +113,37 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
required required
/> />
<Input <div className="space-y-1">
label="Password" <label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
type="password" <div className="relative">
value={password} <input
onChange={(e) => setPassword(e.target.value)} id="password"
placeholder="••••••••" type={showPassword ? 'text' : 'password'}
required 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"> <div className="flex items-center justify-between">
<label className="flex items-center"> <label className="flex items-center">

View File

@@ -19,6 +19,8 @@ export default function SignupClient() {
const [confirmPassword, setConfirmPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); 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="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="w-full max-w-md">
<div className="text-center mb-8"> <div className="text-center mb-8">
<Link href="/" className="inline-flex items-center space-x-2 mb-6"> <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" /> <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> <span className="text-2xl font-bold text-gray-900">QR Master</span>
</Link> </Link>
<h1 className="text-3xl font-bold text-gray-900">Create Account</h1> <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> <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"> <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 required
/> />
<Input <div className="space-y-1">
label="Password" <label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
type="password" <div className="relative">
value={password} <input
onChange={(e) => setPassword(e.target.value)} id="password"
placeholder="••••••••" type={showPassword ? 'text' : 'password'}
required 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 <div className="space-y-1">
label="Confirm Password" <label htmlFor="confirm-password" className="block text-sm font-medium text-gray-700">Confirm Password</label>
type="password" <div className="relative">
value={confirmPassword} <input
onChange={(e) => setConfirmPassword(e.target.value)} id="confirm-password"
placeholder="••••••••" type={showConfirmPassword ? 'text' : 'password'}
required 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}> <Button type="submit" className="w-full" loading={loading}>
Create Account Create Account

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,62 +1,64 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import { db } from '@/lib/db'; import { db } from '@/lib/db';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const userId = cookies().get('userId')?.value; const userId = cookies().get('userId')?.value;
if (!userId) { if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
} }
// Get user with plan info // Get user with plan info
const user = await db.user.findUnique({ const user = await db.user.findUnique({
where: { id: userId }, where: { id: userId },
select: { select: {
plan: true, plan: true,
}, },
}); });
if (!user) { if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 }); return NextResponse.json({ error: 'User not found' }, { status: 404 });
} }
// Count dynamic QR codes // Count dynamic QR codes
const dynamicQRCount = await db.qRCode.count({ const dynamicQRCount = await db.qRCode.count({
where: { where: {
userId, userId,
type: 'DYNAMIC', type: 'DYNAMIC',
}, },
}); });
// Count static QR codes // Count static QR codes
const staticQRCount = await db.qRCode.count({ const staticQRCount = await db.qRCode.count({
where: { where: {
userId, userId,
type: 'STATIC', type: 'STATIC',
}, },
}); });
// Determine limits based on plan // Determine limits based on plan
let dynamicLimit = 3; // FREE plan default let dynamicLimit = 3; // FREE plan default
if (user.plan === 'PRO') { if (user.plan === 'PRO') {
dynamicLimit = 50; dynamicLimit = 50;
} else if (user.plan === 'BUSINESS') { } else if (user.plan === 'BUSINESS') {
dynamicLimit = 500; dynamicLimit = 500;
} } else if ((user.plan as string) === 'ENTERPRISE') {
dynamicLimit = 99999;
return NextResponse.json({ }
dynamicUsed: dynamicQRCount,
dynamicLimit, return NextResponse.json({
staticUsed: staticQRCount, dynamicUsed: dynamicQRCount,
}); dynamicLimit,
} catch (error) { staticUsed: staticQRCount,
console.error('Error fetching user stats:', error); });
return NextResponse.json( } catch (error) {
{ error: 'Internal server error' }, console.error('Error fetching user stats:', error);
{ status: 500 } 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'; const baseUrlFeedback = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050';
destination = `${baseUrlFeedback}/feedback/${slug}`; destination = `${baseUrlFeedback}/feedback/${slug}`;
break; break;
case 'BARCODE':
// Dynamic barcode redirects to its stored URL
destination = ensureAbsoluteUrl(content.url || content.value || 'https://example.com');
break;
default: default:
destination = 'https://example.com'; destination = 'https://example.com';
} }

View File

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

View File

@@ -1,141 +1,164 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { BillingToggle } from '@/components/ui/BillingToggle'; import { BillingToggle } from '@/components/ui/BillingToggle';
interface PricingProps { interface PricingProps {
t: any; // i18n translation function t: any; // i18n translation function
} }
export const Pricing: React.FC<PricingProps> = ({ t }) => { export const Pricing: React.FC<PricingProps> = ({ t }) => {
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month'); const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
const plans = [ const plans = [
{ {
key: 'free', key: 'free',
popular: false, popular: false,
}, },
{ {
key: 'pro', key: 'pro',
popular: true, popular: true,
}, },
{ {
key: 'business', key: 'business',
popular: false, popular: false,
}, },
]; {
key: 'enterprise',
return ( popular: false,
<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 }} return (
whileInView={{ opacity: 1, y: 0 }} <section id="pricing" className="py-16">
viewport={{ once: true }} <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
transition={{ duration: 0.5 }} <motion.div
className="text-center mb-12" initial={{ opacity: 0, y: 20 }}
> whileInView={{ opacity: 1, y: 0 }}
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4"> viewport={{ once: true }}
{t.pricing.title} transition={{ duration: 0.5 }}
</h2> className="text-center mb-12"
<p className="text-xl text-gray-600"> >
{t.pricing.subtitle} <h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
</p> {t.pricing.title}
</motion.div> </h2>
<p className="text-xl text-gray-600">{t.pricing.subtitle}</p>
<div className="flex justify-center mb-8"> </motion.div>
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
</div> <div className="flex justify-center mb-8">
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto"> </div>
{plans.map((plan, index) => (
<motion.div <div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8 max-w-7xl mx-auto">
key={plan.key} {plans.map((plan, index) => (
initial={{ opacity: 0, y: 20 }} <motion.div
whileInView={{ opacity: 1, y: 0 }} key={plan.key}
viewport={{ once: true }} initial={{ opacity: 0, y: 20 }}
transition={{ duration: 0.5, delay: index * 0.1 }} whileInView={{ opacity: 1, y: 0 }}
className="h-full" viewport={{ once: true }}
> transition={{ duration: 0.5, delay: index * 0.1 }}
<Card className="h-full"
className={`h-full flex flex-col ${plan.popular >
? 'border-primary-500 shadow-xl relative scale-105 z-10' <Card
: 'border-gray-200 hover:border-gray-300 hover:shadow-lg transition-all' className={`h-full flex flex-col ${
}`} plan.popular
> ? 'border-primary-500 shadow-xl relative scale-105 z-10'
{plan.popular && ( : 'border-gray-200 hover:border-gray-300 hover:shadow-lg transition-all'
<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} {plan.popular && (
</Badge> <div className="absolute -top-4 left-1/2 transform -translate-x-1/2 w-full text-center">
</div> <Badge variant="info" className="px-4 py-1.5 shadow-sm">
)} {t.pricing[plan.key].badge}
</Badge>
<CardHeader className="text-center pb-8"> </div>
<CardTitle className="text-2xl mb-4"> )}
{t.pricing[plan.key].title}
</CardTitle> <CardHeader className="text-center pb-8">
<div className="flex flex-col items-center"> <CardTitle className="text-2xl mb-4">
<div className="flex items-baseline justify-center"> {t.pricing[plan.key].title}
<span className="text-4xl font-bold"> </CardTitle>
{plan.key === 'free' <div className="flex flex-col items-center">
? t.pricing[plan.key].price <div className="flex items-baseline justify-center">
: billingPeriod === 'month' <span className="text-4xl font-bold">
? t.pricing[plan.key].price {plan.key === 'free' || plan.key === 'enterprise'
: plan.key === 'pro' ? t.pricing[plan.key].price
? '€90' : billingPeriod === 'month'
: '€290'} ? t.pricing[plan.key].price
</span> : plan.key === 'pro'
<span className="text-gray-600 ml-2"> ? '€90'
{plan.key === 'free' : '€290'}
? t.pricing[plan.key].period </span>
: billingPeriod === 'month' <span className="text-gray-600 ml-2">
? t.pricing[plan.key].period {plan.key === 'free' || plan.key === 'enterprise'
: 'per year'} ? t.pricing[plan.key].period
</span> : billingPeriod === 'month'
</div> ? t.pricing[plan.key].period
{billingPeriod === 'year' && plan.key !== 'free' && ( : 'per year'}
<Badge variant="success" className="mt-2"> </span>
Save 16% </div>
</Badge> {billingPeriod === 'year' &&
)} plan.key !== 'free' &&
</div> plan.key !== 'enterprise' && (
</CardHeader> <Badge variant="success" className="mt-2">
Save 16%
<CardContent className="space-y-8 flex-1 flex flex-col"> </Badge>
<ul className="space-y-3 flex-1"> )}
{t.pricing[plan.key].features.map((feature: string, index: number) => ( </div>
<li key={index} className="flex items-start space-x-3"> </CardHeader>
<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" /> <CardContent className="space-y-8 flex-1 flex flex-col">
</svg> <ul className="space-y-3 flex-1">
<span className="text-gray-700">{feature}</span> {t.pricing[plan.key].features.map(
</li> (feature: string, index: number) => (
))} <li key={index} className="flex items-start space-x-3">
</ul> <svg
className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5"
<div className="mt-8 pt-8 border-t border-gray-100"> fill="currentColor"
<Link href="/signup"> viewBox="0 0 20 20"
<Button >
variant={plan.popular ? 'primary' : 'outline'} <path
className="w-full" fillRule="evenodd"
size="lg" 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"
Get Started />
</Button> </svg>
</Link> <span className="text-gray-700">{feature}</span>
</div> </li>
</CardContent> )
</Card> )}
</motion.div> </ul>
))}
</div> <div className="mt-8 pt-8 border-t border-gray-100">
</div> {plan.key === 'enterprise' ? (
</section> <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="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 className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-8">
<div> <div>
<Link href="/" className="flex items-center space-x-2 mb-4 hover:opacity-80 transition-opacity"> <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" /> <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> <span className={`text-xl font-bold ${isDashboard ? 'text-gray-900' : ''}`}>QR Master</span>
</Link> </Link>
<p className={isDashboard ? 'text-gray-500' : 'text-gray-400'}> <p className={isDashboard ? 'text-gray-500' : 'text-gray-400'}>
{translations.tagline} {translations.tagline}
</p> </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="/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="/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="/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> <li>
<Link href="/qr-code-for" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}> <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> </ul>
</div> </div>
<div> <div>
<h3 className={`font-semibold mb-4 ${isDashboard ? 'text-gray-900' : ''}`}>Compare</h3> <h3 className={`font-semibold mb-4 ${isDashboard ? 'text-gray-900' : ''}`}>Compare</h3>
<ul className={`space-y-2 ${isDashboard ? 'text-gray-500' : 'text-gray-400'}`}> <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="/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="/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/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/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/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="/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> <li><Link href="/vs/beaconstac" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>QR Master vs Uniqode</Link></li>
</ul> </ul>
@@ -136,7 +137,7 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
) : ( ) : (
<div></div> <div></div>
)} )}
<p>&copy; 2026 {translations.rights_reserved}</p> <p>&copy; 2026 {translations.rights_reserved}</p>
<div className="w-12"></div> <div className="w-12"></div>
</div> </div>
</div> </div>

View File

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

View File

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