card: кнопка QR-кода визитки (локальная генерация, без внешних сервисов)
This commit is contained in:
Generated
+3056
File diff suppressed because it is too large
Load Diff
@@ -9,11 +9,13 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.28.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import QRCode from 'qrcode';
|
||||
import { api, type BusinessCard } from '../api/client';
|
||||
|
||||
export function CardPage() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const [card, setCard] = useState<BusinessCard | null>(null);
|
||||
const [error, setError] = useState(false);
|
||||
const [qrOpen, setQrOpen] = useState(false);
|
||||
const cardRef = useRef<HTMLElement>(null);
|
||||
const glowRef = useRef<HTMLDivElement>(null);
|
||||
const qrCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!slug) return;
|
||||
@@ -71,6 +74,15 @@ export function CardPage() {
|
||||
toast('Открываю карточку контакта…');
|
||||
} catch { toast('Не удалось открыть'); }
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!qrOpen || !qrCanvasRef.current) return;
|
||||
QRCode.toCanvas(qrCanvasRef.current, window.location.href, {
|
||||
width: 240,
|
||||
margin: 1,
|
||||
color: { dark: '#15132E', light: '#FFFFFF' },
|
||||
}).catch(() => toast('Не удалось построить QR'));
|
||||
}, [qrOpen]);
|
||||
|
||||
function toast(msg: string) {
|
||||
const t = document.getElementById('cp-toast');
|
||||
if (!t) return;
|
||||
@@ -122,6 +134,9 @@ export function CardPage() {
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg>
|
||||
</button>
|
||||
)}
|
||||
<button className="act" title="Показать QR" onClick={() => setQrOpen(true)}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7" rx="1" /><rect x="14" y="3" width="7" height="7" rx="1" /><rect x="3" y="14" width="7" height="7" rx="1" /><path d="M14 14h3v3h-3zM19 14h2v2h-2zM14 19h2v2h-2zM19 19h2v2h-2z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button className="save" onClick={saveContact}>
|
||||
@@ -145,6 +160,16 @@ export function CardPage() {
|
||||
|
||||
<div className="toast" id="cp-toast">Контакт сохранён ✓</div>
|
||||
|
||||
{qrOpen && (
|
||||
<div className="qr-overlay" onClick={() => setQrOpen(false)}>
|
||||
<div className="qr-box" onClick={(e) => e.stopPropagation()}>
|
||||
<canvas ref={qrCanvasRef} />
|
||||
<div className="qr-hint">Отсканируйте, чтобы открыть визитку</div>
|
||||
<button className="qr-close" onClick={() => setQrOpen(false)}>Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style>{cardCss}</style>
|
||||
</div>
|
||||
);
|
||||
@@ -202,4 +227,9 @@ const cardCss = `
|
||||
.cp-card .foot b{color:var(--ink);font-weight:600}
|
||||
.cardpage .toast{position:fixed;left:50%;bottom:26px;transform:translateX(-50%) translateY(20px);background:var(--ink);color:var(--bg);padding:11px 18px;border-radius:12px;font-size:14px;opacity:0;pointer-events:none;transition:all .3s;z-index:6;box-shadow:0 12px 30px -8px rgba(0,0,0,.4)}
|
||||
.cardpage .toast.on{opacity:1;transform:translateX(-50%) translateY(0)}
|
||||
.qr-overlay{position:fixed;inset:0;z-index:10;background:rgba(21,19,46,.55);backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:20px;animation:cprise .2s ease-out}
|
||||
.qr-box{background:var(--card);border-radius:24px;padding:28px 26px 22px;display:flex;flex-direction:column;align-items:center;gap:14px;box-shadow:var(--shadow);max-width:300px}
|
||||
.qr-box canvas{border-radius:14px;display:block}
|
||||
.qr-hint{font-size:13px;color:var(--ink2);text-align:center}
|
||||
.qr-close{border:none;cursor:pointer;padding:12px 22px;border-radius:14px;font-family:'Onest';font-weight:600;font-size:14px;color:#fff;background:linear-gradient(110deg,var(--indigo),var(--violet) 60%,var(--cyan));width:100%}
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user