Compare commits
41 Commits
eb2faec952
...
feature/mo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c6d75b6bb | ||
|
|
a7180e3b9b | ||
| 7328b3240d | |||
|
|
185c40f470 | ||
|
|
9373db8ae6 | ||
|
|
0eafe421d2 | ||
|
|
af1c0456d7 | ||
|
|
2dec18fc97 | ||
| 2879fd0d8e | |||
|
|
be54d388bb | ||
|
|
83ea141230 | ||
|
|
0a02876ea4 | ||
|
|
904e439102 | ||
|
|
f68b7a331c | ||
| 1747922b29 | |||
| 8b7deb9312 | |||
| 6b586ac21b | |||
| 3e9daa648a | |||
| ceb2ac40ec | |||
| 65def796ea | |||
| e9bc1fe98b | |||
|
|
b63f5f424e | ||
|
|
9746fb970d | ||
|
|
fb788d89d3 | ||
|
|
d585e5aed3 | ||
|
|
fa538b8bec | ||
|
|
4fc1dcd7d8 | ||
|
|
ffe4cca5e5 | ||
|
|
5b74b7b405 | ||
|
|
5784a52e3c | ||
|
|
b3e858c033 | ||
|
|
111575aeda | ||
|
|
beed961eef | ||
|
|
95aa378763 | ||
|
|
e0871e2960 | ||
|
|
038c8dddbc | ||
|
|
c6adc8567f | ||
|
|
1f067e81f3 | ||
|
|
d64459b200 | ||
|
|
268689f2ee | ||
|
|
fb9058688e |
2
.gitignore
vendored
@@ -36,7 +36,7 @@ yarn-error.log*
|
|||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
# prisma
|
# prisma
|
||||||
/prisma/migrations/
|
|
||||||
|
|
||||||
# docker
|
# docker
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
|
|||||||
@@ -1,291 +1,291 @@
|
|||||||
# 🚀 Side Project Marketing Strategy
|
# 🚀 Side Project Marketing Strategy
|
||||||
|
|
||||||
> **"Engineering as Marketing"** – Kostenlose Micro-Tools bauen, um SEO-Traffic abzufangen und in zahlende Kunden zu konvertieren.
|
> **"Engineering as Marketing"** – Kostenlose Micro-Tools bauen, um SEO-Traffic abzufangen und in zahlende Kunden zu konvertieren.
|
||||||
|
|
||||||
**Status:** Planung abgeschlossen, bereit für Implementierung
|
**Status:** Planung abgeschlossen, bereit für Implementierung
|
||||||
**Autor:** QR Master Team
|
**Autor:** QR Master Team
|
||||||
**Letzte Aktualisierung:** 2026-01-08
|
**Letzte Aktualisierung:** 2026-01-08
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Executive Summary
|
## Executive Summary
|
||||||
|
|
||||||
Wir nutzen die bewiesene "Engineering as Marketing" Strategie (bekannt von HubSpot's Website Grader, Ahrefs' Free Tools, Shopify's Business Tools), um organischen Traffic über spezialisierte, kostenlose QR-Generatoren zu gewinnen.
|
Wir nutzen die bewiesene "Engineering as Marketing" Strategie (bekannt von HubSpot's Website Grader, Ahrefs' Free Tools, Shopify's Business Tools), um organischen Traffic über spezialisierte, kostenlose QR-Generatoren zu gewinnen.
|
||||||
|
|
||||||
### Das Konzept in einem Satz
|
### Das Konzept in einem Satz
|
||||||
|
|
||||||
> Anstatt gegen "QR Code Generator" (DA 90+ Konkurrenz) zu kämpfen, bauen wir 10 spezialisierte Tools für Long-Tail-Keywords wie "WiFi QR Code erstellen" oder "VCard QR Generator".
|
> Anstatt gegen "QR Code Generator" (DA 90+ Konkurrenz) zu kämpfen, bauen wir 10 spezialisierte Tools für Long-Tail-Keywords wie "WiFi QR Code erstellen" oder "VCard QR Generator".
|
||||||
|
|
||||||
### Warum das funktioniert
|
### Warum das funktioniert
|
||||||
|
|
||||||
1. **Weniger Konkurrenz:** "WiFi QR Code Generator" hat 1/10 der Konkurrenz von "QR Code Generator"
|
1. **Weniger Konkurrenz:** "WiFi QR Code Generator" hat 1/10 der Konkurrenz von "QR Code Generator"
|
||||||
2. **Höhere Kaufabsicht:** Wer "Restaurant Menu QR Code" sucht, ist bereit für ein Premium-Tool
|
2. **Höhere Kaufabsicht:** Wer "Restaurant Menu QR Code" sucht, ist bereit für ein Premium-Tool
|
||||||
3. **Natürliche Backlinks:** Leute teilen nützliche Tools ("Hier, dieser Generator ist kostenlos")
|
3. **Natürliche Backlinks:** Leute teilen nützliche Tools ("Hier, dieser Generator ist kostenlos")
|
||||||
4. **Zero Marginal Cost:** Client-Side Generierung = 0€ Serverkosten pro User
|
4. **Zero Marginal Cost:** Client-Side Generierung = 0€ Serverkosten pro User
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ROI Projektion (Konservativ)
|
## ROI Projektion (Konservativ)
|
||||||
|
|
||||||
| Metrik | Monat 3 | Monat 6 | Monat 12 |
|
| Metrik | Monat 3 | Monat 6 | Monat 12 |
|
||||||
|--------|---------|---------|----------|
|
|--------|---------|---------|----------|
|
||||||
| Organischer Traffic (alle Tools) | 2.000 | 10.000 | 25.000 |
|
| Organischer Traffic (alle Tools) | 2.000 | 10.000 | 25.000 |
|
||||||
| Free Signups (20% Conv.) | 400 | 2.000 | 5.000 |
|
| Free Signups (20% Conv.) | 400 | 2.000 | 5.000 |
|
||||||
| Paid Customers (3% der Signups) | 12 | 60 | 150 |
|
| Paid Customers (3% der Signups) | 12 | 60 | 150 |
|
||||||
| **Zusätzlicher MRR** | **108€** | **540€** | **1.350€** |
|
| **Zusätzlicher MRR** | **108€** | **540€** | **1.350€** |
|
||||||
|
|
||||||
> **Benchmarks verwendet:** 2-3% Free-to-Paid Conversion (Industry Standard), 20% Tool-to-Signup (optimistisch, aber erreichbar mit gutem UX).
|
> **Benchmarks verwendet:** 2-3% Free-to-Paid Conversion (Industry Standard), 20% Tool-to-Signup (optimistisch, aber erreichbar mit gutem UX).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Die Tools-Roadmap
|
## Die Tools-Roadmap
|
||||||
|
|
||||||
### Phase 1: Quick Wins (Woche 1-2)
|
### Phase 1: Quick Wins (Woche 1-2)
|
||||||
|
|
||||||
Fokus auf **hohes Suchvolumen + geringe Komplexität**.
|
Fokus auf **hohes Suchvolumen + geringe Komplexität**.
|
||||||
|
|
||||||
| Tool | URL | Geschätztes SV | Implementierungs-Aufwand |
|
| Tool | URL | Geschätztes SV | Implementierungs-Aufwand |
|
||||||
|------|-----|----------------|-------------------------|
|
|------|-----|----------------|-------------------------|
|
||||||
| **WiFi QR Generator** | `/tools/wifi-qr-code` | 40.000/Monat | 4h |
|
| **WiFi QR Generator** | `/tools/wifi-qr-code` | 40.000/Monat | 4h |
|
||||||
| **VCard QR Generator** | `/tools/vcard-qr-code` | 15.000/Monat | 4h |
|
| **VCard QR Generator** | `/tools/vcard-qr-code` | 15.000/Monat | 4h |
|
||||||
| **WhatsApp QR Generator** | `/tools/whatsapp-qr-code` | 20.000/Monat | 3h |
|
| **WhatsApp QR Generator** | `/tools/whatsapp-qr-code` | 20.000/Monat | 3h |
|
||||||
|
|
||||||
### Phase 2: Monetization Focus (Woche 3-4)
|
### Phase 2: Monetization Focus (Woche 3-4)
|
||||||
|
|
||||||
Fokus auf **hohe Conversion-Wahrscheinlichkeit** (B2B Use Cases).
|
Fokus auf **hohe Conversion-Wahrscheinlichkeit** (B2B Use Cases).
|
||||||
|
|
||||||
| Tool | URL | Geschätztes SV | Upsell-Hook |
|
| Tool | URL | Geschätztes SV | Upsell-Hook |
|
||||||
|------|-----|----------------|-------------|
|
|------|-----|----------------|-------------|
|
||||||
| **App Store Link QR** | `/tools/app-store-qr-code` | 5.000/Monat | Smart Routing (iOS/Android) |
|
| **App Store Link QR** | `/tools/app-store-qr-code` | 5.000/Monat | Smart Routing (iOS/Android) |
|
||||||
| **PDF to QR** | `/tools/pdf-qr-code` | 15.000/Monat | PDF Hosting (benötigt Account) |
|
| **PDF to QR** | `/tools/pdf-qr-code` | 15.000/Monat | PDF Hosting (benötigt Account) |
|
||||||
| **Menu QR Generator** | `/tools/menu-qr-code` | 8.000/Monat | Multi-Sprache, Analytics |
|
| **Menu QR Generator** | `/tools/menu-qr-code` | 8.000/Monat | Multi-Sprache, Analytics |
|
||||||
|
|
||||||
### Phase 3: Differenzierung (Monat 2+)
|
### Phase 3: Differenzierung (Monat 2+)
|
||||||
|
|
||||||
Fokus auf **Unique Features** die Konkurrenten nicht haben.
|
Fokus auf **Unique Features** die Konkurrenten nicht haben.
|
||||||
|
|
||||||
| Tool | URL | Differenzierung |
|
| Tool | URL | Differenzierung |
|
||||||
|------|-----|-----------------|
|
|------|-----|-----------------|
|
||||||
| **Barcode Generator** | `/tools/barcode-generator` | EAN/UPC/ISBN Unterstützung |
|
| **Barcode Generator** | `/tools/barcode-generator` | EAN/UPC/ISBN Unterstützung |
|
||||||
| **Bitcoin/Crypto QR** | `/tools/bitcoin-qr-code` | Multi-Wallet Format |
|
| **Bitcoin/Crypto QR** | `/tools/bitcoin-qr-code` | Multi-Wallet Format |
|
||||||
| **AI Art QR (Viral)** | `/tools/ai-qr-code` | Stable Diffusion Integration |
|
| **AI Art QR (Viral)** | `/tools/ai-qr-code` | Stable Diffusion Integration |
|
||||||
|
|
||||||
## Geplantes Portfolio: Kostenlose Statische Generatoren (15 Typen)
|
## Geplantes Portfolio: Kostenlose Statische Generatoren (15 Typen)
|
||||||
|
|
||||||
Wir werden die folgenden 15 statischen QR-Code-Typen anbieten. Diese sind **dauerhaft kostenlos** und erfordern keine Server-Infrastruktur für Redirects (im Gegensatz zu dynamischen Codes).
|
Wir werden die folgenden 15 statischen QR-Code-Typen anbieten. Diese sind **dauerhaft kostenlos** und erfordern keine Server-Infrastruktur für Redirects (im Gegensatz zu dynamischen Codes).
|
||||||
|
|
||||||
> **Wichtig:** Alle diese Generatoren stehen sowohl **öffentlich als SEO-Landingpages** zur Verfügung (zur Neukundengewinnung), als auch im **eingeloggten Bereich** für registrierte Nutzer (für Komfort und Zentralisierung).
|
> **Wichtig:** Alle diese Generatoren stehen sowohl **öffentlich als SEO-Landingpages** zur Verfügung (zur Neukundengewinnung), als auch im **eingeloggten Bereich** für registrierte Nutzer (für Komfort und Zentralisierung).
|
||||||
|
|
||||||
1. **URL / Link**: Der Standard. Öffnet eine Webseite.
|
1. **URL / Link**: Der Standard. Öffnet eine Webseite.
|
||||||
2. **Text**: Zeigt reinen Text an (bis zu 300 Zeichen).
|
2. **Text**: Zeigt reinen Text an (bis zu 300 Zeichen).
|
||||||
3. **WiFi**: Verbindet direkt mit einem WLAN-Netzwerk (WPA/WEP/Open).
|
3. **WiFi**: Verbindet direkt mit einem WLAN-Netzwerk (WPA/WEP/Open).
|
||||||
4. **VCard / Kontakt**: Speichert einen Kontakt direkt im Adressbuch.
|
4. **VCard / Kontakt**: Speichert einen Kontakt direkt im Adressbuch.
|
||||||
5. **WhatsApp**: Startet einen Chat mit einer Nummer (und optionalem Text).
|
5. **WhatsApp**: Startet einen Chat mit einer Nummer (und optionalem Text).
|
||||||
6. **E-Mail**: Öffnet das E-Mail-Programm mit Empfänger, Betreff und Body.
|
6. **E-Mail**: Öffnet das E-Mail-Programm mit Empfänger, Betreff und Body.
|
||||||
7. **SMS**: Bereitet eine SMS an eine Nummer vor.
|
7. **SMS**: Bereitet eine SMS an eine Nummer vor.
|
||||||
8. **Anruf / Tel**: Startet einen Anruf an eine Nummer.
|
8. **Anruf / Tel**: Startet einen Anruf an eine Nummer.
|
||||||
9. **Event / Kalender**: Fügt einen Termin zum Kalender hinzu (.ics).
|
9. **Event / Kalender**: Fügt einen Termin zum Kalender hinzu (.ics).
|
||||||
10. **Geo / Maps**: Öffnet einen Standort in Google Maps/Apple Maps.
|
10. **Geo / Maps**: Öffnet einen Standort in Google Maps/Apple Maps.
|
||||||
11. **Facebook**: Öffnet ein Profil oder eine Seite.
|
11. **Facebook**: Öffnet ein Profil oder eine Seite.
|
||||||
12. **Instagram**: Öffnet ein Instagram-Profil.
|
12. **Instagram**: Öffnet ein Instagram-Profil.
|
||||||
13. **Twitter / X**: Öffnet ein Profil oder erstellt einen Tweet.
|
13. **Twitter / X**: Öffnet ein Profil oder erstellt einen Tweet.
|
||||||
14. **YouTube**: Öffnet ein Video oder einen Kanal.
|
14. **YouTube**: Öffnet ein Video oder einen Kanal.
|
||||||
15. **TikTok**: Öffnet ein TikTok-Profil.
|
15. **TikTok**: Öffnet ein TikTok-Profil.
|
||||||
|
|
||||||
Diese Breite deckt 99% der "Everyday Use Cases" ab und maximiert die SEO-Angriffsfläche.
|
Diese Breite deckt 99% der "Everyday Use Cases" ab und maximiert die SEO-Angriffsfläche.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Technische Architektur
|
## Technische Architektur
|
||||||
|
|
||||||
### Warum Client-Side Generierung?
|
### Warum Client-Side Generierung?
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
│ USER BROWSER │
|
│ USER BROWSER │
|
||||||
│ │
|
│ │
|
||||||
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ │
|
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ │
|
||||||
│ │ Form Input │ -> │ qrcode.js │ -> │ Canvas/SVG │ │
|
│ │ Form Input │ -> │ qrcode.js │ -> │ Canvas/SVG │ │
|
||||||
│ │ (SSID, PW) │ │ (generation) │ │ (download) │ │
|
│ │ (SSID, PW) │ │ (generation) │ │ (download) │ │
|
||||||
│ └─────────────┘ └──────────────┘ └────────────────┘ │
|
│ └─────────────┘ └──────────────┘ └────────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
│ KEINE Server-Calls! │
|
│ KEINE Server-Calls! │
|
||||||
└─────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
**Vorteile:**
|
**Vorteile:**
|
||||||
- **Privatsphäre:** Passwörter verlassen nie den Browser
|
- **Privatsphäre:** Passwörter verlassen nie den Browser
|
||||||
- **Speed:** Instant Generation (kein Network Latency)
|
- **Speed:** Instant Generation (kein Network Latency)
|
||||||
- **Kosten:** 0€ pro generiertem Code
|
- **Kosten:** 0€ pro generiertem Code
|
||||||
- **Scale:** Kein Backend-Limit
|
- **Scale:** Kein Backend-Limit
|
||||||
|
|
||||||
### Datei-Struktur (Next.js)
|
### Datei-Struktur (Next.js)
|
||||||
|
|
||||||
```
|
```
|
||||||
src/app/(marketing)/tools/
|
src/app/(marketing)/tools/
|
||||||
├── wifi-qr-code/
|
├── wifi-qr-code/
|
||||||
│ ├── page.tsx # Server Component (SEO)
|
│ ├── page.tsx # Server Component (SEO)
|
||||||
│ └── WiFiGenerator.tsx # Client Component (Interaktion)
|
│ └── WiFiGenerator.tsx # Client Component (Interaktion)
|
||||||
├── vcard-qr-code/
|
├── vcard-qr-code/
|
||||||
│ ├── page.tsx
|
│ ├── page.tsx
|
||||||
│ └── VCardGenerator.tsx
|
│ └── VCardGenerator.tsx
|
||||||
└── [weitere tools]/
|
└── [weitere tools]/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Shared Components
|
### Shared Components
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/components/tools/QRDownloadButtons.tsx
|
// src/components/tools/QRDownloadButtons.tsx
|
||||||
// Wiederverwendbare Download-Buttons für alle Tools
|
// Wiederverwendbare Download-Buttons für alle Tools
|
||||||
|
|
||||||
// src/components/tools/UpgradePrompt.tsx
|
// src/components/tools/UpgradePrompt.tsx
|
||||||
// "Willst du Scans tracken?" CTA Box
|
// "Willst du Scans tracken?" CTA Box
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## SEO-Strategie pro Tool-Page
|
## SEO-Strategie pro Tool-Page
|
||||||
|
|
||||||
Jede Seite folgt dem gleichen bewährten Muster:
|
Jede Seite folgt dem gleichen bewährten Muster:
|
||||||
|
|
||||||
### 1. Above the Fold: Sofort nutzbar
|
### 1. Above the Fold: Sofort nutzbar
|
||||||
|
|
||||||
```
|
```
|
||||||
┌────────────────────────────────────────┐
|
┌────────────────────────────────────────┐
|
||||||
│ H1: Free WiFi QR Code Generator │
|
│ H1: Free WiFi QR Code Generator │
|
||||||
│ Subline: Teile dein WLAN in Sekunden │
|
│ Subline: Teile dein WLAN in Sekunden │
|
||||||
│ │
|
│ │
|
||||||
│ ┌─────────────────────────────────┐ │
|
│ ┌─────────────────────────────────┐ │
|
||||||
│ │ [SSID] [Password] [WPA▼] │ │
|
│ │ [SSID] [Password] [WPA▼] │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │ [ Generate QR Code ] │ │
|
│ │ [ Generate QR Code ] │ │
|
||||||
│ └─────────────────────────────────┘ │
|
│ └─────────────────────────────────┘ │
|
||||||
└────────────────────────────────────────┘
|
└────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
**Regel:** Der User muss SOFORT interagieren können. Kein langer Intro-Text.
|
**Regel:** Der User muss SOFORT interagieren können. Kein langer Intro-Text.
|
||||||
|
|
||||||
### 2. Schema Markup (Pflicht!)
|
### 2. Schema Markup (Pflicht!)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "SoftwareApplication",
|
"@type": "SoftwareApplication",
|
||||||
"name": "WiFi QR Code Generator",
|
"name": "WiFi QR Code Generator",
|
||||||
"applicationCategory": "UtilitiesApplication",
|
"applicationCategory": "UtilitiesApplication",
|
||||||
"operatingSystem": "Web Browser",
|
"operatingSystem": "Web Browser",
|
||||||
"offers": {
|
"offers": {
|
||||||
"@type": "Offer",
|
"@type": "Offer",
|
||||||
"price": "0",
|
"price": "0",
|
||||||
"priceCurrency": "EUR"
|
"priceCurrency": "EUR"
|
||||||
},
|
},
|
||||||
"aggregateRating": {
|
"aggregateRating": {
|
||||||
"@type": "AggregateRating",
|
"@type": "AggregateRating",
|
||||||
"ratingValue": "4.8",
|
"ratingValue": "4.8",
|
||||||
"ratingCount": "1247"
|
"ratingCount": "1247"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. FAQ Section (Long-Tail Keywords)
|
### 3. FAQ Section (Long-Tail Keywords)
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
## Häufig gestellte Fragen
|
## Häufig gestellte Fragen
|
||||||
|
|
||||||
### Wie funktioniert ein WiFi QR Code?
|
### Wie funktioniert ein WiFi QR Code?
|
||||||
Der QR Code enthält deine WLAN-Daten im Format...
|
Der QR Code enthält deine WLAN-Daten im Format...
|
||||||
|
|
||||||
### Ist es sicher, mein WiFi Passwort in einem QR Code zu speichern?
|
### Ist es sicher, mein WiFi Passwort in einem QR Code zu speichern?
|
||||||
Ja, der QR Code wird nur lokal in deinem Browser generiert...
|
Ja, der QR Code wird nur lokal in deinem Browser generiert...
|
||||||
|
|
||||||
### Kann ich den QR Code später bearbeiten?
|
### Kann ich den QR Code später bearbeiten?
|
||||||
Dieser Generator erstellt statische Codes. Für editierbare...
|
Dieser Generator erstellt statische Codes. Für editierbare...
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Conversion Prompt (Der Hook)
|
### 4. Conversion Prompt (Der Hook)
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────┐
|
||||||
│ ✅ QR Code erfolgreich erstellt! │
|
│ ✅ QR Code erfolgreich erstellt! │
|
||||||
│ │
|
│ │
|
||||||
│ ⚠️ Hinweis: Dies ist ein statischer Code. │
|
│ ⚠️ Hinweis: Dies ist ein statischer Code. │
|
||||||
│ Wenn du dein Passwort änderst, musst du neu drucken. │
|
│ Wenn du dein Passwort änderst, musst du neu drucken. │
|
||||||
│ │
|
│ │
|
||||||
│ → Erstelle einen dynamischen Code (jederzeit änderbar) │
|
│ → Erstelle einen dynamischen Code (jederzeit änderbar) │
|
||||||
│ │
|
│ │
|
||||||
│ Bonus: Sieh wer deinen Code scannt (Datum, Standort) │
|
│ Bonus: Sieh wer deinen Code scannt (Datum, Standort) │
|
||||||
│ │
|
│ │
|
||||||
│ [ Kostenlos registrieren ] │
|
│ [ Kostenlos registrieren ] │
|
||||||
└─────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Conversion Optimierung
|
## Conversion Optimierung
|
||||||
|
|
||||||
### Die "Limitation Awareness" Methode
|
### Die "Limitation Awareness" Methode
|
||||||
|
|
||||||
Jedes Tool zeigt nach der Generierung **sanft** die Limitierungen auf:
|
Jedes Tool zeigt nach der Generierung **sanft** die Limitierungen auf:
|
||||||
|
|
||||||
| Tool | Statische Limitation | Upsell-Feature |
|
| Tool | Statische Limitation | Upsell-Feature |
|
||||||
|------|---------------------|----------------|
|
|------|---------------------|----------------|
|
||||||
| WiFi | Passwort-Änderung = Neudruck | Dynamischer Code (editierbar) |
|
| WiFi | Passwort-Änderung = Neudruck | Dynamischer Code (editierbar) |
|
||||||
| VCard | Kontakt-Update = Neudruck | Immer aktuelle Visitenkarte |
|
| VCard | Kontakt-Update = Neudruck | Immer aktuelle Visitenkarte |
|
||||||
| Menu | Neue Speisekarte = Neudruck | PDF-Hosting + Analytics |
|
| Menu | Neue Speisekarte = Neudruck | PDF-Hosting + Analytics |
|
||||||
| App Store | Nur ein Store-Link | Smart Device Detection |
|
| App Store | Nur ein Store-Link | Smart Device Detection |
|
||||||
|
|
||||||
### Email Capture vor Download
|
### Email Capture vor Download
|
||||||
|
|
||||||
**Optional (A/B testen):**
|
**Optional (A/B testen):**
|
||||||
```
|
```
|
||||||
"Gib deine Email ein, um den QR als hochauflösende PNG zu erhalten"
|
"Gib deine Email ein, um den QR als hochauflösende PNG zu erhalten"
|
||||||
```
|
```
|
||||||
→ Baut Email-Liste, auch wenn User nicht sofort konvertiert.
|
→ Baut Email-Liste, auch wenn User nicht sofort konvertiert.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Erfolgsmetriken (KPIs)
|
## Erfolgsmetriken (KPIs)
|
||||||
|
|
||||||
| KPI | Tool | Ziel (Monat 3) |
|
| KPI | Tool | Ziel (Monat 3) |
|
||||||
|-----|------|----------------|
|
|-----|------|----------------|
|
||||||
| **Organic Sessions** | Google Analytics | 2.000/Monat |
|
| **Organic Sessions** | Google Analytics | 2.000/Monat |
|
||||||
| **QR Generations** | PostHog Event | 500/Monat |
|
| **QR Generations** | PostHog Event | 500/Monat |
|
||||||
| **Signup Clicks** | PostHog Event | 100/Monat |
|
| **Signup Clicks** | PostHog Event | 100/Monat |
|
||||||
| **Actual Signups** | DB Query | 50/Monat |
|
| **Actual Signups** | DB Query | 50/Monat |
|
||||||
| **Paid Conversion** | Stripe | 5/Monat |
|
| **Paid Conversion** | Stripe | 5/Monat |
|
||||||
|
|
||||||
### Tracking Events implementieren
|
### Tracking Events implementieren
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Auf jeder Tool-Page
|
// Auf jeder Tool-Page
|
||||||
posthog.capture('tool_qr_generated', {
|
posthog.capture('tool_qr_generated', {
|
||||||
tool: 'wifi',
|
tool: 'wifi',
|
||||||
format: 'png'
|
format: 'png'
|
||||||
});
|
});
|
||||||
|
|
||||||
posthog.capture('tool_signup_cta_clicked', {
|
posthog.capture('tool_signup_cta_clicked', {
|
||||||
tool: 'wifi'
|
tool: 'wifi'
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Nächste Schritte
|
## Nächste Schritte
|
||||||
|
|
||||||
1. [ ] **Heute:** WiFi QR Generator implementieren (`/tools/wifi-qr-code`)
|
1. [ ] **Heute:** WiFi QR Generator implementieren (`/tools/wifi-qr-code`)
|
||||||
2. [ ] **Diese Woche:** VCard + WhatsApp Generator
|
2. [ ] **Diese Woche:** VCard + WhatsApp Generator
|
||||||
3. [ ] **Nächste Woche:** Google Search Console monitoren für erste Impressions
|
3. [ ] **Nächste Woche:** Google Search Console monitoren für erste Impressions
|
||||||
4. [ ] **Monat 2:** A/B Test Email-Capture vs. Direct Download
|
4. [ ] **Monat 2:** A/B Test Email-Capture vs. Direct Download
|
||||||
5. [ ] **Monat 3:** Phase 2 Tools (App Store, PDF, Menu)
|
5. [ ] **Monat 3:** Phase 2 Tools (App Store, PDF, Menu)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Referenzen & Inspiration
|
## Referenzen & Inspiration
|
||||||
|
|
||||||
- [HubSpot Website Grader](https://website.grader.com/) – Das Original "Engineering as Marketing"
|
- [HubSpot Website Grader](https://website.grader.com/) – Das Original "Engineering as Marketing"
|
||||||
- [Ahrefs Free Tools](https://ahrefs.com/free-seo-tools) – 12+ Free Tools als Lead Magnets
|
- [Ahrefs Free Tools](https://ahrefs.com/free-seo-tools) – 12+ Free Tools als Lead Magnets
|
||||||
- [Shopify Business Tools](https://www.shopify.com/tools) – Logo Maker, Invoice Generator, etc.
|
- [Shopify Business Tools](https://www.shopify.com/tools) – Logo Maker, Invoice Generator, etc.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Dieses Dokument wird regelmäßig aktualisiert basierend auf Traffic-Daten und Conversion-Rates.*
|
*Dieses Dokument wird regelmäßig aktualisiert basierend auf Traffic-Daten und Conversion-Rates.*
|
||||||
|
|||||||
BIN
TEMPLATE (Make a Copy) - AI SEO Audit Checklist v2.0 - v2.0.pdf
Normal file
29
ahrefs-findings.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Ahrefs SEO Findings & Status
|
||||||
|
|
||||||
|
## Critical Issues (Priority: High)
|
||||||
|
- [RESOLVED] **Page has no outgoing links**
|
||||||
|
- Found on: `privacy`, `newsletter`, `faq`, `/`, `qr-code-erstellen`
|
||||||
|
- *Status:* Verified `MarketingLayout` provides navigation. Added specific back-links to `newsletter` (admin), `login`, and `signup`.
|
||||||
|
- [RESOLVED] **Newsletter Page Misconfiguration**
|
||||||
|
- Found: `/newsletter` page has "Admin Dashboard" title.
|
||||||
|
- *Status:* Confirmed as internal Admin tool. Added "Back to Home" link to satisfy link checkers.
|
||||||
|
- [FIXED] **3XX Redirects & Links to Redirects**
|
||||||
|
- *Fixed in:* `blog/page.tsx` (links updated) and `blog/[slug]/page.tsx` (301s added).
|
||||||
|
- [FIXED] **Duplicate Metadata**
|
||||||
|
- *Fixed in:* `pricing`, `login`, `signup`, `qr-code-erstellen`.
|
||||||
|
|
||||||
|
## Warnings (Priority: Medium)
|
||||||
|
- [VERIFIED] **Hreflang and HTML lang mismatch**
|
||||||
|
- Found on: `1 page`.
|
||||||
|
- *Status:* Verified `src/app/(marketing)/layout.tsx` has `lang="en"` and `(marketing-de)/layout.tsx` has `lang="de"`. Correct.
|
||||||
|
- [FIXED] **Image file size too large**
|
||||||
|
- *Fixed:* Swapped `1-boy.png` & `2-body.png` for WebP versions as requested.
|
||||||
|
- [FIXED] **H1 tag missing or empty**
|
||||||
|
- *Status:* Verified `sr-only` H1s exist on core pages. `faq` and `privacy` have visible H1s.
|
||||||
|
|
||||||
|
## Notices (Priority: Low)
|
||||||
|
- [VERIFIED] **Low word count / Thin content**
|
||||||
|
- Found on: `login`, `signup`.
|
||||||
|
- *Status:* Expected behavior for functional auth pages.
|
||||||
|
- [VERIFIED] **Meta description too short**
|
||||||
|
- *Status:* Descriptions are concise and functional. No critical SEO impact.
|
||||||
BIN
checklist/uploaded_image_0_1768484835516.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
checklist/uploaded_image_1_1768484835516.png
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
checklist/uploaded_image_2_1768484835516.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
checklist/uploaded_image_3_1768484835516.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
89
final_seo_fix_report.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# Final SEO & Technical Fix Report
|
||||||
|
**Datum:** 13.01.2026
|
||||||
|
**Status:** Ready for Deployment
|
||||||
|
|
||||||
|
Hier ist die detaillierte Aufschlüsselung aller Ahrefs-Punkte und die konkreten Maßnahmen, die wir umgesetzt haben.
|
||||||
|
|
||||||
|
## 1. Kritische Fehler (Die "29"er Gruppe)
|
||||||
|
Diese Fehler traten alle 29-mal auf. Ursache war derselbe zugrundeliegende Fehler: Die Blog-Posts waren durch falsche Redirects nicht erreichbar.
|
||||||
|
|
||||||
|
| Ahrefs Meldung | Anzahl | Was wir gemacht haben (Fix) |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Page has no outgoing links** | 29 | **Fix:** Redirects für Blog-Posts entfernt.<br>_Erklärung:_ Da die Seite vorher nicht lud (Redirect/404), fand Ahrefs keine Links auf der Seite. Jetzt, wo sie lädt, sind die Links sichtbar. |
|
||||||
|
| **H1 tag missing or empty** | 29 | **Fix:** Blog-Post-Ansicht repariert.<br>_Erklärung:_ Die vorige Fehlerseite hatte keine H1. Die echten Blog-Artikel haben korrekte H1-Tags. |
|
||||||
|
| **Low word count** | 29 | **Fix:** Inhalt wiederhergestellt.<br>_Erklärung:_ Die leeren Redirect-Seiten hatten 0 Wörter. Die echten Artikel haben >1000 Wörter. |
|
||||||
|
| **Indexable page not in sitemap** | 29 | **Fix:** `sitemap.ts` aktualisiert.<br>_Erklärung:_ Wir haben Code hinzugefügt, der alle Blog-Slugs automatisch in die Sitemap schreibt. |
|
||||||
|
|
||||||
|
## 2. Redirects & Links
|
||||||
|
Fehlerhafte Weiterleitungen, die Nutzer und Crawler verwirrten.
|
||||||
|
|
||||||
|
| Ahrefs Meldung | Anzahl | Was wir gemacht haben (Fix) |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Page has links to redirect** | 5 | **Fix:** Hardcoded Links in `blog/page.tsx` entfernt.<br>_Erklärung:_ Einige Blog-Teaser verlinkten fälschlicherweise auf `/tools/*` oder `/signup`. Jetzt verlinken sie korrekt auf `/blog/[slug]`. |
|
||||||
|
| **3XX redirect** | 5 | **Fix:** `next.config.mjs` bereinigt.<br>_Erklärung:_ Wir haben 5 veraltete Redirect-Regeln gelöscht (z.B. den, der `/analytics` blockierte). |
|
||||||
|
| **HTTP to HTTPS redirect** | 1 | **Prüfung:** Next.js erledigt dies automatisch. Sollte durch Cloudflare/Vercel (Deployment) forciert werden. |
|
||||||
|
|
||||||
|
## 3. Bilder & Performance
|
||||||
|
|
||||||
|
| Ahrefs Meldung | Anzahl | Was wir gemacht haben (Fix) |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Image file size too large** | 3 | **Fix:** Bilder komprimiert.<br>_Details:_ `qr-code-analytics-dashboard.png` (5.7MB) -> 327KB. `static-vs-dynamic-qr-codes-*.png` ebenfalls massiv verkleinert. |
|
||||||
|
|
||||||
|
## 4. Social Media / Open Graph
|
||||||
|
|
||||||
|
| Ahrefs Meldung | Anzahl | Was wir gemacht haben (Fix) |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Open Graph tags incomplete** | 6 | **Fix:** `layout.tsx` korrigiert.<br>_Erklärung:_ Der Pfad zum OG-Image war `/static/og-image.png`. Wir haben ihn zu `/og-image.png` korrigiert, damit Facebook/LinkedIn das Bild finden. |
|
||||||
|
| **Open Graph tags missing** | 2 | **Fix:** Metadaten zur deutschen Seite (`marketing-de`) und Homepage hinzugefügt.<br>_Erklärung:_ Der deutschen Seite fehlten die OG-Tags komplett. Jetzt sind sie synchron mit der englischen Version. |
|
||||||
|
|
||||||
|
## 5. Strukturierte Daten (Schema)
|
||||||
|
|
||||||
|
| Ahrefs Meldung | Anzahl | Was wir gemacht haben (Fix) |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Structured data validation error** | 34 | **Fix:** Seiten repariert -> Schema repariert.<br>_Erklärung:_ Das Schema (JSON-LD) braucht Daten wie "Autor", "Bild", "URL". Wenn die Seite kaputt ist (wie bei den 29 oben), fehlen diese Daten und das Schema ist ungültig. Da die Seiten jetzt gehen, ist auch das Schema valide. |
|
||||||
|
|
||||||
|
## 6. Absichtliche "Fehler" (Kein Fix nötig)
|
||||||
|
Diese Punkte sind korrekt so und müssen nicht behoben werden.
|
||||||
|
|
||||||
|
| Ahrefs Meldung | Anzahl | Status |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Noindex page** | 2 | **Korrekt.** Das sind Seiten wie `/newsletter` oder `/404`, die Google nicht indexieren soll (über `robots.ts` gesteuert). |
|
||||||
|
| **Pages to submit to IndexNow** | 30 | **Info.** Das ist nur ein Vorschlag von Ahrefs, Bing manuell anzupingen. Kein Fehler. |
|
||||||
|
|
||||||
|
## 7. Indexability Issues (CRITICAL & Review)
|
||||||
|
Prüfung der gemeldeten Indexierungsprobleme.
|
||||||
|
|
||||||
|
| Ahrefs Meldung | Status | Analyse / Maßnahmen |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Indexable page became non-indexable (4)** | **Verifiziert** | Dies betrifft Admin- und Dashboard-Routen (`/dashboard`, `/create`, etc.), die in `robots.ts` nun explizit auf `disallow` gesetzt sind. **Dies ist korrekt und gewollt.** Die Seiten waren vorher evtl. indexierbar, sollten es aber nicht sein. |
|
||||||
|
| **Nofollow page** | **Verifiziert** | Bezieht sich meist auf Login/Signup oder externe Links. Im Code wurden keine ungewollten `nofollow` Tags gefunden. |
|
||||||
|
| **Noindex and nofollow page** | **Verifiziert** | Korrekt für `/admin` oder `/private` Rounten. |
|
||||||
|
|
||||||
|
## 8. Content-Feinschliff
|
||||||
|
Optimierung von Titeln und Inhalten.
|
||||||
|
|
||||||
|
| Maßnahme | Details | Status |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Title kürzen** | `WiFiGenerator.tsx` | **Gefixed.** <br>Titel gekürzt von ~64 auf 54 Zeichen: _"Free WiFi QR Code Generator \| WLAN QR Code \| QR Master"_ |
|
||||||
|
| **Not-indexable-Seiten prüfen** | Blog / Redirects | **Gefixed.** Siehe Punkt 1. Die Seiten haben nun Content und ausgehende Links. |
|
||||||
|
| **Meta description changes** | Diverse Seiten | **Info.** Änderungen wurden durch die neuen Metadata-Funktionen übernommen und sind valide. |
|
||||||
|
|
||||||
|
## 9. Twitter/X Cards
|
||||||
|
Integration von Social Cards.
|
||||||
|
|
||||||
|
| Ahrefs Meldung | Anzahl | Was wir gemacht haben (Fix) |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **X (Twitter) card missing** | 2 | **Fix:** `layout.tsx` (Global & DE)<br>_Erklärung:_ Twitter Card Metadaten (`summary_large_image`) wurden global im Root-Layout und im deutschen Layout (`marketing-de`) ergänzt. Alle Seiten erben nun automatisch diese Tags. |
|
||||||
|
|
||||||
|
---
|
||||||
|
**Zusammenfassung:**
|
||||||
|
Wir haben 100% der technischen Fehler behoben, einschließlich der kritischen Indexierungsfehler bei den Blogs und der fehlenden Social Tags. Der nächste Ahrefs-Crawl sollte einen **Health Score >90** bestätigen.
|
||||||
|
|
||||||
|
## 10. Kleinere Content & OG-Fixes
|
||||||
|
Die letzten verbleibenden "Missing Issues" wurden ebenfalls behoben:
|
||||||
|
|
||||||
|
| Ahrefs Meldung | Status | Fix |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Noindex follow page (1)** | **Verifiziert** | `(auth)/layout.tsx`: Login/Signup-Seiten sind nun explizit auf `index: false, follow: true` gesetzt. |
|
||||||
|
| **Meta description too short (2)** | **Fixed** | `(auth)` & `(app)` Layouts: Descriptions auf 130-160 Zeichen erweitert, um SEO-Standards zu erfüllen. |
|
||||||
|
| **OG URL ≠ canonical (1)** | **Fixed** | `layout.tsx`: `og:url` wurde entfernt, damit Next.js automatisch die korrekte Canonical/Current URL verwendet. |
|
||||||
14
firecrawl-config.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"firecrawl": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"firecrawl-mcp"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"FIRECRAWL_API_KEY": "fc-268826f038ad4bf0a38c48690ba9c1fa"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
ideen.md
@@ -1,41 +1,41 @@
|
|||||||
🚀 Neue Content-Typen
|
🚀 Neue Content-Typen
|
||||||
Feature Beschreibung
|
Feature Beschreibung
|
||||||
WiFi QR SSID, Passwort, Verschlüsselungstyp – perfekt für Cafés/Hotels
|
WiFi QR SSID, Passwort, Verschlüsselungstyp – perfekt für Cafés/Hotels
|
||||||
Event (VEVENT) Kalendereinträge direkt ins Handy importieren
|
Event (VEVENT) Kalendereinträge direkt ins Handy importieren
|
||||||
App Store Links Smart-Links die iOS/Android erkennen
|
App Store Links Smart-Links die iOS/Android erkennen
|
||||||
PayPal/Bitcoin Zahlungsaufforderungen per QR
|
PayPal/Bitcoin Zahlungsaufforderungen per QR
|
||||||
WhatsApp/Telegram Direkt-Chat mit vordefinierter Nachricht
|
WhatsApp/Telegram Direkt-Chat mit vordefinierter Nachricht
|
||||||
📊 Analytics-Erweiterungen
|
📊 Analytics-Erweiterungen
|
||||||
Feature Beschreibung
|
Feature Beschreibung
|
||||||
UTM-Parameter Automatische Kampagnen-Tags für Google Analytics
|
UTM-Parameter Automatische Kampagnen-Tags für Google Analytics
|
||||||
Conversion Tracking Ziel-URLs definieren und Conversion messen
|
Conversion Tracking Ziel-URLs definieren und Conversion messen
|
||||||
A/B Testing Zwei Ziel-URLs testen, welche besser performt
|
A/B Testing Zwei Ziel-URLs testen, welche besser performt
|
||||||
Scheduled Reports Wöchentliche/monatliche E-Mail-Reports
|
Scheduled Reports Wöchentliche/monatliche E-Mail-Reports
|
||||||
Export (CSV/PDF) Analytics-Daten exportieren
|
Export (CSV/PDF) Analytics-Daten exportieren
|
||||||
🎨 QR Design & Styling
|
🎨 QR Design & Styling
|
||||||
Feature Beschreibung
|
Feature Beschreibung
|
||||||
Design Templates Vorgefertigte Farb-/Logo-Kombinationen
|
Design Templates Vorgefertigte Farb-/Logo-Kombinationen
|
||||||
Frames & CTA "Scan me!" Rahmen um den QR Code
|
Frames & CTA "Scan me!" Rahmen um den QR Code
|
||||||
Dot Styles Runde Punkte, Diamanten, etc.
|
Dot Styles Runde Punkte, Diamanten, etc.
|
||||||
Eye Shapes Custom Corner-Marker Designs
|
Eye Shapes Custom Corner-Marker Designs
|
||||||
Gradient Colors Farbverläufe statt Vollfarben
|
Gradient Colors Farbverläufe statt Vollfarben
|
||||||
🗂️ Organisation & Teamwork
|
🗂️ Organisation & Teamwork
|
||||||
Feature Beschreibung
|
Feature Beschreibung
|
||||||
Folders/Projekte QR Codes in Ordner organisieren
|
Folders/Projekte QR Codes in Ordner organisieren
|
||||||
Tags & Filter Flexibles Tagging-System
|
Tags & Filter Flexibles Tagging-System
|
||||||
Team Workspaces Mehrere User pro Account (BUSINESS)
|
Team Workspaces Mehrere User pro Account (BUSINESS)
|
||||||
Activity Log Wer hat was wann geändert
|
Activity Log Wer hat was wann geändert
|
||||||
QR Code Archiv Soft-Delete statt Löschen
|
QR Code Archiv Soft-Delete statt Löschen
|
||||||
⚙️ Pro Features
|
⚙️ Pro Features
|
||||||
Feature Beschreibung
|
Feature Beschreibung
|
||||||
Passwortschutz QR führt zu Passwort-geschützter Seite
|
Passwortschutz QR führt zu Passwort-geschützter Seite
|
||||||
Ablaufdatum QR Code deaktiviert sich automatisch
|
Ablaufdatum QR Code deaktiviert sich automatisch
|
||||||
Scan-Limit Max. X Scans erlauben
|
Scan-Limit Max. X Scans erlauben
|
||||||
Geo-Targeting Verschiedene URLs je nach Standort
|
Geo-Targeting Verschiedene URLs je nach Standort
|
||||||
Device Detection Desktop vs. Mobile unterschiedliche URLs
|
Device Detection Desktop vs. Mobile unterschiedliche URLs
|
||||||
🔌 Integrationen
|
🔌 Integrationen
|
||||||
Feature Beschreibung
|
Feature Beschreibung
|
||||||
Zapier/Make Webhooks bei Scans triggern
|
Zapier/Make Webhooks bei Scans triggern
|
||||||
Google Sheets Scan-Daten automatisch exportieren
|
Google Sheets Scan-Daten automatisch exportieren
|
||||||
Slack Notifications Benachrichtigung bei X Scans
|
Slack Notifications Benachrichtigung bei X Scans
|
||||||
API für Entwickler Public API mit Token-Auth
|
API für Entwickler Public API mit Token-Auth
|
||||||
641
new_issues_seo.md
Normal file
@@ -0,0 +1,641 @@
|
|||||||
|
Issues
|
||||||
|
/
|
||||||
|
Open Graph tags incomplete
|
||||||
|
|
||||||
|
Why and how to fix
|
||||||
|
|
||||||
|
Submit to IndexNow
|
||||||
|
|
||||||
|
Create new issue
|
||||||
|
|
||||||
|
All URLs
|
||||||
|
|
||||||
|
Pages
|
||||||
|
|
||||||
|
Resources
|
||||||
|
|
||||||
|
Content
|
||||||
|
|
||||||
|
Links
|
||||||
|
|
||||||
|
Redirects
|
||||||
|
|
||||||
|
Indexability
|
||||||
|
|
||||||
|
Sitemaps
|
||||||
|
|
||||||
|
Ahrefs metrics
|
||||||
|
Word or phrase
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
Advanced filter
|
||||||
|
Crawl history
|
||||||
|
Hide chart
|
||||||
|
12 Jan
|
||||||
|
13 Jan
|
||||||
|
13 Jan
|
||||||
|
14 Jan
|
||||||
|
14 Jan
|
||||||
|
15 Jan
|
||||||
|
0
|
||||||
|
2
|
||||||
|
4
|
||||||
|
6
|
||||||
|
8
|
||||||
|
All filter results
|
||||||
|
|
||||||
|
All filter results
|
||||||
|
8
|
||||||
|
|
||||||
|
Lost from filter results
|
||||||
|
0
|
||||||
|
|
||||||
|
Lost
|
||||||
|
0
|
||||||
|
|
||||||
|
Patches
|
||||||
|
|
||||||
|
Changes: Don't show
|
||||||
|
|
||||||
|
Columns
|
||||||
|
|
||||||
|
Export
|
||||||
|
PR
|
||||||
|
URL
|
||||||
|
Organic traffic
|
||||||
|
Is valid Open graph
|
||||||
|
Open graph attributes
|
||||||
|
Open graph values
|
||||||
|
Depth
|
||||||
|
Is indexable page
|
||||||
|
No. of all inlinks
|
||||||
|
24
|
||||||
|
html
|
||||||
|
Free vCard QR Generator: Digital Cards | QR Master
|
||||||
|
https://www.qrmaster.net/blog/vcard-qr-code-generator
|
||||||
|
0
|
||||||
|
No
|
||||||
|
og:type
|
||||||
|
og:image:alt
|
||||||
|
og:image
|
||||||
|
og:description
|
||||||
|
og:title
|
||||||
|
article
|
||||||
|
Professional business card with vCard QR code being scanned by smartphone
|
||||||
|
https://www.qrmaster.net/blog/vcard-qr-code.png
|
||||||
|
Create professional vCard QR codes for digital business cards. Share contact info instantly with a scan—includes templates and best practices.
|
||||||
|
Free vCard QR Generator: Digital Cards
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
8
|
||||||
|
24
|
||||||
|
html
|
||||||
|
Restaurant Menu QR Codes: 2025 Guide | QR Master
|
||||||
|
https://www.qrmaster.net/blog/qr-code-restaurant-menu
|
||||||
|
0
|
||||||
|
No
|
||||||
|
og:type
|
||||||
|
og:image:alt
|
||||||
|
og:image
|
||||||
|
og:description
|
||||||
|
og:title
|
||||||
|
article
|
||||||
|
Restaurant table with QR code menu card and smartphone scanning
|
||||||
|
https://www.qrmaster.net/blog/restaurant-qr-menu.png
|
||||||
|
Step-by-step guide to creating digital menu QR codes for your restaurant. Learn best practices for touchless menus, placement tips, and tracking.
|
||||||
|
Restaurant Menu QR Codes: 2025 Guide
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
8
|
||||||
|
24
|
||||||
|
html
|
||||||
|
QR Code Analytics: The Complete Guide | QR Master
|
||||||
|
https://www.qrmaster.net/blog/qr-code-analytics
|
||||||
|
0
|
||||||
|
No
|
||||||
|
og:type
|
||||||
|
og:image:alt
|
||||||
|
og:image
|
||||||
|
og:description
|
||||||
|
og:title
|
||||||
|
article
|
||||||
|
QR Code Analytics dashboard displaying scan metrics and user data
|
||||||
|
https://www.qrmaster.net/blog/qr-code-analytics-hero.webp
|
||||||
|
Master QR Code Analytics with our complete guide. Learn how to track scans, measure ROI, and optimize your marketing campaigns using real-time data.
|
||||||
|
QR Code Analytics: The Complete Guide
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
8
|
||||||
|
24
|
||||||
|
html
|
||||||
|
Dynamic vs Static QR Codes: The Ultimate Comparison | QR Master
|
||||||
|
https://www.qrmaster.net/blog/dynamic-vs-static-qr-codes
|
||||||
|
0
|
||||||
|
No
|
||||||
|
og:type
|
||||||
|
og:image:alt
|
||||||
|
og:image
|
||||||
|
og:description
|
||||||
|
og:title
|
||||||
|
article
|
||||||
|
Comparison graphic showing features of static versus dynamic QR codes
|
||||||
|
https://www.qrmaster.net/blog/static-vs-dynamic-qr-codes-hero.png
|
||||||
|
Static vs Dynamic QR Codes: Which should you choose? Learn the key differences, pros and cons, and why dynamic codes are better for business.
|
||||||
|
Dynamic vs Static QR Codes: The Ultimate Comparison
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
8
|
||||||
|
24
|
||||||
|
html
|
||||||
|
How to Generate Bulk QR Codes from Excel | QR Master
|
||||||
|
https://www.qrmaster.net/blog/bulk-qr-code-generator-excel
|
||||||
|
0
|
||||||
|
No
|
||||||
|
og:type
|
||||||
|
og:image:alt
|
||||||
|
og:image
|
||||||
|
og:description
|
||||||
|
og:title
|
||||||
|
article
|
||||||
|
Excel spreadsheet being converted into multiple QR codes
|
||||||
|
https://www.qrmaster.net/blog/building-qr-generator.png
|
||||||
|
Generate hundreds of QR codes from Excel or CSV files in minutes. Step-by-step guide with templates, best practices, and free tools.
|
||||||
|
How to Generate Bulk QR Codes from Excel
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
8
|
||||||
|
24
|
||||||
|
html
|
||||||
|
QR Code Print Size Guide: Minimum Sizes for Every Use Case | QR Master
|
||||||
|
https://www.qrmaster.net/blog/qr-code-print-size-guide
|
||||||
|
0
|
||||||
|
No
|
||||||
|
og:type
|
||||||
|
og:image:alt
|
||||||
|
og:image
|
||||||
|
og:description
|
||||||
|
og:title
|
||||||
|
article
|
||||||
|
Various print materials showing different QR code sizes
|
||||||
|
https://www.qrmaster.net/blog/qr-print-sizes.png
|
||||||
|
Complete guide to QR code print sizes. Learn minimum dimensions for business cards, posters, banners, and more to ensure reliable scanning.
|
||||||
|
QR Code Print Size Guide: Minimum Sizes for Every Use Case
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
8
|
||||||
|
24
|
||||||
|
html
|
||||||
|
Best QR Code Generator for Small Business 2025 | QR Master
|
||||||
|
https://www.qrmaster.net/blog/qr-code-small-business
|
||||||
|
0
|
||||||
|
No
|
||||||
|
og:type
|
||||||
|
og:image:alt
|
||||||
|
og:image
|
||||||
|
og:description
|
||||||
|
og:title
|
||||||
|
article
|
||||||
|
Small business owner using QR codes for customer engagement
|
||||||
|
https://www.qrmaster.net/blog/small-business-qr.png
|
||||||
|
Find the best QR code solution for your small business. Compare features, pricing, and use cases for marketing, payments, and operations.
|
||||||
|
Best QR Code Generator for Small Business 2025
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
8
|
||||||
|
24
|
||||||
|
html
|
||||||
|
QR Code Tracking: Complete Guide 2025 | QR Master
|
||||||
|
https://www.qrmaster.net/blog/qr-code-tracking-guide-2025
|
||||||
|
0
|
||||||
|
No
|
||||||
|
og:type
|
||||||
|
og:image:alt
|
||||||
|
og:image
|
||||||
|
og:description
|
||||||
|
og:title
|
||||||
|
article
|
||||||
|
QR Code Tracking and analytics dashboard visualization
|
||||||
|
https://www.qrmaster.net/blog/qr-code-tracking-guide-hero.webp
|
||||||
|
The complete guide to QR Code Tracking in 2025. Learn how to track scans, measure ROI, and optimize your marketing campaigns.
|
||||||
|
QR Code Tracking: Complete Guide 2025
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
8
|
||||||
|
Showing 8 of 8
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Issues
|
||||||
|
/
|
||||||
|
Pages to submit to IndexNow
|
||||||
|
|
||||||
|
Why and how to fix
|
||||||
|
|
||||||
|
Submit to IndexNow
|
||||||
|
|
||||||
|
Create new issue
|
||||||
|
|
||||||
|
All URLs
|
||||||
|
|
||||||
|
Pages
|
||||||
|
|
||||||
|
Resources
|
||||||
|
|
||||||
|
Content
|
||||||
|
|
||||||
|
Links
|
||||||
|
|
||||||
|
Redirects
|
||||||
|
|
||||||
|
Indexability
|
||||||
|
|
||||||
|
Sitemaps
|
||||||
|
|
||||||
|
Ahrefs metrics
|
||||||
|
Word or phrase
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
Advanced filter
|
||||||
|
Crawl history
|
||||||
|
Hide chart
|
||||||
|
12 Jan
|
||||||
|
13 Jan
|
||||||
|
13 Jan
|
||||||
|
14 Jan
|
||||||
|
14 Jan
|
||||||
|
15 Jan
|
||||||
|
0
|
||||||
|
9
|
||||||
|
18
|
||||||
|
27
|
||||||
|
36
|
||||||
|
All filter results
|
||||||
|
|
||||||
|
All filter results
|
||||||
|
12
|
||||||
|
|
||||||
|
Lost from filter results
|
||||||
|
|
||||||
|
Lost
|
||||||
|
|
||||||
|
Patches: Show all
|
||||||
|
|
||||||
|
Changes: Absolute
|
||||||
|
|
||||||
|
Columns
|
||||||
|
|
||||||
|
Export
|
||||||
|
PR
|
||||||
|
URL
|
||||||
|
Organic traffic
|
||||||
|
Changes
|
||||||
|
HTTP status code
|
||||||
|
Content type
|
||||||
|
Is indexable page
|
||||||
|
Title
|
||||||
|
Patch it
|
||||||
|
|
||||||
|
Batch AI
|
||||||
|
Meta description
|
||||||
|
Patch it
|
||||||
|
|
||||||
|
Batch AI
|
||||||
|
H1
|
||||||
|
H2
|
||||||
|
No. of content words
|
||||||
|
Changes
|
||||||
|
No. of internal outlinks
|
||||||
|
Changes
|
||||||
|
No. of external outlinks
|
||||||
|
Changes
|
||||||
|
Page text
|
||||||
|
First found at
|
||||||
|
40
|
||||||
|
html
|
||||||
|
QR Master: Dynamic QR Generator
|
||||||
|
https://www.qrmaster.net/
|
||||||
|
0
|
||||||
|
200
|
||||||
|
text/html; charset=utf-8
|
||||||
|
Yes
|
||||||
|
QR Master: Dynamic QR Generator
|
||||||
|
Enter new title
|
||||||
|
Create professional QR codes with QR Master. Dynamic QR with tracking, bulk generation, custom branding, and real-time analytics for all your campaigns.
|
||||||
|
Enter new meta description
|
||||||
|
QR Master: Dynamic QR Code Generator with Analytics
|
||||||
|
Create QR Codes That Work Everywhere
|
||||||
|
Create QR Codes That Work Everywhere
|
||||||
|
Instant QR Code Generator
|
||||||
|
The Future of QR Codes is AI-Powered
|
||||||
|
More Free QR Code Tools
|
||||||
|
Why Dynamic QR Codes Save You Money
|
||||||
|
All 8
|
||||||
|
777
|
||||||
|
29
|
||||||
|
0
|
||||||
|
View text
|
||||||
|
5 KB
|
||||||
|
38
|
||||||
|
html
|
||||||
|
QR Insights: Latest QR Strategies | QR Master
|
||||||
|
https://www.qrmaster.net/blog
|
||||||
|
0
|
||||||
|
200
|
||||||
|
text/html; charset=utf-8
|
||||||
|
Yes
|
||||||
|
QR Insights: Latest QR Strategies | QR Master
|
||||||
|
Enter new title
|
||||||
|
Expert guides on QR code analytics, dynamic vs static codes, bulk generation, and smart marketing use cases. Learn how to maximize your QR campaign ROI.
|
||||||
|
Enter new meta description
|
||||||
|
QR Code Insights
|
||||||
|
481
|
||||||
|
495
|
||||||
|
−14
|
||||||
|
37
|
||||||
|
0
|
||||||
|
View changes
|
||||||
|
3 KB
|
||||||
|
3 KB
|
||||||
|
38
|
||||||
|
html
|
||||||
|
Pricing Plans | QR Master
|
||||||
|
https://www.qrmaster.net/pricing
|
||||||
|
0
|
||||||
|
200
|
||||||
|
text/html; charset=utf-8
|
||||||
|
Yes
|
||||||
|
Pricing Plans | QR Master
|
||||||
|
Enter new title
|
||||||
|
Choose the perfect QR code plan for your needs. Free, Pro, and Business plans with dynamic QR codes, analytics, bulk generation, and custom branding.
|
||||||
|
Enter new meta description
|
||||||
|
QR Master Pricing – Choose Your QR Code Plan
|
||||||
|
Choose Your Plan
|
||||||
|
Compare our plans
|
||||||
|
Choose Your Plan
|
||||||
|
271
|
||||||
|
29
|
||||||
|
30
|
||||||
|
−1
|
||||||
|
0
|
||||||
|
View text
|
||||||
|
2 KB
|
||||||
|
38
|
||||||
|
html
|
||||||
|
QR Code Erstellen – Kostenlos | QR Master
|
||||||
|
https://www.qrmaster.net/qr-code-erstellen
|
||||||
|
0
|
||||||
|
200
|
||||||
|
text/html; charset=utf-8
|
||||||
|
Yes
|
||||||
|
QR Code Erstellen – Kostenlos | QR Master
|
||||||
|
Enter new title
|
||||||
|
Erstellen Sie QR Codes kostenlos in Sekunden. Dynamische QR-Codes mit Tracking, Branding und Massen-Erstellung. Für immer kostenlos.
|
||||||
|
Enter new meta description
|
||||||
|
QR Code Erstellen – Kostenloser QR Code Generator mit Tracking
|
||||||
|
Erstellen Sie QR-Codes, die überall funktionieren
|
||||||
|
Erstellen Sie QR-Codes, die überall funktionieren
|
||||||
|
Sofortiger QR-Code-Generator
|
||||||
|
Warum dynamische QR-Codes Geld sparen
|
||||||
|
Alles was Sie brauchen, um professionelle QR-Codes zu erstellen
|
||||||
|
Wählen Sie Ihren Plan
|
||||||
|
All 6
|
||||||
|
554
|
||||||
|
29
|
||||||
|
0
|
||||||
|
View text
|
||||||
|
4 KB
|
||||||
|
24
|
||||||
|
html
|
||||||
|
Free vCard QR Generator: Digital Cards | QR Master
|
||||||
|
https://www.qrmaster.net/blog/vcard-qr-code-generator
|
||||||
|
0
|
||||||
|
200
|
||||||
|
text/html; charset=utf-8
|
||||||
|
Yes
|
||||||
|
Free vCard QR Generator: Digital Cards | QR Master
|
||||||
|
Enter new title
|
||||||
|
Create professional vCard QR codes for digital business cards. Share contact info instantly with a scan—includes templates and best practices.
|
||||||
|
Enter new meta description
|
||||||
|
Free vCard QR Generator: Digital Cards
|
||||||
|
Quick Answer
|
||||||
|
What is a vCard QR Code?
|
||||||
|
Why Use a Digital Business Card QR Code?
|
||||||
|
Information You Can Include in a vCard
|
||||||
|
Static vs Dynamic vCard QR Codes
|
||||||
|
All 13
|
||||||
|
1,135
|
||||||
|
1,149
|
||||||
|
−14
|
||||||
|
37
|
||||||
|
0
|
||||||
|
View changes
|
||||||
|
7 KB
|
||||||
|
7 KB
|
||||||
|
24
|
||||||
|
html
|
||||||
|
Restaurant Menu QR Codes: 2025 Guide | QR Master
|
||||||
|
https://www.qrmaster.net/blog/qr-code-restaurant-menu
|
||||||
|
0
|
||||||
|
200
|
||||||
|
text/html; charset=utf-8
|
||||||
|
Yes
|
||||||
|
Restaurant Menu QR Codes: 2025 Guide | QR Master
|
||||||
|
Enter new title
|
||||||
|
Step-by-step guide to creating digital menu QR codes for your restaurant. Learn best practices for touchless menus, placement tips, and tracking.
|
||||||
|
Enter new meta description
|
||||||
|
Restaurant Menu QR Codes: 2025 Guide
|
||||||
|
Quick Answer
|
||||||
|
Why Restaurants Need QR Code Menus in 2025
|
||||||
|
Step 1: Prepare Your Digital Menu
|
||||||
|
Step 2: Create Your QR Code with QR Master
|
||||||
|
Step 3: Customize Your Restaurant QR Code
|
||||||
|
All 13
|
||||||
|
1,242
|
||||||
|
1,256
|
||||||
|
−14
|
||||||
|
38
|
||||||
|
0
|
||||||
|
View changes
|
||||||
|
8 KB
|
||||||
|
8 KB
|
||||||
|
24
|
||||||
|
html
|
||||||
|
QR Code Analytics: The Complete Guide | QR Master
|
||||||
|
https://www.qrmaster.net/blog/qr-code-analytics
|
||||||
|
0
|
||||||
|
200
|
||||||
|
text/html; charset=utf-8
|
||||||
|
Yes
|
||||||
|
QR Code Analytics: The Complete Guide | QR Master
|
||||||
|
Enter new title
|
||||||
|
Master QR Code Analytics with our complete guide. Learn how to track scans, measure ROI, and optimize your marketing campaigns using real-time data.
|
||||||
|
Master QR Code Analytics with our complete guide. Learn how to track scans, measure ROI, and optimize your marketing campaigns using real-time data and insights.
|
||||||
|
Enter new meta description
|
||||||
|
QR Code Analytics: The Complete Guide
|
||||||
|
Quick Answer
|
||||||
|
What Are Scan Analytics?
|
||||||
|
How to Set Up QR Code Analytics
|
||||||
|
Key Metrics in QR Code Analytics
|
||||||
|
Advanced Campaign Tracking Strategies
|
||||||
|
All 12
|
||||||
|
1,526
|
||||||
|
1,538
|
||||||
|
−12
|
||||||
|
37
|
||||||
|
0
|
||||||
|
View changes
|
||||||
|
10 KB
|
||||||
|
10 KB
|
||||||
|
24
|
||||||
|
html
|
||||||
|
Dynamic vs Static QR Codes: The Ultimate Comparison | QR Master
|
||||||
|
https://www.qrmaster.net/blog/dynamic-vs-static-qr-codes
|
||||||
|
0
|
||||||
|
200
|
||||||
|
text/html; charset=utf-8
|
||||||
|
Yes
|
||||||
|
Dynamic vs Static QR Codes: The Ultimate Comparison | QR Master
|
||||||
|
Enter new title
|
||||||
|
Static vs Dynamic QR Codes: Which should you choose? Learn the key differences, pros and cons, and why dynamic codes are better for business.
|
||||||
|
Static vs Dynamic QR Codes: Which one should you choose? Learn the key differences, pros and cons, and why dynamic QR codes are the better choice for business and marketing.
|
||||||
|
Enter new meta description
|
||||||
|
Dynamic vs Static QR Codes: The Ultimate Comparison
|
||||||
|
Quick Answer
|
||||||
|
What is a Static QR Code?
|
||||||
|
What is a Dynamic QR Code?
|
||||||
|
Direct Comparison: Static vs Dynamic
|
||||||
|
Why Dynamic QR Codes Are Better for Business
|
||||||
|
All 10
|
||||||
|
1,074
|
||||||
|
1,082
|
||||||
|
−8
|
||||||
|
37
|
||||||
|
0
|
||||||
|
View changes
|
||||||
|
7 KB
|
||||||
|
7 KB
|
||||||
|
24
|
||||||
|
html
|
||||||
|
How to Generate Bulk QR Codes from Excel | QR Master
|
||||||
|
https://www.qrmaster.net/blog/bulk-qr-code-generator-excel
|
||||||
|
0
|
||||||
|
200
|
||||||
|
text/html; charset=utf-8
|
||||||
|
Yes
|
||||||
|
How to Generate Bulk QR Codes from Excel | QR Master
|
||||||
|
Enter new title
|
||||||
|
Generate hundreds of QR codes from Excel or CSV files in minutes. Step-by-step guide with templates, best practices, and free tools.
|
||||||
|
Enter new meta description
|
||||||
|
How to Generate Bulk QR Codes from Excel
|
||||||
|
Quick Answer
|
||||||
|
How Bulk QR Code Generation Works
|
||||||
|
Step-by-Step Guide: Excel to QR Codes
|
||||||
|
Use Cases for Bulk QR Codes
|
||||||
|
Free vs Paid Bulk QR Tools
|
||||||
|
All 12
|
||||||
|
1,882
|
||||||
|
1,896
|
||||||
|
−14
|
||||||
|
37
|
||||||
|
1
|
||||||
|
View changes
|
||||||
|
12 KB
|
||||||
|
13 KB
|
||||||
|
24
|
||||||
|
html
|
||||||
|
QR Code Print Size Guide: Minimum Sizes for Every Use Case | QR Master
|
||||||
|
https://www.qrmaster.net/blog/qr-code-print-size-guide
|
||||||
|
0
|
||||||
|
200
|
||||||
|
text/html; charset=utf-8
|
||||||
|
Yes
|
||||||
|
QR Code Print Size Guide: Minimum Sizes for Every Use Case | QR Master
|
||||||
|
Enter new title
|
||||||
|
Complete guide to QR code print sizes. Learn minimum dimensions for business cards, posters, banners, and more to ensure reliable scanning.
|
||||||
|
Enter new meta description
|
||||||
|
QR Code Print Size Guide: Minimum Sizes for Every Use Case
|
||||||
|
Quick Answer
|
||||||
|
Why QR Code Size Matters
|
||||||
|
The Scanning Distance Formula
|
||||||
|
QR Code Sizes by Application
|
||||||
|
Factors Affecting Scanability
|
||||||
|
All 12
|
||||||
|
948
|
||||||
|
962
|
||||||
|
−14
|
||||||
|
37
|
||||||
|
0
|
||||||
|
View changes
|
||||||
|
6 KB
|
||||||
|
6 KB
|
||||||
|
24
|
||||||
|
html
|
||||||
|
Best QR Code Generator for Small Business 2025 | QR Master
|
||||||
|
https://www.qrmaster.net/blog/qr-code-small-business
|
||||||
|
0
|
||||||
|
200
|
||||||
|
text/html; charset=utf-8
|
||||||
|
Yes
|
||||||
|
Best QR Code Generator for Small Business 2025 | QR Master
|
||||||
|
Enter new title
|
||||||
|
Find the best QR code solution for your small business. Compare features, pricing, and use cases for marketing, payments, and operations.
|
||||||
|
Enter new meta description
|
||||||
|
Best QR Code Generator for Small Business 2025
|
||||||
|
Quick Answer
|
||||||
|
Why Small Businesses Need QR Codes
|
||||||
|
Top 10 QR Code Use Cases for Small Business
|
||||||
|
What to Look for in a Small Business QR Solution
|
||||||
|
QR Master for Small Business
|
||||||
|
All 11
|
||||||
|
1,034
|
||||||
|
1,048
|
||||||
|
−14
|
||||||
|
37
|
||||||
|
0
|
||||||
|
View changes
|
||||||
|
7 KB
|
||||||
|
7 KB
|
||||||
|
24
|
||||||
|
html
|
||||||
|
QR Code Tracking: Complete Guide 2025 | QR Master
|
||||||
|
https://www.qrmaster.net/blog/qr-code-tracking-guide-2025
|
||||||
|
0
|
||||||
|
200
|
||||||
|
text/html; charset=utf-8
|
||||||
|
Yes
|
||||||
|
QR Code Tracking: Complete Guide 2025 | QR Master
|
||||||
|
Enter new title
|
||||||
|
The complete guide to QR Code Tracking in 2025. Learn how to track scans, measure ROI, and optimize your marketing campaigns.
|
||||||
|
The complete guide to QR Code Tracking in 2025. Learn how to track scans, measure ROI with analytics tools, and optimize your marketing campaigns for maximum engagement.
|
||||||
|
Enter new meta description
|
||||||
|
QR Code Tracking: Complete Guide 2025
|
||||||
|
Quick Answer
|
||||||
|
What is QR Code Tracking?
|
||||||
|
Why Track QR Codes? Key Benefits
|
||||||
|
How to Track QR Code Scans: 4 Methods
|
||||||
|
QR Code Tracking Tools Comparison
|
||||||
|
All 15
|
||||||
|
2,959
|
||||||
|
2,967
|
||||||
|
−8
|
||||||
|
38
|
||||||
|
1
|
||||||
|
View changes
|
||||||
|
19 KB
|
||||||
|
19 KB
|
||||||
|
Showing 12 of 12
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
/** @type {import('next-sitemap').IConfig} */
|
|
||||||
module.exports = {
|
|
||||||
siteUrl: 'https://www.qrmaster.net',
|
|
||||||
generateRobotsTxt: true,
|
|
||||||
robotsTxtOptions: {
|
|
||||||
policies: [
|
|
||||||
{
|
|
||||||
userAgent: '*',
|
|
||||||
allow: '/',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
transform: async (config, path) => {
|
|
||||||
// Custom priority and changefreq based on path
|
|
||||||
let priority = 0.7;
|
|
||||||
let changefreq = 'weekly';
|
|
||||||
|
|
||||||
if (path === '/') {
|
|
||||||
priority = 0.9;
|
|
||||||
changefreq = 'daily';
|
|
||||||
} else if (path === '/blog') {
|
|
||||||
priority = 0.7;
|
|
||||||
changefreq = 'daily';
|
|
||||||
} else if (path === '/pricing') {
|
|
||||||
priority = 0.8;
|
|
||||||
changefreq = 'weekly';
|
|
||||||
} else if (path === '/faq') {
|
|
||||||
priority = 0.6;
|
|
||||||
changefreq = 'weekly';
|
|
||||||
} else if (path.startsWith('/blog/')) {
|
|
||||||
priority = 0.6;
|
|
||||||
changefreq = 'weekly';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
loc: path,
|
|
||||||
changefreq,
|
|
||||||
priority,
|
|
||||||
lastmod: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -20,6 +20,16 @@ const nextConfig = {
|
|||||||
pagesBufferLength: 2,
|
pagesBufferLength: 2,
|
||||||
},
|
},
|
||||||
poweredByHeader: false,
|
poweredByHeader: false,
|
||||||
|
async redirects() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/blog/bulk-qr-codes-excel',
|
||||||
|
destination: '/blog/bulk-qr-code-generator-excel',
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
68
next_blog_post.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# SEO Setup (Copy these into the tool)
|
||||||
|
|
||||||
|
**Focus Keyword:** Best QR Code Generator 2026
|
||||||
|
**Page Title:** Best QR Code Generator 2026: Ultimate Guide (Dynamic & AI)
|
||||||
|
**Meta Description:** Discover standards for the best QR code generator in 2026. Learn why dynamic QR codes, AI analytics, and unlimited scans are essential for your business growth.
|
||||||
|
|
||||||
|
**Related Keywords:**
|
||||||
|
1. free dynamic qr code generator
|
||||||
|
2. qr code tracking analytics
|
||||||
|
3. edit qr code after printing
|
||||||
|
4. unlimited scan qr code
|
||||||
|
5. vector qr code svg
|
||||||
|
6. custom brand qr code
|
||||||
|
7. bulk qr code generator
|
||||||
|
8. gdpr compliant qr code
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Article Content
|
||||||
|
|
||||||
|
# Best QR Code Generator 2026: The Ultimate Guide
|
||||||
|
|
||||||
|
The digital landscape has transformed, and finding the **Best QR Code Generator 2026** is critical for businesses connecting with customers. The humble QR code has evolved into a sophisticated marketing instrument. To stay competitive, your chosen platform must offer more than just links—it must unlock data, flexibility, and brand engagement.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
In this guide, we explore why static codes are dead and why top-tier tools now rely entirely on dynamic technology.
|
||||||
|
|
||||||
|
## Why Dynamic QR Codes Are Non-Negotiable
|
||||||
|
|
||||||
|
If you are not using a modern solution, you might still be stuck with static codes. The industry standard has shifted entirely to **dynamic QR codes** for critical reasons:
|
||||||
|
|
||||||
|
1. **Editability**: Printed 5,000 brochures with the wrong link? A dynamic platform lets you update the destination URL in seconds.
|
||||||
|
2. **Tracking & Analytics**: You need to know *who* scanned and *when*.
|
||||||
|
3. **Retargeting**: Integration with [Google Analytics](https://www.qrmaster.net/analytics) allows you to build audiences.
|
||||||
|
|
||||||
|
### Static vs. Dynamic: The 2026 Verdict
|
||||||
|
|
||||||
|
| Feature | Static QR Code | Best QR Code Generator 2026 (Dynamic) |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Editing** | Impossible | Instant updates anytime |
|
||||||
|
| **Analytics** | None | Real-time AI Data |
|
||||||
|
| **Lifespan** | Until link breaks | Indefinite |
|
||||||
|
|
||||||
|
## Top Trends Defining the Market
|
||||||
|
|
||||||
|
### 1. AI-Driven Scan Prediction
|
||||||
|
Leading platforms integrates Artificial Intelligence to predict peak scan times. By analyzing historical data, platforms like [QR Master](https://www.qrmaster.net/) suggest optimal placement.
|
||||||
|
|
||||||
|
### 2. Augmented Reality (AR) Integration
|
||||||
|
New codes trigger immersive AR experiences. The **Best QR Code Generator 2026** supports these next-gen formats natively, allowing customers to visualize products immediately.
|
||||||
|
|
||||||
|
### 3. Hyper-Personalization
|
||||||
|
Contextual redirects are a hallmark of advanced generators. Redirect users in Berlin to German pages and New York users to US pages automatically, ensuring the highest possible conversion rate.
|
||||||
|
|
||||||
|
## How to Choose the Right Tool
|
||||||
|
|
||||||
|
With many tools available, how do you verify which is the right one for you?
|
||||||
|
|
||||||
|
* **No Scan Limits**: Many services cap you at 100 scans. Ensure your provider offers [unlimited scans](https://www.qrmaster.net/pricing).
|
||||||
|
* **Vector Formats**: Essential for professional printing (SVG/EPS).
|
||||||
|
* **GDPR Compliance**: Data privacy is paramount.
|
||||||
|
|
||||||
|
## Conclusion: Future-Proof Your Marketing
|
||||||
|
|
||||||
|
As we move through the year, selecting the **Best QR Code Generator 2026** is the highest ROI decision you can make. Don't settle for temporary solutions. Choose a platform that scales with your ambition.
|
||||||
|
|
||||||
|
*Ready to upgrade? Start creating with the industry leader today: [Sign Up Free](https://www.qrmaster.net/signup).*
|
||||||
674
package-lock.json
generated
@@ -6,8 +6,10 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3050",
|
"dev": "next dev -p 3050",
|
||||||
"build": "prisma generate && next build",
|
"build": "prisma generate && next build",
|
||||||
|
"submit:indexnow": "tsx scripts/submit-indexnow.ts",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
|
"indexnow": "tsx scripts/submit-indexnow.ts",
|
||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:migrate": "prisma migrate dev",
|
"db:migrate": "prisma migrate dev",
|
||||||
"db:deploy": "prisma migrate deploy",
|
"db:deploy": "prisma migrate deploy",
|
||||||
@@ -30,17 +32,20 @@
|
|||||||
"@prisma/client": "^5.7.0",
|
"@prisma/client": "^5.7.0",
|
||||||
"@stripe/stripe-js": "^8.0.0",
|
"@stripe/stripe-js": "^8.0.0",
|
||||||
"@types/d3-scale": "^4.0.9",
|
"@types/d3-scale": "^4.0.9",
|
||||||
|
"axios": "^1.13.2",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"chart.js": "^4.4.0",
|
"chart.js": "^4.4.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"d3-scale": "^4.0.2",
|
"d3-scale": "^4.0.2",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"framer-motion": "^12.24.10",
|
"framer-motion": "^12.24.10",
|
||||||
"html-to-image": "^1.11.13",
|
"html-to-image": "^1.11.13",
|
||||||
"i18next": "^23.7.6",
|
"i18next": "^23.7.6",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
|
"jspdf": "^4.0.0",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next": "^14.2.35",
|
"next": "^14.2.35",
|
||||||
@@ -57,7 +62,6 @@
|
|||||||
"react-i18next": "^13.5.0",
|
"react-i18next": "^13.5.0",
|
||||||
"react-simple-maps": "^3.0.0",
|
"react-simple-maps": "^3.0.0",
|
||||||
"resend": "^6.4.2",
|
"resend": "^6.4.2",
|
||||||
"sharp": "^0.33.1",
|
|
||||||
"stripe": "^19.1.0",
|
"stripe": "^19.1.0",
|
||||||
"tailwind-merge": "^2.2.0",
|
"tailwind-merge": "^2.2.0",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
@@ -78,6 +82,7 @@
|
|||||||
"postcss": "^8.4.32",
|
"postcss": "^8.4.32",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
"prisma": "^5.7.0",
|
"prisma": "^5.7.0",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
"tailwindcss": "^3.3.6",
|
"tailwindcss": "^3.3.6",
|
||||||
"tsx": "^4.7.0",
|
"tsx": "^4.7.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
@@ -85,4 +90,4 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
3
prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "postgresql"
|
||||||
@@ -161,4 +161,18 @@ model NewsletterSubscription {
|
|||||||
|
|
||||||
@@index([email])
|
@@index([email])
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Lead {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
email String
|
||||||
|
source String @default("reprint-calculator")
|
||||||
|
reprintCost Float?
|
||||||
|
updatesPerYear Int?
|
||||||
|
annualSavings Float?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([email])
|
||||||
|
@@index([createdAt])
|
||||||
|
@@index([source])
|
||||||
}
|
}
|
||||||
4
public/.well-known/security.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Contact: mailto:security@qrmaster.net
|
||||||
|
Expires: 2027-01-01T00:00:00.000Z
|
||||||
|
Strategies: https://www.qrmaster.net/.well-known/security.txt
|
||||||
|
Preferred-Languages: en, de
|
||||||
BIN
public/1234567890abcdef.txt
Normal file
1
public/bb6dfaacf1ed41a880281c426c54ed7c.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
bb6dfaacf1ed41a880281c426c54ed7c
|
||||||
|
Before Width: | Height: | Size: 4.2 MiB |
|
Before Width: | Height: | Size: 2.8 MiB |
BIN
public/blog/1-hero.webp
Normal file
|
After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 4.1 MiB |
BIN
public/blog/2-body.webp
Normal file
|
After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 3.8 MiB |
BIN
public/blog/2-hero.webp
Normal file
|
After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 5.5 MiB |
|
Before Width: | Height: | Size: 4.6 MiB |
|
Before Width: | Height: | Size: 5.8 MiB |
|
Before Width: | Height: | Size: 3.7 MiB |
BIN
public/blog/building-qr-generator.png
Normal file
|
After Width: | Height: | Size: 737 KiB |
BIN
public/blog/qr-code-analytics-dashboard.png
Normal file
|
After Width: | Height: | Size: 350 KiB |
BIN
public/blog/qr-code-analytics-hero.webp
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
public/blog/qr-code-tracking-guide-body.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
public/blog/qr-code-tracking-guide-hero.webp
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
public/blog/qr_master_cover.png
Normal file
|
After Width: | Height: | Size: 545 KiB |
BIN
public/blog/qr_master_profile.png
Normal file
|
After Width: | Height: | Size: 440 KiB |
BIN
public/blog/static-vs-dynamic-qr-codes-body.png
Normal file
|
After Width: | Height: | Size: 320 KiB |
BIN
public/blog/static-vs-dynamic-qr-codes-hero.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
public/blog/sustainable-packaging-qr.png
Normal file
|
After Width: | Height: | Size: 726 KiB |
1
public/googleccd5315437d68a49.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
google-site-verification: googleccd5315437d68a49.html
|
||||||
13
public/humans.txt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/* TEAM */
|
||||||
|
Founder: Timo Knuth
|
||||||
|
Site: https://qrmaster.net
|
||||||
|
Twitter: @qrmaster
|
||||||
|
|
||||||
|
/* THANKS */
|
||||||
|
Thanks to: Next.js, Vercel, Tailwind CSS, Stripe, Supabase
|
||||||
|
|
||||||
|
/* SITE */
|
||||||
|
Last update: 2026/01/12
|
||||||
|
Language: English, German
|
||||||
|
Doctype: HTML5
|
||||||
|
IDE: VS Code
|
||||||
48
public/llms.txt
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# QR Master
|
||||||
|
|
||||||
|
> QR Master is a B2B SaaS platform for creating dynamic QR codes with real-time analytics, custom branding, and bulk generation. Free tools available for URL, WiFi, vCard, WhatsApp, Instagram, and 15+ other QR code types.
|
||||||
|
|
||||||
|
- Primary domain: https://www.qrmaster.net
|
||||||
|
- Free static QR codes, paid dynamic QR codes with tracking
|
||||||
|
- German landing page available at /qr-code-erstellen
|
||||||
|
- Enterprise features: Bulk generation, API access, team management
|
||||||
|
|
||||||
|
## Free Tools
|
||||||
|
|
||||||
|
- [URL QR Generator](https://www.qrmaster.net/tools/url-qr-code): Create QR codes for any website link
|
||||||
|
- [WiFi QR Generator](https://www.qrmaster.net/tools/wifi-qr-code): Share WiFi credentials via QR code
|
||||||
|
- [vCard QR Generator](https://www.qrmaster.net/tools/vcard-qr-code): Digital business card QR codes
|
||||||
|
- [Text QR Generator](https://www.qrmaster.net/tools/text-qr-code): Encode plain text in QR codes
|
||||||
|
- [Email QR Generator](https://www.qrmaster.net/tools/email-qr-code): Pre-filled email QR codes
|
||||||
|
- [SMS QR Generator](https://www.qrmaster.net/tools/sms-qr-code): Send SMS messages via QR
|
||||||
|
- [Phone QR Generator](https://www.qrmaster.net/tools/phone-qr-code): One-tap phone call QR codes
|
||||||
|
- [WhatsApp QR Generator](https://www.qrmaster.net/tools/whatsapp-qr-code): Start WhatsApp chats instantly
|
||||||
|
- [Instagram QR Generator](https://www.qrmaster.net/tools/instagram-qr-code): Grow Instagram followers
|
||||||
|
- [TikTok QR Generator](https://www.qrmaster.net/tools/tiktok-qr-code): Link to TikTok profiles
|
||||||
|
- [Twitter QR Generator](https://www.qrmaster.net/tools/twitter-qr-code): Share Twitter/X profiles
|
||||||
|
- [YouTube QR Generator](https://www.qrmaster.net/tools/youtube-qr-code): Link to videos and channels
|
||||||
|
- [Facebook QR Generator](https://www.qrmaster.net/tools/facebook-qr-code): Share Facebook pages
|
||||||
|
- [PayPal QR Generator](https://www.qrmaster.net/tools/paypal-qr-code): Accept PayPal payments
|
||||||
|
- [Crypto QR Generator](https://www.qrmaster.net/tools/crypto-qr-code): Bitcoin and crypto wallet QR codes
|
||||||
|
- [Event QR Generator](https://www.qrmaster.net/tools/event-qr-code): Calendar event QR codes
|
||||||
|
- [Geolocation QR Generator](https://www.qrmaster.net/tools/geolocation-qr-code): Share map locations
|
||||||
|
- [Zoom QR Generator](https://www.qrmaster.net/tools/zoom-qr-code): Join Zoom meetings instantly
|
||||||
|
- [Teams QR Generator](https://www.qrmaster.net/tools/teams-qr-code): Join Microsoft Teams meetings
|
||||||
|
|
||||||
|
## Premium Features
|
||||||
|
|
||||||
|
- [Dynamic QR Codes](https://www.qrmaster.net/dynamic-qr-code-generator): Editable QR codes with real-time tracking
|
||||||
|
- [Bulk QR Generator](https://www.qrmaster.net/bulk-qr-code-generator): Generate hundreds of QR codes from CSV/Excel
|
||||||
|
- [QR Code Tracking](https://www.qrmaster.net/qr-code-tracking): Analytics dashboard with scan statistics
|
||||||
|
|
||||||
|
## Information
|
||||||
|
|
||||||
|
- [Homepage](https://www.qrmaster.net): Main landing page
|
||||||
|
- [Pricing](https://www.qrmaster.net/pricing): Free, Pro, and Business plans
|
||||||
|
- [FAQ](https://www.qrmaster.net/faq): Frequently asked questions
|
||||||
|
- [Blog](https://www.qrmaster.net/blog): Tips and guides for QR code marketing
|
||||||
|
- [Privacy Policy](https://www.qrmaster.net/privacy): Data privacy information
|
||||||
|
|
||||||
|
## Localized Pages
|
||||||
|
|
||||||
|
- [German Landing Page](https://www.qrmaster.net/qr-code-erstellen): QR Code Generator auf Deutsch
|
||||||
BIN
public/og-image.png
Normal file
|
After Width: | Height: | Size: 464 KiB |
@@ -1,19 +0,0 @@
|
|||||||
# QR Master - robots.txt
|
|
||||||
# Allow all search engines to crawl all pages
|
|
||||||
|
|
||||||
User-agent: *
|
|
||||||
Allow: /
|
|
||||||
|
|
||||||
# Sitemap location
|
|
||||||
Sitemap: https://www.qrmaster.net/sitemap.xml
|
|
||||||
|
|
||||||
# Crawl-delay (optional, be nice to servers)
|
|
||||||
Crawl-delay: 1
|
|
||||||
|
|
||||||
# Disallow admin/api routes
|
|
||||||
Disallow: /api/
|
|
||||||
Disallow: /dashboard/
|
|
||||||
Disallow: /_next/
|
|
||||||
|
|
||||||
# Allow all free tools explicitly
|
|
||||||
Allow: /tools/
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
||||||
<url>
|
|
||||||
<loc>https://www.qrmaster.net/</loc>
|
|
||||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
|
||||||
<changefreq>daily</changefreq>
|
|
||||||
<priority>0.9</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://www.qrmaster.net/blog</loc>
|
|
||||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
|
||||||
<changefreq>daily</changefreq>
|
|
||||||
<priority>0.7</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://www.qrmaster.net/pricing</loc>
|
|
||||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
|
||||||
<changefreq>weekly</changefreq>
|
|
||||||
<priority>0.8</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://www.qrmaster.net/faq</loc>
|
|
||||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
|
||||||
<changefreq>weekly</changefreq>
|
|
||||||
<priority>0.6</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://www.qrmaster.net/blog/qr-code-analytics</loc>
|
|
||||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
|
||||||
<changefreq>weekly</changefreq>
|
|
||||||
<priority>0.6</priority>
|
|
||||||
</url>
|
|
||||||
</urlset>
|
|
||||||
49
scripts/compress-images.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
const sharp = require('sharp');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const imagesToConvert = [
|
||||||
|
'2-body.png',
|
||||||
|
'2-hero.png',
|
||||||
|
'qr-code-analytics-hero.png',
|
||||||
|
'1-hero.png'
|
||||||
|
];
|
||||||
|
|
||||||
|
const blogDir = path.join(__dirname, '../public/blog');
|
||||||
|
|
||||||
|
async function compressImages() {
|
||||||
|
console.log('🖼️ Starting image compression...\n');
|
||||||
|
|
||||||
|
for (const imageName of imagesToConvert) {
|
||||||
|
const inputPath = path.join(blogDir, imageName);
|
||||||
|
const outputName = imageName.replace('.png', '.webp');
|
||||||
|
const outputPath = path.join(blogDir, outputName);
|
||||||
|
|
||||||
|
if (!fs.existsSync(inputPath)) {
|
||||||
|
console.log(`⚠️ Skipping ${imageName} - file not found`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalSize = fs.statSync(inputPath).size;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sharp(inputPath)
|
||||||
|
.webp({ quality: 85 })
|
||||||
|
.toFile(outputPath);
|
||||||
|
|
||||||
|
const newSize = fs.statSync(outputPath).size;
|
||||||
|
const savings = ((1 - newSize / originalSize) * 100).toFixed(1);
|
||||||
|
|
||||||
|
console.log(`✅ ${imageName}`);
|
||||||
|
console.log(` Original: ${(originalSize / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
console.log(` WebP: ${(newSize / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
console.log(` Savings: ${savings}%\n`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ Failed to convert ${imageName}:`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Done! Remember to update image references in blog-data.ts');
|
||||||
|
}
|
||||||
|
|
||||||
|
compressImages();
|
||||||
21
scripts/submit-indexnow.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
// Helper script to run IndexNow submission
|
||||||
|
// Run with: npx tsx scripts/submit-indexnow.ts
|
||||||
|
|
||||||
|
import { getAllIndexableUrls, submitToIndexNow } from '../src/lib/indexnow';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('Gathering URLs for IndexNow submission...');
|
||||||
|
const urls = getAllIndexableUrls();
|
||||||
|
console.log(`Found ${urls.length} indexable URLs.`);
|
||||||
|
|
||||||
|
// Basic validation of key presence (logic can be improved)
|
||||||
|
if (!process.env.INDEXNOW_KEY) {
|
||||||
|
console.warn('⚠️ WARNING: INDEXNOW_KEY environment variable is not set. Using placeholder.');
|
||||||
|
// In production, you'd fail here. For dev/demo, we proceed but expect failure from API.
|
||||||
|
}
|
||||||
|
|
||||||
|
await submitToIndexNow(urls);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
64
scripts/test-db-lead.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('🔄 Starting Database Diagnostics...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Test Connection
|
||||||
|
console.log('1️⃣ Testing basic connection...');
|
||||||
|
await prisma.$connect();
|
||||||
|
console.log('✅ Connected to database successfully.');
|
||||||
|
|
||||||
|
// 2. Test Lead Table Existence
|
||||||
|
console.log('2️⃣ Testing Lead table access...');
|
||||||
|
try {
|
||||||
|
const count = await prisma.lead.count();
|
||||||
|
console.log(`✅ Lead table found. Current count: ${count}`);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('❌ FAILED to access Lead table.');
|
||||||
|
if (e.code === 'P2021') {
|
||||||
|
console.error(' 👉 Error P2021: The table "Lead" does not exist in the current database.');
|
||||||
|
console.error(' 👉 SOLUTION: Run "npx prisma migrate deploy"');
|
||||||
|
} else {
|
||||||
|
console.error(' 👉 Error:', e.message);
|
||||||
|
}
|
||||||
|
throw e; // rethrow to stop
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Test Writing a dummy lead (optional, rolling back transaction)
|
||||||
|
console.log('3️⃣ Testing write permission...');
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
|
const lead = await tx.lead.create({
|
||||||
|
data: {
|
||||||
|
email: 'test_diagnostic_script@example.com',
|
||||||
|
source: 'diagnostic-script',
|
||||||
|
reprintCost: 0,
|
||||||
|
updatesPerYear: 0,
|
||||||
|
annualSavings: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('✅ Successfully created test lead with ID:', lead.id);
|
||||||
|
// We purposefully throw an error to rollback this transaction so we don't dirty the DB
|
||||||
|
throw new Error('ROLLBACK_TEST');
|
||||||
|
}).catch((e) => {
|
||||||
|
if (e.message === 'ROLLBACK_TEST') {
|
||||||
|
console.log('✅ Transaction rollback successful (cleaning up test data).');
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('\n🎉 ALL CHECKS PASSED! The database is effectively readable and writable.');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n💥 DIAGNOSTICS FAILED');
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
34
scripts/verify-lead-db.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
import { db } from '../src/lib/db';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log('Verifying Lead model...');
|
||||||
|
// Type assertion to bypass potential type generation issues locally if they exist
|
||||||
|
const leadCount = await (db as any).lead.count();
|
||||||
|
console.log(`Current lead count: ${leadCount}`);
|
||||||
|
|
||||||
|
const testLead = await (db as any).lead.create({
|
||||||
|
data: {
|
||||||
|
email: 'test_verify@example.com',
|
||||||
|
source: 'verification-script',
|
||||||
|
reprintCost: 100,
|
||||||
|
updatesPerYear: 12,
|
||||||
|
annualSavings: 1200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log('Successfully created test lead:', testLead.id);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
await (db as any).lead.delete({
|
||||||
|
where: { id: testLead.id }
|
||||||
|
});
|
||||||
|
console.log('Successfully deleted test lead');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Verification failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
743
seo_issues_new.md
Normal file
@@ -0,0 +1,743 @@
|
|||||||
|
Issues
|
||||||
|
/
|
||||||
|
Multiple H1 tags
|
||||||
|
|
||||||
|
Why and how to fix
|
||||||
|
|
||||||
|
Submit to IndexNow
|
||||||
|
|
||||||
|
Create new issue
|
||||||
|
|
||||||
|
All URLs
|
||||||
|
|
||||||
|
Pages
|
||||||
|
|
||||||
|
Resources
|
||||||
|
|
||||||
|
Content
|
||||||
|
|
||||||
|
Links
|
||||||
|
|
||||||
|
Redirects
|
||||||
|
|
||||||
|
Indexability
|
||||||
|
|
||||||
|
Sitemaps
|
||||||
|
|
||||||
|
Ahrefs metrics
|
||||||
|
Word or phrase
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
Advanced filter
|
||||||
|
Crawl history
|
||||||
|
Hide chart
|
||||||
|
12 Jan
|
||||||
|
13 Jan
|
||||||
|
13 Jan
|
||||||
|
14 Jan
|
||||||
|
14 Jan
|
||||||
|
0
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
4
|
||||||
|
All filter results
|
||||||
|
|
||||||
|
All filter results
|
||||||
|
3
|
||||||
|
|
||||||
|
Lost from filter results
|
||||||
|
0
|
||||||
|
|
||||||
|
Lost
|
||||||
|
0
|
||||||
|
|
||||||
|
Patches
|
||||||
|
|
||||||
|
Changes: Don't show
|
||||||
|
|
||||||
|
Columns
|
||||||
|
|
||||||
|
Export
|
||||||
|
PR
|
||||||
|
URL
|
||||||
|
Organic traffic
|
||||||
|
HTTP status code
|
||||||
|
Depth
|
||||||
|
H1
|
||||||
|
H1 length
|
||||||
|
No. of H1
|
||||||
|
Is indexable page
|
||||||
|
40
|
||||||
|
html
|
||||||
|
QR Master: Dynamic QR Generator
|
||||||
|
https://www.qrmaster.net/
|
||||||
|
0
|
||||||
|
200
|
||||||
|
0
|
||||||
|
QR Master: Dynamic QR Code Generator with Analytics
|
||||||
|
Create QR Codes That Work Everywhere
|
||||||
|
51
|
||||||
|
36
|
||||||
|
2
|
||||||
|
Yes
|
||||||
|
38
|
||||||
|
html
|
||||||
|
Pricing Plans | QR Master
|
||||||
|
https://www.qrmaster.net/pricing
|
||||||
|
0
|
||||||
|
200
|
||||||
|
0
|
||||||
|
QR Master Pricing – Choose Your QR Code Plan
|
||||||
|
Choose Your Plan
|
||||||
|
44
|
||||||
|
16
|
||||||
|
2
|
||||||
|
Yes
|
||||||
|
38
|
||||||
|
html
|
||||||
|
QR Code Erstellen – Kostenlos | QR Master
|
||||||
|
https://www.qrmaster.net/qr-code-erstellen
|
||||||
|
0
|
||||||
|
200
|
||||||
|
0
|
||||||
|
QR Code Erstellen – Kostenloser QR Code Generator mit Tracking
|
||||||
|
Erstellen Sie QR-Codes, die überall funktionieren
|
||||||
|
62
|
||||||
|
49
|
||||||
|
2
|
||||||
|
Yes
|
||||||
|
Showing 3 of 3
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Issues
|
||||||
|
/
|
||||||
|
Open Graph tags missing
|
||||||
|
|
||||||
|
Why and how to fix
|
||||||
|
|
||||||
|
Submit to IndexNow
|
||||||
|
|
||||||
|
Create new issue
|
||||||
|
|
||||||
|
All URLs
|
||||||
|
|
||||||
|
Pages
|
||||||
|
|
||||||
|
Resources
|
||||||
|
|
||||||
|
Content
|
||||||
|
|
||||||
|
Links
|
||||||
|
|
||||||
|
Redirects
|
||||||
|
|
||||||
|
Indexability
|
||||||
|
|
||||||
|
Sitemaps
|
||||||
|
|
||||||
|
Ahrefs metrics
|
||||||
|
Word or phrase
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
Advanced filter
|
||||||
|
Crawl history
|
||||||
|
Hide chart
|
||||||
|
12 Jan
|
||||||
|
13 Jan
|
||||||
|
13 Jan
|
||||||
|
14 Jan
|
||||||
|
14 Jan
|
||||||
|
0
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
4
|
||||||
|
All filter results
|
||||||
|
|
||||||
|
All filter results
|
||||||
|
2
|
||||||
|
|
||||||
|
Lost from filter results
|
||||||
|
0
|
||||||
|
|
||||||
|
Lost
|
||||||
|
0
|
||||||
|
|
||||||
|
Patches
|
||||||
|
|
||||||
|
Changes: Don't show
|
||||||
|
|
||||||
|
Columns
|
||||||
|
|
||||||
|
Export
|
||||||
|
PR
|
||||||
|
URL
|
||||||
|
Organic traffic
|
||||||
|
Is valid Open graph
|
||||||
|
Open graph attributes
|
||||||
|
Open graph values
|
||||||
|
Depth
|
||||||
|
Is indexable page
|
||||||
|
No. of all inlinks
|
||||||
|
39
|
||||||
|
html
|
||||||
|
Login to QR Master | Access Your Dashboard
|
||||||
|
https://www.qrmaster.net/login
|
||||||
|
0
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
38
|
||||||
|
38
|
||||||
|
html
|
||||||
|
Create Free Account | QR Master
|
||||||
|
https://www.qrmaster.net/signup
|
||||||
|
0
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
37
|
||||||
|
Showing 2 of 2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Issues
|
||||||
|
/
|
||||||
|
X (Twitter) card missing
|
||||||
|
|
||||||
|
Why and how to fix
|
||||||
|
|
||||||
|
Submit to IndexNow
|
||||||
|
|
||||||
|
Create new issue
|
||||||
|
|
||||||
|
All URLs
|
||||||
|
|
||||||
|
Pages
|
||||||
|
|
||||||
|
Resources
|
||||||
|
|
||||||
|
Content
|
||||||
|
|
||||||
|
Links
|
||||||
|
|
||||||
|
Redirects
|
||||||
|
|
||||||
|
Indexability
|
||||||
|
|
||||||
|
Sitemaps
|
||||||
|
|
||||||
|
Ahrefs metrics
|
||||||
|
Word or phrase
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
Advanced filter
|
||||||
|
Crawl history
|
||||||
|
Hide chart
|
||||||
|
12 Jan
|
||||||
|
13 Jan
|
||||||
|
13 Jan
|
||||||
|
14 Jan
|
||||||
|
14 Jan
|
||||||
|
0
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
4
|
||||||
|
All filter results
|
||||||
|
|
||||||
|
All filter results
|
||||||
|
2
|
||||||
|
|
||||||
|
Lost from filter results
|
||||||
|
0
|
||||||
|
|
||||||
|
Lost
|
||||||
|
0
|
||||||
|
|
||||||
|
Patches
|
||||||
|
|
||||||
|
Changes: Don't show
|
||||||
|
|
||||||
|
Columns
|
||||||
|
|
||||||
|
Export
|
||||||
|
PR
|
||||||
|
URL
|
||||||
|
Organic traffic
|
||||||
|
Is valid X (Twitter) card
|
||||||
|
X (Twitter) card attributes
|
||||||
|
X (Twitter) card values
|
||||||
|
Depth
|
||||||
|
Is indexable page
|
||||||
|
No. of all inlinks
|
||||||
|
39
|
||||||
|
html
|
||||||
|
Login to QR Master | Access Your Dashboard
|
||||||
|
https://www.qrmaster.net/login
|
||||||
|
0
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
38
|
||||||
|
38
|
||||||
|
html
|
||||||
|
Create Free Account | QR Master
|
||||||
|
https://www.qrmaster.net/signup
|
||||||
|
0
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
37
|
||||||
|
Showing 2 of 2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Issues
|
||||||
|
/
|
||||||
|
Slow page
|
||||||
|
|
||||||
|
Why and how to fix
|
||||||
|
|
||||||
|
Submit to IndexNow
|
||||||
|
|
||||||
|
Create new issue
|
||||||
|
|
||||||
|
All URLs
|
||||||
|
|
||||||
|
Pages
|
||||||
|
|
||||||
|
Resources
|
||||||
|
|
||||||
|
Content
|
||||||
|
|
||||||
|
Links
|
||||||
|
|
||||||
|
Redirects
|
||||||
|
|
||||||
|
Indexability
|
||||||
|
|
||||||
|
Sitemaps
|
||||||
|
|
||||||
|
Ahrefs metrics
|
||||||
|
Word or phrase
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
Advanced filter
|
||||||
|
Crawl history
|
||||||
|
Hide chart
|
||||||
|
12 Jan
|
||||||
|
13 Jan
|
||||||
|
13 Jan
|
||||||
|
14 Jan
|
||||||
|
14 Jan
|
||||||
|
0
|
||||||
|
2
|
||||||
|
4
|
||||||
|
6
|
||||||
|
8
|
||||||
|
All filter results
|
||||||
|
|
||||||
|
All filter results
|
||||||
|
8
|
||||||
|
|
||||||
|
Lost from filter results
|
||||||
|
0
|
||||||
|
|
||||||
|
Lost
|
||||||
|
0
|
||||||
|
|
||||||
|
Patches
|
||||||
|
|
||||||
|
Changes: Don't show
|
||||||
|
|
||||||
|
Columns
|
||||||
|
|
||||||
|
Export
|
||||||
|
PR
|
||||||
|
URL
|
||||||
|
Organic traffic
|
||||||
|
HTTP status code
|
||||||
|
Size (bytes)
|
||||||
|
Time to first byte (ms)
|
||||||
|
Loading time (ms)
|
||||||
|
Depth
|
||||||
|
Is indexable page
|
||||||
|
No. of all inlinks
|
||||||
|
First found at
|
||||||
|
39
|
||||||
|
html
|
||||||
|
QR Master FAQ: Dynamic & Bulk QR | QR Master
|
||||||
|
https://www.qrmaster.net/faq
|
||||||
|
0
|
||||||
|
200
|
||||||
|
9,957
|
||||||
|
3,291
|
||||||
|
3,295
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
38
|
||||||
|
38
|
||||||
|
html
|
||||||
|
Free WhatsApp QR Code Generator | Start Chats Instantly | QR Master
|
||||||
|
https://www.qrmaster.net/tools/whatsapp-qr-code
|
||||||
|
0
|
||||||
|
200
|
||||||
|
17,196
|
||||||
|
22,105
|
||||||
|
22,108
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
36
|
||||||
|
38
|
||||||
|
html
|
||||||
|
QR Insights: Latest QR Strategies | QR Master
|
||||||
|
https://www.qrmaster.net/blog
|
||||||
|
0
|
||||||
|
200
|
||||||
|
9,739
|
||||||
|
23,152
|
||||||
|
23,153
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
36
|
||||||
|
38
|
||||||
|
html
|
||||||
|
Free PayPal QR Code Generator | Accept Payments Instantly | QR Master
|
||||||
|
https://www.qrmaster.net/tools/paypal-qr-code
|
||||||
|
0
|
||||||
|
200
|
||||||
|
17,661
|
||||||
|
16,253
|
||||||
|
16,254
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
36
|
||||||
|
38
|
||||||
|
html
|
||||||
|
Free vCard QR Code Generator | QR Master
|
||||||
|
https://www.qrmaster.net/tools/vcard-qr-code
|
||||||
|
0
|
||||||
|
200
|
||||||
|
19,120
|
||||||
|
17,305
|
||||||
|
17,328
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
36
|
||||||
|
38
|
||||||
|
html
|
||||||
|
Free Text QR Code Generator | Text zu QR Code | QR Master
|
||||||
|
https://www.qrmaster.net/tools/text-qr-code
|
||||||
|
0
|
||||||
|
200
|
||||||
|
17,089
|
||||||
|
27,995
|
||||||
|
28,036
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
36
|
||||||
|
38
|
||||||
|
html
|
||||||
|
Free Crypto QR Code Generator | Krypto QR Code Erstellen | QR Master
|
||||||
|
https://www.qrmaster.net/tools/crypto-qr-code
|
||||||
|
0
|
||||||
|
200
|
||||||
|
17,093
|
||||||
|
10,033
|
||||||
|
10,069
|
||||||
|
0
|
||||||
|
Yes
|
||||||
|
36
|
||||||
|
18
|
||||||
|
html
|
||||||
|
Newsletter Admin | QR Master | QR Master
|
||||||
|
https://www.qrmaster.net/newsletter
|
||||||
|
0
|
||||||
|
200
|
||||||
|
7,334
|
||||||
|
11,826
|
||||||
|
11,830
|
||||||
|
1
|
||||||
|
No
|
||||||
|
36
|
||||||
|
https://www.qrmaster.net/
|
||||||
|
Showing 8 of 8
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Issues
|
||||||
|
/
|
||||||
|
Structured data has schema.org validation error
|
||||||
|
|
||||||
|
Why and how to fix
|
||||||
|
|
||||||
|
Submit to IndexNow
|
||||||
|
|
||||||
|
Create new issue
|
||||||
|
|
||||||
|
All URLs
|
||||||
|
|
||||||
|
Pages
|
||||||
|
|
||||||
|
Resources
|
||||||
|
|
||||||
|
Content
|
||||||
|
|
||||||
|
Links
|
||||||
|
|
||||||
|
Redirects
|
||||||
|
|
||||||
|
Indexability
|
||||||
|
|
||||||
|
Sitemaps
|
||||||
|
|
||||||
|
Ahrefs metrics
|
||||||
|
Word or phrase
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
Advanced filter
|
||||||
|
Crawl history
|
||||||
|
Hide chart
|
||||||
|
12 Jan
|
||||||
|
13 Jan
|
||||||
|
13 Jan
|
||||||
|
14 Jan
|
||||||
|
14 Jan
|
||||||
|
0
|
||||||
|
10
|
||||||
|
20
|
||||||
|
30
|
||||||
|
40
|
||||||
|
All filter results
|
||||||
|
|
||||||
|
All filter results
|
||||||
|
12
|
||||||
|
|
||||||
|
Lost from filter results
|
||||||
|
25
|
||||||
|
|
||||||
|
Lost
|
||||||
|
1
|
||||||
|
|
||||||
|
Patches
|
||||||
|
|
||||||
|
Changes: Don't show
|
||||||
|
|
||||||
|
Columns
|
||||||
|
|
||||||
|
Export
|
||||||
|
PR
|
||||||
|
URL
|
||||||
|
Organic traffic
|
||||||
|
Schema items
|
||||||
|
Structured data issues
|
||||||
|
Is indexable page
|
||||||
|
38
|
||||||
|
html
|
||||||
|
QR Insights: Latest QR Strategies | QR Master
|
||||||
|
https://www.qrmaster.net/blog
|
||||||
|
0
|
||||||
|
BreadcrumbList
|
||||||
|
Organization
|
||||||
|
WebSite
|
||||||
|
Schema.org validation error
|
||||||
|
View issues
|
||||||
|
Yes
|
||||||
|
38
|
||||||
|
html
|
||||||
|
QR Code Tracking & Analytics - Track Scans | QR Master | QR Master
|
||||||
|
https://www.qrmaster.net/qr-code-tracking
|
||||||
|
0
|
||||||
|
BreadcrumbList
|
||||||
|
HowTo
|
||||||
|
Organization
|
||||||
|
SoftwareApplication
|
||||||
|
WebSite
|
||||||
|
Schema.org validation error
|
||||||
|
View issues
|
||||||
|
Yes
|
||||||
|
38
|
||||||
|
html
|
||||||
|
Bulk QR Code Generator | Create from Excel | QR Master | QR Master
|
||||||
|
https://www.qrmaster.net/bulk-qr-code-generator
|
||||||
|
0
|
||||||
|
BreadcrumbList
|
||||||
|
FAQPage
|
||||||
|
HowTo
|
||||||
|
Organization
|
||||||
|
SoftwareApplication
|
||||||
|
All 6
|
||||||
|
Schema.org validation error
|
||||||
|
View issues
|
||||||
|
Yes
|
||||||
|
24
|
||||||
|
html
|
||||||
|
Free vCard QR Generator: Digital Cards | QR Master
|
||||||
|
https://www.qrmaster.net/blog/vcard-qr-code-generator
|
||||||
|
0
|
||||||
|
BlogPosting
|
||||||
|
BreadcrumbList
|
||||||
|
HowTo
|
||||||
|
Organization
|
||||||
|
WebSite
|
||||||
|
Schema.org validation error
|
||||||
|
View issues
|
||||||
|
Yes
|
||||||
|
24
|
||||||
|
html
|
||||||
|
Restaurant Menu QR Codes: 2025 Guide | QR Master
|
||||||
|
https://www.qrmaster.net/blog/qr-code-restaurant-menu
|
||||||
|
0
|
||||||
|
BlogPosting
|
||||||
|
BreadcrumbList
|
||||||
|
HowTo
|
||||||
|
Organization
|
||||||
|
WebSite
|
||||||
|
Schema.org validation error
|
||||||
|
View issues
|
||||||
|
Yes
|
||||||
|
24
|
||||||
|
html
|
||||||
|
QR Code Analytics: The Complete Guide | QR Master
|
||||||
|
https://www.qrmaster.net/blog/qr-code-analytics
|
||||||
|
0
|
||||||
|
BlogPosting
|
||||||
|
BreadcrumbList
|
||||||
|
HowTo
|
||||||
|
Organization
|
||||||
|
WebSite
|
||||||
|
Schema.org validation error
|
||||||
|
View issues
|
||||||
|
Yes
|
||||||
|
24
|
||||||
|
html
|
||||||
|
Dynamic vs Static QR Codes: The Ultimate Comparison | QR Master
|
||||||
|
https://www.qrmaster.net/blog/dynamic-vs-static-qr-codes
|
||||||
|
0
|
||||||
|
BlogPosting
|
||||||
|
BreadcrumbList
|
||||||
|
Organization
|
||||||
|
WebSite
|
||||||
|
Schema.org validation error
|
||||||
|
View issues
|
||||||
|
Yes
|
||||||
|
24
|
||||||
|
html
|
||||||
|
How to Generate Bulk QR Codes from Excel | QR Master
|
||||||
|
https://www.qrmaster.net/blog/bulk-qr-code-generator-excel
|
||||||
|
0
|
||||||
|
BlogPosting
|
||||||
|
BreadcrumbList
|
||||||
|
HowTo
|
||||||
|
Organization
|
||||||
|
WebSite
|
||||||
|
Schema.org validation error
|
||||||
|
View issues
|
||||||
|
Yes
|
||||||
|
24
|
||||||
|
html
|
||||||
|
QR Code Print Size Guide: Minimum Sizes for Every Use Case | QR Master
|
||||||
|
https://www.qrmaster.net/blog/qr-code-print-size-guide
|
||||||
|
0
|
||||||
|
BlogPosting
|
||||||
|
BreadcrumbList
|
||||||
|
Organization
|
||||||
|
WebSite
|
||||||
|
Schema.org validation error
|
||||||
|
View issues
|
||||||
|
Yes
|
||||||
|
24
|
||||||
|
html
|
||||||
|
Best QR Code Generator for Small Business 2025 | QR Master
|
||||||
|
https://www.qrmaster.net/blog/qr-code-small-business
|
||||||
|
0
|
||||||
|
BlogPosting
|
||||||
|
BreadcrumbList
|
||||||
|
Organization
|
||||||
|
WebSite
|
||||||
|
Schema.org validation error
|
||||||
|
View issues
|
||||||
|
Yes
|
||||||
|
24
|
||||||
|
html
|
||||||
|
QR Code Tracking: Complete Guide 2025 | QR Master
|
||||||
|
https://www.qrmaster.net/blog/qr-code-tracking-guide-2025
|
||||||
|
0
|
||||||
|
BlogPosting
|
||||||
|
BreadcrumbList
|
||||||
|
HowTo
|
||||||
|
Organization
|
||||||
|
WebSite
|
||||||
|
Schema.org validation error
|
||||||
|
View issues
|
||||||
|
Yes
|
||||||
|
21
|
||||||
|
html
|
||||||
|
Dynamic QR Code Generator | Edit & Track QR | QR Master | QR Master
|
||||||
|
https://www.qrmaster.net/dynamic-qr-code-generator
|
||||||
|
0
|
||||||
|
BreadcrumbList
|
||||||
|
FAQPage
|
||||||
|
HowTo
|
||||||
|
Organization
|
||||||
|
SoftwareApplication
|
||||||
|
All 6
|
||||||
|
Schema.org validation error
|
||||||
|
View issues
|
||||||
|
Yes
|
||||||
|
Showing 12 of 12
|
||||||
68
seo_tasks.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# SEO Remaining Tasks
|
||||||
|
|
||||||
|
This document contains a list of all SEO issues identified in the Ahrefs and Seobility reports that still need to be addressed in the codebase.
|
||||||
|
|
||||||
|
## 1. Content & Metadata Issues
|
||||||
|
|
||||||
|
- [ ] **Fix Missing H1 Tags on Core Pages**
|
||||||
|
- Affected Pages: `/`, `/pricing`, `/login`, `/signup`, `/faq`, `/privacy`, `/newsletter`, `/create`.
|
||||||
|
- **Issue:** These pages are Client Side Rendered (CSR) or lack a server-side `<h1>` tag in the initial HTML payload.
|
||||||
|
- **Action:** Add an `<h1>` (visible or `sr-only`) to the Server Component or ensure the Client Component renders it immediately.
|
||||||
|
|
||||||
|
- [ ] **Fix Low Word Count / Thin Content**
|
||||||
|
- Affected Pages: `/`, `/pricing`, `/login`, `/signup`, `/faq`, `/privacy`.
|
||||||
|
- **Issue:** Crawlers see 0 words on these pages because the content is rendered via JavaScript (`use client`).
|
||||||
|
- **Action:** Implement Server Side Rendering (SSR) for the main content or add `sr-only` semantic fallbacks for crawlers.
|
||||||
|
|
||||||
|
- [ ] **Expand Meta Descriptions**
|
||||||
|
- Affected Pages: `/`, `/pricing`, `/login`, `/signup`, `/newsletter`, `/privacy`, `/faq`, `/qr-code-erstellen`, Blog entries.
|
||||||
|
- **Issue:** Meta descriptions are too short (< 80 characters) or duplicates.
|
||||||
|
- **Action:** Update `generateMetadata` in `page.tsx` files to have descriptions between 110-160 characters.
|
||||||
|
|
||||||
|
- [ ] **Fix Page Titles**
|
||||||
|
- Affected Pages: `/qr-code-erstellen`, Blog posts.
|
||||||
|
- **Issue:** Titles are too long (> 60-70 characters) or have keyword stuffing/repetition.
|
||||||
|
- **Action:** Shorten titles to be concise and click-worthy, avoiding simple concatenation of keywords.
|
||||||
|
|
||||||
|
- [ ] **Fix Duplicate Content & Titles**
|
||||||
|
- Affected Pages: `/pricing`, `/newsletter`, `/login`, `/signup`.
|
||||||
|
- **Issue:** These pages likely share the same metadata or layout without unique content in the crawler's eyes.
|
||||||
|
- **Action:** Ensure each page has unique `title` and `description` in `generateMetadata`.
|
||||||
|
|
||||||
|
## 2. Technical SEO
|
||||||
|
|
||||||
|
- [ ] **Fix 307 Redirects to 301**
|
||||||
|
- **Issue:** Blog posts and legacy URLs are redirecting with status `307` (Temporary) instead of `301` (Permanent).
|
||||||
|
- **Affected Paths:**
|
||||||
|
- `/blog/vcard-qr-code-generator` -> `/create`
|
||||||
|
- `/blog/qr-code-restaurant-menu` -> `/dynamic-qr-code-generator`
|
||||||
|
- `/blog/bulk-qr-code-generator` -> `/bulk-qr-code-generator`
|
||||||
|
- **Action:** Locate these redirects (likely in `next.config.js` or `middleware.ts` or component logic) and change status to 301.
|
||||||
|
|
||||||
|
- [ ] **Fix Indexing of Protected/Private Pages**
|
||||||
|
- **Issue:** Ahrefs is flagging `/pricing` as "Indexable" but likely encountering issues. Verify if `/pricing` should be indexed.
|
||||||
|
- **Action:** Ensure public pages like Pricing are NOT in `(app)` group which has `noindex` in layout, or override the `robots` meta in `pricing/page.tsx`.
|
||||||
|
|
||||||
|
- [ ] **Fix "No Outgoing Links"**
|
||||||
|
- **Issue:** Crawlers see pages as dead ends because links are injected via JS.
|
||||||
|
- **Action:** Ensure standard `<a>` or `Link` tags are present in the initial HTML.
|
||||||
|
|
||||||
|
## 3. Link Profile
|
||||||
|
|
||||||
|
- [ ] **Improve Internal Link Texts**
|
||||||
|
- **Issue:** "Click here" or full URL used as anchor text.
|
||||||
|
- **Action:** Use descriptive keywords for links (e.g., "See our pricing" instead of "Click here").
|
||||||
|
|
||||||
|
- [ ] **Fix Alternate Links (hreflang)**
|
||||||
|
- **Issue:** Mismatch in `hreflang` or missing self-referencing canonicals.
|
||||||
|
- **Action:** Verify `alternates` configuration in `layout.tsx` or `page.tsx` matches the actual URL structure.
|
||||||
|
|
||||||
|
## 4. Performance & Images
|
||||||
|
|
||||||
|
- [ ] **Optimize Large Images**
|
||||||
|
- **Files:** `/blog/1-boy.png`, `/blog/2-body.png` (~4MB each).
|
||||||
|
- **Action:** Convert to WebP/AVIF and resize to < 500KB.
|
||||||
|
|
||||||
|
- [ ] **Improve Page Speed**
|
||||||
|
- **Issue:** Response time for `/qr-code-erstellen` is slow.
|
||||||
|
- **Action:** Check for expensive server-side operations or optimize database queries.
|
||||||
22
seobility-findings.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Seobility SEO Findings & Status
|
||||||
|
|
||||||
|
## Structure & Internal Linking
|
||||||
|
- [FIXED] **Improve Internal Link Texts**
|
||||||
|
- *Status:* Replaced "Read more" with "Read Article" in `blog/page.tsx`.
|
||||||
|
- [VERIFIED] **Pages with few internal links (9 pages)**
|
||||||
|
- *Status:* Core pages. `MarketingLayout` ensures Footer/Nav links exist on all these pages. Design choice.
|
||||||
|
|
||||||
|
## Onpage & Content
|
||||||
|
- [PARTIAL] **Problems with Page Titles (13 pages)**
|
||||||
|
- *Fixed:* Word repetition (Duplication).
|
||||||
|
- *Remaining:* "Too long" titles (e.g. `QR Code Analytics: Track...`).
|
||||||
|
- [VERIFIED] **Keywords not in text**
|
||||||
|
- *Action:* Content reviewed. Titles match page intent. Modern SEO prefers natural language over exact keyword stuffing.
|
||||||
|
- [RESOLVED] **Identical HTML Pages**
|
||||||
|
- *Status:* `privacy`, `faq`, `newsletter`. Verified as False Positives (Unique content found) or Admin Page confusion (`newsletter`).
|
||||||
|
|
||||||
|
## Technical
|
||||||
|
- [VERIFIED] **H1 Headings**
|
||||||
|
- *Status:* **False Positive in Report**. Code review confirms `<h1 className="sr-only">` tags are present on all core pages (Login, Signup, etc.). Crawlers can read this.
|
||||||
|
- [FIXED] **Duplicate Meta Descriptions**
|
||||||
|
- *Status:* Addressed by fixing metadata on core pages.
|
||||||
254
src/app/(app)/AppLayout.tsx
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Dropdown, DropdownItem } from '@/components/ui/Dropdown';
|
||||||
|
import { Footer } from '@/components/ui/Footer';
|
||||||
|
import { useTranslation } from '@/hooks/useTranslation';
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string | null;
|
||||||
|
email: string;
|
||||||
|
plan: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AppLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
|
||||||
|
// Fetch user data on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUser = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/user');
|
||||||
|
if (response.ok) {
|
||||||
|
const userData = await response.json();
|
||||||
|
setUser(userData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUser();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSignOut = async () => {
|
||||||
|
// Track logout event before clearing data
|
||||||
|
try {
|
||||||
|
const { trackEvent, resetUser } = await import('@/components/PostHogProvider');
|
||||||
|
trackEvent('user_logout');
|
||||||
|
resetUser(); // Reset PostHog user session
|
||||||
|
} catch (error) {
|
||||||
|
console.error('PostHog tracking error:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all cookies
|
||||||
|
document.cookie.split(";").forEach(c => {
|
||||||
|
document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/");
|
||||||
|
});
|
||||||
|
// Clear localStorage
|
||||||
|
localStorage.clear();
|
||||||
|
// Redirect to home
|
||||||
|
router.push('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get user initials for avatar (e.g., "Timo Schmidt" -> "TS")
|
||||||
|
const getUserInitials = () => {
|
||||||
|
if (!user) return 'U';
|
||||||
|
|
||||||
|
if (user.name) {
|
||||||
|
const names = user.name.trim().split(' ');
|
||||||
|
if (names.length >= 2) {
|
||||||
|
return (names[0][0] + names[names.length - 1][0]).toUpperCase();
|
||||||
|
}
|
||||||
|
return user.name.substring(0, 2).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to email
|
||||||
|
return user.email.substring(0, 1).toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get display name (first name or full name)
|
||||||
|
const getDisplayName = () => {
|
||||||
|
if (!user) return 'User';
|
||||||
|
|
||||||
|
if (user.name) {
|
||||||
|
return user.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to email without domain
|
||||||
|
return user.email.split('@')[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigation = [
|
||||||
|
{
|
||||||
|
name: t('nav.dashboard'),
|
||||||
|
href: '/dashboard',
|
||||||
|
icon: (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('nav.create_qr'),
|
||||||
|
href: '/create',
|
||||||
|
icon: (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('nav.bulk_creation'),
|
||||||
|
href: '/bulk-creation',
|
||||||
|
icon: (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('nav.analytics'),
|
||||||
|
href: '/analytics',
|
||||||
|
icon: (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('nav.pricing'),
|
||||||
|
href: '/pricing',
|
||||||
|
icon: (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('nav.settings'),
|
||||||
|
href: '/settings',
|
||||||
|
icon: (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Mobile sidebar backdrop */}
|
||||||
|
{sidebarOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
|
||||||
|
onClick={() => setSidebarOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sidebar */}
|
||||||
|
<aside
|
||||||
|
className={`fixed top-0 left-0 z-50 h-full w-64 bg-white border-r border-gray-200 transform transition-transform lg:translate-x-0 ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||||
|
<Link href="/" className="flex items-center space-x-2">
|
||||||
|
<img src="/logo.svg" alt="QR Master" className="w-8 h-8" />
|
||||||
|
<span className="text-xl font-bold text-gray-900">QR Master</span>
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
className="lg:hidden"
|
||||||
|
onClick={() => setSidebarOpen(false)}
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="p-4 space-y-1">
|
||||||
|
{navigation.map((item) => {
|
||||||
|
const isActive = pathname === item.href;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.name}
|
||||||
|
href={item.href}
|
||||||
|
className={`flex items-center space-x-3 px-3 py-2 rounded-lg transition-colors ${isActive
|
||||||
|
? 'bg-primary-50 text-primary-600'
|
||||||
|
: 'text-gray-700 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
<span className="font-medium">{item.name}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className="lg:ml-64">
|
||||||
|
{/* Top bar */}
|
||||||
|
<header className="bg-white border-b border-gray-200">
|
||||||
|
<div className="flex items-center justify-between px-4 py-3">
|
||||||
|
<button
|
||||||
|
className="lg:hidden"
|
||||||
|
onClick={() => setSidebarOpen(true)}
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4 ml-auto">
|
||||||
|
{/* User Menu */}
|
||||||
|
<Dropdown
|
||||||
|
align="right"
|
||||||
|
trigger={
|
||||||
|
<button className="flex items-center space-x-2 text-gray-700 hover:text-gray-900">
|
||||||
|
<div className="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
|
||||||
|
<span className="text-sm font-medium text-primary-600">
|
||||||
|
{getUserInitials()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="hidden md:block font-medium">
|
||||||
|
{getDisplayName()}
|
||||||
|
</span>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DropdownItem onClick={handleSignOut}>
|
||||||
|
Sign Out
|
||||||
|
</DropdownItem>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Page content */}
|
||||||
|
<main className="p-6">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<Footer variant="dashboard" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,254 +1,38 @@
|
|||||||
'use client';
|
import type { Metadata } from 'next';
|
||||||
|
import '@/styles/globals.css';
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
import { Providers } from '@/components/Providers';
|
||||||
|
import AppLayout from './AppLayout';
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
export const metadata: Metadata = {
|
||||||
import Link from 'next/link';
|
title: 'Dashboard | QR Master',
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
description: 'Manage your QR Master dashboard. Create dynamic QR codes, view real-time scan analytics, and configure your account settings in one secure place.',
|
||||||
import { Button } from '@/components/ui/Button';
|
robots: { index: false, follow: false },
|
||||||
import { Dropdown, DropdownItem } from '@/components/ui/Dropdown';
|
icons: {
|
||||||
import { Footer } from '@/components/ui/Footer';
|
icon: [
|
||||||
import { useTranslation } from '@/hooks/useTranslation';
|
{ url: '/favicon.svg', type: 'image/svg+xml' },
|
||||||
|
{ url: '/logo.svg', type: 'image/svg+xml' },
|
||||||
|
],
|
||||||
|
apple: '/logo.svg',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
interface User {
|
export default function RootAppLayout({
|
||||||
id: string;
|
children,
|
||||||
name: string | null;
|
|
||||||
email: string;
|
|
||||||
plan: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AppLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const pathname = usePathname();
|
return (
|
||||||
const router = useRouter();
|
<html lang="en">
|
||||||
const { t } = useTranslation();
|
<body className="font-sans">
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
<Providers>
|
||||||
const [user, setUser] = useState<User | null>(null);
|
<Suspense fallback={null}>
|
||||||
|
<AppLayout>
|
||||||
// Fetch user data on mount
|
{children}
|
||||||
useEffect(() => {
|
</AppLayout>
|
||||||
const fetchUser = async () => {
|
</Suspense>
|
||||||
try {
|
</Providers>
|
||||||
const response = await fetch('/api/user');
|
</body>
|
||||||
if (response.ok) {
|
</html>
|
||||||
const userData = await response.json();
|
);
|
||||||
setUser(userData);
|
}
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching user:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchUser();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
|
||||||
// Track logout event before clearing data
|
|
||||||
try {
|
|
||||||
const { trackEvent, resetUser } = await import('@/components/PostHogProvider');
|
|
||||||
trackEvent('user_logout');
|
|
||||||
resetUser(); // Reset PostHog user session
|
|
||||||
} catch (error) {
|
|
||||||
console.error('PostHog tracking error:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear all cookies
|
|
||||||
document.cookie.split(";").forEach(c => {
|
|
||||||
document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/");
|
|
||||||
});
|
|
||||||
// Clear localStorage
|
|
||||||
localStorage.clear();
|
|
||||||
// Redirect to home
|
|
||||||
router.push('/');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get user initials for avatar (e.g., "Timo Schmidt" -> "TS")
|
|
||||||
const getUserInitials = () => {
|
|
||||||
if (!user) return 'U';
|
|
||||||
|
|
||||||
if (user.name) {
|
|
||||||
const names = user.name.trim().split(' ');
|
|
||||||
if (names.length >= 2) {
|
|
||||||
return (names[0][0] + names[names.length - 1][0]).toUpperCase();
|
|
||||||
}
|
|
||||||
return user.name.substring(0, 2).toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to email
|
|
||||||
return user.email.substring(0, 1).toUpperCase();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get display name (first name or full name)
|
|
||||||
const getDisplayName = () => {
|
|
||||||
if (!user) return 'User';
|
|
||||||
|
|
||||||
if (user.name) {
|
|
||||||
return user.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to email without domain
|
|
||||||
return user.email.split('@')[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigation = [
|
|
||||||
{
|
|
||||||
name: t('nav.dashboard'),
|
|
||||||
href: '/dashboard',
|
|
||||||
icon: (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: t('nav.create_qr'),
|
|
||||||
href: '/create',
|
|
||||||
icon: (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: t('nav.bulk_creation'),
|
|
||||||
href: '/bulk-creation',
|
|
||||||
icon: (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: t('nav.analytics'),
|
|
||||||
href: '/analytics',
|
|
||||||
icon: (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: t('nav.pricing'),
|
|
||||||
href: '/pricing',
|
|
||||||
icon: (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: t('nav.settings'),
|
|
||||||
href: '/settings',
|
|
||||||
icon: (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50">
|
|
||||||
{/* Mobile sidebar backdrop */}
|
|
||||||
{sidebarOpen && (
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
|
|
||||||
onClick={() => setSidebarOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Sidebar */}
|
|
||||||
<aside
|
|
||||||
className={`fixed top-0 left-0 z-50 h-full w-64 bg-white border-r border-gray-200 transform transition-transform lg:translate-x-0 ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
|
||||||
<Link href="/" className="flex items-center space-x-2">
|
|
||||||
<img src="/logo.svg" alt="QR Master" className="w-8 h-8" />
|
|
||||||
<span className="text-xl font-bold text-gray-900">QR Master</span>
|
|
||||||
</Link>
|
|
||||||
<button
|
|
||||||
className="lg:hidden"
|
|
||||||
onClick={() => setSidebarOpen(false)}
|
|
||||||
>
|
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav className="p-4 space-y-1">
|
|
||||||
{navigation.map((item) => {
|
|
||||||
const isActive = pathname === item.href;
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={item.name}
|
|
||||||
href={item.href}
|
|
||||||
className={`flex items-center space-x-3 px-3 py-2 rounded-lg transition-colors ${isActive
|
|
||||||
? 'bg-primary-50 text-primary-600'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{item.icon}
|
|
||||||
<span className="font-medium">{item.name}</span>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main content */}
|
|
||||||
<div className="lg:ml-64">
|
|
||||||
{/* Top bar */}
|
|
||||||
<header className="bg-white border-b border-gray-200">
|
|
||||||
<div className="flex items-center justify-between px-4 py-3">
|
|
||||||
<button
|
|
||||||
className="lg:hidden"
|
|
||||||
onClick={() => setSidebarOpen(true)}
|
|
||||||
>
|
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-4 ml-auto">
|
|
||||||
{/* User Menu */}
|
|
||||||
<Dropdown
|
|
||||||
align="right"
|
|
||||||
trigger={
|
|
||||||
<button className="flex items-center space-x-2 text-gray-700 hover:text-gray-900">
|
|
||||||
<div className="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
|
|
||||||
<span className="text-sm font-medium text-primary-600">
|
|
||||||
{getUserInitials()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="hidden md:block font-medium">
|
|
||||||
{getDisplayName()}
|
|
||||||
</span>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<DropdownItem onClick={handleSignOut}>
|
|
||||||
Sign Out
|
|
||||||
</DropdownItem>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Page content */}
|
|
||||||
<main className="p-6">
|
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<Footer variant="dashboard" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,38 @@
|
|||||||
export default function AuthLayout({
|
import '@/styles/globals.css';
|
||||||
|
import { Providers } from '@/components/Providers';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Authentication | QR Master',
|
||||||
|
description: 'Securely login or sign up to QR Master to manage your dynamic QR codes, track analytics, and access premium features. Your gateway to professional QR management.',
|
||||||
|
icons: {
|
||||||
|
icon: [
|
||||||
|
{ url: '/favicon.svg', type: 'image/svg+xml' },
|
||||||
|
{ url: '/logo.svg', type: 'image/svg+xml' },
|
||||||
|
],
|
||||||
|
apple: '/logo.svg',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AuthRootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white">
|
<html lang="en">
|
||||||
{children}
|
<body className="font-sans">
|
||||||
</div>
|
<Providers>
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white">
|
||||||
|
{children}
|
||||||
|
<div className="py-6 text-center text-sm text-slate-500 space-x-4">
|
||||||
|
<a href="/" className="hover:text-primary-600 transition-colors">Home</a>
|
||||||
|
<a href="/privacy" className="hover:text-primary-600 transition-colors">Privacy</a>
|
||||||
|
<a href="/faq" className="hover:text-primary-600 transition-colors">FAQ</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Providers>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
164
src/app/(auth)/login/ClientPage.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||||
|
import { Input } from '@/components/ui/Input';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { useTranslation } from '@/hooks/useTranslation';
|
||||||
|
import { useCsrf } from '@/hooks/useCsrf';
|
||||||
|
|
||||||
|
export default function LoginClientPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { fetchWithCsrf, loading: csrfLoading } = useCsrf();
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchWithCsrf('/api/auth/simple-login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ email, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.success) {
|
||||||
|
// Store user in localStorage for client-side
|
||||||
|
localStorage.setItem('user', JSON.stringify(data.user));
|
||||||
|
|
||||||
|
// Track successful login with PostHog
|
||||||
|
try {
|
||||||
|
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
|
||||||
|
identifyUser(data.user.id, {
|
||||||
|
email: data.user.email,
|
||||||
|
name: data.user.name,
|
||||||
|
plan: data.user.plan || 'FREE',
|
||||||
|
});
|
||||||
|
trackEvent('user_login', {
|
||||||
|
method: 'email',
|
||||||
|
email: data.user.email,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('PostHog tracking error:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for redirect parameter
|
||||||
|
const redirectUrl = searchParams.get('redirect') || '/dashboard';
|
||||||
|
router.push(redirectUrl);
|
||||||
|
router.refresh();
|
||||||
|
} else {
|
||||||
|
setError(data.error || 'Invalid email or password');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('An error occurred. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGoogleSignIn = () => {
|
||||||
|
// Redirect to Google OAuth API route
|
||||||
|
window.location.href = '/api/auth/google';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input type="checkbox" className="mr-2" />
|
||||||
|
<span className="text-sm text-gray-600">Remember me</span>
|
||||||
|
</label>
|
||||||
|
<Link href="/forgot-password" className="text-sm text-primary-600 hover:text-primary-700">
|
||||||
|
Forgot password?
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full" loading={loading} disabled={csrfLoading || loading}>
|
||||||
|
{csrfLoading ? 'Loading...' : 'Sign In'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="relative my-6">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-sm">
|
||||||
|
<span className="px-2 bg-white text-gray-500">Or continue with</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleGoogleSignIn}
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="#4285F4"
|
||||||
|
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#34A853"
|
||||||
|
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#FBBC05"
|
||||||
|
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#EA4335"
|
||||||
|
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Sign in with Google
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Don't have an account?{' '}
|
||||||
|
<Link href="/signup" className="text-primary-600 hover:text-primary-700 font-medium">
|
||||||
|
Sign up
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,76 +1,38 @@
|
|||||||
'use client';
|
import React, { Suspense } from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
import LoginClientPage from './ClientPage';
|
||||||
import { Input } from '@/components/ui/Input';
|
|
||||||
import { Button } from '@/components/ui/Button';
|
export const metadata: Metadata = {
|
||||||
import { useTranslation } from '@/hooks/useTranslation';
|
title: {
|
||||||
import { useCsrf } from '@/hooks/useCsrf';
|
absolute: 'Login to QR Master | Access Your Dashboard'
|
||||||
|
},
|
||||||
|
description: 'Sign in to QR Master to create, manage, and track your QR codes. Access your dashboard and view analytics.',
|
||||||
|
alternates: {
|
||||||
|
canonical: 'https://www.qrmaster.net/login',
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: 'Login to QR Master | Access Your Dashboard',
|
||||||
|
description: 'Sign in to QR Master to create, manage, and track your QR codes.',
|
||||||
|
url: 'https://www.qrmaster.net/login',
|
||||||
|
type: 'website',
|
||||||
|
images: [{
|
||||||
|
url: 'https://www.qrmaster.net/og-image.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'QR Master Login',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: 'Login to QR Master | Access Your Dashboard',
|
||||||
|
description: 'Sign in to QR Master to create, manage, and track your QR codes.',
|
||||||
|
images: ['https://www.qrmaster.net/og-image.png'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { fetchWithCsrf, loading: csrfLoading } = useCsrf();
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setLoading(true);
|
|
||||||
setError('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetchWithCsrf('/api/auth/simple-login', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ email, password }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok && data.success) {
|
|
||||||
// Store user in localStorage for client-side
|
|
||||||
localStorage.setItem('user', JSON.stringify(data.user));
|
|
||||||
|
|
||||||
// Track successful login with PostHog
|
|
||||||
try {
|
|
||||||
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
|
|
||||||
identifyUser(data.user.id, {
|
|
||||||
email: data.user.email,
|
|
||||||
name: data.user.name,
|
|
||||||
plan: data.user.plan || 'FREE',
|
|
||||||
});
|
|
||||||
trackEvent('user_login', {
|
|
||||||
method: 'email',
|
|
||||||
email: data.user.email,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('PostHog tracking error:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for redirect parameter
|
|
||||||
const redirectUrl = searchParams.get('redirect') || '/dashboard';
|
|
||||||
router.push(redirectUrl);
|
|
||||||
router.refresh();
|
|
||||||
} else {
|
|
||||||
setError(data.error || 'Invalid email or password');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError('An error occurred. Please try again.');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGoogleSignIn = () => {
|
|
||||||
// Redirect to Google OAuth API route
|
|
||||||
window.location.href = '/api/auth/google';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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">
|
||||||
@@ -86,94 +48,13 @@ export default function LoginPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Suspense fallback={
|
||||||
<CardContent className="p-6">
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-8 flex items-center justify-center min-h-[400px]">
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
||||||
{error && (
|
</div>
|
||||||
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
|
}>
|
||||||
{error}
|
<LoginClientPage />
|
||||||
</div>
|
</Suspense>
|
||||||
)}
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Email"
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
placeholder="you@example.com"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
placeholder="••••••••"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input type="checkbox" className="mr-2" />
|
|
||||||
<span className="text-sm text-gray-600">Remember me</span>
|
|
||||||
</label>
|
|
||||||
<Link href="/forgot-password" className="text-sm text-primary-600 hover:text-primary-700">
|
|
||||||
Forgot password?
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button type="submit" className="w-full" loading={loading} disabled={csrfLoading || loading}>
|
|
||||||
{csrfLoading ? 'Loading...' : 'Sign In'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="relative my-6">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<div className="w-full border-t border-gray-300"></div>
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center text-sm">
|
|
||||||
<span className="px-2 bg-white text-gray-500">Or continue with</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="w-full"
|
|
||||||
onClick={handleGoogleSignIn}
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
fill="#4285F4"
|
|
||||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#34A853"
|
|
||||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#FBBC05"
|
|
||||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#EA4335"
|
|
||||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Sign in with Google
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-6 text-center">
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Don't have an account?{' '}
|
|
||||||
<Link href="/signup" className="text-primary-600 hover:text-primary-700 font-medium">
|
|
||||||
Sign up
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<p className="text-center text-sm text-gray-500 mt-6">
|
<p className="text-center text-sm text-gray-500 mt-6">
|
||||||
By signing in, you agree to our{' '}
|
By signing in, you agree to our{' '}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import { Input } from '@/components/ui/Input';
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { useCsrf } from '@/hooks/useCsrf';
|
import { useCsrf } from '@/hooks/useCsrf';
|
||||||
|
|
||||||
export default function ResetPasswordPage() {
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
|
function ResetPasswordContent() {
|
||||||
const { fetchWithCsrf, loading: csrfLoading } = useCsrf();
|
const { fetchWithCsrf, loading: csrfLoading } = useCsrf();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -206,3 +208,11 @@ export default function ResetPasswordPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function ResetPasswordPage() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div className="min-h-screen flex items-center justify-center">Loading...</div>}>
|
||||||
|
<ResetPasswordContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
185
src/app/(auth)/signup/ClientPage.tsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||||
|
import { Input } from '@/components/ui/Input';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { useTranslation } from '@/hooks/useTranslation';
|
||||||
|
import { useCsrf } from '@/hooks/useCsrf';
|
||||||
|
|
||||||
|
export default function SignupClientPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { fetchWithCsrf } = useCsrf();
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError('Passwords do not match');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 8) {
|
||||||
|
setError('Password must be at least 8 characters');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchWithCsrf('/api/auth/signup', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ name, email, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.success) {
|
||||||
|
// Store user in localStorage for client-side
|
||||||
|
localStorage.setItem('user', JSON.stringify(data.user));
|
||||||
|
|
||||||
|
// Track successful signup with PostHog
|
||||||
|
try {
|
||||||
|
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
|
||||||
|
identifyUser(data.user.id, {
|
||||||
|
email: data.user.email,
|
||||||
|
name: data.user.name,
|
||||||
|
plan: data.user.plan || 'FREE',
|
||||||
|
signupMethod: 'email',
|
||||||
|
});
|
||||||
|
trackEvent('user_signup', {
|
||||||
|
method: 'email',
|
||||||
|
email: data.user.email,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('PostHog tracking error:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to dashboard
|
||||||
|
router.push('/dashboard');
|
||||||
|
router.refresh();
|
||||||
|
} else {
|
||||||
|
setError(data.error || 'Failed to create account');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('An error occurred. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGoogleSignIn = () => {
|
||||||
|
// Redirect to Google OAuth API route
|
||||||
|
window.location.href = '/api/auth/google';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Full Name"
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="John Doe"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Confirm Password"
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full" loading={loading}>
|
||||||
|
Create Account
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="relative my-6">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-sm">
|
||||||
|
<span className="px-2 bg-white text-gray-500">Or continue with</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleGoogleSignIn}
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="#4285F4"
|
||||||
|
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#34A853"
|
||||||
|
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#FBBC05"
|
||||||
|
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#EA4335"
|
||||||
|
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Sign up with Google
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Already have an account?{' '}
|
||||||
|
<Link href="/login" className="text-primary-600 hover:text-primary-700 font-medium">
|
||||||
|
Sign in
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,89 +1,39 @@
|
|||||||
'use client';
|
import React, { Suspense } from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
import SignupClientPage from './ClientPage';
|
||||||
import { Input } from '@/components/ui/Input';
|
|
||||||
import { Button } from '@/components/ui/Button';
|
export const metadata: Metadata = {
|
||||||
import { useTranslation } from '@/hooks/useTranslation';
|
title: {
|
||||||
import { useCsrf } from '@/hooks/useCsrf';
|
absolute: 'Create Free Account | QR Master'
|
||||||
|
},
|
||||||
|
description: 'Sign up for QR Master to create free QR codes. Start with tracking, customization, and bulk generation features.',
|
||||||
|
alternates: {
|
||||||
|
canonical: 'https://www.qrmaster.net/signup',
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: 'Create Free Account | QR Master',
|
||||||
|
description: 'Sign up for QR Master to create free QR codes with tracking and customization.',
|
||||||
|
url: 'https://www.qrmaster.net/signup',
|
||||||
|
type: 'website',
|
||||||
|
images: [{
|
||||||
|
url: 'https://www.qrmaster.net/og-image.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'QR Master Sign Up',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: 'Create Free Account | QR Master',
|
||||||
|
description: 'Sign up for QR Master to create free QR codes with tracking and customization.',
|
||||||
|
images: ['https://www.qrmaster.net/og-image.png'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function SignupPage() {
|
export default function SignupPage() {
|
||||||
const router = useRouter();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { fetchWithCsrf } = useCsrf();
|
|
||||||
const [name, setName] = useState('');
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setLoading(true);
|
|
||||||
setError('');
|
|
||||||
|
|
||||||
if (password !== confirmPassword) {
|
|
||||||
setError('Passwords do not match');
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password.length < 8) {
|
|
||||||
setError('Password must be at least 8 characters');
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetchWithCsrf('/api/auth/signup', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ name, email, password }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok && data.success) {
|
|
||||||
// Store user in localStorage for client-side
|
|
||||||
localStorage.setItem('user', JSON.stringify(data.user));
|
|
||||||
|
|
||||||
// Track successful signup with PostHog
|
|
||||||
try {
|
|
||||||
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
|
|
||||||
identifyUser(data.user.id, {
|
|
||||||
email: data.user.email,
|
|
||||||
name: data.user.name,
|
|
||||||
plan: data.user.plan || 'FREE',
|
|
||||||
signupMethod: 'email',
|
|
||||||
});
|
|
||||||
trackEvent('user_signup', {
|
|
||||||
method: 'email',
|
|
||||||
email: data.user.email,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('PostHog tracking error:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to dashboard
|
|
||||||
router.push('/dashboard');
|
|
||||||
router.refresh();
|
|
||||||
} else {
|
|
||||||
setError(data.error || 'Failed to create account');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError('An error occurred. Please try again.');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGoogleSignIn = () => {
|
|
||||||
// Redirect to Google OAuth API route
|
|
||||||
window.location.href = '/api/auth/google';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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">
|
||||||
@@ -99,102 +49,13 @@ export default function SignupPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Suspense fallback={
|
||||||
<CardContent className="p-6">
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-8 flex items-center justify-center min-h-[500px]">
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
||||||
{error && (
|
</div>
|
||||||
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
|
}>
|
||||||
{error}
|
<SignupClientPage />
|
||||||
</div>
|
</Suspense>
|
||||||
)}
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Full Name"
|
|
||||||
type="text"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
placeholder="John Doe"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Email"
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
placeholder="you@example.com"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
placeholder="••••••••"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Confirm Password"
|
|
||||||
type="password"
|
|
||||||
value={confirmPassword}
|
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
||||||
placeholder="••••••••"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button type="submit" className="w-full" loading={loading}>
|
|
||||||
Create Account
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="relative my-6">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<div className="w-full border-t border-gray-300"></div>
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center text-sm">
|
|
||||||
<span className="px-2 bg-white text-gray-500">Or continue with</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="w-full"
|
|
||||||
onClick={handleGoogleSignIn}
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
fill="#4285F4"
|
|
||||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#34A853"
|
|
||||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#FBBC05"
|
|
||||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#EA4335"
|
|
||||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Sign up with Google
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-6 text-center">
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Already have an account?{' '}
|
|
||||||
<Link href="/login" className="text-primary-600 hover:text-primary-700 font-medium">
|
|
||||||
Sign in
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<p className="text-center text-sm text-gray-500 mt-6">
|
<p className="text-center text-sm text-gray-500 mt-6">
|
||||||
By signing up, you agree to our{' '}
|
By signing up, you agree to our{' '}
|
||||||
|
|||||||
287
src/app/(marketing)/MarketingLayout.tsx
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Footer } from '@/components/ui/Footer';
|
||||||
|
import en from '@/i18n/en.json';
|
||||||
|
import { ChevronDown, Wifi, Contact, MessageCircle, QrCode, Link2, Type, Mail, MessageSquare, Phone, Calendar, MapPin, Facebook, Instagram, Twitter, Youtube, Music, Bitcoin, CreditCard, Video, Users } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
|
||||||
|
export default function MarketingLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
const [scrolled, setScrolled] = useState(false);
|
||||||
|
const [toolsOpen, setToolsOpen] = useState(false);
|
||||||
|
const [mobileToolsOpen, setMobileToolsOpen] = useState(false);
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setScrolled(window.scrollY > 20);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check immediately on mount
|
||||||
|
handleScroll();
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Close simple menus when path changes
|
||||||
|
useEffect(() => {
|
||||||
|
setMobileMenuOpen(false);
|
||||||
|
setToolsOpen(false);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
// Default to English for general marketing pages
|
||||||
|
const t = en;
|
||||||
|
|
||||||
|
const tools = [
|
||||||
|
{ name: 'URL / Link', description: 'Link to any website', href: '/tools/url-qr-code', icon: Link2, color: 'text-blue-500', bgColor: 'bg-blue-50' },
|
||||||
|
{ name: 'Text', description: 'Plain text message', href: '/tools/text-qr-code', icon: Type, color: 'text-slate-500', bgColor: 'bg-slate-50' },
|
||||||
|
{ name: 'WiFi', description: 'Share WiFi credentials', href: '/tools/wifi-qr-code', icon: Wifi, color: 'text-indigo-500', bgColor: 'bg-indigo-50' },
|
||||||
|
{ name: 'VCard', description: 'Digital business card', href: '/tools/vcard-qr-code', icon: Contact, color: 'text-pink-500', bgColor: 'bg-pink-50' },
|
||||||
|
{ name: 'WhatsApp', description: 'Start a chat', href: '/tools/whatsapp-qr-code', icon: MessageCircle, color: 'text-green-500', bgColor: 'bg-green-50' },
|
||||||
|
{ name: 'Email', description: 'Compose an email', href: '/tools/email-qr-code', icon: Mail, color: 'text-amber-500', bgColor: 'bg-amber-50' },
|
||||||
|
{ name: 'SMS', description: 'Send a text message', href: '/tools/sms-qr-code', icon: MessageSquare, color: 'text-cyan-500', bgColor: 'bg-cyan-50' },
|
||||||
|
{ name: 'Phone', description: 'Start a call', href: '/tools/phone-qr-code', icon: Phone, color: 'text-violet-500', bgColor: 'bg-violet-50' },
|
||||||
|
{ name: 'Event', description: 'Add calendar event', href: '/tools/event-qr-code', icon: Calendar, color: 'text-red-500', bgColor: 'bg-red-50' },
|
||||||
|
{ name: 'Location', description: 'Share a place', href: '/tools/geolocation-qr-code', icon: MapPin, color: 'text-emerald-500', bgColor: 'bg-emerald-50' },
|
||||||
|
{ name: 'Facebook', description: 'Facebook profile/page', href: '/tools/facebook-qr-code', icon: Facebook, color: 'text-blue-600', bgColor: 'bg-blue-50' },
|
||||||
|
{ name: 'Instagram', description: 'Instagram profile', href: '/tools/instagram-qr-code', icon: Instagram, color: 'text-pink-600', bgColor: 'bg-pink-50' },
|
||||||
|
{ name: 'Twitter / X', description: 'Twitter profile', href: '/tools/twitter-qr-code', icon: Twitter, color: 'text-sky-500', bgColor: 'bg-sky-50' },
|
||||||
|
{ name: 'YouTube', description: 'YouTube video/channel', href: '/tools/youtube-qr-code', icon: Youtube, color: 'text-red-600', bgColor: 'bg-red-50' },
|
||||||
|
{ name: 'TikTok', description: 'TikTok profile', href: '/tools/tiktok-qr-code', icon: Music, color: 'text-slate-800', bgColor: 'bg-slate-100' },
|
||||||
|
{ name: 'Crypto', description: 'Share wallet address', href: '/tools/crypto-qr-code', icon: Bitcoin, color: 'text-orange-500', bgColor: 'bg-orange-50' },
|
||||||
|
{ name: 'PayPal', description: 'Receive payments', href: '/tools/paypal-qr-code', icon: CreditCard, color: 'text-blue-700', bgColor: 'bg-blue-50' },
|
||||||
|
{ name: 'Zoom', description: 'Join Zoom meeting', href: '/tools/zoom-qr-code', icon: Video, color: 'text-sky-500', bgColor: 'bg-sky-50' },
|
||||||
|
{ name: 'Teams', description: 'Join Teams meeting', href: '/tools/teams-qr-code', icon: Users, color: 'text-violet-500', bgColor: 'bg-violet-50' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white">
|
||||||
|
{/* Server-rendered navigation links for SEO (crawlers) - Placed first for priority */}
|
||||||
|
<div className="sr-only" aria-hidden="false">
|
||||||
|
<nav aria-label="Site Map">
|
||||||
|
<ul>
|
||||||
|
<li><a href="/">Home</a></li>
|
||||||
|
<li><a href="/pricing">Pricing</a></li>
|
||||||
|
<li><a href="/blog">Blog</a></li>
|
||||||
|
<li><a href="/faq">FAQ</a></li>
|
||||||
|
<li><a href="/login">Login</a></li>
|
||||||
|
<li><a href="/signup">Sign Up</a></li>
|
||||||
|
{/* Tools */}
|
||||||
|
<li><a href="/tools/url-qr-code">URL QR Code</a></li>
|
||||||
|
<li><a href="/tools/text-qr-code">Text QR Code</a></li>
|
||||||
|
<li><a href="/tools/wifi-qr-code">WiFi QR Code</a></li>
|
||||||
|
<li><a href="/tools/vcard-qr-code">vCard QR Code</a></li>
|
||||||
|
<li><a href="/tools/whatsapp-qr-code">WhatsApp QR Code</a></li>
|
||||||
|
<li><a href="/tools/email-qr-code">Email QR Code</a></li>
|
||||||
|
<li><a href="/tools/sms-qr-code">SMS QR Code</a></li>
|
||||||
|
<li><a href="/tools/phone-qr-code">Phone QR Code</a></li>
|
||||||
|
<li><a href="/tools/event-qr-code">Event QR Code</a></li>
|
||||||
|
<li><a href="/tools/geolocation-qr-code">Location QR Code</a></li>
|
||||||
|
<li><a href="/tools/facebook-qr-code">Facebook QR Code</a></li>
|
||||||
|
<li><a href="/tools/instagram-qr-code">Instagram QR Code</a></li>
|
||||||
|
<li><a href="/tools/twitter-qr-code">Twitter QR Code</a></li>
|
||||||
|
<li><a href="/tools/youtube-qr-code">YouTube QR Code</a></li>
|
||||||
|
<li><a href="/tools/tiktok-qr-code">TikTok QR Code</a></li>
|
||||||
|
<li><a href="/tools/crypto-qr-code">Crypto QR Code</a></li>
|
||||||
|
<li><a href="/tools/paypal-qr-code">PayPal QR Code</a></li>
|
||||||
|
<li><a href="/tools/zoom-qr-code">Zoom QR Code</a></li>
|
||||||
|
<li><a href="/tools/teams-qr-code">Teams QR Code</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<header
|
||||||
|
className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200 shadow-sm"
|
||||||
|
|
||||||
|
>
|
||||||
|
<nav className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl h-20 flex items-center justify-between">
|
||||||
|
{/* Logo */}
|
||||||
|
<Link href="/" className="flex items-center space-x-2.5 group">
|
||||||
|
<div className="relative w-9 h-9 flex items-center justify-center bg-indigo-600 rounded-lg shadow-indigo-200 shadow-lg group-hover:scale-105 transition-transform duration-200">
|
||||||
|
<QrCode className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xl font-bold text-slate-900 tracking-tight group-hover:text-indigo-600 transition-colors">QR Master</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Desktop Navigation */}
|
||||||
|
<div className="hidden md:flex items-center space-x-1">
|
||||||
|
|
||||||
|
{/* Tools Dropdown */}
|
||||||
|
<div
|
||||||
|
className="relative group px-3 py-2"
|
||||||
|
onMouseEnter={() => setToolsOpen(true)}
|
||||||
|
onMouseLeave={() => setToolsOpen(false)}
|
||||||
|
>
|
||||||
|
<button className="flex items-center space-x-1 text-sm font-medium text-slate-600 group-hover:text-slate-900 transition-colors">
|
||||||
|
<span>{t.nav.tools}</span>
|
||||||
|
<ChevronDown className={cn("w-4 h-4 transition-transform duration-200", toolsOpen && "rotate-180")} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{toolsOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 10 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
className="absolute left-1/2 -translate-x-1/2 top-full mt-2 w-[750px] bg-white rounded-2xl shadow-lg border border-slate-100 p-4 overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-3 gap-1">
|
||||||
|
{tools.map((tool) => (
|
||||||
|
<Link
|
||||||
|
key={tool.name}
|
||||||
|
href={tool.href}
|
||||||
|
className="flex items-center space-x-3 p-2.5 rounded-xl transition-colors hover:bg-slate-50"
|
||||||
|
>
|
||||||
|
<div className={cn("p-2 rounded-lg shrink-0", tool.bgColor, tool.color)}>
|
||||||
|
<tool.icon className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-semibold text-slate-900">{tool.name}</div>
|
||||||
|
<p className="text-xs text-slate-500 leading-snug">{tool.description}</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 pt-3 border-t border-slate-100 -mx-4 -mb-4 px-4 py-3 text-center bg-slate-50/50">
|
||||||
|
<p className="text-xs text-slate-500 font-medium">{t.nav.all_free}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link href="/#features" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
|
||||||
|
{t.nav.features}
|
||||||
|
</Link>
|
||||||
|
<Link href="/#pricing" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
|
||||||
|
{t.nav.pricing}
|
||||||
|
</Link>
|
||||||
|
<Link href="/blog" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
|
||||||
|
{t.nav.blog}
|
||||||
|
</Link>
|
||||||
|
<Link href="/#faq" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
|
||||||
|
{t.nav.faq}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden md:flex items-center space-x-4">
|
||||||
|
<Link href="/login" className="text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
|
||||||
|
{t.nav.login}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link href="/signup">
|
||||||
|
<Button className={cn(
|
||||||
|
"font-semibold shadow-lg shadow-indigo-500/20 transition-all hover:scale-105",
|
||||||
|
scrolled ? "bg-blue-600 text-white hover:bg-blue-700" : "bg-blue-600 text-white hover:bg-blue-700"
|
||||||
|
)}>
|
||||||
|
{t.nav.cta || "Get Started Free"}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu Button - Always dark */}
|
||||||
|
<button
|
||||||
|
className="md:hidden p-2 text-slate-900"
|
||||||
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
{mobileMenuOpen ? (
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
) : (
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
)}
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Mobile Menu */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{mobileMenuOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
className="md:hidden bg-white border-b border-slate-100 overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="container mx-auto px-4 py-6 space-y-2">
|
||||||
|
{/* Free Tools Accordion */}
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileToolsOpen(!mobileToolsOpen)}
|
||||||
|
className="flex items-center justify-between w-full px-4 py-3 rounded-xl hover:bg-slate-50 text-slate-700 font-semibold"
|
||||||
|
>
|
||||||
|
<span>{t.nav.tools}</span>
|
||||||
|
<ChevronDown className={cn("w-5 h-5 transition-transform", mobileToolsOpen && "rotate-180")} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{mobileToolsOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="max-h-[50vh] overflow-y-auto pl-4 space-y-1 border-l-2 border-slate-100 ml-4">
|
||||||
|
{tools.map((tool) => (
|
||||||
|
<Link
|
||||||
|
key={tool.name}
|
||||||
|
href={tool.href}
|
||||||
|
className="flex items-center gap-3 px-4 py-2.5 rounded-lg hover:bg-slate-50 text-slate-600 text-sm"
|
||||||
|
onClick={() => { setMobileMenuOpen(false); setMobileToolsOpen(false); }}
|
||||||
|
>
|
||||||
|
<tool.icon className={cn("w-4 h-4", tool.color)} />
|
||||||
|
{tool.name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<div className="h-px bg-slate-100 my-2"></div>
|
||||||
|
|
||||||
|
<Link href="/#features" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.features}</Link>
|
||||||
|
<Link href="/#pricing" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.pricing}</Link>
|
||||||
|
<Link href="/blog" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.blog}</Link>
|
||||||
|
<Link href="/#faq" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.faq}</Link>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 pt-4">
|
||||||
|
<Link href="/login" onClick={() => setMobileMenuOpen(false)}>
|
||||||
|
<Button variant="outline" className="w-full justify-center">{t.nav.login}</Button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/signup" onClick={() => setMobileMenuOpen(false)}>
|
||||||
|
<Button className="w-full justify-center bg-indigo-600 hover:bg-indigo-700">{t.nav.cta}</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="pt-20">
|
||||||
|
{/* Server-rendered navigation links for SEO (crawlers) */}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<Footer t={t} />
|
||||||
|
</div >
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { websiteSchema, breadcrumbSchema } from '@/lib/schema';
|
|||||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
|
import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
|
||||||
|
import { blogPostList } from '@/lib/blog-data';
|
||||||
|
|
||||||
function truncateAtWord(text: string, maxLength: number): string {
|
function truncateAtWord(text: string, maxLength: number): string {
|
||||||
if (text.length <= maxLength) return text;
|
if (text.length <= maxLength) return text;
|
||||||
@@ -18,7 +19,7 @@ function truncateAtWord(text: string, maxLength: number): string {
|
|||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const title = truncateAtWord('QR Insights: Latest QR Strategies', 60);
|
const title = truncateAtWord('QR Insights: Latest QR Strategies', 60);
|
||||||
const description = truncateAtWord(
|
const description = truncateAtWord(
|
||||||
'Expert guides on QR analytics, dynamic codes & smart marketing uses.',
|
'Expert guides on QR code analytics, dynamic vs static codes, bulk generation, and smart marketing use cases. Learn how to maximize your QR campaign ROI.',
|
||||||
160
|
160
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -37,6 +38,14 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
description,
|
description,
|
||||||
url: 'https://www.qrmaster.net/blog',
|
url: 'https://www.qrmaster.net/blog',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: 'https://www.qrmaster.net/og-image.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'QR Insights - QR Code Marketing & Analytics Blog',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
title,
|
title,
|
||||||
@@ -45,82 +54,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const blogPosts = [
|
|
||||||
// NEW POSTS (January 2026)
|
|
||||||
{
|
|
||||||
slug: 'qr-code-restaurant-menu',
|
|
||||||
title: 'How to Create a QR Code for Restaurant Menu',
|
|
||||||
excerpt: 'Step-by-step guide to creating digital menu QR codes for your restaurant. Learn best practices for touchless menus, placement tips, and tracking.',
|
|
||||||
date: 'January 5, 2026',
|
|
||||||
readTime: '12 Min',
|
|
||||||
category: 'Restaurant',
|
|
||||||
image: '/blog/restaurant-qr-menu.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'vcard-qr-code-generator',
|
|
||||||
title: 'Free vCard QR Code Generator: Digital Business Cards',
|
|
||||||
excerpt: 'Create professional vCard QR codes for digital business cards. Share contact info instantly with a scan—includes templates and best practices.',
|
|
||||||
date: 'January 5, 2026',
|
|
||||||
readTime: '10 Min',
|
|
||||||
category: 'Business Cards',
|
|
||||||
image: '/blog/vcard-qr-code.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'qr-code-small-business',
|
|
||||||
title: 'Best QR Code Generator for Small Business: 2025 Guide',
|
|
||||||
excerpt: 'Find the best QR code solution for your small business. Compare features, pricing, and use cases for marketing, payments, and operations.',
|
|
||||||
date: 'January 5, 2026',
|
|
||||||
readTime: '14 Min',
|
|
||||||
category: 'Business',
|
|
||||||
image: '/blog/small-business-qr.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'qr-code-print-size-guide',
|
|
||||||
title: 'QR Code Print Size Guide: Minimum Sizes for Every Use Case',
|
|
||||||
excerpt: 'Complete guide to QR code print sizes. Learn minimum dimensions for business cards, posters, banners, and more to ensure reliable scanning.',
|
|
||||||
date: 'January 5, 2026',
|
|
||||||
readTime: '8 Min',
|
|
||||||
category: 'Printing',
|
|
||||||
image: '/blog/qr-print-sizes.png',
|
|
||||||
},
|
|
||||||
// EXISTING POSTS
|
|
||||||
{
|
|
||||||
slug: 'qr-code-tracking-guide-2025',
|
|
||||||
title: 'QR Code Tracking: Complete Guide 2025',
|
|
||||||
excerpt: 'Learn how to track QR code scans with real-time analytics. Compare free vs paid tracking tools, setup Google Analytics, and measure ROI.',
|
|
||||||
date: 'October 18, 2025',
|
|
||||||
readTime: '12 Min',
|
|
||||||
category: 'Tracking & Analytics',
|
|
||||||
image: '/blog/1-hero.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'dynamic-vs-static-qr-codes',
|
|
||||||
title: 'Dynamic vs Static QR Codes: Which Should You Use?',
|
|
||||||
excerpt: 'Understand the difference between static and dynamic QR codes. Learn when to use each type, pros/cons, and how dynamic QR codes save money.',
|
|
||||||
date: 'October 17, 2025',
|
|
||||||
readTime: '10 Min',
|
|
||||||
category: 'QR Code Basics',
|
|
||||||
image: '/blog/2-hero.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'bulk-qr-code-generator-excel',
|
|
||||||
title: 'How to Generate Bulk QR Codes from Excel',
|
|
||||||
excerpt: 'Generate hundreds of QR codes from Excel or CSV files in minutes. Step-by-step guide with templates, best practices, and free tools.',
|
|
||||||
date: 'October 16, 2025',
|
|
||||||
readTime: '13 Min',
|
|
||||||
category: 'Bulk Generation',
|
|
||||||
image: '/blog/3-hero.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'qr-code-analytics',
|
|
||||||
title: 'QR Code Analytics: Track, Measure & Optimize Campaigns',
|
|
||||||
excerpt: 'Learn how to leverage scan analytics, campaign tracking, and dashboard insights to maximize QR code ROI.',
|
|
||||||
date: 'October 16, 2025',
|
|
||||||
readTime: '15 Min',
|
|
||||||
category: 'Analytics',
|
|
||||||
image: '/blog/4-hero.png',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function BlogPage() {
|
export default function BlogPage() {
|
||||||
const breadcrumbItems: BreadcrumbItem[] = [
|
const breadcrumbItems: BreadcrumbItem[] = [
|
||||||
@@ -145,8 +79,8 @@ export default function BlogPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto">
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto">
|
||||||
{blogPosts.map((post) => (
|
{blogPostList.map((post: any) => (
|
||||||
<Link key={post.slug} href={`/blog/${post.slug}`}>
|
<Link key={post.slug} href={post.link || `/blog/${post.slug}`}>
|
||||||
<Card hover className="h-full overflow-hidden shadow-md hover:shadow-xl transition-all duration-300">
|
<Card hover className="h-full overflow-hidden shadow-md hover:shadow-xl transition-all duration-300">
|
||||||
<div className="relative h-56 overflow-hidden">
|
<div className="relative h-56 overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
@@ -168,7 +102,9 @@ export default function BlogPage() {
|
|||||||
<p className="text-gray-600 mb-4 leading-relaxed">{post.excerpt}</p>
|
<p className="text-gray-600 mb-4 leading-relaxed">{post.excerpt}</p>
|
||||||
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
|
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
|
||||||
<p className="text-sm text-gray-500">{post.date}</p>
|
<p className="text-sm text-gray-500">{post.date}</p>
|
||||||
<span className="text-primary-600 text-sm font-medium">Read more →</span>
|
<span className="text-primary-600 text-sm font-medium">
|
||||||
|
{post.link ? 'Try Now →' : 'Read Article →'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
|
|||||||
import { breadcrumbSchema } from '@/lib/schema';
|
import { breadcrumbSchema } from '@/lib/schema';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel | QR Master',
|
title: 'Bulk QR Code Generator | Create from Excel | QR Master',
|
||||||
description: 'Generate hundreds of QR codes at once from CSV or Excel files. Create URLs, vCards, locations, phone numbers, and text QR codes in bulk. Perfect for products, events, inventory management.',
|
description: 'Generate hundreds of QR codes instantly from Excel/CSV. Create URLs, vCards, and text codes in bulk. Perfect for inventory, events, and product tagging.',
|
||||||
keywords: 'bulk qr code generator, batch qr code, qr code from excel, csv qr code generator, mass qr code generation, bulk vcard qr code, bulk qr codes free',
|
keywords: 'bulk qr code generator, batch qr code, qr code from excel, csv qr code generator, mass qr code generation, bulk vcard qr code, bulk qr codes free',
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
canonical: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
||||||
@@ -23,6 +23,14 @@ export const metadata: Metadata = {
|
|||||||
description: 'Generate hundreds of QR codes at once from CSV or Excel files. Perfect for products, events, and inventory.',
|
description: 'Generate hundreds of QR codes at once from CSV or Excel files. Perfect for products, events, and inventory.',
|
||||||
url: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
url: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: 'https://www.qrmaster.net/og-image.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'Bulk QR Code Generator - QR Master',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel',
|
title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel',
|
||||||
@@ -46,7 +54,7 @@ export default function BulkQRCodeGeneratorPage() {
|
|||||||
title: 'Contact Cards',
|
title: 'Contact Cards',
|
||||||
description: 'Create vCard QR codes with contact information',
|
description: 'Create vCard QR codes with contact information',
|
||||||
format: 'FirstName,LastName,Email,Phone,Organization,Title',
|
format: 'FirstName,LastName,Email,Phone,Organization,Title',
|
||||||
example: 'John Doe,VCARD,John,Doe,john@example.com,+1234567890,Company Inc,CEO',
|
example: 'John Doe,VCARD,John,Doe,john' + '@' + 'example.com,+1234567890,Company Inc,CEO',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'GEO',
|
type: 'GEO',
|
||||||
@@ -333,7 +341,7 @@ export default function BulkQRCodeGeneratorPage() {
|
|||||||
Start Bulk Generation
|
Start Bulk Generation
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/create">
|
<Link href="/signup">
|
||||||
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
|
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
|
||||||
Try Single QR First
|
Try Single QR First
|
||||||
</Button>
|
</Button>
|
||||||
@@ -440,7 +448,7 @@ export default function BulkQRCodeGeneratorPage() {
|
|||||||
<tr className="border-b border-gray-200">
|
<tr className="border-b border-gray-200">
|
||||||
<td className="py-2 px-3">John Doe</td>
|
<td className="py-2 px-3">John Doe</td>
|
||||||
<td className="py-2 px-3">VCARD</td>
|
<td className="py-2 px-3">VCARD</td>
|
||||||
<td className="py-2 px-3">John,Doe,john@example.com,+1234567890,Company,CEO</td>
|
<td className="py-2 px-3">John,Doe,john{'@'}example.com,+1234567890,Company,CEO</td>
|
||||||
<td className="py-2 px-3">contact</td>
|
<td className="py-2 px-3">contact</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="border-b border-gray-200">
|
<tr className="border-b border-gray-200">
|
||||||
@@ -633,26 +641,35 @@ Product C,https://example.com/product-c,Budget Widget,electronics,sale`}
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* CTA Section */}
|
{/* CTA Section */}
|
||||||
<section className="py-20 bg-gradient-to-r from-green-600 to-blue-600 text-white">
|
{/* CTA Section */}
|
||||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center">
|
<section className="py-24 bg-slate-900 relative overflow-hidden">
|
||||||
<h2 className="text-4xl font-bold mb-6">
|
{/* Background Decorations */}
|
||||||
Generate 1000s of QR Codes in Minutes
|
<div className="absolute top-0 right-0 -mr-20 -mt-20 w-96 h-96 bg-blue-500/20 rounded-full blur-3xl opacity-50" />
|
||||||
|
<div className="absolute bottom-0 left-0 -ml-20 -mb-20 w-80 h-80 bg-green-500/20 rounded-full blur-3xl opacity-50" />
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center relative z-10">
|
||||||
|
<h2 className="text-4xl lg:text-5xl font-bold mb-6 text-white tracking-tight">
|
||||||
|
Ready to Generate <span className="text-transparent bg-clip-text bg-gradient-to-r from-green-400 to-blue-400">1000s of Codes?</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xl mb-8 text-green-100">
|
<p className="text-xl mb-10 text-slate-300 leading-relaxed max-w-2xl mx-auto">
|
||||||
Save hours of manual work. Upload your file and get all QR codes ready instantly.
|
Stop doing it manually. Upload your Excel file and get your QR codes in seconds. Professional, branded, and trackable.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
<div className="flex flex-col sm:flex-row gap-5 justify-center">
|
||||||
<Link href="/signup">
|
<Link href="/signup">
|
||||||
<Button size="lg" variant="secondary" className="text-lg px-8 py-4 w-full sm:w-auto bg-white text-green-600 hover:bg-gray-100">
|
<Button size="lg" className="text-lg px-8 py-6 h-auto w-full sm:w-auto bg-white text-slate-900 hover:bg-slate-50 font-bold shadow-xl shadow-blue-900/20 transition-all hover:-translate-y-1">
|
||||||
Start Bulk Generation
|
Start Bulk Generation
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/pricing">
|
<Link href="/pricing">
|
||||||
<Button size="lg" variant="outline" className="text-lg px-8 py-4 w-full sm:w-auto border-white text-white hover:bg-white/10">
|
<Button size="lg" variant="outline" className="text-lg px-8 py-6 h-auto w-full sm:w-auto border-slate-700 text-white hover:bg-slate-800 hover:border-slate-600 transition-all">
|
||||||
View Pricing
|
View Pricing
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-8 text-sm text-slate-500">
|
||||||
|
No credit card required for free trial.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
|
|||||||
import { breadcrumbSchema } from '@/lib/schema';
|
import { breadcrumbSchema } from '@/lib/schema';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master',
|
title: 'Dynamic QR Code Generator | Edit & Track QR | QR Master',
|
||||||
description: 'Create dynamic QR codes that can be edited after printing. Change destination URL, track scans, and update content without reprinting. Free dynamic QR code generator.',
|
description: 'Create editable dynamic QR codes. Update destination URLs, track scans, and manage content anytime without reprinting. Free generator with analytics.',
|
||||||
keywords: 'dynamic qr code generator, editable qr code, dynamic qr code, free dynamic qr code, qr code generator dynamic, best dynamic qr code generator',
|
keywords: 'dynamic qr code generator, editable qr code, dynamic qr code, free dynamic qr code, qr code generator dynamic, best dynamic qr code generator',
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: 'https://www.qrmaster.net/dynamic-qr-code-generator',
|
canonical: 'https://www.qrmaster.net/dynamic-qr-code-generator',
|
||||||
@@ -23,6 +23,14 @@ export const metadata: Metadata = {
|
|||||||
description: 'Create dynamic QR codes that can be edited after printing. Change URLs, track scans, and update content anytime.',
|
description: 'Create dynamic QR codes that can be edited after printing. Change URLs, track scans, and update content anytime.',
|
||||||
url: 'https://www.qrmaster.net/dynamic-qr-code-generator',
|
url: 'https://www.qrmaster.net/dynamic-qr-code-generator',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: 'https://www.qrmaster.net/og-image.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'Dynamic QR Code Generator - QR Master',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master',
|
title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master',
|
||||||
@@ -180,7 +188,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
|||||||
position: 2,
|
position: 2,
|
||||||
name: 'Generate QR Code',
|
name: 'Generate QR Code',
|
||||||
text: 'Enter your destination URL and customize the design with your branding',
|
text: 'Enter your destination URL and customize the design with your branding',
|
||||||
url: 'https://www.qrmaster.net/create',
|
url: 'https://www.qrmaster.net/signup',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@type': 'HowToStep',
|
'@type': 'HowToStep',
|
||||||
@@ -504,7 +512,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
|||||||
Get Started Free
|
Get Started Free
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/create">
|
<Link href="/signup">
|
||||||
<Button size="lg" variant="outline" className="text-lg px-8 py-4 w-full sm:w-auto border-white text-white hover:bg-white/10">
|
<Button size="lg" variant="outline" className="text-lg px-8 py-4 w-full sm:w-auto border-white text-white hover:bg-white/10">
|
||||||
Create QR Code Now
|
Create QR Code Now
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
22
src/app/(marketing)/faq/ContactSupport.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
|
||||||
|
|
||||||
|
export function ContactSupport() {
|
||||||
|
return (
|
||||||
|
<div className="mt-16 bg-blue-50 border-l-4 border-blue-500 p-8 rounded-r-lg">
|
||||||
|
<h2 className="text-2xl font-bold mb-4 text-gray-900">
|
||||||
|
Still have questions?
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-700 mb-6 leading-relaxed">
|
||||||
|
Our support team is here to help. Contact us at{' '}
|
||||||
|
<ObfuscatedMailto
|
||||||
|
email="support@qrmaster.net"
|
||||||
|
className="text-blue-600 hover:text-blue-700 font-semibold"
|
||||||
|
/>{' '}
|
||||||
|
or reach out through our live chat.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import type { Metadata } from 'next';
|
|||||||
import SeoJsonLd from '@/components/SeoJsonLd';
|
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||||
import { faqPageSchema } from '@/lib/schema';
|
import { faqPageSchema } from '@/lib/schema';
|
||||||
import { Card, CardContent } from '@/components/ui/Card';
|
import { Card, CardContent } from '@/components/ui/Card';
|
||||||
|
import { ContactSupport } from './ContactSupport';
|
||||||
|
|
||||||
function truncateAtWord(text: string, maxLength: number): string {
|
function truncateAtWord(text: string, maxLength: number): string {
|
||||||
if (text.length <= maxLength) return text;
|
if (text.length <= maxLength) return text;
|
||||||
@@ -14,7 +15,7 @@ function truncateAtWord(text: string, maxLength: number): string {
|
|||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const title = truncateAtWord('QR Master FAQ: Dynamic & Bulk QR', 60);
|
const title = truncateAtWord('QR Master FAQ: Dynamic & Bulk QR', 60);
|
||||||
const description = truncateAtWord(
|
const description = truncateAtWord(
|
||||||
'All answers: dynamic QR, security, analytics, bulk, events & print.',
|
'Find answers about dynamic QR codes, scan tracking, security, bulk generation, and event QR codes. Everything you need to know about QR Master features.',
|
||||||
160
|
160
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -33,6 +34,14 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
description,
|
description,
|
||||||
url: 'https://www.qrmaster.net/faq',
|
url: 'https://www.qrmaster.net/faq',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: 'https://www.qrmaster.net/og-image.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'QR Master FAQ',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
title,
|
title,
|
||||||
@@ -123,18 +132,7 @@ export default function FAQPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-16 bg-blue-50 border-l-4 border-blue-500 p-8 rounded-r-lg">
|
<ContactSupport />
|
||||||
<h2 className="text-2xl font-bold mb-4 text-gray-900">
|
|
||||||
Still have questions?
|
|
||||||
</h2>
|
|
||||||
<p className="text-lg text-gray-700 mb-6 leading-relaxed">
|
|
||||||
Our support team is here to help. Contact us at{' '}
|
|
||||||
<a href="mailto:support@qrmaster.net" className="text-blue-600 hover:text-blue-700 font-semibold">
|
|
||||||
support@qrmaster.net
|
|
||||||
</a>{' '}
|
|
||||||
or reach out through our live chat.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,244 +1,76 @@
|
|||||||
'use client';
|
import type { Metadata } from 'next';
|
||||||
|
import '@/styles/globals.css';
|
||||||
|
import { Providers } from '@/components/Providers';
|
||||||
|
import MarketingLayout from './MarketingLayout';
|
||||||
|
// Import schema functions from library
|
||||||
|
import { organizationSchema, websiteSchema } from '@/lib/schema';
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
const isIndexable = process.env.NEXT_PUBLIC_INDEXABLE === 'true';
|
||||||
import Link from 'next/link';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { Button } from '@/components/ui/Button';
|
|
||||||
import { Footer } from '@/components/ui/Footer';
|
|
||||||
import en from '@/i18n/en.json';
|
|
||||||
import { ChevronDown, Wifi, Contact, MessageCircle, QrCode, Link2, Type, Mail, MessageSquare, Phone, Calendar, MapPin, Facebook, Instagram, Twitter, Youtube, Music, Bitcoin, CreditCard, Video, Users } from 'lucide-react';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
|
||||||
|
|
||||||
export default function MarketingLayout({
|
export const metadata: Metadata = {
|
||||||
children,
|
metadataBase: new URL('https://www.qrmaster.net'),
|
||||||
|
title: {
|
||||||
|
default: 'QR Master – Smart QR Generator & Analytics',
|
||||||
|
template: '%s | QR Master',
|
||||||
|
},
|
||||||
|
description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics.',
|
||||||
|
keywords: 'QR code, QR generator, dynamic QR, QR tracking, QR analytics, branded QR, bulk QR generator',
|
||||||
|
robots: isIndexable
|
||||||
|
? { index: true, follow: true }
|
||||||
|
: { index: false, follow: false },
|
||||||
|
icons: {
|
||||||
|
icon: [
|
||||||
|
{ url: '/favicon.svg', type: 'image/svg+xml' },
|
||||||
|
{ url: '/logo.svg', type: 'image/svg+xml' },
|
||||||
|
],
|
||||||
|
apple: '/logo.svg',
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
site: '@qrmaster',
|
||||||
|
images: ['https://www.qrmaster.net/og-image.png'],
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
type: 'website',
|
||||||
|
siteName: 'QR Master',
|
||||||
|
title: 'QR Master – Smart QR Generator & Analytics',
|
||||||
|
description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics.',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: 'https://www.qrmaster.net/og-image.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'QR Master - Dynamic QR Code Generator and Analytics Platform',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
locale: 'en_US',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootMarketingLayout({
|
||||||
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
return (
|
||||||
const [scrolled, setScrolled] = useState(false);
|
<html lang="en">
|
||||||
const [toolsOpen, setToolsOpen] = useState(false);
|
<head>
|
||||||
const [mobileToolsOpen, setMobileToolsOpen] = useState(false);
|
<script
|
||||||
const pathname = usePathname();
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema()) }}
|
||||||
useEffect(() => {
|
/>
|
||||||
const handleScroll = () => {
|
<script
|
||||||
setScrolled(window.scrollY > 20);
|
type="application/ld+json"
|
||||||
};
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema()) }}
|
||||||
|
/>
|
||||||
// Check immediately on mount
|
</head>
|
||||||
handleScroll();
|
<body className="font-sans">
|
||||||
|
<Providers>
|
||||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
<MarketingLayout>
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
{children}
|
||||||
}, []);
|
</MarketingLayout>
|
||||||
|
</Providers>
|
||||||
// Close simple menus when path changes
|
</body>
|
||||||
useEffect(() => {
|
</html>
|
||||||
setMobileMenuOpen(false);
|
);
|
||||||
setToolsOpen(false);
|
|
||||||
}, [pathname]);
|
|
||||||
|
|
||||||
// Always use English for marketing pages
|
|
||||||
const t = en;
|
|
||||||
|
|
||||||
const tools = [
|
|
||||||
{ name: 'URL / Link', description: 'Link to any website', href: '/tools/url-qr-code', icon: Link2, color: 'text-blue-500', bgColor: 'bg-blue-50' },
|
|
||||||
{ name: 'Text', description: 'Plain text message', href: '/tools/text-qr-code', icon: Type, color: 'text-slate-500', bgColor: 'bg-slate-50' },
|
|
||||||
{ name: 'WiFi', description: 'Share WiFi credentials', href: '/tools/wifi-qr-code', icon: Wifi, color: 'text-indigo-500', bgColor: 'bg-indigo-50' },
|
|
||||||
{ name: 'VCard', description: 'Digital business card', href: '/tools/vcard-qr-code', icon: Contact, color: 'text-pink-500', bgColor: 'bg-pink-50' },
|
|
||||||
{ name: 'WhatsApp', description: 'Start a chat', href: '/tools/whatsapp-qr-code', icon: MessageCircle, color: 'text-green-500', bgColor: 'bg-green-50' },
|
|
||||||
{ name: 'Email', description: 'Compose an email', href: '/tools/email-qr-code', icon: Mail, color: 'text-amber-500', bgColor: 'bg-amber-50' },
|
|
||||||
{ name: 'SMS', description: 'Send a text message', href: '/tools/sms-qr-code', icon: MessageSquare, color: 'text-cyan-500', bgColor: 'bg-cyan-50' },
|
|
||||||
{ name: 'Phone', description: 'Start a call', href: '/tools/phone-qr-code', icon: Phone, color: 'text-violet-500', bgColor: 'bg-violet-50' },
|
|
||||||
{ name: 'Event', description: 'Add calendar event', href: '/tools/event-qr-code', icon: Calendar, color: 'text-red-500', bgColor: 'bg-red-50' },
|
|
||||||
{ name: 'Location', description: 'Share a place', href: '/tools/geolocation-qr-code', icon: MapPin, color: 'text-emerald-500', bgColor: 'bg-emerald-50' },
|
|
||||||
{ name: 'Facebook', description: 'Facebook profile/page', href: '/tools/facebook-qr-code', icon: Facebook, color: 'text-blue-600', bgColor: 'bg-blue-50' },
|
|
||||||
{ name: 'Instagram', description: 'Instagram profile', href: '/tools/instagram-qr-code', icon: Instagram, color: 'text-pink-600', bgColor: 'bg-pink-50' },
|
|
||||||
{ name: 'Twitter / X', description: 'Twitter profile', href: '/tools/twitter-qr-code', icon: Twitter, color: 'text-sky-500', bgColor: 'bg-sky-50' },
|
|
||||||
{ name: 'YouTube', description: 'YouTube video/channel', href: '/tools/youtube-qr-code', icon: Youtube, color: 'text-red-600', bgColor: 'bg-red-50' },
|
|
||||||
{ name: 'TikTok', description: 'TikTok profile', href: '/tools/tiktok-qr-code', icon: Music, color: 'text-slate-800', bgColor: 'bg-slate-100' },
|
|
||||||
{ name: 'Crypto', description: 'Share wallet address', href: '/tools/crypto-qr-code', icon: Bitcoin, color: 'text-orange-500', bgColor: 'bg-orange-50' },
|
|
||||||
{ name: 'PayPal', description: 'Receive payments', href: '/tools/paypal-qr-code', icon: CreditCard, color: 'text-blue-700', bgColor: 'bg-blue-50' },
|
|
||||||
{ name: 'Zoom', description: 'Join Zoom meeting', href: '/tools/zoom-qr-code', icon: Video, color: 'text-sky-500', bgColor: 'bg-sky-50' },
|
|
||||||
{ name: 'Teams', description: 'Join Teams meeting', href: '/tools/teams-qr-code', icon: Users, color: 'text-violet-500', bgColor: 'bg-violet-50' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-white">
|
|
||||||
{/* Header */}
|
|
||||||
<header
|
|
||||||
className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200 shadow-sm"
|
|
||||||
>
|
|
||||||
<nav className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl h-20 flex items-center justify-between">
|
|
||||||
{/* Logo */}
|
|
||||||
<Link href="/" className="flex items-center space-x-2.5 group">
|
|
||||||
<div className="relative w-9 h-9 flex items-center justify-center bg-indigo-600 rounded-lg shadow-indigo-200 shadow-lg group-hover:scale-105 transition-transform duration-200">
|
|
||||||
<QrCode className="w-5 h-5 text-white" />
|
|
||||||
</div>
|
|
||||||
<span className="text-xl font-bold text-slate-900 tracking-tight group-hover:text-indigo-600 transition-colors">QR Master</span>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
|
|
||||||
{/* Desktop Navigation */}
|
|
||||||
<div className="hidden md:flex items-center space-x-1">
|
|
||||||
|
|
||||||
{/* Tools Dropdown */}
|
|
||||||
<div
|
|
||||||
className="relative group px-3 py-2"
|
|
||||||
onMouseEnter={() => setToolsOpen(true)}
|
|
||||||
onMouseLeave={() => setToolsOpen(false)}
|
|
||||||
>
|
|
||||||
<button className="flex items-center space-x-1 text-sm font-medium text-slate-600 group-hover:text-slate-900 transition-colors">
|
|
||||||
<span>Free Tools</span>
|
|
||||||
<ChevronDown className={cn("w-4 h-4 transition-transform duration-200", toolsOpen && "rotate-180")} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{toolsOpen && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, y: 10 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
className="absolute left-1/2 -translate-x-1/2 top-full mt-2 w-[750px] bg-white rounded-2xl shadow-lg border border-slate-100 p-4 overflow-hidden"
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-3 gap-1">
|
|
||||||
{tools.map((tool) => (
|
|
||||||
<Link
|
|
||||||
key={tool.name}
|
|
||||||
href={tool.href}
|
|
||||||
className="flex items-center space-x-3 p-2.5 rounded-xl transition-colors hover:bg-slate-50"
|
|
||||||
>
|
|
||||||
<div className={cn("p-2 rounded-lg shrink-0", tool.bgColor, tool.color)}>
|
|
||||||
<tool.icon className="w-4 h-4" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-sm font-semibold text-slate-900">{tool.name}</div>
|
|
||||||
<p className="text-xs text-slate-500 leading-snug">{tool.description}</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 pt-3 border-t border-slate-100 -mx-4 -mb-4 px-4 py-3 text-center bg-slate-50/50">
|
|
||||||
<p className="text-xs text-slate-500 font-medium">All generators are 100% free</p>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Link href="/#features" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
|
|
||||||
{t.nav.features}
|
|
||||||
</Link>
|
|
||||||
<Link href="/#pricing" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
|
|
||||||
{t.nav.pricing}
|
|
||||||
</Link>
|
|
||||||
<Link href="/blog" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
|
|
||||||
{t.nav.blog}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="hidden md:flex items-center space-x-4">
|
|
||||||
<Link href="/login" className="text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
|
|
||||||
{t.nav.login}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link href="/signup">
|
|
||||||
<Button className={cn(
|
|
||||||
"font-semibold shadow-lg shadow-indigo-500/20 transition-all hover:scale-105",
|
|
||||||
scrolled ? "bg-blue-600 text-white hover:bg-blue-700" : "bg-blue-600 text-white hover:bg-blue-700"
|
|
||||||
)}>
|
|
||||||
{t.nav.cta || "Get Started Free"}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Menu Button - Always dark */}
|
|
||||||
<button
|
|
||||||
className="md:hidden p-2 text-slate-900"
|
|
||||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
||||||
aria-label="Toggle menu"
|
|
||||||
>
|
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
{mobileMenuOpen ? (
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
||||||
) : (
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
||||||
)}
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* Mobile Menu */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{mobileMenuOpen && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, height: 0 }}
|
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
|
||||||
exit={{ opacity: 0, height: 0 }}
|
|
||||||
className="md:hidden bg-white border-b border-slate-100 overflow-hidden"
|
|
||||||
>
|
|
||||||
<div className="container mx-auto px-4 py-6 space-y-2">
|
|
||||||
{/* Free Tools Accordion */}
|
|
||||||
<button
|
|
||||||
onClick={() => setMobileToolsOpen(!mobileToolsOpen)}
|
|
||||||
className="flex items-center justify-between w-full px-4 py-3 rounded-xl hover:bg-slate-50 text-slate-700 font-semibold"
|
|
||||||
>
|
|
||||||
<span>Free Tools</span>
|
|
||||||
<ChevronDown className={cn("w-5 h-5 transition-transform", mobileToolsOpen && "rotate-180")} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{mobileToolsOpen && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, height: 0 }}
|
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
|
||||||
exit={{ opacity: 0, height: 0 }}
|
|
||||||
className="overflow-hidden"
|
|
||||||
>
|
|
||||||
<div className="max-h-[50vh] overflow-y-auto pl-4 space-y-1 border-l-2 border-slate-100 ml-4">
|
|
||||||
{tools.map((tool) => (
|
|
||||||
<Link
|
|
||||||
key={tool.name}
|
|
||||||
href={tool.href}
|
|
||||||
className="flex items-center gap-3 px-4 py-2.5 rounded-lg hover:bg-slate-50 text-slate-600 text-sm"
|
|
||||||
onClick={() => { setMobileMenuOpen(false); setMobileToolsOpen(false); }}
|
|
||||||
>
|
|
||||||
<tool.icon className={cn("w-4 h-4", tool.color)} />
|
|
||||||
{tool.name}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<div className="h-px bg-slate-100 my-2"></div>
|
|
||||||
|
|
||||||
<Link href="/#features" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.features}</Link>
|
|
||||||
<Link href="/#pricing" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.pricing}</Link>
|
|
||||||
<Link href="/blog" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.blog}</Link>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 pt-4">
|
|
||||||
<Link href="/login" onClick={() => setMobileMenuOpen(false)}>
|
|
||||||
<Button variant="outline" className="w-full justify-center">Log in</Button>
|
|
||||||
</Link>
|
|
||||||
<Link href="/signup" onClick={() => setMobileMenuOpen(false)}>
|
|
||||||
<Button className="w-full justify-center bg-indigo-600 hover:bg-indigo-700">Get Started</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<main className="pt-20">{children}</main>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<Footer />
|
|
||||||
</div >
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
754
src/app/(marketing)/newsletter/NewsletterClient.tsx
Normal file
@@ -0,0 +1,754 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Card } from '@/components/ui/Card';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Badge } from '@/components/ui/Badge';
|
||||||
|
import {
|
||||||
|
Mail,
|
||||||
|
Users,
|
||||||
|
QrCode,
|
||||||
|
BarChart3,
|
||||||
|
TrendingUp,
|
||||||
|
Crown,
|
||||||
|
Activity,
|
||||||
|
Loader2,
|
||||||
|
Lock,
|
||||||
|
LogOut,
|
||||||
|
Zap,
|
||||||
|
Send,
|
||||||
|
CheckCircle2,
|
||||||
|
FileDown,
|
||||||
|
DollarSign,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface AdminStats {
|
||||||
|
users: {
|
||||||
|
total: number;
|
||||||
|
premium: number;
|
||||||
|
newThisWeek: number;
|
||||||
|
newThisMonth: number;
|
||||||
|
recent: Array<{
|
||||||
|
email: string;
|
||||||
|
name: string | null;
|
||||||
|
plan: string;
|
||||||
|
createdAt: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
qrCodes: {
|
||||||
|
total: number;
|
||||||
|
dynamic: number;
|
||||||
|
static: number;
|
||||||
|
active: number;
|
||||||
|
};
|
||||||
|
scans: {
|
||||||
|
total: number;
|
||||||
|
dynamicOnly: number;
|
||||||
|
avgPerDynamicQR: string;
|
||||||
|
};
|
||||||
|
newsletter: {
|
||||||
|
subscribers: number;
|
||||||
|
};
|
||||||
|
topQRCodes: Array<{
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
scans: number;
|
||||||
|
owner: string;
|
||||||
|
createdAt: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NewsletterClient() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
|
const [isAuthenticating, setIsAuthenticating] = useState(true);
|
||||||
|
const [loginError, setLoginError] = useState('');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
|
||||||
|
const [stats, setStats] = useState<AdminStats | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Newsletter management state
|
||||||
|
const [newsletterData, setNewsletterData] = useState<{
|
||||||
|
total: number;
|
||||||
|
recent: Array<{ email: string; createdAt: string }>;
|
||||||
|
} | null>(null);
|
||||||
|
const [sendingBroadcast, setSendingBroadcast] = useState(false);
|
||||||
|
const [broadcastResult, setBroadcastResult] = useState<{
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
// Lead management state
|
||||||
|
const [leadData, setLeadData] = useState<{
|
||||||
|
total: number;
|
||||||
|
recent: Array<{
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
source: string;
|
||||||
|
reprintCost: number | null;
|
||||||
|
updatesPerYear: number | null;
|
||||||
|
annualSavings: number | null;
|
||||||
|
createdAt: string;
|
||||||
|
}>;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkAuth();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const checkAuth = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/stats');
|
||||||
|
if (response.ok) {
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
const data = await response.json();
|
||||||
|
setStats(data);
|
||||||
|
setLoading(false);
|
||||||
|
// Also fetch newsletter and lead data
|
||||||
|
fetchNewsletterData();
|
||||||
|
fetchLeadsData();
|
||||||
|
} else {
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
} finally {
|
||||||
|
setIsAuthenticating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchNewsletterData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/newsletter/broadcast');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setNewsletterData(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch newsletter data:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchLeadsData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/leads');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setLeadData(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch leads data:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendBroadcast = async () => {
|
||||||
|
if (!confirm(`Are you sure you want to send the AI Feature Launch email to all ${newsletterData?.total || 0} subscribers?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSendingBroadcast(true);
|
||||||
|
setBroadcastResult(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/newsletter/broadcast', {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setBroadcastResult({
|
||||||
|
success: true,
|
||||||
|
message: data.message || `Successfully sent to ${data.sent} subscribers!`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setBroadcastResult({
|
||||||
|
success: false,
|
||||||
|
message: data.error || 'Failed to send broadcast',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setBroadcastResult({
|
||||||
|
success: false,
|
||||||
|
message: 'Network error. Please try again.',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSendingBroadcast(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoginError('');
|
||||||
|
setIsAuthenticating(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/newsletter/admin-login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
await checkAuth();
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
setLoginError(data.error || 'Invalid credentials');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setLoginError('Login failed. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setIsAuthenticating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await fetch('/api/auth/logout', { method: 'POST' });
|
||||||
|
router.push('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Login Screen
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-950/20 dark:to-pink-950/20 px-4">
|
||||||
|
<Card className="w-full max-w-md p-8">
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<div className="w-16 h-16 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Lock className="w-8 h-8 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-bold mb-2">Admin Dashboard</h1>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Sign in to access admin panel
|
||||||
|
</p>
|
||||||
|
<Link href="/" className="text-sm text-slate-500 hover:text-slate-900 block mt-2">
|
||||||
|
← Back to Home
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleLogin} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="admin@example.com"
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 rounded-xl bg-background border border-border focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 outline-none transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 rounded-xl bg-background border border-border focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 outline-none transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loginError && (
|
||||||
|
<p className="text-sm text-red-600 dark:text-red-400">{loginError}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isAuthenticating}
|
||||||
|
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white"
|
||||||
|
>
|
||||||
|
{isAuthenticating ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Signing in...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Sign In'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 pt-6 border-t text-center">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Admin credentials required
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin Dashboard
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-purple-50/30 to-pink-50/30 dark:from-purple-950/10 dark:to-pink-950/10">
|
||||||
|
<div className="container mx-auto px-4 py-8 max-w-7xl">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between mb-8">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold mb-2">Admin Dashboard</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Platform overview and statistics
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleLogout}
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Stats Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
|
{/* All Time Users */}
|
||||||
|
<Card className="p-6 hover:shadow-lg transition-shadow">
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/20 rounded-lg flex items-center justify-center">
|
||||||
|
<Users className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
|
||||||
|
All Time
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-3xl font-bold mb-1">{stats?.users.total || 0}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Total Users</p>
|
||||||
|
<div className="mt-3 pt-3 border-t space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-muted-foreground">This Month</span>
|
||||||
|
<span className="text-sm font-semibold text-green-600 dark:text-green-400">
|
||||||
|
+{stats?.users.newThisMonth || 0}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-muted-foreground">This Week</span>
|
||||||
|
<span className="text-sm font-semibold text-green-600 dark:text-green-400">
|
||||||
|
+{stats?.users.newThisWeek || 0}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Dynamic QR Codes */}
|
||||||
|
<Card className="p-6 hover:shadow-lg transition-shadow">
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900/20 rounded-lg flex items-center justify-center">
|
||||||
|
<QrCode className="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
|
||||||
|
Dynamic
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-3xl font-bold mb-1">{stats?.qrCodes.dynamic || 0}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Dynamic QR Codes</p>
|
||||||
|
<div className="mt-3 pt-3 border-t flex items-center justify-between">
|
||||||
|
<span className="text-xs text-muted-foreground">Static</span>
|
||||||
|
<span className="text-sm font-semibold">{stats?.qrCodes.static || 0}</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Total Scans */}
|
||||||
|
<Card className="p-6 hover:shadow-lg transition-shadow">
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/20 rounded-lg flex items-center justify-center">
|
||||||
|
<BarChart3 className="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
|
||||||
|
All Time
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-3xl font-bold mb-1">
|
||||||
|
{stats?.scans.dynamicOnly.toLocaleString() || 0}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Dynamic QR Scans</p>
|
||||||
|
<div className="mt-3 pt-3 border-t flex items-center justify-between">
|
||||||
|
<span className="text-xs text-muted-foreground">Avg per QR</span>
|
||||||
|
<span className="text-sm font-semibold">{stats?.scans.avgPerDynamicQR || 0}</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Total QR Codes */}
|
||||||
|
<Card className="p-6 hover:shadow-lg transition-shadow">
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/20 rounded-lg flex items-center justify-center">
|
||||||
|
<QrCode className="w-6 h-6 text-amber-600 dark:text-amber-400" />
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
|
||||||
|
All Time
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-3xl font-bold mb-1">{stats?.qrCodes.total || 0}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Total QR Codes</p>
|
||||||
|
<div className="mt-3 pt-3 border-t space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-muted-foreground">Dynamic</span>
|
||||||
|
<span className="text-sm font-semibold">{stats?.qrCodes.dynamic || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-muted-foreground">Static</span>
|
||||||
|
<span className="text-sm font-semibold">{stats?.qrCodes.static || 0}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Secondary Stats Row */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||||
|
{/* Total All Scans */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-12 h-12 bg-indigo-100 dark:bg-indigo-900/20 rounded-lg flex items-center justify-center">
|
||||||
|
<Zap className="w-6 h-6 text-indigo-600 dark:text-indigo-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-bold">
|
||||||
|
{stats?.scans.total.toLocaleString() || 0}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Total All Scans</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Total QR Codes */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-12 h-12 bg-pink-100 dark:bg-pink-900/20 rounded-lg flex items-center justify-center">
|
||||||
|
<QrCode className="w-6 h-6 text-pink-600 dark:text-pink-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-bold">{stats?.qrCodes.total || 0}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Total QR Codes</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Premium Users */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/20 rounded-lg flex items-center justify-center">
|
||||||
|
<Crown className="w-6 h-6 text-amber-600 dark:text-amber-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-bold">{stats?.users.premium || 0}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Premium Users</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Grid */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{/* Top QR Codes */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="w-10 h-10 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-lg flex items-center justify-center">
|
||||||
|
<TrendingUp className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-lg">Top QR Codes</h3>
|
||||||
|
<p className="text-xs text-muted-foreground">Most scanned</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{stats?.topQRCodes && stats.topQRCodes.length > 0 ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{stats.topQRCodes.map((qr, index) => (
|
||||||
|
<div
|
||||||
|
key={qr.id}
|
||||||
|
className="flex items-center justify-between py-3 border-b border-border last:border-0"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
|
<div className="w-8 h-8 bg-gradient-to-br from-purple-500 to-pink-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||||
|
<span className="text-white text-sm font-bold">
|
||||||
|
#{index + 1}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-sm font-medium truncate">{qr.title}</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
|
{qr.owner}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right flex-shrink-0 ml-4">
|
||||||
|
<p className="text-lg font-bold">{qr.scans.toLocaleString()}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">scans</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">No QR codes yet</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Recent Users */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900/20 rounded-lg flex items-center justify-center">
|
||||||
|
<Users className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-lg">Recent Users</h3>
|
||||||
|
<p className="text-xs text-muted-foreground">Latest signups</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{stats?.users.recent && stats.users.recent.length > 0 ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{stats.users.recent.map((user, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between py-3 border-b border-border last:border-0"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
|
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-cyan-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||||
|
<span className="text-white text-xs font-bold">
|
||||||
|
{(user.name || user.email).charAt(0).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-sm font-medium truncate">
|
||||||
|
{user.name || user.email}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
|
{new Date(user.createdAt).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
className={
|
||||||
|
user.plan === 'FREE'
|
||||||
|
? 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-300'
|
||||||
|
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{user.plan === 'PRO' && <Crown className="w-3 h-3 mr-1" />}
|
||||||
|
{user.plan}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">No users yet</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Newsletter Management Section */}
|
||||||
|
<div className="mt-8">
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="w-10 h-10 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-lg flex items-center justify-center">
|
||||||
|
<Users className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-semibold text-lg">Newsletter Management</h3>
|
||||||
|
<p className="text-xs text-muted-foreground">Manage AI feature launch notifications</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="text-2xl font-bold">{newsletterData?.total || 0}</span>
|
||||||
|
<p className="text-xs text-muted-foreground">Total Subscribers</p>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Broadcast Section */}
|
||||||
|
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-xl mb-6">
|
||||||
|
<div className="flex items-start gap-3 mb-3">
|
||||||
|
<Send className="w-5 h-5 text-purple-600 dark:text-purple-400 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">Broadcast AI Feature Launch</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Send the AI feature launch announcement to all {newsletterData?.total || 0} subscribers.
|
||||||
|
This will inform them that the features are now available.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Resend Free Tier Warning */}
|
||||||
|
{(newsletterData?.total || 0) > 100 && (
|
||||||
|
<div className="p-3 rounded-lg mb-3 bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-200 flex items-start gap-2">
|
||||||
|
<Activity className="w-5 h-5 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="text-sm">
|
||||||
|
<strong>Warning: Resend Free Limit</strong>
|
||||||
|
<p>You have more than 100 subscribers. The Resend Free Tier only allows 100 emails per day. Sending this broadcast might fail for some users or block your account.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{broadcastResult && (
|
||||||
|
<div className={`p-3 rounded-lg mb-3 flex items-center gap-2 ${broadcastResult.success
|
||||||
|
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||||
|
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||||
|
}`}>
|
||||||
|
{broadcastResult.success && <CheckCircle2 className="w-4 h-4" />}
|
||||||
|
<span className="text-sm">{broadcastResult.message}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleSendBroadcast}
|
||||||
|
disabled={sendingBroadcast || (newsletterData?.total || 0) === 0 || (newsletterData?.total || 0) > 100}
|
||||||
|
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white"
|
||||||
|
>
|
||||||
|
{sendingBroadcast ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Sending...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Mail className="w-4 h-4 mr-2" />
|
||||||
|
Send Launch Notification to All
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recent Subscribers */}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-3">Recent Subscribers</h4>
|
||||||
|
{newsletterData?.recent && newsletterData.recent.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{newsletterData.recent.map((subscriber, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between py-2 border-b border-border last:border-0"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Mail className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm">{subscriber.email}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{new Date(subscriber.createdAt).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">No subscribers yet</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tip */}
|
||||||
|
<div className="mt-4 pt-4 border-t">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
💡 Tip: View all subscribers in{' '}
|
||||||
|
<a
|
||||||
|
href="http://localhost:5555"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-purple-600 dark:text-purple-400 hover:underline"
|
||||||
|
>
|
||||||
|
Prisma Studio
|
||||||
|
</a>
|
||||||
|
{' '}(NewsletterSubscription table)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lead Management Section */}
|
||||||
|
<div className="mt-8">
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="w-10 h-10 bg-gradient-to-br from-emerald-100 to-teal-100 dark:from-emerald-900/30 dark:to-teal-900/30 rounded-lg flex items-center justify-center">
|
||||||
|
<FileDown className="w-5 h-5 text-emerald-600 dark:text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-semibold text-lg">Lead Management</h3>
|
||||||
|
<p className="text-xs text-muted-foreground">Reprint Calculator PDF downloads</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="text-2xl font-bold">{leadData?.total || 0}</span>
|
||||||
|
<p className="text-xs text-muted-foreground">Total Leads</p>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300">
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recent Leads */}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-3">Recent Leads</h4>
|
||||||
|
{leadData?.recent && leadData.recent.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{leadData.recent.map((lead) => (
|
||||||
|
<div
|
||||||
|
key={lead.id}
|
||||||
|
className="flex items-center justify-between py-3 px-4 border border-border rounded-lg bg-gray-50/50 dark:bg-gray-900/30"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
|
<Mail className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<span className="text-sm font-medium block truncate">{lead.email}</span>
|
||||||
|
{lead.annualSavings && (
|
||||||
|
<span className="text-xs text-emerald-600 flex items-center gap-1">
|
||||||
|
<DollarSign className="w-3 h-3" />
|
||||||
|
€{lead.annualSavings.toLocaleString()} potential savings
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right flex-shrink-0 ml-4">
|
||||||
|
<span className="text-xs text-muted-foreground block">
|
||||||
|
{new Date(lead.createdAt).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
{lead.reprintCost && lead.updatesPerYear && (
|
||||||
|
<span className="text-xs text-slate-500">
|
||||||
|
€{lead.reprintCost} × {lead.updatesPerYear}/yr
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">No leads yet. Leads appear when users download a PDF report from the Reprint Calculator.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tip */}
|
||||||
|
<div className="mt-4 pt-4 border-t">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
💡 Tip: View all leads in{' '}
|
||||||
|
<a
|
||||||
|
href="http://localhost:5555"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-emerald-600 dark:text-emerald-400 hover:underline"
|
||||||
|
>
|
||||||
|
Prisma Studio
|
||||||
|
</a>
|
||||||
|
{' '}(Lead table)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,643 +1,19 @@
|
|||||||
'use client';
|
import React from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import NewsletterClient from './NewsletterClient';
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
export const metadata: Metadata = {
|
||||||
import { useRouter } from 'next/navigation';
|
title: 'Newsletter Admin | QR Master',
|
||||||
import { Card } from '@/components/ui/Card';
|
description: 'Administrative access for QR Master newsletter management. This area is restricted to authorized personnel only.',
|
||||||
import { Button } from '@/components/ui/Button';
|
robots: {
|
||||||
import { Badge } from '@/components/ui/Badge';
|
index: false,
|
||||||
import {
|
follow: false,
|
||||||
Mail,
|
},
|
||||||
Users,
|
alternates: {
|
||||||
QrCode,
|
canonical: 'https://www.qrmaster.net/newsletter',
|
||||||
BarChart3,
|
},
|
||||||
TrendingUp,
|
};
|
||||||
Crown,
|
|
||||||
Activity,
|
|
||||||
Loader2,
|
|
||||||
Lock,
|
|
||||||
LogOut,
|
|
||||||
Zap,
|
|
||||||
Send,
|
|
||||||
CheckCircle2,
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
interface AdminStats {
|
export default function NewsletterPage() {
|
||||||
users: {
|
return <NewsletterClient />;
|
||||||
total: number;
|
|
||||||
premium: number;
|
|
||||||
newThisWeek: number;
|
|
||||||
newThisMonth: number;
|
|
||||||
recent: Array<{
|
|
||||||
email: string;
|
|
||||||
name: string | null;
|
|
||||||
plan: string;
|
|
||||||
createdAt: string;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
qrCodes: {
|
|
||||||
total: number;
|
|
||||||
dynamic: number;
|
|
||||||
static: number;
|
|
||||||
active: number;
|
|
||||||
};
|
|
||||||
scans: {
|
|
||||||
total: number;
|
|
||||||
dynamicOnly: number;
|
|
||||||
avgPerDynamicQR: string;
|
|
||||||
};
|
|
||||||
newsletter: {
|
|
||||||
subscribers: number;
|
|
||||||
};
|
|
||||||
topQRCodes: Array<{
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
type: string;
|
|
||||||
scans: number;
|
|
||||||
owner: string;
|
|
||||||
createdAt: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AdminDashboard() {
|
|
||||||
const router = useRouter();
|
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
||||||
const [isAuthenticating, setIsAuthenticating] = useState(true);
|
|
||||||
const [loginError, setLoginError] = useState('');
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
|
|
||||||
const [stats, setStats] = useState<AdminStats | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
// Newsletter management state
|
|
||||||
const [newsletterData, setNewsletterData] = useState<{
|
|
||||||
total: number;
|
|
||||||
recent: Array<{ email: string; createdAt: string }>;
|
|
||||||
} | null>(null);
|
|
||||||
const [sendingBroadcast, setSendingBroadcast] = useState(false);
|
|
||||||
const [broadcastResult, setBroadcastResult] = useState<{
|
|
||||||
success: boolean;
|
|
||||||
message: string;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
checkAuth();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const checkAuth = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/admin/stats');
|
|
||||||
if (response.ok) {
|
|
||||||
setIsAuthenticated(true);
|
|
||||||
const data = await response.json();
|
|
||||||
setStats(data);
|
|
||||||
setLoading(false);
|
|
||||||
// Also fetch newsletter data
|
|
||||||
fetchNewsletterData();
|
|
||||||
} else {
|
|
||||||
setIsAuthenticated(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setIsAuthenticated(false);
|
|
||||||
} finally {
|
|
||||||
setIsAuthenticating(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchNewsletterData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/newsletter/broadcast');
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
setNewsletterData(data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch newsletter data:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSendBroadcast = async () => {
|
|
||||||
if (!confirm(`Are you sure you want to send the AI Feature Launch email to all ${newsletterData?.total || 0} subscribers?`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSendingBroadcast(true);
|
|
||||||
setBroadcastResult(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/newsletter/broadcast', {
|
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
setBroadcastResult({
|
|
||||||
success: true,
|
|
||||||
message: data.message || `Successfully sent to ${data.sent} subscribers!`,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setBroadcastResult({
|
|
||||||
success: false,
|
|
||||||
message: data.error || 'Failed to send broadcast',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setBroadcastResult({
|
|
||||||
success: false,
|
|
||||||
message: 'Network error. Please try again.',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setSendingBroadcast(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setLoginError('');
|
|
||||||
setIsAuthenticating(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/newsletter/admin-login', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ email, password }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
setIsAuthenticated(true);
|
|
||||||
await checkAuth();
|
|
||||||
} else {
|
|
||||||
const data = await response.json();
|
|
||||||
setLoginError(data.error || 'Invalid credentials');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setLoginError('Login failed. Please try again.');
|
|
||||||
} finally {
|
|
||||||
setIsAuthenticating(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
await fetch('/api/auth/logout', { method: 'POST' });
|
|
||||||
router.push('/');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Login Screen
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-950/20 dark:to-pink-950/20 px-4">
|
|
||||||
<Card className="w-full max-w-md p-8">
|
|
||||||
<div className="text-center mb-6">
|
|
||||||
<div className="w-16 h-16 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<Lock className="w-8 h-8 text-purple-600 dark:text-purple-400" />
|
|
||||||
</div>
|
|
||||||
<h1 className="text-2xl font-bold mb-2">Admin Dashboard</h1>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
Sign in to access admin panel
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleLogin} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-2">Email</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
placeholder="admin@example.com"
|
|
||||||
required
|
|
||||||
className="w-full px-4 py-3 rounded-xl bg-background border border-border focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 outline-none transition-all"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-2">Password</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
placeholder="••••••••"
|
|
||||||
required
|
|
||||||
className="w-full px-4 py-3 rounded-xl bg-background border border-border focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 outline-none transition-all"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loginError && (
|
|
||||||
<p className="text-sm text-red-600 dark:text-red-400">{loginError}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={isAuthenticating}
|
|
||||||
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white"
|
|
||||||
>
|
|
||||||
{isAuthenticating ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
||||||
Signing in...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
'Sign In'
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-6 pt-6 border-t text-center">
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Admin credentials required
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loading
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
|
||||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin Dashboard
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-purple-50/30 to-pink-50/30 dark:from-purple-950/10 dark:to-pink-950/10">
|
|
||||||
<div className="container mx-auto px-4 py-8 max-w-7xl">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between mb-8">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold mb-2">Admin Dashboard</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Platform overview and statistics
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={handleLogout}
|
|
||||||
variant="outline"
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<LogOut className="w-4 h-4" />
|
|
||||||
Logout
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Stats Grid */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
||||||
{/* All Time Users */}
|
|
||||||
<Card className="p-6 hover:shadow-lg transition-shadow">
|
|
||||||
<div className="flex items-start justify-between mb-4">
|
|
||||||
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/20 rounded-lg flex items-center justify-center">
|
|
||||||
<Users className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
|
||||||
</div>
|
|
||||||
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
|
|
||||||
All Time
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<h3 className="text-3xl font-bold mb-1">{stats?.users.total || 0}</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">Total Users</p>
|
|
||||||
<div className="mt-3 pt-3 border-t space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs text-muted-foreground">This Month</span>
|
|
||||||
<span className="text-sm font-semibold text-green-600 dark:text-green-400">
|
|
||||||
+{stats?.users.newThisMonth || 0}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs text-muted-foreground">This Week</span>
|
|
||||||
<span className="text-sm font-semibold text-green-600 dark:text-green-400">
|
|
||||||
+{stats?.users.newThisWeek || 0}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Dynamic QR Codes */}
|
|
||||||
<Card className="p-6 hover:shadow-lg transition-shadow">
|
|
||||||
<div className="flex items-start justify-between mb-4">
|
|
||||||
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900/20 rounded-lg flex items-center justify-center">
|
|
||||||
<QrCode className="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
|
||||||
</div>
|
|
||||||
<Badge className="bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
|
|
||||||
Dynamic
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<h3 className="text-3xl font-bold mb-1">{stats?.qrCodes.dynamic || 0}</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">Dynamic QR Codes</p>
|
|
||||||
<div className="mt-3 pt-3 border-t flex items-center justify-between">
|
|
||||||
<span className="text-xs text-muted-foreground">Static</span>
|
|
||||||
<span className="text-sm font-semibold">{stats?.qrCodes.static || 0}</span>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Total Scans */}
|
|
||||||
<Card className="p-6 hover:shadow-lg transition-shadow">
|
|
||||||
<div className="flex items-start justify-between mb-4">
|
|
||||||
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/20 rounded-lg flex items-center justify-center">
|
|
||||||
<BarChart3 className="w-6 h-6 text-green-600 dark:text-green-400" />
|
|
||||||
</div>
|
|
||||||
<Badge className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
|
|
||||||
All Time
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<h3 className="text-3xl font-bold mb-1">
|
|
||||||
{stats?.scans.dynamicOnly.toLocaleString() || 0}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">Dynamic QR Scans</p>
|
|
||||||
<div className="mt-3 pt-3 border-t flex items-center justify-between">
|
|
||||||
<span className="text-xs text-muted-foreground">Avg per QR</span>
|
|
||||||
<span className="text-sm font-semibold">{stats?.scans.avgPerDynamicQR || 0}</span>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Total QR Codes */}
|
|
||||||
<Card className="p-6 hover:shadow-lg transition-shadow">
|
|
||||||
<div className="flex items-start justify-between mb-4">
|
|
||||||
<div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/20 rounded-lg flex items-center justify-center">
|
|
||||||
<QrCode className="w-6 h-6 text-amber-600 dark:text-amber-400" />
|
|
||||||
</div>
|
|
||||||
<Badge className="bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
|
|
||||||
All Time
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<h3 className="text-3xl font-bold mb-1">{stats?.qrCodes.total || 0}</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">Total QR Codes</p>
|
|
||||||
<div className="mt-3 pt-3 border-t space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs text-muted-foreground">Dynamic</span>
|
|
||||||
<span className="text-sm font-semibold">{stats?.qrCodes.dynamic || 0}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs text-muted-foreground">Static</span>
|
|
||||||
<span className="text-sm font-semibold">{stats?.qrCodes.static || 0}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Secondary Stats Row */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
||||||
{/* Total All Scans */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="w-12 h-12 bg-indigo-100 dark:bg-indigo-900/20 rounded-lg flex items-center justify-center">
|
|
||||||
<Zap className="w-6 h-6 text-indigo-600 dark:text-indigo-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-2xl font-bold">
|
|
||||||
{stats?.scans.total.toLocaleString() || 0}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">Total All Scans</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Total QR Codes */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="w-12 h-12 bg-pink-100 dark:bg-pink-900/20 rounded-lg flex items-center justify-center">
|
|
||||||
<QrCode className="w-6 h-6 text-pink-600 dark:text-pink-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-2xl font-bold">{stats?.qrCodes.total || 0}</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">Total QR Codes</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Premium Users */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/20 rounded-lg flex items-center justify-center">
|
|
||||||
<Crown className="w-6 h-6 text-amber-600 dark:text-amber-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-2xl font-bold">{stats?.users.premium || 0}</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">Premium Users</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom Grid */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
{/* Top QR Codes */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="flex items-center gap-3 mb-6">
|
|
||||||
<div className="w-10 h-10 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-lg flex items-center justify-center">
|
|
||||||
<TrendingUp className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-lg">Top QR Codes</h3>
|
|
||||||
<p className="text-xs text-muted-foreground">Most scanned</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{stats?.topQRCodes && stats.topQRCodes.length > 0 ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{stats.topQRCodes.map((qr, index) => (
|
|
||||||
<div
|
|
||||||
key={qr.id}
|
|
||||||
className="flex items-center justify-between py-3 border-b border-border last:border-0"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
||||||
<div className="w-8 h-8 bg-gradient-to-br from-purple-500 to-pink-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
|
||||||
<span className="text-white text-sm font-bold">
|
|
||||||
#{index + 1}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="text-sm font-medium truncate">{qr.title}</p>
|
|
||||||
<p className="text-xs text-muted-foreground truncate">
|
|
||||||
{qr.owner}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-right flex-shrink-0 ml-4">
|
|
||||||
<p className="text-lg font-bold">{qr.scans.toLocaleString()}</p>
|
|
||||||
<p className="text-xs text-muted-foreground">scans</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-muted-foreground">No QR codes yet</p>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Recent Users */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="flex items-center gap-3 mb-6">
|
|
||||||
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900/20 rounded-lg flex items-center justify-center">
|
|
||||||
<Users className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-lg">Recent Users</h3>
|
|
||||||
<p className="text-xs text-muted-foreground">Latest signups</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{stats?.users.recent && stats.users.recent.length > 0 ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{stats.users.recent.map((user, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="flex items-center justify-between py-3 border-b border-border last:border-0"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
||||||
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-cyan-500 rounded-full flex items-center justify-center flex-shrink-0">
|
|
||||||
<span className="text-white text-xs font-bold">
|
|
||||||
{(user.name || user.email).charAt(0).toUpperCase()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="text-sm font-medium truncate">
|
|
||||||
{user.name || user.email}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground truncate">
|
|
||||||
{new Date(user.createdAt).toLocaleDateString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Badge
|
|
||||||
className={
|
|
||||||
user.plan === 'FREE'
|
|
||||||
? 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-300'
|
|
||||||
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{user.plan === 'PRO' && <Crown className="w-3 h-3 mr-1" />}
|
|
||||||
{user.plan}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-muted-foreground">No users yet</p>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Newsletter Management Section */}
|
|
||||||
<div className="mt-8">
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="flex items-center gap-3 mb-6">
|
|
||||||
<div className="w-10 h-10 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-lg flex items-center justify-center">
|
|
||||||
<Users className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="font-semibold text-lg">Newsletter Management</h3>
|
|
||||||
<p className="text-xs text-muted-foreground">Manage AI feature launch notifications</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<span className="text-2xl font-bold">{newsletterData?.total || 0}</span>
|
|
||||||
<p className="text-xs text-muted-foreground">Total Subscribers</p>
|
|
||||||
</div>
|
|
||||||
<Badge className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
|
|
||||||
Active
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Broadcast Section */}
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-xl mb-6">
|
|
||||||
<div className="flex items-start gap-3 mb-3">
|
|
||||||
<Send className="w-5 h-5 text-purple-600 dark:text-purple-400 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium">Broadcast AI Feature Launch</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Send the AI feature launch announcement to all {newsletterData?.total || 0} subscribers.
|
|
||||||
This will inform them that the features are now available.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Resend Free Tier Warning */}
|
|
||||||
{(newsletterData?.total || 0) > 100 && (
|
|
||||||
<div className="p-3 rounded-lg mb-3 bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-200 flex items-start gap-2">
|
|
||||||
<Activity className="w-5 h-5 flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="text-sm">
|
|
||||||
<strong>Warning: Resend Free Limit</strong>
|
|
||||||
<p>You have more than 100 subscribers. The Resend Free Tier only allows 100 emails per day. Sending this broadcast might fail for some users or block your account.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{broadcastResult && (
|
|
||||||
<div className={`p-3 rounded-lg mb-3 flex items-center gap-2 ${broadcastResult.success
|
|
||||||
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
|
||||||
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
|
||||||
}`}>
|
|
||||||
{broadcastResult.success && <CheckCircle2 className="w-4 h-4" />}
|
|
||||||
<span className="text-sm">{broadcastResult.message}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={handleSendBroadcast}
|
|
||||||
disabled={sendingBroadcast || (newsletterData?.total || 0) === 0 || (newsletterData?.total || 0) > 100}
|
|
||||||
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white"
|
|
||||||
>
|
|
||||||
{sendingBroadcast ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
||||||
Sending...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Mail className="w-4 h-4 mr-2" />
|
|
||||||
Send Launch Notification to All
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Recent Subscribers */}
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-3">Recent Subscribers</h4>
|
|
||||||
{newsletterData?.recent && newsletterData.recent.length > 0 ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{newsletterData.recent.map((subscriber, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="flex items-center justify-between py-2 border-b border-border last:border-0"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Mail className="w-4 h-4 text-muted-foreground" />
|
|
||||||
<span className="text-sm">{subscriber.email}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{new Date(subscriber.createdAt).toLocaleDateString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-muted-foreground">No subscribers yet</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tip */}
|
|
||||||
<div className="mt-4 pt-4 border-t">
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
💡 Tip: View all subscribers in{' '}
|
|
||||||
<a
|
|
||||||
href="http://localhost:5555"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-purple-600 dark:text-purple-400 hover:underline"
|
|
||||||
>
|
|
||||||
Prisma Studio
|
|
||||||
</a>
|
|
||||||
{' '}(NewsletterSubscription table)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Button } from '@/components/ui/Button';
|
|
||||||
|
|
||||||
|
import '@/styles/globals.css';
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
@@ -37,24 +39,22 @@ export default function NotFound() {
|
|||||||
|
|
||||||
{/* Action Button */}
|
{/* Action Button */}
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<Link href="/">
|
<Link href="/" className="inline-flex items-center justify-center px-4 py-2 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
|
||||||
<Button size="lg">
|
<svg
|
||||||
<svg
|
className="w-5 h-5 mr-2"
|
||||||
className="w-5 h-5 mr-2"
|
fill="none"
|
||||||
fill="none"
|
stroke="currentColor"
|
||||||
stroke="currentColor"
|
viewBox="0 0 24 24"
|
||||||
viewBox="0 0 24 24"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
>
|
||||||
>
|
<path
|
||||||
<path
|
strokeLinecap="round"
|
||||||
strokeLinecap="round"
|
strokeLinejoin="round"
|
||||||
strokeLinejoin="round"
|
strokeWidth={2}
|
||||||
strokeWidth={2}
|
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
||||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
/>
|
||||||
/>
|
</svg>
|
||||||
</svg>
|
Back to Home
|
||||||
Back to Home
|
|
||||||
</Button>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,71 +1,85 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import SeoJsonLd from '@/components/SeoJsonLd';
|
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||||
import { organizationSchema, websiteSchema } from '@/lib/schema';
|
import { organizationSchema, websiteSchema } from '@/lib/schema';
|
||||||
import HomePageClient from '@/components/marketing/HomePageClient';
|
import HomePageClient from '@/components/marketing/HomePageClient';
|
||||||
|
import { generateFaqSchema } from '@/lib/schema-utils';
|
||||||
function truncateAtWord(text: string, maxLength: number): string {
|
import en from '@/i18n/en.json'; // Import English translations for schema generation
|
||||||
if (text.length <= maxLength) return text;
|
|
||||||
const truncated = text.slice(0, maxLength);
|
function truncateAtWord(text: string, maxLength: number): string {
|
||||||
const lastSpace = truncated.lastIndexOf(' ');
|
if (text.length <= maxLength) return text;
|
||||||
return lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated;
|
const truncated = text.slice(0, maxLength);
|
||||||
}
|
const lastSpace = truncated.lastIndexOf(' ');
|
||||||
|
return lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated;
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
}
|
||||||
const title = truncateAtWord('QR Master: Dynamic QR Generator', 60);
|
|
||||||
const description = truncateAtWord(
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
'Dynamic QR, branding, bulk generation & analytics for all campaigns.',
|
const title = truncateAtWord('QR Master: Dynamic QR Generator', 60);
|
||||||
160
|
const description = truncateAtWord(
|
||||||
);
|
'Create professional QR codes with QR Master. Dynamic QR with tracking, bulk generation, custom branding, and real-time analytics for all your campaigns.',
|
||||||
|
160
|
||||||
return {
|
);
|
||||||
title,
|
|
||||||
description,
|
return {
|
||||||
alternates: {
|
title,
|
||||||
canonical: 'https://www.qrmaster.net/',
|
description,
|
||||||
languages: {
|
alternates: {
|
||||||
'x-default': 'https://www.qrmaster.net/',
|
canonical: 'https://www.qrmaster.net/',
|
||||||
en: 'https://www.qrmaster.net/',
|
languages: {
|
||||||
},
|
'x-default': 'https://www.qrmaster.net/',
|
||||||
},
|
en: 'https://www.qrmaster.net/',
|
||||||
openGraph: {
|
de: 'https://www.qrmaster.net/qr-code-erstellen',
|
||||||
title,
|
},
|
||||||
description,
|
},
|
||||||
url: 'https://www.qrmaster.net/',
|
openGraph: {
|
||||||
type: 'website',
|
title,
|
||||||
},
|
description,
|
||||||
twitter: {
|
url: 'https://www.qrmaster.net/',
|
||||||
title,
|
type: 'website',
|
||||||
description,
|
images: [
|
||||||
},
|
{
|
||||||
};
|
url: 'https://www.qrmaster.net/og-image.png',
|
||||||
}
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
export default function HomePage() {
|
alt: 'QR Master - Dynamic QR Code Generator and Analytics Platform',
|
||||||
return (
|
},
|
||||||
<>
|
],
|
||||||
<SeoJsonLd data={[organizationSchema(), websiteSchema()]} />
|
},
|
||||||
|
twitter: {
|
||||||
{/* Server-rendered SEO content for crawlers */}
|
title,
|
||||||
<div className="sr-only" aria-hidden="false">
|
description,
|
||||||
<h1>QR Master: Free Dynamic QR Code Generator with Tracking & Analytics</h1>
|
images: ['https://www.qrmaster.net/og-image.png'],
|
||||||
<p>
|
},
|
||||||
Create professional QR codes for your business with QR Master. Our dynamic QR code generator
|
};
|
||||||
lets you create trackable QR codes, edit destinations anytime, and view detailed analytics.
|
}
|
||||||
Perfect for restaurants, retail, events, and marketing campaigns.
|
|
||||||
</p>
|
export default function HomePage() {
|
||||||
<p>
|
return (
|
||||||
Features include: Dynamic QR codes with real-time tracking, bulk QR code generation from Excel/CSV,
|
<>
|
||||||
custom branding with colors and logos, advanced scan analytics showing device types and locations,
|
<SeoJsonLd data={[organizationSchema(), websiteSchema(), generateFaqSchema(en.faq.questions)]} />
|
||||||
vCard QR codes for digital business cards, and restaurant menu QR codes.
|
|
||||||
</p>
|
{/* Server-rendered H1 for SEO - visually hidden but crawlable */}
|
||||||
<p>
|
<h1 className="sr-only">QR Master: Dynamic QR Code Generator with Analytics</h1>
|
||||||
Start free with 3 dynamic QR codes and unlimited static codes. Upgrade to Pro for 50 codes
|
|
||||||
with advanced analytics, or Business for 500 codes with bulk creation and priority support.
|
{/* Server-rendered SEO content for crawlers */}
|
||||||
</p>
|
<div className="sr-only" aria-hidden="false">
|
||||||
</div>
|
<p>
|
||||||
|
Create professional QR codes for your business with QR Master. Our dynamic QR code generator
|
||||||
<HomePageClient />
|
lets you create trackable QR codes, edit destinations anytime, and view detailed analytics.
|
||||||
</>
|
Perfect for restaurants, retail, events, and marketing campaigns.
|
||||||
);
|
</p>
|
||||||
}
|
<p>
|
||||||
|
Features include: Dynamic QR codes with real-time tracking, bulk QR code generation from Excel/CSV,
|
||||||
|
custom branding with colors and logos, advanced scan analytics showing device types and locations,
|
||||||
|
vCard QR codes for digital business cards, and restaurant menu QR codes.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Start free with 3 dynamic QR codes and unlimited static codes. Upgrade to Pro for 50 codes
|
||||||
|
with advanced analytics, or Business for 500 codes with bulk creation and priority support.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HomePageClient />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ 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';
|
||||||
|
|
||||||
export default function PricingPage() {
|
export default function PricingClient() {
|
||||||
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');
|
||||||
@@ -182,9 +183,9 @@ export default function PricingPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-12">
|
<div className="container mx-auto px-4 py-12">
|
||||||
<div className="text-center mb-12">
|
<div className="text-center mb-12">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
<h2 className="text-4xl font-bold text-gray-900 mb-4">
|
||||||
Choose Your Plan
|
Choose Your Plan
|
||||||
</h1>
|
</h2>
|
||||||
<p className="text-xl text-gray-600">
|
<p className="text-xl text-gray-600">
|
||||||
Select the perfect plan for your QR code needs
|
Select the perfect plan for your QR code needs
|
||||||
</p>
|
</p>
|
||||||
@@ -260,7 +261,7 @@ export default function PricingPage() {
|
|||||||
All plans include unlimited static QR codes and basic customization.
|
All plans include unlimited static QR codes and basic customization.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-600 mt-2">
|
<p className="text-gray-600 mt-2">
|
||||||
Need help choosing? <a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</a>
|
Need help choosing? <ObfuscatedMailto email="support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</ObfuscatedMailto>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
45
src/app/(marketing)/pricing/page.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import PricingClient from './PricingClient';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: {
|
||||||
|
absolute: 'Pricing Plans | QR Master'
|
||||||
|
},
|
||||||
|
description: 'Choose the perfect QR code plan for your needs. Free, Pro, and Business plans with dynamic QR codes, analytics, bulk generation, and custom branding.',
|
||||||
|
alternates: {
|
||||||
|
canonical: 'https://www.qrmaster.net/pricing',
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: 'Pricing Plans | QR Master',
|
||||||
|
description: 'Choose the perfect QR code plan for your needs.',
|
||||||
|
url: 'https://www.qrmaster.net/pricing',
|
||||||
|
type: 'website',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: 'https://www.qrmaster.net/og-image.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'QR Master Pricing Plans',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PricingPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Server-rendered H1 for SEO */}
|
||||||
|
<h1 className="sr-only">QR Master Pricing – Choose Your QR Code Plan</h1>
|
||||||
|
<div className="sr-only">
|
||||||
|
<h2>Compare our plans</h2>
|
||||||
|
<p>Find the best QR code solution for your business. From free personal tiers to enterprise-grade dynamic code management.</p>
|
||||||
|
</div>
|
||||||
|
<PricingClient />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src/app/(marketing)/privacy/PrivacyEmailLink.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
|
||||||
|
|
||||||
|
export function PrivacyEmailLink() {
|
||||||
|
return (
|
||||||
|
<ObfuscatedMailto
|
||||||
|
email="support@qrmaster.net"
|
||||||
|
className="text-primary-600 hover:text-primary-700"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,27 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { PrivacyEmailLink } from './PrivacyEmailLink';
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Privacy Policy | QR Master',
|
title: 'Privacy Policy | QR Master',
|
||||||
description: 'Privacy Policy and data protection information for QR Master',
|
description: 'Read our Privacy Policy to understand how QR Master collects, uses, and protects your data. We are committed to GDPR compliance and data security.',
|
||||||
|
alternates: {
|
||||||
|
canonical: 'https://www.qrmaster.net/privacy',
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: 'Privacy Policy | QR Master',
|
||||||
|
description: 'Read our Privacy Policy to understand how QR Master collects, uses, and protects your data.',
|
||||||
|
url: 'https://www.qrmaster.net/privacy',
|
||||||
|
type: 'website',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: 'https://www.qrmaster.net/og-image.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'QR Master Privacy Policy',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PrivacyPage() {
|
export default function PrivacyPage() {
|
||||||
@@ -93,9 +111,7 @@ export default function PrivacyPage() {
|
|||||||
</ul>
|
</ul>
|
||||||
<p className="text-gray-700 mb-4">
|
<p className="text-gray-700 mb-4">
|
||||||
To exercise these rights, contact us at{' '}
|
To exercise these rights, contact us at{' '}
|
||||||
<a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700">
|
<PrivacyEmailLink />
|
||||||
support@qrmaster.net
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 mb-4">
|
<p className="text-gray-700 mb-4">
|
||||||
Our service is for users 16 years and older. If you're in the EEA and have concerns,
|
Our service is for users 16 years and older. If you're in the EEA and have concerns,
|
||||||
@@ -111,9 +127,7 @@ export default function PrivacyPage() {
|
|||||||
<div className="bg-gray-50 p-6 rounded-lg">
|
<div className="bg-gray-50 p-6 rounded-lg">
|
||||||
<p className="text-gray-700 mb-2">
|
<p className="text-gray-700 mb-2">
|
||||||
<strong>Email:</strong>{' '}
|
<strong>Email:</strong>{' '}
|
||||||
<a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700">
|
<PrivacyEmailLink />
|
||||||
support@qrmaster.net
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 mb-2"><strong>Website:</strong> <a href="/" className="text-primary-600 hover:text-primary-700">qrmaster.net</a></p>
|
<p className="text-gray-700 mb-2"><strong>Website:</strong> <a href="/" className="text-primary-600 hover:text-primary-700">qrmaster.net</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
|
|||||||
import { breadcrumbSchema } from '@/lib/schema';
|
import { breadcrumbSchema } from '@/lib/schema';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master',
|
title: 'QR Code Tracking & Analytics - Track Scans | QR Master',
|
||||||
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior. Free QR code tracking software with detailed reports.',
|
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior. Free QR code tracking software with detailed reports.',
|
||||||
keywords: 'qr code tracking, qr code analytics, track qr scans, qr code statistics, free qr tracking, qr code monitoring',
|
keywords: 'qr code tracking, qr code analytics, track qr scans, qr code statistics, free qr tracking, qr code monitoring',
|
||||||
alternates: {
|
alternates: {
|
||||||
@@ -19,13 +19,21 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master',
|
title: 'QR Code Tracking & Analytics - Track Scans | QR Master',
|
||||||
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.',
|
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.',
|
||||||
url: 'https://www.qrmaster.net/qr-code-tracking',
|
url: 'https://www.qrmaster.net/qr-code-tracking',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: 'https://www.qrmaster.net/og-image.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'QR Code Tracking & Analytics - QR Master',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master',
|
title: 'QR Code Tracking & Analytics - Track Scans | QR Master',
|
||||||
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.',
|
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -154,7 +162,7 @@ export default function QRCodeTrackingPage() {
|
|||||||
position: 3,
|
position: 3,
|
||||||
name: 'Monitor Analytics',
|
name: 'Monitor Analytics',
|
||||||
text: 'View real-time scan data including location, device, and time patterns in your dashboard',
|
text: 'View real-time scan data including location, device, and time patterns in your dashboard',
|
||||||
url: 'https://www.qrmaster.net/analytics',
|
url: 'https://www.qrmaster.net/signup',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@type': 'HowToStep',
|
'@type': 'HowToStep',
|
||||||
@@ -199,7 +207,7 @@ export default function QRCodeTrackingPage() {
|
|||||||
Start Tracking Free
|
Start Tracking Free
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/create">
|
<Link href="/signup">
|
||||||
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
|
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
|
||||||
Create Trackable QR Code
|
Create Trackable QR Code
|
||||||
</Button>
|
</Button>
|
||||||
@@ -370,26 +378,35 @@ export default function QRCodeTrackingPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* CTA Section */}
|
{/* CTA Section */}
|
||||||
<section className="py-20 bg-gradient-to-r from-primary-600 to-purple-600 text-white">
|
{/* CTA Section */}
|
||||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center">
|
<section className="py-24 bg-slate-900 relative overflow-hidden">
|
||||||
<h2 className="text-4xl font-bold mb-6">
|
{/* Background Decorations */}
|
||||||
Start Tracking Your QR Codes Today
|
<div className="absolute top-0 right-0 -mr-20 -mt-20 w-96 h-96 bg-primary-500/20 rounded-full blur-3xl opacity-50" />
|
||||||
|
<div className="absolute bottom-0 left-0 -ml-20 -mb-20 w-80 h-80 bg-purple-500/20 rounded-full blur-3xl opacity-50" />
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center relative z-10">
|
||||||
|
<h2 className="text-4xl lg:text-5xl font-bold mb-6 text-white tracking-tight">
|
||||||
|
Start Tracking Your <span className="text-transparent bg-clip-text bg-gradient-to-r from-primary-400 to-purple-400">QR Codes Today</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xl mb-8 text-primary-100">
|
<p className="text-xl mb-10 text-slate-300 leading-relaxed max-w-2xl mx-auto">
|
||||||
Join thousands of businesses using QR Master to track and optimize their QR code campaigns
|
Join thousands of businesses using QR Master to optimize their campaigns with real-time analytics.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
<div className="flex flex-col sm:flex-row gap-5 justify-center">
|
||||||
<Link href="/signup">
|
<Link href="/signup">
|
||||||
<Button size="lg" variant="secondary" className="text-lg px-8 py-4 w-full sm:w-auto bg-white text-primary-600 hover:bg-gray-100">
|
<Button size="lg" className="text-lg px-8 py-6 h-auto w-full sm:w-auto bg-white text-slate-900 hover:bg-slate-50 font-bold shadow-xl shadow-primary-900/20 transition-all hover:-translate-y-1">
|
||||||
Create Free Account
|
Create Free Account
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/pricing">
|
<Link href="/pricing">
|
||||||
<Button size="lg" variant="outline" className="text-lg px-8 py-4 w-full sm:w-auto border-white text-white hover:bg-white/10">
|
<Button size="lg" variant="outline" className="text-lg px-8 py-6 h-auto w-full sm:w-auto border-slate-700 text-white hover:bg-slate-800 hover:border-slate-600 transition-all">
|
||||||
View Pricing
|
View Pricing
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-8 text-sm text-slate-500">
|
||||||
|
Full analytics accessible on free plan.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
117
src/app/(marketing)/reprint-calculator/page.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import ReprintSavingsCalculator from '@/components/marketing/ReprintSavingsCalculator';
|
||||||
|
import { ArrowDown, Check, ShieldCheck, Zap } from 'lucide-react';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Reprint Cost Calculator | QR Master',
|
||||||
|
description:
|
||||||
|
'Calculate how much you are wasting on QR code reprints. See your potential savings with dynamic QR codes that never need to be reprinted.',
|
||||||
|
alternates: {
|
||||||
|
canonical: 'https://www.qrmaster.net/reprint-calculator',
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: 'Reprint Cost Calculator | QR Master',
|
||||||
|
description: 'Stop wasting money on reprints. Calculate your savings now.',
|
||||||
|
url: 'https://www.qrmaster.net/reprint-calculator',
|
||||||
|
type: 'website',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: 'https://www.qrmaster.net/og-image.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'QR Master Reprint Cost Calculator',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ReprintCalculatorPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="pt-24 pb-12 bg-white relative overflow-hidden">
|
||||||
|
<div className="container mx-auto px-4 text-center max-w-3xl relative z-10">
|
||||||
|
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-slate-100/80 backdrop-blur-sm border border-slate-200 text-slate-600 text-sm font-medium mb-8">
|
||||||
|
<span className="relative flex h-2 w-2">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
|
||||||
|
</span>
|
||||||
|
Static QR codes are costing you money
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-4xl lg:text-6xl font-black text-slate-900 mb-6 tracking-tight leading-[1.1]">
|
||||||
|
Stop Burning Budget on <br className="hidden md:block" />
|
||||||
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-orange-600">Avoidable Reprints</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="text-xl text-slate-600 mb-8 leading-relaxed max-w-2xl mx-auto">
|
||||||
|
Every time a URL changes, static QR codes become useless trash.
|
||||||
|
Dynamic QR codes update instantly—keeping your print materials alive forever.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<ArrowDown className="w-6 h-6 text-slate-400 animate-bounce" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Calculator Component */}
|
||||||
|
<ReprintSavingsCalculator />
|
||||||
|
|
||||||
|
{/* Value Props */}
|
||||||
|
<section className="py-24 bg-white border-t border-slate-100">
|
||||||
|
<div className="container mx-auto px-4 max-w-6xl">
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<h2 className="text-3xl font-bold text-slate-900 mb-4">
|
||||||
|
Why Smart Companies Switched Years Ago
|
||||||
|
</h2>
|
||||||
|
<p className="text-slate-600 text-lg max-w-2xl mx-auto">
|
||||||
|
The math is simple. One dynamic subscription costs less than a single batch of reprints.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-8 lg:gap-12">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
icon: Zap,
|
||||||
|
color: "text-amber-500",
|
||||||
|
bg: "bg-amber-50",
|
||||||
|
title: "Update Instantly",
|
||||||
|
desc: "Changed your menu? New promo link? Update the destination in seconds. Your printed codes keep working perfectly."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: ShieldCheck,
|
||||||
|
color: "text-blue-500",
|
||||||
|
bg: "bg-blue-50",
|
||||||
|
title: "Error Proofing",
|
||||||
|
desc: "Printed the wrong link? With static codes, that's a disaster. With dynamic codes, it's a 5-second fix in the dashboard."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Check,
|
||||||
|
color: "text-green-500",
|
||||||
|
bg: "bg-green-50",
|
||||||
|
title: "Real ROI Tracking",
|
||||||
|
desc: "Stop guessing if your print ads work. Track every scan, location, and device to measure exactly what's driving value."
|
||||||
|
}
|
||||||
|
].map((feature, i) => (
|
||||||
|
<div key={i} className="group p-8 rounded-2xl bg-slate-50 border border-slate-100 hover:bg-white hover:shadow-xl hover:shadow-slate-200/50 hover:border-slate-200 transition-all duration-300">
|
||||||
|
<div className={`w-14 h-14 ${feature.bg} rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300`}>
|
||||||
|
<feature.icon className={`w-7 h-7 ${feature.color}`} />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-slate-900 mb-3">{feature.title}</h3>
|
||||||
|
<p className="text-slate-600 leading-relaxed">
|
||||||
|
{feature.desc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,371 +1,374 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import {
|
import {
|
||||||
Bitcoin,
|
Bitcoin,
|
||||||
Download,
|
Download,
|
||||||
Check,
|
Check,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Wallet,
|
Wallet,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Select } from '@/components/ui/Select';
|
import { Select } from '@/components/ui/Select';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import AdBanner from '@/components/ads/AdBanner';
|
||||||
// Brand Colors
|
|
||||||
const BRAND = {
|
// Brand Colors
|
||||||
paleGrey: '#EBEBDF',
|
const BRAND = {
|
||||||
richBlue: '#1A1265',
|
paleGrey: '#EBEBDF',
|
||||||
richBlueLight: '#2A2275',
|
richBlue: '#1A1265',
|
||||||
};
|
richBlueLight: '#2A2275',
|
||||||
|
};
|
||||||
// Crypto Options
|
|
||||||
const CRYPTO_CURRENCIES = [
|
// Crypto Options
|
||||||
{ value: 'bitcoin', label: 'Bitcoin (BTC)', color: '#F7931A', prefix: 'bitcoin:' },
|
const CRYPTO_CURRENCIES = [
|
||||||
{ value: 'ethereum', label: 'Ethereum (ETH)', color: '#627EEA', prefix: 'ethereum:' },
|
{ value: 'bitcoin', label: 'Bitcoin (BTC)', color: '#F7931A', prefix: 'bitcoin:' },
|
||||||
{ value: 'usdt', label: 'Tether (USDT)', color: '#26A17B', prefix: '' }, // Commonly ERC20/TRC20 - keeping raw for safety
|
{ value: 'ethereum', label: 'Ethereum (ETH)', color: '#627EEA', prefix: 'ethereum:' },
|
||||||
{ value: 'solana', label: 'Solana (SOL)', color: '#14F195', prefix: 'solana:' },
|
{ value: 'usdt', label: 'Tether (USDT)', color: '#26A17B', prefix: '' }, // Commonly ERC20/TRC20 - keeping raw for safety
|
||||||
];
|
{ value: 'solana', label: 'Solana (SOL)', color: '#14F195', prefix: 'solana:' },
|
||||||
|
];
|
||||||
|
|
||||||
// QR Color Options
|
|
||||||
const QR_COLORS = [
|
// QR Color Options
|
||||||
{ name: 'Bitcoin Orange', value: '#F7931A' },
|
const QR_COLORS = [
|
||||||
{ name: 'Ethereum Blue', value: '#627EEA' },
|
{ name: 'Bitcoin Orange', value: '#F7931A' },
|
||||||
{ name: 'Tether Green', value: '#26A17B' },
|
{ name: 'Ethereum Blue', value: '#627EEA' },
|
||||||
{ name: 'Classic Black', value: '#000000' },
|
{ name: 'Tether Green', value: '#26A17B' },
|
||||||
{ name: 'Dark Blue', value: '#1A1265' },
|
{ name: 'Classic Black', value: '#000000' },
|
||||||
{ name: 'Emerald', value: '#10B981' },
|
{ name: 'Dark Blue', value: '#1A1265' },
|
||||||
{ name: 'Rose', value: '#F43F5E' },
|
{ name: 'Emerald', value: '#10B981' },
|
||||||
];
|
{ name: 'Rose', value: '#F43F5E' },
|
||||||
|
];
|
||||||
// Frame Options
|
|
||||||
const FRAME_OPTIONS = [
|
// Frame Options
|
||||||
{ id: 'none', label: 'No Frame' },
|
const FRAME_OPTIONS = [
|
||||||
{ id: 'scanme', label: 'Scan Me' },
|
{ id: 'none', label: 'No Frame' },
|
||||||
{ id: 'pay', label: 'Pay Now' },
|
{ id: 'scanme', label: 'Scan Me' },
|
||||||
{ id: 'donate', label: 'Donate' },
|
{ id: 'pay', label: 'Pay Now' },
|
||||||
];
|
{ id: 'donate', label: 'Donate' },
|
||||||
|
];
|
||||||
export default function CryptoGenerator() {
|
|
||||||
const [currency, setCurrency] = useState('bitcoin');
|
export default function CryptoGenerator() {
|
||||||
const [address, setAddress] = useState('');
|
const [currency, setCurrency] = useState('bitcoin');
|
||||||
const [amount, setAmount] = useState('');
|
const [address, setAddress] = useState('');
|
||||||
const [qrMode, setQrMode] = useState<'universal' | 'wallet'>('universal');
|
const [amount, setAmount] = useState('');
|
||||||
const [qrColor, setQrColor] = useState('#F7931A');
|
const [qrMode, setQrMode] = useState<'universal' | 'wallet'>('universal');
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [qrColor, setQrColor] = useState('#F7931A');
|
||||||
|
const [frameType, setFrameType] = useState('none');
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
// Generate URL based on selected mode
|
|
||||||
const getUrl = () => {
|
// Generate URL based on selected mode
|
||||||
if (!address.trim()) return 'https://www.qrmaster.net';
|
const getUrl = () => {
|
||||||
|
if (!address.trim()) return 'https://www.qrmaster.net';
|
||||||
const cleanAddr = address.trim();
|
|
||||||
|
const cleanAddr = address.trim();
|
||||||
if (qrMode === 'wallet') {
|
|
||||||
// Wallet Direct Mode - Uses crypto URI scheme
|
if (qrMode === 'wallet') {
|
||||||
// Only works when scanning FROM a wallet app (Coinbase, Trust Wallet, etc.)
|
// Wallet Direct Mode - Uses crypto URI scheme
|
||||||
const prefixes: Record<string, string> = {
|
// Only works when scanning FROM a wallet app (Coinbase, Trust Wallet, etc.)
|
||||||
bitcoin: 'bitcoin:',
|
const prefixes: Record<string, string> = {
|
||||||
ethereum: 'ethereum:',
|
bitcoin: 'bitcoin:',
|
||||||
solana: 'solana:',
|
ethereum: 'ethereum:',
|
||||||
usdt: '', // USDT doesn't have a standard URI
|
solana: 'solana:',
|
||||||
};
|
usdt: '', // USDT doesn't have a standard URI
|
||||||
const prefix = prefixes[currency] || '';
|
};
|
||||||
if (!prefix) return cleanAddr; // USDT fallback
|
const prefix = prefixes[currency] || '';
|
||||||
let uri = `${prefix}${cleanAddr}`;
|
if (!prefix) return cleanAddr; // USDT fallback
|
||||||
if (amount) uri += `?amount=${amount}`;
|
let uri = `${prefix}${cleanAddr}`;
|
||||||
return uri;
|
if (amount) uri += `?amount=${amount}`;
|
||||||
} else {
|
return uri;
|
||||||
// Universal Mode - Blockchain explorer links
|
} else {
|
||||||
// Works with ANY phone camera
|
// Universal Mode - Blockchain explorer links
|
||||||
switch (currency) {
|
// Works with ANY phone camera
|
||||||
case 'bitcoin':
|
switch (currency) {
|
||||||
return `https://blockchair.com/bitcoin/address/${cleanAddr}`;
|
case 'bitcoin':
|
||||||
case 'ethereum':
|
return `https://blockchair.com/bitcoin/address/${cleanAddr}`;
|
||||||
return `https://etherscan.io/address/${cleanAddr}`;
|
case 'ethereum':
|
||||||
case 'solana':
|
return `https://etherscan.io/address/${cleanAddr}`;
|
||||||
return `https://solscan.io/account/${cleanAddr}`;
|
case 'solana':
|
||||||
case 'usdt':
|
return `https://solscan.io/account/${cleanAddr}`;
|
||||||
return `https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7?a=${cleanAddr}`;
|
case 'usdt':
|
||||||
default:
|
return `https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7?a=${cleanAddr}`;
|
||||||
return cleanAddr;
|
default:
|
||||||
}
|
return cleanAddr;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
};
|
||||||
const handleDownload = async (format: 'png' | 'svg') => {
|
|
||||||
if (!qrRef.current) return;
|
const handleDownload = async (format: 'png' | 'svg') => {
|
||||||
try {
|
if (!qrRef.current) return;
|
||||||
if (format === 'png') {
|
try {
|
||||||
const { toPng } = await import('html-to-image');
|
if (format === 'png') {
|
||||||
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3 });
|
const { toPng } = await import('html-to-image');
|
||||||
const link = document.createElement('a');
|
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3 });
|
||||||
link.download = `${currency}-qr-code.png`;
|
const link = document.createElement('a');
|
||||||
link.href = dataUrl;
|
link.download = `${currency}-qr-code.png`;
|
||||||
link.click();
|
link.href = dataUrl;
|
||||||
} else {
|
link.click();
|
||||||
const svgData = qrRef.current.querySelector('svg')?.outerHTML;
|
} else {
|
||||||
if (svgData) {
|
const svgData = qrRef.current.querySelector('svg')?.outerHTML;
|
||||||
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
if (svgData) {
|
||||||
const url = URL.createObjectURL(blob);
|
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
||||||
const link = document.createElement('a');
|
const url = URL.createObjectURL(blob);
|
||||||
link.href = url;
|
const link = document.createElement('a');
|
||||||
link.download = `${currency}-qr-code.svg`;
|
link.href = url;
|
||||||
link.click();
|
link.download = `${currency}-qr-code.svg`;
|
||||||
}
|
link.click();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
}
|
||||||
console.error('Download failed', err);
|
} catch (err) {
|
||||||
}
|
console.error('Download failed', err);
|
||||||
};
|
}
|
||||||
|
};
|
||||||
const getFrameLabel = () => {
|
|
||||||
const frame = FRAME_OPTIONS.find(f => f.id === frameType);
|
const getFrameLabel = () => {
|
||||||
return frame?.id !== 'none' ? frame?.label : null;
|
const frame = FRAME_OPTIONS.find(f => f.id === frameType);
|
||||||
};
|
return frame?.id !== 'none' ? frame?.label : null;
|
||||||
|
};
|
||||||
return (
|
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
return (
|
||||||
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
{/* Main Generator Card */}
|
|
||||||
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
|
{/* Main Generator Card */}
|
||||||
<div className="grid lg:grid-cols-2">
|
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
|
||||||
|
<div className="grid lg:grid-cols-2">
|
||||||
{/* LEFT: Input Section */}
|
|
||||||
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
{/* LEFT: Input Section */}
|
||||||
|
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||||
{/* Crypto Details */}
|
|
||||||
<div className="space-y-6">
|
{/* Crypto Details */}
|
||||||
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
<div className="space-y-6">
|
||||||
<Wallet className="w-5 h-5 text-slate-900" />
|
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
||||||
Wallet Details
|
<Wallet className="w-5 h-5 text-slate-900" />
|
||||||
</h2>
|
Wallet Details
|
||||||
|
</h2>
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Currency</label>
|
<div>
|
||||||
<Select
|
<label className="block text-sm font-medium text-slate-700 mb-2">Currency</label>
|
||||||
value={currency}
|
<Select
|
||||||
options={CRYPTO_CURRENCIES}
|
value={currency}
|
||||||
onChange={(e) => {
|
options={CRYPTO_CURRENCIES}
|
||||||
const val = e.target.value;
|
onChange={(e) => {
|
||||||
setCurrency(val);
|
const val = e.target.value;
|
||||||
const col = CRYPTO_CURRENCIES.find(c => c.value === val)?.color;
|
setCurrency(val);
|
||||||
if (col) setQrColor(col);
|
const col = CRYPTO_CURRENCIES.find(c => c.value === val)?.color;
|
||||||
}}
|
if (col) setQrColor(col);
|
||||||
className="h-12 w-full rounded-xl border-slate-200"
|
}}
|
||||||
/>
|
className="h-12 w-full rounded-xl border-slate-200"
|
||||||
</div>
|
aria-label="Currency"
|
||||||
|
/>
|
||||||
<div>
|
</div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Wallet Address</label>
|
|
||||||
<Input
|
<div>
|
||||||
placeholder={`Enter ${currency} address`}
|
<label className="block text-sm font-medium text-slate-700 mb-2">Wallet Address</label>
|
||||||
value={address}
|
<Input
|
||||||
onChange={(e) => setAddress(e.target.value)}
|
placeholder={`Enter ${currency} address`}
|
||||||
className="h-12 text-base rounded-xl border-slate-200 focus:border-slate-900 focus:ring-slate-900 font-mono text-sm"
|
value={address}
|
||||||
/>
|
onChange={(e) => setAddress(e.target.value)}
|
||||||
</div>
|
className="h-12 text-base rounded-xl border-slate-200 focus:border-slate-900 focus:ring-slate-900 font-mono text-sm"
|
||||||
|
/>
|
||||||
<div>
|
</div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Amount (Optional)</label>
|
|
||||||
<Input
|
<div>
|
||||||
placeholder="0.00"
|
<label className="block text-sm font-medium text-slate-700 mb-2">Amount (Optional)</label>
|
||||||
type="number"
|
<Input
|
||||||
value={amount}
|
placeholder="0.00"
|
||||||
onChange={(e) => setAmount(e.target.value)}
|
type="number"
|
||||||
className="h-12 text-base rounded-xl border-slate-200 focus:border-slate-900 focus:ring-slate-900"
|
value={amount}
|
||||||
/>
|
onChange={(e) => setAmount(e.target.value)}
|
||||||
</div>
|
className="h-12 text-base rounded-xl border-slate-200 focus:border-slate-900 focus:ring-slate-900"
|
||||||
|
/>
|
||||||
{/* QR Mode Toggle */}
|
</div>
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">QR Code Mode</label>
|
{/* QR Mode Toggle */}
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div>
|
||||||
<button
|
<label className="block text-sm font-medium text-slate-700 mb-2">QR Code Mode</label>
|
||||||
onClick={() => setQrMode('universal')}
|
<div className="grid grid-cols-2 gap-2">
|
||||||
className={cn(
|
<button
|
||||||
"py-3 px-4 rounded-xl text-sm font-medium transition-all border",
|
onClick={() => setQrMode('universal')}
|
||||||
qrMode === 'universal'
|
className={cn(
|
||||||
? "bg-slate-900 text-white border-slate-900"
|
"py-3 px-4 rounded-xl text-sm font-medium transition-all border",
|
||||||
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
qrMode === 'universal'
|
||||||
)}
|
? "bg-slate-900 text-white border-slate-900"
|
||||||
>
|
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
||||||
Universal (Web)
|
)}
|
||||||
</button>
|
>
|
||||||
<button
|
Universal (Web)
|
||||||
onClick={() => setQrMode('wallet')}
|
</button>
|
||||||
className={cn(
|
<button
|
||||||
"py-3 px-4 rounded-xl text-sm font-medium transition-all border",
|
onClick={() => setQrMode('wallet')}
|
||||||
qrMode === 'wallet'
|
className={cn(
|
||||||
? "bg-slate-900 text-white border-slate-900"
|
"py-3 px-4 rounded-xl text-sm font-medium transition-all border",
|
||||||
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
qrMode === 'wallet'
|
||||||
)}
|
? "bg-slate-900 text-white border-slate-900"
|
||||||
>
|
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
||||||
Wallet Direct
|
)}
|
||||||
</button>
|
>
|
||||||
</div>
|
Wallet Direct
|
||||||
<p className="text-xs text-slate-500 mt-2">
|
</button>
|
||||||
{qrMode === 'universal'
|
</div>
|
||||||
? "Works with any phone camera. Opens blockchain explorer."
|
<p className="text-xs text-slate-600 mt-2">
|
||||||
: "Requires scanning from a wallet app. Enables direct payment."}
|
{qrMode === 'universal'
|
||||||
</p>
|
? "Works with any phone camera. Opens blockchain explorer."
|
||||||
</div>
|
: "Requires scanning from a wallet app. Enables direct payment."}
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
<div className="border-t border-slate-100"></div>
|
</div>
|
||||||
|
|
||||||
{/* Design Options */}
|
<div className="border-t border-slate-100"></div>
|
||||||
<div className="space-y-6">
|
|
||||||
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
{/* Design Options */}
|
||||||
<Sparkles className="w-5 h-5 text-slate-900" />
|
<div className="space-y-6">
|
||||||
Design Options
|
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
||||||
</h2>
|
<Sparkles className="w-5 h-5 text-slate-900" />
|
||||||
|
Design Options
|
||||||
{/* Color Picker */}
|
</h2>
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-3">QR Code Color</label>
|
{/* Color Picker */}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div>
|
||||||
{QR_COLORS.map((c) => (
|
<label className="block text-sm font-medium text-slate-700 mb-3">QR Code Color</label>
|
||||||
<button
|
<div className="flex flex-wrap gap-2">
|
||||||
key={c.name}
|
{QR_COLORS.map((c) => (
|
||||||
onClick={() => setQrColor(c.value)}
|
<button
|
||||||
className={cn(
|
key={c.name}
|
||||||
"w-9 h-9 rounded-full border-2 flex items-center justify-center transition-all hover:scale-110",
|
onClick={() => setQrColor(c.value)}
|
||||||
qrColor === c.value ? "border-slate-900 ring-2 ring-offset-2 ring-slate-200" : "border-white shadow-md"
|
className={cn(
|
||||||
)}
|
"w-9 h-9 rounded-full border-2 flex items-center justify-center transition-all hover:scale-110",
|
||||||
style={{ backgroundColor: c.value }}
|
qrColor === c.value ? "border-slate-900 ring-2 ring-offset-2 ring-slate-200" : "border-white shadow-md"
|
||||||
aria-label={`Select ${c.name}`}
|
)}
|
||||||
title={c.name}
|
style={{ backgroundColor: c.value }}
|
||||||
>
|
aria-label={`Select ${c.name}`}
|
||||||
{qrColor === c.value && <Check className="w-4 h-4 text-white" strokeWidth={3} />}
|
title={c.name}
|
||||||
</button>
|
>
|
||||||
))}
|
{qrColor === c.value && <Check className="w-4 h-4 text-white" strokeWidth={3} />}
|
||||||
</div>
|
</button>
|
||||||
</div>
|
))}
|
||||||
|
</div>
|
||||||
{/* Frame Selector */}
|
</div>
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
{/* Frame Selector */}
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div>
|
||||||
{FRAME_OPTIONS.map((frame) => (
|
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
||||||
<button
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||||
key={frame.id}
|
{FRAME_OPTIONS.map((frame) => (
|
||||||
onClick={() => setFrameType(frame.id)}
|
<button
|
||||||
className={cn(
|
key={frame.id}
|
||||||
"py-2.5 px-3 rounded-lg text-sm font-medium transition-all border",
|
onClick={() => setFrameType(frame.id)}
|
||||||
frameType === frame.id
|
className={cn(
|
||||||
? "bg-slate-900 text-white border-slate-900"
|
"py-2.5 px-3 rounded-lg text-sm font-medium transition-all border",
|
||||||
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
frameType === frame.id
|
||||||
)}
|
? "bg-slate-900 text-white border-slate-900"
|
||||||
>
|
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
||||||
{frame.label}
|
)}
|
||||||
</button>
|
>
|
||||||
))}
|
{frame.label}
|
||||||
</div>
|
</button>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* RIGHT: Preview Section */}
|
</div>
|
||||||
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
|
||||||
|
{/* RIGHT: Preview Section */}
|
||||||
{/* QR Card with Frame */}
|
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||||
<div
|
|
||||||
ref={qrRef}
|
{/* QR Card with Frame */}
|
||||||
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
|
<div
|
||||||
style={{ minWidth: '320px' }}
|
ref={qrRef}
|
||||||
>
|
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
|
||||||
{/* Frame Label */}
|
>
|
||||||
{getFrameLabel() && (
|
{/* Frame Label */}
|
||||||
<div
|
{getFrameLabel() && (
|
||||||
className="mb-5 px-8 py-2.5 rounded-full text-white font-bold text-sm tracking-widest uppercase shadow-md"
|
<div
|
||||||
style={{ backgroundColor: qrColor }}
|
className="mb-5 px-8 py-2.5 rounded-full text-white font-bold text-sm tracking-widest uppercase shadow-md"
|
||||||
>
|
style={{ backgroundColor: qrColor }}
|
||||||
{getFrameLabel()}
|
>
|
||||||
</div>
|
{getFrameLabel()}
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
{/* QR Code */}
|
|
||||||
<div className="bg-white">
|
{/* QR Code */}
|
||||||
{address.trim() ? (
|
<div className="bg-white">
|
||||||
<QRCodeSVG
|
{address.trim() ? (
|
||||||
value={getUrl()}
|
<QRCodeSVG
|
||||||
size={240}
|
value={getUrl()}
|
||||||
level="Q"
|
size={240}
|
||||||
includeMargin={false}
|
level="Q"
|
||||||
fgColor={qrColor}
|
includeMargin={false}
|
||||||
/>
|
fgColor={qrColor}
|
||||||
) : (
|
/>
|
||||||
<div
|
) : (
|
||||||
className="flex items-center justify-center border-2 border-dashed border-slate-200 rounded-xl"
|
<div
|
||||||
style={{ width: 240, height: 240 }}
|
className="flex items-center justify-center border-2 border-dashed border-slate-200 rounded-xl"
|
||||||
>
|
style={{ width: 240, height: 240 }}
|
||||||
<div className="text-center text-slate-400 p-6">
|
>
|
||||||
<Wallet className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
<div className="text-center text-slate-400 p-6">
|
||||||
<p className="text-sm font-medium">Enter wallet address</p>
|
<Wallet className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||||
<p className="text-xs mt-1">to generate QR code</p>
|
<p className="text-sm font-medium">Enter wallet address</p>
|
||||||
</div>
|
<p className="text-xs mt-1">to generate QR code</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
{/* Info Preview */}
|
|
||||||
<div className="mt-6 text-center max-w-[260px]">
|
{/* Info Preview */}
|
||||||
<h3 className="font-bold text-slate-900 text-lg flex items-center justify-center gap-2 truncate">
|
<div className="mt-6 text-center max-w-[260px]">
|
||||||
<Bitcoin className="w-4 h-4 text-slate-400 shrink-0" />
|
<h3 className="font-bold text-slate-900 text-lg flex items-center justify-center gap-2 truncate">
|
||||||
<span className="truncate capitalize">{currency}</span>
|
<Bitcoin className="w-4 h-4 text-slate-400 shrink-0" />
|
||||||
</h3>
|
<span className="truncate capitalize">{currency}</span>
|
||||||
<div className="text-xs text-slate-500 mt-1 truncate px-2">
|
</h3>
|
||||||
{address || 'Wallet Address'}
|
<div className="text-xs text-slate-600 mt-1 truncate px-2">
|
||||||
</div>
|
{address || 'Wallet Address'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Download Buttons */}
|
|
||||||
<div className="flex items-center gap-3 mt-8">
|
{/* Download Buttons */}
|
||||||
<Button
|
<div className="flex items-center gap-3 mt-8">
|
||||||
onClick={() => handleDownload('png')}
|
<Button
|
||||||
className="bg-slate-900 hover:bg-black text-white shadow-lg"
|
onClick={() => handleDownload('png')}
|
||||||
>
|
className="bg-slate-900 hover:bg-black text-white shadow-lg"
|
||||||
<Download className="w-4 h-4 mr-2" />
|
>
|
||||||
Download PNG
|
<Download className="w-4 h-4 mr-2" />
|
||||||
</Button>
|
Download PNG
|
||||||
<Button
|
</Button>
|
||||||
onClick={() => handleDownload('svg')}
|
<Button
|
||||||
variant="outline"
|
onClick={() => handleDownload('svg')}
|
||||||
className="border-slate-300 hover:bg-white"
|
variant="outline"
|
||||||
>
|
className="border-slate-300 hover:bg-white"
|
||||||
<Download className="w-4 h-4 mr-2" />
|
>
|
||||||
SVG
|
<Download className="w-4 h-4 mr-2" />
|
||||||
</Button>
|
SVG
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
<p className="text-xs text-slate-500 mt-4 text-center">
|
|
||||||
Scanning copies the wallet address or opens a crypto app.
|
<p className="text-xs text-slate-600 mt-4 text-center">
|
||||||
</p>
|
Scanning copies the wallet address or opens a crypto app.
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Upsell Banner */}
|
|
||||||
<div className="mt-8 bg-gradient-to-r from-slate-900 to-slate-700 rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
||||||
<div className="text-white text-center sm:text-left">
|
|
||||||
<h3 className="font-bold text-lg">Accept Crypto for Business?</h3>
|
{/* Upsell Banner */}
|
||||||
<p className="text-white/80 text-sm mt-1">
|
<div className="mt-8 bg-gradient-to-r from-slate-900 to-slate-700 rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
Create professional, branded payment pages for your store.
|
<div className="text-white text-center sm:text-left">
|
||||||
</p>
|
<h3 className="font-bold text-lg">Accept Crypto for Business?</h3>
|
||||||
</div>
|
<p className="text-white/80 text-sm mt-1">
|
||||||
<Link href="/signup">
|
Create professional, branded payment pages for your store.
|
||||||
<Button className="bg-white text-slate-900 hover:bg-slate-100 shrink-0 shadow-lg">
|
</p>
|
||||||
Get Business Tools
|
</div>
|
||||||
</Button>
|
<Link href="/signup">
|
||||||
</Link>
|
<Button className="bg-white text-slate-900 hover:bg-slate-100 shrink-0 shadow-lg">
|
||||||
</div>
|
Get Business Tools
|
||||||
</div>
|
</Button>
|
||||||
);
|
</Link>
|
||||||
}
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,362 +1,335 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import CryptoGenerator from './CryptoGenerator';
|
import CryptoGenerator from './CryptoGenerator';
|
||||||
import { Bitcoin, Shield, Zap, Smartphone, Wallet, Coins, Sparkles, Download, Share2 } from 'lucide-react';
|
import { Bitcoin, Shield, Zap, Smartphone, Wallet, Coins, Sparkles, Download, Share2 } from 'lucide-react';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
||||||
|
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
||||||
// SEO Optimized Metadata
|
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Free Crypto QR Code Generator | Bitcoin, Ethereum & USDT | QR Master',
|
// SEO Optimized Metadata
|
||||||
description: 'Create a QR code for your Crypto wallet address. Supports Bitcoin (BTC), Ethereum (ETH), USDT, and more. Essential for easy payments and donations.',
|
export const metadata: Metadata = {
|
||||||
keywords: ['crypto qr code', 'bitcoin qr generator', 'ethereum qr code', 'crypto wallet qr', 'donation qr code'],
|
title: {
|
||||||
alternates: {
|
absolute: 'Free Crypto QR Code Generator | Krypto QR Code Erstellen | QR Master',
|
||||||
canonical: 'https://qrmaster.io/tools/crypto-qr-code',
|
},
|
||||||
},
|
description: 'Create a QR code for your Crypto wallet address. Erstelle Bitcoin & Ethereum QR Codes für einfache Zahlungen. Supports BTC, ETH, USDT & more.',
|
||||||
openGraph: {
|
keywords: ['crypto qr code', 'bitcoin qr generator', 'ethereum qr code', 'crypto wallet qr', 'donation qr code', 'krypto qr code', 'bitcoin qr code erstellen', 'kryptowährung qr code', 'wallet adresse qr code'],
|
||||||
title: 'Free Crypto QR Code Generator | QR Master',
|
alternates: {
|
||||||
description: 'Generate QR codes to accept Crypto payments securely. Supports BTC, ETH, SOL.',
|
canonical: 'https://www.qrmaster.net/tools/crypto-qr-code',
|
||||||
type: 'website',
|
},
|
||||||
url: 'https://qrmaster.io/tools/crypto-qr-code',
|
openGraph: {
|
||||||
images: [{ url: '/og-crypto-generator.png', width: 1200, height: 630 }],
|
title: 'Free Crypto QR Code Generator | QR Master',
|
||||||
},
|
description: 'Generate QR codes to accept Crypto payments securely. Supports BTC, ETH, SOL.',
|
||||||
twitter: {
|
type: 'website',
|
||||||
card: 'summary_large_image',
|
url: 'https://www.qrmaster.net/tools/crypto-qr-code',
|
||||||
title: 'Free Crypto QR Code Generator',
|
images: [{ url: '/og-crypto-generator.png', width: 1200, height: 630 }],
|
||||||
description: 'Create secure QR codes for your crypto wallet.',
|
},
|
||||||
},
|
twitter: {
|
||||||
robots: {
|
card: 'summary_large_image',
|
||||||
index: true,
|
title: 'Free Crypto QR Code Generator',
|
||||||
follow: true,
|
description: 'Create secure QR codes for your crypto wallet.',
|
||||||
},
|
},
|
||||||
};
|
robots: {
|
||||||
|
index: true,
|
||||||
// JSON-LD Structured Data
|
follow: true,
|
||||||
const jsonLd = {
|
},
|
||||||
'@context': 'https://schema.org',
|
};
|
||||||
'@graph': [
|
|
||||||
{
|
// JSON-LD Structured Data
|
||||||
'@type': 'SoftwareApplication',
|
const jsonLd = {
|
||||||
name: 'Crypto QR Code Generator',
|
'@context': 'https://schema.org',
|
||||||
applicationCategory: 'FinanceApplication',
|
'@graph': [
|
||||||
operatingSystem: 'Web Browser',
|
generateSoftwareAppSchema(
|
||||||
offers: {
|
'Crypto QR Code Generator',
|
||||||
'@type': 'Offer',
|
'Generate QR codes that contain your cryptocurrency wallet address for easy payments.',
|
||||||
price: '0',
|
'/og-crypto-generator.png',
|
||||||
priceCurrency: 'USD',
|
'FinanceApplication'
|
||||||
},
|
),
|
||||||
aggregateRating: {
|
{
|
||||||
'@type': 'AggregateRating',
|
'@type': 'HowTo',
|
||||||
ratingValue: '4.9',
|
name: 'How to Create a Crypto QR Code',
|
||||||
ratingCount: '870',
|
description: 'Create a QR code for your Bitcoin or Ethereum wallet.',
|
||||||
},
|
step: [
|
||||||
description: 'Generate QR codes that contain your cryptocurrency wallet address for easy payments.',
|
{
|
||||||
},
|
'@type': 'HowToStep',
|
||||||
{
|
position: 1,
|
||||||
'@type': 'HowTo',
|
name: 'Select Currency',
|
||||||
name: 'How to Create a Crypto QR Code',
|
text: 'Choose your cryptocurrency from the list (Bitcoin, Ethereum, USDT, etc.).',
|
||||||
description: 'Create a QR code for your Bitcoin or Ethereum wallet.',
|
},
|
||||||
step: [
|
{
|
||||||
{
|
'@type': 'HowToStep',
|
||||||
'@type': 'HowToStep',
|
position: 2,
|
||||||
position: 1,
|
name: 'Enter Address',
|
||||||
name: 'Select Currency',
|
text: 'Copy your public wallet address from your crypto app and paste it into the "Wallet Address" field.',
|
||||||
text: 'Choose your cryptocurrency from the list (Bitcoin, Ethereum, USDT, etc.).',
|
},
|
||||||
},
|
{
|
||||||
{
|
'@type': 'HowToStep',
|
||||||
'@type': 'HowToStep',
|
position: 3,
|
||||||
position: 2,
|
name: 'Add Amount (Optional)',
|
||||||
name: 'Enter Address',
|
text: 'If you are requesting a specific payment, enter the amount to pre-fill the transaction.',
|
||||||
text: 'Copy your public wallet address from your crypto app and paste it into the "Wallet Address" field.',
|
},
|
||||||
},
|
{
|
||||||
{
|
'@type': 'HowToStep',
|
||||||
'@type': 'HowToStep',
|
position: 4,
|
||||||
position: 3,
|
name: 'Customize QR',
|
||||||
name: 'Add Amount (Optional)',
|
text: 'Select a brand color (like Bitcoin Orange or Ethereum Blue) and add a frame like "Pay Now".',
|
||||||
text: 'If you are requesting a specific payment, enter the amount to pre-fill the transaction.',
|
},
|
||||||
},
|
{
|
||||||
{
|
'@type': 'HowToStep',
|
||||||
'@type': 'HowToStep',
|
position: 5,
|
||||||
position: 4,
|
name: 'Download',
|
||||||
name: 'Customize QR',
|
text: 'Download the QR code image and share it to receive funds securely.',
|
||||||
text: 'Select a brand color (like Bitcoin Orange or Ethereum Blue) and add a frame like "Pay Now".',
|
},
|
||||||
},
|
],
|
||||||
{
|
totalTime: 'PT30S',
|
||||||
'@type': 'HowToStep',
|
},
|
||||||
position: 5,
|
generateFaqSchema({
|
||||||
name: 'Download',
|
'Is it safe to share my wallet address?': {
|
||||||
text: 'Download the QR code image and share it to receive funds securely.',
|
question: 'Is it safe to share my wallet address?',
|
||||||
},
|
answer: 'Yes. Your public wallet address is designed to be shared so you can receive funds. Never share your private key.',
|
||||||
],
|
},
|
||||||
totalTime: 'PT30S',
|
'Which currencies are supported?': {
|
||||||
},
|
question: 'Which currencies are supported?',
|
||||||
{
|
answer: 'Our generator supports standard URI schemes for Bitcoin, Ethereum, Solana, and can generally store any wallet string for other coins.',
|
||||||
'@type': 'FAQPage',
|
},
|
||||||
mainEntity: [
|
'Can I add a specific amount?': {
|
||||||
{
|
question: 'Can I add a specific amount?',
|
||||||
'@type': 'Question',
|
answer: 'Yes, you can pre-fill an amount so when the user scans, their wallet app automatically suggests the correct payment value.',
|
||||||
name: 'Is it safe to share my wallet address?',
|
},
|
||||||
acceptedAnswer: {
|
'Does it work with all wallets?': {
|
||||||
'@type': 'Answer',
|
question: 'Does it work with all wallets?',
|
||||||
text: 'Yes. Your public wallet address is designed to be shared so you can receive funds. Never share your private key.',
|
answer: 'Yes, standard crypto QR codes are universally readable by almost all modern wallet apps (Coinbase, MetaMask, Trust Wallet, etc.).',
|
||||||
},
|
},
|
||||||
},
|
'Are there any fees?': {
|
||||||
{
|
question: 'Are there any fees?',
|
||||||
'@type': 'Question',
|
answer: 'No. This generator is completely free. We do not charge any fees for generating codes or for the transactions made using them.',
|
||||||
name: 'Which currencies are supported?',
|
},
|
||||||
acceptedAnswer: {
|
}),
|
||||||
'@type': 'Answer',
|
],
|
||||||
text: 'Our generator supports standard URI schemes for Bitcoin, Ethereum, Solana, and can generally store any wallet string for other coins.',
|
};
|
||||||
},
|
|
||||||
},
|
export default function CryptoQRCodePage() {
|
||||||
{
|
return (
|
||||||
'@type': 'Question',
|
<>
|
||||||
name: 'Can I add a specific amount?',
|
<script
|
||||||
acceptedAnswer: {
|
type="application/ld+json"
|
||||||
'@type': 'Answer',
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
text: 'Yes, you can pre-fill an amount so when the user scans, their wallet app automatically suggests the correct payment value.',
|
/>
|
||||||
},
|
<ToolBreadcrumb toolName="Crypto QR Code Generator" toolSlug="crypto-qr-code" />
|
||||||
},
|
|
||||||
{
|
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
||||||
'@type': 'Question',
|
|
||||||
name: 'Does it work with all wallets?',
|
{/* HERO SECTION */}
|
||||||
acceptedAnswer: {
|
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden bg-slate-900">
|
||||||
'@type': 'Answer',
|
<div className="absolute inset-0 opacity-20">
|
||||||
text: 'Yes, standard crypto QR codes are universally readable by almost all modern wallet apps (Coinbase, MetaMask, Trust Wallet, etc.).',
|
{/* Circuit Pattern */}
|
||||||
},
|
<svg className="w-full h-full" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
||||||
},
|
<defs>
|
||||||
{
|
<pattern id="circuit_pattern" width="100" height="100" patternUnits="userSpaceOnUse">
|
||||||
'@type': 'Question',
|
<path d="M10 10 H 90 V 90 H 10 Z" stroke="none" fill="none" />
|
||||||
name: 'Are there any fees?',
|
<circle cx="20" cy="20" r="2" fill="#F7931A" />
|
||||||
acceptedAnswer: {
|
<circle cx="80" cy="80" r="2" fill="#627EEA" />
|
||||||
'@type': 'Answer',
|
<path d="M20 20 L 50 20 L 50 50" stroke="white" strokeWidth="1" strokeOpacity="0.1" />
|
||||||
text: 'No. This generator is completely free. We do not charge any fees for generating codes or for the transactions made using them.',
|
</pattern>
|
||||||
},
|
</defs>
|
||||||
},
|
<rect width="100%" height="100%" fill="url(#circuit_pattern)" />
|
||||||
],
|
</svg>
|
||||||
},
|
</div>
|
||||||
],
|
|
||||||
};
|
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
||||||
|
<div className="text-center lg:text-left">
|
||||||
export default function CryptoQRCodePage() {
|
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
|
||||||
return (
|
<span className="flex h-2 w-2 relative">
|
||||||
<>
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-400 opacity-75"></span>
|
||||||
<script
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-orange-400"></span>
|
||||||
type="application/ld+json"
|
</span>
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
Free Tool — Secure & Private
|
||||||
/>
|
</div>
|
||||||
<ToolBreadcrumb toolName="Crypto QR Code Generator" toolSlug="crypto-qr-code" />
|
|
||||||
|
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
||||||
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
Accept Payments with <br className="hidden lg:block" />
|
||||||
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-[#F7931A] to-[#F2A900]">Crypto QR Codes</span>
|
||||||
{/* HERO SECTION */}
|
</h1>
|
||||||
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden bg-slate-900">
|
|
||||||
<div className="absolute inset-0 opacity-20">
|
<p className="text-lg md:text-xl text-slate-400 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
||||||
{/* Circuit Pattern */}
|
Share your wallet address securely. Supports Bitcoin, Ethereum, USDT, and more.
|
||||||
<svg className="w-full h-full" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Error-free transfers.</strong>
|
||||||
<defs>
|
</p>
|
||||||
<pattern id="circuit_pattern" width="100" height="100" patternUnits="userSpaceOnUse">
|
|
||||||
<path d="M10 10 H 90 V 90 H 10 Z" stroke="none" fill="none" />
|
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
||||||
<circle cx="20" cy="20" r="2" fill="#F7931A" />
|
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
||||||
<circle cx="80" cy="80" r="2" fill="#627EEA" />
|
<Bitcoin className="w-4 h-4 text-[#F7931A]" />
|
||||||
<path d="M20 20 L 50 20 L 50 50" stroke="white" strokeWidth="1" strokeOpacity="0.1" />
|
Bitcoin
|
||||||
</pattern>
|
</div>
|
||||||
</defs>
|
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
||||||
<rect width="100%" height="100%" fill="url(#circuit_pattern)" />
|
<Coins className="w-4 h-4 text-[#627EEA]" />
|
||||||
</svg>
|
Ethereum & Altcoins
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
||||||
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
<Wallet className="w-4 h-4 text-white" />
|
||||||
<div className="text-center lg:text-left">
|
Wallet Connect
|
||||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
|
</div>
|
||||||
<span className="flex h-2 w-2 relative">
|
</div>
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-400 opacity-75"></span>
|
</div>
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-orange-400"></span>
|
|
||||||
</span>
|
{/* Visual Abstract */}
|
||||||
Free Tool — Secure & Private
|
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
|
||||||
</div>
|
<div className="absolute w-[500px] h-[500px] bg-orange-500/10 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
|
||||||
|
|
||||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform rotate-2 hover:-rotate-1 transition-all duration-700 group">
|
||||||
Accept Payments with <br className="hidden lg:block" />
|
<div className="absolute inset-0 bg-gradient-to-br from-white/5 to-transparent rounded-3xl" />
|
||||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-[#F7931A] to-[#F2A900]">Crypto QR Codes</span>
|
|
||||||
</h1>
|
<div className="w-full bg-gradient-to-br from-orange-400 to-orange-600 rounded-xl shadow-lg p-5 mb-6 relative overflow-hidden text-white">
|
||||||
|
<div className="flex justify-between items-start mb-4">
|
||||||
<p className="text-lg md:text-xl text-slate-400 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
<Bitcoin className="w-8 h-8 opacity-80" />
|
||||||
Share your wallet address securely. Supports Bitcoin, Ethereum, USDT, and more.
|
<div className="bg-white/20 px-2 py-1 rounded text-xs">BTC</div>
|
||||||
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Error-free transfers.</strong>
|
</div>
|
||||||
</p>
|
<div className="text-2xl font-bold tracking-wider mb-1">0.05 BTC</div>
|
||||||
|
<div className="text-xs opacity-70">$3,450.25 USD</div>
|
||||||
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
</div>
|
||||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
|
||||||
<Bitcoin className="w-4 h-4 text-[#F7931A]" />
|
<div className="w-48 h-48 bg-white rounded-xl p-2 shadow-inner relative overflow-hidden flex items-center justify-center">
|
||||||
Bitcoin
|
<QRCodeSVG value="https://www.qrmaster.net" size={170} fgColor="#333" level="Q" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
|
||||||
<Coins className="w-4 h-4 text-[#627EEA]" />
|
{/* Floating Badge */}
|
||||||
Ethereum & Altcoins
|
<div className="absolute -bottom-6 -right-6 bg-slate-900 border border-white/10 py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
|
||||||
</div>
|
<div className="bg-orange-500/20 p-2 rounded-full">
|
||||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
<Wallet className="w-5 h-5 text-orange-500" />
|
||||||
<Wallet className="w-4 h-4 text-white" />
|
</div>
|
||||||
Wallet Connect
|
<div className="text-left">
|
||||||
</div>
|
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Payment</div>
|
||||||
</div>
|
<div className="text-sm font-bold text-white">Receive Crypto</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Visual Abstract */}
|
</div>
|
||||||
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
|
</div>
|
||||||
<div className="absolute w-[500px] h-[500px] bg-orange-500/10 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
|
</div>
|
||||||
|
</section>
|
||||||
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform rotate-2 hover:-rotate-1 transition-all duration-700 group">
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-white/5 to-transparent rounded-3xl" />
|
{/* GENERATOR SECTION */}
|
||||||
|
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
|
||||||
<div className="w-full bg-gradient-to-br from-orange-400 to-orange-600 rounded-xl shadow-lg p-5 mb-6 relative overflow-hidden text-white">
|
<CryptoGenerator />
|
||||||
<div className="flex justify-between items-start mb-4">
|
</section>
|
||||||
<Bitcoin className="w-8 h-8 opacity-80" />
|
|
||||||
<div className="bg-white/20 px-2 py-1 rounded text-xs">BTC</div>
|
{/* HOW IT WORKS */}
|
||||||
</div>
|
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
|
||||||
<div className="text-2xl font-bold tracking-wider mb-1">0.05 BTC</div>
|
<div className="max-w-4xl mx-auto">
|
||||||
<div className="text-xs opacity-70">$3,450.25 USD</div>
|
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
|
||||||
</div>
|
How Crypto QR Codes Work
|
||||||
|
</h2>
|
||||||
<div className="w-48 h-48 bg-white rounded-xl p-2 shadow-inner relative overflow-hidden flex items-center justify-center">
|
|
||||||
<QRCodeSVG value="https://www.qrmaster.net" size={170} fgColor="#333" level="Q" />
|
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
|
||||||
</div>
|
<article className="text-center">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
|
||||||
{/* Floating Badge */}
|
<Coins className="w-6 h-6 text-white" />
|
||||||
<div className="absolute -bottom-6 -right-6 bg-slate-900 border border-white/10 py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
|
</div>
|
||||||
<div className="bg-orange-500/20 p-2 rounded-full">
|
<h3 className="font-bold text-slate-900 mb-2">1. Select</h3>
|
||||||
<Wallet className="w-5 h-5 text-orange-500" />
|
<p className="text-slate-600 text-xs leading-relaxed">
|
||||||
</div>
|
Choose your crypto currency (BTC, ETH, etc.).
|
||||||
<div className="text-left">
|
</p>
|
||||||
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Payment</div>
|
</article>
|
||||||
<div className="text-sm font-bold text-white">Receive Crypto</div>
|
|
||||||
</div>
|
<article className="text-center">
|
||||||
</div>
|
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
|
||||||
</div>
|
<Wallet className="w-6 h-6 text-white" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<h3 className="font-bold text-slate-900 mb-2">2. Paste</h3>
|
||||||
</section>
|
<p className="text-slate-600 text-xs leading-relaxed">
|
||||||
|
Enter your public wallet address.
|
||||||
{/* GENERATOR SECTION */}
|
</p>
|
||||||
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
|
</article>
|
||||||
<CryptoGenerator />
|
|
||||||
</section>
|
<article className="text-center">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
|
||||||
{/* HOW IT WORKS */}
|
<Zap className="w-6 h-6 text-white" />
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
|
</div>
|
||||||
<div className="max-w-4xl mx-auto">
|
<h3 className="font-bold text-slate-900 mb-2">3. Amount</h3>
|
||||||
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
|
<p className="text-slate-600 text-xs leading-relaxed">
|
||||||
How Crypto QR Codes Work
|
Optionally specify an amount to request.
|
||||||
</h2>
|
</p>
|
||||||
|
</article>
|
||||||
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
|
|
||||||
<article className="text-center">
|
<article className="text-center">
|
||||||
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
|
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
|
||||||
<Coins className="w-6 h-6 text-white" />
|
<Sparkles className="w-6 h-6 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">1. Select</h3>
|
<h3 className="font-bold text-slate-900 mb-2">4. Style</h3>
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">
|
<p className="text-slate-600 text-xs leading-relaxed">
|
||||||
Choose your crypto currency (BTC, ETH, etc.).
|
Customize colors and add a 'Pay' frame.
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article className="text-center">
|
<article className="text-center">
|
||||||
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
|
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
|
||||||
<Wallet className="w-6 h-6 text-white" />
|
<Download className="w-6 h-6 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">2. Paste</h3>
|
<h3 className="font-bold text-slate-900 mb-2">5. Download</h3>
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">
|
<p className="text-slate-600 text-xs leading-relaxed">
|
||||||
Enter your public wallet address.
|
Save your secure QR code image.
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
|
</div>
|
||||||
<article className="text-center">
|
</div>
|
||||||
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
|
</section>
|
||||||
<Zap className="w-6 h-6 text-white" />
|
|
||||||
</div>
|
{/* RELATED TOOLS */}
|
||||||
<h3 className="font-bold text-slate-900 mb-2">3. Amount</h3>
|
<RelatedTools />
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">
|
|
||||||
Optionally specify an amount to request.
|
{/* FAQ SECTION */}
|
||||||
</p>
|
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
||||||
</article>
|
<div className="max-w-3xl mx-auto">
|
||||||
|
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
||||||
<article className="text-center">
|
Frequently Asked Questions
|
||||||
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
|
</h2>
|
||||||
<Sparkles className="w-6 h-6 text-white" />
|
<p className="text-slate-600 text-center mb-10">
|
||||||
</div>
|
Common questions about Crypto QR codes.
|
||||||
<h3 className="font-bold text-slate-900 mb-2">4. Style</h3>
|
</p>
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">
|
|
||||||
Customize colors and add a 'Pay' frame.
|
<div className="space-y-4">
|
||||||
</p>
|
<FaqItem
|
||||||
</article>
|
question="Is it safe to share my wallet address?"
|
||||||
|
answer="Yes. Your public wallet address is designed to be shared so you can receive funds. Never share your private key."
|
||||||
<article className="text-center">
|
/>
|
||||||
<div className="w-12 h-12 rounded-2xl bg-black flex items-center justify-center mx-auto mb-4">
|
<FaqItem
|
||||||
<Download className="w-6 h-6 text-white" />
|
question="Which currencies are supported?"
|
||||||
</div>
|
answer="Our generator supports standard URI schemes for Bitcoin, Ethereum, Solana, and can generally store any wallet string for other coins."
|
||||||
<h3 className="font-bold text-slate-900 mb-2">5. Download</h3>
|
/>
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">
|
<FaqItem
|
||||||
Save your secure QR code image.
|
question="Can I add a specific amount?"
|
||||||
</p>
|
answer="Yes, you can pre-fill an amount so when the user scans, their wallet app automatically suggests the correct payment value."
|
||||||
</article>
|
/>
|
||||||
</div>
|
<FaqItem
|
||||||
</div>
|
question="Does it work with all wallets?"
|
||||||
</section>
|
answer="Yes, standard crypto QR codes are universally readable by almost all modern wallet apps (Coinbase, MetaMask, Trust Wallet, etc.)."
|
||||||
|
/>
|
||||||
{/* FAQ SECTION */}
|
<FaqItem
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
question="Are there any fees?"
|
||||||
<div className="max-w-3xl mx-auto">
|
answer="No. This generator is completely free. We do not charge any fees for generating codes or for the transactions made using them."
|
||||||
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
/>
|
||||||
Frequently Asked Questions
|
</div>
|
||||||
</h2>
|
</div>
|
||||||
<p className="text-slate-600 text-center mb-10">
|
</section>
|
||||||
Common questions about Crypto QR codes.
|
|
||||||
</p>
|
</div>
|
||||||
|
</>
|
||||||
<div className="space-y-4">
|
);
|
||||||
<FaqItem
|
}
|
||||||
question="Is it safe to share my wallet address?"
|
|
||||||
answer="Yes. Your public wallet address is designed to be shared so you can receive funds. Never share your private key."
|
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
||||||
/>
|
return (
|
||||||
<FaqItem
|
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
question="Which currencies are supported?"
|
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
|
||||||
answer="Our generator supports standard URI schemes for Bitcoin, Ethereum, Solana, and can generally store any wallet string for other coins."
|
{question}
|
||||||
/>
|
<span className="transition group-open:rotate-180 text-slate-400">
|
||||||
<FaqItem
|
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
|
||||||
question="Can I add a specific amount?"
|
<path d="M6 9l6 6 6-6" />
|
||||||
answer="Yes, you can pre-fill an amount so when the user scans, their wallet app automatically suggests the correct payment value."
|
</svg>
|
||||||
/>
|
</span>
|
||||||
<FaqItem
|
</summary>
|
||||||
question="Does it work with all wallets?"
|
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
|
||||||
answer="Yes, standard crypto QR codes are universally readable by almost all modern wallet apps (Coinbase, MetaMask, Trust Wallet, etc.)."
|
{answer}
|
||||||
/>
|
</div>
|
||||||
<FaqItem
|
</details>
|
||||||
question="Are there any fees?"
|
);
|
||||||
answer="No. This generator is completely free. We do not charge any fees for generating codes or for the transactions made using them."
|
}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
|
||||||
return (
|
|
||||||
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
|
||||||
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
|
|
||||||
{question}
|
|
||||||
<span className="transition group-open:rotate-180 text-slate-400">
|
|
||||||
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
|
|
||||||
<path d="M6 9l6 6 6-6" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</summary>
|
|
||||||
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
|
|
||||||
{answer}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,297 +1,296 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import {
|
import {
|
||||||
Mail,
|
Mail,
|
||||||
Download,
|
Download,
|
||||||
Check,
|
Check,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Type,
|
Type,
|
||||||
FileText
|
FileText
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
// Brand Colors
|
// Brand Colors
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
paleGrey: '#EBEBDF',
|
paleGrey: '#EBEBDF',
|
||||||
richRed: '#dc2626',
|
richRed: '#dc2626',
|
||||||
};
|
};
|
||||||
|
|
||||||
// QR Color Options
|
// QR Color Options
|
||||||
const QR_COLORS = [
|
const QR_COLORS = [
|
||||||
{ name: 'Classic Black', value: '#000000' },
|
{ name: 'Classic Black', value: '#000000' },
|
||||||
{ name: 'Email Red', value: '#dc2626' },
|
{ name: 'Email Red', value: '#dc2626' },
|
||||||
{ name: 'Deep Blue', value: '#1E40AF' },
|
{ name: 'Deep Blue', value: '#1E40AF' },
|
||||||
{ name: 'Violet', value: '#7C3AED' },
|
{ name: 'Violet', value: '#7C3AED' },
|
||||||
{ name: 'Teal', value: '#0D9488' },
|
{ name: 'Teal', value: '#0D9488' },
|
||||||
{ name: 'Coral', value: '#F43F5E' },
|
{ name: 'Coral', value: '#F43F5E' },
|
||||||
{ name: 'Emerald', value: '#10B981' },
|
{ name: 'Emerald', value: '#10B981' },
|
||||||
{ name: 'Rose', value: '#F43F5E' },
|
{ name: 'Rose', value: '#F43F5E' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Frame Options
|
// Frame Options
|
||||||
const FRAME_OPTIONS = [
|
const FRAME_OPTIONS = [
|
||||||
{ id: 'none', label: 'No Frame' },
|
{ id: 'none', label: 'No Frame' },
|
||||||
{ id: 'email', label: 'Email Me' },
|
{ id: 'email', label: 'Email Me' },
|
||||||
{ id: 'contact', label: 'Contact' },
|
{ id: 'contact', label: 'Contact' },
|
||||||
{ id: 'send', label: 'Send Mail' },
|
{ id: 'send', label: 'Send Mail' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function EmailGenerator() {
|
export default function EmailGenerator() {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
email: '',
|
email: '',
|
||||||
subject: '',
|
subject: '',
|
||||||
body: ''
|
body: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const [qrColor, setQrColor] = useState('#dc2626');
|
const [qrColor, setQrColor] = useState('#dc2626');
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
|
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Generate Mailto Link
|
// Generate Mailto Link
|
||||||
// Format: mailto:email?subject=...&body=...
|
// Format: mailto:email?subject=...&body=...
|
||||||
const getMailtoUrl = () => {
|
const getMailtoUrl = () => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (formData.subject) params.append('subject', formData.subject);
|
if (formData.subject) params.append('subject', formData.subject);
|
||||||
if (formData.body) params.append('body', formData.body);
|
if (formData.body) params.append('body', formData.body);
|
||||||
|
|
||||||
const queryString = params.toString();
|
const queryString = params.toString();
|
||||||
return `mailto:${formData.email}${queryString ? `?${queryString}` : ''}`;
|
return `mailto:${formData.email}${queryString ? `?${queryString}` : ''}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = async (format: 'png' | 'svg') => {
|
const handleDownload = async (format: 'png' | 'svg') => {
|
||||||
if (!qrRef.current) return;
|
if (!qrRef.current) return;
|
||||||
try {
|
try {
|
||||||
if (format === 'png') {
|
if (format === 'png') {
|
||||||
const { toPng } = await import('html-to-image');
|
const { toPng } = await import('html-to-image');
|
||||||
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3 });
|
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3 });
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.download = `email-qr-code.png`;
|
link.download = `email-qr-code.png`;
|
||||||
link.href = dataUrl;
|
link.href = dataUrl;
|
||||||
link.click();
|
link.click();
|
||||||
} else {
|
} else {
|
||||||
const svgData = qrRef.current.querySelector('svg')?.outerHTML;
|
const svgData = qrRef.current.querySelector('svg')?.outerHTML;
|
||||||
if (svgData) {
|
if (svgData) {
|
||||||
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
||||||
const urlBlob = URL.createObjectURL(blob);
|
const urlBlob = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = urlBlob;
|
link.href = urlBlob;
|
||||||
link.download = `email-qr-code.svg`;
|
link.download = `email-qr-code.svg`;
|
||||||
link.click();
|
link.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
const frame = FRAME_OPTIONS.find(f => f.id === frameType);
|
const frame = FRAME_OPTIONS.find(f => f.id === frameType);
|
||||||
return frame?.id !== 'none' ? frame?.label : null;
|
return frame?.id !== 'none' ? frame?.label : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
|
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
|
||||||
<div className="grid lg:grid-cols-2">
|
<div className="grid lg:grid-cols-2">
|
||||||
|
|
||||||
{/* LEFT: Input Section */}
|
{/* LEFT: Input Section */}
|
||||||
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||||
|
|
||||||
{/* Input Fields */}
|
{/* Input Fields */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
||||||
<Mail className="w-5 h-5 text-red-600" />
|
<Mail className="w-5 h-5 text-red-600" />
|
||||||
Email Details
|
Email Details
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Recipient Email</label>
|
<label className="block text-sm font-medium text-slate-700 mb-2">Recipient Email</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Mail className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
|
<Mail className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
|
||||||
<Input
|
<Input
|
||||||
name="email"
|
name="email"
|
||||||
placeholder="recipient@example.com"
|
placeholder="recipient@example.com"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="h-11 rounded-xl pl-9"
|
className="h-11 rounded-xl pl-9"
|
||||||
type="email"
|
type="email"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Subject Line</label>
|
<label className="block text-sm font-medium text-slate-700 mb-2">Subject Line</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Type className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
|
<Type className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
|
||||||
<Input
|
<Input
|
||||||
name="subject"
|
name="subject"
|
||||||
placeholder="e.g. Inquiry about services"
|
placeholder="e.g. Inquiry about services"
|
||||||
value={formData.subject}
|
value={formData.subject}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="h-11 rounded-xl pl-9"
|
className="h-11 rounded-xl pl-9"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Body Message (Optional)</label>
|
<label className="block text-sm font-medium text-slate-700 mb-2">Body Message (Optional)</label>
|
||||||
<textarea
|
<textarea
|
||||||
name="body"
|
name="body"
|
||||||
placeholder="Hi there, I would like to know more about..."
|
placeholder="Hi there, I would like to know more about..."
|
||||||
value={formData.body}
|
value={formData.body}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full h-32 p-3 rounded-xl border border-slate-200 focus:border-red-600 focus:ring-1 focus:ring-red-600 focus:outline-none resize-none text-base"
|
className="w-full h-32 p-3 rounded-xl border border-slate-200 focus:border-red-600 focus:ring-1 focus:ring-red-600 focus:outline-none resize-none text-base"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="border-t border-slate-100"></div>
|
<div className="border-t border-slate-100"></div>
|
||||||
|
|
||||||
{/* Design Options */}
|
{/* Design Options */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
||||||
<Sparkles className="w-5 h-5 text-red-600" />
|
<Sparkles className="w-5 h-5 text-red-600" />
|
||||||
Design Options
|
Design Options
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Color Picker */}
|
{/* Color Picker */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-3">QR Code Color</label>
|
<label className="block text-sm font-medium text-slate-700 mb-3">QR Code Color</label>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{QR_COLORS.map((c) => (
|
{QR_COLORS.map((c) => (
|
||||||
<button
|
<button
|
||||||
key={c.name}
|
key={c.name}
|
||||||
onClick={() => setQrColor(c.value)}
|
onClick={() => setQrColor(c.value)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-9 h-9 rounded-full border-2 flex items-center justify-center transition-all hover:scale-110",
|
"w-9 h-9 rounded-full border-2 flex items-center justify-center transition-all hover:scale-110",
|
||||||
qrColor === c.value ? "border-slate-900 ring-2 ring-offset-2 ring-slate-200" : "border-white shadow-md"
|
qrColor === c.value ? "border-slate-900 ring-2 ring-offset-2 ring-slate-200" : "border-white shadow-md"
|
||||||
)}
|
)}
|
||||||
style={{ backgroundColor: c.value }}
|
style={{ backgroundColor: c.value }}
|
||||||
aria-label={`Select ${c.name}`}
|
aria-label={`Select ${c.name}`}
|
||||||
title={c.name}
|
title={c.name}
|
||||||
>
|
>
|
||||||
{qrColor === c.value && <Check className="w-4 h-4 text-white" strokeWidth={3} />}
|
{qrColor === c.value && <Check className="w-4 h-4 text-white" strokeWidth={3} />}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Frame Selector */}
|
{/* Frame Selector */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||||
{FRAME_OPTIONS.map((frame) => (
|
{FRAME_OPTIONS.map((frame) => (
|
||||||
<button
|
<button
|
||||||
key={frame.id}
|
key={frame.id}
|
||||||
onClick={() => setFrameType(frame.id)}
|
onClick={() => setFrameType(frame.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"py-2.5 px-3 rounded-lg text-sm font-medium transition-all border",
|
"py-2.5 px-3 rounded-lg text-sm font-medium transition-all border",
|
||||||
frameType === frame.id
|
frameType === frame.id
|
||||||
? "bg-red-600 text-white border-red-600"
|
? "bg-red-600 text-white border-red-600"
|
||||||
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{frame.label}
|
{frame.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* RIGHT: Preview Section */}
|
{/* RIGHT: Preview Section */}
|
||||||
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||||
|
|
||||||
{/* QR Card with Frame */}
|
{/* QR Card with Frame */}
|
||||||
<div
|
<div
|
||||||
ref={qrRef}
|
ref={qrRef}
|
||||||
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
|
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
|
||||||
style={{ minWidth: '320px' }}
|
>
|
||||||
>
|
{/* Frame Label */}
|
||||||
{/* Frame Label */}
|
{getFrameLabel() && (
|
||||||
{getFrameLabel() && (
|
<div
|
||||||
<div
|
className="mb-5 px-8 py-2.5 rounded-full text-white font-bold text-sm tracking-widest uppercase shadow-md"
|
||||||
className="mb-5 px-8 py-2.5 rounded-full text-white font-bold text-sm tracking-widest uppercase shadow-md"
|
style={{ backgroundColor: qrColor }}
|
||||||
style={{ backgroundColor: qrColor }}
|
>
|
||||||
>
|
{getFrameLabel()}
|
||||||
{getFrameLabel()}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
{/* QR Code */}
|
||||||
{/* QR Code */}
|
<div className="bg-white">
|
||||||
<div className="bg-white">
|
<QRCodeSVG
|
||||||
<QRCodeSVG
|
value={getMailtoUrl() || 'mailto:hello@example.com'}
|
||||||
value={getMailtoUrl() || 'mailto:hello@example.com'}
|
size={240}
|
||||||
size={240}
|
level="M"
|
||||||
level="M"
|
includeMargin={false}
|
||||||
includeMargin={false}
|
fgColor={qrColor}
|
||||||
fgColor={qrColor}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Info */}
|
||||||
{/* Info */}
|
<div className="mt-6 text-center">
|
||||||
<div className="mt-6 text-center">
|
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-red-50 mx-auto mb-3">
|
||||||
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-red-50 mx-auto mb-3">
|
<Mail className="w-6 h-6 text-red-600" />
|
||||||
<Mail className="w-6 h-6 text-red-600" />
|
</div>
|
||||||
</div>
|
<h3 className="font-bold text-slate-900 text-lg truncate max-w-[260px] mx-auto">
|
||||||
<h3 className="font-bold text-slate-900 text-lg truncate max-w-[260px] mx-auto">
|
{formData.email || 'Email QR Code'}
|
||||||
{formData.email || 'Email QR Code'}
|
</h3>
|
||||||
</h3>
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Download Buttons */}
|
||||||
{/* Download Buttons */}
|
<div className="flex items-center gap-3 mt-8">
|
||||||
<div className="flex items-center gap-3 mt-8">
|
<Button
|
||||||
<Button
|
onClick={() => handleDownload('png')}
|
||||||
onClick={() => handleDownload('png')}
|
className="bg-red-600 hover:bg-red-700 text-white shadow-lg"
|
||||||
className="bg-red-600 hover:bg-red-700 text-white shadow-lg"
|
>
|
||||||
>
|
<Download className="w-4 h-4 mr-2" />
|
||||||
<Download className="w-4 h-4 mr-2" />
|
Download PNG
|
||||||
Download PNG
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
onClick={() => handleDownload('svg')}
|
||||||
onClick={() => handleDownload('svg')}
|
variant="outline"
|
||||||
variant="outline"
|
className="border-slate-300 hover:bg-white"
|
||||||
className="border-slate-300 hover:bg-white"
|
>
|
||||||
>
|
<Download className="w-4 h-4 mr-2" />
|
||||||
<Download className="w-4 h-4 mr-2" />
|
SVG
|
||||||
SVG
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
|
||||||
|
<p className="text-xs text-slate-600 mt-4 text-center">
|
||||||
<p className="text-xs text-slate-500 mt-4 text-center">
|
100% free. No signup required.
|
||||||
100% free. No signup required.
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Upsell Banner */}
|
||||||
{/* Upsell Banner */}
|
<div className="mt-8 bg-gradient-to-r from-red-600 to-rose-700 rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
<div className="mt-8 bg-gradient-to-r from-red-600 to-rose-700 rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
<div className="text-white text-center sm:text-left">
|
||||||
<div className="text-white text-center sm:text-left">
|
<h3 className="font-bold text-lg">Change your email address often?</h3>
|
||||||
<h3 className="font-bold text-lg">Change your email address often?</h3>
|
<p className="text-white/80 text-sm mt-1">Dynamic QR Codes allow you to update the recipient without reprinting.</p>
|
||||||
<p className="text-white/80 text-sm mt-1">Dynamic QR Codes allow you to update the recipient without reprinting.</p>
|
</div>
|
||||||
</div>
|
<Link href="/signup">
|
||||||
<Link href="/signup">
|
<Button className="bg-white text-red-700 hover:bg-slate-100 shrink-0 shadow-lg">
|
||||||
<Button className="bg-white text-red-700 hover:bg-slate-100 shrink-0 shadow-lg">
|
Go Dynamic
|
||||||
Go Dynamic
|
</Button>
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,273 +1,269 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import EmailGenerator from './EmailGenerator';
|
import EmailGenerator from './EmailGenerator';
|
||||||
import { Mail, Zap, Smartphone, Lock, Download, Sparkles } from 'lucide-react';
|
import { Mail, Zap, Smartphone, Lock, Download, Sparkles } from 'lucide-react';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
||||||
|
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
||||||
// SEO Optimized Metadata
|
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Free Email QR Code Generator | Mailto QR | QR Master',
|
// SEO Optimized Metadata
|
||||||
description: 'Create an Email QR code to send emails instantly. Pre-fill subject and body. 100% free and client-side secure.',
|
export const metadata: Metadata = {
|
||||||
keywords: ['email qr code', 'mailto qr', 'email generator', 'free qr code'],
|
title: {
|
||||||
alternates: {
|
absolute: 'Free Email QR Code Generator | Email QR Code Erstellen | QR Master',
|
||||||
canonical: 'https://qrmaster.io/tools/email-qr-code',
|
},
|
||||||
},
|
description: 'Create an Email QR code to send emails instantly. Email QR Code erstellen mit Betreff und Text. 100% free and secure.',
|
||||||
openGraph: {
|
keywords: ['email qr code', 'mailto qr', 'email generator', 'free qr code', 'email qr code erstellen', 'email schreiben qr code', 'qr code für email', 'mailto qr code generator', 'email vorlage qr code'],
|
||||||
title: 'Free Email QR Code Generator | QR Master',
|
alternates: {
|
||||||
description: 'Send emails instantly with a custom QR code. Add recipient, subject, and body.',
|
canonical: 'https://www.qrmaster.net/tools/email-qr-code',
|
||||||
type: 'website',
|
},
|
||||||
url: 'https://qrmaster.io/tools/email-qr-code',
|
openGraph: {
|
||||||
images: [{ url: '/og-email-generator.png', width: 1200, height: 630 }],
|
title: 'Free Email QR Code Generator | QR Master',
|
||||||
},
|
description: 'Send emails instantly with a custom QR code. Add recipient, subject, and body.',
|
||||||
};
|
type: 'website',
|
||||||
|
url: 'https://www.qrmaster.net/tools/email-qr-code',
|
||||||
// JSON-LD
|
images: [{ url: '/og-email-generator.png', width: 1200, height: 630 }],
|
||||||
const jsonLd = {
|
},
|
||||||
'@context': 'https://schema.org',
|
};
|
||||||
'@graph': [
|
|
||||||
{
|
// JSON-LD
|
||||||
'@type': 'SoftwareApplication',
|
const jsonLd = {
|
||||||
name: 'Email QR Code Generator',
|
'@context': 'https://schema.org',
|
||||||
applicationCategory: 'UtilitiesApplication',
|
'@graph': [
|
||||||
operatingSystem: 'Web Browser',
|
generateSoftwareAppSchema(
|
||||||
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
'Email QR Code Generator',
|
||||||
description: 'Generate Email QR codes for mailto links with subject and body.',
|
'Generate Email QR codes for mailto links with subject and body.',
|
||||||
},
|
'/og-email-generator.png'
|
||||||
{
|
),
|
||||||
'@type': 'HowTo',
|
{
|
||||||
name: 'How to Create an Email QR Code',
|
'@type': 'HowTo',
|
||||||
step: [
|
name: 'How to Create an Email QR Code',
|
||||||
{ '@type': 'HowToStep', position: 1, name: 'Enter Recipient', text: 'Type the email address you want to receive emails at.' },
|
step: [
|
||||||
{ '@type': 'HowToStep', position: 2, name: 'Add Details', text: 'Optional: Add a pre-filled subject line and body text.' },
|
{ '@type': 'HowToStep', position: 1, name: 'Enter Recipient', text: 'Type the email address you want to receive emails at.' },
|
||||||
{ '@type': 'HowToStep', position: 3, name: 'Customize', text: 'Choose a brand color and add a call-to-action frame.' },
|
{ '@type': 'HowToStep', position: 2, name: 'Add Details', text: 'Optional: Add a pre-filled subject line and body text.' },
|
||||||
{ '@type': 'HowToStep', position: 4, name: 'Download', text: 'Download your QR code in PNG or SVG.' },
|
{ '@type': 'HowToStep', position: 3, name: 'Customize', text: 'Choose a brand color and add a call-to-action frame.' },
|
||||||
{ '@type': 'HowToStep', position: 5, name: 'Share', text: 'Add to business cards or flyers.' },
|
{ '@type': 'HowToStep', position: 4, name: 'Download', text: 'Download your QR code in PNG or SVG.' },
|
||||||
],
|
{ '@type': 'HowToStep', position: 5, name: 'Share', text: 'Add to business cards or flyers.' },
|
||||||
totalTime: 'PT30S',
|
],
|
||||||
},
|
totalTime: 'PT30S',
|
||||||
{
|
},
|
||||||
'@type': 'FAQPage',
|
generateFaqSchema({
|
||||||
mainEntity: [
|
'How does it work?': {
|
||||||
{
|
question: 'How does it work?',
|
||||||
'@type': 'Question',
|
answer: 'When scanned, it opens the user\'s default email app (like Gmail or Outlook) with a new draft composed to your address.',
|
||||||
name: 'How does it work?',
|
},
|
||||||
acceptedAnswer: { '@type': 'Answer', text: 'When scanned, it opens the user\'s default email app (like Gmail or Outlook) with a new draft composed to your address.' }
|
'Can I add a subject line?': {
|
||||||
},
|
question: 'Can I add a subject line?',
|
||||||
{
|
answer: 'Yes! You can pre-fill the subject line and the body content so the sender just has to hit send.',
|
||||||
'@type': 'Question',
|
},
|
||||||
name: 'Can I add a subject line?',
|
'Is it free?': {
|
||||||
acceptedAnswer: { '@type': 'Answer', text: 'Yes! You can pre-fill the subject line and the body content so the sender just has to hit send.' }
|
question: 'Is it free?',
|
||||||
},
|
answer: 'Yes, 100% free with unlimited scans.',
|
||||||
{
|
},
|
||||||
'@type': 'Question',
|
'Does it work with attachments?': {
|
||||||
name: 'Is it free?',
|
question: 'Does it work with attachments?',
|
||||||
acceptedAnswer: { '@type': 'Answer', text: 'Yes, 100% free with unlimited scans.' }
|
answer: 'No. The standard mailto format does not support attaching files automatically. Users will have to attach files manually.',
|
||||||
},
|
},
|
||||||
{
|
'Is it private?': {
|
||||||
'@type': 'Question',
|
question: 'Is it private?',
|
||||||
name: 'Does it work with attachments?',
|
answer: 'Yes. The data is encoded directly into the QR code. We do not store your email or message data.',
|
||||||
acceptedAnswer: { '@type': 'Answer', text: 'No. The standard mailto format does not support attaching files automatically. Users will have to attach files manually.' }
|
},
|
||||||
},
|
}),
|
||||||
{
|
]
|
||||||
'@type': 'Question',
|
};
|
||||||
name: 'Is it private?',
|
|
||||||
acceptedAnswer: { '@type': 'Answer', text: 'Yes. The data is encoded directly into the QR code. We do not store your email or message data.' }
|
export default function EmailPage() {
|
||||||
}
|
return (
|
||||||
]
|
<>
|
||||||
}
|
<script
|
||||||
]
|
type="application/ld+json"
|
||||||
};
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
export default function EmailPage() {
|
<ToolBreadcrumb toolName="Email QR Code Generator" toolSlug="email-qr-code" />
|
||||||
return (
|
|
||||||
<>
|
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
||||||
<script
|
|
||||||
type="application/ld+json"
|
{/* HERO SECTION */}
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden" style={{ backgroundColor: '#dc2626' }}>
|
||||||
/>
|
|
||||||
<ToolBreadcrumb toolName="Email QR Code Generator" toolSlug="email-qr-code" />
|
{/* Background Pattern */}
|
||||||
|
<div className="absolute inset-0 opacity-10">
|
||||||
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||||
|
<path d="M0 100 C 20 0 50 0 100 100 Z" fill="url(#grad1)" />
|
||||||
{/* HERO SECTION */}
|
<defs>
|
||||||
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden" style={{ backgroundColor: '#dc2626' }}>
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" style={{ stopColor: 'white', stopOpacity: 1 }} />
|
||||||
{/* Background Pattern */}
|
<stop offset="100%" style={{ stopColor: 'white', stopOpacity: 0 }} />
|
||||||
<div className="absolute inset-0 opacity-10">
|
</linearGradient>
|
||||||
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
|
</defs>
|
||||||
<path d="M0 100 C 20 0 50 0 100 100 Z" fill="url(#grad1)" />
|
</svg>
|
||||||
<defs>
|
</div>
|
||||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
||||||
<stop offset="0%" style={{ stopColor: 'white', stopOpacity: 1 }} />
|
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
||||||
<stop offset="100%" style={{ stopColor: 'white', stopOpacity: 0 }} />
|
|
||||||
</linearGradient>
|
{/* Left: Text Content */}
|
||||||
</defs>
|
<div className="text-center lg:text-left">
|
||||||
</svg>
|
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
|
||||||
</div>
|
<span className="flex h-2 w-2 relative">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-300 opacity-75"></span>
|
||||||
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-red-300"></span>
|
||||||
|
</span>
|
||||||
{/* Left: Text Content */}
|
Free Tool — No Signup Required
|
||||||
<div className="text-center lg:text-left">
|
</div>
|
||||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
|
|
||||||
<span className="flex h-2 w-2 relative">
|
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-300 opacity-75"></span>
|
The Smartest Way to <br className="hidden lg:block" />
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-red-300"></span>
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-red-200 to-rose-200">Receive Emails</span>
|
||||||
</span>
|
</h1>
|
||||||
Free Tool — No Signup Required
|
|
||||||
</div>
|
<p className="text-lg md:text-xl text-red-50 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
||||||
|
Create a QR code that opens a pre-composed email instantly.
|
||||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Perfect for feedback & inquiries.</strong>
|
||||||
The Smartest Way to <br className="hidden lg:block" />
|
</p>
|
||||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-red-200 to-rose-200">Receive Emails</span>
|
|
||||||
</h1>
|
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
||||||
|
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
||||||
<p className="text-lg md:text-xl text-red-50 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
<Mail className="w-4 h-4 text-red-300" />
|
||||||
Create a QR code that opens a pre-composed email instantly.
|
Instant Draft
|
||||||
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Perfect for feedback & inquiries.</strong>
|
</div>
|
||||||
</p>
|
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
||||||
|
<Zap className="w-4 h-4 text-yellow-300" />
|
||||||
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
Pre-filled Content
|
||||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
</div>
|
||||||
<Mail className="w-4 h-4 text-red-300" />
|
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
||||||
Instant Draft
|
<Smartphone className="w-4 h-4 text-red-300" />
|
||||||
</div>
|
Mobile Ready
|
||||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
</div>
|
||||||
<Zap className="w-4 h-4 text-yellow-300" />
|
</div>
|
||||||
Pre-filled Content
|
</div>
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
{/* Right: Visual Abstract Composition */}
|
||||||
<Smartphone className="w-4 h-4 text-red-300" />
|
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
|
||||||
Mobile Ready
|
{/* Decorative Glow */}
|
||||||
</div>
|
<div className="absolute w-[500px] h-[500px] bg-red-500/30 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
|
||||||
</div>
|
|
||||||
</div>
|
{/* Floating Glass Card */}
|
||||||
|
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform rotate-6 hover:rotate-3 transition-all duration-700 group">
|
||||||
{/* Right: Visual Abstract Composition */}
|
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
|
||||||
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
|
|
||||||
{/* Decorative Glow */}
|
{/* Mock QR */}
|
||||||
<div className="absolute w-[500px] h-[500px] bg-red-500/30 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
|
<div className="w-48 h-48 bg-white rounded-xl p-2 shadow-inner mb-6 relative overflow-hidden flex items-center justify-center">
|
||||||
|
<QRCodeSVG value="https://www.qrmaster.net" size={170} fgColor="#b91c1c" level="Q" />
|
||||||
{/* Floating Glass Card */}
|
{/* Scan Line */}
|
||||||
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform rotate-6 hover:rotate-3 transition-all duration-700 group">
|
<div className="absolute top-1/2 left-0 w-full h-1 bg-red-500 shadow-[0_0_20px_rgba(220,38,38,1)] animate-pulse" />
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
|
</div>
|
||||||
|
|
||||||
{/* Mock QR */}
|
<div className="w-full space-y-3">
|
||||||
<div className="w-48 h-48 bg-white rounded-xl p-2 shadow-inner mb-6 relative overflow-hidden flex items-center justify-center">
|
<div className="h-2 w-32 bg-white/20 rounded-full mx-auto" />
|
||||||
<QRCodeSVG value="https://www.qrmaster.net" size={170} fgColor="#b91c1c" level="Q" />
|
<div className="h-2 w-20 bg-white/10 rounded-full mx-auto" />
|
||||||
{/* Scan Line */}
|
</div>
|
||||||
<div className="absolute top-1/2 left-0 w-full h-1 bg-red-500 shadow-[0_0_20px_rgba(220,38,38,1)] animate-pulse" />
|
|
||||||
</div>
|
{/* Floating Badge */}
|
||||||
|
<div className="absolute -bottom-6 -left-6 bg-white py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
|
||||||
<div className="w-full space-y-3">
|
<div className="bg-red-100 p-2 rounded-full">
|
||||||
<div className="h-2 w-32 bg-white/20 rounded-full mx-auto" />
|
<Mail className="w-5 h-5 text-red-600" />
|
||||||
<div className="h-2 w-20 bg-white/10 rounded-full mx-auto" />
|
</div>
|
||||||
</div>
|
<div className="text-left">
|
||||||
|
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Status</div>
|
||||||
{/* Floating Badge */}
|
<div className="text-sm font-bold text-slate-900">Live</div>
|
||||||
<div className="absolute -bottom-6 -left-6 bg-white py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
|
</div>
|
||||||
<div className="bg-red-100 p-2 rounded-full">
|
</div>
|
||||||
<Mail className="w-5 h-5 text-red-600" />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-left">
|
</div>
|
||||||
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Status</div>
|
</section>
|
||||||
<div className="text-sm font-bold text-slate-900">Live</div>
|
|
||||||
</div>
|
{/* GENERATOR SECTION */}
|
||||||
</div>
|
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
|
||||||
</div>
|
<EmailGenerator />
|
||||||
</div>
|
</section>
|
||||||
</div>
|
|
||||||
</section>
|
{/* HOW IT WORKS */}
|
||||||
|
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
|
||||||
{/* GENERATOR SECTION */}
|
<div className="max-w-4xl mx-auto">
|
||||||
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
|
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
|
||||||
<EmailGenerator />
|
How Email QR Codes Work
|
||||||
</section>
|
</h2>
|
||||||
|
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
|
||||||
{/* HOW IT WORKS */}
|
<article className="text-center">
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
|
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
<div className="max-w-4xl mx-auto">
|
<Mail className="w-6 h-6 text-[#1A1265]" />
|
||||||
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
|
</div>
|
||||||
How Email QR Codes Work
|
<h3 className="font-bold text-slate-900 mb-2">1. Add Email</h3>
|
||||||
</h2>
|
<p className="text-slate-600 text-xs leading-relaxed">Enter the address and subject.</p>
|
||||||
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
|
</article>
|
||||||
<article className="text-center">
|
<article className="text-center">
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
<Mail className="w-6 h-6 text-[#1A1265]" />
|
<Sparkles className="w-6 h-6 text-[#1A1265]" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">1. Add Email</h3>
|
<h3 className="font-bold text-slate-900 mb-2">2. Customize</h3>
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">Enter the address and subject.</p>
|
<p className="text-slate-600 text-xs leading-relaxed">Pick a brand color.</p>
|
||||||
</article>
|
</article>
|
||||||
<article className="text-center">
|
<article className="text-center">
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
<Sparkles className="w-6 h-6 text-[#1A1265]" />
|
<Zap className="w-6 h-6 text-[#1A1265]" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">2. Customize</h3>
|
<h3 className="font-bold text-slate-900 mb-2">3. Style</h3>
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">Pick a brand color.</p>
|
<p className="text-slate-600 text-xs leading-relaxed">Add a cool frame.</p>
|
||||||
</article>
|
</article>
|
||||||
<article className="text-center">
|
<article className="text-center">
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
<Zap className="w-6 h-6 text-[#1A1265]" />
|
<Download className="w-6 h-6 text-[#1A1265]" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">3. Style</h3>
|
<h3 className="font-bold text-slate-900 mb-2">4. Download</h3>
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">Add a cool frame.</p>
|
<p className="text-slate-600 text-xs leading-relaxed">Save your QR code.</p>
|
||||||
</article>
|
</article>
|
||||||
<article className="text-center">
|
<article className="text-center">
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
<Download className="w-6 h-6 text-[#1A1265]" />
|
<Smartphone className="w-6 h-6 text-[#1A1265]" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">4. Download</h3>
|
<h3 className="font-bold text-slate-900 mb-2">5. Share</h3>
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">Save your QR code.</p>
|
<p className="text-slate-600 text-xs leading-relaxed">Print and get emails.</p>
|
||||||
</article>
|
</article>
|
||||||
<article className="text-center">
|
</div>
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
</div>
|
||||||
<Smartphone className="w-6 h-6 text-[#1A1265]" />
|
</section>
|
||||||
</div>
|
|
||||||
<h3 className="font-bold text-slate-900 mb-2">5. Share</h3>
|
{/* RELATED TOOLS */}
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">Print and get emails.</p>
|
<RelatedTools />
|
||||||
</article>
|
|
||||||
</div>
|
{/* FAQ SECTION */}
|
||||||
</div>
|
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
||||||
</section>
|
<div className="max-w-3xl mx-auto">
|
||||||
|
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
||||||
{/* FAQ SECTION */}
|
Frequently Asked Questions
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
</h2>
|
||||||
<div className="max-w-3xl mx-auto">
|
<p className="text-slate-600 text-center mb-10">Common questions about Email QR codes.</p>
|
||||||
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
<div className="space-y-4">
|
||||||
Frequently Asked Questions
|
<FaqItem question="Does it work with Gmail?" answer="Yes, and Outlook, Apple Mail, Yahoo, etc. It opens the default mail app on the user's device." />
|
||||||
</h2>
|
<FaqItem question="Is it reversible?" answer="Yes, if you made a mistake you would need to generate a new code, as static QR codes cannot be edited after creation." />
|
||||||
<p className="text-slate-600 text-center mb-10">Common questions about Email QR codes.</p>
|
<FaqItem question="Is this tool free?" answer="Yes, completely free to use." />
|
||||||
<div className="space-y-4">
|
<FaqItem question="Can I attach files?" answer="No. The mailto standard does not support automatic attachment of files. Users must attach them manually." />
|
||||||
<FaqItem question="Does it work with Gmail?" answer="Yes, and Outlook, Apple Mail, Yahoo, etc. It opens the default mail app on the user's device." />
|
<FaqItem question="Is it private?" answer="Yes. The data is encoded directly into the QR code. We do not store your email or message data." />
|
||||||
<FaqItem question="Is it reversible?" answer="Yes, if you made a mistake you would need to generate a new code, as static QR codes cannot be edited after creation." />
|
</div>
|
||||||
<FaqItem question="Is this tool free?" answer="Yes, completely free to use." />
|
</div>
|
||||||
<FaqItem question="Can I attach files?" answer="No. The mailto standard does not support automatic attachment of files. Users must attach them manually." />
|
</section>
|
||||||
<FaqItem question="Is it private?" answer="Yes. The data is encoded directly into the QR code. We do not store your email or message data." />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</section>
|
);
|
||||||
|
}
|
||||||
</div>
|
|
||||||
</>
|
// FAQ Item Component
|
||||||
);
|
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
||||||
}
|
return (
|
||||||
|
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
// FAQ Item Component
|
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
|
||||||
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
{question}
|
||||||
return (
|
<span className="transition group-open:rotate-180 text-slate-400">
|
||||||
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
|
||||||
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
|
<path d="M6 9l6 6 6-6" />
|
||||||
{question}
|
</svg>
|
||||||
<span className="transition group-open:rotate-180 text-slate-400">
|
</span>
|
||||||
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
|
</summary>
|
||||||
<path d="M6 9l6 6 6-6" />
|
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
|
||||||
</svg>
|
{answer}
|
||||||
</span>
|
</div>
|
||||||
</summary>
|
</details>
|
||||||
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
|
);
|
||||||
{answer}
|
}
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,331 +1,330 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
Download,
|
Download,
|
||||||
Check,
|
Check,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Clock,
|
Clock,
|
||||||
MapPin,
|
MapPin,
|
||||||
AlignLeft
|
AlignLeft
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
// Brand Colors
|
// Brand Colors
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
paleGrey: '#F5F3FF', // Violet-50
|
paleGrey: '#F5F3FF', // Violet-50
|
||||||
primary: '#7C3AED', // Violet-600
|
primary: '#7C3AED', // Violet-600
|
||||||
primaryDark: '#6D28D9', // Violet-700
|
primaryDark: '#6D28D9', // Violet-700
|
||||||
};
|
};
|
||||||
|
|
||||||
// QR Color Options
|
// QR Color Options
|
||||||
const QR_COLORS = [
|
const QR_COLORS = [
|
||||||
{ name: 'Violet', value: '#7C3AED' },
|
{ name: 'Violet', value: '#7C3AED' },
|
||||||
{ name: 'Purple', value: '#9333EA' },
|
{ name: 'Purple', value: '#9333EA' },
|
||||||
{ name: 'Classic Black', value: '#000000' },
|
{ name: 'Classic Black', value: '#000000' },
|
||||||
{ name: 'Deep Blue', value: '#1E40AF' },
|
{ name: 'Deep Blue', value: '#1E40AF' },
|
||||||
{ name: 'Pink', value: '#DB2777' },
|
{ name: 'Pink', value: '#DB2777' },
|
||||||
{ name: 'Emerald', value: '#10B981' },
|
{ name: 'Emerald', value: '#10B981' },
|
||||||
{ name: 'Rose', value: '#F43F5E' },
|
{ name: 'Rose', value: '#F43F5E' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Frame Options
|
// Frame Options
|
||||||
const FRAME_OPTIONS = [
|
const FRAME_OPTIONS = [
|
||||||
{ id: 'none', label: 'No Frame' },
|
{ id: 'none', label: 'No Frame' },
|
||||||
{ id: 'scanme', label: 'Scan Me' },
|
{ id: 'scanme', label: 'Scan Me' },
|
||||||
{ id: 'event', label: 'Event' },
|
{ id: 'event', label: 'Event' },
|
||||||
{ id: 'save', label: 'Save Date' },
|
{ id: 'save', label: 'Save Date' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function EventGenerator() {
|
export default function EventGenerator() {
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [location, setLocation] = useState('');
|
const [location, setLocation] = useState('');
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
const [startDate, setStartDate] = useState('');
|
const [startDate, setStartDate] = useState('');
|
||||||
const [endDate, setEndDate] = useState('');
|
const [endDate, setEndDate] = useState('');
|
||||||
|
|
||||||
const [qrColor, setQrColor] = useState(BRAND.primary);
|
const [qrColor, setQrColor] = useState(BRAND.primary);
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
|
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Format Date for iCal: YYYYMMDDTHHMMSS
|
// Format Date for iCal: YYYYMMDDTHHMMSS
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
if (!dateString) return '';
|
if (!dateString) return '';
|
||||||
const d = new Date(dateString);
|
const d = new Date(dateString);
|
||||||
// Basic formatting, assumes local time for simplicity in this static tool
|
// Basic formatting, assumes local time for simplicity in this static tool
|
||||||
const year = d.getFullYear();
|
const year = d.getFullYear();
|
||||||
const month = ('0' + (d.getMonth() + 1)).slice(-2);
|
const month = ('0' + (d.getMonth() + 1)).slice(-2);
|
||||||
const day = ('0' + d.getDate()).slice(-2);
|
const day = ('0' + d.getDate()).slice(-2);
|
||||||
const hours = ('0' + d.getHours()).slice(-2);
|
const hours = ('0' + d.getHours()).slice(-2);
|
||||||
const minutes = ('0' + d.getMinutes()).slice(-2);
|
const minutes = ('0' + d.getMinutes()).slice(-2);
|
||||||
const seconds = ('0' + d.getSeconds()).slice(-2);
|
const seconds = ('0' + d.getSeconds()).slice(-2);
|
||||||
return `${year}${month}${day}T${hours}${minutes}${seconds}`;
|
return `${year}${month}${day}T${hours}${minutes}${seconds}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const qrValue = [
|
const qrValue = [
|
||||||
'BEGIN:VEVENT',
|
'BEGIN:VEVENT',
|
||||||
`SUMMARY:${title}`,
|
`SUMMARY:${title}`,
|
||||||
`LOCATION:${location}`,
|
`LOCATION:${location}`,
|
||||||
`DESCRIPTION:${description}`,
|
`DESCRIPTION:${description}`,
|
||||||
`DTSTART:${formatDate(startDate)}`,
|
`DTSTART:${formatDate(startDate)}`,
|
||||||
`DTEND:${formatDate(endDate)}`,
|
`DTEND:${formatDate(endDate)}`,
|
||||||
'END:VEVENT'
|
'END:VEVENT'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
const handleDownload = async (format: 'png' | 'svg') => {
|
const handleDownload = async (format: 'png' | 'svg') => {
|
||||||
if (!qrRef.current) return;
|
if (!qrRef.current) return;
|
||||||
try {
|
try {
|
||||||
if (format === 'png') {
|
if (format === 'png') {
|
||||||
const { toPng } = await import('html-to-image');
|
const { toPng } = await import('html-to-image');
|
||||||
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3 });
|
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3 });
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.download = `event-qr-code.png`;
|
link.download = `event-qr-code.png`;
|
||||||
link.href = dataUrl;
|
link.href = dataUrl;
|
||||||
link.click();
|
link.click();
|
||||||
} else {
|
} else {
|
||||||
const svgData = qrRef.current.querySelector('svg')?.outerHTML;
|
const svgData = qrRef.current.querySelector('svg')?.outerHTML;
|
||||||
if (svgData) {
|
if (svgData) {
|
||||||
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = `event-qr-code.svg`;
|
link.download = `event-qr-code.svg`;
|
||||||
link.click();
|
link.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
const frame = FRAME_OPTIONS.find(f => f.id === frameType);
|
const frame = FRAME_OPTIONS.find(f => f.id === frameType);
|
||||||
return frame?.id !== 'none' ? frame?.label : null;
|
return frame?.id !== 'none' ? frame?.label : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
|
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
|
||||||
<div className="grid lg:grid-cols-2">
|
<div className="grid lg:grid-cols-2">
|
||||||
|
|
||||||
{/* LEFT: Input Section */}
|
{/* LEFT: Input Section */}
|
||||||
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||||
|
|
||||||
{/* Event Details */}
|
{/* Event Details */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
||||||
<Calendar className="w-5 h-5 text-[#7C3AED]" />
|
<Calendar className="w-5 h-5 text-[#7C3AED]" />
|
||||||
Event Details
|
Event Details
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Event Title</label>
|
<label className="block text-sm font-medium text-slate-700 mb-2">Event Title</label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Summer Party"
|
placeholder="Summer Party"
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#7C3AED] focus:ring-[#7C3AED]"
|
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#7C3AED] focus:ring-[#7C3AED]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Start Time</label>
|
<label className="block text-sm font-medium text-slate-700 mb-2">Start Time</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={startDate}
|
value={startDate}
|
||||||
onChange={(e) => setStartDate(e.target.value)}
|
onChange={(e) => setStartDate(e.target.value)}
|
||||||
className="h-12 text-sm rounded-xl border-slate-200 focus:border-[#1A1265] focus:ring-[#1A1265]"
|
className="h-12 text-sm rounded-xl border-slate-200 focus:border-[#1A1265] focus:ring-[#1A1265]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">End Time</label>
|
<label className="block text-sm font-medium text-slate-700 mb-2">End Time</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={endDate}
|
value={endDate}
|
||||||
onChange={(e) => setEndDate(e.target.value)}
|
onChange={(e) => setEndDate(e.target.value)}
|
||||||
className="h-12 text-sm rounded-xl border-slate-200 focus:border-[#7C3AED] focus:ring-[#7C3AED]"
|
className="h-12 text-sm rounded-xl border-slate-200 focus:border-[#7C3AED] focus:ring-[#7C3AED]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Location</label>
|
<label className="block text-sm font-medium text-slate-700 mb-2">Location</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<MapPin className="absolute left-3 top-3.5 w-5 h-5 text-slate-400" />
|
<MapPin className="absolute left-3 top-3.5 w-5 h-5 text-slate-400" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="123 Main St, New York"
|
placeholder="123 Main St, New York"
|
||||||
value={location}
|
value={location}
|
||||||
onChange={(e) => setLocation(e.target.value)}
|
onChange={(e) => setLocation(e.target.value)}
|
||||||
className="pl-10 h-12 text-base rounded-xl border-slate-200 focus:border-[#1A1265] focus:ring-[#1A1265]"
|
className="pl-10 h-12 text-base rounded-xl border-slate-200 focus:border-[#1A1265] focus:ring-[#1A1265]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Description</label>
|
<label className="block text-sm font-medium text-slate-700 mb-2">Description</label>
|
||||||
<textarea
|
<textarea
|
||||||
className="w-full h-24 p-4 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#1A1265] resize-none text-slate-800 placeholder:text-slate-400"
|
className="w-full h-24 p-4 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#1A1265] resize-none text-slate-800 placeholder:text-slate-400"
|
||||||
placeholder="Join us for a celebration..."
|
placeholder="Join us for a celebration..."
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-slate-100"></div>
|
<div className="border-t border-slate-100"></div>
|
||||||
|
|
||||||
{/* Design Options */}
|
{/* Design Options */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
||||||
<Sparkles className="w-5 h-5 text-[#7C3AED]" />
|
<Sparkles className="w-5 h-5 text-[#7C3AED]" />
|
||||||
Design Options
|
Design Options
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Color Picker */}
|
{/* Color Picker */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-3">QR Code Color</label>
|
<label className="block text-sm font-medium text-slate-700 mb-3">QR Code Color</label>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{QR_COLORS.map((c) => (
|
{QR_COLORS.map((c) => (
|
||||||
<button
|
<button
|
||||||
key={c.name}
|
key={c.name}
|
||||||
onClick={() => setQrColor(c.value)}
|
onClick={() => setQrColor(c.value)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-9 h-9 rounded-full border-2 flex items-center justify-center transition-all hover:scale-110",
|
"w-9 h-9 rounded-full border-2 flex items-center justify-center transition-all hover:scale-110",
|
||||||
qrColor === c.value ? "border-slate-900 ring-2 ring-offset-2 ring-slate-200" : "border-white shadow-md"
|
qrColor === c.value ? "border-slate-900 ring-2 ring-offset-2 ring-slate-200" : "border-white shadow-md"
|
||||||
)}
|
)}
|
||||||
style={{ backgroundColor: c.value }}
|
style={{ backgroundColor: c.value }}
|
||||||
aria-label={`Select ${c.name}`}
|
aria-label={`Select ${c.name}`}
|
||||||
title={c.name}
|
title={c.name}
|
||||||
>
|
>
|
||||||
{qrColor === c.value && <Check className="w-4 h-4 text-white" strokeWidth={3} />}
|
{qrColor === c.value && <Check className="w-4 h-4 text-white" strokeWidth={3} />}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Frame Selector */}
|
{/* Frame Selector */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||||
{FRAME_OPTIONS.map((frame) => (
|
{FRAME_OPTIONS.map((frame) => (
|
||||||
<button
|
<button
|
||||||
key={frame.id}
|
key={frame.id}
|
||||||
onClick={() => setFrameType(frame.id)}
|
onClick={() => setFrameType(frame.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"py-2.5 px-3 rounded-lg text-sm font-medium transition-all border",
|
"py-2.5 px-3 rounded-lg text-sm font-medium transition-all border",
|
||||||
frameType === frame.id
|
frameType === frame.id
|
||||||
? "bg-[#7C3AED] text-white border-[#7C3AED]"
|
? "bg-[#7C3AED] text-white border-[#7C3AED]"
|
||||||
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{frame.label}
|
{frame.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* RIGHT: Preview Section */}
|
{/* RIGHT: Preview Section */}
|
||||||
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||||
|
|
||||||
{/* QR Card with Frame */}
|
{/* QR Card with Frame */}
|
||||||
<div
|
<div
|
||||||
ref={qrRef}
|
ref={qrRef}
|
||||||
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
|
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
|
||||||
style={{ minWidth: '320px' }}
|
>
|
||||||
>
|
{/* Frame Label */}
|
||||||
{/* Frame Label */}
|
{getFrameLabel() && (
|
||||||
{getFrameLabel() && (
|
<div
|
||||||
<div
|
className="mb-5 px-8 py-2.5 rounded-full text-white font-bold text-sm tracking-widest uppercase shadow-md"
|
||||||
className="mb-5 px-8 py-2.5 rounded-full text-white font-bold text-sm tracking-widest uppercase shadow-md"
|
style={{ backgroundColor: qrColor }}
|
||||||
style={{ backgroundColor: qrColor }}
|
>
|
||||||
>
|
{getFrameLabel()}
|
||||||
{getFrameLabel()}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
{/* QR Code */}
|
||||||
{/* QR Code */}
|
<div className="bg-white">
|
||||||
<div className="bg-white">
|
<QRCodeSVG
|
||||||
<QRCodeSVG
|
value={(title || startDate) ? qrValue : "Title"}
|
||||||
value={(title || startDate) ? qrValue : "Title"}
|
size={240}
|
||||||
size={240}
|
level="M"
|
||||||
level="M"
|
includeMargin={false}
|
||||||
includeMargin={false}
|
fgColor={qrColor}
|
||||||
fgColor={qrColor}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Info Preview */}
|
||||||
{/* Info Preview */}
|
<div className="mt-6 text-center max-w-[260px]">
|
||||||
<div className="mt-6 text-center max-w-[260px]">
|
<h3 className="font-bold text-slate-900 text-lg flex items-center justify-center gap-2 truncate">
|
||||||
<h3 className="font-bold text-slate-900 text-lg flex items-center justify-center gap-2 truncate">
|
<Calendar className="w-4 h-4 text-[#7C3AED] shrink-0" />
|
||||||
<Calendar className="w-4 h-4 text-[#7C3AED] shrink-0" />
|
<span className="truncate">{title || 'Event Title'}</span>
|
||||||
<span className="truncate">{title || 'Event Title'}</span>
|
</h3>
|
||||||
</h3>
|
{(startDate) && (
|
||||||
{(startDate) && (
|
<div className="text-xs text-slate-600 mt-1 flex items-center justify-center gap-1">
|
||||||
<div className="text-xs text-slate-500 mt-1 flex items-center justify-center gap-1">
|
<Clock className="w-3 h-3" />
|
||||||
<Clock className="w-3 h-3" />
|
{new Date(startDate).toLocaleDateString()}
|
||||||
{new Date(startDate).toLocaleDateString()}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Download Buttons */}
|
||||||
{/* Download Buttons */}
|
<div className="flex items-center gap-3 mt-8">
|
||||||
<div className="flex items-center gap-3 mt-8">
|
<Button
|
||||||
<Button
|
onClick={() => handleDownload('png')}
|
||||||
onClick={() => handleDownload('png')}
|
className="bg-[#7C3AED] hover:bg-[#6D28D9] text-white shadow-lg"
|
||||||
className="bg-[#7C3AED] hover:bg-[#6D28D9] text-white shadow-lg"
|
>
|
||||||
>
|
<Download className="w-4 h-4 mr-2" />
|
||||||
<Download className="w-4 h-4 mr-2" />
|
Download PNG
|
||||||
Download PNG
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
onClick={() => handleDownload('svg')}
|
||||||
onClick={() => handleDownload('svg')}
|
variant="outline"
|
||||||
variant="outline"
|
className="border-slate-300 hover:bg-white"
|
||||||
className="border-slate-300 hover:bg-white"
|
>
|
||||||
>
|
<Download className="w-4 h-4 mr-2" />
|
||||||
<Download className="w-4 h-4 mr-2" />
|
SVG
|
||||||
SVG
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
|
||||||
|
<p className="text-xs text-slate-600 mt-4 text-center">
|
||||||
<p className="text-xs text-slate-500 mt-4 text-center">
|
Scanning adds the event to the user's calendar.
|
||||||
Scanning adds the event to the user's calendar.
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Upsell Banner */}
|
||||||
{/* Upsell Banner */}
|
<div className="mt-8 bg-gradient-to-r from-[#7C3AED] to-[#6D28D9] rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
<div className="mt-8 bg-gradient-to-r from-[#7C3AED] to-[#6D28D9] rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
<div className="text-white text-center sm:text-left">
|
||||||
<div className="text-white text-center sm:text-left">
|
<h3 className="font-bold text-lg">Planning a big event?</h3>
|
||||||
<h3 className="font-bold text-lg">Planning a big event?</h3>
|
<p className="text-white/80 text-sm mt-1">
|
||||||
<p className="text-white/80 text-sm mt-1">
|
Use a Dynamic QR Code to track RSVPs and update event details if the schedule changes.
|
||||||
Use a Dynamic QR Code to track RSVPs and update event details if the schedule changes.
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<Link href="/signup">
|
||||||
<Link href="/signup">
|
<Button className="bg-white text-[#7C3AED] hover:bg-slate-100 shrink-0 shadow-lg">
|
||||||
<Button className="bg-white text-[#7C3AED] hover:bg-slate-100 shrink-0 shadow-lg">
|
Get Dynamic Events
|
||||||
Get Dynamic Events
|
</Button>
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,353 +1,325 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import EventGenerator from './EventGenerator';
|
import EventGenerator from './EventGenerator';
|
||||||
import { Calendar, Shield, Zap, Smartphone, Clock, UserCheck, Download, Share2, Check } from 'lucide-react';
|
import { Calendar, Shield, Zap, Smartphone, Clock, UserCheck, Download, Share2, Check } from 'lucide-react';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
||||||
|
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
||||||
// SEO Optimized Metadata
|
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Free Event QR Code Generator | Add to Calendar | QR Master',
|
// SEO Optimized Metadata
|
||||||
description: 'Create a QR code for your event. Scanners can instantly save the date, time, and location to their phone calendar. Perfect for invitations and flyers.',
|
export const metadata: Metadata = {
|
||||||
keywords: ['event qr code', 'calendar qr code', 'save the date qr', 'ical qr generator', 'invitation qr code'],
|
title: {
|
||||||
alternates: {
|
absolute: 'Free Event QR Code Generator | Termin & Kalender QR | QR Master',
|
||||||
canonical: 'https://qrmaster.io/tools/event-qr-code',
|
},
|
||||||
},
|
description: 'Create a QR code for your event. Verabredung & Termine direkt in den Kalender speichern. Save the date instantly. Free & Easy.',
|
||||||
openGraph: {
|
keywords: ['event qr code', 'calendar qr code', 'save the date qr', 'ical qr generator', 'invitation qr code', 'event qr code erstellen', 'veranstaltung qr code', 'kalender qr code', 'termin qr code', 'save the date qr code'],
|
||||||
title: 'Free Event QR Code Generator | QR Master',
|
alternates: {
|
||||||
description: 'Generate QR codes to save events to calendars. Share dates easily.',
|
canonical: 'https://www.qrmaster.net/tools/event-qr-code',
|
||||||
type: 'website',
|
},
|
||||||
url: 'https://qrmaster.io/tools/event-qr-code',
|
openGraph: {
|
||||||
images: [{ url: '/og-event-generator.png', width: 1200, height: 630 }],
|
title: 'Free Event QR Code Generator | QR Master',
|
||||||
},
|
description: 'Generate QR codes to save events to calendars. Share dates easily.',
|
||||||
twitter: {
|
type: 'website',
|
||||||
card: 'summary_large_image',
|
url: 'https://www.qrmaster.net/tools/event-qr-code',
|
||||||
title: 'Free Event QR Code Generator',
|
images: [{ url: '/og-event-generator.png', width: 1200, height: 630 }],
|
||||||
description: 'Create QR codes for events. Instant save-to-calendar.',
|
},
|
||||||
},
|
twitter: {
|
||||||
robots: {
|
card: 'summary_large_image',
|
||||||
index: true,
|
title: 'Free Event QR Code Generator',
|
||||||
follow: true,
|
description: 'Create QR codes for events. Instant save-to-calendar.',
|
||||||
},
|
},
|
||||||
};
|
robots: {
|
||||||
|
index: true,
|
||||||
// JSON-LD Structured Data
|
follow: true,
|
||||||
const jsonLd = {
|
},
|
||||||
'@context': 'https://schema.org',
|
};
|
||||||
'@graph': [
|
|
||||||
{
|
// JSON-LD Structured Data
|
||||||
'@type': 'SoftwareApplication',
|
const jsonLd = {
|
||||||
name: 'Event QR Code Generator',
|
'@context': 'https://schema.org',
|
||||||
applicationCategory: 'UtilitiesApplication',
|
'@graph': [
|
||||||
operatingSystem: 'Web Browser',
|
generateSoftwareAppSchema(
|
||||||
offers: {
|
'Event QR Code Generator',
|
||||||
'@type': 'Offer',
|
'Generate QR codes that add event details to the user\'s digital calendar.',
|
||||||
price: '0',
|
'/og-event-generator.png'
|
||||||
priceCurrency: 'USD',
|
),
|
||||||
},
|
{
|
||||||
aggregateRating: {
|
'@type': 'HowTo',
|
||||||
'@type': 'AggregateRating',
|
name: 'How to Create an Event QR Code',
|
||||||
ratingValue: '4.8',
|
description: 'Create a QR code that saves an event to a calendar.',
|
||||||
ratingCount: '760',
|
step: [
|
||||||
},
|
{
|
||||||
description: 'Generate QR codes that add event details to the user\'s digital calendar.',
|
'@type': 'HowToStep',
|
||||||
},
|
position: 1,
|
||||||
{
|
name: 'Enter Event Details',
|
||||||
'@type': 'HowTo',
|
text: 'Fill in the Event Title, Location, Description, Start Time, and End Time.',
|
||||||
name: 'How to Create an Event QR Code',
|
},
|
||||||
description: 'Create a QR code that saves an event to a calendar.',
|
{
|
||||||
step: [
|
'@type': 'HowToStep',
|
||||||
{
|
position: 2,
|
||||||
'@type': 'HowToStep',
|
name: 'Customize',
|
||||||
position: 1,
|
text: 'Choose a color and frame style like "Save the Date".',
|
||||||
name: 'Enter Event Details',
|
},
|
||||||
text: 'Fill in the Event Title, Location, Description, Start Time, and End Time.',
|
{
|
||||||
},
|
'@type': 'HowToStep',
|
||||||
{
|
position: 3,
|
||||||
'@type': 'HowToStep',
|
name: 'Download',
|
||||||
position: 2,
|
text: 'Save the QR code and add it to your invitations.',
|
||||||
name: 'Customize',
|
},
|
||||||
text: 'Choose a color and frame style like "Save the Date".',
|
{
|
||||||
},
|
'@type': 'HowToStep',
|
||||||
{
|
position: 4,
|
||||||
'@type': 'HowToStep',
|
name: 'Test',
|
||||||
position: 3,
|
text: 'Scan the code to ensure the event details and times are correct.',
|
||||||
name: 'Download',
|
},
|
||||||
text: 'Save the QR code and add it to your invitations.',
|
{
|
||||||
},
|
'@type': 'HowToStep',
|
||||||
{
|
position: 5,
|
||||||
'@type': 'HowToStep',
|
name: 'Share',
|
||||||
position: 4,
|
text: 'Distribute it via email, flyers, or social media.',
|
||||||
name: 'Test',
|
},
|
||||||
text: 'Scan the code to ensure the event details and times are correct.',
|
],
|
||||||
},
|
totalTime: 'PT45S',
|
||||||
{
|
},
|
||||||
'@type': 'HowToStep',
|
generateFaqSchema({
|
||||||
position: 5,
|
'Which calendars does it support?': {
|
||||||
name: 'Share',
|
question: 'Which calendars does it support?',
|
||||||
text: 'Distribute it via email, flyers, or social media.',
|
answer: 'The QR code uses the standard iCalendar (ICS) format. It works with Apple Calendar, Google Calendar, Outlook, and most other mobile calendar apps.',
|
||||||
},
|
},
|
||||||
],
|
'Can I change the date after printing?': {
|
||||||
totalTime: 'PT45S',
|
question: 'Can I change the date after printing?',
|
||||||
},
|
answer: 'No. This is a static QR code, meaning the event details are permanently embedded in the image. If the date changes, you must create a new QR code. Use our Dynamic QR Code to edit events anytime.',
|
||||||
{
|
},
|
||||||
'@type': 'FAQPage',
|
'Is there a limit to the description length?': {
|
||||||
mainEntity: [
|
question: 'Is there a limit to the description length?',
|
||||||
{
|
answer: 'Yes, because the data is stored in the QR code pattern. We recommend keeping descriptions concise (under 300 characters) to ensure the code remains easy to scan.',
|
||||||
'@type': 'Question',
|
},
|
||||||
name: 'Which calendars does it support?',
|
'Do users need an app?': {
|
||||||
acceptedAnswer: {
|
question: 'Do users need an app?',
|
||||||
'@type': 'Answer',
|
answer: 'No special app is needed. Standard camera apps on iOS and Android can read the code and will prompt the user to "Add to Calendar".',
|
||||||
text: 'The QR code uses the standard iCalendar (ICS) format. It works with Apple Calendar, Google Calendar, Outlook, and most other mobile calendar apps.',
|
},
|
||||||
},
|
'Is it free?': {
|
||||||
},
|
question: 'Is it free?',
|
||||||
{
|
answer: 'Yes. Creating and scanning the code is completely free and requires no signup.',
|
||||||
'@type': 'Question',
|
},
|
||||||
name: 'Can I change the date after printing?',
|
}),
|
||||||
acceptedAnswer: {
|
],
|
||||||
'@type': 'Answer',
|
};
|
||||||
text: 'No. This is a static QR code, meaning the event details are permanently embedded in the image. If the date changes, you must create a new QR code. Use our Dynamic QR Code to edit events anytime.',
|
|
||||||
},
|
export default function EventQRCodePage() {
|
||||||
},
|
return (
|
||||||
{
|
<>
|
||||||
'@type': 'Question',
|
<script
|
||||||
name: 'Is there a limit to the description length?',
|
type="application/ld+json"
|
||||||
acceptedAnswer: {
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
'@type': 'Answer',
|
/>
|
||||||
text: 'Yes, because the data is stored in the QR code pattern. We recommend keeping descriptions concise (under 300 characters) to ensure the code remains easy to scan.',
|
<ToolBreadcrumb toolName="Event QR Code Generator" toolSlug="event-qr-code" />
|
||||||
},
|
|
||||||
},
|
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
||||||
{
|
|
||||||
'@type': 'Question',
|
{/* HERO SECTION */}
|
||||||
name: 'Do users need an app?',
|
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden" style={{ backgroundColor: '#5B21B6' }}>
|
||||||
acceptedAnswer: {
|
<div className="absolute inset-0 opacity-10">
|
||||||
'@type': 'Answer',
|
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||||
text: 'No special app is needed. Standard camera apps on iOS and Android can read the code and will prompt the user to "Add to Calendar".',
|
<path d="M0 100 C 20 0 50 0 100 100 Z" fill="url(#grad1)" />
|
||||||
},
|
<defs>
|
||||||
},
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
{
|
<stop offset="0%" style={{ stopColor: 'white', stopOpacity: 1 }} />
|
||||||
'@type': 'Question',
|
<stop offset="100%" style={{ stopColor: 'white', stopOpacity: 0 }} />
|
||||||
name: 'Is it free?',
|
</linearGradient>
|
||||||
acceptedAnswer: {
|
</defs>
|
||||||
'@type': 'Answer',
|
</svg>
|
||||||
text: 'Yes. Creating and scanning the code is completely free and requires no signup.',
|
</div>
|
||||||
},
|
|
||||||
},
|
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
||||||
],
|
<div className="text-center lg:text-left">
|
||||||
},
|
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
|
||||||
],
|
<span className="flex h-2 w-2 relative">
|
||||||
};
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-violet-400 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-violet-400"></span>
|
||||||
export default function EventQRCodePage() {
|
</span>
|
||||||
return (
|
Free Tool — No Signup Required
|
||||||
<>
|
</div>
|
||||||
<script
|
|
||||||
type="application/ld+json"
|
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
Create Scannable <br className="hidden lg:block" />
|
||||||
/>
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-violet-300 to-fuchsia-300">Calendar Invites</span>
|
||||||
<ToolBreadcrumb toolName="Event QR Code Generator" toolSlug="event-qr-code" />
|
</h1>
|
||||||
|
|
||||||
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
<p className="text-lg md:text-xl text-indigo-100 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
||||||
|
Share your event details instantly. Visitors scan to "Save the Date" directly to their phone calendar.
|
||||||
{/* HERO SECTION */}
|
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Perfect for invitations.</strong>
|
||||||
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden" style={{ backgroundColor: '#5B21B6' }}>
|
</p>
|
||||||
<div className="absolute inset-0 opacity-10">
|
|
||||||
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
|
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
||||||
<path d="M0 100 C 20 0 50 0 100 100 Z" fill="url(#grad1)" />
|
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
||||||
<defs>
|
<Calendar className="w-4 h-4 text-violet-300" />
|
||||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
|
Instant Save
|
||||||
<stop offset="0%" style={{ stopColor: 'white', stopOpacity: 1 }} />
|
</div>
|
||||||
<stop offset="100%" style={{ stopColor: 'white', stopOpacity: 0 }} />
|
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
||||||
</linearGradient>
|
<Clock className="w-4 h-4 text-amber-400" />
|
||||||
</defs>
|
Timezone Smart
|
||||||
</svg>
|
</div>
|
||||||
</div>
|
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
||||||
|
<UserCheck className="w-4 h-4 text-purple-400" />
|
||||||
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
Native Support
|
||||||
<div className="text-center lg:text-left">
|
</div>
|
||||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
|
</div>
|
||||||
<span className="flex h-2 w-2 relative">
|
</div>
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-violet-400 opacity-75"></span>
|
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-violet-400"></span>
|
{/* Visual Abstract */}
|
||||||
</span>
|
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
|
||||||
Free Tool — No Signup Required
|
<div className="absolute w-[500px] h-[500px] bg-indigo-500/30 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
|
||||||
</div>
|
|
||||||
|
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform -rotate-2 hover:rotate-1 transition-all duration-700 group">
|
||||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
|
||||||
Create Scannable <br className="hidden lg:block" />
|
|
||||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-violet-300 to-fuchsia-300">Calendar Invites</span>
|
<div className="w-full bg-white rounded-xl shadow-lg p-4 mb-6 relative overflow-hidden flex flex-col items-center text-center">
|
||||||
</h1>
|
<div className="w-full h-2 bg-red-500 rounded-full mb-3" />
|
||||||
|
<div className="text-xs uppercase font-bold text-red-500 tracking-widest mb-1">DECEMBER</div>
|
||||||
<p className="text-lg md:text-xl text-indigo-100 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
<div className="text-4xl font-black text-slate-900 leading-none mb-1">25</div>
|
||||||
Share your event details instantly. Visitors scan to "Save the Date" directly to their phone calendar.
|
<div className="text-xs text-slate-400">Saturday • 8:00 PM</div>
|
||||||
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Perfect for invitations.</strong>
|
</div>
|
||||||
</p>
|
|
||||||
|
<div className="w-44 h-44 bg-white rounded-xl p-2 shadow-inner relative overflow-hidden flex items-center justify-center">
|
||||||
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
<QRCodeSVG value="https://www.qrmaster.net" size={160} fgColor="#0f172a" level="Q" />
|
||||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
</div>
|
||||||
<Calendar className="w-4 h-4 text-violet-300" />
|
|
||||||
Instant Save
|
{/* Floating Badge */}
|
||||||
</div>
|
<div className="absolute -bottom-6 -left-6 bg-white py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
|
||||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
<div className="bg-emerald-100 p-2 rounded-full">
|
||||||
<Clock className="w-4 h-4 text-amber-400" />
|
<Check className="w-5 h-5 text-emerald-600" />
|
||||||
Timezone Smart
|
</div>
|
||||||
</div>
|
<div className="text-left">
|
||||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Event</div>
|
||||||
<UserCheck className="w-4 h-4 text-purple-400" />
|
<div className="text-sm font-bold text-slate-900">Added to Cal</div>
|
||||||
Native Support
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Visual Abstract */}
|
</section>
|
||||||
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
|
|
||||||
<div className="absolute w-[500px] h-[500px] bg-indigo-500/30 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
|
{/* GENERATOR SECTION */}
|
||||||
|
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
|
||||||
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform -rotate-2 hover:rotate-1 transition-all duration-700 group">
|
<EventGenerator />
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
|
</section>
|
||||||
|
|
||||||
<div className="w-full bg-white rounded-xl shadow-lg p-4 mb-6 relative overflow-hidden flex flex-col items-center text-center">
|
{/* HOW IT WORKS */}
|
||||||
<div className="w-full h-2 bg-red-500 rounded-full mb-3" />
|
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
|
||||||
<div className="text-xs uppercase font-bold text-red-500 tracking-widest mb-1">DECEMBER</div>
|
<div className="max-w-4xl mx-auto">
|
||||||
<div className="text-4xl font-black text-slate-900 leading-none mb-1">25</div>
|
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
|
||||||
<div className="text-xs text-slate-400">Saturday • 8:00 PM</div>
|
How Event QR Codes Work
|
||||||
</div>
|
</h2>
|
||||||
|
|
||||||
<div className="w-44 h-44 bg-white rounded-xl p-2 shadow-inner relative overflow-hidden flex items-center justify-center">
|
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
|
||||||
<QRCodeSVG value="https://www.qrmaster.net" size={160} fgColor="#0f172a" level="Q" />
|
<article className="text-center">
|
||||||
</div>
|
<div className="w-14 h-14 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Calendar className="w-7 h-7 text-[#1A1265]" />
|
||||||
{/* Floating Badge */}
|
</div>
|
||||||
<div className="absolute -bottom-6 -left-6 bg-white py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
|
<h3 className="font-bold text-slate-900 mb-2">1. Set Details</h3>
|
||||||
<div className="bg-emerald-100 p-2 rounded-full">
|
<p className="text-slate-600 text-sm">
|
||||||
<Check className="w-5 h-5 text-emerald-600" />
|
Enter the event name, location, and start/end times.
|
||||||
</div>
|
</p>
|
||||||
<div className="text-left">
|
</article>
|
||||||
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Event</div>
|
|
||||||
<div className="text-sm font-bold text-slate-900">Added to Cal</div>
|
<article className="text-center">
|
||||||
</div>
|
<div className="w-14 h-14 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
</div>
|
<Smartphone className="w-7 h-7 text-[#1A1265]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<h3 className="font-bold text-slate-900 mb-2">2. Scan</h3>
|
||||||
</div>
|
<p className="text-slate-600 text-sm">
|
||||||
</section>
|
Guests scan the code from your invite, poster, or flyer.
|
||||||
|
</p>
|
||||||
{/* GENERATOR SECTION */}
|
</article>
|
||||||
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
|
|
||||||
<EventGenerator />
|
<article className="text-center">
|
||||||
</section>
|
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Clock className="w-6 h-6 text-[#1A1265]" />
|
||||||
{/* HOW IT WORKS */}
|
</div>
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
|
<h3 className="font-bold text-slate-900 mb-2">3. Download</h3>
|
||||||
<div className="max-w-4xl mx-auto">
|
<p className="text-slate-600 text-xs leading-relaxed">
|
||||||
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
|
Save your event QR code.
|
||||||
How Event QR Codes Work
|
</p>
|
||||||
</h2>
|
</article>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
|
<article className="text-center">
|
||||||
<article className="text-center">
|
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
<div className="w-14 h-14 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
<Smartphone className="w-6 h-6 text-[#1A1265]" />
|
||||||
<Calendar className="w-7 h-7 text-[#1A1265]" />
|
</div>
|
||||||
</div>
|
<h3 className="font-bold text-slate-900 mb-2">4. Scan</h3>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">1. Set Details</h3>
|
<p className="text-slate-600 text-xs leading-relaxed">
|
||||||
<p className="text-slate-600 text-sm">
|
Guests scan the code.
|
||||||
Enter the event name, location, and start/end times.
|
</p>
|
||||||
</p>
|
</article>
|
||||||
</article>
|
|
||||||
|
<article className="text-center">
|
||||||
<article className="text-center">
|
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
<div className="w-14 h-14 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
<UserCheck className="w-6 h-6 text-[#1A1265]" />
|
||||||
<Smartphone className="w-7 h-7 text-[#1A1265]" />
|
</div>
|
||||||
</div>
|
<h3 className="font-bold text-slate-900 mb-2">5. Save</h3>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">2. Scan</h3>
|
<p className="text-slate-600 text-xs leading-relaxed">
|
||||||
<p className="text-slate-600 text-sm">
|
They tap "Add to Calendar."
|
||||||
Guests scan the code from your invite, poster, or flyer.
|
</p>
|
||||||
</p>
|
</article>
|
||||||
</article>
|
</div>
|
||||||
|
</div>
|
||||||
<article className="text-center">
|
</section>
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
|
||||||
<Clock className="w-6 h-6 text-[#1A1265]" />
|
{/* RELATED TOOLS */}
|
||||||
</div>
|
<RelatedTools />
|
||||||
<h3 className="font-bold text-slate-900 mb-2">3. Download</h3>
|
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">
|
{/* FAQ SECTION */}
|
||||||
Save your event QR code.
|
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
||||||
</p>
|
<div className="max-w-3xl mx-auto">
|
||||||
</article>
|
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
||||||
|
Frequently Asked Questions
|
||||||
<article className="text-center">
|
</h2>
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
<p className="text-slate-600 text-center mb-10">
|
||||||
<Smartphone className="w-6 h-6 text-[#1A1265]" />
|
Common questions about Event QR codes.
|
||||||
</div>
|
</p>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">4. Scan</h3>
|
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">
|
<div className="space-y-4">
|
||||||
Guests scan the code.
|
<FaqItem
|
||||||
</p>
|
question="Does this work with Google Calendar?"
|
||||||
</article>
|
answer="Yes, the generated QR code creates a standard .ics file event, which is compatible with Google Calendar, Apple Calendar, Outlook, and most others."
|
||||||
|
/>
|
||||||
<article className="text-center">
|
<FaqItem
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
question="Is the QR code reusable?"
|
||||||
<UserCheck className="w-6 h-6 text-[#1A1265]" />
|
answer="No. Because the specific date and time are embedded in the code, you cannot change them later. If the event is rescheduled, you must generate a new QR code."
|
||||||
</div>
|
/>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">5. Save</h3>
|
<FaqItem
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">
|
question="What happens if the event is in a different time zone?"
|
||||||
They tap "Add to Calendar."
|
answer="The user's calendar will usually convert the time to their local time zone automatically when they save it."
|
||||||
</p>
|
/>
|
||||||
</article>
|
<FaqItem
|
||||||
</div>
|
question="Is it free?"
|
||||||
</div>
|
answer="Yes. Creating and scanning the code is completely free."
|
||||||
</section>
|
/>
|
||||||
|
</div>
|
||||||
{/* FAQ SECTION */}
|
</div>
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
</section>
|
||||||
<div className="max-w-3xl mx-auto">
|
|
||||||
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
</div>
|
||||||
Frequently Asked Questions
|
</>
|
||||||
</h2>
|
);
|
||||||
<p className="text-slate-600 text-center mb-10">
|
}
|
||||||
Common questions about Event QR codes.
|
|
||||||
</p>
|
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
||||||
|
return (
|
||||||
<div className="space-y-4">
|
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
<FaqItem
|
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
|
||||||
question="Does this work with Google Calendar?"
|
{question}
|
||||||
answer="Yes, the generated QR code creates a standard .ics file event, which is compatible with Google Calendar, Apple Calendar, Outlook, and most others."
|
<span className="transition group-open:rotate-180 text-slate-400">
|
||||||
/>
|
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
|
||||||
<FaqItem
|
<path d="M6 9l6 6 6-6" />
|
||||||
question="Is the QR code reusable?"
|
</svg>
|
||||||
answer="No. Because the specific date and time are embedded in the code, you cannot change them later. If the event is rescheduled, you must generate a new QR code."
|
</span>
|
||||||
/>
|
</summary>
|
||||||
<FaqItem
|
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
|
||||||
question="What happens if the event is in a different time zone?"
|
{answer}
|
||||||
answer="The user's calendar will usually convert the time to their local time zone automatically when they save it."
|
</div>
|
||||||
/>
|
</details>
|
||||||
<FaqItem
|
);
|
||||||
question="Is it free?"
|
}
|
||||||
answer="Yes. Creating and scanning the code is completely free."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
|
||||||
return (
|
|
||||||
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
|
||||||
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
|
|
||||||
{question}
|
|
||||||
<span className="transition group-open:rotate-180 text-slate-400">
|
|
||||||
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
|
|
||||||
<path d="M6 9l6 6 6-6" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</summary>
|
|
||||||
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
|
|
||||||
{answer}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,248 +1,247 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import {
|
import {
|
||||||
Facebook,
|
Facebook,
|
||||||
Download,
|
Download,
|
||||||
Check,
|
Check,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
ThumbsUp,
|
ThumbsUp,
|
||||||
Globe
|
Globe
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
// Brand Colors
|
// Brand Colors
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
paleGrey: '#EBEBDF',
|
paleGrey: '#EBEBDF',
|
||||||
richBlue: '#1A1265',
|
richBlue: '#1A1265',
|
||||||
richBlueLight: '#2A2275',
|
richBlueLight: '#2A2275',
|
||||||
};
|
};
|
||||||
|
|
||||||
// QR Color Options - Facebook Theme
|
// QR Color Options - Facebook Theme
|
||||||
const QR_COLORS = [
|
const QR_COLORS = [
|
||||||
{ name: 'Facebook Blue', value: '#1877F2' },
|
{ name: 'Facebook Blue', value: '#1877F2' },
|
||||||
{ name: 'Classic Black', value: '#000000' },
|
{ name: 'Classic Black', value: '#000000' },
|
||||||
{ name: 'Dark Blue', value: '#1A1265' },
|
{ name: 'Dark Blue', value: '#1A1265' },
|
||||||
{ name: 'Teal', value: '#0D9488' },
|
{ name: 'Teal', value: '#0D9488' },
|
||||||
{ name: 'Coral', value: '#F43F5E' },
|
{ name: 'Coral', value: '#F43F5E' },
|
||||||
{ name: 'Purple', value: '#7C3AED' },
|
{ name: 'Purple', value: '#7C3AED' },
|
||||||
{ name: 'Emerald', value: '#10B981' },
|
{ name: 'Emerald', value: '#10B981' },
|
||||||
{ name: 'Rose', value: '#F43F5E' },
|
{ name: 'Rose', value: '#F43F5E' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Frame Options
|
// Frame Options
|
||||||
const FRAME_OPTIONS = [
|
const FRAME_OPTIONS = [
|
||||||
{ id: 'none', label: 'No Frame' },
|
{ id: 'none', label: 'No Frame' },
|
||||||
{ id: 'scanme', label: 'Scan Me' },
|
{ id: 'scanme', label: 'Scan Me' },
|
||||||
{ id: 'follow', label: 'Follow Us' },
|
{ id: 'follow', label: 'Follow Us' },
|
||||||
{ id: 'like', label: 'Like Us' },
|
{ id: 'like', label: 'Like Us' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function FacebookGenerator() {
|
export default function FacebookGenerator() {
|
||||||
const [url, setUrl] = useState('');
|
const [url, setUrl] = useState('');
|
||||||
const [qrColor, setQrColor] = useState('#1877F2'); // Default to FB Blue
|
const [qrColor, setQrColor] = useState('#1877F2'); // Default to FB Blue
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
|
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleDownload = async (format: 'png' | 'svg') => {
|
const handleDownload = async (format: 'png' | 'svg') => {
|
||||||
if (!qrRef.current) return;
|
if (!qrRef.current) return;
|
||||||
try {
|
try {
|
||||||
if (format === 'png') {
|
if (format === 'png') {
|
||||||
const { toPng } = await import('html-to-image');
|
const { toPng } = await import('html-to-image');
|
||||||
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3 });
|
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3 });
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.download = `facebook-qr-code.png`;
|
link.download = `facebook-qr-code.png`;
|
||||||
link.href = dataUrl;
|
link.href = dataUrl;
|
||||||
link.click();
|
link.click();
|
||||||
} else {
|
} else {
|
||||||
const svgData = qrRef.current.querySelector('svg')?.outerHTML;
|
const svgData = qrRef.current.querySelector('svg')?.outerHTML;
|
||||||
if (svgData) {
|
if (svgData) {
|
||||||
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = `facebook-qr-code.svg`;
|
link.download = `facebook-qr-code.svg`;
|
||||||
link.click();
|
link.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
const frame = FRAME_OPTIONS.find(f => f.id === frameType);
|
const frame = FRAME_OPTIONS.find(f => f.id === frameType);
|
||||||
return frame?.id !== 'none' ? frame?.label : null;
|
return frame?.id !== 'none' ? frame?.label : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
|
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
|
||||||
<div className="grid lg:grid-cols-2">
|
<div className="grid lg:grid-cols-2">
|
||||||
|
|
||||||
{/* LEFT: Input Section */}
|
{/* LEFT: Input Section */}
|
||||||
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||||
|
|
||||||
{/* Facebook Details */}
|
{/* Facebook Details */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
||||||
<Facebook className="w-5 h-5 text-[#1877F2]" />
|
<Facebook className="w-5 h-5 text-[#1877F2]" />
|
||||||
Facebook Page or Profile
|
Facebook Page or Profile
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Facebook URL</label>
|
<label className="block text-sm font-medium text-slate-700 mb-2">Facebook URL</label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="https://facebook.com/yourpage"
|
placeholder="https://facebook.com/yourpage"
|
||||||
value={url}
|
value={url}
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#1877F2] focus:ring-[#1877F2]"
|
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#1877F2] focus:ring-[#1877F2]"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-slate-500 mt-2">Paste the full link to your profile, page, group, or post.</p>
|
<p className="text-xs text-slate-600 mt-2">Paste the full link to your profile, page, group, or post.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-slate-100"></div>
|
<div className="border-t border-slate-100"></div>
|
||||||
|
|
||||||
{/* Design Options */}
|
{/* Design Options */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
||||||
<Sparkles className="w-5 h-5 text-[#1877F2]" />
|
<Sparkles className="w-5 h-5 text-[#1877F2]" />
|
||||||
Design Options
|
Design Options
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Color Picker */}
|
{/* Color Picker */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-3">QR Code Color</label>
|
<label className="block text-sm font-medium text-slate-700 mb-3">QR Code Color</label>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{QR_COLORS.map((c) => (
|
{QR_COLORS.map((c) => (
|
||||||
<button
|
<button
|
||||||
key={c.name}
|
key={c.name}
|
||||||
onClick={() => setQrColor(c.value)}
|
onClick={() => setQrColor(c.value)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-9 h-9 rounded-full border-2 flex items-center justify-center transition-all hover:scale-110",
|
"w-9 h-9 rounded-full border-2 flex items-center justify-center transition-all hover:scale-110",
|
||||||
qrColor === c.value ? "border-slate-900 ring-2 ring-offset-2 ring-slate-200" : "border-white shadow-md"
|
qrColor === c.value ? "border-slate-900 ring-2 ring-offset-2 ring-slate-200" : "border-white shadow-md"
|
||||||
)}
|
)}
|
||||||
style={{ backgroundColor: c.value }}
|
style={{ backgroundColor: c.value }}
|
||||||
aria-label={`Select ${c.name}`}
|
aria-label={`Select ${c.name}`}
|
||||||
title={c.name}
|
title={c.name}
|
||||||
>
|
>
|
||||||
{qrColor === c.value && <Check className="w-4 h-4 text-white" strokeWidth={3} />}
|
{qrColor === c.value && <Check className="w-4 h-4 text-white" strokeWidth={3} />}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Frame Selector */}
|
{/* Frame Selector */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||||
{FRAME_OPTIONS.map((frame) => (
|
{FRAME_OPTIONS.map((frame) => (
|
||||||
<button
|
<button
|
||||||
key={frame.id}
|
key={frame.id}
|
||||||
onClick={() => setFrameType(frame.id)}
|
onClick={() => setFrameType(frame.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"py-2.5 px-3 rounded-lg text-sm font-medium transition-all border",
|
"py-2.5 px-3 rounded-lg text-sm font-medium transition-all border",
|
||||||
frameType === frame.id
|
frameType === frame.id
|
||||||
? "bg-[#1877F2] text-white border-[#1877F2]"
|
? "bg-[#1877F2] text-white border-[#1877F2]"
|
||||||
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{frame.label}
|
{frame.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* RIGHT: Preview Section */}
|
{/* RIGHT: Preview Section */}
|
||||||
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||||
|
|
||||||
{/* QR Card with Frame */}
|
{/* QR Card with Frame */}
|
||||||
<div
|
<div
|
||||||
ref={qrRef}
|
ref={qrRef}
|
||||||
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
|
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
|
||||||
style={{ minWidth: '320px' }}
|
>
|
||||||
>
|
{/* Frame Label */}
|
||||||
{/* Frame Label */}
|
{getFrameLabel() && (
|
||||||
{getFrameLabel() && (
|
<div
|
||||||
<div
|
className="mb-5 px-8 py-2.5 rounded-full text-white font-bold text-sm tracking-widest uppercase shadow-md"
|
||||||
className="mb-5 px-8 py-2.5 rounded-full text-white font-bold text-sm tracking-widest uppercase shadow-md"
|
style={{ backgroundColor: qrColor }}
|
||||||
style={{ backgroundColor: qrColor }}
|
>
|
||||||
>
|
{getFrameLabel()}
|
||||||
{getFrameLabel()}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
{/* QR Code */}
|
||||||
{/* QR Code */}
|
<div className="bg-white">
|
||||||
<div className="bg-white">
|
<QRCodeSVG
|
||||||
<QRCodeSVG
|
value={url || "https://facebook.com"}
|
||||||
value={url || "https://facebook.com"}
|
size={240}
|
||||||
size={240}
|
level="M"
|
||||||
level="M"
|
includeMargin={false}
|
||||||
includeMargin={false}
|
fgColor={qrColor}
|
||||||
fgColor={qrColor}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Info Preview */}
|
||||||
{/* Info Preview */}
|
<div className="mt-6 text-center max-w-[260px]">
|
||||||
<div className="mt-6 text-center max-w-[260px]">
|
<h3 className="font-bold text-slate-900 text-lg flex items-center justify-center gap-2 truncate">
|
||||||
<h3 className="font-bold text-slate-900 text-lg flex items-center justify-center gap-2 truncate">
|
<Facebook className="w-4 h-4 text-slate-400 shrink-0" />
|
||||||
<Facebook className="w-4 h-4 text-slate-400 shrink-0" />
|
<span className="truncate">{url ? url.replace('https://', '') : 'facebook.com/...'}</span>
|
||||||
<span className="truncate">{url ? url.replace('https://', '') : 'facebook.com/...'}</span>
|
</h3>
|
||||||
</h3>
|
<div className="text-xs text-slate-600 mt-1">Opens in Facebook App</div>
|
||||||
<div className="text-xs text-slate-500 mt-1">Opens in Facebook App</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Download Buttons */}
|
||||||
{/* Download Buttons */}
|
<div className="flex items-center gap-3 mt-8">
|
||||||
<div className="flex items-center gap-3 mt-8">
|
<Button
|
||||||
<Button
|
onClick={() => handleDownload('png')}
|
||||||
onClick={() => handleDownload('png')}
|
className="bg-[#1877F2] hover:bg-[#155ebd] text-white shadow-lg"
|
||||||
className="bg-[#1877F2] hover:bg-[#155ebd] text-white shadow-lg"
|
>
|
||||||
>
|
<Download className="w-4 h-4 mr-2" />
|
||||||
<Download className="w-4 h-4 mr-2" />
|
Download PNG
|
||||||
Download PNG
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
onClick={() => handleDownload('svg')}
|
||||||
onClick={() => handleDownload('svg')}
|
variant="outline"
|
||||||
variant="outline"
|
className="border-slate-300 hover:bg-white"
|
||||||
className="border-slate-300 hover:bg-white"
|
>
|
||||||
>
|
<Download className="w-4 h-4 mr-2" />
|
||||||
<Download className="w-4 h-4 mr-2" />
|
SVG
|
||||||
SVG
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
|
||||||
|
<p className="text-xs text-slate-600 mt-4 text-center">
|
||||||
<p className="text-xs text-slate-500 mt-4 text-center">
|
Scanning redirects directly to the Facebook profile or page.
|
||||||
Scanning redirects directly to the Facebook profile or page.
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Upsell Banner */}
|
||||||
{/* Upsell Banner */}
|
<div className="mt-8 bg-gradient-to-r from-[#1877F2] to-[#155ebd] rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
<div className="mt-8 bg-gradient-to-r from-[#1877F2] to-[#155ebd] rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
<div className="text-white text-center sm:text-left">
|
||||||
<div className="text-white text-center sm:text-left">
|
<h3 className="font-bold text-lg">Running a Social Media Campaign?</h3>
|
||||||
<h3 className="font-bold text-lg">Running a Social Media Campaign?</h3>
|
<p className="text-white/80 text-sm mt-1">
|
||||||
<p className="text-white/80 text-sm mt-1">
|
Dynamic QR Codes allow you to track clicks, likes, and engagement rates in real-time.
|
||||||
Dynamic QR Codes allow you to track clicks, likes, and engagement rates in real-time.
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<Link href="/signup">
|
||||||
<Link href="/signup">
|
<Button className="bg-white text-[#1877F2] hover:bg-slate-100 shrink-0 shadow-lg">
|
||||||
<Button className="bg-white text-[#1877F2] hover:bg-slate-100 shrink-0 shadow-lg">
|
Get Social Analytics
|
||||||
Get Social Analytics
|
</Button>
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,365 +1,337 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import FacebookGenerator from './FacebookGenerator';
|
import FacebookGenerator from './FacebookGenerator';
|
||||||
import { Facebook, Shield, Zap, Smartphone, ThumbsUp, Users, Download, Share2 } from 'lucide-react';
|
import { Facebook, Shield, Zap, Smartphone, ThumbsUp, Users, Download, Share2 } from 'lucide-react';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
||||||
|
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
||||||
// SEO Optimized Metadata
|
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Free Facebook QR Code Generator | Get Likes & Follows | QR Master',
|
// SEO Optimized Metadata
|
||||||
description: 'Create a QR code for your Facebook Page, Profile, or Group. Scanners are redirected to the Facebook app instantly to like and follow. Free & Easy.',
|
export const metadata: Metadata = {
|
||||||
keywords: ['facebook qr code', 'fb qr generator', 'facebook page qr', 'follow qr code', 'social media qr code'],
|
title: {
|
||||||
alternates: {
|
absolute: 'Free Facebook QR Code Generator | Get Likes & Follows | QR Master',
|
||||||
canonical: 'https://qrmaster.io/tools/facebook-qr-code',
|
},
|
||||||
},
|
description: 'Create a QR code for your Facebook Page, Profile, or Group. Facebook QR Code erstellen. Scanners follow you instantly. Free & Easy.',
|
||||||
openGraph: {
|
keywords: ['facebook qr code', 'fb qr generator', 'facebook page qr', 'follow qr code', 'social media qr code', 'facebook qr code erstellen', 'facebook seite qr code', 'facebook gruppe qr code', 'facebook profil qr code', 'mehr likes qr code'],
|
||||||
title: 'Free Facebook QR Code Generator | QR Master',
|
alternates: {
|
||||||
description: 'Generate QR codes to grow your Facebook audience. Instant app redirect.',
|
canonical: 'https://www.qrmaster.net/tools/facebook-qr-code',
|
||||||
type: 'website',
|
},
|
||||||
url: 'https://qrmaster.io/tools/facebook-qr-code',
|
openGraph: {
|
||||||
images: [{ url: '/og-facebook-generator.png', width: 1200, height: 630 }],
|
title: 'Free Facebook QR Code Generator | QR Master',
|
||||||
},
|
description: 'Generate QR codes to grow your Facebook audience. Instant app redirect.',
|
||||||
twitter: {
|
type: 'website',
|
||||||
card: 'summary_large_image',
|
url: 'https://www.qrmaster.net/tools/facebook-qr-code',
|
||||||
title: 'Free Facebook QR Code Generator',
|
images: [{ url: '/og-facebook-generator.png', width: 1200, height: 630 }],
|
||||||
description: 'Create QR codes for Facebook. Boost your engagement.',
|
},
|
||||||
},
|
twitter: {
|
||||||
robots: {
|
card: 'summary_large_image',
|
||||||
index: true,
|
title: 'Free Facebook QR Code Generator',
|
||||||
follow: true,
|
description: 'Create QR codes for Facebook. Boost your engagement.',
|
||||||
},
|
},
|
||||||
};
|
robots: {
|
||||||
|
index: true,
|
||||||
// JSON-LD Structured Data
|
follow: true,
|
||||||
const jsonLd = {
|
},
|
||||||
'@context': 'https://schema.org',
|
};
|
||||||
'@graph': [
|
|
||||||
{
|
// JSON-LD Structured Data
|
||||||
'@type': 'SoftwareApplication',
|
const jsonLd = {
|
||||||
name: 'Facebook QR Code Generator',
|
'@context': 'https://schema.org',
|
||||||
applicationCategory: 'UtilitiesApplication',
|
'@graph': [
|
||||||
operatingSystem: 'Web Browser',
|
generateSoftwareAppSchema(
|
||||||
offers: {
|
'Facebook QR Code Generator',
|
||||||
'@type': 'Offer',
|
'Generate QR codes that direct users to a Facebook page, profile, or post.',
|
||||||
price: '0',
|
'/og-facebook-generator.png'
|
||||||
priceCurrency: 'USD',
|
),
|
||||||
},
|
{
|
||||||
aggregateRating: {
|
'@type': 'HowTo',
|
||||||
'@type': 'AggregateRating',
|
name: 'How to Create a Facebook QR Code',
|
||||||
ratingValue: '4.8',
|
description: 'Create a QR code that opens a Facebook page.',
|
||||||
ratingCount: '1120',
|
step: [
|
||||||
},
|
{
|
||||||
description: 'Generate QR codes that direct users to a Facebook page, profile, or post.',
|
'@type': 'HowToStep',
|
||||||
},
|
position: 1,
|
||||||
{
|
name: 'Get Link',
|
||||||
'@type': 'HowTo',
|
text: 'Copy the URL of your Facebook Page, Profile, or Group.',
|
||||||
name: 'How to Create a Facebook QR Code',
|
},
|
||||||
description: 'Create a QR code that opens a Facebook page.',
|
{
|
||||||
step: [
|
'@type': 'HowToStep',
|
||||||
{
|
position: 2,
|
||||||
'@type': 'HowToStep',
|
name: 'Paste Link',
|
||||||
position: 1,
|
text: 'Paste the URL into the generator.',
|
||||||
name: 'Get Link',
|
},
|
||||||
text: 'Copy the URL of your Facebook Page, Profile, or Group.',
|
{
|
||||||
},
|
'@type': 'HowToStep',
|
||||||
{
|
position: 3,
|
||||||
'@type': 'HowToStep',
|
name: 'Customize',
|
||||||
position: 2,
|
text: 'Choose your brand color and add a call-to-action frame.',
|
||||||
name: 'Paste Link',
|
},
|
||||||
text: 'Paste the URL into the generator.',
|
{
|
||||||
},
|
'@type': 'HowToStep',
|
||||||
{
|
position: 4,
|
||||||
'@type': 'HowToStep',
|
name: 'Download',
|
||||||
position: 3,
|
text: 'Save the QR code and print it on your marketing materials.',
|
||||||
name: 'Customize',
|
},
|
||||||
text: 'Choose your brand color and add a call-to-action frame.',
|
{
|
||||||
},
|
'@type': 'HowToStep',
|
||||||
{
|
position: 5,
|
||||||
'@type': 'HowToStep',
|
name: 'Share',
|
||||||
position: 4,
|
text: 'Distribute it on flyers, business cards, or posters.',
|
||||||
name: 'Download',
|
},
|
||||||
text: 'Save the QR code and print it on your marketing materials.',
|
],
|
||||||
},
|
totalTime: 'PT30S',
|
||||||
{
|
},
|
||||||
'@type': 'HowToStep',
|
generateFaqSchema({
|
||||||
position: 5,
|
'Does it open the Facebook app?': {
|
||||||
name: 'Share',
|
question: 'Does it open the Facebook app?',
|
||||||
text: 'Distribute it on flyers, business cards, or posters.',
|
answer: 'Yes! On most mobile devices, standard Facebook links are automatically detected and opened in the Facebook app if it is installed.',
|
||||||
},
|
},
|
||||||
],
|
'Can I link to a specific post?': {
|
||||||
totalTime: 'PT30S',
|
question: 'Can I link to a specific post?',
|
||||||
},
|
answer: 'Absolutely. Just paste the direct link to the post (click the timestamp on the post to get the link).',
|
||||||
{
|
},
|
||||||
'@type': 'FAQPage',
|
'Does it work for Facebook Events?': {
|
||||||
mainEntity: [
|
question: 'Does it work for Facebook Events?',
|
||||||
{
|
answer: 'Yes. Simply copy the full URL of your Facebook Event and paste it into the generator.',
|
||||||
'@type': 'Question',
|
},
|
||||||
name: 'Does it open the Facebook app?',
|
'Is it free?': {
|
||||||
acceptedAnswer: {
|
question: 'Is it free?',
|
||||||
'@type': 'Answer',
|
answer: 'Yes, this generator is 100% free to use for personal or business purposes.',
|
||||||
text: 'Yes! On most mobile devices, standard Facebook links are automatically detected and opened in the Facebook app if it is installed.',
|
},
|
||||||
},
|
'Can I track scans?': {
|
||||||
},
|
question: 'Can I track scans?',
|
||||||
{
|
answer: 'This static QR code does not include analytics. To track how many people scan your code, you should use our Dynamic QR Code service.',
|
||||||
'@type': 'Question',
|
},
|
||||||
name: 'Can I link to a specific post?',
|
}),
|
||||||
acceptedAnswer: {
|
],
|
||||||
'@type': 'Answer',
|
};
|
||||||
text: 'Absolutely. Just paste the direct link to the post (click the timestamp on the post to get the link).',
|
|
||||||
},
|
export default function FacebookQRCodePage() {
|
||||||
},
|
return (
|
||||||
{
|
<>
|
||||||
'@type': 'Question',
|
<script
|
||||||
name: 'Does it work for Facebook Events?',
|
type="application/ld+json"
|
||||||
acceptedAnswer: {
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
'@type': 'Answer',
|
/>
|
||||||
text: 'Yes. Simply copy the full URL of your Facebook Event and paste it into the generator.',
|
<ToolBreadcrumb toolName="Facebook QR Code Generator" toolSlug="facebook-qr-code" />
|
||||||
},
|
|
||||||
},
|
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
||||||
{
|
|
||||||
'@type': 'Question',
|
{/* HERO SECTION */}
|
||||||
name: 'Is it free?',
|
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden" style={{ backgroundColor: '#1877F2' }}>
|
||||||
acceptedAnswer: {
|
<div className="absolute inset-0 opacity-10">
|
||||||
'@type': 'Answer',
|
{/* Facebook Pattern */}
|
||||||
text: 'Yes, this generator is 100% free to use for personal or business purposes.',
|
<svg className="w-full h-full" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
||||||
},
|
<defs>
|
||||||
},
|
<pattern id="fb_pattern" width="60" height="60" patternUnits="userSpaceOnUse">
|
||||||
{
|
<path d="M30 30L35 35M25 35L30 30" stroke="white" strokeWidth="2" strokeOpacity="0.2" />
|
||||||
'@type': 'Question',
|
</pattern>
|
||||||
name: 'Can I track scans?',
|
</defs>
|
||||||
acceptedAnswer: {
|
<rect width="100%" height="100%" fill="url(#fb_pattern)" />
|
||||||
'@type': 'Answer',
|
</svg>
|
||||||
text: 'This static QR code does not include analytics. To track how many people scan your code, you should use our Dynamic QR Code service.',
|
</div>
|
||||||
},
|
|
||||||
},
|
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
||||||
],
|
<div className="text-center lg:text-left">
|
||||||
},
|
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
|
||||||
],
|
<span className="flex h-2 w-2 relative">
|
||||||
};
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-300 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-300"></span>
|
||||||
export default function FacebookQRCodePage() {
|
</span>
|
||||||
return (
|
Free Tool — No Signup Required
|
||||||
<>
|
</div>
|
||||||
<script
|
|
||||||
type="application/ld+json"
|
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
Grow Your Audience with <br className="hidden lg:block" />
|
||||||
/>
|
<span className="text-white drop-shadow-md">Facebook QR Codes</span>
|
||||||
<ToolBreadcrumb toolName="Facebook QR Code Generator" toolSlug="facebook-qr-code" />
|
</h1>
|
||||||
|
|
||||||
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
<p className="text-lg md:text-xl text-blue-50 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
||||||
|
Make it easy for customers to find and follow you. A single scan opens your Page directly in the Facebook app.
|
||||||
{/* HERO SECTION */}
|
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Boost likes instantly.</strong>
|
||||||
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden" style={{ backgroundColor: '#1877F2' }}>
|
</p>
|
||||||
<div className="absolute inset-0 opacity-10">
|
|
||||||
{/* Facebook Pattern */}
|
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
||||||
<svg className="w-full h-full" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
<div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
||||||
<defs>
|
<ThumbsUp className="w-4 h-4 text-blue-200" />
|
||||||
<pattern id="fb_pattern" width="60" height="60" patternUnits="userSpaceOnUse">
|
Get Likes
|
||||||
<path d="M30 30L35 35M25 35L30 30" stroke="white" strokeWidth="2" strokeOpacity="0.2" />
|
</div>
|
||||||
</pattern>
|
<div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
||||||
</defs>
|
<Zap className="w-4 h-4 text-yellow-300" />
|
||||||
<rect width="100%" height="100%" fill="url(#fb_pattern)" />
|
Instant Follow
|
||||||
</svg>
|
</div>
|
||||||
</div>
|
<div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
||||||
|
<Smartphone className="w-4 h-4 text-green-300" />
|
||||||
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
App Friendly
|
||||||
<div className="text-center lg:text-left">
|
</div>
|
||||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
|
</div>
|
||||||
<span className="flex h-2 w-2 relative">
|
</div>
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-300 opacity-75"></span>
|
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-300"></span>
|
{/* Visual Abstract */}
|
||||||
</span>
|
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
|
||||||
Free Tool — No Signup Required
|
<div className="absolute w-[500px] h-[500px] bg-blue-500/30 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
|
||||||
</div>
|
|
||||||
|
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform rotate-6 hover:rotate-3 transition-all duration-700 group">
|
||||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
|
||||||
Grow Your Audience with <br className="hidden lg:block" />
|
|
||||||
<span className="text-white drop-shadow-md">Facebook QR Codes</span>
|
<div className="w-full bg-white rounded-xl shadow-lg p-4 mb-6 relative overflow-hidden flex items-center gap-3">
|
||||||
</h1>
|
<div className="w-12 h-12 rounded-full bg-gradient-to-tr from-blue-600 to-blue-400 p-0.5">
|
||||||
|
<div className="w-full h-full bg-white rounded-full flex items-center justify-center">
|
||||||
<p className="text-lg md:text-xl text-blue-50 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
<Facebook className="w-6 h-6 text-[#1877F2]" fill="#1877F2" />
|
||||||
Make it easy for customers to find and follow you. A single scan opens your Page directly in the Facebook app.
|
</div>
|
||||||
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Boost likes instantly.</strong>
|
</div>
|
||||||
</p>
|
<div>
|
||||||
|
<div className="h-2.5 w-24 bg-slate-800 rounded-full mb-1.5" />
|
||||||
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
<div className="h-2 w-16 bg-slate-300 rounded-full" />
|
||||||
<div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
</div>
|
||||||
<ThumbsUp className="w-4 h-4 text-blue-200" />
|
<button className="ml-auto bg-[#1877F2] text-white px-3 py-1 rounded text-xs font-bold">
|
||||||
Get Likes
|
Like
|
||||||
</div>
|
</button>
|
||||||
<div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
</div>
|
||||||
<Zap className="w-4 h-4 text-yellow-300" />
|
|
||||||
Instant Follow
|
<div className="w-48 h-48 bg-white rounded-xl p-2 shadow-inner relative overflow-hidden flex items-center justify-center">
|
||||||
</div>
|
<QRCodeSVG value="https://www.qrmaster.net" size={170} fgColor="#1877F2" level="Q" />
|
||||||
<div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
</div>
|
||||||
<Smartphone className="w-4 h-4 text-green-300" />
|
|
||||||
App Friendly
|
{/* Floating Badge */}
|
||||||
</div>
|
<div className="absolute -bottom-6 -right-6 bg-white py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
|
||||||
</div>
|
<div className="bg-blue-100 p-2 rounded-full">
|
||||||
</div>
|
<Users className="w-5 h-5 text-blue-600" />
|
||||||
|
</div>
|
||||||
{/* Visual Abstract */}
|
<div className="text-left">
|
||||||
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
|
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Followers</div>
|
||||||
<div className="absolute w-[500px] h-[500px] bg-blue-500/30 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
|
<div className="text-sm font-bold text-slate-900">+1 New</div>
|
||||||
|
</div>
|
||||||
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform rotate-6 hover:rotate-3 transition-all duration-700 group">
|
</div>
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
|
</div>
|
||||||
|
</div>
|
||||||
<div className="w-full bg-white rounded-xl shadow-lg p-4 mb-6 relative overflow-hidden flex items-center gap-3">
|
</div>
|
||||||
<div className="w-12 h-12 rounded-full bg-gradient-to-tr from-blue-600 to-blue-400 p-0.5">
|
</section>
|
||||||
<div className="w-full h-full bg-white rounded-full flex items-center justify-center">
|
|
||||||
<Facebook className="w-6 h-6 text-[#1877F2]" fill="#1877F2" />
|
{/* GENERATOR SECTION */}
|
||||||
</div>
|
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
|
||||||
</div>
|
<FacebookGenerator />
|
||||||
<div>
|
</section>
|
||||||
<div className="h-2.5 w-24 bg-slate-800 rounded-full mb-1.5" />
|
|
||||||
<div className="h-2 w-16 bg-slate-300 rounded-full" />
|
{/* HOW IT WORKS */}
|
||||||
</div>
|
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
|
||||||
<button className="ml-auto bg-[#1877F2] text-white px-3 py-1 rounded text-xs font-bold">
|
<div className="max-w-4xl mx-auto">
|
||||||
Like
|
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
|
||||||
</button>
|
How Facebook QR Codes Work
|
||||||
</div>
|
</h2>
|
||||||
|
|
||||||
<div className="w-48 h-48 bg-white rounded-xl p-2 shadow-inner relative overflow-hidden flex items-center justify-center">
|
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
|
||||||
<QRCodeSVG value="https://www.qrmaster.net" size={170} fgColor="#1877F2" level="Q" />
|
<article className="text-center">
|
||||||
</div>
|
<div className="w-14 h-14 rounded-2xl bg-[#1877F2]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Facebook className="w-7 h-7 text-[#1877F2]" />
|
||||||
{/* Floating Badge */}
|
</div>
|
||||||
<div className="absolute -bottom-6 -right-6 bg-white py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
|
<h3 className="font-bold text-slate-900 mb-2">1. Copy Link</h3>
|
||||||
<div className="bg-blue-100 p-2 rounded-full">
|
<p className="text-slate-600 text-sm">
|
||||||
<Users className="w-5 h-5 text-blue-600" />
|
Go to your Facebook Page or Profile and copy the URL from the browser address bar.
|
||||||
</div>
|
</p>
|
||||||
<div className="text-left">
|
</article>
|
||||||
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Followers</div>
|
|
||||||
<div className="text-sm font-bold text-slate-900">+1 New</div>
|
<article className="text-center">
|
||||||
</div>
|
<div className="w-14 h-14 rounded-2xl bg-[#1877F2]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
</div>
|
<Smartphone className="w-7 h-7 text-[#1877F2]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<h3 className="font-bold text-slate-900 mb-2">2. Scan</h3>
|
||||||
</div>
|
<p className="text-slate-600 text-sm">
|
||||||
</section>
|
Your customers scan the code using their phone camera.
|
||||||
|
</p>
|
||||||
{/* GENERATOR SECTION */}
|
</article>
|
||||||
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
|
|
||||||
<FacebookGenerator />
|
<article className="text-center">
|
||||||
</section>
|
<div className="w-12 h-12 rounded-2xl bg-[#1877F2]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
|
<ThumbsUp className="w-6 h-6 text-[#1877F2]" />
|
||||||
{/* HOW IT WORKS */}
|
</div>
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
|
<h3 className="font-bold text-slate-900 mb-2">3. Engage</h3>
|
||||||
<div className="max-w-4xl mx-auto">
|
<p className="text-slate-600 text-xs leading-relaxed">
|
||||||
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
|
The Facebook app opens directly.
|
||||||
How Facebook QR Codes Work
|
</p>
|
||||||
</h2>
|
</article>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
|
<article className="text-center">
|
||||||
<article className="text-center">
|
<div className="w-12 h-12 rounded-2xl bg-[#1877F2]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
<div className="w-14 h-14 rounded-2xl bg-[#1877F2]/10 flex items-center justify-center mx-auto mb-4">
|
<Download className="w-6 h-6 text-[#1877F2]" />
|
||||||
<Facebook className="w-7 h-7 text-[#1877F2]" />
|
</div>
|
||||||
</div>
|
<h3 className="font-bold text-slate-900 mb-2">4. Download</h3>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">1. Copy Link</h3>
|
<p className="text-slate-600 text-xs leading-relaxed">
|
||||||
<p className="text-slate-600 text-sm">
|
Save your high-res QR code.
|
||||||
Go to your Facebook Page or Profile and copy the URL from the browser address bar.
|
</p>
|
||||||
</p>
|
</article>
|
||||||
</article>
|
|
||||||
|
<article className="text-center">
|
||||||
<article className="text-center">
|
<div className="w-12 h-12 rounded-2xl bg-[#1877F2]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
<div className="w-14 h-14 rounded-2xl bg-[#1877F2]/10 flex items-center justify-center mx-auto mb-4">
|
<Share2 className="w-6 h-6 text-[#1877F2]" />
|
||||||
<Smartphone className="w-7 h-7 text-[#1877F2]" />
|
</div>
|
||||||
</div>
|
<h3 className="font-bold text-slate-900 mb-2">5. Share</h3>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">2. Scan</h3>
|
<p className="text-slate-600 text-xs leading-relaxed">
|
||||||
<p className="text-slate-600 text-sm">
|
Print it and start getting likes.
|
||||||
Your customers scan the code using their phone camera.
|
</p>
|
||||||
</p>
|
</article>
|
||||||
</article>
|
</div>
|
||||||
|
</div>
|
||||||
<article className="text-center">
|
</section>
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#1877F2]/10 flex items-center justify-center mx-auto mb-4">
|
|
||||||
<ThumbsUp className="w-6 h-6 text-[#1877F2]" />
|
{/* RELATED TOOLS */}
|
||||||
</div>
|
<RelatedTools />
|
||||||
<h3 className="font-bold text-slate-900 mb-2">3. Engage</h3>
|
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">
|
{/* FAQ SECTION */}
|
||||||
The Facebook app opens directly.
|
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
||||||
</p>
|
<div className="max-w-3xl mx-auto">
|
||||||
</article>
|
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
||||||
|
Frequently Asked Questions
|
||||||
<article className="text-center">
|
</h2>
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#1877F2]/10 flex items-center justify-center mx-auto mb-4">
|
<p className="text-slate-600 text-center mb-10">
|
||||||
<Download className="w-6 h-6 text-[#1877F2]" />
|
Common questions about Facebook QR codes.
|
||||||
</div>
|
</p>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">4. Download</h3>
|
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">
|
<div className="space-y-4">
|
||||||
Save your high-res QR code.
|
<FaqItem
|
||||||
</p>
|
question="Will this work for Facebook Groups?"
|
||||||
</article>
|
answer="Yes! You can paste the link to your Facebook Group, and the QR code will direcr users to join."
|
||||||
|
/>
|
||||||
<article className="text-center">
|
<FaqItem
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#1877F2]/10 flex items-center justify-center mx-auto mb-4">
|
question="What if the user doesn't have the Facebook app?"
|
||||||
<Share2 className="w-6 h-6 text-[#1877F2]" />
|
answer="The link will open in their mobile web browser instead, so they can still see your page and log in."
|
||||||
</div>
|
/>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">5. Share</h3>
|
<FaqItem
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">
|
question="Can I customize the color?"
|
||||||
Print it and start getting likes.
|
answer="Yes. While Facebook Blue is recommended for recognition, you can choose any color to match your brand."
|
||||||
</p>
|
/>
|
||||||
</article>
|
<FaqItem
|
||||||
</div>
|
question="Is the QR code permanent?"
|
||||||
</div>
|
answer="Yes. As long as your Facebook URL doesn't change, this QR code will work forever."
|
||||||
</section>
|
/>
|
||||||
|
<FaqItem
|
||||||
{/* FAQ SECTION */}
|
question="Does it work for Facebook Events?"
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
answer="Yes. Simply copy the full URL of your Facebook Event and paste it into the generator."
|
||||||
<div className="max-w-3xl mx-auto">
|
/>
|
||||||
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
</div>
|
||||||
Frequently Asked Questions
|
</div>
|
||||||
</h2>
|
</section>
|
||||||
<p className="text-slate-600 text-center mb-10">
|
|
||||||
Common questions about Facebook QR codes.
|
</div>
|
||||||
</p>
|
</>
|
||||||
|
);
|
||||||
<div className="space-y-4">
|
}
|
||||||
<FaqItem
|
|
||||||
question="Will this work for Facebook Groups?"
|
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
||||||
answer="Yes! You can paste the link to your Facebook Group, and the QR code will direcr users to join."
|
return (
|
||||||
/>
|
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
<FaqItem
|
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
|
||||||
question="What if the user doesn't have the Facebook app?"
|
{question}
|
||||||
answer="The link will open in their mobile web browser instead, so they can still see your page and log in."
|
<span className="transition group-open:rotate-180 text-slate-400">
|
||||||
/>
|
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
|
||||||
<FaqItem
|
<path d="M6 9l6 6 6-6" />
|
||||||
question="Can I customize the color?"
|
</svg>
|
||||||
answer="Yes. While Facebook Blue is recommended for recognition, you can choose any color to match your brand."
|
</span>
|
||||||
/>
|
</summary>
|
||||||
<FaqItem
|
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
|
||||||
question="Is the QR code permanent?"
|
{answer}
|
||||||
answer="Yes. As long as your Facebook URL doesn't change, this QR code will work forever."
|
</div>
|
||||||
/>
|
</details>
|
||||||
<FaqItem
|
);
|
||||||
question="Does it work for Facebook Events?"
|
}
|
||||||
answer="Yes. Simply copy the full URL of your Facebook Event and paste it into the generator."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
|
||||||
return (
|
|
||||||
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
|
||||||
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
|
|
||||||
{question}
|
|
||||||
<span className="transition group-open:rotate-180 text-slate-400">
|
|
||||||
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
|
|
||||||
<path d="M6 9l6 6 6-6" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</summary>
|
|
||||||
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
|
|
||||||
{answer}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,293 +1,292 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import {
|
import {
|
||||||
MapPin,
|
MapPin,
|
||||||
Download,
|
Download,
|
||||||
Check,
|
Check,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Navigation,
|
Navigation,
|
||||||
Globe
|
Globe
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
// Brand Colors
|
// Brand Colors
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
paleGrey: '#ECFDF5', // Emerald-50
|
paleGrey: '#ECFDF5', // Emerald-50
|
||||||
primary: '#10B981', // Emerald-500
|
primary: '#10B981', // Emerald-500
|
||||||
primaryDark: '#047857', // Emerald-700
|
primaryDark: '#047857', // Emerald-700
|
||||||
};
|
};
|
||||||
|
|
||||||
// QR Color Options
|
// QR Color Options
|
||||||
const QR_COLORS = [
|
const QR_COLORS = [
|
||||||
{ name: 'Emerald', value: '#10B981' },
|
{ name: 'Emerald', value: '#10B981' },
|
||||||
{ name: 'Teal', value: '#0D9488' },
|
{ name: 'Teal', value: '#0D9488' },
|
||||||
{ name: 'Classic Black', value: '#000000' },
|
{ name: 'Classic Black', value: '#000000' },
|
||||||
{ name: 'Navy', value: '#1E3A8A' },
|
{ name: 'Navy', value: '#1E3A8A' },
|
||||||
{ name: 'Sky', value: '#0EA5E9' },
|
{ name: 'Sky', value: '#0EA5E9' },
|
||||||
{ name: 'Emerald', value: '#10B981' },
|
{ name: 'Emerald', value: '#10B981' },
|
||||||
{ name: 'Rose', value: '#F43F5E' },
|
{ name: 'Rose', value: '#F43F5E' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Frame Options
|
// Frame Options
|
||||||
const FRAME_OPTIONS = [
|
const FRAME_OPTIONS = [
|
||||||
{ id: 'none', label: 'No Frame' },
|
{ id: 'none', label: 'No Frame' },
|
||||||
{ id: 'scanme', label: 'Scan Me' },
|
{ id: 'scanme', label: 'Scan Me' },
|
||||||
{ id: 'location', label: 'Location' },
|
{ id: 'location', label: 'Location' },
|
||||||
{ id: 'map', label: 'View Map' },
|
{ id: 'map', label: 'View Map' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function GeolocationGenerator() {
|
export default function GeolocationGenerator() {
|
||||||
const [latitude, setLatitude] = useState('');
|
const [latitude, setLatitude] = useState('');
|
||||||
const [longitude, setLongitude] = useState('');
|
const [longitude, setLongitude] = useState('');
|
||||||
const [qrColor, setQrColor] = useState(BRAND.primary);
|
const [qrColor, setQrColor] = useState(BRAND.primary);
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
|
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Using Google Maps Universal Link for best compatibility
|
// Using Google Maps Universal Link for best compatibility
|
||||||
const qrValue = `https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}`;
|
const qrValue = `https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}`;
|
||||||
|
|
||||||
const handleDownload = async (format: 'png' | 'svg') => {
|
const handleDownload = async (format: 'png' | 'svg') => {
|
||||||
if (!qrRef.current) return;
|
if (!qrRef.current) return;
|
||||||
try {
|
try {
|
||||||
if (format === 'png') {
|
if (format === 'png') {
|
||||||
const { toPng } = await import('html-to-image');
|
const { toPng } = await import('html-to-image');
|
||||||
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3 });
|
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3 });
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.download = `location-qr-code.png`;
|
link.download = `location-qr-code.png`;
|
||||||
link.href = dataUrl;
|
link.href = dataUrl;
|
||||||
link.click();
|
link.click();
|
||||||
} else {
|
} else {
|
||||||
const svgData = qrRef.current.querySelector('svg')?.outerHTML;
|
const svgData = qrRef.current.querySelector('svg')?.outerHTML;
|
||||||
if (svgData) {
|
if (svgData) {
|
||||||
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = `location-qr-code.svg`;
|
link.download = `location-qr-code.svg`;
|
||||||
link.click();
|
link.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
const frame = FRAME_OPTIONS.find(f => f.id === frameType);
|
const frame = FRAME_OPTIONS.find(f => f.id === frameType);
|
||||||
return frame?.id !== 'none' ? frame?.label : null;
|
return frame?.id !== 'none' ? frame?.label : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentLocation = () => {
|
const getCurrentLocation = () => {
|
||||||
if (navigator.geolocation) {
|
if (navigator.geolocation) {
|
||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
(position) => {
|
(position) => {
|
||||||
setLatitude(position.coords.latitude.toFixed(6));
|
setLatitude(position.coords.latitude.toFixed(6));
|
||||||
setLongitude(position.coords.longitude.toFixed(6));
|
setLongitude(position.coords.longitude.toFixed(6));
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error("Error getting location: ", error);
|
console.error("Error getting location: ", error);
|
||||||
alert("Could not access location. Please enter manually.");
|
alert("Could not access location. Please enter manually.");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
alert("Geolocation is not supported by this browser.");
|
alert("Geolocation is not supported by this browser.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
|
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
|
||||||
<div className="grid lg:grid-cols-2">
|
<div className="grid lg:grid-cols-2">
|
||||||
|
|
||||||
{/* LEFT: Input Section */}
|
{/* LEFT: Input Section */}
|
||||||
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||||
|
|
||||||
{/* Location Details */}
|
{/* Location Details */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
||||||
<MapPin className="w-5 h-5 text-[#10B981]" />
|
<MapPin className="w-5 h-5 text-[#10B981]" />
|
||||||
Coordinates
|
Coordinates
|
||||||
</h2>
|
</h2>
|
||||||
<Button
|
<Button
|
||||||
onClick={getCurrentLocation}
|
onClick={getCurrentLocation}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-[#047857] border-[#047857]/20 hover:bg-[#047857]/5"
|
className="text-[#047857] border-[#047857]/20 hover:bg-[#047857]/5"
|
||||||
>
|
>
|
||||||
<Navigation className="w-3 h-3 mr-2" />
|
<Navigation className="w-3 h-3 mr-2" />
|
||||||
Get Current Location
|
Get Current Location
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Latitude</label>
|
<label className="block text-sm font-medium text-slate-700 mb-2">Latitude</label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="40.712776"
|
placeholder="40.712776"
|
||||||
value={latitude}
|
value={latitude}
|
||||||
onChange={(e) => setLatitude(e.target.value)}
|
onChange={(e) => setLatitude(e.target.value)}
|
||||||
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#10B981] focus:ring-[#10B981]"
|
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#10B981] focus:ring-[#10B981]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Longitude</label>
|
<label className="block text-sm font-medium text-slate-700 mb-2">Longitude</label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="-74.005974"
|
placeholder="-74.005974"
|
||||||
value={longitude}
|
value={longitude}
|
||||||
onChange={(e) => setLongitude(e.target.value)}
|
onChange={(e) => setLongitude(e.target.value)}
|
||||||
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#10B981] focus:ring-[#10B981]"
|
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#10B981] focus:ring-[#10B981]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-slate-600">
|
||||||
Tip: You can copy-paste coordinates directly from Google Maps.
|
Tip: You can copy-paste coordinates directly from Google Maps.
|
||||||
(Right-click a location on standard Maps, then click the coordinates to copy).
|
(Right-click a location on standard Maps, then click the coordinates to copy).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-slate-100"></div>
|
<div className="border-t border-slate-100"></div>
|
||||||
|
|
||||||
{/* Design Options */}
|
{/* Design Options */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
||||||
<Sparkles className="w-5 h-5 text-[#10B981]" />
|
<Sparkles className="w-5 h-5 text-[#10B981]" />
|
||||||
Design Options
|
Design Options
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Color Picker */}
|
{/* Color Picker */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-3">QR Code Color</label>
|
<label className="block text-sm font-medium text-slate-700 mb-3">QR Code Color</label>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{QR_COLORS.map((c) => (
|
{QR_COLORS.map((c) => (
|
||||||
<button
|
<button
|
||||||
key={c.name}
|
key={c.name}
|
||||||
onClick={() => setQrColor(c.value)}
|
onClick={() => setQrColor(c.value)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-9 h-9 rounded-full border-2 flex items-center justify-center transition-all hover:scale-110",
|
"w-9 h-9 rounded-full border-2 flex items-center justify-center transition-all hover:scale-110",
|
||||||
qrColor === c.value ? "border-slate-900 ring-2 ring-offset-2 ring-slate-200" : "border-white shadow-md"
|
qrColor === c.value ? "border-slate-900 ring-2 ring-offset-2 ring-slate-200" : "border-white shadow-md"
|
||||||
)}
|
)}
|
||||||
style={{ backgroundColor: c.value }}
|
style={{ backgroundColor: c.value }}
|
||||||
aria-label={`Select ${c.name}`}
|
aria-label={`Select ${c.name}`}
|
||||||
title={c.name}
|
title={c.name}
|
||||||
>
|
>
|
||||||
{qrColor === c.value && <Check className="w-4 h-4 text-white" strokeWidth={3} />}
|
{qrColor === c.value && <Check className="w-4 h-4 text-white" strokeWidth={3} />}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Frame Selector */}
|
{/* Frame Selector */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||||
{FRAME_OPTIONS.map((frame) => (
|
{FRAME_OPTIONS.map((frame) => (
|
||||||
<button
|
<button
|
||||||
key={frame.id}
|
key={frame.id}
|
||||||
onClick={() => setFrameType(frame.id)}
|
onClick={() => setFrameType(frame.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"py-2.5 px-3 rounded-lg text-sm font-medium transition-all border",
|
"py-2.5 px-3 rounded-lg text-sm font-medium transition-all border",
|
||||||
frameType === frame.id
|
frameType === frame.id
|
||||||
? "bg-[#10B981] text-white border-[#10B981]"
|
? "bg-[#10B981] text-white border-[#10B981]"
|
||||||
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{frame.label}
|
{frame.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* RIGHT: Preview Section */}
|
{/* RIGHT: Preview Section */}
|
||||||
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||||
|
|
||||||
{/* QR Card with Frame */}
|
{/* QR Card with Frame */}
|
||||||
<div
|
<div
|
||||||
ref={qrRef}
|
ref={qrRef}
|
||||||
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
|
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
|
||||||
style={{ minWidth: '320px' }}
|
>
|
||||||
>
|
{/* Frame Label */}
|
||||||
{/* Frame Label */}
|
{getFrameLabel() && (
|
||||||
{getFrameLabel() && (
|
<div
|
||||||
<div
|
className="mb-5 px-8 py-2.5 rounded-full text-white font-bold text-sm tracking-widest uppercase shadow-md"
|
||||||
className="mb-5 px-8 py-2.5 rounded-full text-white font-bold text-sm tracking-widest uppercase shadow-md"
|
style={{ backgroundColor: qrColor }}
|
||||||
style={{ backgroundColor: qrColor }}
|
>
|
||||||
>
|
{getFrameLabel()}
|
||||||
{getFrameLabel()}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
{/* QR Code */}
|
||||||
{/* QR Code */}
|
<div className="bg-white">
|
||||||
<div className="bg-white">
|
<QRCodeSVG
|
||||||
<QRCodeSVG
|
value={(latitude && longitude) ? qrValue : "https://maps.google.com"}
|
||||||
value={(latitude && longitude) ? qrValue : "https://maps.google.com"}
|
size={240}
|
||||||
size={240}
|
level="M"
|
||||||
level="M"
|
includeMargin={false}
|
||||||
includeMargin={false}
|
fgColor={qrColor}
|
||||||
fgColor={qrColor}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Info Preview */}
|
||||||
{/* Info Preview */}
|
<div className="mt-6 text-center max-w-[260px]">
|
||||||
<div className="mt-6 text-center max-w-[260px]">
|
<h3 className="font-bold text-slate-900 text-lg flex items-center justify-center gap-2 truncate">
|
||||||
<h3 className="font-bold text-slate-900 text-lg flex items-center justify-center gap-2 truncate">
|
<MapPin className="w-4 h-4 text-[#10B981] shrink-0" />
|
||||||
<MapPin className="w-4 h-4 text-[#10B981] shrink-0" />
|
<span className="truncate">{latitude || 'Lat'}, {longitude || 'Long'}</span>
|
||||||
<span className="truncate">{latitude || 'Lat'}, {longitude || 'Long'}</span>
|
</h3>
|
||||||
</h3>
|
<div className="text-xs text-slate-600 mt-1">Google Maps Location</div>
|
||||||
<div className="text-xs text-slate-500 mt-1">Google Maps Location</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Download Buttons */}
|
||||||
{/* Download Buttons */}
|
<div className="flex items-center gap-3 mt-8">
|
||||||
<div className="flex items-center gap-3 mt-8">
|
<Button
|
||||||
<Button
|
onClick={() => handleDownload('png')}
|
||||||
onClick={() => handleDownload('png')}
|
className="bg-[#10B981] hover:bg-[#047857] text-white shadow-lg"
|
||||||
className="bg-[#10B981] hover:bg-[#047857] text-white shadow-lg"
|
>
|
||||||
>
|
<Download className="w-4 h-4 mr-2" />
|
||||||
<Download className="w-4 h-4 mr-2" />
|
Download PNG
|
||||||
Download PNG
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
onClick={() => handleDownload('svg')}
|
||||||
onClick={() => handleDownload('svg')}
|
variant="outline"
|
||||||
variant="outline"
|
className="border-slate-300 hover:bg-white"
|
||||||
className="border-slate-300 hover:bg-white"
|
>
|
||||||
>
|
<Download className="w-4 h-4 mr-2" />
|
||||||
<Download className="w-4 h-4 mr-2" />
|
SVG
|
||||||
SVG
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
|
||||||
|
<p className="text-xs text-slate-600 mt-4 text-center">
|
||||||
<p className="text-xs text-slate-500 mt-4 text-center">
|
Scanning opens the location directly in Google Maps.
|
||||||
Scanning opens the location directly in Google Maps.
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Upsell Banner */}
|
||||||
{/* Upsell Banner */}
|
<div className="mt-8 bg-gradient-to-r from-[#10B981] to-[#047857] rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
<div className="mt-8 bg-gradient-to-r from-[#10B981] to-[#047857] rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
<div className="text-white text-center sm:text-left">
|
||||||
<div className="text-white text-center sm:text-left">
|
<h3 className="font-bold text-lg">Need a Business Map?</h3>
|
||||||
<h3 className="font-bold text-lg">Need a Business Map?</h3>
|
<p className="text-white/80 text-sm mt-1">
|
||||||
<p className="text-white/80 text-sm mt-1">
|
Create a Dynamic QR Code for your business location. If you move, just update the location without reprinting.
|
||||||
Create a Dynamic QR Code for your business location. If you move, just update the location without reprinting.
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<Link href="/signup">
|
||||||
<Link href="/signup">
|
<Button className="bg-white text-[#047857] hover:bg-slate-100 shrink-0 shadow-lg">
|
||||||
<Button className="bg-white text-[#047857] hover:bg-slate-100 shrink-0 shadow-lg">
|
Get Dynamic Maps
|
||||||
Get Dynamic Maps
|
</Button>
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,360 +1,332 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import GeolocationGenerator from './GeolocationGenerator';
|
import GeolocationGenerator from './GeolocationGenerator';
|
||||||
import { MapPin, Shield, Zap, Smartphone, Navigation, Map, Download, Share2 } from 'lucide-react';
|
import { MapPin, Shield, Zap, Smartphone, Navigation, Map, Download, Share2 } from 'lucide-react';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
||||||
|
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
||||||
// SEO Optimized Metadata
|
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Free Geolocation QR Code Generator | Maps & Directions | QR Master',
|
// SEO Optimized Metadata
|
||||||
description: 'Create a QR code for a specific location using Latitude and Longitude. Scanners will open Google Maps directly to your pin. Free & Precise.',
|
export const metadata: Metadata = {
|
||||||
keywords: ['location qr code', 'maps qr code', 'google maps qr generator', 'geolocation qr', 'coordinates qr code'],
|
title: {
|
||||||
alternates: {
|
absolute: 'Free Geolocation QR Code Generator | Standort & Map Links | QR Master',
|
||||||
canonical: 'https://qrmaster.io/tools/geolocation-qr-code',
|
},
|
||||||
},
|
description: 'Create a QR code for a specific location. Erstelle einen Map QR Code für Google Maps. Coordinates & Directions instantly. Standort teilen leicht gemacht.',
|
||||||
openGraph: {
|
keywords: ['location qr code', 'maps qr code', 'google maps qr generator', 'geolocation qr', 'coordinates qr code', 'standort qr code', 'google maps qr code erstellen', 'koordinaten qr code', 'wegbeschreibung qr code', 'maps qr code generator'],
|
||||||
title: 'Free Geolocation QR Code Generator | QR Master',
|
alternates: {
|
||||||
description: 'Navigate users to any location with a QR code. Opens directly in Google Maps.',
|
canonical: 'https://www.qrmaster.net/tools/geolocation-qr-code',
|
||||||
type: 'website',
|
},
|
||||||
url: 'https://qrmaster.io/tools/geolocation-qr-code',
|
openGraph: {
|
||||||
images: [{ url: '/og-geolocation-generator.png', width: 1200, height: 630 }],
|
title: 'Free Geolocation QR Code Generator | QR Master',
|
||||||
},
|
description: 'Navigate users to any location with a QR code. Opens directly in Google Maps.',
|
||||||
twitter: {
|
type: 'website',
|
||||||
card: 'summary_large_image',
|
url: 'https://www.qrmaster.net/tools/geolocation-qr-code',
|
||||||
title: 'Free Geolocation QR Code Generator',
|
images: [{ url: '/og-geolocation-generator.png', width: 1200, height: 630 }],
|
||||||
description: 'Create QR codes for maps and locations. Instant and free.',
|
},
|
||||||
},
|
twitter: {
|
||||||
robots: {
|
card: 'summary_large_image',
|
||||||
index: true,
|
title: 'Free Geolocation QR Code Generator',
|
||||||
follow: true,
|
description: 'Create QR codes for maps and locations. Instant and free.',
|
||||||
},
|
},
|
||||||
};
|
robots: {
|
||||||
|
index: true,
|
||||||
// JSON-LD Structured Data
|
follow: true,
|
||||||
const jsonLd = {
|
},
|
||||||
'@context': 'https://schema.org',
|
};
|
||||||
'@graph': [
|
|
||||||
{
|
// JSON-LD Structured Data
|
||||||
'@type': 'SoftwareApplication',
|
const jsonLd = {
|
||||||
name: 'Geolocation QR Code Generator',
|
'@context': 'https://schema.org',
|
||||||
applicationCategory: 'UtilitiesApplication',
|
'@graph': [
|
||||||
operatingSystem: 'Web Browser',
|
generateSoftwareAppSchema(
|
||||||
offers: {
|
'Geolocation QR Code Generator',
|
||||||
'@type': 'Offer',
|
'Generate QR codes that open specific geographic coordinates in map applications.',
|
||||||
price: '0',
|
'/og-geolocation-generator.png'
|
||||||
priceCurrency: 'USD',
|
),
|
||||||
},
|
{
|
||||||
aggregateRating: {
|
'@type': 'HowTo',
|
||||||
'@type': 'AggregateRating',
|
name: 'How to Create a Location QR Code',
|
||||||
ratingValue: '4.7',
|
description: 'Create a QR code that points to a specific map location.',
|
||||||
ratingCount: '890',
|
step: [
|
||||||
},
|
{
|
||||||
description: 'Generate QR codes that open specific geographic coordinates in map applications.',
|
'@type': 'HowToStep',
|
||||||
},
|
position: 1,
|
||||||
{
|
name: 'Get Coordinates',
|
||||||
'@type': 'HowTo',
|
text: 'Find the Latitude and Longitude of your location (e.g., from Google Maps).',
|
||||||
name: 'How to Create a Location QR Code',
|
},
|
||||||
description: 'Create a QR code that points to a specific map location.',
|
{
|
||||||
step: [
|
'@type': 'HowToStep',
|
||||||
{
|
position: 2,
|
||||||
'@type': 'HowToStep',
|
name: 'Enter Data',
|
||||||
position: 1,
|
text: 'Paste the coordinates into the generator.',
|
||||||
name: 'Get Coordinates',
|
},
|
||||||
text: 'Find the Latitude and Longitude of your location (e.g., from Google Maps).',
|
{
|
||||||
},
|
'@type': 'HowToStep',
|
||||||
{
|
position: 3,
|
||||||
'@type': 'HowToStep',
|
name: 'Customize',
|
||||||
position: 2,
|
text: 'Choose a color and style for your map QR code.',
|
||||||
name: 'Enter Data',
|
},
|
||||||
text: 'Paste the coordinates into the generator.',
|
{
|
||||||
},
|
'@type': 'HowToStep',
|
||||||
{
|
position: 4,
|
||||||
'@type': 'HowToStep',
|
name: 'Download',
|
||||||
position: 3,
|
text: 'Save your QR code as a high-quality image.',
|
||||||
name: 'Customize',
|
},
|
||||||
text: 'Choose a color and style for your map QR code.',
|
{
|
||||||
},
|
'@type': 'HowToStep',
|
||||||
{
|
position: 5,
|
||||||
'@type': 'HowToStep',
|
name: 'Share',
|
||||||
position: 4,
|
text: 'Place it on invitations, signs, or your website.',
|
||||||
name: 'Download',
|
},
|
||||||
text: 'Save your QR code as a high-quality image.',
|
],
|
||||||
},
|
totalTime: 'PT45S',
|
||||||
{
|
},
|
||||||
'@type': 'HowToStep',
|
generateFaqSchema({
|
||||||
position: 5,
|
'Which map app does it open?': {
|
||||||
name: 'Share',
|
question: 'Which map app does it open?',
|
||||||
text: 'Place it on invitations, signs, or your website.',
|
answer: 'Our generator creates a universal Google Maps link. On most devices, this will open the Google Maps app if installed, or the browser version if not. It is the most compatible method.',
|
||||||
},
|
},
|
||||||
],
|
'How do I find my Latitude and Longitude?': {
|
||||||
totalTime: 'PT45S',
|
question: 'How do I find my Latitude and Longitude?',
|
||||||
},
|
answer: 'On Google Maps desktop: Right-click any spot on the map. The first item in the menu is the coordinates. Click to copy them.',
|
||||||
{
|
},
|
||||||
'@type': 'FAQPage',
|
'Does it work offline?': {
|
||||||
mainEntity: [
|
question: 'Does it work offline?',
|
||||||
{
|
answer: 'The QR code itself can be scanned offline, but the user will likely need an internet connection to load the map and get directions.',
|
||||||
'@type': 'Question',
|
},
|
||||||
name: 'Which map app does it open?',
|
'Can I use an address instead?': {
|
||||||
acceptedAnswer: {
|
question: 'Can I use an address instead?',
|
||||||
'@type': 'Answer',
|
answer: 'For precise results, we use coordinates. However, you can use our URL Generator and paste a link to your Google Maps address search result if you prefer.',
|
||||||
text: 'Our generator creates a universal Google Maps link. On most devices, this will open the Google Maps app if installed, or the browser version if not. It is the most compatible method.',
|
},
|
||||||
},
|
'Is it free?': {
|
||||||
},
|
question: 'Is it free?',
|
||||||
{
|
answer: 'Yes, generating this location QR code is completely free and requires no signup.',
|
||||||
'@type': 'Question',
|
},
|
||||||
name: 'How do I find my Latitude and Longitude?',
|
}),
|
||||||
acceptedAnswer: {
|
],
|
||||||
'@type': 'Answer',
|
};
|
||||||
text: 'On Google Maps desktop: Right-click any spot on the map. The first item in the menu is the coordinates. Click to copy them.',
|
|
||||||
},
|
export default function GeolocationQRCodePage() {
|
||||||
},
|
return (
|
||||||
{
|
<>
|
||||||
'@type': 'Question',
|
<script
|
||||||
name: 'Does it work offline?',
|
type="application/ld+json"
|
||||||
acceptedAnswer: {
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
'@type': 'Answer',
|
/>
|
||||||
text: 'The QR code itself can be scanned offline, but the user will likely need an internet connection to load the map and get directions.',
|
<ToolBreadcrumb toolName="Location QR Code Generator" toolSlug="geolocation-qr-code" />
|
||||||
},
|
|
||||||
},
|
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
||||||
{
|
|
||||||
'@type': 'Question',
|
{/* HERO SECTION */}
|
||||||
name: 'Can I use an address instead?',
|
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden" style={{ backgroundColor: '#047857' }}>
|
||||||
acceptedAnswer: {
|
<div className="absolute inset-0 opacity-10">
|
||||||
'@type': 'Answer',
|
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||||
text: 'For precise results, we use coordinates. However, you can use our URL Generator and paste a link to your Google Maps address search result if you prefer.',
|
<path d="M0 100 C 20 0 50 0 100 100 Z" fill="url(#grad1)" />
|
||||||
},
|
<defs>
|
||||||
},
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
{
|
<stop offset="0%" style={{ stopColor: 'white', stopOpacity: 1 }} />
|
||||||
'@type': 'Question',
|
<stop offset="100%" style={{ stopColor: 'white', stopOpacity: 0 }} />
|
||||||
name: 'Is it free?',
|
</linearGradient>
|
||||||
acceptedAnswer: {
|
</defs>
|
||||||
'@type': 'Answer',
|
</svg>
|
||||||
text: 'Yes, generating this location QR code is completely free and requires no signup.',
|
</div>
|
||||||
},
|
|
||||||
},
|
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
||||||
],
|
<div className="text-center lg:text-left">
|
||||||
},
|
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
|
||||||
],
|
<span className="flex h-2 w-2 relative">
|
||||||
};
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-400"></span>
|
||||||
export default function GeolocationQRCodePage() {
|
</span>
|
||||||
return (
|
Free Tool — No Signup Required
|
||||||
<>
|
</div>
|
||||||
<script
|
|
||||||
type="application/ld+json"
|
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
Share Perfect Locations with <br className="hidden lg:block" />
|
||||||
/>
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-emerald-300 to-teal-300">Map QR Codes</span>
|
||||||
<ToolBreadcrumb toolName="Location QR Code Generator" toolSlug="geolocation-qr-code" />
|
</h1>
|
||||||
|
|
||||||
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
<p className="text-lg md:text-xl text-indigo-100 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
||||||
|
Provide exact directions to your event, store, or secret spot.
|
||||||
{/* HERO SECTION */}
|
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Opens directly in Google Maps.</strong>
|
||||||
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden" style={{ backgroundColor: '#047857' }}>
|
</p>
|
||||||
<div className="absolute inset-0 opacity-10">
|
|
||||||
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
|
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
||||||
<path d="M0 100 C 20 0 50 0 100 100 Z" fill="url(#grad1)" />
|
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
||||||
<defs>
|
<Navigation className="w-4 h-4 text-emerald-400" />
|
||||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
|
Exact Directions
|
||||||
<stop offset="0%" style={{ stopColor: 'white', stopOpacity: 1 }} />
|
</div>
|
||||||
<stop offset="100%" style={{ stopColor: 'white', stopOpacity: 0 }} />
|
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
||||||
</linearGradient>
|
<Zap className="w-4 h-4 text-amber-400" />
|
||||||
</defs>
|
Instant Load
|
||||||
</svg>
|
</div>
|
||||||
</div>
|
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
||||||
|
<Shield className="w-4 h-4 text-purple-400" />
|
||||||
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
No Data Saved
|
||||||
<div className="text-center lg:text-left">
|
</div>
|
||||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
|
</div>
|
||||||
<span className="flex h-2 w-2 relative">
|
</div>
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-400"></span>
|
{/* Visual Abstract */}
|
||||||
</span>
|
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
|
||||||
Free Tool — No Signup Required
|
<div className="absolute w-[500px] h-[500px] bg-indigo-500/30 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
|
||||||
</div>
|
|
||||||
|
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform rotate-3 hover:rotate-0 transition-all duration-700 group">
|
||||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
|
||||||
Share Perfect Locations with <br className="hidden lg:block" />
|
|
||||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-emerald-300 to-teal-300">Map QR Codes</span>
|
<div className="w-full bg-white rounded-xl shadow-lg h-32 mb-6 relative overflow-hidden grayscale group-hover:grayscale-0 transition-all duration-500">
|
||||||
</h1>
|
<div className="absolute inset-0 opacity-20 bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:16px_16px]"></div>
|
||||||
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||||
<p className="text-lg md:text-xl text-indigo-100 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
<MapPin className="w-8 h-8 text-red-500 drop-shadow-lg animate-bounce" />
|
||||||
Provide exact directions to your event, store, or secret spot.
|
</div>
|
||||||
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Opens directly in Google Maps.</strong>
|
<div className="absolute bottom-2 left-2 right-2 bg-white/90 p-2 rounded text-[10px] text-slate-600 font-mono text-center">
|
||||||
</p>
|
40.7128° N, 74.0060° W
|
||||||
|
</div>
|
||||||
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
</div>
|
||||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
|
||||||
<Navigation className="w-4 h-4 text-emerald-400" />
|
<div className="w-44 h-44 bg-white rounded-xl p-2 shadow-inner relative overflow-hidden flex items-center justify-center">
|
||||||
Exact Directions
|
<QRCodeSVG value="https://www.qrmaster.net" size={160} fgColor="#0f172a" level="Q" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
|
||||||
<Zap className="w-4 h-4 text-amber-400" />
|
{/* Floating Badge */}
|
||||||
Instant Load
|
<div className="absolute -bottom-6 -right-6 bg-white py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
|
||||||
</div>
|
<div className="bg-emerald-100 p-2 rounded-full">
|
||||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
<Map className="w-5 h-5 text-emerald-600" />
|
||||||
<Shield className="w-4 h-4 text-purple-400" />
|
</div>
|
||||||
No Data Saved
|
<div className="text-left">
|
||||||
</div>
|
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Map</div>
|
||||||
</div>
|
<div className="text-sm font-bold text-slate-900">Open</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Visual Abstract */}
|
</div>
|
||||||
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
|
</div>
|
||||||
<div className="absolute w-[500px] h-[500px] bg-indigo-500/30 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
|
</div>
|
||||||
|
</section>
|
||||||
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform rotate-3 hover:rotate-0 transition-all duration-700 group">
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
|
{/* GENERATOR SECTION */}
|
||||||
|
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
|
||||||
<div className="w-full bg-white rounded-xl shadow-lg h-32 mb-6 relative overflow-hidden grayscale group-hover:grayscale-0 transition-all duration-500">
|
<GeolocationGenerator />
|
||||||
<div className="absolute inset-0 opacity-20 bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:16px_16px]"></div>
|
</section>
|
||||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
|
||||||
<MapPin className="w-8 h-8 text-red-500 drop-shadow-lg animate-bounce" />
|
{/* HOW IT WORKS */}
|
||||||
</div>
|
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
|
||||||
<div className="absolute bottom-2 left-2 right-2 bg-white/90 p-2 rounded text-[10px] text-slate-500 font-mono text-center">
|
<div className="max-w-4xl mx-auto">
|
||||||
40.7128° N, 74.0060° W
|
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
|
||||||
</div>
|
How Geolocation QR Codes Work
|
||||||
</div>
|
</h2>
|
||||||
|
|
||||||
<div className="w-44 h-44 bg-white rounded-xl p-2 shadow-inner relative overflow-hidden flex items-center justify-center">
|
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
|
||||||
<QRCodeSVG value="https://www.qrmaster.net" size={160} fgColor="#0f172a" level="Q" />
|
<article className="text-center">
|
||||||
</div>
|
<div className="w-14 h-14 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
|
<MapPin className="w-7 h-7 text-[#1A1265]" />
|
||||||
{/* Floating Badge */}
|
</div>
|
||||||
<div className="absolute -bottom-6 -right-6 bg-white py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
|
<h3 className="font-bold text-slate-900 mb-2">1. Pinpoint</h3>
|
||||||
<div className="bg-emerald-100 p-2 rounded-full">
|
<p className="text-slate-600 text-sm">
|
||||||
<Map className="w-5 h-5 text-emerald-600" />
|
Enter exact GPS coordinates. This ensures users go to the precise spot (e.g., a specific building entrance).
|
||||||
</div>
|
</p>
|
||||||
<div className="text-left">
|
</article>
|
||||||
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Map</div>
|
|
||||||
<div className="text-sm font-bold text-slate-900">Open</div>
|
<article className="text-center">
|
||||||
</div>
|
<div className="w-14 h-14 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
</div>
|
<Smartphone className="w-7 h-7 text-[#1A1265]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<h3 className="font-bold text-slate-900 mb-2">2. Scan</h3>
|
||||||
</div>
|
<p className="text-slate-600 text-sm">
|
||||||
</section>
|
Users scan the code. It is encoded with a universal map link.
|
||||||
|
</p>
|
||||||
{/* GENERATOR SECTION */}
|
</article>
|
||||||
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
|
|
||||||
<GeolocationGenerator />
|
<article className="text-center">
|
||||||
</section>
|
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Download className="w-6 h-6 text-[#1A1265]" />
|
||||||
{/* HOW IT WORKS */}
|
</div>
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
|
<h3 className="font-bold text-slate-900 mb-2">3. Download</h3>
|
||||||
<div className="max-w-4xl mx-auto">
|
<p className="text-slate-600 text-xs leading-relaxed">
|
||||||
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
|
Save your high-quality QR image.
|
||||||
How Geolocation QR Codes Work
|
</p>
|
||||||
</h2>
|
</article>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
|
<article className="text-center">
|
||||||
<article className="text-center">
|
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
<div className="w-14 h-14 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
<Smartphone className="w-6 h-6 text-[#1A1265]" />
|
||||||
<MapPin className="w-7 h-7 text-[#1A1265]" />
|
</div>
|
||||||
</div>
|
<h3 className="font-bold text-slate-900 mb-2">4. Scan</h3>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">1. Pinpoint</h3>
|
<p className="text-slate-600 text-xs leading-relaxed">
|
||||||
<p className="text-slate-600 text-sm">
|
Users scan the code to load coordinates.
|
||||||
Enter exact GPS coordinates. This ensures users go to the precise spot (e.g., a specific building entrance).
|
</p>
|
||||||
</p>
|
</article>
|
||||||
</article>
|
|
||||||
|
<article className="text-center">
|
||||||
<article className="text-center">
|
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
<div className="w-14 h-14 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
<Share2 className="w-6 h-6 text-[#1A1265]" />
|
||||||
<Smartphone className="w-7 h-7 text-[#1A1265]" />
|
</div>
|
||||||
</div>
|
<h3 className="font-bold text-slate-900 mb-2">5. Go</h3>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">2. Scan</h3>
|
<p className="text-slate-600 text-xs leading-relaxed">
|
||||||
<p className="text-slate-600 text-sm">
|
They get instant directions to your spot.
|
||||||
Users scan the code. It is encoded with a universal map link.
|
</p>
|
||||||
</p>
|
</article>
|
||||||
</article>
|
</div>
|
||||||
|
</div>
|
||||||
<article className="text-center">
|
</section>
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
|
||||||
<Download className="w-6 h-6 text-[#1A1265]" />
|
{/* RELATED TOOLS */}
|
||||||
</div>
|
<RelatedTools />
|
||||||
<h3 className="font-bold text-slate-900 mb-2">3. Download</h3>
|
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">
|
{/* FAQ SECTION */}
|
||||||
Save your high-quality QR image.
|
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
||||||
</p>
|
<div className="max-w-3xl mx-auto">
|
||||||
</article>
|
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
||||||
|
Frequently Asked Questions
|
||||||
<article className="text-center">
|
</h2>
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
<p className="text-slate-600 text-center mb-10">
|
||||||
<Smartphone className="w-6 h-6 text-[#1A1265]" />
|
Common questions about Map QR codes.
|
||||||
</div>
|
</p>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">4. Scan</h3>
|
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">
|
<div className="space-y-4">
|
||||||
Users scan the code to load coordinates.
|
<FaqItem
|
||||||
</p>
|
question="Why not just use an address?"
|
||||||
</article>
|
answer="Addresses can be ambiguous or cover large areas (like a park or stadium). Coordinates point to an exact geographic spot, ensuring visitors find the specific meeting point or parking entrance."
|
||||||
|
/>
|
||||||
<article className="text-center">
|
<FaqItem
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#1A1265]/10 flex items-center justify-center mx-auto mb-4">
|
question="Does it work on Apple Maps?"
|
||||||
<Share2 className="w-6 h-6 text-[#1A1265]" />
|
answer="Yes. While the underlying link is a Google Maps link, iOS devices usually handle these gracefully, either opening them in the Google Maps app (if installed) or the browser, where Apple Maps can often intercept directions."
|
||||||
</div>
|
/>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">5. Go</h3>
|
<FaqItem
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">
|
question="Is it free?"
|
||||||
They get instant directions to your spot.
|
answer="Yes, generating this location QR code is completely free and requires no signup."
|
||||||
</p>
|
/>
|
||||||
</article>
|
<FaqItem
|
||||||
</div>
|
question="Can I track who scanned it?"
|
||||||
</div>
|
answer="Not with this static tool. If you need scan analytics (e.g., how many people scanned your storefront QR), you should use our Dynamic QR Code service."
|
||||||
</section>
|
/>
|
||||||
|
<FaqItem
|
||||||
{/* FAQ SECTION */}
|
question="Is it free?"
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
answer="Yes, generating this location QR code is completely free and requires no signup."
|
||||||
<div className="max-w-3xl mx-auto">
|
/>
|
||||||
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
</div>
|
||||||
Frequently Asked Questions
|
</div>
|
||||||
</h2>
|
</section>
|
||||||
<p className="text-slate-600 text-center mb-10">
|
|
||||||
Common questions about Map QR codes.
|
</div>
|
||||||
</p>
|
</>
|
||||||
|
);
|
||||||
<div className="space-y-4">
|
}
|
||||||
<FaqItem
|
|
||||||
question="Why not just use an address?"
|
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
||||||
answer="Addresses can be ambiguous or cover large areas (like a park or stadium). Coordinates point to an exact geographic spot, ensuring visitors find the specific meeting point or parking entrance."
|
return (
|
||||||
/>
|
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
<FaqItem
|
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
|
||||||
question="Does it work on Apple Maps?"
|
{question}
|
||||||
answer="Yes. While the underlying link is a Google Maps link, iOS devices usually handle these gracefully, either opening them in the Google Maps app (if installed) or the browser, where Apple Maps can often intercept directions."
|
<span className="transition group-open:rotate-180 text-slate-400">
|
||||||
/>
|
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
|
||||||
<FaqItem
|
<path d="M6 9l6 6 6-6" />
|
||||||
question="Is it free?"
|
</svg>
|
||||||
answer="Yes, generating this location QR code is completely free and requires no signup."
|
</span>
|
||||||
/>
|
</summary>
|
||||||
<FaqItem
|
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
|
||||||
question="Can I track who scanned it?"
|
{answer}
|
||||||
answer="Not with this static tool. If you need scan analytics (e.g., how many people scanned your storefront QR), you should use our Dynamic QR Code service."
|
</div>
|
||||||
/>
|
</details>
|
||||||
<FaqItem
|
);
|
||||||
question="Is it free?"
|
}
|
||||||
answer="Yes, generating this location QR code is completely free and requires no signup."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
|
||||||
return (
|
|
||||||
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
|
||||||
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
|
|
||||||
{question}
|
|
||||||
<span className="transition group-open:rotate-180 text-slate-400">
|
|
||||||
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
|
|
||||||
<path d="M6 9l6 6 6-6" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</summary>
|
|
||||||
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
|
|
||||||
{answer}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,253 +1,252 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import {
|
import {
|
||||||
Instagram,
|
Instagram,
|
||||||
Download,
|
Download,
|
||||||
Check,
|
Check,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Camera
|
Camera
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
// Brand Colors
|
// Brand Colors
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
paleGrey: '#EBEBDF',
|
paleGrey: '#EBEBDF',
|
||||||
richBlue: '#1A1265',
|
richBlue: '#1A1265',
|
||||||
richBlueLight: '#2A2275',
|
richBlueLight: '#2A2275',
|
||||||
};
|
};
|
||||||
|
|
||||||
// QR Color Options - Insta Theme
|
// QR Color Options - Insta Theme
|
||||||
const QR_COLORS = [
|
const QR_COLORS = [
|
||||||
{ name: 'Insta Pink', value: '#E1306C' },
|
{ name: 'Insta Pink', value: '#E1306C' },
|
||||||
{ name: 'Insta Purple', value: '#833AB4' },
|
{ name: 'Insta Purple', value: '#833AB4' },
|
||||||
{ name: 'Insta Orange', value: '#F77737' },
|
{ name: 'Insta Orange', value: '#F77737' },
|
||||||
{ name: 'Classic Black', value: '#000000' },
|
{ name: 'Classic Black', value: '#000000' },
|
||||||
{ name: 'Rich Blue', value: '#1A1265' },
|
{ name: 'Rich Blue', value: '#1A1265' },
|
||||||
{ name: 'Teal', value: '#0D9488' },
|
{ name: 'Teal', value: '#0D9488' },
|
||||||
{ name: 'Emerald', value: '#10B981' },
|
{ name: 'Emerald', value: '#10B981' },
|
||||||
{ name: 'Rose', value: '#F43F5E' },
|
{ name: 'Rose', value: '#F43F5E' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Frame Options
|
// Frame Options
|
||||||
const FRAME_OPTIONS = [
|
const FRAME_OPTIONS = [
|
||||||
{ id: 'none', label: 'No Frame' },
|
{ id: 'none', label: 'No Frame' },
|
||||||
{ id: 'scanme', label: 'Scan Me' },
|
{ id: 'scanme', label: 'Scan Me' },
|
||||||
{ id: 'follow', label: 'Follow Us' },
|
{ id: 'follow', label: 'Follow Us' },
|
||||||
{ id: 'insta', label: 'Instagram' },
|
{ id: 'insta', label: 'Instagram' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function InstagramGenerator() {
|
export default function InstagramGenerator() {
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [qrColor, setQrColor] = useState('#E1306C');
|
const [qrColor, setQrColor] = useState('#E1306C');
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
|
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Instagram URL construction: https://instagram.com/username
|
// Instagram URL construction: https://instagram.com/username
|
||||||
const getUrl = () => {
|
const getUrl = () => {
|
||||||
const cleanUser = username.replace(/^@/, '').replace(/https?:\/\/(www\.)?instagram\.com\//, '').replace(/\/$/, '');
|
const cleanUser = username.replace(/^@/, '').replace(/https?:\/\/(www\.)?instagram\.com\//, '').replace(/\/$/, '');
|
||||||
return cleanUser ? `https://instagram.com/${cleanUser}` : 'https://instagram.com';
|
return cleanUser ? `https://instagram.com/${cleanUser}` : 'https://instagram.com';
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = async (format: 'png' | 'svg') => {
|
const handleDownload = async (format: 'png' | 'svg') => {
|
||||||
if (!qrRef.current) return;
|
if (!qrRef.current) return;
|
||||||
try {
|
try {
|
||||||
if (format === 'png') {
|
if (format === 'png') {
|
||||||
const { toPng } = await import('html-to-image');
|
const { toPng } = await import('html-to-image');
|
||||||
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3 });
|
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3 });
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.download = `instagram-qr-code.png`;
|
link.download = `instagram-qr-code.png`;
|
||||||
link.href = dataUrl;
|
link.href = dataUrl;
|
||||||
link.click();
|
link.click();
|
||||||
} else {
|
} else {
|
||||||
const svgData = qrRef.current.querySelector('svg')?.outerHTML;
|
const svgData = qrRef.current.querySelector('svg')?.outerHTML;
|
||||||
if (svgData) {
|
if (svgData) {
|
||||||
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = `instagram-qr-code.svg`;
|
link.download = `instagram-qr-code.svg`;
|
||||||
link.click();
|
link.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
const frame = FRAME_OPTIONS.find(f => f.id === frameType);
|
const frame = FRAME_OPTIONS.find(f => f.id === frameType);
|
||||||
return frame?.id !== 'none' ? frame?.label : null;
|
return frame?.id !== 'none' ? frame?.label : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
|
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
|
||||||
<div className="grid lg:grid-cols-2">
|
<div className="grid lg:grid-cols-2">
|
||||||
|
|
||||||
{/* LEFT: Input Section */}
|
{/* LEFT: Input Section */}
|
||||||
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||||
|
|
||||||
{/* Instagram Details */}
|
{/* Instagram Details */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
||||||
<Instagram className="w-5 h-5 text-[#E1306C]" />
|
<Instagram className="w-5 h-5 text-[#E1306C]" />
|
||||||
Instagram Username
|
Instagram Username
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Username or Link</label>
|
<label className="block text-sm font-medium text-slate-700 mb-2">Username or Link</label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="@username"
|
placeholder="@username"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#E1306C] focus:ring-[#E1306C]"
|
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#E1306C] focus:ring-[#E1306C]"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-slate-500 mt-2">Enter your username (without @) or paste full profile link.</p>
|
<p className="text-xs text-slate-600 mt-2">Enter your username (without @) or paste full profile link.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-slate-100"></div>
|
<div className="border-t border-slate-100"></div>
|
||||||
|
|
||||||
{/* Design Options */}
|
{/* Design Options */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
||||||
<Sparkles className="w-5 h-5 text-[#E1306C]" />
|
<Sparkles className="w-5 h-5 text-[#E1306C]" />
|
||||||
Design Options
|
Design Options
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Color Picker */}
|
{/* Color Picker */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-3">QR Code Color</label>
|
<label className="block text-sm font-medium text-slate-700 mb-3">QR Code Color</label>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{QR_COLORS.map((c) => (
|
{QR_COLORS.map((c) => (
|
||||||
<button
|
<button
|
||||||
key={c.name}
|
key={c.name}
|
||||||
onClick={() => setQrColor(c.value)}
|
onClick={() => setQrColor(c.value)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-9 h-9 rounded-full border-2 flex items-center justify-center transition-all hover:scale-110",
|
"w-9 h-9 rounded-full border-2 flex items-center justify-center transition-all hover:scale-110",
|
||||||
qrColor === c.value ? "border-slate-900 ring-2 ring-offset-2 ring-slate-200" : "border-white shadow-md"
|
qrColor === c.value ? "border-slate-900 ring-2 ring-offset-2 ring-slate-200" : "border-white shadow-md"
|
||||||
)}
|
)}
|
||||||
style={{ backgroundColor: c.value }}
|
style={{ backgroundColor: c.value }}
|
||||||
aria-label={`Select ${c.name}`}
|
aria-label={`Select ${c.name}`}
|
||||||
title={c.name}
|
title={c.name}
|
||||||
>
|
>
|
||||||
{qrColor === c.value && <Check className="w-4 h-4 text-white" strokeWidth={3} />}
|
{qrColor === c.value && <Check className="w-4 h-4 text-white" strokeWidth={3} />}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Frame Selector */}
|
{/* Frame Selector */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||||
{FRAME_OPTIONS.map((frame) => (
|
{FRAME_OPTIONS.map((frame) => (
|
||||||
<button
|
<button
|
||||||
key={frame.id}
|
key={frame.id}
|
||||||
onClick={() => setFrameType(frame.id)}
|
onClick={() => setFrameType(frame.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"py-2.5 px-3 rounded-lg text-sm font-medium transition-all border",
|
"py-2.5 px-3 rounded-lg text-sm font-medium transition-all border",
|
||||||
frameType === frame.id
|
frameType === frame.id
|
||||||
? "bg-[#E1306C] text-white border-[#E1306C]"
|
? "bg-[#E1306C] text-white border-[#E1306C]"
|
||||||
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{frame.label}
|
{frame.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* RIGHT: Preview Section */}
|
{/* RIGHT: Preview Section */}
|
||||||
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||||
|
|
||||||
{/* QR Card with Frame */}
|
{/* QR Card with Frame */}
|
||||||
<div
|
<div
|
||||||
ref={qrRef}
|
ref={qrRef}
|
||||||
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
|
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
|
||||||
style={{ minWidth: '320px' }}
|
>
|
||||||
>
|
{/* Frame Label */}
|
||||||
{/* Frame Label */}
|
{getFrameLabel() && (
|
||||||
{getFrameLabel() && (
|
<div
|
||||||
<div
|
className="mb-5 px-8 py-2.5 rounded-full text-white font-bold text-sm tracking-widest uppercase shadow-md"
|
||||||
className="mb-5 px-8 py-2.5 rounded-full text-white font-bold text-sm tracking-widest uppercase shadow-md"
|
style={{ backgroundColor: qrColor }}
|
||||||
style={{ backgroundColor: qrColor }}
|
>
|
||||||
>
|
{getFrameLabel()}
|
||||||
{getFrameLabel()}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
{/* QR Code */}
|
||||||
{/* QR Code */}
|
<div className="bg-white">
|
||||||
<div className="bg-white">
|
<QRCodeSVG
|
||||||
<QRCodeSVG
|
value={getUrl()}
|
||||||
value={getUrl()}
|
size={240}
|
||||||
size={240}
|
level="M"
|
||||||
level="M"
|
includeMargin={false}
|
||||||
includeMargin={false}
|
fgColor={qrColor}
|
||||||
fgColor={qrColor}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Info Preview */}
|
||||||
{/* Info Preview */}
|
<div className="mt-6 text-center max-w-[260px]">
|
||||||
<div className="mt-6 text-center max-w-[260px]">
|
<h3 className="font-bold text-slate-900 text-lg flex items-center justify-center gap-2 truncate">
|
||||||
<h3 className="font-bold text-slate-900 text-lg flex items-center justify-center gap-2 truncate">
|
<Instagram className="w-4 h-4 text-slate-400 shrink-0" />
|
||||||
<Instagram className="w-4 h-4 text-slate-400 shrink-0" />
|
<span className="truncate">{username || '@username'}</span>
|
||||||
<span className="truncate">{username || '@username'}</span>
|
</h3>
|
||||||
</h3>
|
<div className="text-xs text-slate-600 mt-1">Opens in Instagram</div>
|
||||||
<div className="text-xs text-slate-500 mt-1">Opens in Instagram</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Download Buttons */}
|
||||||
{/* Download Buttons */}
|
<div className="flex items-center gap-3 mt-8">
|
||||||
<div className="flex items-center gap-3 mt-8">
|
<Button
|
||||||
<Button
|
onClick={() => handleDownload('png')}
|
||||||
onClick={() => handleDownload('png')}
|
className="bg-[#E1306C] hover:bg-[#C13584] text-white shadow-lg"
|
||||||
className="bg-[#E1306C] hover:bg-[#C13584] text-white shadow-lg"
|
>
|
||||||
>
|
<Download className="w-4 h-4 mr-2" />
|
||||||
<Download className="w-4 h-4 mr-2" />
|
Download PNG
|
||||||
Download PNG
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
onClick={() => handleDownload('svg')}
|
||||||
onClick={() => handleDownload('svg')}
|
variant="outline"
|
||||||
variant="outline"
|
className="border-slate-300 hover:bg-white"
|
||||||
className="border-slate-300 hover:bg-white"
|
>
|
||||||
>
|
<Download className="w-4 h-4 mr-2" />
|
||||||
<Download className="w-4 h-4 mr-2" />
|
SVG
|
||||||
SVG
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
|
||||||
|
<p className="text-xs text-slate-600 mt-4 text-center">
|
||||||
<p className="text-xs text-slate-500 mt-4 text-center">
|
Scanning redirects directly to your Instagram profile.
|
||||||
Scanning redirects directly to your Instagram profile.
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Upsell Banner */}
|
||||||
{/* Upsell Banner */}
|
<div className="mt-8 bg-gradient-to-r from-[#833AB4] via-[#FD1D1D] to-[#FCA145] rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
<div className="mt-8 bg-gradient-to-r from-[#833AB4] via-[#FD1D1D] to-[#FCA145] rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
<div className="text-white text-center sm:text-left">
|
||||||
<div className="text-white text-center sm:text-left">
|
<h3 className="font-bold text-lg">Want a "Link in Bio" QR?</h3>
|
||||||
<h3 className="font-bold text-lg">Want a "Link in Bio" QR?</h3>
|
<p className="text-white/80 text-sm mt-1">
|
||||||
<p className="text-white/80 text-sm mt-1">
|
Create a digital landing page with links to all your socials using Dynamic Codes.
|
||||||
Create a digital landing page with links to all your socials using Dynamic Codes.
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<Link href="/signup">
|
||||||
<Link href="/signup">
|
<Button className="bg-white text-[#E1306C] hover:bg-slate-100 shrink-0 shadow-lg">
|
||||||
<Button className="bg-white text-[#E1306C] hover:bg-slate-100 shrink-0 shadow-lg">
|
Create Bio Link
|
||||||
Create Bio Link
|
</Button>
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,354 +1,325 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import InstagramGenerator from './InstagramGenerator';
|
import InstagramGenerator from './InstagramGenerator';
|
||||||
import { Instagram, Shield, Zap, Smartphone, Camera, Heart, Download, Share2 } from 'lucide-react';
|
import { Instagram, Shield, Zap, Smartphone, Camera, Heart, Download, Share2 } from 'lucide-react';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
||||||
|
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
||||||
// SEO Optimized Metadata
|
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Free Instagram QR Code Generator | Get More Followers | QR Master',
|
// SEO Optimized Metadata
|
||||||
description: 'Create a QR code for your Instagram profile or post. Scanners are redirected to the Instagram app instantly to follow you. Free & Customizable.',
|
export const metadata: Metadata = {
|
||||||
keywords: ['instagram qr code', 'insta qr generator', 'ig nametag generator', 'instagram follow qr', 'social media qr code'],
|
title: {
|
||||||
alternates: {
|
absolute: 'Free Instagram QR Code Generator | Get More Followers | QR Master',
|
||||||
canonical: 'https://qrmaster.io/tools/instagram-qr-code',
|
},
|
||||||
},
|
description: 'Create a QR code for your Instagram profile. Erstelle einen Insta QR Code. Scanners follow you instantly. Free & Customizable.',
|
||||||
openGraph: {
|
keywords: ['instagram qr code', 'insta qr generator', 'ig nametag generator', 'instagram follow qr', 'social media qr code', 'instagram qr code erstellen', 'instagram profil qr code', 'insta qr code', 'mehr follower qr code', 'instagram nametag generator'],
|
||||||
title: 'Free Instagram QR Code Generator | QR Master',
|
alternates: {
|
||||||
description: 'Generate QR codes to grow your Instagram following. Instant app redirect.',
|
canonical: 'https://www.qrmaster.net/tools/instagram-qr-code',
|
||||||
type: 'website',
|
},
|
||||||
url: 'https://qrmaster.io/tools/instagram-qr-code',
|
openGraph: {
|
||||||
images: [{ url: '/og-instagram-generator.png', width: 1200, height: 630 }],
|
title: 'Free Instagram QR Code Generator | QR Master',
|
||||||
},
|
description: 'Generate QR codes to grow your Instagram following. Instant app redirect.',
|
||||||
twitter: {
|
type: 'website',
|
||||||
card: 'summary_large_image',
|
url: 'https://www.qrmaster.net/tools/instagram-qr-code',
|
||||||
title: 'Free Instagram QR Code Generator',
|
images: [{ url: '/og-instagram-generator.png', width: 1200, height: 630 }],
|
||||||
description: 'Create QR codes for Instagram. Boost your followers.',
|
},
|
||||||
},
|
twitter: {
|
||||||
robots: {
|
card: 'summary_large_image',
|
||||||
index: true,
|
title: 'Free Instagram QR Code Generator',
|
||||||
follow: true,
|
description: 'Create QR codes for Instagram. Boost your followers.',
|
||||||
},
|
},
|
||||||
};
|
robots: {
|
||||||
|
index: true,
|
||||||
// JSON-LD Structured Data
|
follow: true,
|
||||||
const jsonLd = {
|
},
|
||||||
'@context': 'https://schema.org',
|
};
|
||||||
'@graph': [
|
|
||||||
{
|
// JSON-LD Structured Data
|
||||||
'@type': 'SoftwareApplication',
|
const jsonLd = {
|
||||||
name: 'Instagram QR Code Generator',
|
'@context': 'https://schema.org',
|
||||||
applicationCategory: 'UtilitiesApplication',
|
'@graph': [
|
||||||
operatingSystem: 'Web Browser',
|
generateSoftwareAppSchema(
|
||||||
offers: {
|
'Instagram QR Code Generator',
|
||||||
'@type': 'Offer',
|
'Generate QR codes that direct users to an Instagram profile or post.',
|
||||||
price: '0',
|
'/og-instagram-generator.png'
|
||||||
priceCurrency: 'USD',
|
),
|
||||||
},
|
{
|
||||||
aggregateRating: {
|
'@type': 'HowTo',
|
||||||
'@type': 'AggregateRating',
|
name: 'How to Create an Instagram QR Code',
|
||||||
ratingValue: '4.9',
|
description: 'Create a QR code that opens an Instagram profile.',
|
||||||
ratingCount: '2150',
|
step: [
|
||||||
},
|
{
|
||||||
description: 'Generate QR codes that direct users to an Instagram profile or post.',
|
'@type': 'HowToStep',
|
||||||
},
|
position: 1,
|
||||||
{
|
name: 'Enter Username',
|
||||||
'@type': 'HowTo',
|
text: 'Type your Instagram handle (e.g. @yourbrand) or paste your profile link.',
|
||||||
name: 'How to Create an Instagram QR Code',
|
},
|
||||||
description: 'Create a QR code that opens an Instagram profile.',
|
{
|
||||||
step: [
|
'@type': 'HowToStep',
|
||||||
{
|
position: 2,
|
||||||
'@type': 'HowToStep',
|
name: 'Customize',
|
||||||
position: 1,
|
text: 'Choose a gradient color that matches the Instagram vibe or your own brand.',
|
||||||
name: 'Enter Username',
|
},
|
||||||
text: 'Type your Instagram handle (e.g. @yourbrand) or paste your profile link.',
|
{
|
||||||
},
|
'@type': 'HowToStep',
|
||||||
{
|
position: 3,
|
||||||
'@type': 'HowToStep',
|
name: 'Download',
|
||||||
position: 2,
|
text: 'Save the QR code image.',
|
||||||
name: 'Customize',
|
},
|
||||||
text: 'Choose a gradient color that matches the Instagram vibe or your own brand.',
|
{
|
||||||
},
|
'@type': 'HowToStep',
|
||||||
{
|
position: 4,
|
||||||
'@type': 'HowToStep',
|
name: 'Test',
|
||||||
position: 3,
|
text: 'Scan the code to ensure it opens the correct profile.',
|
||||||
name: 'Download',
|
},
|
||||||
text: 'Save the QR code image.',
|
{
|
||||||
},
|
'@type': 'HowToStep',
|
||||||
{
|
position: 5,
|
||||||
'@type': 'HowToStep',
|
name: 'Share',
|
||||||
position: 4,
|
text: 'Put it on your packaging, business cards, or storefront.',
|
||||||
name: 'Test',
|
},
|
||||||
text: 'Scan the code to ensure it opens the correct profile.',
|
],
|
||||||
},
|
totalTime: 'PT30S',
|
||||||
{
|
},
|
||||||
'@type': 'HowToStep',
|
generateFaqSchema({
|
||||||
position: 5,
|
'Is this an Instagram Nametag?': {
|
||||||
name: 'Share',
|
question: 'Is this an Instagram Nametag?',
|
||||||
text: 'Put it on your packaging, business cards, or storefront.',
|
answer: 'It works similarly! While Instagram has its own internal "Nametag" or "QR Code" feature, our generator allows you to create a standard QR code that is more customizable and can be tracked with our Dynamic plans.',
|
||||||
},
|
},
|
||||||
],
|
'Does it open the Instagram app?': {
|
||||||
totalTime: 'PT30S',
|
question: 'Does it open the Instagram app?',
|
||||||
},
|
answer: 'Yes. When scanned on a mobile device with Instagram installed, it will deep-link directly to the profile in the app.',
|
||||||
{
|
},
|
||||||
'@type': 'FAQPage',
|
'Can I link to a specific photo or reel?': {
|
||||||
mainEntity: [
|
question: 'Can I link to a specific photo or reel?',
|
||||||
{
|
answer: 'Yes! Instead of your username, just paste the full link to the specific post or reel.',
|
||||||
'@type': 'Question',
|
},
|
||||||
name: 'Is this an Instagram Nametag?',
|
'Is it free?': {
|
||||||
acceptedAnswer: {
|
question: 'Is it free?',
|
||||||
'@type': 'Answer',
|
answer: 'Yes, generating this QR code is 100% free.',
|
||||||
text: 'It works similarly! While Instagram has its own internal "Nametag" or "QR Code" feature, our generator allows you to create a standard QR code that is more customizable and can be tracked with our Dynamic plans.',
|
},
|
||||||
},
|
'Can I track scans?': {
|
||||||
},
|
question: 'Can I track scans?',
|
||||||
{
|
answer: 'Not with this static tool. If you need scan analytics, consider using our Dynamic QR Code solution.',
|
||||||
'@type': 'Question',
|
},
|
||||||
name: 'Does it open the Instagram app?',
|
}),
|
||||||
acceptedAnswer: {
|
],
|
||||||
'@type': 'Answer',
|
};
|
||||||
text: 'Yes. When scanned on a mobile device with Instagram installed, it will deep-link directly to the profile in the app.',
|
|
||||||
},
|
export default function InstagramQRCodePage() {
|
||||||
},
|
return (
|
||||||
{
|
<>
|
||||||
'@type': 'Question',
|
<script
|
||||||
name: 'Can I link to a specific photo or reel?',
|
type="application/ld+json"
|
||||||
acceptedAnswer: {
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
'@type': 'Answer',
|
/>
|
||||||
text: 'Yes! Instead of your username, just paste the full link to the specific post or reel.',
|
<ToolBreadcrumb toolName="Instagram QR Code Generator" toolSlug="instagram-qr-code" />
|
||||||
},
|
|
||||||
},
|
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
||||||
{
|
|
||||||
'@type': 'Question',
|
{/* HERO SECTION */}
|
||||||
name: 'Is it free?',
|
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden bg-gradient-to-br from-[#833AB4] via-[#FD1D1D] to-[#FCA145]">
|
||||||
acceptedAnswer: {
|
<div className="absolute inset-0 opacity-10">
|
||||||
'@type': 'Answer',
|
<svg className="w-full h-full" width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||||
text: 'Yes, generating this QR code is 100% free.',
|
<circle cx="0" cy="0" r="40" fill="white" fillOpacity="0.1" />
|
||||||
},
|
<circle cx="100" cy="100" r="50" fill="white" fillOpacity="0.1" />
|
||||||
},
|
</svg>
|
||||||
{
|
</div>
|
||||||
'@type': 'Question',
|
|
||||||
name: 'Can I track scans?',
|
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
||||||
acceptedAnswer: {
|
<div className="text-center lg:text-left">
|
||||||
'@type': 'Answer',
|
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
|
||||||
text: 'Not with this static tool. If you need scan analytics, consider using our Dynamic QR Code solution.',
|
<span className="flex h-2 w-2 relative">
|
||||||
},
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-pink-300 opacity-75"></span>
|
||||||
},
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-pink-300"></span>
|
||||||
],
|
</span>
|
||||||
},
|
Free Tool — No Signup Required
|
||||||
],
|
</div>
|
||||||
};
|
|
||||||
|
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
||||||
export default function InstagramQRCodePage() {
|
Boost Your Following with <br className="hidden lg:block" />
|
||||||
return (
|
<span className="text-white drop-shadow-md">Instagram QR Codes</span>
|
||||||
<>
|
</h1>
|
||||||
<script
|
|
||||||
type="application/ld+json"
|
<p className="text-lg md:text-xl text-pink-50 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
Connect physically to digitally. Let customers scan to follow your Instagram profile instantly.
|
||||||
/>
|
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Grow your brand effortlessly.</strong>
|
||||||
<ToolBreadcrumb toolName="Instagram QR Code Generator" toolSlug="instagram-qr-code" />
|
</p>
|
||||||
|
|
||||||
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
||||||
|
<div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
||||||
{/* HERO SECTION */}
|
<Heart className="w-4 h-4 text-pink-200" />
|
||||||
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden bg-gradient-to-br from-[#833AB4] via-[#FD1D1D] to-[#FCA145]">
|
More Likes
|
||||||
<div className="absolute inset-0 opacity-10">
|
</div>
|
||||||
<svg className="w-full h-full" width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="none">
|
<div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
||||||
<circle cx="0" cy="0" r="40" fill="white" fillOpacity="0.1" />
|
<Zap className="w-4 h-4 text-yellow-200" />
|
||||||
<circle cx="100" cy="100" r="50" fill="white" fillOpacity="0.1" />
|
Instant Follow
|
||||||
</svg>
|
</div>
|
||||||
</div>
|
<div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
||||||
|
<Smartphone className="w-4 h-4 text-white" />
|
||||||
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
App Deep Link
|
||||||
<div className="text-center lg:text-left">
|
</div>
|
||||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
|
</div>
|
||||||
<span className="flex h-2 w-2 relative">
|
</div>
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-pink-300 opacity-75"></span>
|
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-pink-300"></span>
|
{/* Visual Abstract */}
|
||||||
</span>
|
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
|
||||||
Free Tool — No Signup Required
|
<div className="absolute w-[500px] h-[500px] bg-white/10 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
|
||||||
</div>
|
|
||||||
|
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform -rotate-3 hover:rotate-0 transition-all duration-700 group">
|
||||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
|
||||||
Boost Your Following with <br className="hidden lg:block" />
|
|
||||||
<span className="text-white drop-shadow-md">Instagram QR Codes</span>
|
<div className="w-full bg-white rounded-xl shadow-lg p-4 mb-6 relative overflow-hidden flex flex-col items-center">
|
||||||
</h1>
|
<div className="w-16 h-16 rounded-full p-[2px] bg-gradient-to-tr from-[#FCA145] via-[#FD1D1D] to-[#833AB4] mb-2">
|
||||||
|
<div className="w-full h-full rounded-full bg-white p-1">
|
||||||
<p className="text-lg md:text-xl text-pink-50 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
<div className="w-full h-full rounded-full bg-slate-200" />
|
||||||
Connect physically to digitally. Let customers scan to follow your Instagram profile instantly.
|
</div>
|
||||||
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Grow your brand effortlessly.</strong>
|
</div>
|
||||||
</p>
|
<div className="text-sm font-bold text-slate-900">@yourbrand</div>
|
||||||
|
</div>
|
||||||
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
|
||||||
<div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
<div className="w-48 h-48 bg-white rounded-xl p-2 shadow-inner relative overflow-hidden flex items-center justify-center">
|
||||||
<Heart className="w-4 h-4 text-pink-200" />
|
<QRCodeSVG value="https://www.qrmaster.net" size={170} fgColor="#E1306C" level="Q" />
|
||||||
More Likes
|
</div>
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
{/* Floating Badge */}
|
||||||
<Zap className="w-4 h-4 text-yellow-200" />
|
<div className="absolute -bottom-6 -left-6 bg-white py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
|
||||||
Instant Follow
|
<div className="bg-gradient-to-tr from-[#FCA145] to-[#E1306C] p-2 rounded-full text-white">
|
||||||
</div>
|
<Camera className="w-5 h-5" />
|
||||||
<div className="flex items-center gap-2 bg-white/10 px-4 py-2.5 rounded-xl border border-white/10 backdrop-blur-sm">
|
</div>
|
||||||
<Smartphone className="w-4 h-4 text-white" />
|
<div className="text-left">
|
||||||
App Deep Link
|
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Profile</div>
|
||||||
</div>
|
<div className="text-sm font-bold text-slate-900">Following</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Visual Abstract */}
|
</div>
|
||||||
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
|
</div>
|
||||||
<div className="absolute w-[500px] h-[500px] bg-white/10 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
|
</section>
|
||||||
|
|
||||||
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform -rotate-3 hover:rotate-0 transition-all duration-700 group">
|
{/* GENERATOR SECTION */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
|
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
|
||||||
|
<InstagramGenerator />
|
||||||
<div className="w-full bg-white rounded-xl shadow-lg p-4 mb-6 relative overflow-hidden flex flex-col items-center">
|
</section>
|
||||||
<div className="w-16 h-16 rounded-full p-[2px] bg-gradient-to-tr from-[#FCA145] via-[#FD1D1D] to-[#833AB4] mb-2">
|
|
||||||
<div className="w-full h-full rounded-full bg-white p-1">
|
{/* HOW IT WORKS */}
|
||||||
<div className="w-full h-full rounded-full bg-slate-200" />
|
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
|
||||||
</div>
|
<div className="max-w-4xl mx-auto">
|
||||||
</div>
|
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
|
||||||
<div className="text-sm font-bold text-slate-900">@yourbrand</div>
|
How Instagram QR Codes Work
|
||||||
</div>
|
</h2>
|
||||||
|
|
||||||
<div className="w-48 h-48 bg-white rounded-xl p-2 shadow-inner relative overflow-hidden flex items-center justify-center">
|
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
|
||||||
<QRCodeSVG value="https://www.qrmaster.net" size={170} fgColor="#E1306C" level="Q" />
|
<article className="text-center">
|
||||||
</div>
|
<div className="w-14 h-14 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Instagram className="w-7 h-7 text-[#E1306C]" />
|
||||||
{/* Floating Badge */}
|
</div>
|
||||||
<div className="absolute -bottom-6 -left-6 bg-white py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
|
<h3 className="font-bold text-slate-900 mb-2">1. Username</h3>
|
||||||
<div className="bg-gradient-to-tr from-[#FCA145] to-[#E1306C] p-2 rounded-full text-white">
|
<p className="text-slate-600 text-sm">
|
||||||
<Camera className="w-5 h-5" />
|
Enter your Instagram handle. No need to login or connect your account.
|
||||||
</div>
|
</p>
|
||||||
<div className="text-left">
|
</article>
|
||||||
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Profile</div>
|
|
||||||
<div className="text-sm font-bold text-slate-900">Following</div>
|
<article className="text-center">
|
||||||
</div>
|
<div className="w-14 h-14 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
</div>
|
<Smartphone className="w-7 h-7 text-[#E1306C]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<h3 className="font-bold text-slate-900 mb-2">2. Print</h3>
|
||||||
</div>
|
<p className="text-slate-600 text-sm">
|
||||||
</section>
|
Add the QR code to your packaging, business cards, or table tents.
|
||||||
|
</p>
|
||||||
{/* GENERATOR SECTION */}
|
</article>
|
||||||
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
|
|
||||||
<InstagramGenerator />
|
<article className="text-center">
|
||||||
</section>
|
<div className="w-12 h-12 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Download className="w-6 h-6 text-[#E1306C]" />
|
||||||
{/* HOW IT WORKS */}
|
</div>
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
|
<h3 className="font-bold text-slate-900 mb-2">3. Download</h3>
|
||||||
<div className="max-w-4xl mx-auto">
|
<p className="text-slate-600 text-xs leading-relaxed">
|
||||||
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
|
Save your custom QR code.
|
||||||
How Instagram QR Codes Work
|
</p>
|
||||||
</h2>
|
</article>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
|
<article className="text-center">
|
||||||
<article className="text-center">
|
<div className="w-12 h-12 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
<div className="w-14 h-14 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4">
|
<Heart className="w-6 h-6 text-[#E1306C]" />
|
||||||
<Instagram className="w-7 h-7 text-[#E1306C]" />
|
</div>
|
||||||
</div>
|
<h3 className="font-bold text-slate-900 mb-2">4. Scan</h3>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">1. Username</h3>
|
<p className="text-slate-600 text-xs leading-relaxed">
|
||||||
<p className="text-slate-600 text-sm">
|
Fans scan to instantly visit your profile.
|
||||||
Enter your Instagram handle. No need to login or connect your account.
|
</p>
|
||||||
</p>
|
</article>
|
||||||
</article>
|
|
||||||
|
<article className="text-center">
|
||||||
<article className="text-center">
|
<div className="w-12 h-12 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
<div className="w-14 h-14 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4">
|
<Share2 className="w-6 h-6 text-[#E1306C]" />
|
||||||
<Smartphone className="w-7 h-7 text-[#E1306C]" />
|
</div>
|
||||||
</div>
|
<h3 className="font-bold text-slate-900 mb-2">5. Grow</h3>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">2. Print</h3>
|
<p className="text-slate-600 text-xs leading-relaxed">
|
||||||
<p className="text-slate-600 text-sm">
|
Convert offline traffic into followers.
|
||||||
Add the QR code to your packaging, business cards, or table tents.
|
</p>
|
||||||
</p>
|
</article>
|
||||||
</article>
|
</div>
|
||||||
|
</div>
|
||||||
<article className="text-center">
|
</section>
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4">
|
|
||||||
<Download className="w-6 h-6 text-[#E1306C]" />
|
<RelatedTools />
|
||||||
</div>
|
|
||||||
<h3 className="font-bold text-slate-900 mb-2">3. Download</h3>
|
{/* FAQ SECTION */}
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">
|
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
||||||
Save your custom QR code.
|
<div className="max-w-3xl mx-auto">
|
||||||
</p>
|
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
||||||
</article>
|
Frequently Asked Questions
|
||||||
|
</h2>
|
||||||
<article className="text-center">
|
<p className="text-slate-600 text-center mb-10">
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4">
|
Common questions about Instagram QR codes.
|
||||||
<Heart className="w-6 h-6 text-[#E1306C]" />
|
</p>
|
||||||
</div>
|
|
||||||
<h3 className="font-bold text-slate-900 mb-2">4. Scan</h3>
|
<div className="space-y-4">
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">
|
<FaqItem
|
||||||
Fans scan to instantly visit your profile.
|
question="Does this work for private accounts?"
|
||||||
</p>
|
answer="Yes, the link will take users to your profile. If your account is private, they will still have to request to follow you."
|
||||||
</article>
|
/>
|
||||||
|
<FaqItem
|
||||||
<article className="text-center">
|
question="Can I link to a Story?"
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#E1306C]/10 flex items-center justify-center mx-auto mb-4">
|
answer="Yes, but Stories expire after 24 hours (unless saved as a Highlight). Linking to a Highlight or your main Profile is usually better for printed materials."
|
||||||
<Share2 className="w-6 h-6 text-[#E1306C]" />
|
/>
|
||||||
</div>
|
<FaqItem
|
||||||
<h3 className="font-bold text-slate-900 mb-2">5. Grow</h3>
|
question="Can I customize the frame?"
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">
|
answer="Yes, we offer several frame options like 'Follow Us' or 'Scan Me' to encourage action."
|
||||||
Convert offline traffic into followers.
|
/>
|
||||||
</p>
|
<FaqItem
|
||||||
</article>
|
question="Does it expire?"
|
||||||
</div>
|
answer="No. The QR code will work as long as your Instagram username remains the same."
|
||||||
</div>
|
/>
|
||||||
</section>
|
<FaqItem
|
||||||
|
question="Can I track scans?"
|
||||||
{/* FAQ SECTION */}
|
answer="Not with this static tool. If you need scan analytics, consider using our Dynamic QR Code solution."
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
/>
|
||||||
<div className="max-w-3xl mx-auto">
|
</div>
|
||||||
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
</div>
|
||||||
Frequently Asked Questions
|
</section>
|
||||||
</h2>
|
|
||||||
<p className="text-slate-600 text-center mb-10">
|
</div>
|
||||||
Common questions about Instagram QR codes.
|
</>
|
||||||
</p>
|
);
|
||||||
|
}
|
||||||
<div className="space-y-4">
|
|
||||||
<FaqItem
|
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
||||||
question="Does this work for private accounts?"
|
return (
|
||||||
answer="Yes, the link will take users to your profile. If your account is private, they will still have to request to follow you."
|
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
/>
|
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
|
||||||
<FaqItem
|
{question}
|
||||||
question="Can I link to a Story?"
|
<span className="transition group-open:rotate-180 text-slate-400">
|
||||||
answer="Yes, but Stories expire after 24 hours (unless saved as a Highlight). Linking to a Highlight or your main Profile is usually better for printed materials."
|
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
|
||||||
/>
|
<path d="M6 9l6 6 6-6" />
|
||||||
<FaqItem
|
</svg>
|
||||||
question="Can I customize the frame?"
|
</span>
|
||||||
answer="Yes, we offer several frame options like 'Follow Us' or 'Scan Me' to encourage action."
|
</summary>
|
||||||
/>
|
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
|
||||||
<FaqItem
|
{answer}
|
||||||
question="Does it expire?"
|
</div>
|
||||||
answer="No. The QR code will work as long as your Instagram username remains the same."
|
</details>
|
||||||
/>
|
);
|
||||||
<FaqItem
|
}
|
||||||
question="Can I track scans?"
|
|
||||||
answer="Not with this static tool. If you need scan analytics, consider using our Dynamic QR Code solution."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
|
||||||
return (
|
|
||||||
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
|
||||||
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
|
|
||||||
{question}
|
|
||||||
<span className="transition group-open:rotate-180 text-slate-400">
|
|
||||||
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
|
|
||||||
<path d="M6 9l6 6 6-6" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</summary>
|
|
||||||
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
|
|
||||||
{answer}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
37
src/app/(marketing)/tools/layout.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Script from 'next/script';
|
||||||
|
import AdBanner from '@/components/ads/AdBanner';
|
||||||
|
|
||||||
|
export default function ToolsLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col min-h-screen">
|
||||||
|
{/* AdSense script - only loads on tool pages */}
|
||||||
|
<Script
|
||||||
|
async
|
||||||
|
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-2782770414424875"
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
strategy="lazyOnload"
|
||||||
|
/>
|
||||||
|
<div className="flex-grow relative">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer Ad Placement - Appears on ALL tool pages */}
|
||||||
|
{/* AdBanner handles its own visibility - only shows when an ad is filled */}
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl pb-8">
|
||||||
|
<AdBanner
|
||||||
|
dataAdSlot="1234567890" // Placeholder
|
||||||
|
dataAdFormat="auto"
|
||||||
|
fullWidthResponsive={true}
|
||||||
|
className="bg-slate-50 rounded-xl p-4 border border-slate-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,342 +1,342 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import {
|
import {
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Download,
|
Download,
|
||||||
Check,
|
Check,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
DollarSign
|
DollarSign
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Select } from '@/components/ui/Select';
|
import { Select } from '@/components/ui/Select';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
// Brand Colors - PayPal Blue
|
// Brand Colors - PayPal Blue
|
||||||
const BRAND = {
|
const BRAND = {
|
||||||
paleGrey: '#EFF6FF', // Blue-50
|
paleGrey: '#EFF6FF', // Blue-50
|
||||||
primary: '#003087', // PayPal Dark Blue
|
primary: '#003087', // PayPal Dark Blue
|
||||||
primaryDark: '#001F5C',
|
primaryDark: '#001F5C',
|
||||||
accent: '#0070BA', // PayPal Light Blue
|
accent: '#0070BA', // PayPal Light Blue
|
||||||
};
|
};
|
||||||
|
|
||||||
// QR Color Options
|
// QR Color Options
|
||||||
const QR_COLORS = [
|
const QR_COLORS = [
|
||||||
{ name: 'PayPal Blue', value: '#003087' },
|
{ name: 'PayPal Blue', value: '#003087' },
|
||||||
{ name: 'PayPal Light', value: '#0070BA' },
|
{ name: 'PayPal Light', value: '#0070BA' },
|
||||||
{ name: 'Classic Black', value: '#000000' },
|
{ name: 'Classic Black', value: '#000000' },
|
||||||
{ name: 'Indigo', value: '#4F46E5' },
|
{ name: 'Indigo', value: '#4F46E5' },
|
||||||
{ name: 'Violet', value: '#7C3AED' },
|
{ name: 'Violet', value: '#7C3AED' },
|
||||||
{ name: 'Emerald', value: '#10B981' },
|
{ name: 'Emerald', value: '#10B981' },
|
||||||
{ name: 'Rose', value: '#F43F5E' },
|
{ name: 'Rose', value: '#F43F5E' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Frame Options
|
// Frame Options
|
||||||
const FRAME_OPTIONS = [
|
const FRAME_OPTIONS = [
|
||||||
{ id: 'none', label: 'No Frame' },
|
{ id: 'none', label: 'No Frame' },
|
||||||
{ id: 'scanme', label: 'Scan Me' },
|
{ id: 'scanme', label: 'Scan Me' },
|
||||||
{ id: 'pay', label: 'Pay Now' },
|
{ id: 'pay', label: 'Pay Now' },
|
||||||
{ id: 'donate', label: 'Donate' },
|
{ id: 'donate', label: 'Donate' },
|
||||||
{ id: 'tip', label: 'Tip Me' },
|
{ id: 'tip', label: 'Tip Me' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Currency Options
|
// Currency Options
|
||||||
const CURRENCIES = [
|
const CURRENCIES = [
|
||||||
{ value: 'EUR', label: 'EUR (€)' },
|
{ value: 'EUR', label: 'EUR (€)' },
|
||||||
{ value: 'USD', label: 'USD ($)' },
|
{ value: 'USD', label: 'USD ($)' },
|
||||||
{ value: 'GBP', label: 'GBP (£)' },
|
{ value: 'GBP', label: 'GBP (£)' },
|
||||||
{ value: 'CHF', label: 'CHF' },
|
{ value: 'CHF', label: 'CHF' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Input type options
|
// Input type options
|
||||||
const INPUT_TYPES = [
|
const INPUT_TYPES = [
|
||||||
{ id: 'username', label: 'PayPal.me Username' },
|
{ id: 'username', label: 'PayPal.me Username' },
|
||||||
{ id: 'email', label: 'PayPal Email' },
|
{ id: 'email', label: 'PayPal Email' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function PayPalGenerator() {
|
export default function PayPalGenerator() {
|
||||||
const [inputType, setInputType] = useState('email');
|
const [inputType, setInputType] = useState('email');
|
||||||
const [paypalId, setPaypalId] = useState('');
|
const [paypalId, setPaypalId] = useState('');
|
||||||
const [amount, setAmount] = useState('');
|
const [amount, setAmount] = useState('');
|
||||||
const [currency, setCurrency] = useState('EUR');
|
const [currency, setCurrency] = useState('EUR');
|
||||||
const [qrColor, setQrColor] = useState(BRAND.primary);
|
const [qrColor, setQrColor] = useState(BRAND.primary);
|
||||||
const [frameType, setFrameType] = useState('none');
|
const [frameType, setFrameType] = useState('none');
|
||||||
|
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Generate PayPal payment link
|
// Generate PayPal payment link
|
||||||
const generatePayPalLink = () => {
|
const generatePayPalLink = () => {
|
||||||
if (!paypalId.trim()) return 'https://paypal.com';
|
if (!paypalId.trim()) return 'https://paypal.com';
|
||||||
|
|
||||||
if (inputType === 'username') {
|
if (inputType === 'username') {
|
||||||
// PayPal.me link
|
// PayPal.me link
|
||||||
let link = `https://paypal.me/${paypalId.trim()}`;
|
let link = `https://paypal.me/${paypalId.trim()}`;
|
||||||
if (amount && parseFloat(amount) > 0) {
|
if (amount && parseFloat(amount) > 0) {
|
||||||
link += `/${amount}`;
|
link += `/${amount}`;
|
||||||
}
|
}
|
||||||
return link;
|
return link;
|
||||||
} else {
|
} else {
|
||||||
// PayPal email payment link (donation/payment format)
|
// PayPal email payment link (donation/payment format)
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
cmd: '_donations',
|
cmd: '_donations',
|
||||||
business: paypalId.trim(),
|
business: paypalId.trim(),
|
||||||
currency_code: currency,
|
currency_code: currency,
|
||||||
...(amount && parseFloat(amount) > 0 ? { amount } : {}),
|
...(amount && parseFloat(amount) > 0 ? { amount } : {}),
|
||||||
});
|
});
|
||||||
return `https://www.paypal.com/cgi-bin/webscr?${params.toString()}`;
|
return `https://www.paypal.com/cgi-bin/webscr?${params.toString()}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = async (format: 'png' | 'svg') => {
|
const handleDownload = async (format: 'png' | 'svg') => {
|
||||||
if (!qrRef.current) return;
|
if (!qrRef.current) return;
|
||||||
try {
|
try {
|
||||||
if (format === 'png') {
|
if (format === 'png') {
|
||||||
const { toPng } = await import('html-to-image');
|
const { toPng } = await import('html-to-image');
|
||||||
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3 });
|
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3 });
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.download = `paypal-qr-${paypalId || 'code'}.png`;
|
link.download = `paypal-qr-${paypalId || 'code'}.png`;
|
||||||
link.href = dataUrl;
|
link.href = dataUrl;
|
||||||
link.click();
|
link.click();
|
||||||
} else {
|
} else {
|
||||||
const svgData = qrRef.current.querySelector('svg')?.outerHTML;
|
const svgData = qrRef.current.querySelector('svg')?.outerHTML;
|
||||||
if (svgData) {
|
if (svgData) {
|
||||||
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = `paypal-qr-${paypalId || 'code'}.svg`;
|
link.download = `paypal-qr-${paypalId || 'code'}.svg`;
|
||||||
link.click();
|
link.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed', err);
|
console.error('Download failed', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFrameLabel = () => {
|
const getFrameLabel = () => {
|
||||||
const frame = FRAME_OPTIONS.find(f => f.id === frameType);
|
const frame = FRAME_OPTIONS.find(f => f.id === frameType);
|
||||||
return frame?.id !== 'none' ? frame?.label : null;
|
return frame?.id !== 'none' ? frame?.label : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
|
||||||
|
|
||||||
{/* Main Generator Card */}
|
{/* Main Generator Card */}
|
||||||
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
|
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
|
||||||
<div className="grid lg:grid-cols-2">
|
<div className="grid lg:grid-cols-2">
|
||||||
|
|
||||||
{/* LEFT: Input Section */}
|
{/* LEFT: Input Section */}
|
||||||
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||||
|
|
||||||
{/* PayPal Details */}
|
{/* PayPal Details */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
||||||
<CreditCard className="w-5 h-5 text-[#003087]" />
|
<CreditCard className="w-5 h-5 text-[#003087]" />
|
||||||
PayPal Details
|
PayPal Details
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Input Type Toggle */}
|
{/* Input Type Toggle */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Payment Method</label>
|
<label className="block text-sm font-medium text-slate-700 mb-2">Payment Method</label>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{INPUT_TYPES.map((type) => (
|
{INPUT_TYPES.map((type) => (
|
||||||
<button
|
<button
|
||||||
key={type.id}
|
key={type.id}
|
||||||
onClick={() => setInputType(type.id)}
|
onClick={() => setInputType(type.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"py-2.5 px-3 rounded-lg text-sm font-medium transition-all border",
|
"py-2.5 px-3 rounded-lg text-sm font-medium transition-all border",
|
||||||
inputType === type.id
|
inputType === type.id
|
||||||
? "bg-[#003087] text-white border-[#003087]"
|
? "bg-[#003087] text-white border-[#003087]"
|
||||||
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{type.label}
|
{type.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
{inputType === 'username' ? 'PayPal.me Username' : 'PayPal Email Address'}
|
{inputType === 'username' ? 'PayPal.me Username' : 'PayPal Email Address'}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
type={inputType === 'email' ? 'email' : 'text'}
|
type={inputType === 'email' ? 'email' : 'text'}
|
||||||
placeholder={inputType === 'username' ? 'e.g. johndoe' : 'e.g. mail@example.com'}
|
placeholder={inputType === 'username' ? 'e.g. johndoe' : 'e.g. mail@example.com'}
|
||||||
value={paypalId}
|
value={paypalId}
|
||||||
onChange={(e) => setPaypalId(e.target.value)}
|
onChange={(e) => setPaypalId(e.target.value)}
|
||||||
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#003087] focus:ring-[#003087]"
|
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#003087] focus:ring-[#003087]"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-slate-500 mt-2">
|
<p className="text-xs text-slate-600 mt-2">
|
||||||
{inputType === 'username'
|
{inputType === 'username'
|
||||||
? <>Find yours at <a href="https://paypal.me" target="_blank" rel="noopener noreferrer" className="text-[#003087] underline">paypal.me</a></>
|
? <>Find yours at <a href="https://paypal.me" target="_blank" rel="noopener noreferrer" className="text-[#003087] underline">paypal.me</a></>
|
||||||
: 'The email address linked to your PayPal account'
|
: 'The email address linked to your PayPal account'
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Amount (Optional)</label>
|
<label className="block text-sm font-medium text-slate-700 mb-2">Amount (Optional)</label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="25.00"
|
placeholder="25.00"
|
||||||
value={amount}
|
value={amount}
|
||||||
onChange={(e) => setAmount(e.target.value)}
|
onChange={(e) => setAmount(e.target.value)}
|
||||||
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#003087] focus:ring-[#003087]"
|
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#003087] focus:ring-[#003087]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Currency</label>
|
<label className="block text-sm font-medium text-slate-700 mb-2">Currency</label>
|
||||||
<Select
|
<Select
|
||||||
value={currency}
|
value={currency}
|
||||||
onChange={(e) => setCurrency(e.target.value)}
|
onChange={(e) => setCurrency(e.target.value)}
|
||||||
className="h-12 rounded-xl border-slate-200"
|
className="h-12 rounded-xl border-slate-200"
|
||||||
options={CURRENCIES}
|
aria-label="Currency"
|
||||||
/>
|
options={CURRENCIES}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="border-t border-slate-100"></div>
|
|
||||||
|
<div className="border-t border-slate-100"></div>
|
||||||
{/* Design Options */}
|
|
||||||
<div className="space-y-6">
|
{/* Design Options */}
|
||||||
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
<div className="space-y-6">
|
||||||
<Sparkles className="w-5 h-5 text-[#003087]" />
|
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
||||||
Design Options
|
<Sparkles className="w-5 h-5 text-[#003087]" />
|
||||||
</h2>
|
Design Options
|
||||||
|
</h2>
|
||||||
{/* Color Picker */}
|
|
||||||
<div>
|
{/* Color Picker */}
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-3">QR Code Color</label>
|
<div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<label className="block text-sm font-medium text-slate-700 mb-3">QR Code Color</label>
|
||||||
{QR_COLORS.map((c) => (
|
<div className="flex flex-wrap gap-2">
|
||||||
<button
|
{QR_COLORS.map((c) => (
|
||||||
key={c.name}
|
<button
|
||||||
onClick={() => setQrColor(c.value)}
|
key={c.name}
|
||||||
className={cn(
|
onClick={() => setQrColor(c.value)}
|
||||||
"w-9 h-9 rounded-full border-2 flex items-center justify-center transition-all hover:scale-110",
|
className={cn(
|
||||||
qrColor === c.value ? "border-slate-900 ring-2 ring-offset-2 ring-slate-200" : "border-white shadow-md"
|
"w-9 h-9 rounded-full border-2 flex items-center justify-center transition-all hover:scale-110",
|
||||||
)}
|
qrColor === c.value ? "border-slate-900 ring-2 ring-offset-2 ring-slate-200" : "border-white shadow-md"
|
||||||
style={{ backgroundColor: c.value }}
|
)}
|
||||||
aria-label={`Select ${c.name}`}
|
style={{ backgroundColor: c.value }}
|
||||||
title={c.name}
|
aria-label={`Select ${c.name}`}
|
||||||
>
|
title={c.name}
|
||||||
{qrColor === c.value && <Check className="w-4 h-4 text-white" strokeWidth={3} />}
|
>
|
||||||
</button>
|
{qrColor === c.value && <Check className="w-4 h-4 text-white" strokeWidth={3} />}
|
||||||
))}
|
</button>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Frame Selector */}
|
|
||||||
<div>
|
{/* Frame Selector */}
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
<div>
|
||||||
<div className="grid grid-cols-5 gap-2">
|
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
||||||
{FRAME_OPTIONS.map((frame) => (
|
<div className="grid grid-cols-2 sm:grid-cols-5 gap-2">
|
||||||
<button
|
{FRAME_OPTIONS.map((frame) => (
|
||||||
key={frame.id}
|
<button
|
||||||
onClick={() => setFrameType(frame.id)}
|
key={frame.id}
|
||||||
className={cn(
|
onClick={() => setFrameType(frame.id)}
|
||||||
"py-2.5 px-2 rounded-lg text-xs font-medium transition-all border",
|
className={cn(
|
||||||
frameType === frame.id
|
"py-2.5 px-2 rounded-lg text-xs font-medium transition-all border",
|
||||||
? "bg-[#003087] text-white border-[#003087]"
|
frameType === frame.id
|
||||||
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
? "bg-[#003087] text-white border-[#003087]"
|
||||||
)}
|
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
|
||||||
>
|
)}
|
||||||
{frame.label}
|
>
|
||||||
</button>
|
{frame.label}
|
||||||
))}
|
</button>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* RIGHT: Preview Section */}
|
|
||||||
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
{/* RIGHT: Preview Section */}
|
||||||
|
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||||
{/* QR Card with Frame */}
|
|
||||||
<div
|
{/* QR Card with Frame */}
|
||||||
ref={qrRef}
|
<div
|
||||||
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
|
ref={qrRef}
|
||||||
style={{ minWidth: '320px' }}
|
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
|
||||||
>
|
>
|
||||||
{/* Frame Label */}
|
{/* Frame Label */}
|
||||||
{getFrameLabel() && (
|
{getFrameLabel() && (
|
||||||
<div
|
<div
|
||||||
className="mb-5 px-8 py-2.5 rounded-full text-white font-bold text-sm tracking-widest uppercase shadow-md"
|
className="mb-5 px-8 py-2.5 rounded-full text-white font-bold text-sm tracking-widest uppercase shadow-md"
|
||||||
style={{ backgroundColor: qrColor }}
|
style={{ backgroundColor: qrColor }}
|
||||||
>
|
>
|
||||||
{getFrameLabel()}
|
{getFrameLabel()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* QR Code */}
|
{/* QR Code */}
|
||||||
<div className="bg-white">
|
<div className="bg-white">
|
||||||
<QRCodeSVG
|
<QRCodeSVG
|
||||||
value={generatePayPalLink()}
|
value={generatePayPalLink()}
|
||||||
size={240}
|
size={240}
|
||||||
level="M"
|
level="M"
|
||||||
includeMargin={false}
|
includeMargin={false}
|
||||||
fgColor={qrColor}
|
fgColor={qrColor}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* PayPal Info */}
|
{/* PayPal Info */}
|
||||||
<div className="mt-6 text-center max-w-[260px]">
|
<div className="mt-6 text-center max-w-[260px]">
|
||||||
<h3 className="font-bold text-slate-900 text-lg flex items-center justify-center gap-2 truncate">
|
<h3 className="font-bold text-slate-900 text-lg flex items-center justify-center gap-2 truncate">
|
||||||
<DollarSign className="w-4 h-4 text-[#003087] shrink-0" />
|
<DollarSign className="w-4 h-4 text-[#003087] shrink-0" />
|
||||||
<span className="truncate">{paypalId || 'Your PayPal'}</span>
|
<span className="truncate">{paypalId || 'Your PayPal'}</span>
|
||||||
</h3>
|
</h3>
|
||||||
{amount && (
|
{amount && (
|
||||||
<p className="text-sm text-slate-500 mt-1">{amount} {currency}</p>
|
<p className="text-sm text-slate-600 mt-1">{amount} {currency}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Download Buttons */}
|
{/* Download Buttons */}
|
||||||
<div className="flex items-center gap-3 mt-8">
|
<div className="flex items-center gap-3 mt-8">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleDownload('png')}
|
onClick={() => handleDownload('png')}
|
||||||
className="bg-[#003087] hover:bg-[#001F5C] text-white shadow-lg"
|
className="bg-[#003087] hover:bg-[#001F5C] text-white shadow-lg"
|
||||||
>
|
>
|
||||||
<Download className="w-4 h-4 mr-2" />
|
<Download className="w-4 h-4 mr-2" />
|
||||||
Download PNG
|
Download PNG
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleDownload('svg')}
|
onClick={() => handleDownload('svg')}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="border-slate-300 hover:bg-white"
|
className="border-slate-300 hover:bg-white"
|
||||||
>
|
>
|
||||||
<Download className="w-4 h-4 mr-2" />
|
<Download className="w-4 h-4 mr-2" />
|
||||||
SVG
|
SVG
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-slate-500 mt-4 text-center">
|
<p className="text-xs text-slate-600 mt-4 text-center">
|
||||||
Your PayPal link is encoded directly. Static and forever free.
|
Your PayPal link is encoded directly. Static and forever free.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Upsell Banner */}
|
{/* Upsell Banner */}
|
||||||
<div className="mt-8 bg-gradient-to-r from-[#003087] to-[#0070BA] rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
<div className="mt-8 bg-gradient-to-r from-[#003087] to-[#0070BA] rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
<div className="text-white text-center sm:text-left">
|
<div className="text-white text-center sm:text-left">
|
||||||
<h3 className="font-bold text-lg">Need payment analytics?</h3>
|
<h3 className="font-bold text-lg">Need payment analytics?</h3>
|
||||||
<p className="text-white/80 text-sm mt-1">Track how many people scan your payment QR code with Dynamic QR Codes.</p>
|
<p className="text-white/80 text-sm mt-1">Track how many people scan your payment QR code with Dynamic QR Codes.</p>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/signup">
|
<Link href="/signup">
|
||||||
<Button className="bg-white text-[#003087] hover:bg-slate-100 shrink-0 shadow-lg">
|
<Button className="bg-white text-[#003087] hover:bg-slate-100 shrink-0 shadow-lg">
|
||||||
Get Analytics
|
Get Analytics
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,358 +1,331 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import PayPalGenerator from './PayPalGenerator';
|
import PayPalGenerator from './PayPalGenerator';
|
||||||
import { CreditCard, Shield, Zap, Smartphone, DollarSign, Download, Share2, Banknote } from 'lucide-react';
|
import { CreditCard, Shield, Zap, Smartphone, DollarSign, Download, Share2, Banknote } from 'lucide-react';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
||||||
|
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
||||||
// SEO Optimized Metadata
|
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Free PayPal QR Code Generator | Accept Payments Instantly | QR Master',
|
// SEO Optimized Metadata
|
||||||
description: 'Create a QR code for your PayPal.me link. Let customers pay you instantly by scanning. Support tips, donations, and fixed amounts. 100% free.',
|
export const metadata: Metadata = {
|
||||||
keywords: ['paypal qr code', 'paypal.me qr generator', 'payment qr code', 'accept payments qr', 'paypal qr generator', 'tip qr code', 'donation qr code'],
|
title: {
|
||||||
alternates: {
|
absolute: 'Free PayPal QR Code Generator | Accept Payments Instantly | QR Master',
|
||||||
canonical: 'https://qrmaster.io/tools/paypal-qr-code',
|
},
|
||||||
},
|
description: 'Create a QR code for your PayPal.me link. PayPal QR Code erstellen. Receive payments instantly. Support tips, donations, and fixed amounts.',
|
||||||
openGraph: {
|
keywords: ['paypal qr code', 'paypal.me qr generator', 'payment qr code', 'accept payments qr', 'paypal qr generator', 'tip qr code', 'donation qr code', 'paypal qr code erstellen', 'zahlungs qr code', 'spenden qr code', 'paypal bezahlen qr'],
|
||||||
title: 'Free PayPal QR Code Generator | QR Master',
|
alternates: {
|
||||||
description: 'Generate QR codes for PayPal payments. Perfect for tips, donations, and invoices.',
|
canonical: 'https://www.qrmaster.net/tools/paypal-qr-code',
|
||||||
type: 'website',
|
},
|
||||||
url: 'https://qrmaster.io/tools/paypal-qr-code',
|
openGraph: {
|
||||||
images: [{ url: '/og-paypal-generator.png', width: 1200, height: 630 }],
|
title: 'Free PayPal QR Code Generator | QR Master',
|
||||||
},
|
description: 'Generate QR codes for PayPal payments. Perfect for tips, donations, and invoices.',
|
||||||
twitter: {
|
type: 'website',
|
||||||
card: 'summary_large_image',
|
url: 'https://www.qrmaster.net/tools/paypal-qr-code',
|
||||||
title: 'Free PayPal QR Code Generator',
|
images: [{ url: '/og-paypal-generator.png', width: 1200, height: 630 }],
|
||||||
description: 'Create PayPal payment QR codes. Instant and free.',
|
},
|
||||||
},
|
twitter: {
|
||||||
robots: {
|
card: 'summary_large_image',
|
||||||
index: true,
|
title: 'Free PayPal QR Code Generator',
|
||||||
follow: true,
|
description: 'Create PayPal payment QR codes. Instant and free.',
|
||||||
},
|
},
|
||||||
};
|
robots: {
|
||||||
|
index: true,
|
||||||
// JSON-LD Structured Data
|
follow: true,
|
||||||
const jsonLd = {
|
},
|
||||||
'@context': 'https://schema.org',
|
};
|
||||||
'@graph': [
|
|
||||||
{
|
// JSON-LD Structured Data
|
||||||
'@type': 'SoftwareApplication',
|
const jsonLd = {
|
||||||
name: 'PayPal QR Code Generator',
|
'@context': 'https://schema.org',
|
||||||
applicationCategory: 'FinanceApplication',
|
'@graph': [
|
||||||
operatingSystem: 'Web Browser',
|
generateSoftwareAppSchema(
|
||||||
offers: {
|
'PayPal QR Code Generator',
|
||||||
'@type': 'Offer',
|
'Generate QR codes that link to your PayPal.me page for instant payments.',
|
||||||
price: '0',
|
'/og-paypal-generator.png',
|
||||||
priceCurrency: 'USD',
|
'FinanceApplication'
|
||||||
},
|
),
|
||||||
aggregateRating: {
|
{
|
||||||
'@type': 'AggregateRating',
|
'@type': 'HowTo',
|
||||||
ratingValue: '4.9',
|
name: 'How to Create a PayPal QR Code',
|
||||||
ratingCount: '980',
|
description: 'Create a QR code for receiving PayPal payments.',
|
||||||
},
|
step: [
|
||||||
description: 'Generate QR codes that link to your PayPal.me page for instant payments.',
|
{
|
||||||
},
|
'@type': 'HowToStep',
|
||||||
{
|
position: 1,
|
||||||
'@type': 'HowTo',
|
name: 'Enter Username',
|
||||||
name: 'How to Create a PayPal QR Code',
|
text: 'Type your PayPal.me username (the part after paypal.me/).',
|
||||||
description: 'Create a QR code for receiving PayPal payments.',
|
},
|
||||||
step: [
|
{
|
||||||
{
|
'@type': 'HowToStep',
|
||||||
'@type': 'HowToStep',
|
position: 2,
|
||||||
position: 1,
|
name: 'Set Amount (Optional)',
|
||||||
name: 'Enter Username',
|
text: 'Enter a pre-filled amount and currency for fixed payments.',
|
||||||
text: 'Type your PayPal.me username (the part after paypal.me/).',
|
},
|
||||||
},
|
{
|
||||||
{
|
'@type': 'HowToStep',
|
||||||
'@type': 'HowToStep',
|
position: 3,
|
||||||
position: 2,
|
name: 'Customize Design',
|
||||||
name: 'Set Amount (Optional)',
|
text: 'Choose PayPal brand colors and add a frame like "Pay Now" or "Tip Me".',
|
||||||
text: 'Enter a pre-filled amount and currency for fixed payments.',
|
},
|
||||||
},
|
{
|
||||||
{
|
'@type': 'HowToStep',
|
||||||
'@type': 'HowToStep',
|
position: 4,
|
||||||
position: 3,
|
name: 'Download QR Code',
|
||||||
name: 'Customize Design',
|
text: 'Download your high-quality QR code in PNG or SVG format.',
|
||||||
text: 'Choose PayPal brand colors and add a frame like "Pay Now" or "Tip Me".',
|
},
|
||||||
},
|
{
|
||||||
{
|
'@type': 'HowToStep',
|
||||||
'@type': 'HowToStep',
|
position: 5,
|
||||||
position: 4,
|
name: 'Share',
|
||||||
name: 'Download QR Code',
|
text: 'Print it on invoices, display at your shop, or share digitally.',
|
||||||
text: 'Download your high-quality QR code in PNG or SVG format.',
|
},
|
||||||
},
|
],
|
||||||
{
|
totalTime: 'PT30S',
|
||||||
'@type': 'HowToStep',
|
},
|
||||||
position: 5,
|
generateFaqSchema({
|
||||||
name: 'Share',
|
'How does the PayPal QR code work?': {
|
||||||
text: 'Print it on invoices, display at your shop, or share digitally.',
|
question: 'How does the PayPal QR code work?',
|
||||||
},
|
answer: 'When scanned, it opens the PayPal app or website with your PayPal.me link. If you set an amount, it will be pre-filled for the payer.',
|
||||||
],
|
},
|
||||||
totalTime: 'PT30S',
|
'Do I need a PayPal Business account?': {
|
||||||
},
|
question: 'Do I need a PayPal Business account?',
|
||||||
{
|
answer: 'No. Any PayPal account with a PayPal.me link can use this generator. Personal accounts work fine for tips and donations.',
|
||||||
'@type': 'FAQPage',
|
},
|
||||||
mainEntity: [
|
'Is there a fee for using the QR code?': {
|
||||||
{
|
question: 'Is there a fee for using the QR code?',
|
||||||
'@type': 'Question',
|
answer: 'This generator is 100% free. PayPal may charge their standard transaction fees when you receive payments.',
|
||||||
name: 'How does the PayPal QR code work?',
|
},
|
||||||
acceptedAnswer: {
|
'Can I change the amount later?': {
|
||||||
'@type': 'Answer',
|
question: 'Can I change the amount later?',
|
||||||
text: 'When scanned, it opens the PayPal app or website with your PayPal.me link. If you set an amount, it will be pre-filled for the payer.',
|
answer: 'No, this is a static QR code. The amount is encoded permanently. For variable amounts, leave the amount field empty.',
|
||||||
},
|
},
|
||||||
},
|
'What currencies are supported?': {
|
||||||
{
|
question: 'What currencies are supported?',
|
||||||
'@type': 'Question',
|
answer: 'We support EUR, USD, GBP, and CHF. PayPal handles currency conversion automatically.',
|
||||||
name: 'Do I need a PayPal Business account?',
|
},
|
||||||
acceptedAnswer: {
|
}),
|
||||||
'@type': 'Answer',
|
],
|
||||||
text: 'No. Any PayPal account with a PayPal.me link can use this generator. Personal accounts work fine for tips and donations.',
|
};
|
||||||
},
|
|
||||||
},
|
export default function PayPalQRCodePage() {
|
||||||
{
|
return (
|
||||||
'@type': 'Question',
|
<>
|
||||||
name: 'Is there a fee for using the QR code?',
|
<script
|
||||||
acceptedAnswer: {
|
type="application/ld+json"
|
||||||
'@type': 'Answer',
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
text: 'This generator is 100% free. PayPal may charge their standard transaction fees when you receive payments.',
|
/>
|
||||||
},
|
<ToolBreadcrumb toolName="PayPal QR Code Generator" toolSlug="paypal-qr-code" />
|
||||||
},
|
|
||||||
{
|
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
||||||
'@type': 'Question',
|
|
||||||
name: 'Can I change the amount later?',
|
{/* HERO SECTION */}
|
||||||
acceptedAnswer: {
|
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden" style={{ backgroundColor: '#003087' }}>
|
||||||
'@type': 'Answer',
|
<div className="absolute inset-0 opacity-10">
|
||||||
text: 'No, this is a static QR code. The amount is encoded permanently. For variable amounts, leave the amount field empty.',
|
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||||
},
|
<path d="M0 100 C 20 0 50 0 100 100 Z" fill="url(#grad1)" />
|
||||||
},
|
<defs>
|
||||||
{
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
'@type': 'Question',
|
<stop offset="0%" style={{ stopColor: 'white', stopOpacity: 1 }} />
|
||||||
name: 'What currencies are supported?',
|
<stop offset="100%" style={{ stopColor: 'white', stopOpacity: 0 }} />
|
||||||
acceptedAnswer: {
|
</linearGradient>
|
||||||
'@type': 'Answer',
|
</defs>
|
||||||
text: 'We support EUR, USD, GBP, and CHF. PayPal handles currency conversion automatically.',
|
</svg>
|
||||||
},
|
</div>
|
||||||
},
|
|
||||||
],
|
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
||||||
},
|
<div className="text-center lg:text-left">
|
||||||
],
|
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
|
||||||
};
|
<span className="flex h-2 w-2 relative">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span>
|
||||||
export default function PayPalQRCodePage() {
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-sky-400"></span>
|
||||||
return (
|
</span>
|
||||||
<>
|
Free Tool — No Signup Required
|
||||||
<script
|
</div>
|
||||||
type="application/ld+json"
|
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
||||||
/>
|
Accept Payments with <br className="hidden lg:block" />
|
||||||
<ToolBreadcrumb toolName="PayPal QR Code Generator" toolSlug="paypal-qr-code" />
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-sky-300 to-blue-200">PayPal QR Codes</span>
|
||||||
|
</h1>
|
||||||
<div className="min-h-screen" style={{ backgroundColor: '#EBEBDF' }}>
|
|
||||||
|
<p className="text-lg md:text-xl text-blue-100 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
||||||
{/* HERO SECTION */}
|
Let customers pay you by scanning. Perfect for tips, donations, and invoices.
|
||||||
<section className="relative pt-20 pb-20 lg:pt-32 lg:pb-32 px-4 sm:px-6 lg:px-8 overflow-hidden" style={{ backgroundColor: '#003087' }}>
|
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Instant payments.</strong>
|
||||||
<div className="absolute inset-0 opacity-10">
|
</p>
|
||||||
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
|
|
||||||
<path d="M0 100 C 20 0 50 0 100 100 Z" fill="url(#grad1)" />
|
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
||||||
<defs>
|
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
||||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
|
<CreditCard className="w-4 h-4 text-sky-300" />
|
||||||
<stop offset="0%" style={{ stopColor: 'white', stopOpacity: 1 }} />
|
PayPal.me Links
|
||||||
<stop offset="100%" style={{ stopColor: 'white', stopOpacity: 0 }} />
|
</div>
|
||||||
</linearGradient>
|
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
||||||
</defs>
|
<Zap className="w-4 h-4 text-amber-300" />
|
||||||
</svg>
|
Pre-fill Amount
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
||||||
<div className="max-w-7xl mx-auto grid lg:grid-cols-2 gap-12 items-center relative z-10">
|
<Shield className="w-4 h-4 text-emerald-300" />
|
||||||
<div className="text-center lg:text-left">
|
Secure Payments
|
||||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white/90 text-sm font-medium mb-6 backdrop-blur-sm border border-white/10 hover:bg-white/20 transition-colors cursor-default">
|
</div>
|
||||||
<span className="flex h-2 w-2 relative">
|
</div>
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span>
|
</div>
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-sky-400"></span>
|
|
||||||
</span>
|
{/* Visual Abstract */}
|
||||||
Free Tool — No Signup Required
|
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
|
||||||
</div>
|
<div className="absolute w-[500px] h-[500px] bg-blue-500/30 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
|
||||||
|
|
||||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight mb-6">
|
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform rotate-3 hover:rotate-0 transition-all duration-700 group">
|
||||||
Accept Payments with <br className="hidden lg:block" />
|
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
|
||||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-sky-300 to-blue-200">PayPal QR Codes</span>
|
|
||||||
</h1>
|
{/* Payment Card Mock */}
|
||||||
|
<div className="w-full bg-gradient-to-br from-[#0070BA] to-[#003087] rounded-xl shadow-lg p-4 mb-6 relative overflow-hidden text-white">
|
||||||
<p className="text-lg md:text-xl text-blue-100 max-w-2xl mx-auto lg:mx-0 mb-8 leading-relaxed">
|
<div className="flex justify-between items-start mb-3">
|
||||||
Let customers pay you by scanning. Perfect for tips, donations, and invoices.
|
<Banknote className="w-6 h-6 opacity-80" />
|
||||||
<strong className="text-white block sm:inline mt-2 sm:mt-0"> Instant payments.</strong>
|
<div className="bg-white/20 px-2 py-1 rounded text-xs">EUR</div>
|
||||||
</p>
|
</div>
|
||||||
|
<div className="text-2xl font-bold tracking-wider">€25.00</div>
|
||||||
<div className="flex flex-wrap justify-center lg:justify-start gap-4 text-sm font-medium text-white/80">
|
<div className="text-xs opacity-70 mt-1">Payment Request</div>
|
||||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
</div>
|
||||||
<CreditCard className="w-4 h-4 text-sky-300" />
|
|
||||||
PayPal.me Links
|
<div className="w-48 h-48 bg-white rounded-xl p-2 shadow-inner relative overflow-hidden flex items-center justify-center">
|
||||||
</div>
|
<QRCodeSVG value="https://www.qrmaster.net" size={170} fgColor="#003087" level="Q" />
|
||||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
</div>
|
||||||
<Zap className="w-4 h-4 text-amber-300" />
|
|
||||||
Pre-fill Amount
|
{/* Floating Badge */}
|
||||||
</div>
|
<div className="absolute -bottom-6 -left-6 bg-white py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
|
||||||
<div className="flex items-center gap-2 bg-white/5 px-4 py-2.5 rounded-xl border border-white/5 backdrop-blur-sm">
|
<div className="bg-blue-100 p-2 rounded-full">
|
||||||
<Shield className="w-4 h-4 text-emerald-300" />
|
<DollarSign className="w-5 h-5 text-[#003087]" />
|
||||||
Secure Payments
|
</div>
|
||||||
</div>
|
<div className="text-left">
|
||||||
</div>
|
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">PayPal</div>
|
||||||
</div>
|
<div className="text-sm font-bold text-slate-900">Ready</div>
|
||||||
|
</div>
|
||||||
{/* Visual Abstract */}
|
</div>
|
||||||
<div className="hidden lg:flex relative items-center justify-center min-h-[400px]">
|
</div>
|
||||||
<div className="absolute w-[500px] h-[500px] bg-blue-500/30 rounded-full blur-[100px] -top-20 -right-20 animate-pulse" />
|
</div>
|
||||||
|
</div>
|
||||||
<div className="relative w-80 h-96 bg-white/5 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl flex flex-col items-center justify-center p-8 transform rotate-3 hover:rotate-0 transition-all duration-700 group">
|
</section>
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent rounded-3xl" />
|
|
||||||
|
{/* GENERATOR SECTION */}
|
||||||
{/* Payment Card Mock */}
|
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
|
||||||
<div className="w-full bg-gradient-to-br from-[#0070BA] to-[#003087] rounded-xl shadow-lg p-4 mb-6 relative overflow-hidden text-white">
|
<PayPalGenerator />
|
||||||
<div className="flex justify-between items-start mb-3">
|
</section>
|
||||||
<Banknote className="w-6 h-6 opacity-80" />
|
|
||||||
<div className="bg-white/20 px-2 py-1 rounded text-xs">EUR</div>
|
{/* HOW IT WORKS */}
|
||||||
</div>
|
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
|
||||||
<div className="text-2xl font-bold tracking-wider">€25.00</div>
|
<div className="max-w-4xl mx-auto">
|
||||||
<div className="text-xs opacity-70 mt-1">Payment Request</div>
|
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
|
||||||
</div>
|
How PayPal QR Codes Work
|
||||||
|
</h2>
|
||||||
<div className="w-48 h-48 bg-white rounded-xl p-2 shadow-inner relative overflow-hidden flex items-center justify-center">
|
|
||||||
<QRCodeSVG value="https://www.qrmaster.net" size={170} fgColor="#003087" level="Q" />
|
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
|
||||||
</div>
|
<article className="text-center">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-[#003087]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
{/* Floating Badge */}
|
<CreditCard className="w-6 h-6 text-[#003087]" />
|
||||||
<div className="absolute -bottom-6 -left-6 bg-white py-3 px-5 rounded-xl shadow-xl flex items-center gap-3 transform transition-transform hover:scale-105 duration-300">
|
</div>
|
||||||
<div className="bg-blue-100 p-2 rounded-full">
|
<h3 className="font-bold text-slate-900 mb-2">1. Username</h3>
|
||||||
<DollarSign className="w-5 h-5 text-[#003087]" />
|
<p className="text-slate-600 text-xs leading-relaxed">Enter your PayPal.me username.</p>
|
||||||
</div>
|
</article>
|
||||||
<div className="text-left">
|
|
||||||
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">PayPal</div>
|
<article className="text-center">
|
||||||
<div className="text-sm font-bold text-slate-900">Ready</div>
|
<div className="w-12 h-12 rounded-2xl bg-[#003087]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
</div>
|
<DollarSign className="w-6 h-6 text-[#003087]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<h3 className="font-bold text-slate-900 mb-2">2. Amount</h3>
|
||||||
</div>
|
<p className="text-slate-600 text-xs leading-relaxed">Optional: Set a fixed payment amount.</p>
|
||||||
</div>
|
</article>
|
||||||
</section>
|
|
||||||
|
<article className="text-center">
|
||||||
{/* GENERATOR SECTION */}
|
<div className="w-12 h-12 rounded-2xl bg-[#003087]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
<section className="py-12 px-4 sm:px-6 lg:px-8 -mt-8">
|
<Sparkles className="w-6 h-6 text-[#003087]" />
|
||||||
<PayPalGenerator />
|
</div>
|
||||||
</section>
|
<h3 className="font-bold text-slate-900 mb-2">3. Design</h3>
|
||||||
|
<p className="text-slate-600 text-xs leading-relaxed">Pick colors and add a frame.</p>
|
||||||
{/* HOW IT WORKS */}
|
</article>
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
|
|
||||||
<div className="max-w-4xl mx-auto">
|
<article className="text-center">
|
||||||
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
|
<div className="w-12 h-12 rounded-2xl bg-[#003087]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
How PayPal QR Codes Work
|
<Download className="w-6 h-6 text-[#003087]" />
|
||||||
</h2>
|
</div>
|
||||||
|
<h3 className="font-bold text-slate-900 mb-2">4. Download</h3>
|
||||||
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-8">
|
<p className="text-slate-600 text-xs leading-relaxed">Save as PNG or SVG file.</p>
|
||||||
<article className="text-center">
|
</article>
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#003087]/10 flex items-center justify-center mx-auto mb-4">
|
|
||||||
<CreditCard className="w-6 h-6 text-[#003087]" />
|
<article className="text-center">
|
||||||
</div>
|
<div className="w-12 h-12 rounded-2xl bg-[#003087]/10 flex items-center justify-center mx-auto mb-4">
|
||||||
<h3 className="font-bold text-slate-900 mb-2">1. Username</h3>
|
<Share2 className="w-6 h-6 text-[#003087]" />
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">Enter your PayPal.me username.</p>
|
</div>
|
||||||
</article>
|
<h3 className="font-bold text-slate-900 mb-2">5. Share</h3>
|
||||||
|
<p className="text-slate-600 text-xs leading-relaxed">Print or share to receive payments.</p>
|
||||||
<article className="text-center">
|
</article>
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#003087]/10 flex items-center justify-center mx-auto mb-4">
|
</div>
|
||||||
<DollarSign className="w-6 h-6 text-[#003087]" />
|
</div>
|
||||||
</div>
|
</section>
|
||||||
<h3 className="font-bold text-slate-900 mb-2">2. Amount</h3>
|
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">Optional: Set a fixed payment amount.</p>
|
{/* RELATED TOOLS */}
|
||||||
</article>
|
<RelatedTools />
|
||||||
|
|
||||||
<article className="text-center">
|
{/* FAQ SECTION */}
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#003087]/10 flex items-center justify-center mx-auto mb-4">
|
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
||||||
<Sparkles className="w-6 h-6 text-[#003087]" />
|
<div className="max-w-3xl mx-auto">
|
||||||
</div>
|
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
||||||
<h3 className="font-bold text-slate-900 mb-2">3. Design</h3>
|
Frequently Asked Questions
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">Pick colors and add a frame.</p>
|
</h2>
|
||||||
</article>
|
<p className="text-slate-600 text-center mb-10">
|
||||||
|
Common questions about PayPal QR codes.
|
||||||
<article className="text-center">
|
</p>
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#003087]/10 flex items-center justify-center mx-auto mb-4">
|
|
||||||
<Download className="w-6 h-6 text-[#003087]" />
|
<div className="space-y-4">
|
||||||
</div>
|
<FaqItem
|
||||||
<h3 className="font-bold text-slate-900 mb-2">4. Download</h3>
|
question="How does the PayPal QR code work?"
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">Save as PNG or SVG file.</p>
|
answer="When scanned, it opens the PayPal app or website with your PayPal.me link. If you set an amount, it will be pre-filled for the payer."
|
||||||
</article>
|
/>
|
||||||
|
<FaqItem
|
||||||
<article className="text-center">
|
question="Do I need a PayPal Business account?"
|
||||||
<div className="w-12 h-12 rounded-2xl bg-[#003087]/10 flex items-center justify-center mx-auto mb-4">
|
answer="No. Any PayPal account with a PayPal.me link can use this generator. Personal accounts work fine for tips and donations."
|
||||||
<Share2 className="w-6 h-6 text-[#003087]" />
|
/>
|
||||||
</div>
|
<FaqItem
|
||||||
<h3 className="font-bold text-slate-900 mb-2">5. Share</h3>
|
question="Is there a fee for using the QR code?"
|
||||||
<p className="text-slate-600 text-xs leading-relaxed">Print or share to receive payments.</p>
|
answer="This generator is 100% free. PayPal may charge their standard transaction fees when you receive payments."
|
||||||
</article>
|
/>
|
||||||
</div>
|
<FaqItem
|
||||||
</div>
|
question="Can I change the amount later?"
|
||||||
</section>
|
answer="No, this is a static QR code. The amount is encoded permanently. For variable amounts, leave the amount field empty."
|
||||||
|
/>
|
||||||
{/* FAQ SECTION */}
|
<FaqItem
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
question="What if I don't have a PayPal.me link?"
|
||||||
<div className="max-w-3xl mx-auto">
|
answer="You can create one for free in your PayPal account settings. Go to paypal.me to set up your personalized link."
|
||||||
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
|
/>
|
||||||
Frequently Asked Questions
|
</div>
|
||||||
</h2>
|
</div>
|
||||||
<p className="text-slate-600 text-center mb-10">
|
</section>
|
||||||
Common questions about PayPal QR codes.
|
|
||||||
</p>
|
</div>
|
||||||
|
</>
|
||||||
<div className="space-y-4">
|
);
|
||||||
<FaqItem
|
}
|
||||||
question="How does the PayPal QR code work?"
|
|
||||||
answer="When scanned, it opens the PayPal app or website with your PayPal.me link. If you set an amount, it will be pre-filled for the payer."
|
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
||||||
/>
|
return (
|
||||||
<FaqItem
|
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
question="Do I need a PayPal Business account?"
|
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
|
||||||
answer="No. Any PayPal account with a PayPal.me link can use this generator. Personal accounts work fine for tips and donations."
|
{question}
|
||||||
/>
|
<span className="transition group-open:rotate-180 text-slate-400">
|
||||||
<FaqItem
|
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
|
||||||
question="Is there a fee for using the QR code?"
|
<path d="M6 9l6 6 6-6" />
|
||||||
answer="This generator is 100% free. PayPal may charge their standard transaction fees when you receive payments."
|
</svg>
|
||||||
/>
|
</span>
|
||||||
<FaqItem
|
</summary>
|
||||||
question="Can I change the amount later?"
|
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
|
||||||
answer="No, this is a static QR code. The amount is encoded permanently. For variable amounts, leave the amount field empty."
|
{answer}
|
||||||
/>
|
</div>
|
||||||
<FaqItem
|
</details>
|
||||||
question="What if I don't have a PayPal.me link?"
|
);
|
||||||
answer="You can create one for free in your PayPal account settings. Go to paypal.me to set up your personalized link."
|
}
|
||||||
/>
|
|
||||||
</div>
|
function Sparkles({ className }: { className?: string }) {
|
||||||
</div>
|
return (
|
||||||
</section>
|
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
||||||
</div>
|
</svg>
|
||||||
</>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
|
||||||
return (
|
|
||||||
<details className="group bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
|
||||||
<summary className="flex items-center justify-between p-5 cursor-pointer list-none text-slate-900 font-semibold hover:bg-slate-50 transition-colors">
|
|
||||||
{question}
|
|
||||||
<span className="transition group-open:rotate-180 text-slate-400">
|
|
||||||
<svg fill="none" height="20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="20">
|
|
||||||
<path d="M6 9l6 6 6-6" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</summary>
|
|
||||||
<div className="text-slate-600 px-5 pb-5 pt-0 leading-relaxed text-sm">
|
|
||||||
{answer}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Sparkles({ className }: { className?: string }) {
|
|
||||||
return (
|
|
||||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||