search console SEO ableitungen
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,392 +1,392 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Dialog } from '@/components/ui/Dialog';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
interface Integration {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
status: 'active' | 'inactive' | 'coming_soon';
|
||||
category: string;
|
||||
features: string[];
|
||||
}
|
||||
|
||||
export default function IntegrationsPage() {
|
||||
const { t } = useTranslation();
|
||||
const [selectedIntegration, setSelectedIntegration] = useState<Integration | null>(null);
|
||||
const [showSetupDialog, setShowSetupDialog] = useState(false);
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [webhookUrl, setWebhookUrl] = useState('');
|
||||
|
||||
const integrations: Integration[] = [
|
||||
{
|
||||
id: 'zapier',
|
||||
name: 'Zapier',
|
||||
description: 'Connect QR Master with 5,000+ apps',
|
||||
icon: '⚡',
|
||||
status: 'active',
|
||||
category: 'Automation',
|
||||
features: [
|
||||
'Trigger actions when QR codes are scanned',
|
||||
'Create QR codes from other apps',
|
||||
'Update QR destinations automatically',
|
||||
'Sync analytics to spreadsheets',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'airtable',
|
||||
name: 'Airtable',
|
||||
description: 'Sync QR codes with your Airtable bases',
|
||||
icon: '📊',
|
||||
status: 'inactive',
|
||||
category: 'Database',
|
||||
features: [
|
||||
'Two-way sync with Airtable',
|
||||
'Bulk import from bases',
|
||||
'Auto-update QR content',
|
||||
'Analytics dashboard integration',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'google-sheets',
|
||||
name: 'Google Sheets',
|
||||
description: 'Manage QR codes from spreadsheets',
|
||||
icon: '📈',
|
||||
status: 'inactive',
|
||||
category: 'Spreadsheet',
|
||||
features: [
|
||||
'Import QR codes from sheets',
|
||||
'Export analytics data',
|
||||
'Real-time sync',
|
||||
'Collaborative QR management',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'slack',
|
||||
name: 'Slack',
|
||||
description: 'Get QR scan notifications in Slack',
|
||||
icon: '💬',
|
||||
status: 'coming_soon',
|
||||
category: 'Communication',
|
||||
features: [
|
||||
'Real-time scan notifications',
|
||||
'Daily analytics summaries',
|
||||
'Team collaboration',
|
||||
'Custom alert rules',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'webhook',
|
||||
name: 'Webhooks',
|
||||
description: 'Send data to any URL',
|
||||
icon: '🔗',
|
||||
status: 'active',
|
||||
category: 'Developer',
|
||||
features: [
|
||||
'Custom webhook endpoints',
|
||||
'Real-time event streaming',
|
||||
'Retry logic',
|
||||
'Event filtering',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'api',
|
||||
name: 'REST API',
|
||||
description: 'Full programmatic access',
|
||||
icon: '🔧',
|
||||
status: 'active',
|
||||
category: 'Developer',
|
||||
features: [
|
||||
'Complete CRUD operations',
|
||||
'Bulk operations',
|
||||
'Analytics API',
|
||||
'Rate limiting: 1000 req/hour',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const stats = {
|
||||
totalQRCodes: 234,
|
||||
activeIntegrations: 2,
|
||||
syncStatus: 'Synced',
|
||||
availableServices: 6,
|
||||
};
|
||||
|
||||
const handleActivate = (integration: Integration) => {
|
||||
setSelectedIntegration(integration);
|
||||
setShowSetupDialog(true);
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
// Simulate API test
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
alert('Connection successful!');
|
||||
};
|
||||
|
||||
const handleSaveIntegration = () => {
|
||||
setShowSetupDialog(false);
|
||||
// Update integration status
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">{t('integrations.title')}</h1>
|
||||
<p className="text-gray-600 mt-2">{t('integrations.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-1">QR Codes Total</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.totalQRCodes}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-1">Active Integrations</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.activeIntegrations}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 bg-success-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-success-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-1">Sync Status</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.syncStatus}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 bg-info-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-info-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-1">Available Services</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.availableServices}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 bg-warning-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-warning-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Integration Cards */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{integrations.map((integration) => (
|
||||
<Card key={integration.id} className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="text-3xl">{integration.icon}</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg">{integration.name}</CardTitle>
|
||||
<Badge
|
||||
variant={
|
||||
integration.status === 'active' ? 'success' :
|
||||
integration.status === 'coming_soon' ? 'warning' :
|
||||
'default'
|
||||
}
|
||||
className="mt-1"
|
||||
>
|
||||
{integration.status === 'active' ? 'Active' :
|
||||
integration.status === 'coming_soon' ? 'Coming Soon' :
|
||||
'Inactive'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-600 mb-4">{integration.description}</p>
|
||||
|
||||
<div className="space-y-2 mb-4">
|
||||
{integration.features.slice(0, 3).map((feature, index) => (
|
||||
<div key={index} className="flex items-start space-x-2">
|
||||
<svg className="w-4 h-4 text-success-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-sm text-gray-700">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{integration.status === 'active' ? (
|
||||
<Button variant="outline" className="w-full">
|
||||
Configure
|
||||
</Button>
|
||||
) : integration.status === 'coming_soon' ? (
|
||||
<Button variant="outline" className="w-full" disabled>
|
||||
Coming Soon
|
||||
</Button>
|
||||
) : (
|
||||
<Button className="w-full" onClick={() => handleActivate(integration)}>
|
||||
Activate & Configure
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Setup Dialog */}
|
||||
{showSetupDialog && selectedIntegration && (
|
||||
<Dialog
|
||||
open={showSetupDialog}
|
||||
onOpenChange={setShowSetupDialog}
|
||||
>
|
||||
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6 max-w-lg mx-auto">
|
||||
<h2 className="text-lg font-semibold mb-4">Setup {selectedIntegration.name}</h2>
|
||||
<div className="space-y-4">
|
||||
{selectedIntegration.id === 'zapier' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Webhook URL
|
||||
</label>
|
||||
<Input
|
||||
value="https://hooks.zapier.com/hooks/catch/123456/abcdef/"
|
||||
readOnly
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Copy this URL to your Zapier trigger
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Events to Send
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center">
|
||||
<input type="checkbox" className="mr-2" defaultChecked />
|
||||
<span className="text-sm">QR Code Scanned</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input type="checkbox" className="mr-2" defaultChecked />
|
||||
<span className="text-sm">QR Code Created</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input type="checkbox" className="mr-2" />
|
||||
<span className="text-sm">QR Code Updated</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-gray-50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 mb-2">Sample Payload</h4>
|
||||
<pre className="text-xs text-gray-600 overflow-x-auto">
|
||||
{`{
|
||||
"event": "qr_scanned",
|
||||
"qr_id": "abc123",
|
||||
"title": "Product Page",
|
||||
"timestamp": "2025-01-01T12:00:00Z",
|
||||
"location": "United States",
|
||||
"device": "mobile"
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedIntegration.id === 'airtable' && (
|
||||
<>
|
||||
<Input
|
||||
label="API Key"
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder="key..."
|
||||
/>
|
||||
<Input
|
||||
label="Base ID"
|
||||
value=""
|
||||
placeholder="app..."
|
||||
/>
|
||||
<Input
|
||||
label="Table Name"
|
||||
value=""
|
||||
placeholder="QR Codes"
|
||||
/>
|
||||
<Button variant="outline" onClick={handleTestConnection}>
|
||||
Test Connection
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedIntegration.id === 'google-sheets' && (
|
||||
<>
|
||||
<div className="text-center p-6">
|
||||
<Button>
|
||||
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
||||
</svg>
|
||||
Connect Google Account
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
label="Spreadsheet URL"
|
||||
value=""
|
||||
placeholder="https://docs.google.com/spreadsheets/..."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-3 pt-4">
|
||||
<Button variant="outline" onClick={() => setShowSetupDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSaveIntegration}>
|
||||
Save Integration
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Dialog } from '@/components/ui/Dialog';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
interface Integration {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
status: 'active' | 'inactive' | 'coming_soon';
|
||||
category: string;
|
||||
features: string[];
|
||||
}
|
||||
|
||||
export default function IntegrationsPage() {
|
||||
const { t } = useTranslation();
|
||||
const [selectedIntegration, setSelectedIntegration] = useState<Integration | null>(null);
|
||||
const [showSetupDialog, setShowSetupDialog] = useState(false);
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [webhookUrl, setWebhookUrl] = useState('');
|
||||
|
||||
const integrations: Integration[] = [
|
||||
{
|
||||
id: 'zapier',
|
||||
name: 'Zapier',
|
||||
description: 'Connect QR Master with 5,000+ apps',
|
||||
icon: '⚡',
|
||||
status: 'active',
|
||||
category: 'Automation',
|
||||
features: [
|
||||
'Trigger actions when QR codes are scanned',
|
||||
'Create QR codes from other apps',
|
||||
'Update QR destinations automatically',
|
||||
'Sync analytics to spreadsheets',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'airtable',
|
||||
name: 'Airtable',
|
||||
description: 'Sync QR codes with your Airtable bases',
|
||||
icon: '📊',
|
||||
status: 'inactive',
|
||||
category: 'Database',
|
||||
features: [
|
||||
'Two-way sync with Airtable',
|
||||
'Bulk import from bases',
|
||||
'Auto-update QR content',
|
||||
'Analytics dashboard integration',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'google-sheets',
|
||||
name: 'Google Sheets',
|
||||
description: 'Manage QR codes from spreadsheets',
|
||||
icon: '📈',
|
||||
status: 'inactive',
|
||||
category: 'Spreadsheet',
|
||||
features: [
|
||||
'Import QR codes from sheets',
|
||||
'Export analytics data',
|
||||
'Real-time sync',
|
||||
'Collaborative QR management',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'slack',
|
||||
name: 'Slack',
|
||||
description: 'Get QR scan notifications in Slack',
|
||||
icon: '💬',
|
||||
status: 'coming_soon',
|
||||
category: 'Communication',
|
||||
features: [
|
||||
'Real-time scan notifications',
|
||||
'Daily analytics summaries',
|
||||
'Team collaboration',
|
||||
'Custom alert rules',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'webhook',
|
||||
name: 'Webhooks',
|
||||
description: 'Send data to any URL',
|
||||
icon: '🔗',
|
||||
status: 'active',
|
||||
category: 'Developer',
|
||||
features: [
|
||||
'Custom webhook endpoints',
|
||||
'Real-time event streaming',
|
||||
'Retry logic',
|
||||
'Event filtering',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'api',
|
||||
name: 'REST API',
|
||||
description: 'Full programmatic access',
|
||||
icon: '🔧',
|
||||
status: 'active',
|
||||
category: 'Developer',
|
||||
features: [
|
||||
'Complete CRUD operations',
|
||||
'Bulk operations',
|
||||
'Analytics API',
|
||||
'Rate limiting: 1000 req/hour',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const stats = {
|
||||
totalQRCodes: 234,
|
||||
activeIntegrations: 2,
|
||||
syncStatus: 'Synced',
|
||||
availableServices: 6,
|
||||
};
|
||||
|
||||
const handleActivate = (integration: Integration) => {
|
||||
setSelectedIntegration(integration);
|
||||
setShowSetupDialog(true);
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
// Simulate API test
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
alert('Connection successful!');
|
||||
};
|
||||
|
||||
const handleSaveIntegration = () => {
|
||||
setShowSetupDialog(false);
|
||||
// Update integration status
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">{t('integrations.title')}</h1>
|
||||
<p className="text-gray-600 mt-2">{t('integrations.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-1">QR Codes Total</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.totalQRCodes}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-1">Active Integrations</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.activeIntegrations}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 bg-success-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-success-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-1">Sync Status</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.syncStatus}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 bg-info-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-info-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-1">Available Services</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.availableServices}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 bg-warning-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-warning-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Integration Cards */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{integrations.map((integration) => (
|
||||
<Card key={integration.id} className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="text-3xl">{integration.icon}</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg">{integration.name}</CardTitle>
|
||||
<Badge
|
||||
variant={
|
||||
integration.status === 'active' ? 'success' :
|
||||
integration.status === 'coming_soon' ? 'warning' :
|
||||
'default'
|
||||
}
|
||||
className="mt-1"
|
||||
>
|
||||
{integration.status === 'active' ? 'Active' :
|
||||
integration.status === 'coming_soon' ? 'Coming Soon' :
|
||||
'Inactive'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-600 mb-4">{integration.description}</p>
|
||||
|
||||
<div className="space-y-2 mb-4">
|
||||
{integration.features.slice(0, 3).map((feature, index) => (
|
||||
<div key={index} className="flex items-start space-x-2">
|
||||
<svg className="w-4 h-4 text-success-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-sm text-gray-700">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{integration.status === 'active' ? (
|
||||
<Button variant="outline" className="w-full">
|
||||
Configure
|
||||
</Button>
|
||||
) : integration.status === 'coming_soon' ? (
|
||||
<Button variant="outline" className="w-full" disabled>
|
||||
Coming Soon
|
||||
</Button>
|
||||
) : (
|
||||
<Button className="w-full" onClick={() => handleActivate(integration)}>
|
||||
Activate & Configure
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Setup Dialog */}
|
||||
{showSetupDialog && selectedIntegration && (
|
||||
<Dialog
|
||||
open={showSetupDialog}
|
||||
onOpenChange={setShowSetupDialog}
|
||||
>
|
||||
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6 max-w-lg mx-auto">
|
||||
<h2 className="text-lg font-semibold mb-4">Setup {selectedIntegration.name}</h2>
|
||||
<div className="space-y-4">
|
||||
{selectedIntegration.id === 'zapier' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Webhook URL
|
||||
</label>
|
||||
<Input
|
||||
value="https://hooks.zapier.com/hooks/catch/123456/abcdef/"
|
||||
readOnly
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Copy this URL to your Zapier trigger
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Events to Send
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center">
|
||||
<input type="checkbox" className="mr-2" defaultChecked />
|
||||
<span className="text-sm">QR Code Scanned</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input type="checkbox" className="mr-2" defaultChecked />
|
||||
<span className="text-sm">QR Code Created</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input type="checkbox" className="mr-2" />
|
||||
<span className="text-sm">QR Code Updated</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-gray-50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 mb-2">Sample Payload</h4>
|
||||
<pre className="text-xs text-gray-600 overflow-x-auto">
|
||||
{`{
|
||||
"event": "qr_scanned",
|
||||
"qr_id": "abc123",
|
||||
"title": "Product Page",
|
||||
"timestamp": "2025-01-01T12:00:00Z",
|
||||
"location": "United States",
|
||||
"device": "mobile"
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedIntegration.id === 'airtable' && (
|
||||
<>
|
||||
<Input
|
||||
label="API Key"
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder="key..."
|
||||
/>
|
||||
<Input
|
||||
label="Base ID"
|
||||
value=""
|
||||
placeholder="app..."
|
||||
/>
|
||||
<Input
|
||||
label="Table Name"
|
||||
value=""
|
||||
placeholder="QR Codes"
|
||||
/>
|
||||
<Button variant="outline" onClick={handleTestConnection}>
|
||||
Test Connection
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedIntegration.id === 'google-sheets' && (
|
||||
<>
|
||||
<div className="text-center p-6">
|
||||
<Button>
|
||||
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
||||
</svg>
|
||||
Connect Google Account
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
label="Spreadsheet URL"
|
||||
value=""
|
||||
placeholder="https://docs.google.com/spreadsheets/..."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-3 pt-4">
|
||||
<Button variant="outline" onClick={() => setShowSetupDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSaveIntegration}>
|
||||
Save Integration
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,386 +1,386 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { useCsrf } from '@/hooks/useCsrf';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
import ChangePasswordModal from '@/components/settings/ChangePasswordModal';
|
||||
|
||||
type TabType = 'profile' | 'subscription';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { fetchWithCsrf } = useCsrf();
|
||||
const [activeTab, setActiveTab] = useState<TabType>('profile');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||
|
||||
// Profile states
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
// Subscription states
|
||||
const [plan, setPlan] = useState('FREE');
|
||||
const [usageStats, setUsageStats] = useState({
|
||||
dynamicUsed: 0,
|
||||
dynamicLimit: 3,
|
||||
staticUsed: 0,
|
||||
});
|
||||
|
||||
// Load user data
|
||||
useEffect(() => {
|
||||
const fetchUserData = async () => {
|
||||
try {
|
||||
// Load from localStorage
|
||||
const userStr = localStorage.getItem('user');
|
||||
if (userStr) {
|
||||
const user = JSON.parse(userStr);
|
||||
setName(user.name || '');
|
||||
setEmail(user.email || '');
|
||||
}
|
||||
|
||||
// Fetch plan from API
|
||||
const planResponse = await fetch('/api/user/plan');
|
||||
if (planResponse.ok) {
|
||||
const data = await planResponse.json();
|
||||
setPlan(data.plan || 'FREE');
|
||||
}
|
||||
|
||||
// Fetch usage stats from API
|
||||
const statsResponse = await fetch('/api/user/stats');
|
||||
if (statsResponse.ok) {
|
||||
const data = await statsResponse.json();
|
||||
setUsageStats(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load user data:', e);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserData();
|
||||
}, []);
|
||||
|
||||
const handleSaveProfile = async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Save to backend API
|
||||
const response = await fetchWithCsrf('/api/user/profile', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to update profile');
|
||||
}
|
||||
|
||||
// Update user data in localStorage
|
||||
const userStr = localStorage.getItem('user');
|
||||
if (userStr) {
|
||||
const user = JSON.parse(userStr);
|
||||
user.name = name;
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
}
|
||||
|
||||
showToast('Profile updated successfully!', 'success');
|
||||
} catch (error: any) {
|
||||
console.error('Error saving profile:', error);
|
||||
showToast(error.message || 'Failed to update profile', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleManageSubscription = async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetchWithCsrf('/api/stripe/portal', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to open subscription management');
|
||||
}
|
||||
|
||||
// Redirect to Stripe Customer Portal
|
||||
window.location.href = data.url;
|
||||
} catch (error: any) {
|
||||
console.error('Error opening portal:', error);
|
||||
showToast(error.message || 'Failed to open subscription management', 'error');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAccount = async () => {
|
||||
const confirmed = window.confirm(
|
||||
'Are you sure you want to delete your account? This will permanently delete all your data, including all QR codes and analytics. This action cannot be undone.'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
// Double confirmation for safety
|
||||
const doubleConfirmed = window.confirm(
|
||||
'This is your last warning. Are you absolutely sure you want to permanently delete your account?'
|
||||
);
|
||||
|
||||
if (!doubleConfirmed) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetchWithCsrf('/api/user/delete', {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to delete account');
|
||||
}
|
||||
|
||||
// Clear local storage and redirect to login
|
||||
localStorage.clear();
|
||||
showToast('Account deleted successfully', 'success');
|
||||
|
||||
// Redirect to home page after a short delay
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1500);
|
||||
} catch (error: any) {
|
||||
console.error('Error deleting account:', error);
|
||||
showToast(error.message || 'Failed to delete account', 'error');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getPlanLimits = () => {
|
||||
switch (plan) {
|
||||
case 'PRO':
|
||||
return { dynamic: 50, price: '€9', period: 'per month' };
|
||||
case 'BUSINESS':
|
||||
return { dynamic: 500, price: '€29', period: 'per month' };
|
||||
default:
|
||||
return { dynamic: 3, price: '€0', period: 'forever' };
|
||||
}
|
||||
};
|
||||
|
||||
const planLimits = getPlanLimits();
|
||||
const usagePercentage = (usageStats.dynamicUsed / usageStats.dynamicLimit) * 100;
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Settings</h1>
|
||||
<p className="text-gray-600 mt-2">Manage your account settings and preferences</p>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="border-b border-gray-200 mb-6">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab('profile')}
|
||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'profile'
|
||||
? 'border-primary-500 text-primary-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Profile
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('subscription')}
|
||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'subscription'
|
||||
? 'border-primary-500 text-primary-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Subscription
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'profile' && (
|
||||
<div className="space-y-6">
|
||||
{/* Profile Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Profile Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="Enter your name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
disabled
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-50 text-gray-500 cursor-not-allowed"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Email cannot be changed
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Security */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Security</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900">Password</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Update your password to keep your account secure
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowPasswordModal(true)}
|
||||
>
|
||||
Change Password
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Account Deletion */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-red-600">Delete Account</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900">Delete your account</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Permanently delete your account and all data. This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-red-600 text-red-600 hover:bg-red-50"
|
||||
onClick={handleDeleteAccount}
|
||||
>
|
||||
Delete Account
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleSaveProfile}
|
||||
disabled={loading}
|
||||
size="lg"
|
||||
variant="primary"
|
||||
>
|
||||
{loading ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'subscription' && (
|
||||
<div className="space-y-6">
|
||||
{/* Current Plan */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Current Plan</CardTitle>
|
||||
<Badge variant={plan === 'FREE' ? 'default' : plan === 'PRO' ? 'info' : 'warning'}>
|
||||
{plan}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-baseline">
|
||||
<span className="text-4xl font-bold">{planLimits.price}</span>
|
||||
<span className="text-gray-600 ml-2">{planLimits.period}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600">Dynamic QR Codes</span>
|
||||
<span className="font-medium">
|
||||
{usageStats.dynamicUsed} of {usageStats.dynamicLimit} used
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-primary-600 h-2 rounded-full transition-all"
|
||||
style={{ width: `${Math.min(usagePercentage, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600">Static QR Codes</span>
|
||||
<span className="font-medium">Unlimited ∞</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div className="bg-success-600 h-2 rounded-full" style={{ width: '100%' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{plan !== 'FREE' && (
|
||||
<div className="pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => window.location.href = '/pricing'}
|
||||
>
|
||||
Manage Subscription
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{plan === 'FREE' && (
|
||||
<div className="pt-4 border-t">
|
||||
<Button variant="primary" className="w-full" onClick={() => window.location.href = '/pricing'}>
|
||||
Upgrade Plan
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Change Password Modal */}
|
||||
<ChangePasswordModal
|
||||
isOpen={showPasswordModal}
|
||||
onClose={() => setShowPasswordModal(false)}
|
||||
onSuccess={() => {
|
||||
setShowPasswordModal(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { useCsrf } from '@/hooks/useCsrf';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
import ChangePasswordModal from '@/components/settings/ChangePasswordModal';
|
||||
|
||||
type TabType = 'profile' | 'subscription';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { fetchWithCsrf } = useCsrf();
|
||||
const [activeTab, setActiveTab] = useState<TabType>('profile');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||
|
||||
// Profile states
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
// Subscription states
|
||||
const [plan, setPlan] = useState('FREE');
|
||||
const [usageStats, setUsageStats] = useState({
|
||||
dynamicUsed: 0,
|
||||
dynamicLimit: 3,
|
||||
staticUsed: 0,
|
||||
});
|
||||
|
||||
// Load user data
|
||||
useEffect(() => {
|
||||
const fetchUserData = async () => {
|
||||
try {
|
||||
// Load from localStorage
|
||||
const userStr = localStorage.getItem('user');
|
||||
if (userStr) {
|
||||
const user = JSON.parse(userStr);
|
||||
setName(user.name || '');
|
||||
setEmail(user.email || '');
|
||||
}
|
||||
|
||||
// Fetch plan from API
|
||||
const planResponse = await fetch('/api/user/plan');
|
||||
if (planResponse.ok) {
|
||||
const data = await planResponse.json();
|
||||
setPlan(data.plan || 'FREE');
|
||||
}
|
||||
|
||||
// Fetch usage stats from API
|
||||
const statsResponse = await fetch('/api/user/stats');
|
||||
if (statsResponse.ok) {
|
||||
const data = await statsResponse.json();
|
||||
setUsageStats(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load user data:', e);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserData();
|
||||
}, []);
|
||||
|
||||
const handleSaveProfile = async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Save to backend API
|
||||
const response = await fetchWithCsrf('/api/user/profile', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to update profile');
|
||||
}
|
||||
|
||||
// Update user data in localStorage
|
||||
const userStr = localStorage.getItem('user');
|
||||
if (userStr) {
|
||||
const user = JSON.parse(userStr);
|
||||
user.name = name;
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
}
|
||||
|
||||
showToast('Profile updated successfully!', 'success');
|
||||
} catch (error: any) {
|
||||
console.error('Error saving profile:', error);
|
||||
showToast(error.message || 'Failed to update profile', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleManageSubscription = async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetchWithCsrf('/api/stripe/portal', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to open subscription management');
|
||||
}
|
||||
|
||||
// Redirect to Stripe Customer Portal
|
||||
window.location.href = data.url;
|
||||
} catch (error: any) {
|
||||
console.error('Error opening portal:', error);
|
||||
showToast(error.message || 'Failed to open subscription management', 'error');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAccount = async () => {
|
||||
const confirmed = window.confirm(
|
||||
'Are you sure you want to delete your account? This will permanently delete all your data, including all QR codes and analytics. This action cannot be undone.'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
// Double confirmation for safety
|
||||
const doubleConfirmed = window.confirm(
|
||||
'This is your last warning. Are you absolutely sure you want to permanently delete your account?'
|
||||
);
|
||||
|
||||
if (!doubleConfirmed) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetchWithCsrf('/api/user/delete', {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to delete account');
|
||||
}
|
||||
|
||||
// Clear local storage and redirect to login
|
||||
localStorage.clear();
|
||||
showToast('Account deleted successfully', 'success');
|
||||
|
||||
// Redirect to home page after a short delay
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1500);
|
||||
} catch (error: any) {
|
||||
console.error('Error deleting account:', error);
|
||||
showToast(error.message || 'Failed to delete account', 'error');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getPlanLimits = () => {
|
||||
switch (plan) {
|
||||
case 'PRO':
|
||||
return { dynamic: 50, price: '€9', period: 'per month' };
|
||||
case 'BUSINESS':
|
||||
return { dynamic: 500, price: '€29', period: 'per month' };
|
||||
default:
|
||||
return { dynamic: 3, price: '€0', period: 'forever' };
|
||||
}
|
||||
};
|
||||
|
||||
const planLimits = getPlanLimits();
|
||||
const usagePercentage = (usageStats.dynamicUsed / usageStats.dynamicLimit) * 100;
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Settings</h1>
|
||||
<p className="text-gray-600 mt-2">Manage your account settings and preferences</p>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="border-b border-gray-200 mb-6">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab('profile')}
|
||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'profile'
|
||||
? 'border-primary-500 text-primary-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Profile
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('subscription')}
|
||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'subscription'
|
||||
? 'border-primary-500 text-primary-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Subscription
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'profile' && (
|
||||
<div className="space-y-6">
|
||||
{/* Profile Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Profile Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="Enter your name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
disabled
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-50 text-gray-500 cursor-not-allowed"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Email cannot be changed
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Security */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Security</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900">Password</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Update your password to keep your account secure
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowPasswordModal(true)}
|
||||
>
|
||||
Change Password
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Account Deletion */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-red-600">Delete Account</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900">Delete your account</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Permanently delete your account and all data. This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-red-600 text-red-600 hover:bg-red-50"
|
||||
onClick={handleDeleteAccount}
|
||||
>
|
||||
Delete Account
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleSaveProfile}
|
||||
disabled={loading}
|
||||
size="lg"
|
||||
variant="primary"
|
||||
>
|
||||
{loading ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'subscription' && (
|
||||
<div className="space-y-6">
|
||||
{/* Current Plan */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Current Plan</CardTitle>
|
||||
<Badge variant={plan === 'FREE' ? 'default' : plan === 'PRO' ? 'info' : 'warning'}>
|
||||
{plan}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-baseline">
|
||||
<span className="text-4xl font-bold">{planLimits.price}</span>
|
||||
<span className="text-gray-600 ml-2">{planLimits.period}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600">Dynamic QR Codes</span>
|
||||
<span className="font-medium">
|
||||
{usageStats.dynamicUsed} of {usageStats.dynamicLimit} used
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-primary-600 h-2 rounded-full transition-all"
|
||||
style={{ width: `${Math.min(usagePercentage, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600">Static QR Codes</span>
|
||||
<span className="font-medium">Unlimited ∞</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div className="bg-success-600 h-2 rounded-full" style={{ width: '100%' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{plan !== 'FREE' && (
|
||||
<div className="pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => window.location.href = '/pricing'}
|
||||
>
|
||||
Manage Subscription
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{plan === 'FREE' && (
|
||||
<div className="pt-4 border-t">
|
||||
<Button variant="primary" className="w-full" onClick={() => window.location.href = '/pricing'}>
|
||||
Upgrade Plan
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Change Password Modal */}
|
||||
<ChangePasswordModal
|
||||
isOpen={showPasswordModal}
|
||||
onClose={() => setShowPasswordModal(false)}
|
||||
onSuccess={() => {
|
||||
setShowPasswordModal(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,158 +1,158 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||
|
||||
export default function TestPage() {
|
||||
const [testResults, setTestResults] = useState<any>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const runTest = async () => {
|
||||
setLoading(true);
|
||||
const results: any = {};
|
||||
|
||||
try {
|
||||
// Step 1: Create a STATIC QR code
|
||||
console.log('Creating STATIC QR code...');
|
||||
const createResponse = await fetch('/api/qrs', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: 'Test Static QR',
|
||||
contentType: 'URL',
|
||||
content: { url: 'https://google.com' },
|
||||
isStatic: true,
|
||||
tags: [],
|
||||
style: {
|
||||
foregroundColor: '#000000',
|
||||
backgroundColor: '#FFFFFF',
|
||||
cornerStyle: 'square',
|
||||
size: 200,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const createdQR = await createResponse.json();
|
||||
results.created = createdQR;
|
||||
console.log('Created QR:', createdQR);
|
||||
|
||||
// Step 2: Fetch all QR codes
|
||||
console.log('Fetching QR codes...');
|
||||
const fetchResponse = await fetch('/api/qrs');
|
||||
const allQRs = await fetchResponse.json();
|
||||
results.fetched = allQRs;
|
||||
console.log('Fetched QRs:', allQRs);
|
||||
|
||||
// Step 3: Check debug endpoint
|
||||
console.log('Checking debug endpoint...');
|
||||
const debugResponse = await fetch('/api/debug');
|
||||
const debugData = await debugResponse.json();
|
||||
results.debug = debugData;
|
||||
console.log('Debug data:', debugData);
|
||||
|
||||
} catch (error) {
|
||||
results.error = String(error);
|
||||
console.error('Test error:', error);
|
||||
}
|
||||
|
||||
setTestResults(results);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const getQRValue = (qr: any) => {
|
||||
// Check for qrContent field
|
||||
if (qr?.content?.qrContent) {
|
||||
return qr.content.qrContent;
|
||||
}
|
||||
// Check for direct URL
|
||||
if (qr?.content?.url) {
|
||||
return qr.content.url;
|
||||
}
|
||||
// Fallback to redirect
|
||||
return `http://localhost:3001/r/${qr?.slug || 'unknown'}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<h1 className="text-3xl font-bold mb-6">QR Code Test Page</h1>
|
||||
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Test Static QR Code Creation</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button onClick={runTest} loading={loading}>
|
||||
Run Test
|
||||
</Button>
|
||||
|
||||
{testResults.created && (
|
||||
<div className="mt-6">
|
||||
<h3 className="font-semibold mb-2">Created QR Code:</h3>
|
||||
<pre className="bg-gray-100 p-3 rounded text-xs overflow-auto">
|
||||
{JSON.stringify(testResults.created, null, 2)}
|
||||
</pre>
|
||||
|
||||
<div className="mt-4">
|
||||
<h4 className="font-semibold mb-2">QR Code Preview:</h4>
|
||||
<div className="bg-gray-50 p-4 rounded">
|
||||
<QRCodeSVG
|
||||
value={getQRValue(testResults.created)}
|
||||
size={200}
|
||||
/>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
QR Value: {getQRValue(testResults.created)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{testResults.fetched && (
|
||||
<div className="mt-6">
|
||||
<h3 className="font-semibold mb-2">All QR Codes:</h3>
|
||||
<pre className="bg-gray-100 p-3 rounded text-xs overflow-auto max-h-64">
|
||||
{JSON.stringify(testResults.fetched, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{testResults.debug && (
|
||||
<div className="mt-6">
|
||||
<h3 className="font-semibold mb-2">Debug Data:</h3>
|
||||
<pre className="bg-gray-100 p-3 rounded text-xs overflow-auto max-h-64">
|
||||
{JSON.stringify(testResults.debug, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{testResults.error && (
|
||||
<div className="mt-6 p-4 bg-red-50 text-red-600 rounded">
|
||||
Error: {testResults.error}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Manual QR Tests</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">Direct URL QR (Should go to Google):</h3>
|
||||
<QRCodeSVG value="https://google.com" size={150} />
|
||||
<p className="text-sm text-gray-600 mt-1">Value: https://google.com</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">Redirect QR (Goes through localhost):</h3>
|
||||
<QRCodeSVG value="http://localhost:3001/r/test-slug" size={150} />
|
||||
<p className="text-sm text-gray-600 mt-1">Value: http://localhost:3001/r/test-slug</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||
|
||||
export default function TestPage() {
|
||||
const [testResults, setTestResults] = useState<any>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const runTest = async () => {
|
||||
setLoading(true);
|
||||
const results: any = {};
|
||||
|
||||
try {
|
||||
// Step 1: Create a STATIC QR code
|
||||
console.log('Creating STATIC QR code...');
|
||||
const createResponse = await fetch('/api/qrs', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: 'Test Static QR',
|
||||
contentType: 'URL',
|
||||
content: { url: 'https://google.com' },
|
||||
isStatic: true,
|
||||
tags: [],
|
||||
style: {
|
||||
foregroundColor: '#000000',
|
||||
backgroundColor: '#FFFFFF',
|
||||
cornerStyle: 'square',
|
||||
size: 200,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const createdQR = await createResponse.json();
|
||||
results.created = createdQR;
|
||||
console.log('Created QR:', createdQR);
|
||||
|
||||
// Step 2: Fetch all QR codes
|
||||
console.log('Fetching QR codes...');
|
||||
const fetchResponse = await fetch('/api/qrs');
|
||||
const allQRs = await fetchResponse.json();
|
||||
results.fetched = allQRs;
|
||||
console.log('Fetched QRs:', allQRs);
|
||||
|
||||
// Step 3: Check debug endpoint
|
||||
console.log('Checking debug endpoint...');
|
||||
const debugResponse = await fetch('/api/debug');
|
||||
const debugData = await debugResponse.json();
|
||||
results.debug = debugData;
|
||||
console.log('Debug data:', debugData);
|
||||
|
||||
} catch (error) {
|
||||
results.error = String(error);
|
||||
console.error('Test error:', error);
|
||||
}
|
||||
|
||||
setTestResults(results);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const getQRValue = (qr: any) => {
|
||||
// Check for qrContent field
|
||||
if (qr?.content?.qrContent) {
|
||||
return qr.content.qrContent;
|
||||
}
|
||||
// Check for direct URL
|
||||
if (qr?.content?.url) {
|
||||
return qr.content.url;
|
||||
}
|
||||
// Fallback to redirect
|
||||
return `http://localhost:3001/r/${qr?.slug || 'unknown'}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<h1 className="text-3xl font-bold mb-6">QR Code Test Page</h1>
|
||||
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Test Static QR Code Creation</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button onClick={runTest} loading={loading}>
|
||||
Run Test
|
||||
</Button>
|
||||
|
||||
{testResults.created && (
|
||||
<div className="mt-6">
|
||||
<h3 className="font-semibold mb-2">Created QR Code:</h3>
|
||||
<pre className="bg-gray-100 p-3 rounded text-xs overflow-auto">
|
||||
{JSON.stringify(testResults.created, null, 2)}
|
||||
</pre>
|
||||
|
||||
<div className="mt-4">
|
||||
<h4 className="font-semibold mb-2">QR Code Preview:</h4>
|
||||
<div className="bg-gray-50 p-4 rounded">
|
||||
<QRCodeSVG
|
||||
value={getQRValue(testResults.created)}
|
||||
size={200}
|
||||
/>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
QR Value: {getQRValue(testResults.created)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{testResults.fetched && (
|
||||
<div className="mt-6">
|
||||
<h3 className="font-semibold mb-2">All QR Codes:</h3>
|
||||
<pre className="bg-gray-100 p-3 rounded text-xs overflow-auto max-h-64">
|
||||
{JSON.stringify(testResults.fetched, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{testResults.debug && (
|
||||
<div className="mt-6">
|
||||
<h3 className="font-semibold mb-2">Debug Data:</h3>
|
||||
<pre className="bg-gray-100 p-3 rounded text-xs overflow-auto max-h-64">
|
||||
{JSON.stringify(testResults.debug, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{testResults.error && (
|
||||
<div className="mt-6 p-4 bg-red-50 text-red-600 rounded">
|
||||
Error: {testResults.error}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Manual QR Tests</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">Direct URL QR (Should go to Google):</h3>
|
||||
<QRCodeSVG value="https://google.com" size={150} />
|
||||
<p className="text-sm text-gray-600 mt-1">Value: https://google.com</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">Redirect QR (Goes through localhost):</h3>
|
||||
<QRCodeSVG value="http://localhost:3001/r/test-slug" size={150} />
|
||||
<p className="text-sm text-gray-600 mt-1">Value: http://localhost:3001/r/test-slug</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user