import { useEffect, useRef, useState } from 'react'; import { useParams } from 'react-router-dom'; import { api, type BusinessCard } from '../api/client'; export function CardPage() { const { slug } = useParams<{ slug: string }>(); const [card, setCard] = useState(null); const [error, setError] = useState(false); const cardRef = useRef(null); const glowRef = useRef(null); useEffect(() => { if (!slug) return; api.getCard(slug).then(({ card }) => setCard(card)).catch(() => setError(true)); }, [slug]); useEffect(() => { if (!card) return; if (!window.matchMedia('(hover:hover)').matches) return; const el = cardRef.current, glow = glowRef.current; function onMove(e: MouseEvent) { if (!el) return; const r = el.getBoundingClientRect(); const dx = (e.clientX - (r.left + r.width / 2)) / r.width; const dy = (e.clientY - (r.top + r.height / 2)) / r.height; el.style.transform = `rotateY(${(dx * 5).toFixed(2)}deg) rotateX(${(-dy * 5).toFixed(2)}deg)`; if (glow) { glow.style.left = e.clientX + 'px'; glow.style.top = e.clientY + 'px'; } } function onLeave() { if (el) el.style.transform = ''; } document.addEventListener('mousemove', onMove); document.addEventListener('mouseleave', onLeave); return () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseleave', onLeave); }; }, [card]); if (error) return
Визитка не найдена
; if (!card) return
загрузка…
; const initials = card.name.split(/\s+/).map((w) => w[0]).slice(0, 2).join('').toUpperCase(); const telHref = card.phone ? 'tel:' + card.phone.replace(/[^+\d]/g, '') : ''; function saveContact() { if (!card) return; const parts = card.name.split(/\s+/); const lines = ['BEGIN:VCARD', 'VERSION:3.0', `N:${parts[1] || ''};${parts[0] || ''};;;`, `FN:${card.name}`]; if (card.company) lines.push(`ORG:${card.company}`); if (card.role) lines.push(`TITLE:${card.role.replace(/·/g, ',')}`); if (card.phone) lines.push(`TEL;TYPE=CELL,VOICE:${card.phone.replace(/[^+\d]/g, '')}`); if (card.email) lines.push(`EMAIL;TYPE=INTERNET:${card.email}`); if (card.product_url) lines.push(`URL:${card.product_url}`); if (card.site_url) lines.push(`URL:${card.site_url}`); if (card.photo && card.photo.startsWith('data:image')) { const b64 = card.photo.split(',')[1]; const type = (card.photo.match(/data:image\/(\w+)/) || [])[1]?.toUpperCase() || 'JPEG'; if (b64 && b64.length < 60000) lines.push(`PHOTO;ENCODING=b;TYPE=${type}:${b64}`); } lines.push('END:VCARD'); const vcard = lines.join('\r\n'); try { const isIOS = /iP(hone|ad|od)/.test(navigator.userAgent); if (isIOS) { window.location.href = 'data:text/vcard;charset=utf-8,' + encodeURIComponent(vcard); } else { const blob = new Blob([vcard], { type: 'text/vcard;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${card.slug}.vcf`; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); } toast('Открываю карточку контакта…'); } catch { toast('Не удалось открыть'); } } function toast(msg: string) { const t = document.getElementById('cp-toast'); if (!t) return; t.textContent = msg; t.classList.add('on'); setTimeout(() => t.classList.remove('on'), 2200); } return (
{card.photo ? {card.name} : {initials}}

{card.name}

{card.role &&
{card.role}
} {card.company && (
{card.company}
)}
{card.phone && ( )} {card.email && ( )} {card.telegram && ( )} {card.site_url && ( )}
{card.product_title && ( {card.product_title}{card.product_subtitle && {card.product_subtitle}} )}
Умный Байт · umbyte.ru
Контакт сохранён ✓
); } const cardCss = ` @import url('https://fonts.googleapis.com/css2?family=Unbounded:wght@400;500;600;700&family=Onest:wght@400;500;600&display=swap'); .cardpage{--bg:#E9EDF7;--bg2:#DDE3F2;--card:#FFFFFF;--card2:#FBFCFF;--ink:#15132E;--ink2:#5C5984;--line:#E7E9F4;--indigo:#312E81;--indigo2:#4F4BC4;--cyan:#06B6D4;--cyan-glow:#22D3EE;--violet:#7C5CFF;--shadow:0 30px 70px -24px rgba(49,46,129,.32),0 10px 30px -18px rgba(6,182,212,.28); font-family:'Onest',sans-serif;color:var(--ink);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:30px 18px;position:relative;overflow:hidden;perspective:1200px;background:linear-gradient(165deg,var(--bg),var(--bg2))} @media (prefers-color-scheme:dark){.cardpage{--bg:#0C0B22;--bg2:#141233;--card:#17163a;--card2:#1c1a44;--ink:#F4F4FF;--ink2:#A6A3D9;--line:rgba(255,255,255,.12);--indigo:#4F4BC4;--indigo2:#6A66E0;--cyan:#22D3EE;--cyan-glow:#67E8F9;--shadow:0 30px 80px -24px rgba(0,0,0,.7),0 0 40px -10px rgba(34,211,238,.25)}} .cardpage-msg{min-height:100vh;display:flex;align-items:center;justify-content:center;color:#94a3b8;font-family:'Onest',sans-serif;background:#E9EDF7} @media (prefers-color-scheme:dark){.cardpage-msg{background:#0C0B22}} .cardpage *{margin:0;padding:0;box-sizing:border-box} .cardpage .blob{position:fixed;border-radius:50%;filter:blur(70px);opacity:.6;z-index:0;pointer-events:none;will-change:transform} .cardpage .b1{width:380px;height:380px;background:radial-gradient(circle,rgba(34,211,238,.5),transparent 70%);top:-80px;left:-60px;animation:cpf1 14s ease-in-out infinite} .cardpage .b2{width:420px;height:420px;background:radial-gradient(circle,rgba(124,92,255,.4),transparent 70%);bottom:-120px;right:-80px;animation:cpf2 18s ease-in-out infinite} .cardpage .b3{width:300px;height:300px;background:radial-gradient(circle,rgba(79,75,196,.4),transparent 70%);top:40%;left:60%;animation:cpf3 16s ease-in-out infinite} @keyframes cpf1{0%,100%{transform:translate(0,0) scale(1)}50%{transform:translate(60px,40px) scale(1.12)}} @keyframes cpf2{0%,100%{transform:translate(0,0) scale(1)}50%{transform:translate(-50px,-40px) scale(1.1)}} @keyframes cpf3{0%,100%{transform:translate(0,0) scale(1)}50%{transform:translate(-40px,50px) scale(1.15)}} .cardpage .glow{position:fixed;width:300px;height:300px;border-radius:50%;pointer-events:none;z-index:1;background:radial-gradient(circle,rgba(34,211,238,.18),transparent 65%);transform:translate(-50%,-50%);left:-999px;top:-999px;mix-blend-mode:multiply} @media (prefers-color-scheme:dark){.cardpage .glow{mix-blend-mode:screen}} .cp-card{position:relative;z-index:2;width:100%;max-width:404px;background:linear-gradient(180deg,var(--card),var(--card2));border-radius:30px;padding:38px 28px 26px;box-shadow:var(--shadow);display:flex;flex-direction:column;align-items:center;text-align:center;transform-style:preserve-3d;transition:transform .15s ease-out;will-change:transform} .cp-card::before{content:"";position:absolute;inset:0;border-radius:30px;padding:1.5px;pointer-events:none;background:linear-gradient(135deg,var(--cyan-glow),var(--violet) 35%,var(--indigo2) 60%,transparent 80%);background-size:200% 200%;animation:cpholo 6s linear infinite;-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude} @keyframes cpholo{0%{background-position:0% 50%}100%{background-position:200% 50%}} .cardpage .reveal{opacity:0;transform:translateY(14px);animation:cprise .7s cubic-bezier(.2,.7,.2,1) forwards} @keyframes cprise{to{opacity:1;transform:none}} .cardpage .d1{animation-delay:.05s}.cardpage .d2{animation-delay:.14s}.cardpage .d3{animation-delay:.22s}.cardpage .d4{animation-delay:.32s}.cardpage .d5{animation-delay:.42s}.cardpage .d6{animation-delay:.52s} .cp-avatar{width:112px;height:112px;border-radius:50%;position:relative;margin-bottom:22px;display:flex;align-items:center;justify-content:center} .cp-avatar .ring{position:absolute;inset:0;border-radius:50%;background:conic-gradient(from 0deg,var(--cyan-glow),var(--violet),var(--indigo),var(--cyan-glow));animation:cpspin 7s linear infinite} @keyframes cpspin{to{transform:rotate(360deg)}} .cp-avatar .inner{position:absolute;inset:5px;border-radius:50%;background:linear-gradient(180deg,var(--card),var(--card2));display:flex;align-items:center;justify-content:center;overflow:hidden;z-index:1} .cp-avatar .inner img{width:100%;height:100%;object-fit:cover} .cp-avatar .inner span{font-family:'Unbounded';font-weight:600;font-size:38px;letter-spacing:-1px;background:linear-gradient(150deg,var(--indigo2),var(--indigo));-webkit-background-clip:text;background-clip:text;color:transparent} @media (prefers-color-scheme:dark){.cp-avatar .inner span{background:linear-gradient(150deg,#fff,#cfe9ff);-webkit-background-clip:text;background-clip:text;color:transparent}} .cp-card h1{font-family:'Unbounded';font-weight:600;font-size:26px;letter-spacing:-.6px;line-height:1.1} .cp-card .role{color:var(--cyan);font-weight:600;font-size:14.5px;margin-top:9px} @media (prefers-color-scheme:dark){.cp-card .role{color:var(--cyan-glow)}} .cp-card .company{color:var(--ink2);font-size:13px;margin-top:7px;display:flex;align-items:center;gap:7px;justify-content:center} .cp-card .actions{display:flex;gap:12px;margin:26px 0 6px} .cp-card .act{width:56px;height:56px;border-radius:18px;border:1px solid var(--line);background:var(--card);display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--indigo);box-shadow:0 6px 16px -8px rgba(49,46,129,.4);transition:transform .18s,box-shadow .18s,color .18s,border-color .18s;-webkit-tap-highlight-color:transparent} @media (prefers-color-scheme:dark){.cp-card .act{color:var(--cyan-glow)}} .cp-card .act:hover{transform:translateY(-4px);color:var(--cyan);border-color:rgba(6,182,212,.5);box-shadow:0 12px 22px -10px rgba(6,182,212,.5)} .cp-card .act svg{width:23px;height:23px} .cp-card .save{margin-top:18px;width:100%;border:none;cursor:pointer;padding:17px 20px;border-radius:18px;font-family:'Onest';font-weight:600;font-size:16px;color:#fff;background:linear-gradient(110deg,var(--indigo),var(--violet) 35%,var(--cyan) 70%,var(--indigo));background-size:220% 100%;opacity:0;animation:cpfade .7s ease .42s forwards,cphb 5s linear infinite;box-shadow:0 16px 34px -12px rgba(49,46,129,.6);display:flex;align-items:center;justify-content:center;gap:10px;transition:transform .18s,box-shadow .18s} @keyframes cphb{0%{background-position:0 0}100%{background-position:220% 0}} .cp-card .save:hover{transform:translateY(-2px);box-shadow:0 20px 42px -12px rgba(124,92,255,.6)} .cp-card .product{margin-top:13px;width:100%;text-decoration:none;color:var(--ink);border:1px solid var(--line);background:var(--card);border-radius:18px;padding:14px 16px;display:flex;align-items:center;gap:13px;text-align:left;transition:transform .18s,border-color .18s,box-shadow .18s} .cp-card .product:hover{transform:translateY(-2px);border-color:rgba(6,182,212,.5);box-shadow:0 12px 24px -14px rgba(6,182,212,.5)} .cp-card .product .ic{width:42px;height:42px;border-radius:12px;background:linear-gradient(145deg,var(--indigo2),var(--indigo));display:flex;align-items:center;justify-content:center;flex-shrink:0} .cp-card .product .t{font-weight:600;font-size:15px;display:block} .cp-card .product .s{color:var(--ink2);font-size:12px;margin-top:2px;display:block} .cp-card .product .go{margin-left:auto;color:var(--cyan)} .cp-card .foot{margin-top:24px;display:flex;align-items:center;gap:8px;color:var(--ink2);font-size:12px} .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)} `;