search console SEO ableitungen

This commit is contained in:
2026-03-23 19:01:52 -05:00
parent d47108d27c
commit e6b19e7a1c
150 changed files with 26257 additions and 25909 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}