most done
Browse files- components/HuggingFaceQRGenerator.tsx +189 -87
- styles/qr-generator.css +15 -13
components/HuggingFaceQRGenerator.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
'use client';
|
| 2 |
|
| 3 |
-
import React, { useState } from 'react';
|
| 4 |
import { parseHuggingFaceUrl } from '../lib/huggingface';
|
| 5 |
import QRCodeWithLogo from './QRCodeWithLogo';
|
| 6 |
import { saveAs } from 'file-saver';
|
|
@@ -11,7 +11,7 @@ import { Input } from '@/components/ui/input';
|
|
| 11 |
import { Label } from '@/components/ui/label';
|
| 12 |
import { Badge } from '@/components/ui/badge';
|
| 13 |
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
| 14 |
-
import { Download,
|
| 15 |
|
| 16 |
const HuggingFaceQRGenerator = () => {
|
| 17 |
const [inputUrl, setInputUrl] = useState('');
|
|
@@ -19,10 +19,20 @@ const HuggingFaceQRGenerator = () => {
|
|
| 19 |
const [loading, setLoading] = useState(false);
|
| 20 |
const [error, setError] = useState('');
|
| 21 |
const [qrCodeInstance, setQrCodeInstance] = useState<any>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
const handleGenerate = async () => {
|
| 24 |
setError('');
|
| 25 |
setLoading(true);
|
|
|
|
| 26 |
|
| 27 |
try {
|
| 28 |
// Parse the URL to extract username and resource info
|
|
@@ -56,26 +66,55 @@ const HuggingFaceQRGenerator = () => {
|
|
| 56 |
originalAvatarUrl: data.avatarUrl,
|
| 57 |
qrValue: parsed.profileUrl
|
| 58 |
});
|
|
|
|
| 59 |
} catch (err: any) {
|
| 60 |
setError(err.message || 'Invalid URL or username');
|
| 61 |
setProfileData(null);
|
|
|
|
| 62 |
} finally {
|
| 63 |
setLoading(false);
|
| 64 |
}
|
| 65 |
};
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
const handleDownload = async (format = 'png') => {
|
| 68 |
if (!profileData) return;
|
| 69 |
|
| 70 |
try {
|
| 71 |
-
const cardElement = document.getElementById('qr-card');
|
|
|
|
| 72 |
|
| 73 |
-
if (format === '
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
const dataUrl = await htmlToImage.toPng(cardElement!, {
|
| 76 |
quality: 1.0,
|
| 77 |
backgroundColor: '#ffffff',
|
| 78 |
pixelRatio: 2,
|
|
|
|
|
|
|
| 79 |
style: {
|
| 80 |
margin: '0',
|
| 81 |
borderRadius: '12px'
|
|
@@ -115,6 +154,34 @@ const HuggingFaceQRGenerator = () => {
|
|
| 115 |
}
|
| 116 |
};
|
| 117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
const getResourceIcon = (type: string) => {
|
| 119 |
switch(type) {
|
| 120 |
case 'model': return '🤖';
|
|
@@ -125,71 +192,102 @@ const HuggingFaceQRGenerator = () => {
|
|
| 125 |
};
|
| 126 |
|
| 127 |
const isValid = inputUrl.trim().length > 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
return (
|
| 130 |
-
<div className="min-h-screen
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
<
|
| 134 |
-
<
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
<
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
<
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
</div>
|
| 158 |
-
<CheckCircle2
|
| 159 |
-
className={`absolute right-3 top-1/2 -translate-y-1/2 ${isValid ? 'text-emerald-500' : 'text-muted-foreground/30'}`}
|
| 160 |
-
/>
|
| 161 |
-
</div>
|
| 162 |
-
</div>
|
| 163 |
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
| 171 |
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
<div className="qr-preview flex justify-center">
|
| 183 |
-
<div className="qr-phone-bg">
|
| 184 |
-
<div className="qr-topbar">
|
| 185 |
-
<button aria-label="Back"><ChevronLeft size={18} /></button>
|
| 186 |
-
<button aria-label="Menu"><Menu size={18} /></button>
|
| 187 |
-
</div>
|
| 188 |
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
<div className="qr-avatar-wrap">
|
| 191 |
<img
|
| 192 |
-
src={profileData.
|
| 193 |
alt={profileData.fullName}
|
| 194 |
className="qr-avatar"
|
| 195 |
crossOrigin="anonymous"
|
|
@@ -207,42 +305,46 @@ const HuggingFaceQRGenerator = () => {
|
|
| 207 |
dotsColor="#000000"
|
| 208 |
/>
|
| 209 |
</div>
|
| 210 |
-
<p className="qr-caption">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
<div className="qr-brand">
|
| 212 |
<img src="https://huggingface.co/front/assets/huggingface_logo.svg" alt="Hugging Face" />
|
| 213 |
<span>Hugging Face</span>
|
| 214 |
</div>
|
| 215 |
</div>
|
| 216 |
</div>
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
<div className="qr-
|
| 221 |
-
<
|
| 222 |
-
<
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
</div>
|
| 227 |
-
<div className="qr-share-
|
| 228 |
-
<span
|
| 229 |
-
<
|
| 230 |
-
|
| 231 |
-
<button className="qr-circle"><Mail size={18} /></button>
|
| 232 |
-
<button className="qr-circle"><MoreHorizontal size={18} /></button>
|
| 233 |
-
</div>
|
| 234 |
-
<div className="qr-share-texts">
|
| 235 |
-
<span>SMS</span>
|
| 236 |
-
<span>Email</span>
|
| 237 |
-
<span>Other</span>
|
| 238 |
-
</div>
|
| 239 |
</div>
|
| 240 |
-
<button className="qr-close" aria-label="Close">×</button>
|
| 241 |
</div>
|
| 242 |
</div>
|
| 243 |
</div>
|
| 244 |
-
|
| 245 |
-
|
| 246 |
</div>
|
| 247 |
);
|
| 248 |
};
|
|
|
|
| 1 |
'use client';
|
| 2 |
|
| 3 |
+
import React, { useRef, useState } from 'react';
|
| 4 |
import { parseHuggingFaceUrl } from '../lib/huggingface';
|
| 5 |
import QRCodeWithLogo from './QRCodeWithLogo';
|
| 6 |
import { saveAs } from 'file-saver';
|
|
|
|
| 11 |
import { Label } from '@/components/ui/label';
|
| 12 |
import { Badge } from '@/components/ui/badge';
|
| 13 |
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
| 14 |
+
import { Download, Twitter, Facebook, Linkedin, ChevronLeft } from 'lucide-react';
|
| 15 |
|
| 16 |
const HuggingFaceQRGenerator = () => {
|
| 17 |
const [inputUrl, setInputUrl] = useState('');
|
|
|
|
| 19 |
const [loading, setLoading] = useState(false);
|
| 20 |
const [error, setError] = useState('');
|
| 21 |
const [qrCodeInstance, setQrCodeInstance] = useState<any>(null);
|
| 22 |
+
const [showQR, setShowQR] = useState(false);
|
| 23 |
+
const [gradientIndex, setGradientIndex] = useState(0);
|
| 24 |
+
const gradients = [
|
| 25 |
+
['#f1c40f', '#f39c12'],
|
| 26 |
+
['#34d399', '#10b981'],
|
| 27 |
+
['#60a5fa', '#6366f1'],
|
| 28 |
+
['#fb7185', '#f472b6'],
|
| 29 |
+
['#f59e0b', '#ef4444']
|
| 30 |
+
];
|
| 31 |
|
| 32 |
const handleGenerate = async () => {
|
| 33 |
setError('');
|
| 34 |
setLoading(true);
|
| 35 |
+
setShowQR(false);
|
| 36 |
|
| 37 |
try {
|
| 38 |
// Parse the URL to extract username and resource info
|
|
|
|
| 66 |
originalAvatarUrl: data.avatarUrl,
|
| 67 |
qrValue: parsed.profileUrl
|
| 68 |
});
|
| 69 |
+
setShowQR(true);
|
| 70 |
} catch (err: any) {
|
| 71 |
setError(err.message || 'Invalid URL or username');
|
| 72 |
setProfileData(null);
|
| 73 |
+
setShowQR(false);
|
| 74 |
} finally {
|
| 75 |
setLoading(false);
|
| 76 |
}
|
| 77 |
};
|
| 78 |
|
| 79 |
+
const handleBack = () => {
|
| 80 |
+
setShowQR(false);
|
| 81 |
+
};
|
| 82 |
+
|
| 83 |
+
const handleCycleBackground = () => {
|
| 84 |
+
setGradientIndex((prev) => (prev + 1) % gradients.length);
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
const phoneRef = useRef<HTMLDivElement | null>(null);
|
| 88 |
+
const cardRef = useRef<HTMLDivElement | null>(null);
|
| 89 |
+
|
| 90 |
const handleDownload = async (format = 'png') => {
|
| 91 |
if (!profileData) return;
|
| 92 |
|
| 93 |
try {
|
| 94 |
+
const cardElement = cardRef.current || document.getElementById('qr-card');
|
| 95 |
+
const phoneElement = phoneRef.current;
|
| 96 |
|
| 97 |
+
if (format === 'full' && phoneElement) {
|
| 98 |
+
const rect = phoneElement.getBoundingClientRect();
|
| 99 |
+
const dataUrl = await htmlToImage.toPng(phoneElement, {
|
| 100 |
+
quality: 1.0,
|
| 101 |
+
pixelRatio: 2,
|
| 102 |
+
width: Math.ceil(rect.width),
|
| 103 |
+
height: Math.ceil(rect.height),
|
| 104 |
+
style: { margin: '0' }
|
| 105 |
+
});
|
| 106 |
+
const response = await fetch(dataUrl);
|
| 107 |
+
const blob = await response.blob();
|
| 108 |
+
saveAs(blob, `huggingface-${profileData.username}-phone.png`);
|
| 109 |
+
} else if (format === 'png' || format === 'card') {
|
| 110 |
+
// Download just the inner card
|
| 111 |
+
const rect = cardElement!.getBoundingClientRect();
|
| 112 |
const dataUrl = await htmlToImage.toPng(cardElement!, {
|
| 113 |
quality: 1.0,
|
| 114 |
backgroundColor: '#ffffff',
|
| 115 |
pixelRatio: 2,
|
| 116 |
+
width: Math.ceil(rect.width),
|
| 117 |
+
height: Math.ceil(rect.height),
|
| 118 |
style: {
|
| 119 |
margin: '0',
|
| 120 |
borderRadius: '12px'
|
|
|
|
| 154 |
}
|
| 155 |
};
|
| 156 |
|
| 157 |
+
const handleShareSms = () => {
|
| 158 |
+
const text = encodeURIComponent(`Check out my Hugging Face profile: ${profileData?.qrValue || ''}`);
|
| 159 |
+
window.open(`sms:?&body=${text}`, '_blank');
|
| 160 |
+
};
|
| 161 |
+
|
| 162 |
+
const handleShareEmail = () => {
|
| 163 |
+
const subject = encodeURIComponent('My Hugging Face Profile');
|
| 164 |
+
const body = encodeURIComponent(`Hey! Check out my Hugging Face profile: ${profileData?.qrValue || ''}`);
|
| 165 |
+
window.open(`mailto:?subject=${subject}&body=${body}`, '_self');
|
| 166 |
+
};
|
| 167 |
+
|
| 168 |
+
const handleShareNative = async () => {
|
| 169 |
+
try {
|
| 170 |
+
if (navigator.share) {
|
| 171 |
+
await navigator.share({
|
| 172 |
+
title: 'Hugging Face Profile',
|
| 173 |
+
text: 'Check out my Hugging Face profile!',
|
| 174 |
+
url: profileData?.qrValue || ''
|
| 175 |
+
});
|
| 176 |
+
} else {
|
| 177 |
+
await navigator.clipboard.writeText(profileData?.qrValue || '');
|
| 178 |
+
alert('Link copied to clipboard');
|
| 179 |
+
}
|
| 180 |
+
} catch (e) {
|
| 181 |
+
// ignore cancel
|
| 182 |
+
}
|
| 183 |
+
};
|
| 184 |
+
|
| 185 |
const getResourceIcon = (type: string) => {
|
| 186 |
switch(type) {
|
| 187 |
case 'model': return '🤖';
|
|
|
|
| 192 |
};
|
| 193 |
|
| 194 |
const isValid = inputUrl.trim().length > 0
|
| 195 |
+
|
| 196 |
+
// Validate HuggingFace username format
|
| 197 |
+
const validateInput = (input: string): boolean => {
|
| 198 |
+
if (!input.trim()) return true; // empty is neutral
|
| 199 |
+
// Check if it's a valid username (alphanumeric, hyphens, underscores)
|
| 200 |
+
// or a valid HuggingFace URL
|
| 201 |
+
const usernamePattern = /^[a-zA-Z0-9_-]+$/;
|
| 202 |
+
const urlPattern = /huggingface\.co/i;
|
| 203 |
+
|
| 204 |
+
return usernamePattern.test(input) || urlPattern.test(input);
|
| 205 |
+
};
|
| 206 |
+
|
| 207 |
+
const inputIsInvalid = inputUrl.trim().length > 0 && !validateInput(inputUrl);
|
| 208 |
|
| 209 |
return (
|
| 210 |
+
<div className="min-h-screen bg-linear-to-br from-purple-50 via-pink-50 to-blue-50 dark:from-gray-900 dark:via-purple-900 dark:to-gray-900">
|
| 211 |
+
{/* Input form - hidden when QR is shown */}
|
| 212 |
+
{!showQR && (
|
| 213 |
+
<div className="min-h-screen grid place-items-center p-6 md:p-10">
|
| 214 |
+
<div className="w-full max-w-2xl mx-auto">
|
| 215 |
+
{/* Input card */}
|
| 216 |
+
<Card className="shadow-xl">
|
| 217 |
+
<CardHeader className="pb-4">
|
| 218 |
+
<div className="flex items-center gap-2">
|
| 219 |
+
<span className="text-3xl">🤗</span>
|
| 220 |
+
<div>
|
| 221 |
+
<CardTitle className="text-2xl font-bold tracking-tight">Hugging Face</CardTitle>
|
| 222 |
+
<CardDescription className="mt-1 text-muted-foreground">Generate a clean QR code for any Hugging Face profile or resource.</CardDescription>
|
| 223 |
+
</div>
|
| 224 |
+
</div>
|
| 225 |
+
</CardHeader>
|
| 226 |
+
<CardContent className="p-6 md:p-8 space-y-7">
|
| 227 |
+
<div className="space-y-3">
|
| 228 |
+
<Label className="text-xs tracking-wider font-medium">HUGGING FACE USERNAME</Label>
|
| 229 |
+
<div className="relative">
|
| 230 |
+
<div className="flex items-stretch overflow-hidden rounded-md border">
|
| 231 |
+
<span className="hidden sm:inline-flex items-center px-3 text-sm text-muted-foreground bg-secondary/40 select-none">
|
| 232 |
+
https://huggingface.co/
|
| 233 |
+
</span>
|
| 234 |
+
<Input
|
| 235 |
+
type="text"
|
| 236 |
+
value={inputUrl}
|
| 237 |
+
onChange={(e) => setInputUrl(e.target.value)}
|
| 238 |
+
placeholder="username or full URL"
|
| 239 |
+
className="h-11 border-0 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
| 240 |
+
onKeyPress={(e) => e.key === 'Enter' && handleGenerate()}
|
| 241 |
+
disabled={loading}
|
| 242 |
+
aria-invalid={inputIsInvalid}
|
| 243 |
+
aria-describedby="hf-input-help"
|
| 244 |
+
/>
|
| 245 |
+
</div>
|
| 246 |
+
</div>
|
| 247 |
+
<p id="hf-input-help" className="text-xs text-muted-foreground">Paste a full URL or just the username, e.g. <span className="font-mono">reubencf</span>.</p>
|
| 248 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
|
| 250 |
+
<Button
|
| 251 |
+
onClick={handleGenerate}
|
| 252 |
+
disabled={!inputUrl || loading}
|
| 253 |
+
size="lg"
|
| 254 |
+
className="rounded-md w-full sm:w-auto"
|
| 255 |
+
>
|
| 256 |
+
{loading ? 'Generating…' : 'Generate QR Code'}
|
| 257 |
+
</Button>
|
| 258 |
|
| 259 |
+
{error && (
|
| 260 |
+
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 px-4 py-2 rounded-lg text-sm">
|
| 261 |
+
{error}
|
| 262 |
+
</div>
|
| 263 |
+
)}
|
| 264 |
+
</CardContent>
|
| 265 |
+
</Card>
|
| 266 |
+
</div>
|
| 267 |
+
</div>
|
| 268 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
|
| 270 |
+
{/* Full screen QR preview - shown after successful generation */}
|
| 271 |
+
{showQR && profileData && (
|
| 272 |
+
<div
|
| 273 |
+
className="fixed inset-0 bg-linear-to-br from-purple-50 via-pink-50 to-blue-50 dark:from-gray-900 dark:via-purple-900 dark:to-gray-900 z-50 p-4 md:p-6 overflow-y-auto"
|
| 274 |
+
style={{ background: `linear-gradient(135deg, ${gradients[gradientIndex][0]}, ${gradients[gradientIndex][1]})` }}
|
| 275 |
+
>
|
| 276 |
+
{/* Back button moved outside of the phone so it won't appear in exports */}
|
| 277 |
+
<div className="qr-topbar">
|
| 278 |
+
<button onClick={handleBack} aria-label="Back"><ChevronLeft size={18} /></button>
|
| 279 |
+
</div>
|
| 280 |
+
<div className="qr-preview mx-auto flex justify-center py-6">
|
| 281 |
+
<div
|
| 282 |
+
className="qr-phone-bg"
|
| 283 |
+
ref={phoneRef}
|
| 284 |
+
style={{ background: `linear-gradient(135deg, ${gradients[gradientIndex][0]}, ${gradients[gradientIndex][1]})` }}
|
| 285 |
+
onClick={handleCycleBackground}
|
| 286 |
+
>
|
| 287 |
+
<div className="qr-card-v2" id="qr-card" ref={cardRef} onClick={(e) => e.stopPropagation()}>
|
| 288 |
<div className="qr-avatar-wrap">
|
| 289 |
<img
|
| 290 |
+
src={profileData.avatarUrl}
|
| 291 |
alt={profileData.fullName}
|
| 292 |
className="qr-avatar"
|
| 293 |
crossOrigin="anonymous"
|
|
|
|
| 305 |
dotsColor="#000000"
|
| 306 |
/>
|
| 307 |
</div>
|
| 308 |
+
<p className="qr-caption">{(() => {
|
| 309 |
+
const t = profileData?.resourceType || profileData?.type;
|
| 310 |
+
if (t === 'model') return 'Scan to open this model on Hugging Face';
|
| 311 |
+
if (t === 'dataset') return 'Scan to open this dataset on Hugging Face';
|
| 312 |
+
if (t === 'space') return 'Scan to open this Space on Hugging Face';
|
| 313 |
+
if (profileData?.fullName) return `Scan to open ${profileData.fullName} on Hugging Face`;
|
| 314 |
+
return 'Scan to open on Hugging Face';
|
| 315 |
+
})()}</p>
|
| 316 |
<div className="qr-brand">
|
| 317 |
<img src="https://huggingface.co/front/assets/huggingface_logo.svg" alt="Hugging Face" />
|
| 318 |
<span>Hugging Face</span>
|
| 319 |
</div>
|
| 320 |
</div>
|
| 321 |
</div>
|
| 322 |
+
</div>
|
| 323 |
+
{/* Share sheet placed below the phone (not part of exported element) */}
|
| 324 |
+
<div className="qr-share-sheet" onClick={(e) => e.stopPropagation()}>
|
| 325 |
+
<div className="qr-download">
|
| 326 |
+
<button onClick={() => handleDownload('full')} className="qr-circle">
|
| 327 |
+
<Download size={18} />
|
| 328 |
+
</button>
|
| 329 |
+
<span>Download</span>
|
| 330 |
+
</div>
|
| 331 |
+
<div className="qr-share-group">
|
| 332 |
+
<span className="qr-share-label">Share to</span>
|
| 333 |
+
<div className="qr-share-actions">
|
| 334 |
+
<button className="qr-circle" onClick={() => handleShare('linkedin')} aria-label="Share on LinkedIn"><Linkedin size={18} /></button>
|
| 335 |
+
<button className="qr-circle" onClick={() => handleShare('facebook')} aria-label="Share on Facebook"><Facebook size={18} /></button>
|
| 336 |
+
<button className="qr-circle" onClick={() => handleShare('twitter')} aria-label="Share on X (Twitter)"><Twitter size={18} /></button>
|
| 337 |
</div>
|
| 338 |
+
<div className="qr-share-texts">
|
| 339 |
+
<span>LinkedIn</span>
|
| 340 |
+
<span>Facebook</span>
|
| 341 |
+
<span>X</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
</div>
|
|
|
|
| 343 |
</div>
|
| 344 |
</div>
|
| 345 |
</div>
|
| 346 |
+
</div>
|
| 347 |
+
)}
|
| 348 |
</div>
|
| 349 |
);
|
| 350 |
};
|
styles/qr-generator.css
CHANGED
|
@@ -487,14 +487,15 @@
|
|
| 487 |
.hf-cta:disabled { opacity: .6; cursor: not-allowed; }
|
| 488 |
|
| 489 |
/* Download card layout (screenshot 2) */
|
| 490 |
-
.qr-preview { display:flex; justify-content:center; margin: 1.5rem 0 0.5rem; }
|
| 491 |
.qr-phone-bg {
|
| 492 |
position: relative;
|
| 493 |
width: 380px;
|
| 494 |
max-width: 100%;
|
|
|
|
| 495 |
border-radius: 32px;
|
| 496 |
background: linear-gradient(135deg,#f1c40f,#f39c12);
|
| 497 |
-
padding:
|
| 498 |
box-shadow: 0 12px 28px rgba(0,0,0,.15);
|
| 499 |
}
|
| 500 |
|
|
@@ -528,7 +529,7 @@
|
|
| 528 |
.qr-bg-help { text-align:center; color:#fef3c7; font-size: .78rem; margin-top: 10px; }
|
| 529 |
|
| 530 |
@media (max-width: 480px) {
|
| 531 |
-
.qr-phone-bg { padding:
|
| 532 |
.qr-card-v2 { padding: 52px 14px 18px; border-radius: 20px; }
|
| 533 |
}
|
| 534 |
|
|
@@ -547,19 +548,19 @@
|
|
| 547 |
|
| 548 |
/* Bottom share sheet */
|
| 549 |
.qr-share-sheet {
|
| 550 |
-
position:
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
background: #fff;
|
| 555 |
-
border-
|
| 556 |
-
border-top-right-radius: 18px;
|
| 557 |
box-shadow: 0 -8px 24px rgba(0,0,0,.22);
|
| 558 |
display: grid;
|
| 559 |
grid-template-columns: 110px 1fr;
|
| 560 |
align-items: center;
|
| 561 |
gap: 8px 12px;
|
| 562 |
padding: 10px 12px 12px;
|
|
|
|
| 563 |
}
|
| 564 |
|
| 565 |
.qr-share-sheet .qr-close {
|
|
@@ -577,8 +578,9 @@
|
|
| 577 |
.qr-circle {
|
| 578 |
width: 36px; height: 36px; border-radius: 9999px; display:flex; align-items:center; justify-content:center;
|
| 579 |
background: #f3f4f6; border: 1px solid #e5e7eb; color:#374151;
|
|
|
|
| 580 |
}
|
| 581 |
-
.qr-share-group { display:
|
| 582 |
-
.qr-share-label { font-size: 12px; color:#6b7280; margin-left:
|
| 583 |
-
.qr-share-actions { display:
|
| 584 |
-
.qr-share-texts { display:
|
|
|
|
| 487 |
.hf-cta:disabled { opacity: .6; cursor: not-allowed; }
|
| 488 |
|
| 489 |
/* Download card layout (screenshot 2) */
|
| 490 |
+
.qr-preview { display:flex; flex-direction: column; align-items: center; justify-content:center; margin: 1.5rem 0 0.5rem; }
|
| 491 |
.qr-phone-bg {
|
| 492 |
position: relative;
|
| 493 |
width: 380px;
|
| 494 |
max-width: 100%;
|
| 495 |
+
height: auto;
|
| 496 |
border-radius: 32px;
|
| 497 |
background: linear-gradient(135deg,#f1c40f,#f39c12);
|
| 498 |
+
padding: 40px 18px 18px;
|
| 499 |
box-shadow: 0 12px 28px rgba(0,0,0,.15);
|
| 500 |
}
|
| 501 |
|
|
|
|
| 529 |
.qr-bg-help { text-align:center; color:#fef3c7; font-size: .78rem; margin-top: 10px; }
|
| 530 |
|
| 531 |
@media (max-width: 480px) {
|
| 532 |
+
.qr-phone-bg { padding: 44px 14px 16px; border-radius: 28px; height: auto; }
|
| 533 |
.qr-card-v2 { padding: 52px 14px 18px; border-radius: 20px; }
|
| 534 |
}
|
| 535 |
|
|
|
|
| 548 |
|
| 549 |
/* Bottom share sheet */
|
| 550 |
.qr-share-sheet {
|
| 551 |
+
position: relative;
|
| 552 |
+
width: 380px;
|
| 553 |
+
max-width: 100%;
|
| 554 |
+
margin: 10px auto 0;
|
| 555 |
background: #fff;
|
| 556 |
+
border-radius: 18px;
|
|
|
|
| 557 |
box-shadow: 0 -8px 24px rgba(0,0,0,.22);
|
| 558 |
display: grid;
|
| 559 |
grid-template-columns: 110px 1fr;
|
| 560 |
align-items: center;
|
| 561 |
gap: 8px 12px;
|
| 562 |
padding: 10px 12px 12px;
|
| 563 |
+
font-family: var(--font-inter), var(--font-geist-sans), system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
| 564 |
}
|
| 565 |
|
| 566 |
.qr-share-sheet .qr-close {
|
|
|
|
| 578 |
.qr-circle {
|
| 579 |
width: 36px; height: 36px; border-radius: 9999px; display:flex; align-items:center; justify-content:center;
|
| 580 |
background: #f3f4f6; border: 1px solid #e5e7eb; color:#374151;
|
| 581 |
+
cursor: pointer;
|
| 582 |
}
|
| 583 |
+
.qr-share-group { display:grid; grid-template-rows: auto auto auto; gap: 6px; justify-items: center; }
|
| 584 |
+
.qr-share-label { font-size: 12px; color:#6b7280; margin-left: 0; text-align: center; letter-spacing: .02em; }
|
| 585 |
+
.qr-share-actions { display:grid; grid-template-columns: repeat(3, 1fr); gap: 14px; align-items:center; justify-items: center; }
|
| 586 |
+
.qr-share-texts { display:grid; grid-template-columns: repeat(3, 1fr); gap: 12px; color:#6b7280; font-size: 12px; text-align: center; }
|