🧩 Components: QR код модал и реферальная система
This commit is contained in:
167
components/QRCodeModal.tsx
Normal file
167
components/QRCodeModal.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { X, Download, Share2, Copy, Check } from 'lucide-react';
|
||||||
|
|
||||||
|
interface QRCodeModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
url: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QRCodeModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
url,
|
||||||
|
title = 'QR Код подписки',
|
||||||
|
}: QRCodeModalProps) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [qrDataUrl, setQrDataUrl] = useState<string>('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && url) {
|
||||||
|
// Generate QR code using qrcode library
|
||||||
|
import('qrcode').then((QRCode) => {
|
||||||
|
QRCode.toDataURL(url, {
|
||||||
|
width: 300,
|
||||||
|
margin: 2,
|
||||||
|
color: {
|
||||||
|
dark: '#000000',
|
||||||
|
light: '#FFFFFF',
|
||||||
|
},
|
||||||
|
errorCorrectionLevel: 'H',
|
||||||
|
})
|
||||||
|
.then((dataUrl) => {
|
||||||
|
setQrDataUrl(dataUrl);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Failed to generate QR code:', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isOpen, url]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const downloadQR = () => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = qrDataUrl;
|
||||||
|
a.download = 'vpn-subscription-qr.png';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
};
|
||||||
|
|
||||||
|
const shareQR = async () => {
|
||||||
|
if (!navigator.share) {
|
||||||
|
// Fallback: copy URL to clipboard
|
||||||
|
await copyUrl();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert data URL to blob
|
||||||
|
const response = await fetch(qrDataUrl);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const file = new File([blob], 'vpn-qr-code.png', { type: 'image/png' });
|
||||||
|
|
||||||
|
await navigator.share({
|
||||||
|
title: 'VPN Подписка',
|
||||||
|
text: 'QR код для подключения к VPN',
|
||||||
|
files: [file],
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to share QR code:', err);
|
||||||
|
// Fallback to copying URL
|
||||||
|
await copyUrl();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyUrl = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(url);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy URL:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-[var(--bg-card)] border border-slate-700 rounded-lg p-6 max-w-sm w-full"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="text-lg font-semibold text-white">{title}</h3>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 hover:bg-slate-700 rounded-lg transition-colors text-white"
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* QR Code */}
|
||||||
|
<div className="bg-white p-4 rounded-lg mb-6 flex items-center justify-center">
|
||||||
|
{qrDataUrl ? (
|
||||||
|
<img src={qrDataUrl} alt="QR Code" className="w-full h-auto max-w-[256px]" />
|
||||||
|
) : (
|
||||||
|
<div className="w-64 h-64 flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[var(--primary)]"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info text */}
|
||||||
|
<p className="text-sm text-slate-400 text-center mb-6">
|
||||||
|
Отсканируйте QR код в приложении Umbrix для автоматического подключения
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<button
|
||||||
|
onClick={downloadQR}
|
||||||
|
disabled={!qrDataUrl}
|
||||||
|
className="flex items-center justify-center gap-2 px-4 py-3 bg-slate-800 hover:bg-slate-700 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg transition-colors text-white"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
<span>Скачать</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={shareQR}
|
||||||
|
disabled={!qrDataUrl}
|
||||||
|
className="flex items-center justify-center gap-2 px-4 py-3 bg-[var(--primary)] hover:bg-[var(--primary)]/80 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg transition-colors text-white"
|
||||||
|
>
|
||||||
|
<Share2 className="h-4 w-4" />
|
||||||
|
<span>Поделиться</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Copy URL button */}
|
||||||
|
<button
|
||||||
|
onClick={copyUrl}
|
||||||
|
className="w-full mt-3 flex items-center justify-center gap-2 px-4 py-3 bg-slate-800/50 hover:bg-slate-700/50 rounded-lg transition-colors text-slate-300 text-sm"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<>
|
||||||
|
<Check className="h-4 w-4 text-green-500" />
|
||||||
|
<span className="text-green-500">Скопировано!</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
<span>Скопировать ссылку</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
134
components/ReferralModal.tsx
Normal file
134
components/ReferralModal.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { X, Copy, Share2, Gift, Users, Award } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ReferralModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
referralUrl: string;
|
||||||
|
onShare: () => void;
|
||||||
|
onCopy: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReferralModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
referralUrl,
|
||||||
|
onShare,
|
||||||
|
onCopy
|
||||||
|
}: ReferralModalProps) {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-end sm:items-center justify-center"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<div
|
||||||
|
className="relative w-full max-w-lg bg-[var(--bg-card)] rounded-t-3xl sm:rounded-3xl shadow-2xl border border-slate-700 max-h-[90vh] overflow-y-auto"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="sticky top-0 bg-[var(--bg-card)] border-b border-slate-700 p-6 flex items-center justify-between z-10">
|
||||||
|
<h2 className="text-xl font-bold flex items-center gap-2">
|
||||||
|
<Gift className="w-6 h-6 text-[var(--primary)]" />
|
||||||
|
Пригласи друга
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 hover:bg-slate-700 rounded-full transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Условия программы */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-slate-800/50 border border-slate-700 rounded-xl p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Award className="w-6 h-6 flex-shrink-0 mt-1" style={{ color: 'var(--primary)' }} />
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold mb-2" style={{ color: 'var(--primary)' }}>Ты получишь:</h3>
|
||||||
|
<ul className="space-y-2 text-sm text-slate-300">
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="mt-0.5" style={{ color: 'var(--primary)' }}>✓</span>
|
||||||
|
<span><strong>+7 дней</strong> бесплатно за каждого друга</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="mt-0.5" style={{ color: 'var(--primary)' }}>✓</span>
|
||||||
|
<span><strong>10% скидка</strong> на продление при оплате</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="mt-0.5" style={{ color: 'var(--primary)' }}>✓</span>
|
||||||
|
<span>За 5 друзей → <strong>1 месяц в подарок</strong></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-800/50 border border-slate-700 rounded-xl p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Users className="w-6 h-6 flex-shrink-0 mt-1" style={{ color: 'var(--primary)' }} />
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold mb-2" style={{ color: 'var(--primary)' }}>Твой друг получит:</h3>
|
||||||
|
<ul className="space-y-2 text-sm text-slate-300">
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="mt-0.5" style={{ color: 'var(--primary)' }}>✓</span>
|
||||||
|
<span><strong>7 дней бесплатного</strong> триала</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="mt-0.5" style={{ color: 'var(--primary)' }}>✓</span>
|
||||||
|
<span><strong>-10% скидка</strong> на первую покупку</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Реферальная ссылка */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="text-sm text-slate-400">Твоя реферальная ссылка:</label>
|
||||||
|
<div className="flex items-center gap-2 p-3 bg-slate-800 rounded-lg border border-slate-700">
|
||||||
|
<code className="flex-1 text-sm text-[var(--primary)] truncate">
|
||||||
|
{referralUrl}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<button
|
||||||
|
onClick={onShare}
|
||||||
|
className="w-full flex items-center justify-center gap-3 px-6 py-4 rounded-xl font-semibold transition-all shadow-lg hover:opacity-90"
|
||||||
|
style={{ background: 'var(--primary)', color: 'white' }}
|
||||||
|
>
|
||||||
|
<Share2 className="w-5 h-5" />
|
||||||
|
Поделиться с друзьями
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onCopy}
|
||||||
|
className="w-full flex items-center justify-center gap-3 px-6 py-4 bg-slate-700 hover:bg-slate-600 rounded-xl font-semibold transition-all"
|
||||||
|
>
|
||||||
|
<Copy className="w-5 h-5" />
|
||||||
|
Скопировать ссылку
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<div className="text-xs text-slate-500 text-center">
|
||||||
|
Бонусы начисляются после активации подписки приглашенным другом
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user