feat: Add Guest QR Generator, HomeNavbar, and enforce SVG tier limits
This commit is contained in:
@@ -15,6 +15,7 @@ export default function CreateQRPage() {
|
||||
const [customSlug, setCustomSlug] = useState('');
|
||||
const [randomSlug, setRandomSlug] = useState('');
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [userRole, setUserRole] = useState<string>('free');
|
||||
const baseUrl = typeof window !== 'undefined' ? (process.env.NEXT_PUBLIC_APP_URL || 'https://qr.houseofwebsites.ai') : 'https://qr.houseofwebsites.ai';
|
||||
|
||||
const [qrOptions, setQrOptions] = useState<Options>({
|
||||
@@ -55,6 +56,13 @@ export default function CreateQRPage() {
|
||||
} else {
|
||||
setQrOptions(prev => ({ ...prev, data: destinationUrl }));
|
||||
}
|
||||
|
||||
// Fetch user role
|
||||
api.get('/auth/me').then(res => {
|
||||
if (res.data?.user?.role) {
|
||||
setUserRole(res.data.user.role);
|
||||
}
|
||||
}).catch(() => {});
|
||||
}, [baseUrl]);
|
||||
|
||||
// Keep QR data in sync with type and destinationUrl
|
||||
@@ -124,9 +132,15 @@ export default function CreateQRPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDownload('svg')}
|
||||
className="bg-white border border-gray-300 text-gray-700 px-6 py-2.5 rounded-lg hover:bg-gray-50 transition-colors font-semibold shadow-sm"
|
||||
disabled={userRole === 'free' || userRole === 'free_trial'}
|
||||
className={`px-6 py-2.5 rounded-lg font-semibold shadow-sm transition-colors border border-gray-300 ${
|
||||
userRole === 'free' || userRole === 'free_trial'
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-white text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
title={userRole === 'free' || userRole === 'free_trial' ? 'SVG export is for Pro users' : 'Download SVG'}
|
||||
>
|
||||
Download SVG
|
||||
{userRole === 'free' || userRole === 'free_trial' ? 'Download SVG (Pro)' : 'Download SVG'}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -16,6 +16,7 @@ interface QRCode {
|
||||
export default function DashboardPage() {
|
||||
const [qrs, setQrs] = useState<QRCode[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [userRole, setUserRole] = useState<string>('free');
|
||||
|
||||
// Edit Modal State
|
||||
const [editingQr, setEditingQr] = useState<QRCode | null>(null);
|
||||
@@ -27,10 +28,16 @@ export default function DashboardPage() {
|
||||
|
||||
const fetchQRs = async () => {
|
||||
try {
|
||||
const res = await api.get('/qr');
|
||||
const [res, authRes] = await Promise.all([
|
||||
api.get('/qr'),
|
||||
api.get('/auth/me').catch(() => null)
|
||||
]);
|
||||
setQrs(res.data.qr_codes || []);
|
||||
if (authRes?.data?.user?.role) {
|
||||
setUserRole(authRes.data.user.role);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch QRs", err);
|
||||
console.error("Failed to fetch data", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -197,10 +204,15 @@ export default function DashboardPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDownload(qr, 'svg')}
|
||||
className="flex-1 bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-2 rounded-lg transition-colors flex justify-center items-center text-sm shadow-sm"
|
||||
title="Download SVG"
|
||||
disabled={userRole === 'free' || userRole === 'free_trial'}
|
||||
className={`flex-1 font-bold py-2 px-2 rounded-lg transition-colors flex justify-center items-center text-sm shadow-sm ${
|
||||
userRole === 'free' || userRole === 'free_trial'
|
||||
? 'bg-gray-200 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-green-500 hover:bg-green-600 text-white'
|
||||
}`}
|
||||
title={userRole === 'free' || userRole === 'free_trial' ? 'SVG export is for Pro users' : 'Download SVG'}
|
||||
>
|
||||
SVG
|
||||
{userRole === 'free' || userRole === 'free_trial' ? 'SVG (Pro)' : 'SVG'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
+11
-21
@@ -1,31 +1,13 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import HomeNavbar from "@/components/HomeNavbar";
|
||||
import GuestQRGenerator from "@/components/GuestQRGenerator";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 flex flex-col font-sans text-gray-100">
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="fixed w-full z-50 bg-gray-900/80 backdrop-blur-md border-b border-gray-800">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-20 items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg shadow-indigo-500/30">
|
||||
<span className="text-white font-black text-xl tracking-tighter">CQ</span>
|
||||
</div>
|
||||
<span className="font-extrabold text-2xl tracking-tight text-white">CK-QR</span>
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">
|
||||
<Link href="/login" className="text-gray-300 hover:text-white font-medium px-4 transition-colors">
|
||||
Login
|
||||
</Link>
|
||||
<Link href="/register" className="bg-white text-gray-900 px-6 py-2.5 rounded-full font-bold hover:bg-gray-100 transition-colors shadow-lg hover:shadow-white/20">
|
||||
Start Free Trial
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<HomeNavbar />
|
||||
|
||||
{/* Hero Section with Parallax */}
|
||||
<div
|
||||
@@ -77,6 +59,14 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Guest Static QR Generator Section */}
|
||||
<div className="bg-gray-950 py-20 relative z-10">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<GuestQRGenerator />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Trust & Scale Metrics (Data Section) */}
|
||||
<div className="bg-gray-900 border-y border-gray-800 py-12">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Link from 'next/link';
|
||||
|
||||
// Load LivePreview dynamically since it depends on browser APIs
|
||||
const LivePreview = dynamic(() => import('@/components/qr-editor/LivePreview'), { ssr: false });
|
||||
|
||||
export default function GuestQRGenerator() {
|
||||
const [url, setUrl] = useState('https://ck-qr.com');
|
||||
|
||||
const defaultOptions = {
|
||||
width: 300,
|
||||
height: 300,
|
||||
type: 'svg' as const,
|
||||
data: url || 'https://ck-qr.com',
|
||||
margin: 10,
|
||||
qrOptions: {
|
||||
typeNumber: 0 as const,
|
||||
mode: 'Byte' as const,
|
||||
errorCorrectionLevel: 'Q' as const
|
||||
},
|
||||
imageOptions: {
|
||||
hideBackgroundDots: true,
|
||||
imageSize: 0.4,
|
||||
margin: 5
|
||||
},
|
||||
dotsOptions: {
|
||||
color: '#4f46e5',
|
||||
type: 'rounded' as const
|
||||
},
|
||||
backgroundOptions: {
|
||||
color: '#ffffff',
|
||||
},
|
||||
cornersSquareOptions: {
|
||||
color: '#4f46e5',
|
||||
type: 'extra-rounded' as const
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const QRCodeStyling = (await import('qr-code-styling')).default;
|
||||
const qrCode = new QRCodeStyling(defaultOptions);
|
||||
qrCode.download({ name: 'Guest-Static-QR', extension: 'png' });
|
||||
} catch (err) {
|
||||
console.error('Download failed', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 border border-gray-800 p-8 rounded-3xl lg:p-12 shadow-2xl relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/10 rounded-full blur-3xl"></div>
|
||||
<div className="absolute bottom-0 left-0 w-64 h-64 bg-purple-500/10 rounded-full blur-3xl"></div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-12 items-center relative z-10">
|
||||
<div className="flex-1 w-full text-center lg:text-left">
|
||||
<h2 className="text-3xl lg:text-4xl font-black text-white mb-4">Try it now, for free.</h2>
|
||||
<p className="text-gray-400 text-lg mb-8">
|
||||
Enter a destination URL below to instantly generate a basic static QR code. No signup required.
|
||||
</p>
|
||||
|
||||
<div className="mb-6">
|
||||
<input
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder="https://yourwebsite.com"
|
||||
className="w-full bg-gray-950 border border-gray-700 text-white rounded-xl px-4 py-4 focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all font-medium"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-8">
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="flex-1 bg-gray-800 hover:bg-gray-700 border border-gray-700 text-white font-bold py-3 px-6 rounded-xl transition-colors shadow-sm"
|
||||
>
|
||||
Download PNG
|
||||
</button>
|
||||
<Link
|
||||
href="/register"
|
||||
className="flex-1 bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-3 px-6 rounded-xl transition-colors shadow-lg shadow-indigo-500/25 flex items-center justify-center gap-2"
|
||||
>
|
||||
Track Scans (Free Trial)
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" /></svg>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-500">
|
||||
Static QR codes cannot be edited later and do not track scan analytics.
|
||||
<Link href="/register" className="text-indigo-400 hover:text-indigo-300 font-medium ml-1">Upgrade to track.</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full lg:w-1/3 flex justify-center">
|
||||
<div className="pointer-events-none transform scale-90 sm:scale-100">
|
||||
<LivePreview options={defaultOptions} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import api from '@/lib/api';
|
||||
|
||||
export default function HomeNavbar() {
|
||||
const [user, setUser] = useState<{ email: string; role: string } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user is logged in
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const res = await api.get('/auth/me');
|
||||
if (res.data?.user) {
|
||||
setUser(res.data.user);
|
||||
}
|
||||
} catch (err) {
|
||||
// Not logged in or error
|
||||
}
|
||||
};
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<nav className="fixed w-full z-50 bg-gray-900/80 backdrop-blur-md border-b border-gray-800">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-20 items-center">
|
||||
<Link href="/" className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg shadow-indigo-500/30">
|
||||
<span className="text-white font-black text-xl tracking-tighter">CQ</span>
|
||||
</div>
|
||||
<span className="font-extrabold text-2xl tracking-tight text-white">CK-QR</span>
|
||||
</Link>
|
||||
<div className="flex gap-4 items-center">
|
||||
{user ? (
|
||||
<>
|
||||
<Link href="/dashboard" className="text-gray-300 hover:text-white font-medium px-4 transition-colors flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-indigo-600 text-white rounded-full flex items-center justify-center text-sm font-bold uppercase">
|
||||
{user.email.charAt(0)}
|
||||
</div>
|
||||
Dashboard
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link href="/login" className="text-gray-300 hover:text-white font-medium px-4 transition-colors">
|
||||
Login
|
||||
</Link>
|
||||
<Link href="/register" className="bg-white text-gray-900 px-6 py-2.5 rounded-full font-bold hover:bg-gray-100 transition-colors shadow-lg hover:shadow-white/20">
|
||||
Start Free Trial
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user