Reubencf commited on
Commit
fa4d7a4
·
1 Parent(s): 31d0d62
app/globals.css CHANGED
@@ -49,7 +49,7 @@
49
  }
50
 
51
  body {
52
- font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif;
53
  }
54
 
55
  :root {
 
49
  }
50
 
51
  body {
52
+ font-family: var(--font-poppins), var(--font-inter), Arial, Helvetica, sans-serif;
53
  }
54
 
55
  :root {
app/layout.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import type { Metadata } from "next";
2
- import { Geist, Geist_Mono, Inter } from "next/font/google";
3
  import "./globals.css";
4
  import "../styles/qr-generator.css";
5
 
@@ -18,9 +18,19 @@ const inter = Inter({
18
  subsets: ["latin"],
19
  });
20
 
 
 
 
 
 
 
21
  export const metadata: Metadata = {
22
- title: "Hugging Face QR Code Generator",
23
- description: "Generate beautiful QR codes for Hugging Face profiles with embedded avatars",
 
 
 
 
24
  };
25
 
26
  export default function RootLayout({
@@ -31,7 +41,7 @@ export default function RootLayout({
31
  return (
32
  <html lang="en">
33
  <body
34
- className={`${geistSans.variable} ${geistMono.variable} ${inter.variable} antialiased`}
35
  >
36
  {children}
37
  </body>
 
1
  import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono, Inter, Poppins } from "next/font/google";
3
  import "./globals.css";
4
  import "../styles/qr-generator.css";
5
 
 
18
  subsets: ["latin"],
19
  });
20
 
21
+ const poppins = Poppins({
22
+ variable: "--font-poppins",
23
+ subsets: ["latin"],
24
+ weight: ["400", "600", "700"],
25
+ });
26
+
27
  export const metadata: Metadata = {
28
+ title: "HF QR Generator",
29
+ description: "Generate clean and beautiful QR codes for Hugging Face user profiles, models, datasets, and spaces with custom avatars",
30
+ icons: {
31
+ icon: "/hf-logo.svg",
32
+ apple: "/hf-logo.svg",
33
+ },
34
  };
35
 
36
  export default function RootLayout({
 
41
  return (
42
  <html lang="en">
43
  <body
44
+ className={`${geistSans.variable} ${geistMono.variable} ${inter.variable} ${poppins.variable} antialiased`}
45
  >
46
  {children}
47
  </body>
components/HuggingFaceQRGenerator.tsx CHANGED
@@ -11,7 +11,9 @@ 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, Twitter, Facebook, Linkedin, ChevronLeft } from 'lucide-react';
 
 
15
 
16
  const HuggingFaceQRGenerator = () => {
17
  const [inputUrl, setInputUrl] = useState('');
@@ -182,6 +184,40 @@ const HuggingFaceQRGenerator = () => {
182
  }
183
  };
184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  const getResourceIcon = (type: string) => {
186
  switch(type) {
187
  case 'model': return '🤖';
@@ -210,51 +246,53 @@ const HuggingFaceQRGenerator = () => {
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">
@@ -322,23 +360,29 @@ const HuggingFaceQRGenerator = () => {
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>
 
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, ChevronLeft, Copy } from 'lucide-react';
15
+ import { FaFacebook, FaLinkedin } from 'react-icons/fa';
16
+ import { FaSquareXTwitter } from 'react-icons/fa6';
17
 
18
  const HuggingFaceQRGenerator = () => {
19
  const [inputUrl, setInputUrl] = useState('');
 
184
  }
185
  };
186
 
187
+ const handleCopyImage = async () => {
188
+ if (!profileData) return;
189
+
190
+ try {
191
+ const phoneElement = phoneRef.current;
192
+ if (!phoneElement) return;
193
+
194
+ const rect = phoneElement.getBoundingClientRect();
195
+ const dataUrl = await htmlToImage.toPng(phoneElement, {
196
+ quality: 1.0,
197
+ pixelRatio: 2,
198
+ width: Math.ceil(rect.width),
199
+ height: Math.ceil(rect.height),
200
+ style: { margin: '0' }
201
+ });
202
+
203
+ // Convert data URL to blob
204
+ const response = await fetch(dataUrl);
205
+ const blob = await response.blob();
206
+
207
+ // Copy to clipboard
208
+ await navigator.clipboard.write([
209
+ new ClipboardItem({
210
+ 'image/png': blob
211
+ })
212
+ ]);
213
+
214
+ alert('Image copied to clipboard!');
215
+ } catch (err) {
216
+ console.error('Copy error:', err);
217
+ alert('Failed to copy image. Your browser may not support this feature.');
218
+ }
219
+ };
220
+
221
  const getResourceIcon = (type: string) => {
222
  switch(type) {
223
  case 'model': return '🤖';
 
246
  <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">
247
  {/* Input form - hidden when QR is shown */}
248
  {!showQR && (
249
+ <div className="min-h-screen grid place-items-center p-6 md:p-10 bg-white/80">
250
  <div className="w-full max-w-2xl mx-auto">
251
  {/* Input card */}
252
+ <Card className="shadow-xl" style={{ padding: 15, fontFamily: 'var(--font-inter)', boxShadow: '0 10px 40px rgba(0, 0, 0, 0.1), 0 2px 8px rgba(0, 0, 0, 0.06)' }}>
253
  <CardHeader className="pb-4">
254
+ <div className="flex flex-col gap-3">
255
+ <img src="/logo.svg" alt="Hugging Face" className="h-7 w-auto" />
256
+ <CardDescription className="text-muted-foreground" style={{ fontFamily: 'var(--font-inter)' }}>Generate a clean QR code for any Hugging Face profile or resource.</CardDescription>
 
 
 
257
  </div>
258
  </CardHeader>
259
  <CardContent className="p-6 md:p-8 space-y-7">
260
  <div className="space-y-3">
261
+ <Label className="text-xs tracking-wider font-medium" style={{ fontFamily: 'var(--font-inter)' }}>HUGGING FACE USERNAME</Label>
262
  <div className="relative">
263
  <div className="flex items-stretch overflow-hidden rounded-md border">
264
+ <span className="hidden sm:inline-flex items-center text-sm text-muted-foreground select-none" style={{ paddingLeft: '8px', paddingRight: '5px', backgroundColor: '#f5f5f5', fontFamily: 'var(--font-inter)' }}>
265
  https://huggingface.co/
266
  </span>
267
  <Input
268
  type="text"
269
  value={inputUrl}
270
  onChange={(e) => setInputUrl(e.target.value)}
271
+ placeholder="Reubencf"
272
+ className="h-12 border-0 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0"
273
+ style={{ paddingLeft: '3px', paddingRight: '12px', fontFamily: 'var(--font-inter)' }}
274
  onKeyPress={(e) => e.key === 'Enter' && handleGenerate()}
275
  disabled={loading}
276
  aria-invalid={inputIsInvalid}
277
  aria-describedby="hf-input-help"
278
+ autoFocus
279
  />
280
  </div>
281
  </div>
282
+ <p id="hf-input-help" className="text-xs text-muted-foreground" style={{ paddingTop: '5px', paddingBottom: '4px', fontFamily: 'var(--font-inter)' }}>Paste a full URL or just the username, e.g. <span className="font-mono">reubencf</span>.</p>
283
  </div>
284
 
285
+ <div className="pt-2 flex justify-end">
286
+ <Button
287
+ onClick={handleGenerate}
288
+ disabled={!inputUrl || loading}
289
+ className="rounded-md h-auto bg-neutral-900 text-white hover:bg-neutral-800 disabled:bg-neutral-700 disabled:opacity-100"
290
+ style={{ padding: '12px' }}
291
+ aria-label="Generate QR Code"
292
+ >
293
+ {loading ? 'Generating…' : 'Generate QR Code'}
294
+ </Button>
295
+ </div>
296
 
297
  {error && (
298
  <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">
 
360
  </div>
361
  {/* Share sheet placed below the phone (not part of exported element) */}
362
  <div className="qr-share-sheet" onClick={(e) => e.stopPropagation()}>
363
+ <div className="qr-download-group">
364
+ <span className="qr-share-label">Actions</span>
365
+ <div className="qr-download-actions">
366
+ <div className="qr-download-item">
367
+ <button onClick={() => handleDownload('full')} className="qr-circle" aria-label="Download">
368
+ <Download size={18} />
369
+ </button>
370
+ <span className="qr-action-text">Download</span>
371
+ </div>
372
+ <div className="qr-download-item">
373
+ <button onClick={handleCopyImage} className="qr-circle" aria-label="Copy">
374
+ <Copy size={18} />
375
+ </button>
376
+ <span className="qr-action-text">Copy</span>
377
+ </div>
378
+ </div>
379
  </div>
380
  <div className="qr-share-group">
381
  <span className="qr-share-label">Share to</span>
382
  <div className="qr-share-actions">
383
+ <button className="qr-circle" onClick={() => handleShare('linkedin')} aria-label="Share on LinkedIn"><FaLinkedin size={20} /></button>
384
+ <button className="qr-circle" onClick={() => handleShare('facebook')} aria-label="Share on Facebook"><FaFacebook size={20} /></button>
385
+ <button className="qr-circle" onClick={() => handleShare('twitter')} aria-label="Share on X (Twitter)"><FaSquareXTwitter size={20} /></button>
 
 
 
 
 
386
  </div>
387
  </div>
388
  </div>
package-lock.json CHANGED
@@ -21,6 +21,7 @@
21
  "qrcode-generator": "^2.0.4",
22
  "react": "19.2.0",
23
  "react-dom": "19.2.0",
 
24
  "react-qr-code": "^2.0.18",
25
  "tailwind-merge": "^3.3.1"
26
  },
@@ -34,7 +35,7 @@
34
  "eslint-config-next": "16.0.1",
35
  "tailwindcss": "^4",
36
  "tw-animate-css": "^1.4.0",
37
- "typescript": "^5"
38
  }
39
  },
40
  "node_modules/@alloc/quick-lru": {
@@ -5748,6 +5749,15 @@
5748
  "react": "^19.2.0"
5749
  }
5750
  },
 
 
 
 
 
 
 
 
 
5751
  "node_modules/react-is": {
5752
  "version": "16.13.1",
5753
  "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
 
21
  "qrcode-generator": "^2.0.4",
22
  "react": "19.2.0",
23
  "react-dom": "19.2.0",
24
+ "react-icons": "^5.5.0",
25
  "react-qr-code": "^2.0.18",
26
  "tailwind-merge": "^3.3.1"
27
  },
 
35
  "eslint-config-next": "16.0.1",
36
  "tailwindcss": "^4",
37
  "tw-animate-css": "^1.4.0",
38
+ "typescript": "5.9.3"
39
  }
40
  },
41
  "node_modules/@alloc/quick-lru": {
 
5749
  "react": "^19.2.0"
5750
  }
5751
  },
5752
+ "node_modules/react-icons": {
5753
+ "version": "5.5.0",
5754
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
5755
+ "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
5756
+ "license": "MIT",
5757
+ "peerDependencies": {
5758
+ "react": "*"
5759
+ }
5760
+ },
5761
  "node_modules/react-is": {
5762
  "version": "16.13.1",
5763
  "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
package.json CHANGED
@@ -22,6 +22,7 @@
22
  "qrcode-generator": "^2.0.4",
23
  "react": "19.2.0",
24
  "react-dom": "19.2.0",
 
25
  "react-qr-code": "^2.0.18",
26
  "tailwind-merge": "^3.3.1"
27
  },
@@ -35,6 +36,6 @@
35
  "eslint-config-next": "16.0.1",
36
  "tailwindcss": "^4",
37
  "tw-animate-css": "^1.4.0",
38
- "typescript": "^5"
39
  }
40
  }
 
22
  "qrcode-generator": "^2.0.4",
23
  "react": "19.2.0",
24
  "react-dom": "19.2.0",
25
+ "react-icons": "^5.5.0",
26
  "react-qr-code": "^2.0.18",
27
  "tailwind-merge": "^3.3.1"
28
  },
 
36
  "eslint-config-next": "16.0.1",
37
  "tailwindcss": "^4",
38
  "tw-animate-css": "^1.4.0",
39
+ "typescript": "5.9.3"
40
  }
41
  }
public/file.svg DELETED
public/globe.svg DELETED
public/hf-logo.svg ADDED
public/logo.svg ADDED
public/next.svg DELETED
public/vercel.svg DELETED
public/window.svg DELETED
styles/qr-generator.css CHANGED
@@ -556,10 +556,10 @@
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
 
@@ -574,13 +574,15 @@
574
  font-size: 18px;
575
  }
576
 
577
- .qr-download { display:flex; flex-direction: column; align-items: center; gap: 6px; }
 
 
 
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; }
 
556
  border-radius: 18px;
557
  box-shadow: 0 -8px 24px rgba(0,0,0,.22);
558
  display: grid;
559
+ grid-template-columns: 1fr 1fr;
560
+ align-items: start;
561
+ gap: 12px 16px;
562
+ padding: 12px 16px 14px;
563
  font-family: var(--font-inter), var(--font-geist-sans), system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
564
  }
565
 
 
574
  font-size: 18px;
575
  }
576
 
577
+ .qr-download-group { display:flex; flex-direction: column; gap: 8px; }
578
+ .qr-download-actions { display:flex; gap: 12px; }
579
+ .qr-download-item { display:flex; flex-direction: column; align-items: center; gap: 6px; }
580
+ .qr-action-text { font-size: 11px; color:#6b7280; }
581
  .qr-circle {
582
  width: 36px; height: 36px; border-radius: 9999px; display:flex; align-items:center; justify-content:center;
583
  background: #f3f4f6; border: 1px solid #e5e7eb; color:#374151;
584
  cursor: pointer;
585
  }
586
+ .qr-share-group { display:flex; flex-direction: column; gap: 8px; }
587
+ .qr-share-label { font-size: 12px; color:#6b7280; margin-left: 0; text-align: left; letter-spacing: .02em; font-weight: 600; }
588
+ .qr-share-actions { display:flex; gap: 14px; }