Add client code directly
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
import { useEffect, useState, Suspense } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
function AdContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const destinationUrl = searchParams.get('url');
|
||||
const [countdown, setCountdown] = useState(3);
|
||||
|
||||
useEffect(() => {
|
||||
if (!destinationUrl) return;
|
||||
|
||||
if (countdown > 0) {
|
||||
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
} else {
|
||||
// Countdown finished, redirect
|
||||
window.location.replace(destinationUrl);
|
||||
}
|
||||
}, [countdown, destinationUrl]);
|
||||
|
||||
if (!destinationUrl) {
|
||||
return <div className="p-8 text-center text-red-500">Invalid routing parameters.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center p-4">
|
||||
<div className="max-w-2xl w-full bg-white rounded-2xl shadow-xl overflow-hidden text-center">
|
||||
|
||||
{/* Ad Placeholder Header */}
|
||||
<div className="bg-indigo-600 p-4 text-white">
|
||||
<p className="text-sm uppercase tracking-wider font-semibold opacity-80">Advertisement</p>
|
||||
<h1 className="text-2xl font-bold mt-1">Upgrade to CK-QR Pro to remove ads!</h1>
|
||||
</div>
|
||||
|
||||
{/* Ad Content Body */}
|
||||
<div className="p-12">
|
||||
<div className="w-full h-48 bg-gray-200 rounded-lg border-2 border-dashed border-gray-300 flex items-center justify-center mb-8">
|
||||
<span className="text-gray-400 font-medium">[ Ad Space ]</span>
|
||||
</div>
|
||||
|
||||
<div className="text-gray-600">
|
||||
<p className="mb-2">You are being redirected to your destination in...</p>
|
||||
<div className="text-5xl font-black text-indigo-600 my-4">{countdown}</div>
|
||||
<p className="text-sm">seconds</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AdRedirectPage() {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<AdContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex font-sans">
|
||||
|
||||
{/* Admin Sidebar */}
|
||||
<div className="w-64 bg-gray-900 border-r border-gray-800 flex flex-col hidden md:flex">
|
||||
<div className="h-20 flex items-center px-8 border-b border-gray-800">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold tracking-tighter">CQ</span>
|
||||
</div>
|
||||
<span className="text-white font-bold text-lg tracking-tight">Admin Portal</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto py-6 px-4 space-y-2">
|
||||
<Link href="/admin" className="flex items-center gap-3 text-gray-400 hover:text-white hover:bg-gray-800 px-4 py-3 rounded-xl transition-colors font-medium">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
|
||||
User Management
|
||||
</Link>
|
||||
<Link href="/admin/settings" className="flex items-center gap-3 text-gray-400 hover:text-white hover:bg-gray-800 px-4 py-3 rounded-xl transition-colors font-medium">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><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>
|
||||
API Integrations
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-gray-800">
|
||||
<Link href="/login" className="flex items-center gap-3 text-gray-500 hover:text-red-400 px-4 py-2 transition-colors font-medium">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></svg>
|
||||
Log Out
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex-1 flex flex-col h-screen overflow-hidden">
|
||||
<main className="flex-1 overflow-y-auto bg-gray-50">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
'use client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import api from '@/lib/api';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
role: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export default function AdminPage() {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const res = await api.get('/admin/users');
|
||||
setUsers(res.data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
const handleTierChange = async (userId: string, newRole: string) => {
|
||||
try {
|
||||
await api.put(`/admin/users/${userId}/tier`, { role: newRole });
|
||||
alert('User tier updated successfully!');
|
||||
fetchUsers();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Failed to update tier');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-10 max-w-6xl mx-auto font-sans">
|
||||
<div className="mb-10">
|
||||
<h1 className="text-4xl font-extrabold text-gray-900 tracking-tight">User Management</h1>
|
||||
<p className="text-gray-500 mt-2 font-medium">Manage user subscriptions, trial statuses, and roles.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-xl shadow-gray-200/50 border border-gray-200 overflow-hidden">
|
||||
|
||||
<div className="p-6 border-b border-gray-100 bg-gray-50/50 flex justify-between items-center">
|
||||
<h3 className="text-lg font-bold text-gray-800">Platform Users</h3>
|
||||
<span className="bg-indigo-100 text-indigo-700 font-bold px-3 py-1 rounded-full text-xs">{users.length} Total</span>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="p-10 text-center text-gray-500 font-medium">Loading users...</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 bg-gray-50">
|
||||
<th className="px-6 py-4 text-xs font-bold text-gray-500 uppercase tracking-widest">Account / Email</th>
|
||||
<th className="px-6 py-4 text-xs font-bold text-gray-500 uppercase tracking-widest">Status / Tier</th>
|
||||
<th className="px-6 py-4 text-xs font-bold text-gray-500 uppercase tracking-widest">Joined</th>
|
||||
<th className="px-6 py-4 text-xs font-bold text-gray-500 uppercase tracking-widest text-right">Admin Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 bg-white">
|
||||
{users.map((user) => (
|
||||
<tr key={user.id} className="hover:bg-indigo-50/30 transition-colors">
|
||||
<td className="px-6 py-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-indigo-100 to-purple-100 flex items-center justify-center border border-indigo-200 text-indigo-700 font-bold">
|
||||
{user.email.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="font-bold text-gray-900">{user.email}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-5">
|
||||
<span className={`px-3 py-1 inline-flex text-xs font-bold rounded-full border shadow-sm
|
||||
${user.role === 'pro' ? 'bg-green-50 text-green-700 border-green-200' :
|
||||
user.role === 'enterprise' ? 'bg-purple-50 text-purple-700 border-purple-200' :
|
||||
user.role === 'free_trial' ? 'bg-amber-50 text-amber-700 border-amber-200' :
|
||||
'bg-gray-50 text-gray-700 border-gray-200'}`}>
|
||||
{user.role === 'free_trial' ? 'FREE TRIAL' : user.role.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-5 text-sm font-medium text-gray-500">
|
||||
{new Date(user.created_at).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })}
|
||||
</td>
|
||||
<td className="px-6 py-5 text-right">
|
||||
<select
|
||||
value={user.role}
|
||||
onChange={(e) => handleTierChange(user.id, e.target.value)}
|
||||
className="bg-white border-gray-300 rounded-lg shadow-sm py-2 px-3 border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm font-medium text-gray-700 hover:border-indigo-400 transition-colors cursor-pointer"
|
||||
>
|
||||
<option value="free">Free Tier</option>
|
||||
<option value="free_trial">14-Day Trial</option>
|
||||
<option value="pro">Pro Tier</option>
|
||||
<option value="enterprise">Enterprise</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import api from '@/lib/api';
|
||||
|
||||
interface Setting {
|
||||
key: string;
|
||||
value: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
export default function AdminSettingsPage() {
|
||||
const [settings, setSettings] = useState<Setting[]>([]);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
const res = await api.get('/admin/settings');
|
||||
setSettings(res.data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (key: string, newValue: string) => {
|
||||
setSettings(prev => prev.map(s => s.key === key ? { ...s, value: newValue } : s));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setIsSaving(true);
|
||||
const payload = settings.map(s => ({ key: s.key, value: s.value }));
|
||||
await api.put('/admin/settings', { settings: payload });
|
||||
alert('Settings saved successfully!');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Failed to save settings.');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const groupedSettings = settings.reduce((acc, curr) => {
|
||||
if (!acc[curr.category]) acc[curr.category] = [];
|
||||
acc[curr.category].push(curr);
|
||||
return acc;
|
||||
}, {} as Record<string, Setting[]>);
|
||||
|
||||
return (
|
||||
<div className="p-10 max-w-5xl mx-auto font-sans pb-32">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-10 gap-4">
|
||||
<div>
|
||||
<h1 className="text-4xl font-extrabold text-gray-900 tracking-tight">API Integrations</h1>
|
||||
<p className="text-gray-500 mt-2 font-medium">Securely manage your 3rd-party webhook and API keys.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="bg-indigo-600 text-white px-8 py-3 rounded-xl font-bold hover:bg-indigo-700 disabled:opacity-50 transition-all shadow-lg hover:shadow-indigo-500/30 flex items-center gap-2"
|
||||
>
|
||||
{isSaving ? (
|
||||
'Saving to Database...'
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" /></svg>
|
||||
Save Configuration
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{Object.keys(groupedSettings).map(category => (
|
||||
<div key={category} className="bg-white rounded-2xl shadow-xl shadow-gray-200/50 border border-gray-200 overflow-hidden relative">
|
||||
|
||||
{/* Category Header */}
|
||||
<div className="bg-gradient-to-r from-gray-50 to-white px-8 py-5 border-b border-gray-100 flex items-center gap-3">
|
||||
<div className="w-2 h-6 bg-indigo-500 rounded-full"></div>
|
||||
<h2 className="text-xl font-bold text-gray-900 capitalize">{category} Configuration</h2>
|
||||
</div>
|
||||
|
||||
<div className="p-8 grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{groupedSettings[category].map(setting => {
|
||||
const isSecret = setting.key.includes('key') || setting.key.includes('secret') || setting.key.includes('password');
|
||||
return (
|
||||
<div key={setting.key} className="space-y-2">
|
||||
<label className="block text-sm font-bold text-gray-700 uppercase tracking-wider">
|
||||
{setting.key.replace(/_/g, ' ')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={isSecret ? 'password' : 'text'}
|
||||
value={setting.value || ''}
|
||||
onChange={(e) => handleInputChange(setting.key, e.target.value)}
|
||||
className="w-full bg-gray-50 border-gray-300 rounded-xl shadow-inner px-4 py-3 border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 font-mono text-sm text-gray-800 placeholder-gray-400 transition-colors"
|
||||
placeholder={`Enter ${setting.key}`}
|
||||
/>
|
||||
{isSecret && (
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<svg className="w-5 h-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col font-sans">
|
||||
<nav className="bg-gray-900 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-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="flex-1 max-w-4xl mx-auto w-full px-6 py-16">
|
||||
<h1 className="text-5xl font-black text-gray-900 mb-6">About Us</h1>
|
||||
<p className="text-xl text-gray-600 mb-10 leading-relaxed">
|
||||
We are bridging the gap between the physical and digital worlds.
|
||||
</p>
|
||||
<div className="prose prose-lg prose-indigo max-w-none text-gray-700">
|
||||
<h2>Our Mission</h2>
|
||||
<p>
|
||||
At CK-QR, we believe that physical marketing shouldn't be static. In a world where data moves at the speed of light, printed materials have historically been left behind. Our mission is to give every physical object a dynamic digital heartbeat.
|
||||
</p>
|
||||
<h2>The Team</h2>
|
||||
<p>
|
||||
We are a remote-first team of engineers, designers, and marketers who are passionate about building highly scalable, fault-tolerant infrastructure. We handle billions of requests per month so our customers never have to worry about a broken link.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function BlogPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col font-sans">
|
||||
<nav className="bg-gray-900 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-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="flex-1 max-w-4xl mx-auto w-full px-6 py-16">
|
||||
<h1 className="text-5xl font-black text-gray-900 mb-6">Engineering Blog</h1>
|
||||
<p className="text-xl text-gray-600 mb-10 leading-relaxed">
|
||||
Deep dives into how we scale dynamic routing globally.
|
||||
</p>
|
||||
<div className="prose prose-lg prose-indigo max-w-none text-gray-700">
|
||||
<p>
|
||||
Our blog is currently undergoing a migration. Please check back soon for articles on Edge computing, Node.js performance, and modern frontend architecture.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function CareersPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col font-sans">
|
||||
<nav className="bg-gray-900 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-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="flex-1 max-w-4xl mx-auto w-full px-6 py-16">
|
||||
<h1 className="text-5xl font-black text-gray-900 mb-6">Careers</h1>
|
||||
<p className="text-xl text-gray-600 mb-10 leading-relaxed">
|
||||
Help us build the future of dynamic routing.
|
||||
</p>
|
||||
<div className="prose prose-lg prose-indigo max-w-none text-gray-700">
|
||||
<h2>Open Positions</h2>
|
||||
<p>
|
||||
We are always looking for talented individuals to join our team. While we don't have any specific roles open right now, we are always happy to hear from exceptional people.
|
||||
</p>
|
||||
<ul className="mt-8">
|
||||
<li className="p-4 bg-white rounded-lg shadow-sm border border-gray-200 mb-4 list-none">
|
||||
<h3 className="text-lg font-bold m-0 text-gray-900">Senior Full-Stack Engineer</h3>
|
||||
<p className="text-sm text-gray-500 m-0">Remote (US/EU) - Next.js, Node, Postgres</p>
|
||||
</li>
|
||||
<li className="p-4 bg-white rounded-lg shadow-sm border border-gray-200 list-none">
|
||||
<h3 className="text-lg font-bold m-0 text-gray-900">Product Marketing Manager</h3>
|
||||
<p className="text-sm text-gray-500 m-0">Remote (Global) - B2B SaaS</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col font-sans">
|
||||
<nav className="bg-gray-900 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-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="flex-1 max-w-4xl mx-auto w-full px-6 py-16">
|
||||
<h1 className="text-5xl font-black text-gray-900 mb-6">Contact Sales</h1>
|
||||
<p className="text-xl text-gray-600 mb-10 leading-relaxed">
|
||||
Looking for a custom enterprise plan or have technical questions? We're here to help.
|
||||
</p>
|
||||
<div className="bg-white p-8 rounded-2xl shadow-sm border border-gray-200">
|
||||
<form className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-2">First Name</label>
|
||||
<input type="text" className="w-full bg-gray-50 border border-gray-300 rounded-xl px-4 py-3" placeholder="Jane" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-2">Last Name</label>
|
||||
<input type="text" className="w-full bg-gray-50 border border-gray-300 rounded-xl px-4 py-3" placeholder="Doe" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-2">Work Email</label>
|
||||
<input type="email" className="w-full bg-gray-50 border border-gray-300 rounded-xl px-4 py-3" placeholder="jane@company.com" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-2">Message</label>
|
||||
<textarea rows={4} className="w-full bg-gray-50 border border-gray-300 rounded-xl px-4 py-3" placeholder="How can we help?"></textarea>
|
||||
</div>
|
||||
<button type="button" onClick={(e) => { e.preventDefault(); alert('Form submitted!'); }} className="bg-indigo-600 text-white font-bold py-4 px-8 rounded-xl hover:bg-indigo-700 transition-colors w-full sm:w-auto">
|
||||
Send Message
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
'use client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import api from '@/lib/api';
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell
|
||||
} from 'recharts';
|
||||
|
||||
interface QRCode {
|
||||
id: string;
|
||||
name: string;
|
||||
short_url_id: string;
|
||||
}
|
||||
|
||||
interface AnalyticsData {
|
||||
totalScans: number;
|
||||
osBreakdown: { os: string; count: string }[];
|
||||
deviceBreakdown: { device_type: string; count: string }[];
|
||||
timeline: { date: string; count: string }[];
|
||||
}
|
||||
|
||||
const COLORS = ['#4f46e5', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
|
||||
|
||||
export default function AnalyticsPage() {
|
||||
const [qrs, setQrs] = useState<QRCode[]>([]);
|
||||
const [selectedQrId, setSelectedQrId] = useState<string>('');
|
||||
const [analytics, setAnalytics] = useState<AnalyticsData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingAnalytics, setLoadingAnalytics] = useState(false);
|
||||
|
||||
// Fetch QRs on mount
|
||||
useEffect(() => {
|
||||
const fetchQRs = async () => {
|
||||
try {
|
||||
const res = await api.get('/qr');
|
||||
const qrList = res.data.qr_codes || [];
|
||||
setQrs(qrList);
|
||||
if (qrList.length > 0) {
|
||||
setSelectedQrId(qrList[0].id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch QRs", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchQRs();
|
||||
}, []);
|
||||
|
||||
// Fetch analytics when selectedQrId changes
|
||||
useEffect(() => {
|
||||
if (!selectedQrId) return;
|
||||
|
||||
const fetchAnalytics = async () => {
|
||||
setLoadingAnalytics(true);
|
||||
try {
|
||||
const res = await api.get(`/analytics/${selectedQrId}`);
|
||||
setAnalytics(res.data);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch analytics", err);
|
||||
setAnalytics(null);
|
||||
} finally {
|
||||
setLoadingAnalytics(false);
|
||||
}
|
||||
};
|
||||
fetchAnalytics();
|
||||
}, [selectedQrId]);
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-8 text-center text-gray-500">Loading your data...</div>;
|
||||
}
|
||||
|
||||
if (qrs.length === 0) {
|
||||
return (
|
||||
<div className="p-8 max-w-4xl mx-auto font-sans h-full flex flex-col items-center justify-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">No data yet</h1>
|
||||
<p className="text-gray-500 mb-6">Create a dynamic QR code first to see analytics.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-6xl mx-auto font-sans">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 bg-white p-6 rounded-2xl shadow-sm border border-gray-200 gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-extrabold text-gray-900 tracking-tight">Analytics & Insights</h1>
|
||||
<p className="text-gray-500 mt-1">Deep dive into your QR scan metrics.</p>
|
||||
</div>
|
||||
|
||||
{/* QR Selector */}
|
||||
<div className="w-full md:w-64">
|
||||
<label className="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">Select QR Code</label>
|
||||
<select
|
||||
value={selectedQrId}
|
||||
onChange={(e) => setSelectedQrId(e.target.value)}
|
||||
className="w-full bg-gray-50 border border-gray-300 text-gray-900 rounded-xl px-4 py-2.5 font-semibold focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all shadow-sm"
|
||||
>
|
||||
{qrs.map(qr => (
|
||||
<option key={qr.id} value={qr.id}>{qr.name} (/l/{qr.short_url_id})</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loadingAnalytics ? (
|
||||
<div className="py-20 text-center text-indigo-500 font-bold animate-pulse">Crunching numbers...</div>
|
||||
) : !analytics ? (
|
||||
<div className="py-20 text-center text-red-500 font-bold">Failed to load analytics data.</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Top Metric Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200">
|
||||
<h3 className="text-sm font-bold text-gray-500 uppercase tracking-wider mb-2">Total Scans</h3>
|
||||
<div className="text-5xl font-black text-indigo-600">{analytics.totalScans.toLocaleString()}</div>
|
||||
</div>
|
||||
|
||||
{/* Adding placeholders for conversion rate / unique scanners since backend doesn't track unique yet */}
|
||||
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 opacity-50">
|
||||
<h3 className="text-sm font-bold text-gray-500 uppercase tracking-wider mb-2">Unique Scanners</h3>
|
||||
<div className="text-5xl font-black text-gray-900">--</div>
|
||||
<p className="text-xs text-gray-400 mt-2">Available in Pro</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 opacity-50">
|
||||
<h3 className="text-sm font-bold text-gray-500 uppercase tracking-wider mb-2">Top Location</h3>
|
||||
<div className="text-3xl font-black text-gray-900 mt-2">Unknown</div>
|
||||
<p className="text-xs text-gray-400 mt-2">Available in Pro</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline Chart */}
|
||||
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-6">Scans Over Time (Last 7 Days)</h3>
|
||||
<div className="h-72 w-full">
|
||||
{analytics.timeline.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={analytics.timeline}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e5e7eb" />
|
||||
<XAxis dataKey="date" tick={{fontSize: 12, fill: '#6b7280'}} axisLine={false} tickLine={false} tickFormatter={(val) => new Date(val).toLocaleDateString(undefined, {month: 'short', day: 'numeric'})} />
|
||||
<YAxis tick={{fontSize: 12, fill: '#6b7280'}} axisLine={false} tickLine={false} />
|
||||
<Tooltip contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }} />
|
||||
<Line type="monotone" dataKey="count" stroke="#4f46e5" strokeWidth={4} dot={{r: 4, fill: '#4f46e5', strokeWidth: 2, stroke: '#fff'}} activeDot={{r: 6}} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center text-gray-400">No scan data for the past 7 days.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Breakdown Charts */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
{/* Device Types */}
|
||||
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-6">Device Types</h3>
|
||||
<div className="h-64 flex items-center justify-center">
|
||||
{analytics.deviceBreakdown.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={analytics.deviceBreakdown}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={80}
|
||||
paddingAngle={5}
|
||||
dataKey="count"
|
||||
nameKey="device_type"
|
||||
>
|
||||
{analytics.deviceBreakdown.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<span className="text-gray-400">No device data</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Legend */}
|
||||
<div className="flex justify-center gap-4 flex-wrap mt-2">
|
||||
{analytics.deviceBreakdown.map((entry, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 rounded-full" style={{backgroundColor: COLORS[index % COLORS.length]}}></span>
|
||||
<span className="text-sm font-medium text-gray-700 capitalize">{entry.device_type} ({entry.count})</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Operating Systems */}
|
||||
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-6">Operating Systems</h3>
|
||||
<div className="h-64 flex items-center justify-center">
|
||||
{analytics.osBreakdown.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={analytics.osBreakdown}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={80}
|
||||
paddingAngle={5}
|
||||
dataKey="count"
|
||||
nameKey="os"
|
||||
>
|
||||
{analytics.osBreakdown.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<span className="text-gray-400">No OS data</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Legend */}
|
||||
<div className="flex justify-center gap-4 flex-wrap mt-2">
|
||||
{analytics.osBreakdown.map((entry, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 rounded-full" style={{backgroundColor: COLORS[index % COLORS.length]}}></span>
|
||||
<span className="text-sm font-medium text-gray-700 capitalize">{entry.os || 'Unknown'} ({entry.count})</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import type { Options } from 'qr-code-styling';
|
||||
import dynamic from 'next/dynamic';
|
||||
const LivePreview = dynamic(() => import('@/components/qr-editor/LivePreview'), { ssr: false });
|
||||
import DesignPanel from '@/components/qr-editor/DesignPanel';
|
||||
import api from '@/lib/api';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function CreateQRPage() {
|
||||
const router = useRouter();
|
||||
const [destinationUrl, setDestinationUrl] = useState('https://ck-qr.com');
|
||||
const [qrName, setQrName] = useState('');
|
||||
const [customSlug, setCustomSlug] = useState('');
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
|
||||
const [qrOptions, setQrOptions] = useState<Options>({
|
||||
width: 300,
|
||||
height: 300,
|
||||
type: 'svg',
|
||||
data: 'https://ck-qr.com',
|
||||
margin: 10,
|
||||
qrOptions: {
|
||||
typeNumber: 0,
|
||||
mode: 'Byte',
|
||||
errorCorrectionLevel: 'Q'
|
||||
},
|
||||
imageOptions: {
|
||||
hideBackgroundDots: true,
|
||||
imageSize: 0.4,
|
||||
margin: 5
|
||||
},
|
||||
dotsOptions: {
|
||||
color: '#4f46e5',
|
||||
type: 'rounded'
|
||||
},
|
||||
backgroundOptions: {
|
||||
color: '#ffffff',
|
||||
},
|
||||
cornersSquareOptions: {
|
||||
color: '#4f46e5',
|
||||
type: 'extra-rounded'
|
||||
}
|
||||
});
|
||||
|
||||
// Keep QR data in sync with URL input
|
||||
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newUrl = e.target.value;
|
||||
setDestinationUrl(newUrl);
|
||||
setQrOptions(prev => ({ ...prev, data: newUrl || 'https://ck-qr.com' }));
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
try {
|
||||
setIsGenerating(true);
|
||||
// Generate Dynamic QR via API
|
||||
const response = await api.post('/qr/generate', {
|
||||
name: qrName,
|
||||
customSlug: customSlug,
|
||||
type: 'dynamic',
|
||||
dataType: 'URL',
|
||||
destinationUrl: destinationUrl,
|
||||
designData: qrOptions
|
||||
});
|
||||
|
||||
alert('QR Code Successfully Generated & Saved!');
|
||||
router.push('/dashboard');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Failed to generate QR Code. Ensure you are logged in.');
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto h-full">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-800">Create QR Code</h1>
|
||||
<p className="text-gray-500 mt-1">Design a beautiful, dynamic QR code</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={isGenerating}
|
||||
className="bg-indigo-600 text-white px-6 py-2.5 rounded-lg hover:bg-indigo-700 transition-colors font-semibold shadow-sm disabled:opacity-50"
|
||||
>
|
||||
{isGenerating ? 'Saving...' : 'Save QR Code'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 h-[calc(100vh-180px)]">
|
||||
{/* Left Column - Controls */}
|
||||
<div className="lg:col-span-7 xl:col-span-8 space-y-6 overflow-y-auto pr-2 pb-10">
|
||||
|
||||
{/* Data Input Panel */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<h2 className="text-xl font-bold text-gray-800 border-b pb-4 mb-6">QR Content</h2>
|
||||
<div className="flex flex-col gap-6">
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Internal Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={qrName}
|
||||
onChange={e => setQrName(e.target.value)}
|
||||
placeholder="e.g. Front Door Menu"
|
||||
className="w-full border-gray-300 rounded-lg shadow-sm p-3 border focus:ring-indigo-500 focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Destination URL</label>
|
||||
<input
|
||||
type="url"
|
||||
value={destinationUrl}
|
||||
onChange={handleUrlChange}
|
||||
placeholder="https://your-website.com"
|
||||
className="w-full border-gray-300 rounded-lg shadow-sm p-3 border focus:ring-indigo-500 focus:border-indigo-500"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-2">This is a dynamic QR. You can change this URL later without reprinting the code.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Custom Short Link (Optional)</label>
|
||||
<div className="flex shadow-sm rounded-lg overflow-hidden border border-gray-300 focus-within:ring-2 focus-within:ring-indigo-500">
|
||||
<span className="inline-flex items-center px-4 bg-gray-50 text-gray-500 border-r border-gray-300 text-sm">
|
||||
/l/
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
value={customSlug}
|
||||
onChange={e => setCustomSlug(e.target.value.replace(/[^a-zA-Z0-9-_]/g, ''))}
|
||||
placeholder="my-promo"
|
||||
className="flex-1 w-full p-3 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-2">Leave blank to auto-generate a random link.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Design Panel */}
|
||||
<DesignPanel options={qrOptions} onChange={setQrOptions} />
|
||||
|
||||
</div>
|
||||
|
||||
{/* Right Column - Live Preview Sticky */}
|
||||
<div className="lg:col-span-5 xl:col-span-4 relative hidden lg:block">
|
||||
<div className="sticky top-0 pt-4">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 flex flex-col items-center">
|
||||
<h3 className="font-semibold text-gray-700 mb-4 self-start">Live Preview</h3>
|
||||
<LivePreview options={qrOptions} />
|
||||
<p className="text-xs text-gray-400 mt-4 text-center">Scan to preview the destination</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
|
||||
const menuItems = [
|
||||
{ name: 'My QR Codes', path: '/dashboard', icon: 'M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z' },
|
||||
{ name: 'Statistics', path: '/dashboard/analytics', icon: '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' },
|
||||
{ name: 'Profile', path: '/dashboard/profile', icon: 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z' },
|
||||
{ name: 'Support', path: '/dashboard/support', icon: 'M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#f4f6f8] flex font-sans">
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<div className="md:hidden fixed top-4 left-4 z-50">
|
||||
<button onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} className="p-2 bg-white rounded-lg shadow-sm border border-gray-200">
|
||||
<svg className="w-6 h-6 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={isMobileMenuOpen ? "M6 18L18 6M6 6l12 12" : "M4 6h16M4 12h16M4 18h16"} />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside className={`fixed md:static inset-y-0 left-0 z-40 w-64 bg-white border-r border-gray-200 transform ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'} md:translate-x-0 transition-transform duration-200 ease-in-out flex flex-col`}>
|
||||
<div className="h-20 flex items-center px-6 border-b border-gray-100">
|
||||
<Link href="/" className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold tracking-tighter">CQ</span>
|
||||
</div>
|
||||
<span className="text-gray-900 font-extrabold text-xl tracking-tight">CK-QR</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<Link href="/dashboard/create" className="flex items-center justify-center gap-2 w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-4 rounded-xl transition-colors shadow-sm mb-6">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /></svg>
|
||||
Create New QR Code
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 px-4 space-y-1 overflow-y-auto">
|
||||
{menuItems.map((item) => {
|
||||
const isActive = pathname === item.path;
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.path}
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-xl transition-colors ${
|
||||
isActive
|
||||
? 'bg-indigo-50 text-indigo-700 font-bold'
|
||||
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900 font-medium'
|
||||
}`}
|
||||
>
|
||||
<svg className={`w-5 h-5 ${isActive ? 'text-indigo-600' : 'text-gray-400'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={item.icon} />
|
||||
</svg>
|
||||
{item.name}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-gray-100">
|
||||
<Link href="/login" onClick={() => localStorage.removeItem('token')} className="flex items-center gap-3 px-4 py-3 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-xl transition-colors font-medium">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></svg>
|
||||
Log out
|
||||
</Link>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 h-screen overflow-y-auto relative">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
{/* Mobile overlay */}
|
||||
{isMobileMenuOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-gray-900/50 z-30 md:hidden"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
'use client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import api from '@/lib/api';
|
||||
|
||||
interface QRCode {
|
||||
id: string;
|
||||
name: string;
|
||||
destination_url: string;
|
||||
short_url_id: string;
|
||||
created_at: string;
|
||||
scans: number;
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [qrs, setQrs] = useState<QRCode[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Edit Modal State
|
||||
const [editingQr, setEditingQr] = useState<QRCode | null>(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
const [editUrl, setEditUrl] = useState('');
|
||||
const [editSlug, setEditSlug] = useState('');
|
||||
const [editError, setEditError] = useState('');
|
||||
const [editLoading, setEditLoading] = useState(false);
|
||||
|
||||
const fetchQRs = async () => {
|
||||
try {
|
||||
const res = await api.get('/qr');
|
||||
setQrs(res.data.qr_codes || []);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch QRs", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchQRs();
|
||||
}, []);
|
||||
|
||||
const openEditModal = (qr: QRCode) => {
|
||||
setEditingQr(qr);
|
||||
setEditName(qr.name);
|
||||
setEditUrl(qr.destination_url);
|
||||
setEditSlug(qr.short_url_id);
|
||||
setEditError('');
|
||||
};
|
||||
|
||||
const handleEditSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!editingQr) return;
|
||||
setEditLoading(true);
|
||||
setEditError('');
|
||||
|
||||
try {
|
||||
await api.put(`/qr/${editingQr.id}`, {
|
||||
name: editName,
|
||||
destinationUrl: editUrl,
|
||||
shortUrlId: editSlug
|
||||
});
|
||||
// Refresh list
|
||||
await fetchQRs();
|
||||
setEditingQr(null);
|
||||
} catch (err: any) {
|
||||
setEditError(err.response?.data?.error || 'Failed to update QR code');
|
||||
} finally {
|
||||
setEditLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Are you sure you want to delete this QR code? The link will immediately stop working.')) return;
|
||||
try {
|
||||
await api.delete(`/qr/${id}`);
|
||||
setQrs(qrs.filter(qr => qr.id !== id));
|
||||
} catch (err) {
|
||||
alert('Failed to delete QR code');
|
||||
}
|
||||
};
|
||||
|
||||
const totalScans = qrs.reduce((sum, qr) => sum + Number(qr.scans), 0);
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:4001';
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto font-sans relative">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-8 bg-white p-6 rounded-2xl shadow-sm border border-gray-200">
|
||||
<div>
|
||||
<h1 className="text-3xl font-extrabold text-gray-900 tracking-tight">My QR Codes</h1>
|
||||
<p className="text-gray-500 mt-1">Manage, track, and dynamically route your active links.</p>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Link
|
||||
href="/dashboard/analytics"
|
||||
className="bg-white border border-gray-300 text-gray-700 px-5 py-2.5 rounded-lg hover:bg-gray-50 transition-colors font-semibold flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-5 h-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><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>
|
||||
Analytics
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Verification Warning (Mock ME-QR style) */}
|
||||
<div className="bg-red-50 border-l-4 border-red-500 p-4 mb-8 rounded-r-xl flex justify-between items-center shadow-sm">
|
||||
<div>
|
||||
<h3 className="text-red-800 font-bold">WARNING!</h3>
|
||||
<p className="text-red-700 text-sm">Your email should be verified to prevent link expiration.</p>
|
||||
</div>
|
||||
<button className="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-6 rounded-lg transition-colors">
|
||||
Verify Email
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* List View */}
|
||||
<div className="space-y-4">
|
||||
{loading ? (
|
||||
<div className="p-12 text-center text-gray-500 bg-white rounded-2xl shadow-sm border border-gray-200">Loading your dynamic links...</div>
|
||||
) : qrs.length === 0 ? (
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-16 text-center">
|
||||
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" /></svg>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-2">No QR codes yet</h3>
|
||||
<p className="text-gray-500 mb-6 max-w-sm mx-auto">Create your first dynamic link to generate a trackable QR code.</p>
|
||||
<Link href="/dashboard/create" className="inline-block bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-8 rounded-xl transition-colors shadow-sm">Create New QR Code</Link>
|
||||
</div>
|
||||
) : (
|
||||
qrs.map((qr) => (
|
||||
<div key={qr.id} className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 flex flex-col md:flex-row items-center gap-8 hover:shadow-md transition-shadow relative group">
|
||||
|
||||
{/* QR Image Placeholder (In real app, fetch from backend) */}
|
||||
<div className="w-32 h-32 bg-gray-100 rounded-xl border border-gray-200 flex-shrink-0 flex items-center justify-center p-2">
|
||||
<img src={`https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(baseUrl + '/l/' + qr.short_url_id)}`} alt="QR" className="w-full h-full object-contain" />
|
||||
</div>
|
||||
|
||||
{/* Info Area */}
|
||||
<div className="flex-1 min-w-0 w-full">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-xl font-bold text-gray-900 truncate">{qr.name}</h3>
|
||||
<button onClick={() => openEditModal(qr)} className="text-gray-400 hover:text-indigo-600 transition-colors">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<a href={`${baseUrl}/l/${qr.short_url_id}`} target="_blank" rel="noopener noreferrer" className="text-indigo-600 font-bold hover:underline flex items-center gap-1 text-sm bg-indigo-50 inline-flex px-3 py-1.5 rounded-lg border border-indigo-100">
|
||||
{baseUrl}/l/{qr.short_url_id}
|
||||
<svg className="w-3 h-3 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6 text-sm text-gray-500">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-semibold text-gray-700">Type:</span> Link
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-semibold text-gray-700">Created:</span> {new Date(qr.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metrics & Actions */}
|
||||
<div className="flex flex-col sm:flex-row items-center gap-8 w-full md:w-auto">
|
||||
<div className="text-center md:text-right">
|
||||
<div className="text-3xl font-black text-gray-900">{qr.scans.toLocaleString()}</div>
|
||||
<div className="text-sm text-gray-500 uppercase tracking-wider font-semibold">Scans</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 w-full sm:w-48">
|
||||
<button className="w-full bg-green-500 hover:bg-green-600 text-white font-bold py-2.5 px-4 rounded-lg transition-colors flex justify-center items-center gap-2 shadow-sm border border-transparent">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
|
||||
Download
|
||||
</button>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => openEditModal(qr)} className="flex-1 bg-white border border-gray-300 hover:bg-gray-50 text-gray-700 font-semibold py-2 px-3 rounded-lg transition-colors text-sm">
|
||||
Edit Link
|
||||
</button>
|
||||
<button onClick={() => handleDelete(qr.id)} className="bg-white border border-gray-300 hover:bg-red-50 hover:text-red-600 hover:border-red-200 text-gray-700 font-semibold py-2 px-3 rounded-lg transition-colors">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Edit Modal Overlay */}
|
||||
{editingQr && (
|
||||
<div className="fixed inset-0 bg-gray-900/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-2xl shadow-xl border border-gray-200 w-full max-w-lg overflow-hidden transform transition-all">
|
||||
<div className="px-6 py-5 border-b border-gray-100 flex justify-between items-center bg-gray-50">
|
||||
<h3 className="text-lg font-bold text-gray-900">Edit Dynamic Link</h3>
|
||||
<button onClick={() => setEditingQr(null)} className="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleEditSubmit} className="p-6 space-y-5">
|
||||
{editError && (
|
||||
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm border border-red-100 font-medium">
|
||||
{editError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-2">QR Title (Internal)</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={editName}
|
||||
onChange={e => setEditName(e.target.value)}
|
||||
className="w-full bg-white border border-gray-300 rounded-xl px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-2">Destination URL</label>
|
||||
<input
|
||||
type="url"
|
||||
required
|
||||
value={editUrl}
|
||||
onChange={e => setEditUrl(e.target.value)}
|
||||
className="w-full bg-white border border-gray-300 rounded-xl px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all shadow-sm"
|
||||
placeholder="https://yourwebsite.com/new-campaign"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-2">Where should scanners be instantly redirected?</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-2">Custom Short Slug</label>
|
||||
<div className="flex flex-col sm:flex-row shadow-sm rounded-xl overflow-hidden border border-gray-300 focus-within:ring-2 focus-within:ring-indigo-500 focus-within:border-transparent transition-all">
|
||||
<span className="inline-flex items-center px-4 bg-gray-50 text-gray-500 font-medium text-sm sm:border-r border-gray-300">
|
||||
{baseUrl.replace(/^https?:\/\//, '')}/l/
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={editSlug}
|
||||
onChange={e => setEditSlug(e.target.value.replace(/[^a-zA-Z0-9-_]/g, ''))}
|
||||
className="flex-1 bg-white px-4 py-3 text-gray-900 focus:outline-none font-bold"
|
||||
placeholder="my-custom-slug"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-2">Only letters, numbers, hyphens, and underscores.</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex gap-3 border-t border-gray-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingQr(null)}
|
||||
className="flex-1 bg-white border border-gray-300 text-gray-700 font-bold py-3 px-4 rounded-xl hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={editLoading}
|
||||
className="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-4 rounded-xl transition-colors shadow-sm disabled:opacity-50"
|
||||
>
|
||||
{editLoading ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
'use client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import api from '@/lib/api';
|
||||
|
||||
export default function ProfilePage() {
|
||||
const [user, setUser] = useState<{ email: string; role: string } | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
const res = await api.get('/auth/me');
|
||||
setUser(res.data.user);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch user", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchUser();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-4xl mx-auto font-sans h-full">
|
||||
<div className="mb-8 bg-white p-6 rounded-2xl shadow-sm border border-gray-200">
|
||||
<h1 className="text-3xl font-extrabold text-gray-900 tracking-tight">Account Profile</h1>
|
||||
<p className="text-gray-500 mt-1">Manage your personal settings and subscription details.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
|
||||
{loading ? (
|
||||
<div className="text-center text-gray-500">Loading profile...</div>
|
||||
) : user ? (
|
||||
<div className="space-y-6">
|
||||
|
||||
<div className="flex items-center gap-6 pb-6 border-b border-gray-100">
|
||||
<div className="w-20 h-20 bg-indigo-100 text-indigo-600 rounded-full flex items-center justify-center text-3xl font-bold uppercase shadow-sm">
|
||||
{user.email.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">{user.email}</h2>
|
||||
<span className="inline-flex items-center mt-2 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-indigo-50 text-indigo-700 border border-indigo-100">
|
||||
{user.role.replace('_', ' ')} Plan
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-4">Security</h3>
|
||||
<button className="w-full bg-white border border-gray-300 hover:bg-gray-50 text-gray-700 font-semibold py-3 px-4 rounded-xl transition-colors shadow-sm text-left flex justify-between items-center">
|
||||
Change Password
|
||||
<svg className="w-5 h-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-4">Billing</h3>
|
||||
<button className="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-3 px-4 rounded-xl transition-colors shadow-sm flex items-center justify-center gap-2">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" /></svg>
|
||||
Upgrade Subscription
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-red-500">Failed to load profile.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function SupportPage() {
|
||||
const faqs = [
|
||||
{ q: "How do dynamic QR codes work?", a: "Dynamic QR codes link to a short URL managed by CK-QR. When scanned, the short URL redirects the user to your final destination URL. Because the QR graphic only contains the short URL, you can change the final destination at any time without reprinting the code." },
|
||||
{ q: "Can I change the short link later?", a: "Yes! You can edit the custom slug of your short link directly from your dashboard by clicking the 'Edit Link' button on any QR code card." },
|
||||
{ q: "Are scans tracked accurately?", a: "We log every scan instantly, capturing the operating system, device type, and timestamp. You can view these metrics on the Statistics page." },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-4xl mx-auto font-sans h-full">
|
||||
<div className="mb-8 bg-white p-6 rounded-2xl shadow-sm border border-gray-200">
|
||||
<h1 className="text-3xl font-extrabold text-gray-900 tracking-tight">Support & FAQ</h1>
|
||||
<p className="text-gray-500 mt-1">Get help and learn how to use CK-QR.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden mb-8">
|
||||
<div className="p-8 bg-indigo-50 border-b border-indigo-100 flex flex-col md:flex-row items-center justify-between gap-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-indigo-900 mb-2">Need direct assistance?</h2>
|
||||
<p className="text-indigo-700 text-sm">Our support team is available Monday through Friday to help you with any technical issues or billing inquiries.</p>
|
||||
</div>
|
||||
<button className="whitespace-nowrap bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-6 rounded-xl transition-colors shadow-sm">
|
||||
Contact Support
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-6 flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
Frequently Asked Questions
|
||||
</h3>
|
||||
<div className="space-y-6">
|
||||
{faqs.map((faq, i) => (
|
||||
<div key={i} className="border-b border-gray-100 pb-6 last:border-0 last:pb-0">
|
||||
<h4 className="font-bold text-gray-900 mb-2">{faq.q}</h4>
|
||||
<p className="text-gray-600 text-sm leading-relaxed">{faq.a}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Link href="/support/how-to-use" className="text-indigo-600 hover:underline font-medium text-sm">
|
||||
View full documentation
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function AnalyticsPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col font-sans">
|
||||
<nav className="bg-gray-900 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-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="flex-1 max-w-4xl mx-auto w-full px-6 py-16">
|
||||
<h1 className="text-5xl font-black text-gray-900 mb-6">Analytics Dashboard</h1>
|
||||
<p className="text-xl text-gray-600 mb-10 leading-relaxed">
|
||||
Turn physical scans into actionable digital intelligence. Track every interaction with your QR campaigns in real-time.
|
||||
</p>
|
||||
<div className="prose prose-lg prose-indigo max-w-none text-gray-700">
|
||||
<h2>Deep Scan Insights</h2>
|
||||
<p>
|
||||
Unlike static QR codes which offer zero visibility, every scan of a CK-QR dynamic code is logged and analyzed. Our platform provides beautiful charts and actionable data to help you understand your audience.
|
||||
</p>
|
||||
<h2>What we track</h2>
|
||||
<ul>
|
||||
<li><strong>Geographic Location:</strong> See exactly which city and country your scans are coming from. Perfect for global product packaging.</li>
|
||||
<li><strong>Device & OS:</strong> Know if your audience is using iOS, Android, or scanning from a desktop webcam.</li>
|
||||
<li><strong>Temporal Data:</strong> View scan spikes over time to measure the immediate impact of a marketing campaign or event.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function ApiWebhooksPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col font-sans">
|
||||
<nav className="bg-gray-900 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-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="flex-1 max-w-4xl mx-auto w-full px-6 py-16">
|
||||
<h1 className="text-5xl font-black text-gray-900 mb-6">API & Webhooks</h1>
|
||||
<p className="text-xl text-gray-600 mb-10 leading-relaxed">
|
||||
Automate your physical marketing at scale. Build custom integrations with our RESTful API.
|
||||
</p>
|
||||
<div className="prose prose-lg prose-indigo max-w-none text-gray-700">
|
||||
<h2>Developer First</h2>
|
||||
<p>
|
||||
Whether you need to generate 10,000 unique QR codes for an event ticketing system, or automatically update destinations based on your internal CMS, our API can handle it.
|
||||
</p>
|
||||
<h2>Capabilities</h2>
|
||||
<ul>
|
||||
<li><strong>Programmatic Generation:</strong> Create, read, update, and delete dynamic QR codes via secure API endpoints.</li>
|
||||
<li><strong>Real-time Webhooks:</strong> Subscribe to scan events. Get a POST request to your server the millisecond someone scans one of your codes.</li>
|
||||
<li><strong>Analytics Export:</strong> Pull raw analytics data into your own business intelligence tools like Tableau or PowerBI.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function DesignStudioPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col font-sans">
|
||||
<nav className="bg-gray-900 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-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="flex-1 max-w-4xl mx-auto w-full px-6 py-16">
|
||||
<h1 className="text-5xl font-black text-gray-900 mb-6">Design Studio</h1>
|
||||
<p className="text-xl text-gray-600 mb-10 leading-relaxed">
|
||||
Ditch the boring black-and-white squares. Build branded, highly scannable QR codes that stand out.
|
||||
</p>
|
||||
<div className="prose prose-lg prose-indigo max-w-none text-gray-700">
|
||||
<h2>Brand Integration</h2>
|
||||
<p>
|
||||
Your QR code is an extension of your brand. Our Design Studio allows you to inject your brand's DNA directly into the code itself, increasing trust and scan rates.
|
||||
</p>
|
||||
<h2>Customization Options</h2>
|
||||
<ul>
|
||||
<li><strong>Logo Injection:</strong> Upload your company logo and seamlessly embed it in the center of the QR code without sacrificing scannability.</li>
|
||||
<li><strong>Color Gradients:</strong> Apply beautiful linear or radial gradients to the QR dots and eyes.</li>
|
||||
<li><strong>Shapes & Patterns:</strong> Change the square dots to circles, crosses, or smooth flowing liquids. Customize the outer and inner eye shapes individually.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function DynamicQRPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col font-sans">
|
||||
<nav className="bg-gray-900 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-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="flex-1 max-w-4xl mx-auto w-full px-6 py-16">
|
||||
<h1 className="text-5xl font-black text-gray-900 mb-6">Dynamic QR Codes</h1>
|
||||
<p className="text-xl text-gray-600 mb-10 leading-relaxed">
|
||||
Never reprint a QR code again. With our dynamic routing engine, you can change the destination URL of your physical QR codes instantly from our dashboard.
|
||||
</p>
|
||||
<div className="prose prose-lg prose-indigo max-w-none text-gray-700">
|
||||
<h2>How it works</h2>
|
||||
<p>
|
||||
When you create a dynamic QR code, we generate a unique short-link (e.g., ckqr.com/r/xyz). Your physical QR code points to this short-link. When a user scans the code, our globally distributed edge network intercepts the request, logs the analytics data, and instantly redirects the user to your final destination URL.
|
||||
</p>
|
||||
<h2>Benefits</h2>
|
||||
<ul>
|
||||
<li><strong>Fix mistakes instantly:</strong> Did you print 10,000 flyers with a typo in the URL? No problem. Update the destination in our dashboard and the printed codes will automatically route correctly.</li>
|
||||
<li><strong>A/B Testing:</strong> Route 50% of traffic to one landing page and 50% to another to test conversion rates.</li>
|
||||
<li><strong>Time-based routing:</strong> Send users to a breakfast menu in the morning, and a dinner menu at night—all from the exact same QR code on the table.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
nextjs-portal {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 348 KiB |
@@ -0,0 +1,33 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "CK-QR | The Dynamic QR Code Engine",
|
||||
description: "Create, manage, and track intelligent QR codes at scale with CK-QR.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function CookiesPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col font-sans">
|
||||
<nav className="bg-gray-900 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-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="flex-1 max-w-4xl mx-auto w-full px-6 py-16">
|
||||
<h1 className="text-5xl font-black text-gray-900 mb-6">Cookie Policy</h1>
|
||||
<p className="text-xl text-gray-500 mb-10">Last updated: August 2026</p>
|
||||
<div className="prose prose-lg prose-indigo max-w-none text-gray-700">
|
||||
<h2>How we use Cookies</h2>
|
||||
<p>
|
||||
We use strictly necessary cookies to keep you logged into the dashboard (`token`). We do not use third-party tracking cookies on our marketing site.
|
||||
</p>
|
||||
<h2>For end-users scanning codes</h2>
|
||||
<p>
|
||||
When a user scans a CK-QR code, the redirect engine does NOT drop any cookies onto the user's device. The redirect happens statelessly at the edge.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col font-sans">
|
||||
<nav className="bg-gray-900 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-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="flex-1 max-w-4xl mx-auto w-full px-6 py-16">
|
||||
<h1 className="text-5xl font-black text-gray-900 mb-6">Privacy Policy</h1>
|
||||
<p className="text-xl text-gray-500 mb-10">Last updated: August 2026</p>
|
||||
<div className="prose prose-lg prose-indigo max-w-none text-gray-700">
|
||||
<h2>1. Data Collection</h2>
|
||||
<p>
|
||||
When users scan your QR codes, we collect basic telemetry data including IP address (anonymized), user agent strings to determine device type, and the timestamp of the scan. We do not collect personally identifiable information (PII) from end-users scanning the codes.
|
||||
</p>
|
||||
<h2>2. Data Usage</h2>
|
||||
<p>
|
||||
This data is used strictly to populate your analytics dashboard. We do not sell this data to third parties or data brokers.
|
||||
</p>
|
||||
<h2>3. Account Deletion</h2>
|
||||
<p>
|
||||
You may request deletion of your CK-QR account at any time. Upon deletion, all associated dynamic QR codes will instantly break and route to a 404 page.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function TermsPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col font-sans">
|
||||
<nav className="bg-gray-900 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-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="flex-1 max-w-4xl mx-auto w-full px-6 py-16">
|
||||
<h1 className="text-5xl font-black text-gray-900 mb-6">Terms of Service</h1>
|
||||
<p className="text-xl text-gray-500 mb-10">Last updated: August 2026</p>
|
||||
<div className="prose prose-lg prose-indigo max-w-none text-gray-700">
|
||||
<h2>1. Acceptable Use</h2>
|
||||
<p>
|
||||
You agree not to use CK-QR's dynamic routing engine to redirect users to malicious websites, malware payloads, or phishing attacks. We actively monitor routing destinations using Google Safe Browsing and will instantly terminate accounts violating this policy.
|
||||
</p>
|
||||
<h2>2. Service Availability</h2>
|
||||
<p>
|
||||
While we strive for 99.9% uptime, we are not liable for business losses incurred due to temporary routing downtime or DNS propagation delays.
|
||||
</p>
|
||||
<h2>3. Subscription and Billing</h2>
|
||||
<p>
|
||||
Paid plans are billed monthly. You may cancel your subscription at any time, but we do not offer prorated refunds for mid-month cancellations.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import api from '@/lib/api';
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const res = await api.post('/auth/login', { email, password });
|
||||
if (res.data.user.role === 'enterprise') {
|
||||
router.push('/admin');
|
||||
} else {
|
||||
router.push('/dashboard');
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || 'Failed to login');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] flex flex-col justify-center py-12 sm:px-6 lg:px-8 font-sans relative overflow-hidden">
|
||||
|
||||
{/* Background Decorators */}
|
||||
<div className="absolute top-[-10%] left-[-10%] w-96 h-96 bg-indigo-600 rounded-full mix-blend-multiply filter blur-[128px] opacity-20 animate-blob"></div>
|
||||
<div className="absolute bottom-[-10%] right-[-10%] w-96 h-96 bg-purple-600 rounded-full mix-blend-multiply filter blur-[128px] opacity-20 animate-blob animation-delay-2000"></div>
|
||||
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md relative z-10">
|
||||
<Link href="/" className="flex justify-center mb-8 group">
|
||||
<div className="w-14 h-14 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl flex items-center justify-center shadow-lg shadow-indigo-500/20 group-hover:scale-105 transition-transform">
|
||||
<span className="text-white font-black text-3xl tracking-tighter">CQ</span>
|
||||
</div>
|
||||
</Link>
|
||||
<h2 className="text-center text-4xl font-black text-white tracking-tight">Welcome Back</h2>
|
||||
<p className="mt-3 text-center text-gray-400 font-medium">
|
||||
New to CK-QR?{' '}
|
||||
<Link href="/register" className="text-indigo-400 hover:text-indigo-300 transition-colors">
|
||||
Start a free trial
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-md relative z-10">
|
||||
<div className="bg-[#111111] py-10 px-8 shadow-2xl sm:rounded-3xl border border-gray-800 backdrop-blur-xl">
|
||||
<form className="space-y-6" onSubmit={handleLogin}>
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/50 text-red-400 p-4 rounded-xl text-sm text-center font-medium">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-300 mb-2">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
className="block w-full px-4 py-3 bg-[#1a1a1a] border border-gray-700 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
|
||||
placeholder="admin@ckqr.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-300 mb-2">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
className="block w-full px-4 py-3 bg-[#1a1a1a] border border-gray-700 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
|
||||
placeholder="••••••••"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div className="flex items-center">
|
||||
<input id="remember-me" type="checkbox" className="h-4 w-4 bg-[#1a1a1a] border-gray-700 rounded text-indigo-500 focus:ring-indigo-500 focus:ring-offset-gray-900" />
|
||||
<label htmlFor="remember-me" className="ml-2 block text-sm font-medium text-gray-400">Remember me</label>
|
||||
</div>
|
||||
<a href="#" className="text-sm font-medium text-indigo-400 hover:text-indigo-300 transition-colors">Forgot password?</a>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full flex justify-center py-4 px-4 border border-transparent rounded-xl shadow-lg shadow-indigo-500/20 text-sm font-bold text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-500 disabled:opacity-50 transition-all active:scale-[0.98]"
|
||||
>
|
||||
{loading ? 'Authenticating...' : 'Sign In'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
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>
|
||||
|
||||
{/* Hero Section with Parallax */}
|
||||
<div
|
||||
className="relative pt-32 pb-20 lg:pt-48 lg:pb-32 bg-fixed bg-center bg-cover"
|
||||
style={{ backgroundImage: "url('/parallax-bg.png')" }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gray-900/80"></div>
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col lg:flex-row items-center gap-16">
|
||||
|
||||
<div className="flex-1 text-center lg:text-left">
|
||||
<h1 className="text-5xl lg:text-7xl font-black text-white tracking-tight leading-[1.1] mb-8">
|
||||
The Engine for <br />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 to-purple-400">
|
||||
Dynamic QR
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xl text-gray-300 mb-10 max-w-2xl mx-auto lg:mx-0 leading-relaxed font-medium">
|
||||
Create, manage, and track intelligent QR codes. Update destinations in real-time, inject brand colors, and analyze global scan traffic instantly.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start">
|
||||
<Link href="/register" className="bg-indigo-600 text-white px-8 py-4 rounded-full font-bold text-lg hover:bg-indigo-700 transition-all shadow-lg hover:shadow-indigo-500/40 hover:-translate-y-1">
|
||||
Start 14-Day Free Trial
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-10 flex items-center justify-center lg:justify-start gap-4 text-sm font-medium text-gray-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" /></svg>
|
||||
No credit card required
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" /></svg>
|
||||
Cancel anytime
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 w-full max-w-2xl hidden lg:block">
|
||||
<div className="relative rounded-2xl overflow-hidden shadow-2xl border border-gray-700/50 transform -rotate-2 hover:rotate-0 transition-transform duration-500 hover:shadow-indigo-500/20">
|
||||
<Image
|
||||
src="/hero-mockup.png"
|
||||
alt="CK-QR Dashboard Preview"
|
||||
width={800}
|
||||
height={600}
|
||||
className="w-full h-auto"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8 text-center divide-x divide-gray-800">
|
||||
<div>
|
||||
<div className="text-4xl font-black text-white mb-2">10M+</div>
|
||||
<div className="text-sm font-bold text-gray-500 uppercase tracking-widest">Scans Tracked</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-4xl font-black text-white mb-2">99.9%</div>
|
||||
<div className="text-sm font-bold text-gray-500 uppercase tracking-widest">Uptime SLA</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-4xl font-black text-white mb-2">250ms</div>
|
||||
<div className="text-sm font-bold text-gray-500 uppercase tracking-widest">Global Routing</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-4xl font-black text-white mb-2">15+</div>
|
||||
<div className="text-sm font-bold text-gray-500 uppercase tracking-widest">Data Types</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Deep Dive Features */}
|
||||
<div className="bg-gray-950 py-32">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-20">
|
||||
<h2 className="text-4xl font-black text-white">Everything you need to scale</h2>
|
||||
<p className="mt-4 text-xl text-gray-400">Built for individuals, marketers, and enterprise teams.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div className="bg-gray-900 border border-gray-800 p-10 rounded-3xl hover:border-indigo-500/50 transition-colors group">
|
||||
<div className="w-14 h-14 bg-indigo-500/10 rounded-2xl flex items-center justify-center mb-8 group-hover:scale-110 transition-transform">
|
||||
<svg className="w-7 h-7 text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-white mb-4">Dynamic Routing</h3>
|
||||
<p className="text-gray-400 leading-relaxed">Update destination URLs instantly without ever needing to reprint your physical QR codes. Fix mistakes in seconds.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900 border border-gray-800 p-10 rounded-3xl hover:border-purple-500/50 transition-colors group">
|
||||
<div className="w-14 h-14 bg-purple-500/10 rounded-2xl flex items-center justify-center mb-8 group-hover:scale-110 transition-transform">
|
||||
<svg className="w-7 h-7 text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><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>
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-white mb-4">Deep Analytics</h3>
|
||||
<p className="text-gray-400 leading-relaxed">Track operating systems, device types, global locations, and view rich temporal charts of your scan traffic.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900 border border-gray-800 p-10 rounded-3xl hover:border-pink-500/50 transition-colors group">
|
||||
<div className="w-14 h-14 bg-pink-500/10 rounded-2xl flex items-center justify-center mb-8 group-hover:scale-110 transition-transform">
|
||||
<svg className="w-7 h-7 text-pink-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" /></svg>
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-white mb-4">Design Studio</h3>
|
||||
<p className="text-gray-400 leading-relaxed">Customize dot patterns, eye shapes, and inject your brand colors or logo directly into the center of the codes.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* How It Works Section */}
|
||||
<div className="bg-gray-900 py-32 border-t border-gray-800">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-20">
|
||||
<h2 className="text-4xl font-black text-white">How it Works</h2>
|
||||
<p className="mt-4 text-xl text-gray-400">Launch your first dynamic QR campaign in under 60 seconds.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 relative">
|
||||
<div className="hidden md:block absolute top-12 left-1/6 right-1/6 h-0.5 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 opacity-20"></div>
|
||||
|
||||
<div className="relative text-center">
|
||||
<div className="w-24 h-24 mx-auto bg-gray-950 border-2 border-indigo-500 rounded-full flex items-center justify-center text-3xl font-black text-indigo-400 mb-8 shadow-[0_0_30px_rgba(99,102,241,0.2)]">1</div>
|
||||
<h3 className="text-2xl font-bold text-white mb-4">Generate & Customize</h3>
|
||||
<p className="text-gray-400 leading-relaxed">Input your destination URL and use our powerful design studio to apply your brand colors, patterns, and logos.</p>
|
||||
</div>
|
||||
|
||||
<div className="relative text-center">
|
||||
<div className="w-24 h-24 mx-auto bg-gray-950 border-2 border-purple-500 rounded-full flex items-center justify-center text-3xl font-black text-purple-400 mb-8 shadow-[0_0_30px_rgba(168,85,247,0.2)]">2</div>
|
||||
<h3 className="text-2xl font-bold text-white mb-4">Download & Print</h3>
|
||||
<p className="text-gray-400 leading-relaxed">Export in high-resolution SVG or PNG formats and embed the code securely onto your physical marketing materials.</p>
|
||||
</div>
|
||||
|
||||
<div className="relative text-center">
|
||||
<div className="w-24 h-24 mx-auto bg-gray-950 border-2 border-pink-500 rounded-full flex items-center justify-center text-3xl font-black text-pink-400 mb-8 shadow-[0_0_30px_rgba(236,72,153,0.2)]">3</div>
|
||||
<h3 className="text-2xl font-bold text-white mb-4">Track & Update</h3>
|
||||
<p className="text-gray-400 leading-relaxed">Watch global scans roll in via the analytics dashboard. Change the destination URL at any time without reprinting!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Use Cases Section */}
|
||||
<div className="bg-gray-950 py-32 border-t border-gray-800">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-20">
|
||||
<h2 className="text-4xl font-black text-white">Built for Every Industry</h2>
|
||||
<p className="mt-4 text-xl text-gray-400">Join thousands of businesses bridging the physical and digital worlds.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="bg-gray-900 border border-gray-800 p-8 rounded-3xl relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-orange-500/10 rounded-full blur-3xl group-hover:bg-orange-500/20 transition-colors"></div>
|
||||
<h3 className="text-xl font-bold text-white mb-2">Restaurants</h3>
|
||||
<p className="text-sm text-gray-400">Dynamic digital menus that can be updated daily without replacing table tents.</p>
|
||||
</div>
|
||||
<div className="bg-gray-900 border border-gray-800 p-8 rounded-3xl relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-500/10 rounded-full blur-3xl group-hover:bg-blue-500/20 transition-colors"></div>
|
||||
<h3 className="text-xl font-bold text-white mb-2">Real Estate</h3>
|
||||
<p className="text-sm text-gray-400">Property yard signs that dynamically route to the latest virtual 3D tour.</p>
|
||||
</div>
|
||||
<div className="bg-gray-900 border border-gray-800 p-8 rounded-3xl relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-emerald-500/10 rounded-full blur-3xl group-hover:bg-emerald-500/20 transition-colors"></div>
|
||||
<h3 className="text-xl font-bold text-white mb-2">Retail & CPG</h3>
|
||||
<p className="text-sm text-gray-400">Product packaging that links to instruction manuals and warranty registrations.</p>
|
||||
</div>
|
||||
<div className="bg-gray-900 border border-gray-800 p-8 rounded-3xl relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-red-500/10 rounded-full blur-3xl group-hover:bg-red-500/20 transition-colors"></div>
|
||||
<h3 className="text-xl font-bold text-white mb-2">Events</h3>
|
||||
<p className="text-sm text-gray-400">Smart ticketing and rapid attendee check-in systems via scannable badges.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing Section */}
|
||||
<div className="bg-gray-900 py-32 border-t border-gray-800">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-20">
|
||||
<h2 className="text-4xl font-black text-white">Simple, transparent pricing</h2>
|
||||
<p className="mt-4 text-xl text-gray-400">No hidden fees. Cancel anytime.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
||||
|
||||
{/* Free Tier */}
|
||||
<div className="bg-gray-950 border border-gray-800 rounded-3xl p-10 flex flex-col">
|
||||
<h3 className="text-2xl font-bold text-white mb-2">Free Trial</h3>
|
||||
<p className="text-gray-400 mb-6">Perfect for testing the waters.</p>
|
||||
<div className="text-4xl font-black text-white mb-8">$0<span className="text-lg text-gray-500 font-medium">/14 days</span></div>
|
||||
<ul className="space-y-4 mb-8 flex-1 text-gray-300">
|
||||
<li className="flex items-center gap-3"><svg className="w-5 h-5 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg> 5 Dynamic QR Codes</li>
|
||||
<li className="flex items-center gap-3"><svg className="w-5 h-5 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg> Basic Analytics</li>
|
||||
<li className="flex items-center gap-3"><svg className="w-5 h-5 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg> Standard Design Tools</li>
|
||||
</ul>
|
||||
<Link href="/register" className="w-full text-center bg-gray-800 hover:bg-gray-700 text-white py-4 rounded-xl font-bold transition-colors">Start Free Trial</Link>
|
||||
</div>
|
||||
|
||||
{/* Pro Tier */}
|
||||
<div className="bg-gradient-to-b from-indigo-900/50 to-gray-950 border-2 border-indigo-500 rounded-3xl p-10 flex flex-col relative transform md:-translate-y-4 shadow-[0_0_40px_rgba(99,102,241,0.15)]">
|
||||
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-indigo-500 text-white px-4 py-1 rounded-full text-sm font-bold tracking-wide uppercase">Most Popular</div>
|
||||
<h3 className="text-2xl font-bold text-white mb-2">Professional</h3>
|
||||
<p className="text-indigo-200 mb-6">For marketers and creators.</p>
|
||||
<div className="text-4xl font-black text-white mb-8">$29<span className="text-lg text-gray-400 font-medium">/mo</span></div>
|
||||
<ul className="space-y-4 mb-8 flex-1 text-gray-200">
|
||||
<li className="flex items-center gap-3"><svg className="w-5 h-5 text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg> Unlimited QR Codes</li>
|
||||
<li className="flex items-center gap-3"><svg className="w-5 h-5 text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg> Advanced Device Analytics</li>
|
||||
<li className="flex items-center gap-3"><svg className="w-5 h-5 text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg> Custom Logos & Colors</li>
|
||||
<li className="flex items-center gap-3"><svg className="w-5 h-5 text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg> API Access</li>
|
||||
</ul>
|
||||
<Link href="/register" className="w-full text-center bg-indigo-600 hover:bg-indigo-500 text-white py-4 rounded-xl font-bold transition-colors shadow-lg shadow-indigo-500/25">Upgrade to Pro</Link>
|
||||
</div>
|
||||
|
||||
{/* Enterprise Tier */}
|
||||
<div className="bg-gray-950 border border-gray-800 rounded-3xl p-10 flex flex-col">
|
||||
<h3 className="text-2xl font-bold text-white mb-2">Enterprise</h3>
|
||||
<p className="text-gray-400 mb-6">For large scale operations.</p>
|
||||
<div className="text-4xl font-black text-white mb-8">$99<span className="text-lg text-gray-500 font-medium">/mo</span></div>
|
||||
<ul className="space-y-4 mb-8 flex-1 text-gray-300">
|
||||
<li className="flex items-center gap-3"><svg className="w-5 h-5 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg> Everything in Pro</li>
|
||||
<li className="flex items-center gap-3"><svg className="w-5 h-5 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg> Multi-user Teams</li>
|
||||
<li className="flex items-center gap-3"><svg className="w-5 h-5 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg> Dedicated Account Manager</li>
|
||||
<li className="flex items-center gap-3"><svg className="w-5 h-5 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg> 99.9% Uptime SLA</li>
|
||||
</ul>
|
||||
<Link href="/register" className="w-full text-center bg-gray-800 hover:bg-gray-700 text-white py-4 rounded-xl font-bold transition-colors">Contact Sales</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-[#050505] pt-20 pb-10 border-t border-gray-900 text-gray-400">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-12 mb-16">
|
||||
<div className="col-span-2 md:col-span-1">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<div className="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold tracking-tighter">CQ</span>
|
||||
</div>
|
||||
<span className="font-extrabold text-xl tracking-tight text-white">CK-QR</span>
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed mb-6">
|
||||
The world's most powerful platform for generating, managing, and tracking dynamic QR codes at scale.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-bold mb-4 uppercase tracking-wider text-sm">Product</h4>
|
||||
<ul className="space-y-3 text-sm">
|
||||
<li><Link href="/features/dynamic-qr" className="hover:text-white transition-colors">Dynamic QR Codes</Link></li>
|
||||
<li><Link href="/features/analytics" className="hover:text-white transition-colors">Analytics Dashboard</Link></li>
|
||||
<li><Link href="/features/design-studio" className="hover:text-white transition-colors">Design Studio</Link></li>
|
||||
<li><Link href="/features/api" className="hover:text-white transition-colors">API & Webhooks</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-bold mb-4 uppercase tracking-wider text-sm">Company</h4>
|
||||
<ul className="space-y-3 text-sm">
|
||||
<li><Link href="/company/about" className="hover:text-white transition-colors">About Us</Link></li>
|
||||
<li><Link href="/company/careers" className="hover:text-white transition-colors">Careers</Link></li>
|
||||
<li><Link href="/company/blog" className="hover:text-white transition-colors">Blog</Link></li>
|
||||
<li><Link href="/company/contact" className="hover:text-white transition-colors">Contact</Link></li>
|
||||
<li><Link href="/support/how-to-use" className="hover:text-white transition-colors text-indigo-400">Support & How-To</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-bold mb-4 uppercase tracking-wider text-sm">Legal</h4>
|
||||
<ul className="space-y-3 text-sm">
|
||||
<li><Link href="/legal/privacy" className="hover:text-white transition-colors">Privacy Policy</Link></li>
|
||||
<li><Link href="/legal/terms" className="hover:text-white transition-colors">Terms of Service</Link></li>
|
||||
<li><Link href="/legal/cookies" className="hover:text-white transition-colors">Cookie Policy</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-900 pt-8 flex flex-col md:flex-row justify-between items-center text-sm">
|
||||
<p>© {new Date().getFullYear()} CK-QR Inc. All rights reserved.</p>
|
||||
<div className="flex gap-4 mt-4 md:mt-0">
|
||||
<a href="#" className="text-gray-500 hover:text-white transition-colors">Twitter</a>
|
||||
<a href="#" className="text-gray-500 hover:text-white transition-colors">GitHub</a>
|
||||
<a href="#" className="text-gray-500 hover:text-white transition-colors">LinkedIn</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import api from '@/lib/api';
|
||||
|
||||
export default function RegisterPage() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleRegister = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await api.post('/auth/register', { email, password });
|
||||
router.push('/dashboard');
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || 'Failed to register');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] flex flex-col justify-center py-12 sm:px-6 lg:px-8 font-sans relative overflow-hidden">
|
||||
|
||||
{/* Background Decorators */}
|
||||
<div className="absolute top-[-10%] right-[-10%] w-96 h-96 bg-indigo-600 rounded-full mix-blend-multiply filter blur-[128px] opacity-20 animate-blob"></div>
|
||||
<div className="absolute bottom-[-10%] left-[-10%] w-96 h-96 bg-pink-600 rounded-full mix-blend-multiply filter blur-[128px] opacity-20 animate-blob animation-delay-2000"></div>
|
||||
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md relative z-10">
|
||||
<Link href="/" className="flex justify-center mb-8 group">
|
||||
<div className="w-14 h-14 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl flex items-center justify-center shadow-lg shadow-indigo-500/20 group-hover:scale-105 transition-transform">
|
||||
<span className="text-white font-black text-3xl tracking-tighter">CQ</span>
|
||||
</div>
|
||||
</Link>
|
||||
<h2 className="text-center text-4xl font-black text-white tracking-tight">Join CK-QR</h2>
|
||||
<p className="mt-3 text-center text-gray-400 font-medium">
|
||||
Start your 14-day free trial. No credit card required.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-md relative z-10">
|
||||
<div className="bg-[#111111] py-10 px-8 shadow-2xl sm:rounded-3xl border border-gray-800 backdrop-blur-xl">
|
||||
<form className="space-y-6" onSubmit={handleRegister}>
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/50 text-red-400 p-4 rounded-xl text-sm text-center font-medium">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-300 mb-2">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
className="block w-full px-4 py-3 bg-[#1a1a1a] border border-gray-700 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
|
||||
placeholder="you@company.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-300 mb-2">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
className="block w-full px-4 py-3 bg-[#1a1a1a] border border-gray-700 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
|
||||
placeholder="••••••••"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full flex justify-center py-4 px-4 border border-transparent rounded-xl shadow-lg shadow-indigo-500/20 text-sm font-bold text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-500 disabled:opacity-50 transition-all active:scale-[0.98]"
|
||||
>
|
||||
{loading ? 'Creating Account...' : 'Create Account'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="mt-6 text-center text-sm font-medium text-gray-500">
|
||||
Already have an account?{' '}
|
||||
<Link href="/login" className="text-indigo-400 hover:text-indigo-300 transition-colors">
|
||||
Log in here
|
||||
</Link>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function SupportPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col font-sans">
|
||||
<nav className="bg-gray-900 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-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="flex-1 max-w-4xl mx-auto w-full px-6 py-16">
|
||||
<h1 className="text-5xl font-black text-gray-900 mb-6">Support & Documentation</h1>
|
||||
<p className="text-xl text-gray-600 mb-10 leading-relaxed">
|
||||
Learn how to get the most out of the CK-QR platform.
|
||||
</p>
|
||||
|
||||
<div className="space-y-12">
|
||||
|
||||
<div className="bg-white p-8 rounded-2xl shadow-sm border border-gray-200">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">1. What is a Dynamic QR Code?</h2>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
Standard QR codes encode the destination URL directly into the black-and-white square pattern. If you need to change the URL, you must generate a completely new square pattern and reprint all your materials.
|
||||
<br/><br/>
|
||||
A <strong>Dynamic QR code</strong> encodes a short-link (e.g. ckqr.com/r/xyz) instead. This short-link sits on our servers and instantly redirects users to your real destination. This allows you to change the final destination at any time without ever changing the physical printed code!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-8 rounded-2xl shadow-sm border border-gray-200">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">2. Creating your first QR Code</h2>
|
||||
<ul className="list-decimal list-inside space-y-3 text-gray-600">
|
||||
<li>Log into your dashboard and click <strong>"Create New QR"</strong>.</li>
|
||||
<li>Enter a title (for your own tracking) and the destination URL where you want users to go.</li>
|
||||
<li>Click <strong>Generate</strong>. The system will create your unique short-link and construct the QR pattern.</li>
|
||||
<li>Download the resulting image and print it on your flyers, menus, or packaging!</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-8 rounded-2xl shadow-sm border border-gray-200">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">3. Updating a Destination URL</h2>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
Made a mistake or launching a new campaign? You don't need to reprint!
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-3 text-gray-600 mt-4">
|
||||
<li>Go to your dashboard and find the specific QR code in the list.</li>
|
||||
<li>Click the <strong>Edit</strong> button next to the destination URL.</li>
|
||||
<li>Type in the new URL and hit save.</li>
|
||||
<li>The redirect is updated instantly globally. Anyone who scans the printed code will now be routed to the new URL.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import Link from 'next/link';
|
||||
import { Home, PlusSquare, BarChart2, Settings, LogOut } from 'lucide-react';
|
||||
|
||||
export default function Sidebar() {
|
||||
return (
|
||||
<div className="w-64 bg-white border-r h-full flex flex-col">
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold text-indigo-600">CK-QR</h1>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 px-4 space-y-2">
|
||||
<Link href="/dashboard" className="flex items-center gap-3 px-3 py-2 text-gray-700 rounded-md hover:bg-indigo-50 hover:text-indigo-600 transition-colors">
|
||||
<Home size={20} />
|
||||
<span>My QR Codes</span>
|
||||
</Link>
|
||||
<Link href="/dashboard/create" className="flex items-center gap-3 px-3 py-2 text-gray-700 rounded-md hover:bg-indigo-50 hover:text-indigo-600 transition-colors">
|
||||
<PlusSquare size={20} />
|
||||
<span>Create QR</span>
|
||||
</Link>
|
||||
<Link href="/dashboard/analytics" className="flex items-center gap-3 px-3 py-2 text-gray-700 rounded-md hover:bg-indigo-50 hover:text-indigo-600 transition-colors">
|
||||
<BarChart2 size={20} />
|
||||
<span>Analytics</span>
|
||||
</Link>
|
||||
<Link href="/dashboard/settings" className="flex items-center gap-3 px-3 py-2 text-gray-700 rounded-md hover:bg-indigo-50 hover:text-indigo-600 transition-colors">
|
||||
<Settings size={20} />
|
||||
<span>Settings</span>
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t">
|
||||
<button className="flex items-center gap-3 px-3 py-2 w-full text-left text-red-600 rounded-md hover:bg-red-50 transition-colors">
|
||||
<LogOut size={20} />
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { Options } from 'qr-code-styling';
|
||||
|
||||
interface DesignPanelProps {
|
||||
options: Options;
|
||||
onChange: (options: Options) => void;
|
||||
}
|
||||
|
||||
export default function DesignPanel({ options, onChange }: DesignPanelProps) {
|
||||
|
||||
const handleColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange({
|
||||
...options,
|
||||
dotsOptions: { ...options.dotsOptions, color: e.target.value }
|
||||
});
|
||||
};
|
||||
|
||||
const handleDotTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
onChange({
|
||||
...options,
|
||||
dotsOptions: { ...options.dotsOptions, type: e.target.value as any }
|
||||
});
|
||||
};
|
||||
|
||||
const handleEyeTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
onChange({
|
||||
...options,
|
||||
cornersSquareOptions: { ...options.cornersSquareOptions, type: e.target.value as any }
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6">
|
||||
<h2 className="text-xl font-bold text-gray-800 border-b pb-4">Design Customization</h2>
|
||||
|
||||
{/* Colors */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Dot Color</label>
|
||||
<div className="flex items-center gap-4">
|
||||
<input
|
||||
type="color"
|
||||
value={options.dotsOptions?.color || '#000000'}
|
||||
onChange={handleColorChange}
|
||||
className="w-10 h-10 rounded cursor-pointer border-0 p-0"
|
||||
/>
|
||||
<span className="text-gray-500 font-mono text-sm">{options.dotsOptions?.color}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shapes */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Dot Pattern</label>
|
||||
<select
|
||||
value={options.dotsOptions?.type || 'square'}
|
||||
onChange={handleDotTypeChange}
|
||||
className="w-full border-gray-300 rounded-lg shadow-sm p-2 border focus:ring-indigo-500 focus:border-indigo-500"
|
||||
>
|
||||
<option value="square">Square</option>
|
||||
<option value="dots">Dots</option>
|
||||
<option value="rounded">Rounded</option>
|
||||
<option value="extra-rounded">Extra Rounded</option>
|
||||
<option value="classy">Classy</option>
|
||||
<option value="classy-rounded">Classy Rounded</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Eye Frame Shape</label>
|
||||
<select
|
||||
value={options.cornersSquareOptions?.type || 'square'}
|
||||
onChange={handleEyeTypeChange}
|
||||
className="w-full border-gray-300 rounded-lg shadow-sm p-2 border focus:ring-indigo-500 focus:border-indigo-500"
|
||||
>
|
||||
<option value="square">Square</option>
|
||||
<option value="dot">Dot</option>
|
||||
<option value="extra-rounded">Extra Rounded</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
'use client';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import QRCodeStyling, { Options } from 'qr-code-styling';
|
||||
|
||||
interface LivePreviewProps {
|
||||
options: Options;
|
||||
}
|
||||
|
||||
export default function LivePreview({ options }: LivePreviewProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [qrCode] = useState<QRCodeStyling>(new QRCodeStyling(options));
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
qrCode.append(ref.current);
|
||||
}
|
||||
}, [qrCode, ref]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!qrCode) return;
|
||||
qrCode.update(options);
|
||||
}, [qrCode, options]);
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center p-8 bg-gray-50 rounded-xl border border-gray-200 min-h-[400px]">
|
||||
<div ref={ref} className="shadow-lg rounded-xl overflow-hidden bg-white" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4001/api',
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Add a request interceptor to attach JWT token
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
// We only access localStorage in the browser
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
Reference in New Issue
Block a user