Compare commits
1 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
05531cda3f |
|
|
@ -36,7 +36,7 @@ yarn-error.log*
|
|||
next-env.d.ts
|
||||
|
||||
# prisma
|
||||
|
||||
/prisma/migrations/
|
||||
|
||||
# docker
|
||||
docker-compose.override.yml
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
# 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.
|
||||
|
Before Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 95 KiB |
|
|
@ -1,89 +0,0 @@
|
|||
# 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. |
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"firecrawl": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"firecrawl-mcp"
|
||||
],
|
||||
"env": {
|
||||
"FIRECRAWL_API_KEY": "fc-268826f038ad4bf0a38c48690ba9c1fa"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,641 +0,0 @@
|
|||
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
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/** @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,16 +20,6 @@ const nextConfig = {
|
|||
pagesBufferLength: 2,
|
||||
},
|
||||
poweredByHeader: false,
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/blog/bulk-qr-codes-excel',
|
||||
destination: '/blog/bulk-qr-code-generator-excel',
|
||||
permanent: true,
|
||||
},
|
||||
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
# 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).*
|
||||
|
|
@ -6,10 +6,8 @@
|
|||
"scripts": {
|
||||
"dev": "next dev -p 3050",
|
||||
"build": "prisma generate && next build",
|
||||
"submit:indexnow": "tsx scripts/submit-indexnow.ts",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"indexnow": "tsx scripts/submit-indexnow.ts",
|
||||
"db:generate": "prisma generate",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:deploy": "prisma migrate deploy",
|
||||
|
|
@ -32,20 +30,17 @@
|
|||
"@prisma/client": "^5.7.0",
|
||||
"@stripe/stripe-js": "^8.0.0",
|
||||
"@types/d3-scale": "^4.0.9",
|
||||
"axios": "^1.13.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"chart.js": "^4.4.0",
|
||||
"clsx": "^2.0.0",
|
||||
"d3-scale": "^4.0.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"dotenv": "^17.2.3",
|
||||
"exceljs": "^4.4.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^12.24.10",
|
||||
"html-to-image": "^1.11.13",
|
||||
"i18next": "^23.7.6",
|
||||
"ioredis": "^5.3.2",
|
||||
"jspdf": "^4.0.0",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "^14.2.35",
|
||||
|
|
@ -62,6 +57,7 @@
|
|||
"react-i18next": "^13.5.0",
|
||||
"react-simple-maps": "^3.0.0",
|
||||
"resend": "^6.4.2",
|
||||
"sharp": "^0.33.1",
|
||||
"stripe": "^19.1.0",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"uuid": "^13.0.0",
|
||||
|
|
@ -82,7 +78,6 @@
|
|||
"postcss": "^8.4.32",
|
||||
"prettier": "^3.1.1",
|
||||
"prisma": "^5.7.0",
|
||||
"sharp": "^0.34.5",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.3"
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
|
|
@ -112,6 +112,10 @@ enum ContentType {
|
|||
SMS
|
||||
TEXT
|
||||
WHATSAPP
|
||||
PDF
|
||||
APP
|
||||
COUPON
|
||||
FEEDBACK
|
||||
}
|
||||
|
||||
enum QRStatus {
|
||||
|
|
@ -162,17 +166,3 @@ model NewsletterSubscription {
|
|||
@@index([email])
|
||||
@@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])
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
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
|
||||
|
|
@ -1 +0,0 @@
|
|||
bb6dfaacf1ed41a880281c426c54ed7c
|
||||
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 4.1 MiB |
|
Before Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 3.8 MiB |
|
Before Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 5.5 MiB |
|
After Width: | Height: | Size: 4.6 MiB |
|
After Width: | Height: | Size: 5.8 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
Before Width: | Height: | Size: 737 KiB |
|
Before Width: | Height: | Size: 350 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 320 KiB |
|
Before Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 726 KiB |
|
|
@ -1 +0,0 @@
|
|||
google-site-verification: googleccd5315437d68a49.html
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
/* 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
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
# 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
|
||||
|
Before Width: | Height: | Size: 464 KiB |
|
|
@ -0,0 +1,19 @@
|
|||
# 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/
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?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>
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
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();
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
|
||||
// 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);
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
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();
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
|
||||
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();
|
||||
|
|
@ -1,743 +0,0 @@
|
|||
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
|
|
@ -1,68 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,254 +0,0 @@
|
|||
'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>
|
||||
);
|
||||
}
|
||||
|
|
@ -14,6 +14,20 @@ import { calculateContrast, cn } from '@/lib/utils';
|
|||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useCsrf } from '@/hooks/useCsrf';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
import {
|
||||
Globe, User, MapPin, Phone, FileText, Smartphone, Ticket, Star, HelpCircle
|
||||
} from 'lucide-react';
|
||||
|
||||
// Tooltip component for form field help
|
||||
const Tooltip = ({ text }: { text: string }) => (
|
||||
<div className="group relative inline-block ml-1">
|
||||
<HelpCircle className="w-4 h-4 text-gray-400 cursor-help" />
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 text-white text-xs rounded-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50 w-48 text-center">
|
||||
{text}
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Content-type specific frame options
|
||||
const getFrameOptionsForContentType = (contentType: string) => {
|
||||
|
|
@ -34,6 +48,14 @@ const getFrameOptionsForContentType = (contentType: string) => {
|
|||
return [...baseOptions, { id: 'chatme', label: 'Chat Me' }, { id: 'whatsapp', label: 'WhatsApp' }];
|
||||
case 'TEXT':
|
||||
return [...baseOptions, { id: 'read', label: 'Read' }, { id: 'info', label: 'Info' }];
|
||||
case 'PDF':
|
||||
return [...baseOptions, { id: 'download', label: 'Download' }, { id: 'view', label: 'View PDF' }];
|
||||
case 'APP':
|
||||
return [...baseOptions, { id: 'getapp', label: 'Get App' }, { id: 'download', label: 'Download' }];
|
||||
case 'COUPON':
|
||||
return [...baseOptions, { id: 'redeem', label: 'Redeem' }, { id: 'save', label: 'Save Offer' }];
|
||||
case 'FEEDBACK':
|
||||
return [...baseOptions, { id: 'review', label: 'Review' }, { id: 'feedback', label: 'Feedback' }];
|
||||
default:
|
||||
return [...baseOptions, { id: 'website', label: 'Website' }, { id: 'visit', label: 'Visit' }];
|
||||
}
|
||||
|
|
@ -102,10 +124,14 @@ export default function CreatePage() {
|
|||
const hasGoodContrast = contrast >= 4.5;
|
||||
|
||||
const contentTypes = [
|
||||
{ value: 'URL', label: 'URL / Website' },
|
||||
{ value: 'VCARD', label: 'Contact Card' },
|
||||
{ value: 'GEO', label: 'Location/Maps' },
|
||||
{ value: 'PHONE', label: 'Phone Number' },
|
||||
{ value: 'URL', label: 'URL / Website', icon: Globe },
|
||||
{ value: 'VCARD', label: 'Contact Card', icon: User },
|
||||
{ value: 'GEO', label: 'Location / Maps', icon: MapPin },
|
||||
{ value: 'PHONE', label: 'Phone Number', icon: Phone },
|
||||
{ value: 'PDF', label: 'PDF / File', icon: FileText },
|
||||
{ value: 'APP', label: 'App Download', icon: Smartphone },
|
||||
{ value: 'COUPON', label: 'Coupon / Discount', icon: Ticket },
|
||||
{ value: 'FEEDBACK', label: 'Feedback / Review', icon: Star },
|
||||
];
|
||||
|
||||
// Get QR content based on content type
|
||||
|
|
@ -128,6 +154,14 @@ export default function CreatePage() {
|
|||
return content.text || 'Sample text';
|
||||
case 'WHATSAPP':
|
||||
return `https://wa.me/${content.phone || '+1234567890'}${content.message ? `?text=${encodeURIComponent(content.message)}` : ''}`;
|
||||
case 'PDF':
|
||||
return content.fileUrl || 'https://example.com/file.pdf';
|
||||
case 'APP':
|
||||
return content.fallbackUrl || content.iosUrl || content.androidUrl || 'https://example.com/app';
|
||||
case 'COUPON':
|
||||
return `Coupon: ${content.code || 'SAVE20'} - ${content.discount || '20% OFF'}`;
|
||||
case 'FEEDBACK':
|
||||
return content.feedbackUrl || 'https://example.com/feedback';
|
||||
default:
|
||||
return 'https://example.com';
|
||||
}
|
||||
|
|
@ -398,6 +432,139 @@ export default function CreatePage() {
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
case 'PDF':
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex items-center mb-1">
|
||||
<label className="block text-sm font-medium text-gray-700">PDF/File URL</label>
|
||||
<Tooltip text="Paste a public link to your PDF (Google Drive, Dropbox, etc.)" />
|
||||
</div>
|
||||
<Input
|
||||
value={content.fileUrl || ''}
|
||||
onChange={(e) => setContent({ ...content, fileUrl: e.target.value })}
|
||||
placeholder="https://drive.google.com/file/d/.../view"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label="File Name (optional)"
|
||||
value={content.fileName || ''}
|
||||
onChange={(e) => setContent({ ...content, fileName: e.target.value })}
|
||||
placeholder="Product Catalog 2026"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case 'APP':
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex items-center mb-1">
|
||||
<label className="block text-sm font-medium text-gray-700">iOS App Store URL</label>
|
||||
<Tooltip text="Link to your app in the Apple App Store" />
|
||||
</div>
|
||||
<Input
|
||||
value={content.iosUrl || ''}
|
||||
onChange={(e) => setContent({ ...content, iosUrl: e.target.value })}
|
||||
placeholder="https://apps.apple.com/app/..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center mb-1">
|
||||
<label className="block text-sm font-medium text-gray-700">Android Play Store URL</label>
|
||||
<Tooltip text="Link to your app in the Google Play Store" />
|
||||
</div>
|
||||
<Input
|
||||
value={content.androidUrl || ''}
|
||||
onChange={(e) => setContent({ ...content, androidUrl: e.target.value })}
|
||||
placeholder="https://play.google.com/store/apps/..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center mb-1">
|
||||
<label className="block text-sm font-medium text-gray-700">Fallback URL</label>
|
||||
<Tooltip text="Where desktop users go (e.g., your website). QR detects device automatically!" />
|
||||
</div>
|
||||
<Input
|
||||
value={content.fallbackUrl || ''}
|
||||
onChange={(e) => setContent({ ...content, fallbackUrl: e.target.value })}
|
||||
placeholder="https://yourapp.com"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
case 'COUPON':
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
label="Coupon Code"
|
||||
value={content.code || ''}
|
||||
onChange={(e) => setContent({ ...content, code: e.target.value })}
|
||||
placeholder="SUMMER20"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="Discount"
|
||||
value={content.discount || ''}
|
||||
onChange={(e) => setContent({ ...content, discount: e.target.value })}
|
||||
placeholder="20% OFF"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="Title"
|
||||
value={content.title || ''}
|
||||
onChange={(e) => setContent({ ...content, title: e.target.value })}
|
||||
placeholder="Summer Sale 2026"
|
||||
/>
|
||||
<Input
|
||||
label="Description (optional)"
|
||||
value={content.description || ''}
|
||||
onChange={(e) => setContent({ ...content, description: e.target.value })}
|
||||
placeholder="Valid on all products"
|
||||
/>
|
||||
<Input
|
||||
label="Expiry Date (optional)"
|
||||
type="date"
|
||||
value={content.expiryDate || ''}
|
||||
onChange={(e) => setContent({ ...content, expiryDate: e.target.value })}
|
||||
/>
|
||||
<Input
|
||||
label="Redeem URL (optional)"
|
||||
value={content.redeemUrl || ''}
|
||||
onChange={(e) => setContent({ ...content, redeemUrl: e.target.value })}
|
||||
placeholder="https://shop.example.com?coupon=SUMMER20"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case 'FEEDBACK':
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
label="Business Name"
|
||||
value={content.businessName || ''}
|
||||
onChange={(e) => setContent({ ...content, businessName: e.target.value })}
|
||||
placeholder="Your Restaurant Name"
|
||||
required
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center mb-1">
|
||||
<label className="block text-sm font-medium text-gray-700">Google Review URL</label>
|
||||
<Tooltip text="Redirect satisfied customers to leave a Google review." />
|
||||
</div>
|
||||
<Input
|
||||
value={content.googleReviewUrl || ''}
|
||||
onChange={(e) => setContent({ ...content, googleReviewUrl: e.target.value })}
|
||||
placeholder="https://search.google.com/local/writereview?placeid=..."
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label="Thank You Message"
|
||||
value={content.thankYouMessage || ''}
|
||||
onChange={(e) => setContent({ ...content, thankYouMessage: e.target.value })}
|
||||
placeholder="Thanks for your feedback!"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
@ -428,12 +595,31 @@ export default function CreatePage() {
|
|||
required
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Content Type"
|
||||
value={contentType}
|
||||
onChange={(e) => setContentType(e.target.value)}
|
||||
options={contentTypes}
|
||||
/>
|
||||
{/* Custom Content Type Selector with Icons */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Content Type</label>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
{contentTypes.map((type) => {
|
||||
const Icon = type.icon;
|
||||
return (
|
||||
<button
|
||||
key={type.value}
|
||||
type="button"
|
||||
onClick={() => setContentType(type.value)}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-2 p-3 rounded-lg border-2 transition-all text-sm",
|
||||
contentType === type.value
|
||||
? "border-primary-500 bg-primary-50 text-primary-700"
|
||||
: "border-gray-200 hover:border-gray-300 text-gray-600"
|
||||
)}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
<span className="text-xs font-medium text-center">{type.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{renderContentFields()}
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -1,38 +1,254 @@
|
|||
import type { Metadata } from 'next';
|
||||
import '@/styles/globals.css';
|
||||
import { Suspense } from 'react';
|
||||
import { Providers } from '@/components/Providers';
|
||||
import AppLayout from './AppLayout';
|
||||
'use client';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Dashboard | QR Master',
|
||||
description: 'Manage your QR Master dashboard. Create dynamic QR codes, view real-time scan analytics, and configure your account settings in one secure place.',
|
||||
robots: { index: false, follow: false },
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/favicon.svg', type: 'image/svg+xml' },
|
||||
{ url: '/logo.svg', type: 'image/svg+xml' },
|
||||
],
|
||||
apple: '/logo.svg',
|
||||
},
|
||||
};
|
||||
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';
|
||||
|
||||
export default function RootAppLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className="font-sans">
|
||||
<Providers>
|
||||
<Suspense fallback={null}>
|
||||
<AppLayout>
|
||||
{children}
|
||||
</AppLayout>
|
||||
</Suspense>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,9 +7,8 @@ import { Badge } from '@/components/ui/Badge';
|
|||
import { showToast } from '@/components/ui/Toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { BillingToggle } from '@/components/ui/BillingToggle';
|
||||
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
|
||||
|
||||
export default function PricingClient() {
|
||||
export default function PricingPage() {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState<string | null>(null);
|
||||
const [currentPlan, setCurrentPlan] = useState<string>('FREE');
|
||||
|
|
@ -183,9 +182,9 @@ export default function PricingClient() {
|
|||
return (
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
Choose Your Plan
|
||||
</h2>
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
Select the perfect plan for your QR code needs
|
||||
</p>
|
||||
|
|
@ -261,7 +260,7 @@ export default function PricingClient() {
|
|||
All plans include unlimited static QR codes and basic customization.
|
||||
</p>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Need help choosing? <ObfuscatedMailto email="support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</ObfuscatedMailto>
|
||||
Need help choosing? <a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -242,6 +242,114 @@ export default function EditQRPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{qrCode.contentType === 'PDF' && (
|
||||
<>
|
||||
<Input
|
||||
label="PDF/File URL"
|
||||
value={content.fileUrl || ''}
|
||||
onChange={(e) => setContent({ ...content, fileUrl: e.target.value })}
|
||||
placeholder="https://drive.google.com/file/d/.../view"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="File Name (optional)"
|
||||
value={content.fileName || ''}
|
||||
onChange={(e) => setContent({ ...content, fileName: e.target.value })}
|
||||
placeholder="Product Catalog 2026"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{qrCode.contentType === 'APP' && (
|
||||
<>
|
||||
<Input
|
||||
label="iOS App Store URL"
|
||||
value={content.iosUrl || ''}
|
||||
onChange={(e) => setContent({ ...content, iosUrl: e.target.value })}
|
||||
placeholder="https://apps.apple.com/app/..."
|
||||
/>
|
||||
<Input
|
||||
label="Android Play Store URL"
|
||||
value={content.androidUrl || ''}
|
||||
onChange={(e) => setContent({ ...content, androidUrl: e.target.value })}
|
||||
placeholder="https://play.google.com/store/apps/..."
|
||||
/>
|
||||
<Input
|
||||
label="Fallback URL (Desktop)"
|
||||
value={content.fallbackUrl || ''}
|
||||
onChange={(e) => setContent({ ...content, fallbackUrl: e.target.value })}
|
||||
placeholder="https://yourapp.com"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{qrCode.contentType === 'COUPON' && (
|
||||
<>
|
||||
<Input
|
||||
label="Coupon Code"
|
||||
value={content.code || ''}
|
||||
onChange={(e) => setContent({ ...content, code: e.target.value })}
|
||||
placeholder="SUMMER20"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="Discount"
|
||||
value={content.discount || ''}
|
||||
onChange={(e) => setContent({ ...content, discount: e.target.value })}
|
||||
placeholder="20% OFF"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="Title"
|
||||
value={content.title || ''}
|
||||
onChange={(e) => setContent({ ...content, title: e.target.value })}
|
||||
placeholder="Summer Sale 2026"
|
||||
/>
|
||||
<Input
|
||||
label="Description (optional)"
|
||||
value={content.description || ''}
|
||||
onChange={(e) => setContent({ ...content, description: e.target.value })}
|
||||
placeholder="Valid on all products"
|
||||
/>
|
||||
<Input
|
||||
label="Expiry Date (optional)"
|
||||
type="date"
|
||||
value={content.expiryDate || ''}
|
||||
onChange={(e) => setContent({ ...content, expiryDate: e.target.value })}
|
||||
/>
|
||||
<Input
|
||||
label="Redeem URL (optional)"
|
||||
value={content.redeemUrl || ''}
|
||||
onChange={(e) => setContent({ ...content, redeemUrl: e.target.value })}
|
||||
placeholder="https://shop.example.com"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{qrCode.contentType === 'FEEDBACK' && (
|
||||
<>
|
||||
<Input
|
||||
label="Business Name"
|
||||
value={content.businessName || ''}
|
||||
onChange={(e) => setContent({ ...content, businessName: e.target.value })}
|
||||
placeholder="Your Restaurant Name"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="Google Review URL (optional)"
|
||||
value={content.googleReviewUrl || ''}
|
||||
onChange={(e) => setContent({ ...content, googleReviewUrl: e.target.value })}
|
||||
placeholder="https://search.google.com/local/writereview?placeid=..."
|
||||
/>
|
||||
<Input
|
||||
label="Thank You Message"
|
||||
value={content.thankYouMessage || ''}
|
||||
onChange={(e) => setContent({ ...content, thankYouMessage: e.target.value })}
|
||||
placeholder="Thanks for your feedback!"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-4 pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Star, ArrowLeft, ChevronLeft, ChevronRight, MessageSquare } from 'lucide-react';
|
||||
|
||||
interface Feedback {
|
||||
id: string;
|
||||
rating: number;
|
||||
comment: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
interface FeedbackStats {
|
||||
total: number;
|
||||
avgRating: number;
|
||||
distribution: { [key: number]: number };
|
||||
}
|
||||
|
||||
interface Pagination {
|
||||
page: number;
|
||||
totalPages: number;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
export default function FeedbackListPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const qrId = params.id as string;
|
||||
|
||||
const [feedbacks, setFeedbacks] = useState<Feedback[]>([]);
|
||||
const [stats, setStats] = useState<FeedbackStats | null>(null);
|
||||
const [pagination, setPagination] = useState<Pagination>({ page: 1, totalPages: 1, hasMore: false });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFeedback(currentPage);
|
||||
}, [qrId, currentPage]);
|
||||
|
||||
const fetchFeedback = async (page: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/qrs/${qrId}/feedback?page=${page}&limit=20`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setFeedbacks(data.feedbacks);
|
||||
setStats(data.stats);
|
||||
setPagination(data.pagination);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching feedback:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderStars = (rating: number) => (
|
||||
<div className="flex gap-0.5">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<Star
|
||||
key={star}
|
||||
className={`w-4 h-4 ${star <= rating ? 'text-amber-400 fill-amber-400' : 'text-gray-200'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (loading && !stats) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<Link href={`/qr/${qrId}`} className="inline-flex items-center text-gray-500 hover:text-gray-700 mb-4">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to QR Code
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Customer Feedback</h1>
|
||||
<p className="text-gray-600 mt-1">{stats?.total || 0} total responses</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Overview */}
|
||||
{stats && (
|
||||
<Card className="mb-8">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-8">
|
||||
{/* Average Rating */}
|
||||
<div className="text-center md:text-left">
|
||||
<div className="text-5xl font-bold text-gray-900 mb-1">{stats.avgRating}</div>
|
||||
<div className="flex justify-center md:justify-start mb-1">
|
||||
{renderStars(Math.round(stats.avgRating))}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">{stats.total} reviews</p>
|
||||
</div>
|
||||
|
||||
{/* Distribution */}
|
||||
<div className="flex-1 space-y-2">
|
||||
{[5, 4, 3, 2, 1].map((rating) => {
|
||||
const count = stats.distribution[rating] || 0;
|
||||
const percentage = stats.total > 0 ? (count / stats.total) * 100 : 0;
|
||||
return (
|
||||
<div key={rating} className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-600 w-12">{rating} stars</span>
|
||||
<div className="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-amber-400 rounded-full transition-all"
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500 w-12 text-right">{count}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Feedback List */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
All Reviews
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{feedbacks.length === 0 ? (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
<Star className="w-12 h-12 mx-auto mb-4 text-gray-300" />
|
||||
<p>No feedback received yet</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-100">
|
||||
{feedbacks.map((feedback) => (
|
||||
<div key={feedback.id} className="py-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
{renderStars(feedback.rating)}
|
||||
<span className="text-sm text-gray-400">
|
||||
{new Date(feedback.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
{feedback.comment && (
|
||||
<p className="text-gray-700">{feedback.comment}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{pagination.totalPages > 1 && (
|
||||
<div className="flex items-center justify-between mt-6 pt-6 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4 mr-1" />
|
||||
Previous
|
||||
</Button>
|
||||
<span className="text-sm text-gray-500">
|
||||
Page {currentPage} of {pagination.totalPages}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage((p) => p + 1)}
|
||||
disabled={!pagination.hasMore}
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="w-4 h-4 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,287 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import {
|
||||
ArrowLeft, Edit, ExternalLink, Star, MessageSquare,
|
||||
BarChart3, Copy, Check, Pause, Play
|
||||
} from 'lucide-react';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
import { useCsrf } from '@/hooks/useCsrf';
|
||||
|
||||
interface QRCode {
|
||||
id: string;
|
||||
title: string;
|
||||
type: 'STATIC' | 'DYNAMIC';
|
||||
contentType: string;
|
||||
content: any;
|
||||
slug: string;
|
||||
status: 'ACTIVE' | 'PAUSED';
|
||||
style: any;
|
||||
createdAt: string;
|
||||
_count?: { scans: number };
|
||||
}
|
||||
|
||||
interface FeedbackStats {
|
||||
total: number;
|
||||
avgRating: number;
|
||||
distribution: { [key: number]: number };
|
||||
}
|
||||
|
||||
export default function QRDetailPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const qrId = params.id as string;
|
||||
const { fetchWithCsrf } = useCsrf();
|
||||
|
||||
const [qrCode, setQrCode] = useState<QRCode | null>(null);
|
||||
const [feedbackStats, setFeedbackStats] = useState<FeedbackStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchQRCode();
|
||||
}, [qrId]);
|
||||
|
||||
const fetchQRCode = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/qrs/${qrId}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setQrCode(data);
|
||||
|
||||
// Fetch feedback stats if it's a feedback QR
|
||||
if (data.contentType === 'FEEDBACK') {
|
||||
const feedbackRes = await fetch(`/api/qrs/${qrId}/feedback?limit=1`);
|
||||
if (feedbackRes.ok) {
|
||||
const feedbackData = await feedbackRes.json();
|
||||
setFeedbackStats(feedbackData.stats);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showToast('QR code not found', 'error');
|
||||
router.push('/dashboard');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching QR code:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const copyLink = async () => {
|
||||
const url = `${window.location.origin}/r/${qrCode?.slug}`;
|
||||
await navigator.clipboard.writeText(url);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
showToast('Link copied!', 'success');
|
||||
};
|
||||
|
||||
const toggleStatus = async () => {
|
||||
if (!qrCode) return;
|
||||
const newStatus = qrCode.status === 'ACTIVE' ? 'PAUSED' : 'ACTIVE';
|
||||
|
||||
try {
|
||||
const res = await fetchWithCsrf(`/api/qrs/${qrId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ status: newStatus }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setQrCode({ ...qrCode, status: newStatus });
|
||||
showToast(`QR code ${newStatus === 'ACTIVE' ? 'activated' : 'paused'}`, 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Failed to update status', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const renderStars = (rating: number) => (
|
||||
<div className="flex gap-0.5">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<Star
|
||||
key={star}
|
||||
className={`w-4 h-4 ${star <= rating ? 'text-amber-400 fill-amber-400' : 'text-gray-200'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!qrCode) return null;
|
||||
|
||||
const qrUrl = `${typeof window !== 'undefined' ? window.location.origin : ''}/r/${qrCode.slug}`;
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<Link href="/dashboard" className="inline-flex items-center text-gray-500 hover:text-gray-700 mb-4">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to Dashboard
|
||||
</Link>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">{qrCode.title}</h1>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Badge variant={qrCode.type === 'DYNAMIC' ? 'info' : 'default'}>
|
||||
{qrCode.type}
|
||||
</Badge>
|
||||
<Badge variant={qrCode.status === 'ACTIVE' ? 'success' : 'warning'}>
|
||||
{qrCode.status}
|
||||
</Badge>
|
||||
<Badge>{qrCode.contentType}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{qrCode.type === 'DYNAMIC' && (
|
||||
<>
|
||||
<Button variant="outline" size="sm" onClick={toggleStatus}>
|
||||
{qrCode.status === 'ACTIVE' ? <Pause className="w-4 h-4 mr-1" /> : <Play className="w-4 h-4 mr-1" />}
|
||||
{qrCode.status === 'ACTIVE' ? 'Pause' : 'Activate'}
|
||||
</Button>
|
||||
<Link href={`/qr/${qrId}/edit`}>
|
||||
<Button variant="outline" size="sm">
|
||||
<Edit className="w-4 h-4 mr-1" /> Edit
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-3 gap-8">
|
||||
{/* Left: QR Code */}
|
||||
<div>
|
||||
<Card>
|
||||
<CardContent className="p-6 flex flex-col items-center">
|
||||
<div className="bg-white p-4 rounded-xl shadow-sm mb-4">
|
||||
<QRCodeSVG
|
||||
value={qrUrl}
|
||||
size={200}
|
||||
fgColor={qrCode.style?.foregroundColor || '#000000'}
|
||||
bgColor={qrCode.style?.backgroundColor || '#FFFFFF'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-2">
|
||||
<Button variant="outline" className="w-full" onClick={copyLink}>
|
||||
{copied ? <Check className="w-4 h-4 mr-2" /> : <Copy className="w-4 h-4 mr-2" />}
|
||||
{copied ? 'Copied!' : 'Copy Link'}
|
||||
</Button>
|
||||
<a href={qrUrl} target="_blank" rel="noopener noreferrer" className="block">
|
||||
<Button variant="outline" className="w-full">
|
||||
<ExternalLink className="w-4 h-4 mr-2" /> Open Link
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Right: Stats & Info */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Quick Stats */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
||||
<Card>
|
||||
<CardContent className="p-4 text-center">
|
||||
<BarChart3 className="w-6 h-6 mx-auto mb-2 text-indigo-500" />
|
||||
<p className="text-2xl font-bold text-gray-900">{qrCode._count?.scans || 0}</p>
|
||||
<p className="text-sm text-gray-500">Total Scans</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4 text-center">
|
||||
<p className="text-2xl font-bold text-gray-900">{qrCode.type}</p>
|
||||
<p className="text-sm text-gray-500">QR Type</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4 text-center">
|
||||
<p className="text-2xl font-bold text-gray-900">
|
||||
{new Date(qrCode.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">Created</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Feedback Summary (only for FEEDBACK type) */}
|
||||
{qrCode.contentType === 'FEEDBACK' && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Star className="w-5 h-5 text-amber-400" />
|
||||
Customer Feedback
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{feedbackStats && feedbackStats.total > 0 ? (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-6 mb-4">
|
||||
{/* Average */}
|
||||
<div className="text-center sm:text-left">
|
||||
<div className="text-4xl font-bold text-gray-900">{feedbackStats.avgRating}</div>
|
||||
{renderStars(Math.round(feedbackStats.avgRating))}
|
||||
<p className="text-sm text-gray-500 mt-1">{feedbackStats.total} reviews</p>
|
||||
</div>
|
||||
|
||||
{/* Distribution */}
|
||||
<div className="flex-1 space-y-1">
|
||||
{[5, 4, 3, 2, 1].map((rating) => {
|
||||
const count = feedbackStats.distribution[rating] || 0;
|
||||
const pct = feedbackStats.total > 0 ? (count / feedbackStats.total) * 100 : 0;
|
||||
return (
|
||||
<div key={rating} className="flex items-center gap-2 text-sm">
|
||||
<span className="w-8 text-gray-500">{rating}★</span>
|
||||
<div className="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-amber-400 rounded-full" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<span className="w-8 text-gray-400 text-right">{count}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500 mb-4">No feedback received yet. Share your QR code to collect reviews!</p>
|
||||
)}
|
||||
|
||||
<Link href={`/qr/${qrId}/feedback`} className="block">
|
||||
<Button variant="outline" className="w-full">
|
||||
<MessageSquare className="w-4 h-4 mr-2" />
|
||||
View All Feedback
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Content Info */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Content Details</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="bg-gray-50 p-4 rounded-lg text-sm overflow-auto">
|
||||
{JSON.stringify(qrCode.content, null, 2)}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,38 +1,11 @@
|
|||
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({
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className="font-sans">
|
||||
<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>
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
'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,38 +1,76 @@
|
|||
import React, { Suspense } from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import LoginClientPage from './ClientPage';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
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'],
|
||||
},
|
||||
};
|
||||
|
||||
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 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 (
|
||||
<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">
|
||||
|
|
@ -48,13 +86,94 @@ export default function LoginPage() {
|
|||
</Link>
|
||||
</div>
|
||||
|
||||
<Suspense fallback={
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-8 flex items-center justify-center min-h-[400px]">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
||||
</div>
|
||||
}>
|
||||
<LoginClientPage />
|
||||
</Suspense>
|
||||
<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>
|
||||
|
||||
<p className="text-center text-sm text-gray-500 mt-6">
|
||||
By signing in, you agree to our{' '}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ import { Input } from '@/components/ui/Input';
|
|||
import { Button } from '@/components/ui/Button';
|
||||
import { useCsrf } from '@/hooks/useCsrf';
|
||||
|
||||
import { Suspense } from 'react';
|
||||
|
||||
function ResetPasswordContent() {
|
||||
export default function ResetPasswordPage() {
|
||||
const { fetchWithCsrf, loading: csrfLoading } = useCsrf();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
|
|
@ -208,11 +206,3 @@ function ResetPasswordContent() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
return (
|
||||
<Suspense fallback={<div className="min-h-screen flex items-center justify-center">Loading...</div>}>
|
||||
<ResetPasswordContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,185 +0,0 @@
|
|||
'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,39 +1,89 @@
|
|||
import React, { Suspense } from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import SignupClientPage from './ClientPage';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
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'],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
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 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 (
|
||||
<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">
|
||||
|
|
@ -49,13 +99,102 @@ export default function SignupPage() {
|
|||
</Link>
|
||||
</div>
|
||||
|
||||
<Suspense fallback={
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-8 flex items-center justify-center min-h-[500px]">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
||||
</div>
|
||||
}>
|
||||
<SignupClientPage />
|
||||
</Suspense>
|
||||
<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>
|
||||
|
||||
<p className="text-center text-sm text-gray-500 mt-6">
|
||||
By signing up, you agree to our{' '}
|
||||
|
|
|
|||
|
|
@ -1,287 +0,0 @@
|
|||
'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,7 +7,6 @@ import { websiteSchema, breadcrumbSchema } from '@/lib/schema';
|
|||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
|
||||
import { blogPostList } from '@/lib/blog-data';
|
||||
|
||||
function truncateAtWord(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) return text;
|
||||
|
|
@ -19,7 +18,7 @@ function truncateAtWord(text: string, maxLength: number): string {
|
|||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const title = truncateAtWord('QR Insights: Latest QR Strategies', 60);
|
||||
const description = truncateAtWord(
|
||||
'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.',
|
||||
'Expert guides on QR analytics, dynamic codes & smart marketing uses.',
|
||||
160
|
||||
);
|
||||
|
||||
|
|
@ -38,14 +37,6 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||
description,
|
||||
url: 'https://www.qrmaster.net/blog',
|
||||
type: 'website',
|
||||
images: [
|
||||
{
|
||||
url: 'https://www.qrmaster.net/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'QR Insights - QR Code Marketing & Analytics Blog',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
|
|
@ -54,7 +45,82 @@ 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() {
|
||||
const breadcrumbItems: BreadcrumbItem[] = [
|
||||
|
|
@ -79,8 +145,8 @@ export default function BlogPage() {
|
|||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto">
|
||||
{blogPostList.map((post: any) => (
|
||||
<Link key={post.slug} href={post.link || `/blog/${post.slug}`}>
|
||||
{blogPosts.map((post) => (
|
||||
<Link key={post.slug} href={`/blog/${post.slug}`}>
|
||||
<Card hover className="h-full overflow-hidden shadow-md hover:shadow-xl transition-all duration-300">
|
||||
<div className="relative h-56 overflow-hidden">
|
||||
<Image
|
||||
|
|
@ -102,9 +168,7 @@ export default function BlogPage() {
|
|||
<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">
|
||||
<p className="text-sm text-gray-500">{post.date}</p>
|
||||
<span className="text-primary-600 text-sm font-medium">
|
||||
{post.link ? 'Try Now →' : 'Read Article →'}
|
||||
</span>
|
||||
<span className="text-primary-600 text-sm font-medium">Read more →</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
|
|||
import { breadcrumbSchema } from '@/lib/schema';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Bulk QR Code Generator | Create from Excel | QR Master',
|
||||
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.',
|
||||
title: 'Bulk QR Code Generator - Create 1000s of QR Codes 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.',
|
||||
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: {
|
||||
canonical: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
||||
|
|
@ -23,14 +23,6 @@ export const metadata: Metadata = {
|
|||
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',
|
||||
type: 'website',
|
||||
images: [
|
||||
{
|
||||
url: 'https://www.qrmaster.net/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'Bulk QR Code Generator - QR Master',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel',
|
||||
|
|
@ -54,7 +46,7 @@ export default function BulkQRCodeGeneratorPage() {
|
|||
title: 'Contact Cards',
|
||||
description: 'Create vCard QR codes with contact information',
|
||||
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',
|
||||
|
|
@ -341,7 +333,7 @@ export default function BulkQRCodeGeneratorPage() {
|
|||
Start Bulk Generation
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/signup">
|
||||
<Link href="/create">
|
||||
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
|
||||
Try Single QR First
|
||||
</Button>
|
||||
|
|
@ -448,7 +440,7 @@ export default function BulkQRCodeGeneratorPage() {
|
|||
<tr className="border-b border-gray-200">
|
||||
<td className="py-2 px-3">John Doe</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>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-200">
|
||||
|
|
@ -641,35 +633,26 @@ Product C,https://example.com/product-c,Budget Widget,electronics,sale`}
|
|||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
{/* CTA Section */}
|
||||
<section className="py-24 bg-slate-900 relative overflow-hidden">
|
||||
{/* Background Decorations */}
|
||||
<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>
|
||||
<section className="py-20 bg-gradient-to-r from-green-600 to-blue-600 text-white">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center">
|
||||
<h2 className="text-4xl font-bold mb-6">
|
||||
Generate 1000s of QR Codes in Minutes
|
||||
</h2>
|
||||
<p className="text-xl mb-10 text-slate-300 leading-relaxed max-w-2xl mx-auto">
|
||||
Stop doing it manually. Upload your Excel file and get your QR codes in seconds. Professional, branded, and trackable.
|
||||
<p className="text-xl mb-8 text-green-100">
|
||||
Save hours of manual work. Upload your file and get all QR codes ready instantly.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-5 justify-center">
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link href="/signup">
|
||||
<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">
|
||||
<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">
|
||||
Start Bulk Generation
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/pricing">
|
||||
<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">
|
||||
<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">
|
||||
View Pricing
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<p className="mt-8 text-sm text-slate-500">
|
||||
No credit card required for free trial.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
|
|||
import { breadcrumbSchema } from '@/lib/schema';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Dynamic QR Code Generator | Edit & Track QR | QR Master',
|
||||
description: 'Create editable dynamic QR codes. Update destination URLs, track scans, and manage content anytime without reprinting. Free generator with analytics.',
|
||||
title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | 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.',
|
||||
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: {
|
||||
canonical: 'https://www.qrmaster.net/dynamic-qr-code-generator',
|
||||
|
|
@ -23,14 +23,6 @@ export const metadata: Metadata = {
|
|||
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',
|
||||
type: 'website',
|
||||
images: [
|
||||
{
|
||||
url: 'https://www.qrmaster.net/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'Dynamic QR Code Generator - QR Master',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master',
|
||||
|
|
@ -188,7 +180,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
|||
position: 2,
|
||||
name: 'Generate QR Code',
|
||||
text: 'Enter your destination URL and customize the design with your branding',
|
||||
url: 'https://www.qrmaster.net/signup',
|
||||
url: 'https://www.qrmaster.net/create',
|
||||
},
|
||||
{
|
||||
'@type': 'HowToStep',
|
||||
|
|
@ -512,7 +504,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
|||
Get Started Free
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/signup">
|
||||
<Link href="/create">
|
||||
<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
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
'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,7 +3,6 @@ import type { Metadata } from 'next';
|
|||
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||
import { faqPageSchema } from '@/lib/schema';
|
||||
import { Card, CardContent } from '@/components/ui/Card';
|
||||
import { ContactSupport } from './ContactSupport';
|
||||
|
||||
function truncateAtWord(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) return text;
|
||||
|
|
@ -15,7 +14,7 @@ function truncateAtWord(text: string, maxLength: number): string {
|
|||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const title = truncateAtWord('QR Master FAQ: Dynamic & Bulk QR', 60);
|
||||
const description = truncateAtWord(
|
||||
'Find answers about dynamic QR codes, scan tracking, security, bulk generation, and event QR codes. Everything you need to know about QR Master features.',
|
||||
'All answers: dynamic QR, security, analytics, bulk, events & print.',
|
||||
160
|
||||
);
|
||||
|
||||
|
|
@ -34,14 +33,6 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||
description,
|
||||
url: 'https://www.qrmaster.net/faq',
|
||||
type: 'website',
|
||||
images: [
|
||||
{
|
||||
url: 'https://www.qrmaster.net/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'QR Master FAQ',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
|
|
@ -132,7 +123,18 @@ export default function FAQPage() {
|
|||
))}
|
||||
</div>
|
||||
|
||||
<ContactSupport />
|
||||
<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{' '}
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,76 +1,248 @@
|
|||
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';
|
||||
'use client';
|
||||
|
||||
const isIndexable = process.env.NEXT_PUBLIC_INDEXABLE === 'true';
|
||||
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 const metadata: Metadata = {
|
||||
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,
|
||||
export default function MarketingLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema()) }}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema()) }}
|
||||
/>
|
||||
</head>
|
||||
<body className="font-sans">
|
||||
<Providers>
|
||||
<MarketingLayout>
|
||||
{children}
|
||||
</MarketingLayout>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
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]);
|
||||
|
||||
// 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>
|
||||
<Link href="/#faq" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
|
||||
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>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>
|
||||
<Link href="/#faq" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>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">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 >
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,754 +0,0 @@
|
|||
'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,19 +1,643 @@
|
|||
import React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import NewsletterClient from './NewsletterClient';
|
||||
'use client';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Newsletter Admin | QR Master',
|
||||
description: 'Administrative access for QR Master newsletter management. This area is restricted to authorized personnel only.',
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/newsletter',
|
||||
},
|
||||
};
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
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,
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function NewsletterPage() {
|
||||
return <NewsletterClient />;
|
||||
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 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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import type { Metadata } from 'next';
|
|||
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||
import { organizationSchema, websiteSchema } from '@/lib/schema';
|
||||
import HomePageClient from '@/components/marketing/HomePageClient';
|
||||
import { generateFaqSchema } from '@/lib/schema-utils';
|
||||
import en from '@/i18n/en.json'; // Import English translations for schema generation
|
||||
|
||||
function truncateAtWord(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) return text;
|
||||
|
|
@ -16,7 +14,7 @@ function truncateAtWord(text: string, maxLength: number): string {
|
|||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const title = truncateAtWord('QR Master: Dynamic QR Generator', 60);
|
||||
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.',
|
||||
'Dynamic QR, branding, bulk generation & analytics for all campaigns.',
|
||||
160
|
||||
);
|
||||
|
||||
|
|
@ -28,7 +26,6 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||
languages: {
|
||||
'x-default': 'https://www.qrmaster.net/',
|
||||
en: 'https://www.qrmaster.net/',
|
||||
de: 'https://www.qrmaster.net/qr-code-erstellen',
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
|
|
@ -36,19 +33,10 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||
description,
|
||||
url: 'https://www.qrmaster.net/',
|
||||
type: 'website',
|
||||
images: [
|
||||
{
|
||||
url: 'https://www.qrmaster.net/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'QR Master - Dynamic QR Code Generator and Analytics Platform',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
images: ['https://www.qrmaster.net/og-image.png'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -56,13 +44,11 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<SeoJsonLd data={[organizationSchema(), websiteSchema(), generateFaqSchema(en.faq.questions)]} />
|
||||
|
||||
{/* Server-rendered H1 for SEO - visually hidden but crawlable */}
|
||||
<h1 className="sr-only">QR Master: Dynamic QR Code Generator with Analytics</h1>
|
||||
<SeoJsonLd data={[organizationSchema(), websiteSchema()]} />
|
||||
|
||||
{/* Server-rendered SEO content for crawlers */}
|
||||
<div className="sr-only" aria-hidden="false">
|
||||
<h1>QR Master: Free Dynamic QR Code Generator with Tracking & Analytics</h1>
|
||||
<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.
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
'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,27 +1,9 @@
|
|||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { PrivacyEmailLink } from './PrivacyEmailLink';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Privacy Policy | 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
description: 'Privacy Policy and data protection information for QR Master',
|
||||
};
|
||||
|
||||
export default function PrivacyPage() {
|
||||
|
|
@ -111,7 +93,9 @@ export default function PrivacyPage() {
|
|||
</ul>
|
||||
<p className="text-gray-700 mb-4">
|
||||
To exercise these rights, contact us at{' '}
|
||||
<PrivacyEmailLink />
|
||||
<a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700">
|
||||
support@qrmaster.net
|
||||
</a>
|
||||
</p>
|
||||
<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,
|
||||
|
|
@ -127,7 +111,9 @@ export default function PrivacyPage() {
|
|||
<div className="bg-gray-50 p-6 rounded-lg">
|
||||
<p className="text-gray-700 mb-2">
|
||||
<strong>Email:</strong>{' '}
|
||||
<PrivacyEmailLink />
|
||||
<a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700">
|
||||
support@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>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
|
|||
import { breadcrumbSchema } from '@/lib/schema';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'QR Code Tracking & Analytics - Track Scans | QR Master',
|
||||
title: 'QR Code Tracking & Analytics - Track Every Scan | 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.',
|
||||
keywords: 'qr code tracking, qr code analytics, track qr scans, qr code statistics, free qr tracking, qr code monitoring',
|
||||
alternates: {
|
||||
|
|
@ -19,21 +19,13 @@ export const metadata: Metadata = {
|
|||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: 'QR Code Tracking & Analytics - Track Scans | QR Master',
|
||||
title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master',
|
||||
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.',
|
||||
url: 'https://www.qrmaster.net/qr-code-tracking',
|
||||
type: 'website',
|
||||
images: [
|
||||
{
|
||||
url: 'https://www.qrmaster.net/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'QR Code Tracking & Analytics - QR Master',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
title: 'QR Code Tracking & Analytics - Track Scans | QR Master',
|
||||
title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master',
|
||||
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.',
|
||||
},
|
||||
};
|
||||
|
|
@ -162,7 +154,7 @@ export default function QRCodeTrackingPage() {
|
|||
position: 3,
|
||||
name: 'Monitor Analytics',
|
||||
text: 'View real-time scan data including location, device, and time patterns in your dashboard',
|
||||
url: 'https://www.qrmaster.net/signup',
|
||||
url: 'https://www.qrmaster.net/analytics',
|
||||
},
|
||||
{
|
||||
'@type': 'HowToStep',
|
||||
|
|
@ -207,7 +199,7 @@ export default function QRCodeTrackingPage() {
|
|||
Start Tracking Free
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/signup">
|
||||
<Link href="/create">
|
||||
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
|
||||
Create Trackable QR Code
|
||||
</Button>
|
||||
|
|
@ -378,35 +370,26 @@ export default function QRCodeTrackingPage() {
|
|||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
{/* CTA Section */}
|
||||
<section className="py-24 bg-slate-900 relative overflow-hidden">
|
||||
{/* Background Decorations */}
|
||||
<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>
|
||||
<section className="py-20 bg-gradient-to-r from-primary-600 to-purple-600 text-white">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center">
|
||||
<h2 className="text-4xl font-bold mb-6">
|
||||
Start Tracking Your QR Codes Today
|
||||
</h2>
|
||||
<p className="text-xl mb-10 text-slate-300 leading-relaxed max-w-2xl mx-auto">
|
||||
Join thousands of businesses using QR Master to optimize their campaigns with real-time analytics.
|
||||
<p className="text-xl mb-8 text-primary-100">
|
||||
Join thousands of businesses using QR Master to track and optimize their QR code campaigns
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-5 justify-center">
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link href="/signup">
|
||||
<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">
|
||||
<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">
|
||||
Create Free Account
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/pricing">
|
||||
<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">
|
||||
<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">
|
||||
View Pricing
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<p className="mt-8 text-sm text-slate-500">
|
||||
Full analytics accessible on free plan.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,117 +0,0 @@
|
|||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -14,7 +14,6 @@ import { Button } from '@/components/ui/Button';
|
|||
import { Input } from '@/components/ui/Input';
|
||||
import { Select } from '@/components/ui/Select';
|
||||
import { cn } from '@/lib/utils';
|
||||
import AdBanner from '@/components/ads/AdBanner';
|
||||
|
||||
// Brand Colors
|
||||
const BRAND = {
|
||||
|
|
@ -138,7 +137,7 @@ export default function CryptoGenerator() {
|
|||
<div className="grid lg:grid-cols-2">
|
||||
|
||||
{/* LEFT: Input Section */}
|
||||
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
|
||||
{/* Crypto Details */}
|
||||
<div className="space-y-6">
|
||||
|
|
@ -159,7 +158,6 @@ export default function CryptoGenerator() {
|
|||
if (col) setQrColor(col);
|
||||
}}
|
||||
className="h-12 w-full rounded-xl border-slate-200"
|
||||
aria-label="Currency"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -211,7 +209,7 @@ export default function CryptoGenerator() {
|
|||
Wallet Direct
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-slate-600 mt-2">
|
||||
<p className="text-xs text-slate-500 mt-2">
|
||||
{qrMode === 'universal'
|
||||
? "Works with any phone camera. Opens blockchain explorer."
|
||||
: "Requires scanning from a wallet app. Enables direct payment."}
|
||||
|
|
@ -253,7 +251,7 @@ export default function CryptoGenerator() {
|
|||
{/* Frame Selector */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{FRAME_OPTIONS.map((frame) => (
|
||||
<button
|
||||
key={frame.id}
|
||||
|
|
@ -274,12 +272,13 @@ export default function CryptoGenerator() {
|
|||
</div>
|
||||
|
||||
{/* RIGHT: Preview Section */}
|
||||
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
|
||||
{/* QR Card with Frame */}
|
||||
<div
|
||||
ref={qrRef}
|
||||
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
|
||||
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
|
||||
style={{ minWidth: '320px' }}
|
||||
>
|
||||
{/* Frame Label */}
|
||||
{getFrameLabel() && (
|
||||
|
|
@ -321,7 +320,7 @@ export default function CryptoGenerator() {
|
|||
<Bitcoin className="w-4 h-4 text-slate-400 shrink-0" />
|
||||
<span className="truncate capitalize">{currency}</span>
|
||||
</h3>
|
||||
<div className="text-xs text-slate-600 mt-1 truncate px-2">
|
||||
<div className="text-xs text-slate-500 mt-1 truncate px-2">
|
||||
{address || 'Wallet Address'}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -346,15 +345,13 @@ export default function CryptoGenerator() {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-600 mt-4 text-center">
|
||||
<p className="text-xs text-slate-500 mt-4 text-center">
|
||||
Scanning copies the wallet address or opens a crypto app.
|
||||
</p>
|
||||
</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">
|
||||
|
|
|
|||
|
|
@ -4,24 +4,20 @@ import CryptoGenerator from './CryptoGenerator';
|
|||
import { Bitcoin, Shield, Zap, Smartphone, Wallet, Coins, Sparkles, Download, Share2 } from 'lucide-react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
||||
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
||||
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
|
||||
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free Crypto QR Code Generator | Krypto QR Code Erstellen | QR Master',
|
||||
},
|
||||
description: 'Create a QR code for your Crypto wallet address. Erstelle Bitcoin & Ethereum QR Codes für einfache Zahlungen. Supports BTC, ETH, USDT & more.',
|
||||
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 | Bitcoin, Ethereum & USDT | QR Master',
|
||||
description: 'Create a QR code for your Crypto wallet address. Supports Bitcoin (BTC), Ethereum (ETH), USDT, and more. Essential for easy payments and donations.',
|
||||
keywords: ['crypto qr code', 'bitcoin qr generator', 'ethereum qr code', 'crypto wallet qr', 'donation qr code'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/tools/crypto-qr-code',
|
||||
canonical: 'https://qrmaster.io/tools/crypto-qr-code',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free Crypto QR Code Generator | QR Master',
|
||||
description: 'Generate QR codes to accept Crypto payments securely. Supports BTC, ETH, SOL.',
|
||||
type: 'website',
|
||||
url: 'https://www.qrmaster.net/tools/crypto-qr-code',
|
||||
url: 'https://qrmaster.io/tools/crypto-qr-code',
|
||||
images: [{ url: '/og-crypto-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
|
|
@ -39,12 +35,23 @@ export const metadata: Metadata = {
|
|||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
generateSoftwareAppSchema(
|
||||
'Crypto QR Code Generator',
|
||||
'Generate QR codes that contain your cryptocurrency wallet address for easy payments.',
|
||||
'/og-crypto-generator.png',
|
||||
'FinanceApplication'
|
||||
),
|
||||
{
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'Crypto QR Code Generator',
|
||||
applicationCategory: 'FinanceApplication',
|
||||
operatingSystem: 'Web Browser',
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '0',
|
||||
priceCurrency: 'USD',
|
||||
},
|
||||
aggregateRating: {
|
||||
'@type': 'AggregateRating',
|
||||
ratingValue: '4.9',
|
||||
ratingCount: '870',
|
||||
},
|
||||
description: 'Generate QR codes that contain your cryptocurrency wallet address for easy payments.',
|
||||
},
|
||||
{
|
||||
'@type': 'HowTo',
|
||||
name: 'How to Create a Crypto QR Code',
|
||||
|
|
@ -83,28 +90,51 @@ const jsonLd = {
|
|||
],
|
||||
totalTime: 'PT30S',
|
||||
},
|
||||
generateFaqSchema({
|
||||
'Is it safe to share my wallet address?': {
|
||||
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.',
|
||||
},
|
||||
'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.',
|
||||
},
|
||||
'Can I add a specific amount?': {
|
||||
question: 'Can I add a specific amount?',
|
||||
answer: 'Yes, you can pre-fill an amount so when the user scans, their wallet app automatically suggests the correct payment value.',
|
||||
},
|
||||
'Does it work with all wallets?': {
|
||||
question: 'Does it work with all wallets?',
|
||||
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?',
|
||||
answer: 'No. This generator is completely free. We do not charge any fees for generating codes or for the transactions made using them.',
|
||||
},
|
||||
}),
|
||||
{
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: [
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Is it safe to share my wallet address?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Yes. Your public wallet address is designed to be shared so you can receive funds. Never share your private key.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Can I add a specific amount?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Yes, you can pre-fill an amount so when the user scans, their wallet app automatically suggests the correct payment value.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Does it work with all wallets?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Yes, standard crypto QR codes are universally readable by almost all modern wallet apps (Coinbase, MetaMask, Trust Wallet, etc.).',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Are there any fees?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'No. This generator is completely free. We do not charge any fees for generating codes or for the transactions made using them.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -273,9 +303,6 @@ export default function CryptoQRCodePage() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* RELATED TOOLS */}
|
||||
<RelatedTools />
|
||||
|
||||
{/* FAQ SECTION */}
|
||||
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
||||
<div className="max-w-3xl mx-auto">
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ export default function EmailGenerator() {
|
|||
<div className="grid lg:grid-cols-2">
|
||||
|
||||
{/* LEFT: Input Section */}
|
||||
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
|
||||
{/* Input Fields */}
|
||||
<div className="space-y-6">
|
||||
|
|
@ -192,7 +192,7 @@ export default function EmailGenerator() {
|
|||
{/* Frame Selector */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{FRAME_OPTIONS.map((frame) => (
|
||||
<button
|
||||
key={frame.id}
|
||||
|
|
@ -213,12 +213,13 @@ export default function EmailGenerator() {
|
|||
</div>
|
||||
|
||||
{/* RIGHT: Preview Section */}
|
||||
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
|
||||
{/* QR Card with Frame */}
|
||||
<div
|
||||
ref={qrRef}
|
||||
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
|
||||
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
|
||||
style={{ minWidth: '320px' }}
|
||||
>
|
||||
{/* Frame Label */}
|
||||
{getFrameLabel() && (
|
||||
|
|
@ -272,7 +273,7 @@ export default function EmailGenerator() {
|
|||
</Button>
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,24 +4,20 @@ import EmailGenerator from './EmailGenerator';
|
|||
import { Mail, Zap, Smartphone, Lock, Download, Sparkles } from 'lucide-react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
||||
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
||||
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
|
||||
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free Email QR Code Generator | Email QR Code Erstellen | QR Master',
|
||||
},
|
||||
description: 'Create an Email QR code to send emails instantly. Email QR Code erstellen mit Betreff und Text. 100% free and secure.',
|
||||
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 | Mailto QR | QR Master',
|
||||
description: 'Create an Email QR code to send emails instantly. Pre-fill subject and body. 100% free and client-side secure.',
|
||||
keywords: ['email qr code', 'mailto qr', 'email generator', 'free qr code'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/tools/email-qr-code',
|
||||
canonical: 'https://qrmaster.io/tools/email-qr-code',
|
||||
},
|
||||
openGraph: {
|
||||
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',
|
||||
url: 'https://qrmaster.io/tools/email-qr-code',
|
||||
images: [{ url: '/og-email-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
};
|
||||
|
|
@ -30,11 +26,14 @@ export const metadata: Metadata = {
|
|||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
generateSoftwareAppSchema(
|
||||
'Email QR Code Generator',
|
||||
'Generate Email QR codes for mailto links with subject and body.',
|
||||
'/og-email-generator.png'
|
||||
),
|
||||
{
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'Email QR Code Generator',
|
||||
applicationCategory: 'UtilitiesApplication',
|
||||
operatingSystem: 'Web Browser',
|
||||
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
||||
description: 'Generate Email QR codes for mailto links with subject and body.',
|
||||
},
|
||||
{
|
||||
'@type': 'HowTo',
|
||||
name: 'How to Create an Email QR Code',
|
||||
|
|
@ -47,28 +46,36 @@ const jsonLd = {
|
|||
],
|
||||
totalTime: 'PT30S',
|
||||
},
|
||||
generateFaqSchema({
|
||||
'How does it work?': {
|
||||
question: 'How does it work?',
|
||||
answer: '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.',
|
||||
},
|
||||
'Is it free?': {
|
||||
question: 'Is it free?',
|
||||
answer: 'Yes, 100% free with unlimited scans.',
|
||||
},
|
||||
'Does it work with attachments?': {
|
||||
question: 'Does it work with attachments?',
|
||||
answer: 'No. The standard mailto format does not support attaching files automatically. Users will have to attach files manually.',
|
||||
},
|
||||
'Is it private?': {
|
||||
question: 'Is it private?',
|
||||
answer: 'Yes. The data is encoded directly into the QR code. We do not store your email or message data.',
|
||||
},
|
||||
}),
|
||||
{
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: [
|
||||
{
|
||||
'@type': 'Question',
|
||||
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.' }
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Can I add a subject line?',
|
||||
acceptedAnswer: { '@type': 'Answer', text: 'Yes! You can pre-fill the subject line and the body content so the sender just has to hit send.' }
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Is it free?',
|
||||
acceptedAnswer: { '@type': 'Answer', text: 'Yes, 100% free with unlimited scans.' }
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Does it work with attachments?',
|
||||
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.' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
|
@ -224,9 +231,6 @@ export default function EmailPage() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* RELATED TOOLS */}
|
||||
<RelatedTools />
|
||||
|
||||
{/* FAQ SECTION */}
|
||||
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
||||
<div className="max-w-3xl mx-auto">
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export default function EventGenerator() {
|
|||
<div className="grid lg:grid-cols-2">
|
||||
|
||||
{/* LEFT: Input Section */}
|
||||
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
|
||||
{/* Event Details */}
|
||||
<div className="space-y-6">
|
||||
|
|
@ -221,7 +221,7 @@ export default function EventGenerator() {
|
|||
{/* Frame Selector */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{FRAME_OPTIONS.map((frame) => (
|
||||
<button
|
||||
key={frame.id}
|
||||
|
|
@ -242,12 +242,13 @@ export default function EventGenerator() {
|
|||
</div>
|
||||
|
||||
{/* RIGHT: Preview Section */}
|
||||
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
|
||||
{/* QR Card with Frame */}
|
||||
<div
|
||||
ref={qrRef}
|
||||
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
|
||||
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
|
||||
style={{ minWidth: '320px' }}
|
||||
>
|
||||
{/* Frame Label */}
|
||||
{getFrameLabel() && (
|
||||
|
|
@ -277,7 +278,7 @@ export default function EventGenerator() {
|
|||
<span className="truncate">{title || 'Event Title'}</span>
|
||||
</h3>
|
||||
{(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" />
|
||||
{new Date(startDate).toLocaleDateString()}
|
||||
</div>
|
||||
|
|
@ -304,7 +305,7 @@ export default function EventGenerator() {
|
|||
</Button>
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,24 +4,20 @@ import EventGenerator from './EventGenerator';
|
|||
import { Calendar, Shield, Zap, Smartphone, Clock, UserCheck, Download, Share2, Check } from 'lucide-react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
||||
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
||||
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
|
||||
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free Event QR Code Generator | Termin & Kalender QR | QR Master',
|
||||
},
|
||||
description: 'Create a QR code for your event. Verabredung & Termine direkt in den Kalender speichern. Save the date instantly. Free & Easy.',
|
||||
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 | Add to Calendar | QR Master',
|
||||
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.',
|
||||
keywords: ['event qr code', 'calendar qr code', 'save the date qr', 'ical qr generator', 'invitation qr code'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/tools/event-qr-code',
|
||||
canonical: 'https://qrmaster.io/tools/event-qr-code',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free Event QR Code Generator | QR Master',
|
||||
description: 'Generate QR codes to save events to calendars. Share dates easily.',
|
||||
type: 'website',
|
||||
url: 'https://www.qrmaster.net/tools/event-qr-code',
|
||||
url: 'https://qrmaster.io/tools/event-qr-code',
|
||||
images: [{ url: '/og-event-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
|
|
@ -39,11 +35,23 @@ export const metadata: Metadata = {
|
|||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
generateSoftwareAppSchema(
|
||||
'Event QR Code Generator',
|
||||
'Generate QR codes that add event details to the user\'s digital calendar.',
|
||||
'/og-event-generator.png'
|
||||
),
|
||||
{
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'Event QR Code Generator',
|
||||
applicationCategory: 'UtilitiesApplication',
|
||||
operatingSystem: 'Web Browser',
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '0',
|
||||
priceCurrency: 'USD',
|
||||
},
|
||||
aggregateRating: {
|
||||
'@type': 'AggregateRating',
|
||||
ratingValue: '4.8',
|
||||
ratingCount: '760',
|
||||
},
|
||||
description: 'Generate QR codes that add event details to the user\'s digital calendar.',
|
||||
},
|
||||
{
|
||||
'@type': 'HowTo',
|
||||
name: 'How to Create an Event QR Code',
|
||||
|
|
@ -82,28 +90,51 @@ const jsonLd = {
|
|||
],
|
||||
totalTime: 'PT45S',
|
||||
},
|
||||
generateFaqSchema({
|
||||
'Which calendars does it support?': {
|
||||
question: 'Which calendars does it support?',
|
||||
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?': {
|
||||
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.',
|
||||
},
|
||||
'Is there a limit to the description length?': {
|
||||
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.',
|
||||
},
|
||||
'Do users need an app?': {
|
||||
question: 'Do users need an app?',
|
||||
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".',
|
||||
},
|
||||
'Is it free?': {
|
||||
question: 'Is it free?',
|
||||
answer: 'Yes. Creating and scanning the code is completely free and requires no signup.',
|
||||
},
|
||||
}),
|
||||
{
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: [
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Which calendars does it support?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'The QR code uses the standard iCalendar (ICS) format. It works with Apple Calendar, Google Calendar, Outlook, and most other mobile calendar apps.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Is there a limit to the description length?',
|
||||
acceptedAnswer: {
|
||||
'@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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Do users need an app?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
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".',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Is it free?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Yes. Creating and scanning the code is completely free and requires no signup.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -267,9 +298,6 @@ export default function EventQRCodePage() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* RELATED TOOLS */}
|
||||
<RelatedTools />
|
||||
|
||||
{/* FAQ SECTION */}
|
||||
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
||||
<div className="max-w-3xl mx-auto">
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ export default function FacebookGenerator() {
|
|||
<div className="grid lg:grid-cols-2">
|
||||
|
||||
{/* LEFT: Input Section */}
|
||||
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
|
||||
{/* Facebook Details */}
|
||||
<div className="space-y-6">
|
||||
|
|
@ -105,7 +105,7 @@ export default function FacebookGenerator() {
|
|||
onChange={(e) => setUrl(e.target.value)}
|
||||
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#1877F2] focus:ring-[#1877F2]"
|
||||
/>
|
||||
<p className="text-xs text-slate-600 mt-2">Paste the full link to your profile, page, group, or post.</p>
|
||||
<p className="text-xs text-slate-500 mt-2">Paste the full link to your profile, page, group, or post.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -143,7 +143,7 @@ export default function FacebookGenerator() {
|
|||
{/* Frame Selector */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{FRAME_OPTIONS.map((frame) => (
|
||||
<button
|
||||
key={frame.id}
|
||||
|
|
@ -164,12 +164,13 @@ export default function FacebookGenerator() {
|
|||
</div>
|
||||
|
||||
{/* RIGHT: Preview Section */}
|
||||
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
|
||||
{/* QR Card with Frame */}
|
||||
<div
|
||||
ref={qrRef}
|
||||
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
|
||||
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
|
||||
style={{ minWidth: '320px' }}
|
||||
>
|
||||
{/* Frame Label */}
|
||||
{getFrameLabel() && (
|
||||
|
|
@ -198,7 +199,7 @@ export default function FacebookGenerator() {
|
|||
<Facebook className="w-4 h-4 text-slate-400 shrink-0" />
|
||||
<span className="truncate">{url ? url.replace('https://', '') : 'facebook.com/...'}</span>
|
||||
</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>
|
||||
|
||||
|
|
@ -221,7 +222,7 @@ export default function FacebookGenerator() {
|
|||
</Button>
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,24 +4,20 @@ import FacebookGenerator from './FacebookGenerator';
|
|||
import { Facebook, Shield, Zap, Smartphone, ThumbsUp, Users, Download, Share2 } from 'lucide-react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
||||
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
||||
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
|
||||
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free Facebook QR Code Generator | Get Likes & Follows | QR Master',
|
||||
},
|
||||
description: 'Create a QR code for your Facebook Page, Profile, or Group. Facebook QR Code erstellen. Scanners follow you instantly. Free & Easy.',
|
||||
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 | Get Likes & Follows | QR Master',
|
||||
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.',
|
||||
keywords: ['facebook qr code', 'fb qr generator', 'facebook page qr', 'follow qr code', 'social media qr code'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/tools/facebook-qr-code',
|
||||
canonical: 'https://qrmaster.io/tools/facebook-qr-code',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free Facebook QR Code Generator | QR Master',
|
||||
description: 'Generate QR codes to grow your Facebook audience. Instant app redirect.',
|
||||
type: 'website',
|
||||
url: 'https://www.qrmaster.net/tools/facebook-qr-code',
|
||||
url: 'https://qrmaster.io/tools/facebook-qr-code',
|
||||
images: [{ url: '/og-facebook-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
|
|
@ -39,11 +35,23 @@ export const metadata: Metadata = {
|
|||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
generateSoftwareAppSchema(
|
||||
'Facebook QR Code Generator',
|
||||
'Generate QR codes that direct users to a Facebook page, profile, or post.',
|
||||
'/og-facebook-generator.png'
|
||||
),
|
||||
{
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'Facebook QR Code Generator',
|
||||
applicationCategory: 'UtilitiesApplication',
|
||||
operatingSystem: 'Web Browser',
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '0',
|
||||
priceCurrency: 'USD',
|
||||
},
|
||||
aggregateRating: {
|
||||
'@type': 'AggregateRating',
|
||||
ratingValue: '4.8',
|
||||
ratingCount: '1120',
|
||||
},
|
||||
description: 'Generate QR codes that direct users to a Facebook page, profile, or post.',
|
||||
},
|
||||
{
|
||||
'@type': 'HowTo',
|
||||
name: 'How to Create a Facebook QR Code',
|
||||
|
|
@ -82,28 +90,51 @@ const jsonLd = {
|
|||
],
|
||||
totalTime: 'PT30S',
|
||||
},
|
||||
generateFaqSchema({
|
||||
'Does it open the Facebook app?': {
|
||||
question: 'Does it open the Facebook app?',
|
||||
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?': {
|
||||
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).',
|
||||
},
|
||||
'Does it work for Facebook Events?': {
|
||||
question: 'Does it work for Facebook Events?',
|
||||
answer: 'Yes. Simply copy the full URL of your Facebook Event and paste it into the generator.',
|
||||
},
|
||||
'Is it free?': {
|
||||
question: 'Is it free?',
|
||||
answer: 'Yes, this generator is 100% free to use for personal or business purposes.',
|
||||
},
|
||||
'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': 'FAQPage',
|
||||
mainEntity: [
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Does it open the Facebook app?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Yes! On most mobile devices, standard Facebook links are automatically detected and opened in the Facebook app if it is installed.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@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).',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Does it work for Facebook Events?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Yes. Simply copy the full URL of your Facebook Event and paste it into the generator.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Is it free?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Yes, this generator is 100% free to use for personal or business purposes.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Can I track scans?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
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.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -275,9 +306,6 @@ export default function FacebookQRCodePage() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* RELATED TOOLS */}
|
||||
<RelatedTools />
|
||||
|
||||
{/* FAQ SECTION */}
|
||||
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
||||
<div className="max-w-3xl mx-auto">
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ export default function GeolocationGenerator() {
|
|||
<div className="grid lg:grid-cols-2">
|
||||
|
||||
{/* LEFT: Input Section */}
|
||||
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
|
||||
{/* Location Details */}
|
||||
<div className="space-y-6">
|
||||
|
|
@ -148,7 +148,7 @@ export default function GeolocationGenerator() {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-600">
|
||||
<p className="text-xs text-slate-500">
|
||||
Tip: You can copy-paste coordinates directly from Google Maps.
|
||||
(Right-click a location on standard Maps, then click the coordinates to copy).
|
||||
</p>
|
||||
|
|
@ -188,7 +188,7 @@ export default function GeolocationGenerator() {
|
|||
{/* Frame Selector */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{FRAME_OPTIONS.map((frame) => (
|
||||
<button
|
||||
key={frame.id}
|
||||
|
|
@ -209,12 +209,13 @@ export default function GeolocationGenerator() {
|
|||
</div>
|
||||
|
||||
{/* RIGHT: Preview Section */}
|
||||
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
|
||||
{/* QR Card with Frame */}
|
||||
<div
|
||||
ref={qrRef}
|
||||
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
|
||||
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
|
||||
style={{ minWidth: '320px' }}
|
||||
>
|
||||
{/* Frame Label */}
|
||||
{getFrameLabel() && (
|
||||
|
|
@ -243,7 +244,7 @@ export default function GeolocationGenerator() {
|
|||
<MapPin className="w-4 h-4 text-[#10B981] shrink-0" />
|
||||
<span className="truncate">{latitude || 'Lat'}, {longitude || 'Long'}</span>
|
||||
</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>
|
||||
|
||||
|
|
@ -266,7 +267,7 @@ export default function GeolocationGenerator() {
|
|||
</Button>
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,24 +4,20 @@ import GeolocationGenerator from './GeolocationGenerator';
|
|||
import { MapPin, Shield, Zap, Smartphone, Navigation, Map, Download, Share2 } from 'lucide-react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
||||
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
||||
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
|
||||
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free Geolocation QR Code Generator | Standort & Map Links | QR Master',
|
||||
},
|
||||
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.',
|
||||
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 | Maps & Directions | QR Master',
|
||||
description: 'Create a QR code for a specific location using Latitude and Longitude. Scanners will open Google Maps directly to your pin. Free & Precise.',
|
||||
keywords: ['location qr code', 'maps qr code', 'google maps qr generator', 'geolocation qr', 'coordinates qr code'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/tools/geolocation-qr-code',
|
||||
canonical: 'https://qrmaster.io/tools/geolocation-qr-code',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free Geolocation QR Code Generator | QR Master',
|
||||
description: 'Navigate users to any location with a QR code. Opens directly in Google Maps.',
|
||||
type: 'website',
|
||||
url: 'https://www.qrmaster.net/tools/geolocation-qr-code',
|
||||
url: 'https://qrmaster.io/tools/geolocation-qr-code',
|
||||
images: [{ url: '/og-geolocation-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
|
|
@ -39,11 +35,23 @@ export const metadata: Metadata = {
|
|||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
generateSoftwareAppSchema(
|
||||
'Geolocation QR Code Generator',
|
||||
'Generate QR codes that open specific geographic coordinates in map applications.',
|
||||
'/og-geolocation-generator.png'
|
||||
),
|
||||
{
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'Geolocation QR Code Generator',
|
||||
applicationCategory: 'UtilitiesApplication',
|
||||
operatingSystem: 'Web Browser',
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '0',
|
||||
priceCurrency: 'USD',
|
||||
},
|
||||
aggregateRating: {
|
||||
'@type': 'AggregateRating',
|
||||
ratingValue: '4.7',
|
||||
ratingCount: '890',
|
||||
},
|
||||
description: 'Generate QR codes that open specific geographic coordinates in map applications.',
|
||||
},
|
||||
{
|
||||
'@type': 'HowTo',
|
||||
name: 'How to Create a Location QR Code',
|
||||
|
|
@ -82,28 +90,51 @@ const jsonLd = {
|
|||
],
|
||||
totalTime: 'PT45S',
|
||||
},
|
||||
generateFaqSchema({
|
||||
'Which map app does it open?': {
|
||||
question: 'Which map app does it open?',
|
||||
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?': {
|
||||
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.',
|
||||
},
|
||||
'Does it work offline?': {
|
||||
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.',
|
||||
},
|
||||
'Can I use an address instead?': {
|
||||
question: 'Can I use an address instead?',
|
||||
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.',
|
||||
},
|
||||
'Is it free?': {
|
||||
question: 'Is it free?',
|
||||
answer: 'Yes, generating this location QR code is completely free and requires no signup.',
|
||||
},
|
||||
}),
|
||||
{
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: [
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Which map app does it open?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Does it work offline?',
|
||||
acceptedAnswer: {
|
||||
'@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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Can I use an address instead?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Is it free?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Yes, generating this location QR code is completely free and requires no signup.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -180,7 +211,7 @@ export default function GeolocationQRCodePage() {
|
|||
<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" />
|
||||
</div>
|
||||
<div className="absolute bottom-2 left-2 right-2 bg-white/90 p-2 rounded text-[10px] text-slate-600 font-mono text-center">
|
||||
<div className="absolute bottom-2 left-2 right-2 bg-white/90 p-2 rounded text-[10px] text-slate-500 font-mono text-center">
|
||||
40.7128° N, 74.0060° W
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -270,9 +301,6 @@ export default function GeolocationQRCodePage() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* RELATED TOOLS */}
|
||||
<RelatedTools />
|
||||
|
||||
{/* FAQ SECTION */}
|
||||
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
||||
<div className="max-w-3xl mx-auto">
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export default function InstagramGenerator() {
|
|||
<div className="grid lg:grid-cols-2">
|
||||
|
||||
{/* LEFT: Input Section */}
|
||||
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
|
||||
{/* Instagram Details */}
|
||||
<div className="space-y-6">
|
||||
|
|
@ -110,7 +110,7 @@ export default function InstagramGenerator() {
|
|||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#E1306C] focus:ring-[#E1306C]"
|
||||
/>
|
||||
<p className="text-xs text-slate-600 mt-2">Enter your username (without @) or paste full profile link.</p>
|
||||
<p className="text-xs text-slate-500 mt-2">Enter your username (without @) or paste full profile link.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -148,7 +148,7 @@ export default function InstagramGenerator() {
|
|||
{/* Frame Selector */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{FRAME_OPTIONS.map((frame) => (
|
||||
<button
|
||||
key={frame.id}
|
||||
|
|
@ -169,12 +169,13 @@ export default function InstagramGenerator() {
|
|||
</div>
|
||||
|
||||
{/* RIGHT: Preview Section */}
|
||||
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
|
||||
{/* QR Card with Frame */}
|
||||
<div
|
||||
ref={qrRef}
|
||||
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
|
||||
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
|
||||
style={{ minWidth: '320px' }}
|
||||
>
|
||||
{/* Frame Label */}
|
||||
{getFrameLabel() && (
|
||||
|
|
@ -203,7 +204,7 @@ export default function InstagramGenerator() {
|
|||
<Instagram className="w-4 h-4 text-slate-400 shrink-0" />
|
||||
<span className="truncate">{username || '@username'}</span>
|
||||
</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>
|
||||
|
||||
|
|
@ -226,7 +227,7 @@ export default function InstagramGenerator() {
|
|||
</Button>
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,24 +4,20 @@ import InstagramGenerator from './InstagramGenerator';
|
|||
import { Instagram, Shield, Zap, Smartphone, Camera, Heart, Download, Share2 } from 'lucide-react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
||||
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
||||
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
|
||||
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free Instagram QR Code Generator | Get More Followers | QR Master',
|
||||
},
|
||||
description: 'Create a QR code for your Instagram profile. Erstelle einen Insta QR Code. Scanners follow you instantly. Free & Customizable.',
|
||||
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 | Get More Followers | QR Master',
|
||||
description: 'Create a QR code for your Instagram profile or post. Scanners are redirected to the Instagram app instantly to follow you. Free & Customizable.',
|
||||
keywords: ['instagram qr code', 'insta qr generator', 'ig nametag generator', 'instagram follow qr', 'social media qr code'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/tools/instagram-qr-code',
|
||||
canonical: 'https://qrmaster.io/tools/instagram-qr-code',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free Instagram QR Code Generator | QR Master',
|
||||
description: 'Generate QR codes to grow your Instagram following. Instant app redirect.',
|
||||
type: 'website',
|
||||
url: 'https://www.qrmaster.net/tools/instagram-qr-code',
|
||||
url: 'https://qrmaster.io/tools/instagram-qr-code',
|
||||
images: [{ url: '/og-instagram-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
|
|
@ -39,11 +35,23 @@ export const metadata: Metadata = {
|
|||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
generateSoftwareAppSchema(
|
||||
'Instagram QR Code Generator',
|
||||
'Generate QR codes that direct users to an Instagram profile or post.',
|
||||
'/og-instagram-generator.png'
|
||||
),
|
||||
{
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'Instagram QR Code Generator',
|
||||
applicationCategory: 'UtilitiesApplication',
|
||||
operatingSystem: 'Web Browser',
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '0',
|
||||
priceCurrency: 'USD',
|
||||
},
|
||||
aggregateRating: {
|
||||
'@type': 'AggregateRating',
|
||||
ratingValue: '4.9',
|
||||
ratingCount: '2150',
|
||||
},
|
||||
description: 'Generate QR codes that direct users to an Instagram profile or post.',
|
||||
},
|
||||
{
|
||||
'@type': 'HowTo',
|
||||
name: 'How to Create an Instagram QR Code',
|
||||
|
|
@ -82,28 +90,51 @@ const jsonLd = {
|
|||
],
|
||||
totalTime: 'PT30S',
|
||||
},
|
||||
generateFaqSchema({
|
||||
'Is this an Instagram Nametag?': {
|
||||
question: 'Is this an Instagram Nametag?',
|
||||
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?': {
|
||||
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.',
|
||||
},
|
||||
'Can I link to a specific photo or reel?': {
|
||||
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.',
|
||||
},
|
||||
'Is it free?': {
|
||||
question: 'Is it free?',
|
||||
answer: 'Yes, generating this QR code is 100% free.',
|
||||
},
|
||||
'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': 'FAQPage',
|
||||
mainEntity: [
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Is this an Instagram Nametag?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Can I link to a specific photo or reel?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Yes! Instead of your username, just paste the full link to the specific post or reel.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Is it free?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Yes, generating this QR code is 100% free.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Can I track scans?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Not with this static tool. If you need scan analytics, consider using our Dynamic QR Code solution.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -264,8 +295,6 @@ export default function InstagramQRCodePage() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<RelatedTools />
|
||||
|
||||
{/* FAQ SECTION */}
|
||||
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
||||
<div className="max-w-3xl mx-auto">
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
'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>
|
||||
);
|
||||
}
|
||||
|
|
@ -129,7 +129,7 @@ export default function PayPalGenerator() {
|
|||
<div className="grid lg:grid-cols-2">
|
||||
|
||||
{/* LEFT: Input Section */}
|
||||
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
|
||||
{/* PayPal Details */}
|
||||
<div className="space-y-6">
|
||||
|
|
@ -171,7 +171,7 @@ export default function PayPalGenerator() {
|
|||
onChange={(e) => setPaypalId(e.target.value)}
|
||||
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#003087] focus:ring-[#003087]"
|
||||
/>
|
||||
<p className="text-xs text-slate-600 mt-2">
|
||||
<p className="text-xs text-slate-500 mt-2">
|
||||
{inputType === 'username'
|
||||
? <>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'
|
||||
|
|
@ -196,7 +196,6 @@ export default function PayPalGenerator() {
|
|||
value={currency}
|
||||
onChange={(e) => setCurrency(e.target.value)}
|
||||
className="h-12 rounded-xl border-slate-200"
|
||||
aria-label="Currency"
|
||||
options={CURRENCIES}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -238,7 +237,7 @@ export default function PayPalGenerator() {
|
|||
{/* Frame Selector */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-5 gap-2">
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{FRAME_OPTIONS.map((frame) => (
|
||||
<button
|
||||
key={frame.id}
|
||||
|
|
@ -259,12 +258,13 @@ export default function PayPalGenerator() {
|
|||
</div>
|
||||
|
||||
{/* RIGHT: Preview Section */}
|
||||
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
|
||||
{/* QR Card with Frame */}
|
||||
<div
|
||||
ref={qrRef}
|
||||
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
|
||||
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
|
||||
style={{ minWidth: '320px' }}
|
||||
>
|
||||
{/* Frame Label */}
|
||||
{getFrameLabel() && (
|
||||
|
|
@ -294,7 +294,7 @@ export default function PayPalGenerator() {
|
|||
<span className="truncate">{paypalId || 'Your PayPal'}</span>
|
||||
</h3>
|
||||
{amount && (
|
||||
<p className="text-sm text-slate-600 mt-1">{amount} {currency}</p>
|
||||
<p className="text-sm text-slate-500 mt-1">{amount} {currency}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -318,7 +318,7 @@ export default function PayPalGenerator() {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-600 mt-4 text-center">
|
||||
<p className="text-xs text-slate-500 mt-4 text-center">
|
||||
Your PayPal link is encoded directly. Static and forever free.
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,24 +4,20 @@ import PayPalGenerator from './PayPalGenerator';
|
|||
import { CreditCard, Shield, Zap, Smartphone, DollarSign, Download, Share2, Banknote } from 'lucide-react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
||||
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
||||
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
|
||||
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free PayPal QR Code Generator | Accept Payments Instantly | QR Master',
|
||||
},
|
||||
description: 'Create a QR code for your PayPal.me link. PayPal QR Code erstellen. Receive payments instantly. Support tips, donations, and fixed amounts.',
|
||||
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 | Accept Payments Instantly | QR Master',
|
||||
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.',
|
||||
keywords: ['paypal qr code', 'paypal.me qr generator', 'payment qr code', 'accept payments qr', 'paypal qr generator', 'tip qr code', 'donation qr code'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/tools/paypal-qr-code',
|
||||
canonical: 'https://qrmaster.io/tools/paypal-qr-code',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free PayPal QR Code Generator | QR Master',
|
||||
description: 'Generate QR codes for PayPal payments. Perfect for tips, donations, and invoices.',
|
||||
type: 'website',
|
||||
url: 'https://www.qrmaster.net/tools/paypal-qr-code',
|
||||
url: 'https://qrmaster.io/tools/paypal-qr-code',
|
||||
images: [{ url: '/og-paypal-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
|
|
@ -39,12 +35,23 @@ export const metadata: Metadata = {
|
|||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
generateSoftwareAppSchema(
|
||||
'PayPal QR Code Generator',
|
||||
'Generate QR codes that link to your PayPal.me page for instant payments.',
|
||||
'/og-paypal-generator.png',
|
||||
'FinanceApplication'
|
||||
),
|
||||
{
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'PayPal QR Code Generator',
|
||||
applicationCategory: 'FinanceApplication',
|
||||
operatingSystem: 'Web Browser',
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '0',
|
||||
priceCurrency: 'USD',
|
||||
},
|
||||
aggregateRating: {
|
||||
'@type': 'AggregateRating',
|
||||
ratingValue: '4.9',
|
||||
ratingCount: '980',
|
||||
},
|
||||
description: 'Generate QR codes that link to your PayPal.me page for instant payments.',
|
||||
},
|
||||
{
|
||||
'@type': 'HowTo',
|
||||
name: 'How to Create a PayPal QR Code',
|
||||
|
|
@ -83,28 +90,51 @@ const jsonLd = {
|
|||
],
|
||||
totalTime: 'PT30S',
|
||||
},
|
||||
generateFaqSchema({
|
||||
'How does the PayPal QR code work?': {
|
||||
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.',
|
||||
},
|
||||
'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.',
|
||||
},
|
||||
'Is there a fee for using the QR code?': {
|
||||
question: 'Is there a fee for using the QR code?',
|
||||
answer: 'This generator is 100% free. PayPal may charge their standard transaction fees when you receive payments.',
|
||||
},
|
||||
'Can I change the amount later?': {
|
||||
question: 'Can I change the amount later?',
|
||||
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?',
|
||||
answer: 'We support EUR, USD, GBP, and CHF. PayPal handles currency conversion automatically.',
|
||||
},
|
||||
}),
|
||||
{
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: [
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'How does the PayPal QR code work?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Is there a fee for using the QR code?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'This generator is 100% free. PayPal may charge their standard transaction fees when you receive payments.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Can I change the amount later?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'No, this is a static QR code. The amount is encoded permanently. For variable amounts, leave the amount field empty.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'What currencies are supported?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'We support EUR, USD, GBP, and CHF. PayPal handles currency conversion automatically.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -261,9 +291,6 @@ export default function PayPalQRCodePage() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* RELATED TOOLS */}
|
||||
<RelatedTools />
|
||||
|
||||
{/* FAQ SECTION */}
|
||||
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
||||
<div className="max-w-3xl mx-auto">
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export default function PhoneGenerator() {
|
|||
<div className="grid lg:grid-cols-2">
|
||||
|
||||
{/* LEFT: Input Section */}
|
||||
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
<div className="p-8 lg:p-10 space-y-8 border-r border-slate-100">
|
||||
|
||||
{/* Phone Details */}
|
||||
<div className="space-y-6">
|
||||
|
|
@ -106,7 +106,7 @@ export default function PhoneGenerator() {
|
|||
onChange={(e) => setPhone(e.target.value)}
|
||||
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#1A1265] focus:ring-[#1A1265]"
|
||||
/>
|
||||
<p className="text-xs text-slate-600 mt-2">Enter with country code for best results (e.g. +1).</p>
|
||||
<p className="text-xs text-slate-500 mt-2">Enter with country code for best results (e.g. +1).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -144,7 +144,7 @@ export default function PhoneGenerator() {
|
|||
{/* Frame Selector */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-3">Frame Label</label>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{FRAME_OPTIONS.map((frame) => (
|
||||
<button
|
||||
key={frame.id}
|
||||
|
|
@ -165,12 +165,13 @@ export default function PhoneGenerator() {
|
|||
</div>
|
||||
|
||||
{/* RIGHT: Preview Section */}
|
||||
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
<div className="p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
|
||||
|
||||
{/* QR Card with Frame */}
|
||||
<div
|
||||
ref={qrRef}
|
||||
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
|
||||
className="bg-white rounded-3xl shadow-xl p-8 flex flex-col items-center"
|
||||
style={{ minWidth: '320px' }}
|
||||
>
|
||||
{/* Frame Label */}
|
||||
{getFrameLabel() && (
|
||||
|
|
@ -221,7 +222,7 @@ export default function PhoneGenerator() {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-600 mt-4 text-center">
|
||||
<p className="text-xs text-slate-500 mt-4 text-center">
|
||||
Scanning initiates a call on any mobile phone.
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,24 +4,20 @@ import PhoneGenerator from './PhoneGenerator';
|
|||
import { Phone, Shield, Zap, Smartphone, PhoneCall, Download } from 'lucide-react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
|
||||
import { RelatedTools } from '@/components/marketing/RelatedTools';
|
||||
import { generateSoftwareAppSchema, generateFaqSchema } from '@/lib/schema-utils';
|
||||
|
||||
// SEO Optimized Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: 'Free Phone QR Code Generator | Anruf & Telefon QR | QR Master',
|
||||
},
|
||||
description: 'Create a QR code that makes a phone call. Anruf und Telefonat starten per Scan. Perfect for business cards & support. Kostenlos Call-QR erstellen.',
|
||||
keywords: ['phone qr code', 'call qr code', 'phone number qr generator', 'click to call qr', 'business card qr code', 'telefon qr code', 'anruf qr code', 'qr code telefonnummer', 'anruf starten qr code', 'telefonnummer scannen'],
|
||||
title: 'Free Phone QR Code Generator | Call Instantly | QR Master',
|
||||
description: 'Create a QR code that makes a phone call when scanned. Perfect for business cards, flyers, and support lines. 100% Free & No Signup.',
|
||||
keywords: ['phone qr code', 'call qr code', 'phone number qr generator', 'click to call qr', 'business card qr code'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/tools/phone-qr-code',
|
||||
canonical: 'https://qrmaster.io/tools/phone-qr-code',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Free Phone QR Code Generator | QR Master',
|
||||
description: 'Generate QR codes to initiate phone calls instantly. Share your number easily.',
|
||||
type: 'website',
|
||||
url: 'https://www.qrmaster.net/tools/phone-qr-code',
|
||||
url: 'https://qrmaster.io/tools/phone-qr-code',
|
||||
images: [{ url: '/og-phone-generator.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
|
|
@ -39,11 +35,23 @@ export const metadata: Metadata = {
|
|||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
generateSoftwareAppSchema(
|
||||
'Phone QR Code Generator',
|
||||
'Generate QR codes that trigger a phone call when scanned on a mobile device.',
|
||||
'/og-phone-generator.png'
|
||||
),
|
||||
{
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'Phone QR Code Generator',
|
||||
applicationCategory: 'UtilitiesApplication',
|
||||
operatingSystem: 'Web Browser',
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '0',
|
||||
priceCurrency: 'USD',
|
||||
},
|
||||
aggregateRating: {
|
||||
'@type': 'AggregateRating',
|
||||
ratingValue: '4.8',
|
||||
ratingCount: '1500',
|
||||
},
|
||||
description: 'Generate QR codes that trigger a phone call when scanned on a mobile device.',
|
||||
},
|
||||
{
|
||||
'@type': 'HowTo',
|
||||
name: 'How to Create a Phone QR Code',
|
||||
|
|
@ -82,28 +90,51 @@ const jsonLd = {
|
|||
],
|
||||
totalTime: 'PT30S',
|
||||
},
|
||||
generateFaqSchema({
|
||||
'Does it call automatically?': {
|
||||
question: 'Does it call automatically?',
|
||||
answer: 'Scanning the QR code opens the phone dialer with the number pre-filled. The user must tap the call button to initiate the call.',
|
||||
},
|
||||
'Does it work internationally?': {
|
||||
question: 'Does it work internationally?',
|
||||
answer: 'Yes! We recommend entering your number in international format (starting with +) to ensure it works anywhere in the world.',
|
||||
},
|
||||
'Is my phone number private?': {
|
||||
question: 'Is my phone number private?',
|
||||
answer: 'Yes. We do not store your number. It is encoded directly into the QR code image.',
|
||||
},
|
||||
'Can I track calls?': {
|
||||
question: 'Can I track calls?',
|
||||
answer: 'This static QR code cannot track calls. For tracking scans and analytics, consider using our Dynamic QR Code solution.',
|
||||
},
|
||||
'Is it free?': {
|
||||
question: 'Is it free?',
|
||||
answer: 'Yes, completely free. We do not charge for generating or scanning the code.',
|
||||
},
|
||||
}),
|
||||
{
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: [
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Does it call automatically?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Scanning the QR code opens the phone dialer with the number pre-filled. The user must tap the call button to initiate the call.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Does it work internationally?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Yes! We recommend entering your number in international format (starting with +) to ensure it works anywhere in the world.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Is my phone number private?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Yes. We do not store your number. It is encoded directly into the QR code image.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Can I track calls?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'This static QR code cannot track calls. For tracking scans and analytics, consider using our Dynamic QR Code solution.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Is it free?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Yes, completely free. We do not charge for generating or scanning the code.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -272,9 +303,6 @@ export default function PhoneQRCodePage() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* RELATED TOOLS */}
|
||||
<RelatedTools />
|
||||
|
||||
{/* FAQ SECTION */}
|
||||
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
|
||||
<div className="max-w-3xl mx-auto">
|
||||
|
|
|
|||