diff --git a/SIDE_PROJECT_STRATEGY.md b/SIDE_PROJECT_STRATEGY.md index 0bf4d85..fd7bcb7 100644 --- a/SIDE_PROJECT_STRATEGY.md +++ b/SIDE_PROJECT_STRATEGY.md @@ -68,9 +68,32 @@ Fokus auf **Unique Features** die Konkurrenten nicht haben. |------|-----|-----------------| | **Barcode Generator** | `/tools/barcode-generator` | EAN/UPC/ISBN Unterstützung | | **Bitcoin/Crypto QR** | `/tools/bitcoin-qr-code` | Multi-Wallet Format | -| **Calendar Event QR** | `/tools/calendar-qr-code` | iCal + Google Cal Support | | **AI Art QR (Viral)** | `/tools/ai-qr-code` | Stable Diffusion Integration | +## Geplantes Portfolio: Kostenlose Statische Generatoren (15 Typen) + +Wir werden die folgenden 15 statischen QR-Code-Typen anbieten. Diese sind **dauerhaft kostenlos** und erfordern keine Server-Infrastruktur für Redirects (im Gegensatz zu dynamischen Codes). + +> **Wichtig:** Alle diese Generatoren stehen sowohl **öffentlich als SEO-Landingpages** zur Verfügung (zur Neukundengewinnung), als auch im **eingeloggten Bereich** für registrierte Nutzer (für Komfort und Zentralisierung). + +1. **URL / Link**: Der Standard. Öffnet eine Webseite. +2. **Text**: Zeigt reinen Text an (bis zu 300 Zeichen). +3. **WiFi**: Verbindet direkt mit einem WLAN-Netzwerk (WPA/WEP/Open). +4. **VCard / Kontakt**: Speichert einen Kontakt direkt im Adressbuch. +5. **WhatsApp**: Startet einen Chat mit einer Nummer (und optionalem Text). +6. **E-Mail**: Öffnet das E-Mail-Programm mit Empfänger, Betreff und Body. +7. **SMS**: Bereitet eine SMS an eine Nummer vor. +8. **Anruf / Tel**: Startet einen Anruf an eine Nummer. +9. **Event / Kalender**: Fügt einen Termin zum Kalender hinzu (.ics). +10. **Geo / Maps**: Öffnet einen Standort in Google Maps/Apple Maps. +11. **Facebook**: Öffnet ein Profil oder eine Seite. +12. **Instagram**: Öffnet ein Instagram-Profil. +13. **Twitter / X**: Öffnet ein Profil oder erstellt einen Tweet. +14. **YouTube**: Öffnet ein Video oder einen Kanal. +15. **TikTok**: Öffnet ein TikTok-Profil. + +Diese Breite deckt 99% der "Everyday Use Cases" ab und maximiert die SEO-Angriffsfläche. + --- ## Technische Architektur diff --git a/public/og-crypto-generator.png b/public/og-crypto-generator.png new file mode 100644 index 0000000..e742d0a Binary files /dev/null and b/public/og-crypto-generator.png differ diff --git a/public/og-email-generator.png b/public/og-email-generator.png new file mode 100644 index 0000000..2d3ad9f Binary files /dev/null and b/public/og-email-generator.png differ diff --git a/public/og-event-generator.png b/public/og-event-generator.png new file mode 100644 index 0000000..c60e530 Binary files /dev/null and b/public/og-event-generator.png differ diff --git a/public/og-facebook-generator.png b/public/og-facebook-generator.png new file mode 100644 index 0000000..7a117f7 Binary files /dev/null and b/public/og-facebook-generator.png differ diff --git a/public/og-geolocation-generator.png b/public/og-geolocation-generator.png new file mode 100644 index 0000000..3cabc0b Binary files /dev/null and b/public/og-geolocation-generator.png differ diff --git a/public/og-instagram-generator.png b/public/og-instagram-generator.png new file mode 100644 index 0000000..e0eea42 Binary files /dev/null and b/public/og-instagram-generator.png differ diff --git a/public/og-location-generator.png b/public/og-location-generator.png new file mode 100644 index 0000000..3cabc0b Binary files /dev/null and b/public/og-location-generator.png differ diff --git a/public/og-paypal-generator.png b/public/og-paypal-generator.png new file mode 100644 index 0000000..f48e1a0 Binary files /dev/null and b/public/og-paypal-generator.png differ diff --git a/public/og-phone-generator.png b/public/og-phone-generator.png new file mode 100644 index 0000000..f669ed0 Binary files /dev/null and b/public/og-phone-generator.png differ diff --git a/public/og-sms-generator.png b/public/og-sms-generator.png new file mode 100644 index 0000000..a4182f3 Binary files /dev/null and b/public/og-sms-generator.png differ diff --git a/public/og-teams-generator.png b/public/og-teams-generator.png new file mode 100644 index 0000000..af0cc1a Binary files /dev/null and b/public/og-teams-generator.png differ diff --git a/public/og-text-generator.png b/public/og-text-generator.png new file mode 100644 index 0000000..989e8f2 Binary files /dev/null and b/public/og-text-generator.png differ diff --git a/public/og-tiktok-generator.png b/public/og-tiktok-generator.png new file mode 100644 index 0000000..f78add3 Binary files /dev/null and b/public/og-tiktok-generator.png differ diff --git a/public/og-twitter-generator.png b/public/og-twitter-generator.png new file mode 100644 index 0000000..be2702d Binary files /dev/null and b/public/og-twitter-generator.png differ diff --git a/public/og-url-generator.png b/public/og-url-generator.png new file mode 100644 index 0000000..da2f9a9 Binary files /dev/null and b/public/og-url-generator.png differ diff --git a/public/og-vcard-generator.png b/public/og-vcard-generator.png new file mode 100644 index 0000000..5364b28 Binary files /dev/null and b/public/og-vcard-generator.png differ diff --git a/public/og-whatsapp-generator.png b/public/og-whatsapp-generator.png new file mode 100644 index 0000000..d96b9dd Binary files /dev/null and b/public/og-whatsapp-generator.png differ diff --git a/public/og-wifi-generator.png b/public/og-wifi-generator.png new file mode 100644 index 0000000..414ee65 Binary files /dev/null and b/public/og-wifi-generator.png differ diff --git a/public/og-youtube-generator.png b/public/og-youtube-generator.png new file mode 100644 index 0000000..16fb0ce Binary files /dev/null and b/public/og-youtube-generator.png differ diff --git a/public/og-zoom-generator.png b/public/og-zoom-generator.png new file mode 100644 index 0000000..8c0682e Binary files /dev/null and b/public/og-zoom-generator.png differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..b32eec0 --- /dev/null +++ b/public/robots.txt @@ -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/ diff --git a/src/app/(app)/create/page.tsx b/src/app/(app)/create/page.tsx index 56c5e55..8dc134b 100644 --- a/src/app/(app)/create/page.tsx +++ b/src/app/(app)/create/page.tsx @@ -1,25 +1,51 @@ 'use client'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { QRCodeSVG } from 'qrcode.react'; +import { toPng } from 'html-to-image'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Input } from '@/components/ui/Input'; import { Select } from '@/components/ui/Select'; import { Button } from '@/components/ui/Button'; import { Badge } from '@/components/ui/Badge'; -import { calculateContrast } from '@/lib/utils'; +import { calculateContrast, cn } from '@/lib/utils'; import { useTranslation } from '@/hooks/useTranslation'; import { useCsrf } from '@/hooks/useCsrf'; import { showToast } from '@/components/ui/Toast'; +// Content-type specific frame options +const getFrameOptionsForContentType = (contentType: string) => { + const baseOptions = [{ id: 'none', label: 'No Frame' }, { id: 'scanme', label: 'Scan Me' }]; + + switch (contentType) { + case 'URL': + return [...baseOptions, { id: 'website', label: 'Website' }, { id: 'visit', label: 'Visit' }]; + case 'PHONE': + return [...baseOptions, { id: 'callme', label: 'Call Me' }, { id: 'call', label: 'Call' }]; + case 'GEO': + return [...baseOptions, { id: 'findus', label: 'Find Us' }, { id: 'navigate', label: 'Navigate' }]; + case 'VCARD': + return [...baseOptions, { id: 'contact', label: 'Contact' }, { id: 'save', label: 'Save' }]; + case 'SMS': + return [...baseOptions, { id: 'textme', label: 'Text Me' }, { id: 'message', label: 'Message' }]; + case 'WHATSAPP': + return [...baseOptions, { id: 'chatme', label: 'Chat Me' }, { id: 'whatsapp', label: 'WhatsApp' }]; + case 'TEXT': + return [...baseOptions, { id: 'read', label: 'Read' }, { id: 'info', label: 'Info' }]; + default: + return [...baseOptions, { id: 'website', label: 'Website' }, { id: 'visit', label: 'Visit' }]; + } +}; + export default function CreatePage() { const router = useRouter(); const { t } = useTranslation(); const { fetchWithCsrf } = useCsrf(); const [loading, setLoading] = useState(false); const [userPlan, setUserPlan] = useState('FREE'); + const qrRef = useRef(null); // Form state const [title, setTitle] = useState(''); @@ -32,6 +58,18 @@ export default function CreatePage() { const [backgroundColor, setBackgroundColor] = useState('#FFFFFF'); const [cornerStyle, setCornerStyle] = useState('square'); const [size, setSize] = useState(200); + const [frameType, setFrameType] = useState('none'); + + // Get frame options for current content type + const frameOptions = getFrameOptionsForContentType(contentType); + + // Reset frame type when content type changes (if current frame is not valid) + useEffect(() => { + const validIds = frameOptions.map(f => f.id); + if (!validIds.includes(frameType)) { + setFrameType('none'); + } + }, [contentType, frameOptions, frameType]); // Logo state const [logoUrl, setLogoUrl] = useState(''); @@ -97,61 +135,58 @@ export default function CreatePage() { const qrContent = getQRContent(); + const getFrameLabel = () => { + const frame = frameOptions.find((f: { id: string; label: string }) => f.id === frameType); + return frame?.id !== 'none' ? frame?.label : null; + }; + const downloadQR = async (format: 'svg' | 'png') => { + if (!qrRef.current) return; try { - // Get the content based on content type - let qrContent = ''; - switch (contentType) { - case 'URL': - qrContent = content.url || ''; - break; - case 'PHONE': - qrContent = `tel:${content.phone || ''}`; - break; - case 'EMAIL': - qrContent = `mailto:${content.email || ''}${content.subject ? `?subject=${encodeURIComponent(content.subject)}` : ''}`; - break; - case 'TEXT': - qrContent = content.text || ''; - break; - default: - qrContent = content.url || ''; - } - - if (!qrContent) return; - - const QRCode = (await import('qrcode')).default; - - if (format === 'svg') { - const svg = await QRCode.toString(qrContent, { - type: 'svg', - width: size, - margin: 2, - color: { - dark: foregroundColor, - light: backgroundColor, - }, - }); - - const blob = new Blob([svg], { type: 'image/svg+xml' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `qrcode-${title || 'download'}.svg`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); + if (format === 'png') { + const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' }); + const link = document.createElement('a'); + link.download = `qrcode-${title || 'download'}.png`; + link.href = dataUrl; + link.click(); } else { - const a = document.createElement('a'); - a.href = qrDataUrl; - a.download = `qrcode-${title || 'download'}.png`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); + // For SVG, we might still want to use the library or just toPng if SVG export of HTML is not needed + // Simplest is to check if we can export the SVG element directly but that misses the frame HTML. + // html-to-image can generate SVG too. + // But usually for SVG users want the vector. Capturing HTML to SVG is possible but complex. + // For now, let's just stick to the SVG code export if NO FRAME is selected, + // otherwise warn or use toPng (as SVG). + // Actually, the previous implementation was good for pure QR. + // If frame is selected, we MUST use a raster export (PNG) or complex HTML-to-SVG. + // Let's rely on toPng for consistency with frames. + const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' }); + // Wait, exporting HTML to valid vector SVG is hard. + // Let's just offer PNG for frames for now to be safe, or just use the same PNG download for both buttons if frame is active? + // No, let's try to grab the INNER SVG if no frame, else... + if (frameType === 'none') { + const svgElement = qrRef.current.querySelector('svg'); + if (svgElement) { + const svgData = new XMLSerializer().serializeToString(svgElement); + const blob = new Blob([svgData], { type: 'image/svg+xml' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `qrcode-${title || 'download'}.svg`; + a.click(); + URL.revokeObjectURL(url); + } + } else { + showToast('SVG download not available with frames yet. Downloading PNG instead.', 'info'); + const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' }); + const link = document.createElement('a'); + link.download = `qrcode-${title || 'download'}.png`; + link.href = dataUrl; + link.click(); + } } } catch (err) { console.error('Error downloading QR code:', err); + showToast('Error downloading QR code', 'error'); } }; @@ -220,6 +255,7 @@ export default function CreatePage() { width: logoSize, excavate, } : undefined, + frameType, // Save frame type }, }; @@ -448,7 +484,7 @@ export default function CreatePage() { )} - + {!canCustomizeColors && (

@@ -461,6 +497,29 @@ export default function CreatePage() {

)} + + {/* Frame Options */} +
+ +
+ {frameOptions.map((frame: { id: string; label: string }) => ( + + ))} +
+
+