revops + onboarding
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { toPng } from 'html-to-image';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
@@ -11,12 +11,14 @@ import { Select } from '@/components/ui/Select';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
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, Upload, Barcode as BarcodeIcon
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useCsrf } from '@/hooks/useCsrf';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
import { trackEvent } from '@/components/PostHogProvider';
|
||||
import { appendRedirectParam, sanitizeRedirectPath } from '@/lib/auth-flow';
|
||||
import {
|
||||
Globe, User, MapPin, Phone, FileText, Smartphone, Ticket, Star, HelpCircle, Upload, Barcode as BarcodeIcon
|
||||
} from 'lucide-react';
|
||||
import Barcode from 'react-barcode';
|
||||
|
||||
// Tooltip component for form field help
|
||||
@@ -99,9 +101,10 @@ function addBarcodeCaptionToSvg(svgElement: SVGElement, caption: string): string
|
||||
return new XMLSerializer().serializeToString(cloned);
|
||||
}
|
||||
|
||||
export default function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
export default function CreatePage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { t } = useTranslation();
|
||||
const { fetchWithCsrf } = useCsrf();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
@@ -145,14 +148,14 @@ export default function CreatePage() {
|
||||
const [excavate, setExcavate] = useState(true);
|
||||
|
||||
// QR preview
|
||||
const [qrDataUrl, setQrDataUrl] = useState('');
|
||||
const [qrDataUrl, setQrDataUrl] = useState('');
|
||||
|
||||
// Check if user can customize colors (PRO+ only)
|
||||
const canCustomizeColors = userPlan === 'PRO' || userPlan === 'BUSINESS';
|
||||
|
||||
// Load user plan
|
||||
useEffect(() => {
|
||||
const fetchUserPlan = async () => {
|
||||
useEffect(() => {
|
||||
const fetchUserPlan = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/user/plan');
|
||||
if (response.ok) {
|
||||
@@ -163,8 +166,44 @@ export default function CreatePage() {
|
||||
console.error('Error fetching user plan:', error);
|
||||
}
|
||||
};
|
||||
fetchUserPlan();
|
||||
}, []);
|
||||
fetchUserPlan();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const queryContentType = searchParams.get('contentType');
|
||||
const useCase = searchParams.get('useCase');
|
||||
const titleParam = searchParams.get('title');
|
||||
const isDynamicParam = searchParams.get('dynamic');
|
||||
|
||||
if (queryContentType) {
|
||||
setContentType(queryContentType);
|
||||
}
|
||||
|
||||
if (titleParam) {
|
||||
setTitle(titleParam);
|
||||
}
|
||||
|
||||
if (isDynamicParam) {
|
||||
setIsDynamic(isDynamicParam === '1');
|
||||
}
|
||||
|
||||
if (useCase === 'menu_pdf') {
|
||||
setContent((prev: any) => ({ ...prev, fileUrl: prev.fileUrl || '' }));
|
||||
} else if (useCase === 'contact_card') {
|
||||
setContent((prev: any) => ({
|
||||
...prev,
|
||||
firstName: prev.firstName || '',
|
||||
lastName: prev.lastName || '',
|
||||
}));
|
||||
} else if (useCase === 'barcode') {
|
||||
setContent((prev: any) => ({
|
||||
...prev,
|
||||
format: prev.format || 'CODE128',
|
||||
}));
|
||||
} else if (queryContentType === 'URL') {
|
||||
setContent((prev: any) => ({ ...prev, url: prev.url || '' }));
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
const contrast = calculateContrast(foregroundColor, backgroundColor);
|
||||
const hasGoodContrast = contrast >= 4.5;
|
||||
@@ -226,13 +265,19 @@ export default function CreatePage() {
|
||||
const downloadQR = async (format: 'svg' | 'png') => {
|
||||
if (!qrRef.current) return;
|
||||
try {
|
||||
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 {
|
||||
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();
|
||||
trackEvent('qr_code_downloaded', {
|
||||
format: 'png',
|
||||
content_type: contentType,
|
||||
qr_type: isDynamic ? 'dynamic' : 'static',
|
||||
plan: userPlan,
|
||||
});
|
||||
} else {
|
||||
// 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.
|
||||
@@ -254,21 +299,34 @@ export default function CreatePage() {
|
||||
: 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();
|
||||
}
|
||||
}
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `qrcode-${title || 'download'}.svg`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
trackEvent('qr_code_downloaded', {
|
||||
format: 'svg',
|
||||
content_type: contentType,
|
||||
qr_type: isDynamic ? 'dynamic' : 'static',
|
||||
plan: userPlan,
|
||||
});
|
||||
}
|
||||
} 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();
|
||||
trackEvent('qr_code_downloaded', {
|
||||
format: 'png',
|
||||
content_type: contentType,
|
||||
qr_type: isDynamic ? 'dynamic' : 'static',
|
||||
plan: userPlan,
|
||||
fallback_from: 'svg_with_frame',
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error downloading QR code:', err);
|
||||
showToast('Error downloading QR code', 'error');
|
||||
@@ -354,18 +412,38 @@ export default function CreatePage() {
|
||||
const responseData = await response.json();
|
||||
console.log('RESPONSE DATA:', responseData);
|
||||
|
||||
if (response.ok) {
|
||||
showToast(`QR Code "${title}" created successfully!`, 'success');
|
||||
|
||||
// Wait a moment so user sees the toast, then redirect
|
||||
setTimeout(() => {
|
||||
router.push('/dashboard');
|
||||
router.refresh();
|
||||
}, 1000);
|
||||
} else {
|
||||
console.error('Error creating QR code:', responseData);
|
||||
showToast(responseData.error || 'Error creating QR code', 'error');
|
||||
}
|
||||
if (response.ok) {
|
||||
trackEvent('qr_code_created', {
|
||||
content_type: contentType,
|
||||
qr_type: isDynamic ? 'dynamic' : 'static',
|
||||
plan: userPlan,
|
||||
has_logo: Boolean(logoUrl),
|
||||
frame_type: frameType,
|
||||
});
|
||||
|
||||
showToast(`QR Code "${title}" created successfully!`, 'success');
|
||||
|
||||
// Wait a moment so user sees the toast, then redirect
|
||||
setTimeout(() => {
|
||||
const redirectTarget = sanitizeRedirectPath(searchParams.get('redirect'));
|
||||
if (searchParams.get('onboarding') === '1') {
|
||||
router.push(appendRedirectParam('/onboarding', redirectTarget, { step: '8' }));
|
||||
} else {
|
||||
router.push('/dashboard');
|
||||
}
|
||||
router.refresh();
|
||||
}, 1000);
|
||||
} else {
|
||||
console.error('Error creating QR code:', responseData);
|
||||
|
||||
if (response.status === 403 && responseData.error === 'Limit reached') {
|
||||
showToast(responseData.message || 'You have reached your plan limit.', 'error');
|
||||
router.push('/pricing?reason=limit_reached');
|
||||
return;
|
||||
}
|
||||
|
||||
showToast(responseData.error || 'Error creating QR code', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating QR code:', error);
|
||||
showToast('Error creating QR code. Please try again.', 'error');
|
||||
@@ -1180,4 +1258,4 @@ export default function CreatePage() {
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user