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"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.28.0"
|
"react-router-dom": "^6.28.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import QRCode from 'qrcode';
|
||||||
import { api, type BusinessCard } from '../api/client';
|
import { api, type BusinessCard } from '../api/client';
|
||||||
|
|
||||||
export function CardPage() {
|
export function CardPage() {
|
||||||
const { slug } = useParams<{ slug: string }>();
|
const { slug } = useParams<{ slug: string }>();
|
||||||
const [card, setCard] = useState<BusinessCard | null>(null);
|
const [card, setCard] = useState<BusinessCard | null>(null);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
const [qrOpen, setQrOpen] = useState(false);
|
||||||
const cardRef = useRef<HTMLElement>(null);
|
const cardRef = useRef<HTMLElement>(null);
|
||||||
const glowRef = useRef<HTMLDivElement>(null);
|
const glowRef = useRef<HTMLDivElement>(null);
|
||||||
|
const qrCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!slug) return;
|
if (!slug) return;
|
||||||
@@ -71,6 +74,15 @@ export function CardPage() {
|
|||||||
toast('Открываю карточку контакта…');
|
toast('Открываю карточку контакта…');
|
||||||
} catch { 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) {
|
function toast(msg: string) {
|
||||||
const t = document.getElementById('cp-toast');
|
const t = document.getElementById('cp-toast');
|
||||||
if (!t) return;
|
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>
|
<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>
|
||||||
)}
|
)}
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<button className="save" onClick={saveContact}>
|
<button className="save" onClick={saveContact}>
|
||||||
@@ -145,6 +160,16 @@ export function CardPage() {
|
|||||||
|
|
||||||
<div className="toast" id="cp-toast">Контакт сохранён ✓</div>
|
<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>
|
<style>{cardCss}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -202,4 +227,9 @@ const cardCss = `
|
|||||||
.cp-card .foot b{color:var(--ink);font-weight:600}
|
.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{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)}
|
.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