Reubencf commited on
Commit
5de7420
·
1 Parent(s): bc774a3

Qr code working

Browse files
.claude/settings.local.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm install:*)",
5
+ "Bash(curl:*)",
6
+ "Bash(node -e:*)",
7
+ "Bash(npx shadcn@latest init -y)",
8
+ "Bash(npx shadcn@latest init:*)",
9
+ "Bash(npx shadcn@latest add:*)",
10
+ "Bash(npm run build:*)"
11
+ ],
12
+ "deny": [],
13
+ "ask": []
14
+ }
15
+ }
app/api/huggingface/route.js ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+ import axios from 'axios';
3
+
4
+ export async function POST(request) {
5
+ try {
6
+ const { username, resourceType, resourceName } = await request.json();
7
+
8
+ if (!username) {
9
+ return NextResponse.json(
10
+ { error: 'Username is required' },
11
+ { status: 400 }
12
+ );
13
+ }
14
+
15
+ try {
16
+ // Fetch the profile page HTML to extract the real avatar URL
17
+ const response = await axios.get(
18
+ `https://huggingface.co/${username}`,
19
+ {
20
+ headers: {
21
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
22
+ }
23
+ }
24
+ );
25
+
26
+ const html = response.data;
27
+
28
+ // Extract the CDN avatar URL from the HTML
29
+ // Look for pattern: https://cdn-avatars.huggingface.co/...
30
+ const avatarMatch = html.match(/https:\/\/cdn-avatars\.huggingface\.co\/[^"'\s]+\.(png|jpg|jpeg|webp)/);
31
+
32
+ let avatarUrl = null;
33
+ if (avatarMatch && avatarMatch[0]) {
34
+ avatarUrl = avatarMatch[0];
35
+ }
36
+
37
+ // If no CDN avatar found, try to find any avatar image
38
+ if (!avatarUrl) {
39
+ // Try to find organization/user avatar in meta tags or other places
40
+ const metaAvatarMatch = html.match(/<meta[^>]+property="og:image"[^>]+content="([^"]+)"/);
41
+ if (metaAvatarMatch && metaAvatarMatch[1]) {
42
+ avatarUrl = metaAvatarMatch[1];
43
+ }
44
+ }
45
+
46
+ // Fallback to a default avatar if none found
47
+ if (!avatarUrl) {
48
+ // Use Hugging Face's default avatar placeholder
49
+ avatarUrl = `https://huggingface.co/front/assets/huggingface_logo-noborder.svg`;
50
+ }
51
+
52
+ // Extract full name if available from the page
53
+ let fullName = username;
54
+ const nameMatch = html.match(/<h1[^>]*>([^<]+)<\/h1>/);
55
+ if (nameMatch && nameMatch[1]) {
56
+ fullName = nameMatch[1].trim();
57
+ }
58
+
59
+ return NextResponse.json({
60
+ username: username,
61
+ fullName: fullName,
62
+ avatarUrl: avatarUrl,
63
+ profileUrl: `https://huggingface.co/${username}`,
64
+ type: 'user',
65
+ resourceType: resourceType || null,
66
+ resourceName: resourceName || null
67
+ });
68
+
69
+ } catch (fetchError) {
70
+ console.log('Error fetching profile page:', fetchError.message);
71
+
72
+ // Fallback response with default avatar
73
+ return NextResponse.json({
74
+ username: username,
75
+ fullName: username,
76
+ avatarUrl: `https://huggingface.co/front/assets/huggingface_logo-noborder.svg`,
77
+ profileUrl: `https://huggingface.co/${username}`,
78
+ type: 'user',
79
+ resourceType: resourceType || null,
80
+ resourceName: resourceName || null
81
+ });
82
+ }
83
+
84
+ } catch (error) {
85
+ console.error('Error in HuggingFace API route:', error);
86
+ return NextResponse.json(
87
+ { error: 'Failed to fetch profile data' },
88
+ { status: 500 }
89
+ );
90
+ }
91
+ }
app/api/proxy-image/route.js ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+ import axios from 'axios';
3
+
4
+ export async function GET(request) {
5
+ try {
6
+ const { searchParams } = new URL(request.url);
7
+ const imageUrl = searchParams.get('url');
8
+
9
+ if (!imageUrl) {
10
+ return NextResponse.json(
11
+ { error: 'Image URL is required' },
12
+ { status: 400 }
13
+ );
14
+ }
15
+
16
+ // Fetch the image
17
+ const response = await axios.get(imageUrl, {
18
+ responseType: 'arraybuffer',
19
+ headers: {
20
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
21
+ }
22
+ });
23
+
24
+ // Get content type from response
25
+ const contentType = response.headers['content-type'] || 'image/png';
26
+
27
+ // Return the image with proper headers
28
+ return new NextResponse(response.data, {
29
+ status: 200,
30
+ headers: {
31
+ 'Content-Type': contentType,
32
+ 'Cache-Control': 'public, max-age=3600',
33
+ 'Access-Control-Allow-Origin': '*'
34
+ }
35
+ });
36
+
37
+ } catch (error) {
38
+ console.error('Error proxying image:', error);
39
+ return NextResponse.json(
40
+ { error: 'Failed to fetch image' },
41
+ { status: 500 }
42
+ );
43
+ }
44
+ }
app/globals.css CHANGED
@@ -1,26 +1,131 @@
1
  @import "tailwindcss";
2
 
3
- :root {
4
- --background: #ffffff;
5
- --foreground: #171717;
6
- }
7
 
8
  @theme inline {
9
  --color-background: var(--background);
10
  --color-foreground: var(--foreground);
11
  --font-sans: var(--font-geist-sans);
12
  --font-mono: var(--font-geist-mono);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  }
14
 
15
- @media (prefers-color-scheme: dark) {
16
- :root {
17
- --background: #0a0a0a;
18
- --foreground: #ededed;
19
- }
20
  }
21
 
22
  body {
23
- background: var(--background);
24
- color: var(--foreground);
25
- font-family: Arial, Helvetica, sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  }
 
1
  @import "tailwindcss";
2
 
3
+ @custom-variant dark (&:is(.dark *));
 
 
 
4
 
5
  @theme inline {
6
  --color-background: var(--background);
7
  --color-foreground: var(--foreground);
8
  --font-sans: var(--font-geist-sans);
9
  --font-mono: var(--font-geist-mono);
10
+ --color-sidebar-ring: var(--sidebar-ring);
11
+ --color-sidebar-border: var(--sidebar-border);
12
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
13
+ --color-sidebar-accent: var(--sidebar-accent);
14
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
15
+ --color-sidebar-primary: var(--sidebar-primary);
16
+ --color-sidebar-foreground: var(--sidebar-foreground);
17
+ --color-sidebar: var(--sidebar);
18
+ --color-chart-5: var(--chart-5);
19
+ --color-chart-4: var(--chart-4);
20
+ --color-chart-3: var(--chart-3);
21
+ --color-chart-2: var(--chart-2);
22
+ --color-chart-1: var(--chart-1);
23
+ --color-ring: var(--ring);
24
+ --color-input: var(--input);
25
+ --color-border: var(--border);
26
+ --color-destructive: var(--destructive);
27
+ --color-accent-foreground: var(--accent-foreground);
28
+ --color-accent: var(--accent);
29
+ --color-muted-foreground: var(--muted-foreground);
30
+ --color-muted: var(--muted);
31
+ --color-secondary-foreground: var(--secondary-foreground);
32
+ --color-secondary: var(--secondary);
33
+ --color-primary-foreground: var(--primary-foreground);
34
+ --color-primary: var(--primary);
35
+ --color-popover-foreground: var(--popover-foreground);
36
+ --color-popover: var(--popover);
37
+ --color-card-foreground: var(--card-foreground);
38
+ --color-card: var(--card);
39
+ --radius-sm: calc(var(--radius) - 4px);
40
+ --radius-md: calc(var(--radius) - 2px);
41
+ --radius-lg: var(--radius);
42
+ --radius-xl: calc(var(--radius) + 4px);
43
  }
44
 
45
+ * {
46
+ margin: 0;
47
+ padding: 0;
48
+ box-sizing: border-box;
 
49
  }
50
 
51
  body {
52
+ font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif;
53
+ }
54
+
55
+ :root {
56
+ --radius: 0.625rem;
57
+ --background: oklch(1 0 0);
58
+ --foreground: oklch(0.145 0 0);
59
+ --card: oklch(1 0 0);
60
+ --card-foreground: oklch(0.145 0 0);
61
+ --popover: oklch(1 0 0);
62
+ --popover-foreground: oklch(0.145 0 0);
63
+ --primary: oklch(0.205 0 0);
64
+ --primary-foreground: oklch(0.985 0 0);
65
+ --secondary: oklch(0.97 0 0);
66
+ --secondary-foreground: oklch(0.205 0 0);
67
+ --muted: oklch(0.97 0 0);
68
+ --muted-foreground: oklch(0.556 0 0);
69
+ --accent: oklch(0.97 0 0);
70
+ --accent-foreground: oklch(0.205 0 0);
71
+ --destructive: oklch(0.577 0.245 27.325);
72
+ --border: oklch(0.922 0 0);
73
+ --input: oklch(0.922 0 0);
74
+ --ring: oklch(0.708 0 0);
75
+ --chart-1: oklch(0.646 0.222 41.116);
76
+ --chart-2: oklch(0.6 0.118 184.704);
77
+ --chart-3: oklch(0.398 0.07 227.392);
78
+ --chart-4: oklch(0.828 0.189 84.429);
79
+ --chart-5: oklch(0.769 0.188 70.08);
80
+ --sidebar: oklch(0.985 0 0);
81
+ --sidebar-foreground: oklch(0.145 0 0);
82
+ --sidebar-primary: oklch(0.205 0 0);
83
+ --sidebar-primary-foreground: oklch(0.985 0 0);
84
+ --sidebar-accent: oklch(0.97 0 0);
85
+ --sidebar-accent-foreground: oklch(0.205 0 0);
86
+ --sidebar-border: oklch(0.922 0 0);
87
+ --sidebar-ring: oklch(0.708 0 0);
88
+ }
89
+
90
+ .dark {
91
+ --background: oklch(0.145 0 0);
92
+ --foreground: oklch(0.985 0 0);
93
+ --card: oklch(0.205 0 0);
94
+ --card-foreground: oklch(0.985 0 0);
95
+ --popover: oklch(0.205 0 0);
96
+ --popover-foreground: oklch(0.985 0 0);
97
+ --primary: oklch(0.922 0 0);
98
+ --primary-foreground: oklch(0.205 0 0);
99
+ --secondary: oklch(0.269 0 0);
100
+ --secondary-foreground: oklch(0.985 0 0);
101
+ --muted: oklch(0.269 0 0);
102
+ --muted-foreground: oklch(0.708 0 0);
103
+ --accent: oklch(0.269 0 0);
104
+ --accent-foreground: oklch(0.985 0 0);
105
+ --destructive: oklch(0.704 0.191 22.216);
106
+ --border: oklch(1 0 0 / 10%);
107
+ --input: oklch(1 0 0 / 15%);
108
+ --ring: oklch(0.556 0 0);
109
+ --chart-1: oklch(0.488 0.243 264.376);
110
+ --chart-2: oklch(0.696 0.17 162.48);
111
+ --chart-3: oklch(0.769 0.188 70.08);
112
+ --chart-4: oklch(0.627 0.265 303.9);
113
+ --chart-5: oklch(0.645 0.246 16.439);
114
+ --sidebar: oklch(0.205 0 0);
115
+ --sidebar-foreground: oklch(0.985 0 0);
116
+ --sidebar-primary: oklch(0.488 0.243 264.376);
117
+ --sidebar-primary-foreground: oklch(0.985 0 0);
118
+ --sidebar-accent: oklch(0.269 0 0);
119
+ --sidebar-accent-foreground: oklch(0.985 0 0);
120
+ --sidebar-border: oklch(1 0 0 / 10%);
121
+ --sidebar-ring: oklch(0.556 0 0);
122
+ }
123
+
124
+ @layer base {
125
+ * {
126
+ @apply border-border outline-ring/50;
127
+ }
128
+ body {
129
+ @apply bg-background text-foreground;
130
+ }
131
  }
app/layout.tsx CHANGED
@@ -1,6 +1,7 @@
1
  import type { Metadata } from "next";
2
- import { Geist, Geist_Mono } from "next/font/google";
3
  import "./globals.css";
 
4
 
5
  const geistSans = Geist({
6
  variable: "--font-geist-sans",
@@ -12,9 +13,14 @@ const geistMono = Geist_Mono({
12
  subsets: ["latin"],
13
  });
14
 
 
 
 
 
 
15
  export const metadata: Metadata = {
16
- title: "Create Next App",
17
- description: "Generated by create next app",
18
  };
19
 
20
  export default function RootLayout({
@@ -25,7 +31,7 @@ export default function RootLayout({
25
  return (
26
  <html lang="en">
27
  <body
28
- className={`${geistSans.variable} ${geistMono.variable} antialiased`}
29
  >
30
  {children}
31
  </body>
 
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
 
6
  const geistSans = Geist({
7
  variable: "--font-geist-sans",
 
13
  subsets: ["latin"],
14
  });
15
 
16
+ const inter = Inter({
17
+ variable: "--font-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
  return (
32
  <html lang="en">
33
  <body
34
+ className={`${geistSans.variable} ${geistMono.variable} ${inter.variable} antialiased`}
35
  >
36
  {children}
37
  </body>
app/page.tsx CHANGED
@@ -1,65 +1,5 @@
1
- import Image from "next/image";
2
 
3
  export default function Home() {
4
- return (
5
- <div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
6
- <main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
7
- <Image
8
- className="dark:invert"
9
- src="/next.svg"
10
- alt="Next.js logo"
11
- width={100}
12
- height={20}
13
- priority
14
- />
15
- <div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
16
- <h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
17
- To get started, edit the page.tsx file.
18
- </h1>
19
- <p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
20
- Looking for a starting point or more instructions? Head over to{" "}
21
- <a
22
- href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
23
- className="font-medium text-zinc-950 dark:text-zinc-50"
24
- >
25
- Templates
26
- </a>{" "}
27
- or the{" "}
28
- <a
29
- href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
30
- className="font-medium text-zinc-950 dark:text-zinc-50"
31
- >
32
- Learning
33
- </a>{" "}
34
- center.
35
- </p>
36
- </div>
37
- <div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
38
- <a
39
- className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
40
- href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
41
- target="_blank"
42
- rel="noopener noreferrer"
43
- >
44
- <Image
45
- className="dark:invert"
46
- src="/vercel.svg"
47
- alt="Vercel logomark"
48
- width={16}
49
- height={16}
50
- />
51
- Deploy Now
52
- </a>
53
- <a
54
- className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
55
- href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
56
- target="_blank"
57
- rel="noopener noreferrer"
58
- >
59
- Documentation
60
- </a>
61
- </div>
62
- </main>
63
- </div>
64
- );
65
- }
 
1
+ import HuggingFaceQRGenerator from '../components/HuggingFaceQRGenerator.tsx';
2
 
3
  export default function Home() {
4
+ return <HuggingFaceQRGenerator />;
5
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "aliases": {
15
+ "components": "@/components",
16
+ "utils": "@/lib/utils",
17
+ "ui": "@/components/ui",
18
+ "lib": "@/lib",
19
+ "hooks": "@/hooks"
20
+ },
21
+ "registries": {}
22
+ }
components/HuggingFaceQRGenerator.js ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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';
7
+ import * as htmlToImage from 'html-to-image';
8
+
9
+ const HuggingFaceQRGenerator = () => {
10
+ const [inputUrl, setInputUrl] = useState('');
11
+ const [profileData, setProfileData] = useState(null);
12
+ const [loading, setLoading] = useState(false);
13
+ const [error, setError] = useState('');
14
+ const [qrCodeInstance, setQrCodeInstance] = useState(null);
15
+
16
+ const handleGenerate = async () => {
17
+ setError('');
18
+ setLoading(true);
19
+
20
+ try {
21
+ // Parse the URL to extract username and resource info
22
+ const parsed = parseHuggingFaceUrl(inputUrl);
23
+
24
+ // Fetch profile data from our API
25
+ const response = await fetch('/api/huggingface', {
26
+ method: 'POST',
27
+ headers: {
28
+ 'Content-Type': 'application/json',
29
+ },
30
+ body: JSON.stringify({
31
+ username: parsed.username,
32
+ resourceType: parsed.resourceType,
33
+ resourceName: parsed.resourceName
34
+ }),
35
+ });
36
+
37
+ if (!response.ok) {
38
+ throw new Error('Failed to fetch profile data');
39
+ }
40
+
41
+ const data = await response.json();
42
+
43
+ // Use proxy for the avatar to avoid CORS issues
44
+ const proxiedAvatarUrl = `/api/proxy-image?url=${encodeURIComponent(data.avatarUrl)}`;
45
+
46
+ setProfileData({
47
+ ...data,
48
+ avatarUrl: proxiedAvatarUrl,
49
+ originalAvatarUrl: data.avatarUrl,
50
+ qrValue: parsed.profileUrl
51
+ });
52
+ } catch (err) {
53
+ setError(err.message || 'Invalid URL or username');
54
+ setProfileData(null);
55
+ } finally {
56
+ setLoading(false);
57
+ }
58
+ };
59
+
60
+ const handleDownload = async (format = 'png') => {
61
+ if (!profileData) return;
62
+
63
+ try {
64
+ const cardElement = document.getElementById('qr-card');
65
+
66
+ if (format === 'png' || format === 'card') {
67
+ // Download the entire card with user info
68
+ const dataUrl = await htmlToImage.toPng(cardElement, {
69
+ quality: 1.0,
70
+ backgroundColor: '#ffffff',
71
+ pixelRatio: 2, // Higher quality
72
+ style: {
73
+ margin: '0',
74
+ padding: '20px',
75
+ borderRadius: '16px'
76
+ }
77
+ });
78
+
79
+ // Convert dataURL to blob
80
+ const response = await fetch(dataUrl);
81
+ const blob = await response.blob();
82
+ saveAs(blob, `huggingface-${profileData.username}-card.png`);
83
+
84
+ } else if (format === 'qr-only' && qrCodeInstance) {
85
+ // Download just the QR code
86
+ const blob = await qrCodeInstance.download({
87
+ name: `huggingface-${profileData.username}`,
88
+ extension: 'png'
89
+ });
90
+ saveAs(blob, `huggingface-${profileData.username}-qr.png`);
91
+
92
+ } else if (format === 'svg' && qrCodeInstance) {
93
+ // Download QR as SVG
94
+ const blob = await qrCodeInstance.download({
95
+ name: `huggingface-${profileData.username}`,
96
+ extension: 'svg'
97
+ });
98
+ saveAs(blob, `huggingface-${profileData.username}.svg`);
99
+ }
100
+ } catch (err) {
101
+ console.error('Download error:', err);
102
+ }
103
+ };
104
+
105
+ const handleShare = (platform) => {
106
+ const shareText = `Check out my Hugging Face profile!`;
107
+ const shareUrl = profileData?.qrValue || '';
108
+
109
+ const shareLinks = {
110
+ twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(shareUrl)}`,
111
+ facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`,
112
+ linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`
113
+ };
114
+
115
+ if (shareLinks[platform]) {
116
+ window.open(shareLinks[platform], '_blank', 'width=600,height=400');
117
+ }
118
+ };
119
+
120
+ const isValid = inputUrl.trim().length > 0;
121
+
122
+ return (
123
+ <div className="qr-generator">
124
+ <div className="hf-card">
125
+ <div className="hf-header">
126
+ <div className="hf-emoji">🤗</div>
127
+ <div className="hf-title-wrap">
128
+ <h1 className="hf-title">Hugging Face</h1>
129
+ <p className="hf-subtitle">Roast your favorite Hugging Face user!</p>
130
+ </div>
131
+ </div>
132
+
133
+ <div className="hf-form">
134
+ <div className="hf-field">
135
+ <label className="hf-label">HUGGING FACE USERNAME</label>
136
+ <div className="hf-input-wrap">
137
+ <input
138
+ type="text"
139
+ value={inputUrl}
140
+ onChange={(e) => setInputUrl(e.target.value)}
141
+ placeholder="https://huggingface.co/"
142
+ className="hf-input"
143
+ onKeyPress={(e) => e.key === 'Enter' && handleGenerate()}
144
+ />
145
+ <span className={`hf-valid ${isValid ? 'active' : ''}`} aria-hidden="true">✓</span>
146
+ </div>
147
+ </div>
148
+
149
+ <div className="hf-field">
150
+ <label className="hf-label">LANGUAGE</label>
151
+ <select className="hf-select" defaultValue="en">
152
+ <option value="en">English</option>
153
+ <option value="es">Spanish</option>
154
+ <option value="fr">French</option>
155
+ </select>
156
+ </div>
157
+
158
+ <button
159
+ onClick={handleGenerate}
160
+ disabled={!inputUrl || loading}
161
+ className="hf-cta"
162
+ >
163
+ {loading ? 'Roasting in progress...' : 'Roast'}
164
+ </button>
165
+
166
+ {error && (
167
+ <div className="error-message" role="alert">{error}</div>
168
+ )}
169
+ </div>
170
+ </div>
171
+
172
+ {profileData && (
173
+ <div className="result-section">
174
+ <div className="profile-info">
175
+ <img
176
+ src={profileData.avatarUrl}
177
+ alt={profileData.fullName}
178
+ className="profile-avatar"
179
+ crossOrigin="anonymous"
180
+ />
181
+ <div className="profile-details">
182
+ <h2>{profileData.fullName}</h2>
183
+ <p className="profile-username">@{profileData.username}</p>
184
+ <a
185
+ href={profileData.profileUrl}
186
+ target="_blank"
187
+ rel="noopener noreferrer"
188
+ className="profile-link"
189
+ >
190
+ View Profile →
191
+ </a>
192
+ </div>
193
+ </div>
194
+
195
+ <div className="qr-preview">
196
+ <div className="qr-phone-bg">
197
+ <div className="qr-card-v2" id="qr-card">
198
+ <div className="qr-avatar-wrap">
199
+ <img
200
+ src={profileData.originalAvatarUrl || profileData.avatarUrl}
201
+ alt={profileData.fullName}
202
+ className="qr-avatar"
203
+ crossOrigin="anonymous"
204
+ />
205
+ </div>
206
+ <div className="qr-card-inner">
207
+ <div className="qr-name">{profileData.fullName}</div>
208
+ <div className="qr-code-holder">
209
+ <QRCodeWithLogo
210
+ value={profileData.qrValue}
211
+ logoUrl="https://huggingface.co/front/assets/huggingface_logo.svg"
212
+ size={260}
213
+ onQRCodeReady={setQrCodeInstance}
214
+ backgroundColor="#FFFFFF"
215
+ dotsColor="#000000"
216
+ />
217
+ </div>
218
+ <p className="qr-caption">Share your QR code so others can follow you</p>
219
+ <div className="qr-brand">
220
+ <img src="https://huggingface.co/front/assets/huggingface_logo.svg" alt="Hugging Face" />
221
+ <span>Hugging Face</span>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ <div className="qr-bg-help">Tap background to change color</div>
226
+ </div>
227
+ </div>
228
+
229
+ <div className="actions-section">
230
+ <div className="download-actions">
231
+ <h3>Download Options</h3>
232
+ <div className="button-group">
233
+ <button
234
+ onClick={() => handleDownload('card')}
235
+ className="download-btn primary"
236
+ >
237
+ Download Card (PNG)
238
+ </button>
239
+ <button
240
+ onClick={() => handleDownload('qr-only')}
241
+ className="download-btn secondary"
242
+ >
243
+ QR Code Only
244
+ </button>
245
+ </div>
246
+ </div>
247
+
248
+ <div className="share-actions">
249
+ <h3>Share Profile</h3>
250
+ <div className="button-group">
251
+ <button
252
+ onClick={() => handleShare('twitter')}
253
+ className="share-btn twitter"
254
+ aria-label="Share on Twitter"
255
+ >
256
+ 𝕏 Twitter
257
+ </button>
258
+ <button
259
+ onClick={() => handleShare('facebook')}
260
+ className="share-btn facebook"
261
+ aria-label="Share on Facebook"
262
+ >
263
+ f Facebook
264
+ </button>
265
+ <button
266
+ onClick={() => handleShare('linkedin')}
267
+ className="share-btn linkedin"
268
+ aria-label="Share on LinkedIn"
269
+ >
270
+ in LinkedIn
271
+ </button>
272
+ </div>
273
+ </div>
274
+ </div>
275
+ </div>
276
+ )}
277
+ </div>
278
+ );
279
+ };
280
+
281
+ export default HuggingFaceQRGenerator;
components/HuggingFaceQRGenerator.tsx ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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';
7
+ import * as htmlToImage from 'html-to-image';
8
+ import { Button } from '@/components/ui/button';
9
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
10
+ 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, Loader2, QrCode, Twitter, Facebook, Linkedin, Sparkles, ExternalLink, CheckCircle2, MessageCircle, Mail, MoreHorizontal, ChevronLeft, Menu } from 'lucide-react';
15
+
16
+ const HuggingFaceQRGenerator = () => {
17
+ const [inputUrl, setInputUrl] = useState('');
18
+ const [profileData, setProfileData] = useState<any>(null);
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
29
+ const parsed = parseHuggingFaceUrl(inputUrl);
30
+
31
+ // Fetch profile data from our API
32
+ const response = await fetch('/api/huggingface', {
33
+ method: 'POST',
34
+ headers: {
35
+ 'Content-Type': 'application/json',
36
+ },
37
+ body: JSON.stringify({
38
+ username: parsed.username,
39
+ resourceType: parsed.resourceType,
40
+ resourceName: parsed.resourceName
41
+ }),
42
+ });
43
+
44
+ if (!response.ok) {
45
+ throw new Error('Failed to fetch profile data');
46
+ }
47
+
48
+ const data = await response.json();
49
+
50
+ // Use proxy for the avatar to avoid CORS issues
51
+ const proxiedAvatarUrl = `/api/proxy-image?url=${encodeURIComponent(data.avatarUrl)}`;
52
+
53
+ setProfileData({
54
+ ...data,
55
+ avatarUrl: proxiedAvatarUrl,
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 === 'png' || format === 'card') {
74
+ // Download the entire card with user info
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'
82
+ }
83
+ });
84
+
85
+ // Convert dataURL to blob
86
+ const response = await fetch(dataUrl);
87
+ const blob = await response.blob();
88
+ saveAs(blob, `huggingface-${profileData.username}-card.png`);
89
+
90
+ } else if (format === 'qr-only' && qrCodeInstance) {
91
+ // Download just the QR code
92
+ const blob = await qrCodeInstance.download({
93
+ name: `huggingface-${profileData.username}`,
94
+ extension: 'png'
95
+ });
96
+ saveAs(blob, `huggingface-${profileData.username}-qr.png`);
97
+ }
98
+ } catch (err) {
99
+ console.error('Download error:', err);
100
+ }
101
+ };
102
+
103
+ const handleShare = (platform: string) => {
104
+ const shareText = `Check out my Hugging Face profile!`;
105
+ const shareUrl = profileData?.qrValue || '';
106
+
107
+ const shareLinks: { [key: string]: string } = {
108
+ twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(shareUrl)}`,
109
+ facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`,
110
+ linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`
111
+ };
112
+
113
+ if (shareLinks[platform]) {
114
+ window.open(shareLinks[platform], '_blank', 'width=600,height=400');
115
+ }
116
+ };
117
+
118
+ const getResourceIcon = (type: string) => {
119
+ switch(type) {
120
+ case 'model': return '🤖';
121
+ case 'dataset': return '📊';
122
+ case 'space': return '🚀';
123
+ default: return '👤';
124
+ }
125
+ };
126
+
127
+ const isValid = inputUrl.trim().length > 0
128
+
129
+ return (
130
+ <div className="min-h-screen grid place-items-center 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 p-4 md:p-8">
131
+ <div className="w-full max-w-3xl">
132
+ {/* Screenshot-1 inspired input card */}
133
+ <Card className="shadow-xl" style={{ fontFamily: 'var(--font-inter)' }}>
134
+ <CardHeader className="pb-2">
135
+ <div className="flex items-center gap-2">
136
+ <span className="text-3xl">🤗</span>
137
+ <CardTitle className="text-2xl">Hugging Face</CardTitle>
138
+ </div>
139
+ </CardHeader>
140
+ <CardContent className="space-y-5">
141
+ <div className="space-y-2">
142
+ <Label>HUGGING FACE USERNAME</Label>
143
+ <div className="relative">
144
+ <div className="flex items-stretch">
145
+ <span className="hidden sm:flex items-center px-3 text-sm text-muted-foreground bg-secondary/60 border border-input rounded-l-md">
146
+ https://huggingface.co/
147
+ </span>
148
+ <Input
149
+ type="text"
150
+ value={inputUrl}
151
+ onChange={(e) => setInputUrl(e.target.value)}
152
+ placeholder="username or full URL"
153
+ className="h-11 sm:rounded-l-none sm:border-l-0 pr-10"
154
+ onKeyPress={(e) => e.key === 'Enter' && handleGenerate()}
155
+ disabled={loading}
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
+ <Button
165
+ onClick={handleGenerate}
166
+ disabled={!inputUrl || loading}
167
+ className="rounded-full px-6 bg-muted text-foreground hover:bg-muted/80 dark:bg-input/40"
168
+ >
169
+ {loading ? 'Generating…' : 'Generate QR Code'}
170
+ </Button>
171
+
172
+ {error && (
173
+ <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">
174
+ {error}
175
+ </div>
176
+ )}
177
+ </CardContent>
178
+ </Card>
179
+
180
+ {/* Result Section - Phone-like preview matching screenshot */}
181
+ {profileData && (
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
+ <div className="qr-card-v2" id="qr-card">
190
+ <div className="qr-avatar-wrap">
191
+ <img
192
+ src={profileData.originalAvatarUrl || profileData.avatarUrl}
193
+ alt={profileData.fullName}
194
+ className="qr-avatar"
195
+ crossOrigin="anonymous"
196
+ />
197
+ </div>
198
+ <div className="qr-card-inner">
199
+ <div className="qr-name">{profileData.fullName}</div>
200
+ <div className="qr-code-holder">
201
+ <QRCodeWithLogo
202
+ value={profileData.qrValue}
203
+ logoUrl="https://huggingface.co/front/assets/huggingface_logo.svg"
204
+ size={210}
205
+ onQRCodeReady={setQrCodeInstance}
206
+ backgroundColor="#FFFFFF"
207
+ dotsColor="#000000"
208
+ />
209
+ </div>
210
+ <p className="qr-caption">Share your QR code so others can follow you</p>
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
+ <div className="qr-bg-help">Tap background to change color</div>
219
+
220
+ <div className="qr-share-sheet">
221
+ <div className="qr-download">
222
+ <button onClick={() => handleDownload('card')} className="qr-circle">
223
+ <Download size={18} />
224
+ </button>
225
+ <span>Download</span>
226
+ </div>
227
+ <div className="qr-share-group">
228
+ <span className="qr-share-label">Share to</span>
229
+ <div className="qr-share-actions">
230
+ <button className="qr-circle"><MessageCircle size={18} /></button>
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
+ </div>
246
+ </div>
247
+ );
248
+ };
249
+
250
+ export default HuggingFaceQRGenerator;
components/QRCodeWithLogo.js ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import React, { useEffect, useRef, useState } from 'react';
4
+ import QRCodeStyling from 'qr-code-styling';
5
+
6
+ const QRCodeWithLogo = ({
7
+ value,
8
+ logoUrl,
9
+ size = 300,
10
+ onQRCodeReady,
11
+ backgroundColor = '#FFFFFF',
12
+ dotsColor = '#000000'
13
+ }) => {
14
+ const ref = useRef(null);
15
+ const [qrCode, setQrCode] = useState(null);
16
+
17
+ useEffect(() => {
18
+ if (!value) return;
19
+
20
+ const qrCodeInstance = new QRCodeStyling({
21
+ width: size,
22
+ height: size,
23
+ type: 'svg',
24
+ data: value,
25
+ image: logoUrl,
26
+ dotsOptions: {
27
+ color: dotsColor,
28
+ type: 'rounded'
29
+ },
30
+ backgroundOptions: {
31
+ color: backgroundColor,
32
+ },
33
+ imageOptions: {
34
+ crossOrigin: 'anonymous',
35
+ margin: 10,
36
+ imageSize: 0.3,
37
+ hideBackgroundDots: true
38
+ },
39
+ cornersSquareOptions: {
40
+ type: 'extra-rounded',
41
+ color: dotsColor
42
+ },
43
+ cornersDotOptions: {
44
+ type: 'dot',
45
+ color: dotsColor
46
+ }
47
+ });
48
+
49
+ setQrCode(qrCodeInstance);
50
+
51
+ if (ref.current) {
52
+ ref.current.innerHTML = '';
53
+ qrCodeInstance.append(ref.current);
54
+ }
55
+
56
+ if (onQRCodeReady) {
57
+ onQRCodeReady(qrCodeInstance);
58
+ }
59
+
60
+ return () => {
61
+ if (ref.current) {
62
+ ref.current.innerHTML = '';
63
+ }
64
+ };
65
+ }, [value, logoUrl, size, backgroundColor, dotsColor]);
66
+
67
+ return <div ref={ref} className="qr-code-container" />;
68
+ };
69
+
70
+ export default QRCodeWithLogo;
components/QRCodeWithLogo.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import React, { useEffect, useRef, useState } from 'react';
4
+ import QRCodeStyling from 'qr-code-styling';
5
+
6
+ interface QRCodeWithLogoProps {
7
+ value: string;
8
+ logoUrl?: string;
9
+ size?: number;
10
+ onQRCodeReady?: (qrCode: QRCodeStyling) => void;
11
+ backgroundColor?: string;
12
+ dotsColor?: string;
13
+ }
14
+
15
+ const QRCodeWithLogo: React.FC<QRCodeWithLogoProps> = ({
16
+ value,
17
+ logoUrl,
18
+ size = 300,
19
+ onQRCodeReady,
20
+ backgroundColor = '#FFFFFF',
21
+ dotsColor = '#000000'
22
+ }) => {
23
+ const ref = useRef<HTMLDivElement>(null);
24
+ const [qrCode, setQrCode] = useState<QRCodeStyling | null>(null);
25
+
26
+ useEffect(() => {
27
+ if (!value) return;
28
+
29
+ const qrCodeInstance = new QRCodeStyling({
30
+ width: size,
31
+ height: size,
32
+ type: 'svg',
33
+ data: value,
34
+ image: logoUrl,
35
+ dotsOptions: {
36
+ color: dotsColor,
37
+ type: 'rounded'
38
+ },
39
+ backgroundOptions: {
40
+ color: backgroundColor,
41
+ },
42
+ imageOptions: {
43
+ crossOrigin: 'anonymous',
44
+ margin: 10,
45
+ imageSize: 0.3,
46
+ hideBackgroundDots: true
47
+ },
48
+ cornersSquareOptions: {
49
+ type: 'extra-rounded',
50
+ color: dotsColor
51
+ },
52
+ cornersDotOptions: {
53
+ type: 'dot',
54
+ color: dotsColor
55
+ }
56
+ });
57
+
58
+ setQrCode(qrCodeInstance);
59
+
60
+ if (ref.current) {
61
+ ref.current.innerHTML = '';
62
+ qrCodeInstance.append(ref.current);
63
+ }
64
+
65
+ if (onQRCodeReady) {
66
+ onQRCodeReady(qrCodeInstance);
67
+ }
68
+
69
+ return () => {
70
+ if (ref.current) {
71
+ ref.current.innerHTML = '';
72
+ }
73
+ };
74
+ }, [value, logoUrl, size, backgroundColor, dotsColor, onQRCodeReady]);
75
+
76
+ return <div ref={ref} className="qr-code-container" />;
77
+ };
78
+
79
+ export default QRCodeWithLogo;
components/ui/avatar.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Avatar({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
12
+ return (
13
+ <AvatarPrimitive.Root
14
+ data-slot="avatar"
15
+ className={cn(
16
+ "relative flex size-8 shrink-0 overflow-hidden rounded-full",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ function AvatarImage({
25
+ className,
26
+ ...props
27
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
28
+ return (
29
+ <AvatarPrimitive.Image
30
+ data-slot="avatar-image"
31
+ className={cn("aspect-square size-full", className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AvatarFallback({
38
+ className,
39
+ ...props
40
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
41
+ return (
42
+ <AvatarPrimitive.Fallback
43
+ data-slot="avatar-fallback"
44
+ className={cn(
45
+ "bg-muted flex size-full items-center justify-center rounded-full",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+
53
+ export { Avatar, AvatarImage, AvatarFallback }
components/ui/badge.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14
+ secondary:
15
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16
+ destructive:
17
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18
+ outline:
19
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ },
25
+ }
26
+ )
27
+
28
+ function Badge({
29
+ className,
30
+ variant,
31
+ asChild = false,
32
+ ...props
33
+ }: React.ComponentProps<"span"> &
34
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
+ const Comp = asChild ? Slot : "span"
36
+
37
+ return (
38
+ <Comp
39
+ data-slot="badge"
40
+ className={cn(badgeVariants({ variant }), className)}
41
+ {...props}
42
+ />
43
+ )
44
+ }
45
+
46
+ export { Badge, badgeVariants }
components/ui/button.tsx ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
+ outline:
16
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost:
20
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27
+ icon: "size-9",
28
+ "icon-sm": "size-8",
29
+ "icon-lg": "size-10",
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: "default",
34
+ size: "default",
35
+ },
36
+ }
37
+ )
38
+
39
+ function Button({
40
+ className,
41
+ variant,
42
+ size,
43
+ asChild = false,
44
+ ...props
45
+ }: React.ComponentProps<"button"> &
46
+ VariantProps<typeof buttonVariants> & {
47
+ asChild?: boolean
48
+ }) {
49
+ const Comp = asChild ? Slot : "button"
50
+
51
+ return (
52
+ <Comp
53
+ data-slot="button"
54
+ className={cn(buttonVariants({ variant, size, className }))}
55
+ {...props}
56
+ />
57
+ )
58
+ }
59
+
60
+ export { Button, buttonVariants }
components/ui/card.tsx ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("leading-none font-semibold", className)}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground text-sm", className)}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn("px-6", className)}
69
+ {...props}
70
+ />
71
+ )
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardHeader,
87
+ CardFooter,
88
+ CardTitle,
89
+ CardAction,
90
+ CardDescription,
91
+ CardContent,
92
+ }
components/ui/input.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+ return (
7
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
13
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ export { Input }
components/ui/label.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Label({ className, ...props }: React.ComponentProps<"label">) {
6
+ return (
7
+ <label
8
+ data-slot="label"
9
+ className={cn(
10
+ "text-xs font-semibold tracking-wider text-muted-foreground",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ export { Label }
19
+
20
+
lib/huggingface.js ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Parse Hugging Face URL and extract username/organization
3
+ * @param {string} url - The Hugging Face URL
4
+ * @returns {object} - Parsed data with type and username
5
+ */
6
+ export function parseHuggingFaceUrl(url) {
7
+ try {
8
+ // Remove trailing slash if present
9
+ url = url.trim().replace(/\/$/, '');
10
+
11
+ // Handle different URL formats
12
+ let parsedUrl;
13
+ if (url.startsWith('http://') || url.startsWith('https://')) {
14
+ parsedUrl = new URL(url);
15
+ } else if (url.startsWith('huggingface.co')) {
16
+ parsedUrl = new URL(`https://${url}`);
17
+ } else {
18
+ // Assume it's just a username
19
+ return {
20
+ type: 'profile',
21
+ username: url,
22
+ profileUrl: `https://huggingface.co/${url}`,
23
+ resourceType: null,
24
+ resourceName: null,
25
+ fullUrl: `https://huggingface.co/${url}`
26
+ };
27
+ }
28
+
29
+ // Check if it's a Hugging Face domain
30
+ if (!parsedUrl.hostname.includes('huggingface.co')) {
31
+ throw new Error('Not a valid Hugging Face URL');
32
+ }
33
+
34
+ const pathParts = parsedUrl.pathname.split('/').filter(part => part);
35
+
36
+ if (pathParts.length === 0) {
37
+ throw new Error('No username or organization found in URL');
38
+ }
39
+
40
+ // Extract username/org from different URL types
41
+ let username = pathParts[0];
42
+ let resourceType = null;
43
+ let resourceName = null;
44
+ let type = 'profile';
45
+
46
+ // Detect resource type and name
47
+ if (pathParts.length >= 2) {
48
+ // Check for direct resource URLs like /username/model-name
49
+ const secondPart = pathParts[1];
50
+
51
+ // Check if it's a resource collection page
52
+ if (secondPart === 'models' || secondPart === 'datasets' || secondPart === 'spaces') {
53
+ // This is a collection page, not a specific resource
54
+ type = 'profile';
55
+ resourceType = null;
56
+ resourceName = null;
57
+ } else {
58
+ // This might be a direct resource URL
59
+ // Models, datasets, and spaces have direct URLs like /username/resource-name
60
+ // We need to detect what type based on context or default to model
61
+ resourceName = secondPart;
62
+
63
+ // Try to detect resource type from URL structure
64
+ if (parsedUrl.hostname === 'huggingface.co') {
65
+ // Check for spaces subdomain or path hints
66
+ if (pathParts.length >= 3) {
67
+ if (pathParts[2] === 'tree' || pathParts[2] === 'blob' || pathParts[2] === 'resolve') {
68
+ resourceType = 'model'; // Repository-like structure usually means model
69
+ } else if (pathParts[2] === 'discussions' || pathParts[2] === 'settings') {
70
+ resourceType = 'model'; // These are common in model repos
71
+ }
72
+ } else {
73
+ // Default to model for direct /username/name pattern
74
+ resourceType = 'model';
75
+ }
76
+ }
77
+ }
78
+ } else if (parsedUrl.hostname.includes('.hf.space')) {
79
+ // This is a Spaces URL like username-spacename.hf.space
80
+ const subdomain = parsedUrl.hostname.split('.')[0];
81
+ if (subdomain.includes('-')) {
82
+ const parts = subdomain.split('-');
83
+ username = parts[0];
84
+ resourceName = parts.slice(1).join('-');
85
+ resourceType = 'space';
86
+ }
87
+ }
88
+
89
+ // Check for datasets in path
90
+ if (parsedUrl.pathname.includes('/datasets/')) {
91
+ const datasetPath = parsedUrl.pathname.split('/datasets/')[1];
92
+ const datasetParts = datasetPath.split('/').filter(p => p);
93
+ if (datasetParts.length >= 2) {
94
+ username = datasetParts[0]; // Correct the username
95
+ resourceType = 'dataset';
96
+ resourceName = datasetParts[1];
97
+ }
98
+ }
99
+
100
+ // Check for spaces in path
101
+ if (parsedUrl.pathname.includes('/spaces/')) {
102
+ const spacePath = parsedUrl.pathname.split('/spaces/')[1];
103
+ const spaceParts = spacePath.split('/').filter(p => p);
104
+ if (spaceParts.length >= 2) {
105
+ username = spaceParts[0]; // Correct the username
106
+ resourceType = 'space';
107
+ resourceName = spaceParts[1];
108
+ }
109
+ }
110
+
111
+ return {
112
+ type,
113
+ username,
114
+ profileUrl: `https://huggingface.co/${username}`,
115
+ resourceType,
116
+ resourceName,
117
+ fullUrl: parsedUrl.href
118
+ };
119
+ } catch (error) {
120
+ throw new Error(`Invalid Hugging Face URL: ${error.message}`);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Get the avatar URL for a Hugging Face user/organization
126
+ * @param {string} username - The username or organization name
127
+ * @returns {string} - The avatar URL
128
+ */
129
+ export function getAvatarUrl(username) {
130
+ // Hugging Face avatar URL pattern
131
+ return `https://huggingface.co/avatars/${username}.svg`;
132
+ }
lib/huggingface.ts ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ interface ParsedHuggingFaceUrl {
2
+ type: string;
3
+ username: string;
4
+ profileUrl: string;
5
+ resourceType: string | null;
6
+ resourceName: string | null;
7
+ fullUrl: string;
8
+ }
9
+
10
+ /**
11
+ * Parse Hugging Face URL and extract username/organization
12
+ * @param {string} url - The Hugging Face URL
13
+ * @returns {ParsedHuggingFaceUrl} - Parsed data with type and username
14
+ */
15
+ export function parseHuggingFaceUrl(url: string): ParsedHuggingFaceUrl {
16
+ try {
17
+ // Remove trailing slash if present
18
+ url = url.trim().replace(/\/$/, '');
19
+
20
+ // Handle different URL formats
21
+ let parsedUrl: URL;
22
+ if (url.startsWith('http://') || url.startsWith('https://')) {
23
+ parsedUrl = new URL(url);
24
+ } else if (url.startsWith('huggingface.co')) {
25
+ parsedUrl = new URL(`https://${url}`);
26
+ } else {
27
+ // Assume it's just a username
28
+ return {
29
+ type: 'profile',
30
+ username: url,
31
+ profileUrl: `https://huggingface.co/${url}`,
32
+ resourceType: null,
33
+ resourceName: null,
34
+ fullUrl: `https://huggingface.co/${url}`
35
+ };
36
+ }
37
+
38
+ // Check if it's a Hugging Face domain
39
+ if (!parsedUrl.hostname.includes('huggingface.co')) {
40
+ throw new Error('Not a valid Hugging Face URL');
41
+ }
42
+
43
+ const pathParts = parsedUrl.pathname.split('/').filter(part => part);
44
+
45
+ if (pathParts.length === 0) {
46
+ throw new Error('No username or organization found in URL');
47
+ }
48
+
49
+ // Extract username/org from different URL types
50
+ let username = pathParts[0];
51
+ let resourceType: string | null = null;
52
+ let resourceName: string | null = null;
53
+ let type = 'profile';
54
+
55
+ // Detect resource type and name
56
+ if (pathParts.length >= 2) {
57
+ // Check for direct resource URLs like /username/model-name
58
+ const secondPart = pathParts[1];
59
+
60
+ // Check if it's a resource collection page
61
+ if (secondPart === 'models' || secondPart === 'datasets' || secondPart === 'spaces') {
62
+ // This is a collection page, not a specific resource
63
+ type = 'profile';
64
+ resourceType = null;
65
+ resourceName = null;
66
+ } else {
67
+ // This might be a direct resource URL
68
+ // Models, datasets, and spaces have direct URLs like /username/resource-name
69
+ // We need to detect what type based on context or default to model
70
+ resourceName = secondPart;
71
+
72
+ // Try to detect resource type from URL structure
73
+ if (parsedUrl.hostname === 'huggingface.co') {
74
+ // Check for spaces subdomain or path hints
75
+ if (pathParts.length >= 3) {
76
+ if (pathParts[2] === 'tree' || pathParts[2] === 'blob' || pathParts[2] === 'resolve') {
77
+ resourceType = 'model'; // Repository-like structure usually means model
78
+ } else if (pathParts[2] === 'discussions' || pathParts[2] === 'settings') {
79
+ resourceType = 'model'; // These are common in model repos
80
+ }
81
+ } else {
82
+ // Default to model for direct /username/name pattern
83
+ resourceType = 'model';
84
+ }
85
+ }
86
+ }
87
+ } else if (parsedUrl.hostname.includes('.hf.space')) {
88
+ // This is a Spaces URL like username-spacename.hf.space
89
+ const subdomain = parsedUrl.hostname.split('.')[0];
90
+ if (subdomain.includes('-')) {
91
+ const parts = subdomain.split('-');
92
+ username = parts[0];
93
+ resourceName = parts.slice(1).join('-');
94
+ resourceType = 'space';
95
+ }
96
+ }
97
+
98
+ // Check for datasets in path
99
+ if (parsedUrl.pathname.includes('/datasets/')) {
100
+ const datasetPath = parsedUrl.pathname.split('/datasets/')[1];
101
+ const datasetParts = datasetPath.split('/').filter(p => p);
102
+ if (datasetParts.length >= 2) {
103
+ username = datasetParts[0]; // Correct the username
104
+ resourceType = 'dataset';
105
+ resourceName = datasetParts[1];
106
+ }
107
+ }
108
+
109
+ // Check for spaces in path
110
+ if (parsedUrl.pathname.includes('/spaces/')) {
111
+ const spacePath = parsedUrl.pathname.split('/spaces/')[1];
112
+ const spaceParts = spacePath.split('/').filter(p => p);
113
+ if (spaceParts.length >= 2) {
114
+ username = spaceParts[0]; // Correct the username
115
+ resourceType = 'space';
116
+ resourceName = spaceParts[1];
117
+ }
118
+ }
119
+
120
+ return {
121
+ type,
122
+ username,
123
+ profileUrl: `https://huggingface.co/${username}`,
124
+ resourceType,
125
+ resourceName,
126
+ fullUrl: parsedUrl.href
127
+ };
128
+ } catch (error: any) {
129
+ throw new Error(`Invalid Hugging Face URL: ${error.message}`);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Get the avatar URL for a Hugging Face user/organization
135
+ * @param {string} username - The username or organization name
136
+ * @returns {string} - The avatar URL
137
+ */
138
+ export function getAvatarUrl(username: string): string {
139
+ // Hugging Face avatar URL pattern
140
+ return `https://huggingface.co/avatars/${username}.svg`;
141
+ }
lib/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
package-lock.json CHANGED
@@ -8,18 +8,32 @@
8
  "name": "qr-code",
9
  "version": "0.1.0",
10
  "dependencies": {
 
 
 
 
 
 
 
 
11
  "next": "16.0.1",
 
 
12
  "react": "19.2.0",
13
- "react-dom": "19.2.0"
 
 
14
  },
15
  "devDependencies": {
16
  "@tailwindcss/postcss": "^4",
 
17
  "@types/node": "^20",
18
  "@types/react": "^19",
19
  "@types/react-dom": "^19",
20
  "eslint": "^9",
21
  "eslint-config-next": "16.0.1",
22
  "tailwindcss": "^4",
 
23
  "typescript": "^5"
24
  }
25
  },
@@ -1214,6 +1228,152 @@
1214
  "node": ">=12.4.0"
1215
  }
1216
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1217
  "node_modules/@rtsao/scc": {
1218
  "version": "1.1.0",
1219
  "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -1519,6 +1679,13 @@
1519
  "dev": true,
1520
  "license": "MIT"
1521
  },
 
 
 
 
 
 
 
1522
  "node_modules/@types/json-schema": {
1523
  "version": "7.0.15",
1524
  "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1547,7 +1714,7 @@
1547
  "version": "19.2.2",
1548
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
1549
  "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
1550
- "dev": true,
1551
  "license": "MIT",
1552
  "dependencies": {
1553
  "csstype": "^3.0.2"
@@ -1557,7 +1724,7 @@
1557
  "version": "19.2.2",
1558
  "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
1559
  "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
1560
- "dev": true,
1561
  "license": "MIT",
1562
  "peerDependencies": {
1563
  "@types/react": "^19.2.0"
@@ -2383,6 +2550,12 @@
2383
  "node": ">= 0.4"
2384
  }
2385
  },
 
 
 
 
 
 
2386
  "node_modules/available-typed-arrays": {
2387
  "version": "1.0.7",
2388
  "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -2409,6 +2582,17 @@
2409
  "node": ">=4"
2410
  }
2411
  },
 
 
 
 
 
 
 
 
 
 
 
2412
  "node_modules/axobject-query": {
2413
  "version": "4.1.0",
2414
  "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -2517,7 +2701,6 @@
2517
  "version": "1.0.2",
2518
  "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
2519
  "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
2520
- "dev": true,
2521
  "license": "MIT",
2522
  "dependencies": {
2523
  "es-errors": "^1.3.0",
@@ -2591,12 +2774,33 @@
2591
  "url": "https://github.com/chalk/chalk?sponsor=1"
2592
  }
2593
  },
 
 
 
 
 
 
 
 
 
 
 
 
2594
  "node_modules/client-only": {
2595
  "version": "0.0.1",
2596
  "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
2597
  "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
2598
  "license": "MIT"
2599
  },
 
 
 
 
 
 
 
 
 
2600
  "node_modules/color-convert": {
2601
  "version": "2.0.1",
2602
  "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2617,6 +2821,18 @@
2617
  "dev": true,
2618
  "license": "MIT"
2619
  },
 
 
 
 
 
 
 
 
 
 
 
 
2620
  "node_modules/concat-map": {
2621
  "version": "0.0.1",
2622
  "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2650,7 +2866,7 @@
2650
  "version": "3.1.3",
2651
  "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
2652
  "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
2653
- "dev": true,
2654
  "license": "MIT"
2655
  },
2656
  "node_modules/damerau-levenshtein": {
@@ -2775,6 +2991,15 @@
2775
  "url": "https://github.com/sponsors/ljharb"
2776
  }
2777
  },
 
 
 
 
 
 
 
 
 
2778
  "node_modules/detect-libc": {
2779
  "version": "2.1.2",
2780
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -2802,7 +3027,6 @@
2802
  "version": "1.0.1",
2803
  "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
2804
  "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
2805
- "dev": true,
2806
  "license": "MIT",
2807
  "dependencies": {
2808
  "call-bind-apply-helpers": "^1.0.1",
@@ -2914,7 +3138,6 @@
2914
  "version": "1.0.1",
2915
  "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
2916
  "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
2917
- "dev": true,
2918
  "license": "MIT",
2919
  "engines": {
2920
  "node": ">= 0.4"
@@ -2924,7 +3147,6 @@
2924
  "version": "1.3.0",
2925
  "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
2926
  "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
2927
- "dev": true,
2928
  "license": "MIT",
2929
  "engines": {
2930
  "node": ">= 0.4"
@@ -2962,7 +3184,6 @@
2962
  "version": "1.1.1",
2963
  "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
2964
  "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
2965
- "dev": true,
2966
  "license": "MIT",
2967
  "dependencies": {
2968
  "es-errors": "^1.3.0"
@@ -2975,7 +3196,6 @@
2975
  "version": "2.1.0",
2976
  "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
2977
  "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
2978
- "dev": true,
2979
  "license": "MIT",
2980
  "dependencies": {
2981
  "es-errors": "^1.3.0",
@@ -3539,6 +3759,12 @@
3539
  "node": ">=16.0.0"
3540
  }
3541
  },
 
 
 
 
 
 
3542
  "node_modules/fill-range": {
3543
  "version": "7.1.1",
3544
  "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -3590,6 +3816,26 @@
3590
  "dev": true,
3591
  "license": "ISC"
3592
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3593
  "node_modules/for-each": {
3594
  "version": "0.3.5",
3595
  "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -3606,11 +3852,26 @@
3606
  "url": "https://github.com/sponsors/ljharb"
3607
  }
3608
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3609
  "node_modules/function-bind": {
3610
  "version": "1.1.2",
3611
  "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
3612
  "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
3613
- "dev": true,
3614
  "license": "MIT",
3615
  "funding": {
3616
  "url": "https://github.com/sponsors/ljharb"
@@ -3671,7 +3932,6 @@
3671
  "version": "1.3.0",
3672
  "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
3673
  "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
3674
- "dev": true,
3675
  "license": "MIT",
3676
  "dependencies": {
3677
  "call-bind-apply-helpers": "^1.0.2",
@@ -3696,7 +3956,6 @@
3696
  "version": "1.0.1",
3697
  "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
3698
  "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
3699
- "dev": true,
3700
  "license": "MIT",
3701
  "dependencies": {
3702
  "dunder-proto": "^1.0.1",
@@ -3784,7 +4043,6 @@
3784
  "version": "1.2.0",
3785
  "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
3786
  "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
3787
- "dev": true,
3788
  "license": "MIT",
3789
  "engines": {
3790
  "node": ">= 0.4"
@@ -3863,7 +4121,6 @@
3863
  "version": "1.1.0",
3864
  "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
3865
  "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
3866
- "dev": true,
3867
  "license": "MIT",
3868
  "engines": {
3869
  "node": ">= 0.4"
@@ -3876,7 +4133,6 @@
3876
  "version": "1.0.2",
3877
  "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
3878
  "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
3879
- "dev": true,
3880
  "license": "MIT",
3881
  "dependencies": {
3882
  "has-symbols": "^1.0.3"
@@ -3892,7 +4148,6 @@
3892
  "version": "2.0.2",
3893
  "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
3894
  "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
3895
- "dev": true,
3896
  "license": "MIT",
3897
  "dependencies": {
3898
  "function-bind": "^1.1.2"
@@ -3918,6 +4173,12 @@
3918
  "hermes-estree": "0.25.1"
3919
  }
3920
  },
 
 
 
 
 
 
3921
  "node_modules/ignore": {
3922
  "version": "5.3.2",
3923
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4431,7 +4692,6 @@
4431
  "version": "4.0.0",
4432
  "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
4433
  "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
4434
- "dev": true,
4435
  "license": "MIT"
4436
  },
4437
  "node_modules/js-yaml": {
@@ -4842,7 +5102,6 @@
4842
  "version": "1.4.0",
4843
  "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
4844
  "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
4845
- "dev": true,
4846
  "license": "MIT",
4847
  "dependencies": {
4848
  "js-tokens": "^3.0.0 || ^4.0.0"
@@ -4861,6 +5120,15 @@
4861
  "yallist": "^3.0.2"
4862
  }
4863
  },
 
 
 
 
 
 
 
 
 
4864
  "node_modules/magic-string": {
4865
  "version": "0.30.21",
4866
  "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -4875,7 +5143,6 @@
4875
  "version": "1.1.0",
4876
  "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
4877
  "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
4878
- "dev": true,
4879
  "license": "MIT",
4880
  "engines": {
4881
  "node": ">= 0.4"
@@ -4905,6 +5172,27 @@
4905
  "node": ">=8.6"
4906
  }
4907
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4908
  "node_modules/minimatch": {
4909
  "version": "3.1.2",
4910
  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -5067,7 +5355,6 @@
5067
  "version": "4.1.1",
5068
  "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
5069
  "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
5070
- "dev": true,
5071
  "license": "MIT",
5072
  "engines": {
5073
  "node": ">=0.10.0"
@@ -5366,7 +5653,6 @@
5366
  "version": "15.8.1",
5367
  "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
5368
  "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
5369
- "dev": true,
5370
  "license": "MIT",
5371
  "dependencies": {
5372
  "loose-envify": "^1.4.0",
@@ -5374,6 +5660,12 @@
5374
  "react-is": "^16.13.1"
5375
  }
5376
  },
 
 
 
 
 
 
5377
  "node_modules/punycode": {
5378
  "version": "2.3.1",
5379
  "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -5384,6 +5676,36 @@
5384
  "node": ">=6"
5385
  }
5386
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5387
  "node_modules/queue-microtask": {
5388
  "version": "1.2.3",
5389
  "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -5430,9 +5752,21 @@
5430
  "version": "16.13.1",
5431
  "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
5432
  "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
5433
- "dev": true,
5434
  "license": "MIT"
5435
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
5436
  "node_modules/reflect.getprototypeof": {
5437
  "version": "1.0.10",
5438
  "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -6043,6 +6377,16 @@
6043
  "url": "https://github.com/sponsors/ljharb"
6044
  }
6045
  },
 
 
 
 
 
 
 
 
 
 
6046
  "node_modules/tailwindcss": {
6047
  "version": "4.1.16",
6048
  "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
@@ -6170,6 +6514,16 @@
6170
  "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
6171
  "license": "0BSD"
6172
  },
 
 
 
 
 
 
 
 
 
 
6173
  "node_modules/type-check": {
6174
  "version": "0.4.0",
6175
  "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -6401,6 +6755,15 @@
6401
  "punycode": "^2.1.0"
6402
  }
6403
  },
 
 
 
 
 
 
 
 
 
6404
  "node_modules/which": {
6405
  "version": "2.0.2",
6406
  "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
 
8
  "name": "qr-code",
9
  "version": "0.1.0",
10
  "dependencies": {
11
+ "@radix-ui/react-avatar": "^1.1.10",
12
+ "@radix-ui/react-slot": "^1.2.3",
13
+ "axios": "^1.13.1",
14
+ "class-variance-authority": "^0.7.1",
15
+ "clsx": "^2.1.1",
16
+ "file-saver": "^2.0.5",
17
+ "html-to-image": "^1.11.13",
18
+ "lucide-react": "^0.552.0",
19
  "next": "16.0.1",
20
+ "qr-code-styling": "^1.9.2",
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
  },
27
  "devDependencies": {
28
  "@tailwindcss/postcss": "^4",
29
+ "@types/file-saver": "^2.0.7",
30
  "@types/node": "^20",
31
  "@types/react": "^19",
32
  "@types/react-dom": "^19",
33
  "eslint": "^9",
34
  "eslint-config-next": "16.0.1",
35
  "tailwindcss": "^4",
36
+ "tw-animate-css": "^1.4.0",
37
  "typescript": "^5"
38
  }
39
  },
 
1228
  "node": ">=12.4.0"
1229
  }
1230
  },
1231
+ "node_modules/@radix-ui/react-avatar": {
1232
+ "version": "1.1.10",
1233
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
1234
+ "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
1235
+ "license": "MIT",
1236
+ "dependencies": {
1237
+ "@radix-ui/react-context": "1.1.2",
1238
+ "@radix-ui/react-primitive": "2.1.3",
1239
+ "@radix-ui/react-use-callback-ref": "1.1.1",
1240
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
1241
+ "@radix-ui/react-use-layout-effect": "1.1.1"
1242
+ },
1243
+ "peerDependencies": {
1244
+ "@types/react": "*",
1245
+ "@types/react-dom": "*",
1246
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1247
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1248
+ },
1249
+ "peerDependenciesMeta": {
1250
+ "@types/react": {
1251
+ "optional": true
1252
+ },
1253
+ "@types/react-dom": {
1254
+ "optional": true
1255
+ }
1256
+ }
1257
+ },
1258
+ "node_modules/@radix-ui/react-compose-refs": {
1259
+ "version": "1.1.2",
1260
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
1261
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
1262
+ "license": "MIT",
1263
+ "peerDependencies": {
1264
+ "@types/react": "*",
1265
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1266
+ },
1267
+ "peerDependenciesMeta": {
1268
+ "@types/react": {
1269
+ "optional": true
1270
+ }
1271
+ }
1272
+ },
1273
+ "node_modules/@radix-ui/react-context": {
1274
+ "version": "1.1.2",
1275
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
1276
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
1277
+ "license": "MIT",
1278
+ "peerDependencies": {
1279
+ "@types/react": "*",
1280
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1281
+ },
1282
+ "peerDependenciesMeta": {
1283
+ "@types/react": {
1284
+ "optional": true
1285
+ }
1286
+ }
1287
+ },
1288
+ "node_modules/@radix-ui/react-primitive": {
1289
+ "version": "2.1.3",
1290
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
1291
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
1292
+ "license": "MIT",
1293
+ "dependencies": {
1294
+ "@radix-ui/react-slot": "1.2.3"
1295
+ },
1296
+ "peerDependencies": {
1297
+ "@types/react": "*",
1298
+ "@types/react-dom": "*",
1299
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1300
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1301
+ },
1302
+ "peerDependenciesMeta": {
1303
+ "@types/react": {
1304
+ "optional": true
1305
+ },
1306
+ "@types/react-dom": {
1307
+ "optional": true
1308
+ }
1309
+ }
1310
+ },
1311
+ "node_modules/@radix-ui/react-slot": {
1312
+ "version": "1.2.3",
1313
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
1314
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
1315
+ "license": "MIT",
1316
+ "dependencies": {
1317
+ "@radix-ui/react-compose-refs": "1.1.2"
1318
+ },
1319
+ "peerDependencies": {
1320
+ "@types/react": "*",
1321
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1322
+ },
1323
+ "peerDependenciesMeta": {
1324
+ "@types/react": {
1325
+ "optional": true
1326
+ }
1327
+ }
1328
+ },
1329
+ "node_modules/@radix-ui/react-use-callback-ref": {
1330
+ "version": "1.1.1",
1331
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
1332
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
1333
+ "license": "MIT",
1334
+ "peerDependencies": {
1335
+ "@types/react": "*",
1336
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1337
+ },
1338
+ "peerDependenciesMeta": {
1339
+ "@types/react": {
1340
+ "optional": true
1341
+ }
1342
+ }
1343
+ },
1344
+ "node_modules/@radix-ui/react-use-is-hydrated": {
1345
+ "version": "0.1.0",
1346
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
1347
+ "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
1348
+ "license": "MIT",
1349
+ "dependencies": {
1350
+ "use-sync-external-store": "^1.5.0"
1351
+ },
1352
+ "peerDependencies": {
1353
+ "@types/react": "*",
1354
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1355
+ },
1356
+ "peerDependenciesMeta": {
1357
+ "@types/react": {
1358
+ "optional": true
1359
+ }
1360
+ }
1361
+ },
1362
+ "node_modules/@radix-ui/react-use-layout-effect": {
1363
+ "version": "1.1.1",
1364
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
1365
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
1366
+ "license": "MIT",
1367
+ "peerDependencies": {
1368
+ "@types/react": "*",
1369
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1370
+ },
1371
+ "peerDependenciesMeta": {
1372
+ "@types/react": {
1373
+ "optional": true
1374
+ }
1375
+ }
1376
+ },
1377
  "node_modules/@rtsao/scc": {
1378
  "version": "1.1.0",
1379
  "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
 
1679
  "dev": true,
1680
  "license": "MIT"
1681
  },
1682
+ "node_modules/@types/file-saver": {
1683
+ "version": "2.0.7",
1684
+ "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
1685
+ "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
1686
+ "dev": true,
1687
+ "license": "MIT"
1688
+ },
1689
  "node_modules/@types/json-schema": {
1690
  "version": "7.0.15",
1691
  "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
 
1714
  "version": "19.2.2",
1715
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
1716
  "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
1717
+ "devOptional": true,
1718
  "license": "MIT",
1719
  "dependencies": {
1720
  "csstype": "^3.0.2"
 
1724
  "version": "19.2.2",
1725
  "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
1726
  "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
1727
+ "devOptional": true,
1728
  "license": "MIT",
1729
  "peerDependencies": {
1730
  "@types/react": "^19.2.0"
 
2550
  "node": ">= 0.4"
2551
  }
2552
  },
2553
+ "node_modules/asynckit": {
2554
+ "version": "0.4.0",
2555
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
2556
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
2557
+ "license": "MIT"
2558
+ },
2559
  "node_modules/available-typed-arrays": {
2560
  "version": "1.0.7",
2561
  "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
 
2582
  "node": ">=4"
2583
  }
2584
  },
2585
+ "node_modules/axios": {
2586
+ "version": "1.13.1",
2587
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz",
2588
+ "integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==",
2589
+ "license": "MIT",
2590
+ "dependencies": {
2591
+ "follow-redirects": "^1.15.6",
2592
+ "form-data": "^4.0.4",
2593
+ "proxy-from-env": "^1.1.0"
2594
+ }
2595
+ },
2596
  "node_modules/axobject-query": {
2597
  "version": "4.1.0",
2598
  "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
 
2701
  "version": "1.0.2",
2702
  "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
2703
  "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
 
2704
  "license": "MIT",
2705
  "dependencies": {
2706
  "es-errors": "^1.3.0",
 
2774
  "url": "https://github.com/chalk/chalk?sponsor=1"
2775
  }
2776
  },
2777
+ "node_modules/class-variance-authority": {
2778
+ "version": "0.7.1",
2779
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
2780
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
2781
+ "license": "Apache-2.0",
2782
+ "dependencies": {
2783
+ "clsx": "^2.1.1"
2784
+ },
2785
+ "funding": {
2786
+ "url": "https://polar.sh/cva"
2787
+ }
2788
+ },
2789
  "node_modules/client-only": {
2790
  "version": "0.0.1",
2791
  "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
2792
  "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
2793
  "license": "MIT"
2794
  },
2795
+ "node_modules/clsx": {
2796
+ "version": "2.1.1",
2797
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
2798
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
2799
+ "license": "MIT",
2800
+ "engines": {
2801
+ "node": ">=6"
2802
+ }
2803
+ },
2804
  "node_modules/color-convert": {
2805
  "version": "2.0.1",
2806
  "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
 
2821
  "dev": true,
2822
  "license": "MIT"
2823
  },
2824
+ "node_modules/combined-stream": {
2825
+ "version": "1.0.8",
2826
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
2827
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
2828
+ "license": "MIT",
2829
+ "dependencies": {
2830
+ "delayed-stream": "~1.0.0"
2831
+ },
2832
+ "engines": {
2833
+ "node": ">= 0.8"
2834
+ }
2835
+ },
2836
  "node_modules/concat-map": {
2837
  "version": "0.0.1",
2838
  "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
 
2866
  "version": "3.1.3",
2867
  "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
2868
  "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
2869
+ "devOptional": true,
2870
  "license": "MIT"
2871
  },
2872
  "node_modules/damerau-levenshtein": {
 
2991
  "url": "https://github.com/sponsors/ljharb"
2992
  }
2993
  },
2994
+ "node_modules/delayed-stream": {
2995
+ "version": "1.0.0",
2996
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
2997
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
2998
+ "license": "MIT",
2999
+ "engines": {
3000
+ "node": ">=0.4.0"
3001
+ }
3002
+ },
3003
  "node_modules/detect-libc": {
3004
  "version": "2.1.2",
3005
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
 
3027
  "version": "1.0.1",
3028
  "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
3029
  "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
 
3030
  "license": "MIT",
3031
  "dependencies": {
3032
  "call-bind-apply-helpers": "^1.0.1",
 
3138
  "version": "1.0.1",
3139
  "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
3140
  "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
 
3141
  "license": "MIT",
3142
  "engines": {
3143
  "node": ">= 0.4"
 
3147
  "version": "1.3.0",
3148
  "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
3149
  "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
 
3150
  "license": "MIT",
3151
  "engines": {
3152
  "node": ">= 0.4"
 
3184
  "version": "1.1.1",
3185
  "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
3186
  "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
 
3187
  "license": "MIT",
3188
  "dependencies": {
3189
  "es-errors": "^1.3.0"
 
3196
  "version": "2.1.0",
3197
  "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
3198
  "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
 
3199
  "license": "MIT",
3200
  "dependencies": {
3201
  "es-errors": "^1.3.0",
 
3759
  "node": ">=16.0.0"
3760
  }
3761
  },
3762
+ "node_modules/file-saver": {
3763
+ "version": "2.0.5",
3764
+ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
3765
+ "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
3766
+ "license": "MIT"
3767
+ },
3768
  "node_modules/fill-range": {
3769
  "version": "7.1.1",
3770
  "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
 
3816
  "dev": true,
3817
  "license": "ISC"
3818
  },
3819
+ "node_modules/follow-redirects": {
3820
+ "version": "1.15.11",
3821
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
3822
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
3823
+ "funding": [
3824
+ {
3825
+ "type": "individual",
3826
+ "url": "https://github.com/sponsors/RubenVerborgh"
3827
+ }
3828
+ ],
3829
+ "license": "MIT",
3830
+ "engines": {
3831
+ "node": ">=4.0"
3832
+ },
3833
+ "peerDependenciesMeta": {
3834
+ "debug": {
3835
+ "optional": true
3836
+ }
3837
+ }
3838
+ },
3839
  "node_modules/for-each": {
3840
  "version": "0.3.5",
3841
  "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
 
3852
  "url": "https://github.com/sponsors/ljharb"
3853
  }
3854
  },
3855
+ "node_modules/form-data": {
3856
+ "version": "4.0.4",
3857
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
3858
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
3859
+ "license": "MIT",
3860
+ "dependencies": {
3861
+ "asynckit": "^0.4.0",
3862
+ "combined-stream": "^1.0.8",
3863
+ "es-set-tostringtag": "^2.1.0",
3864
+ "hasown": "^2.0.2",
3865
+ "mime-types": "^2.1.12"
3866
+ },
3867
+ "engines": {
3868
+ "node": ">= 6"
3869
+ }
3870
+ },
3871
  "node_modules/function-bind": {
3872
  "version": "1.1.2",
3873
  "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
3874
  "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
 
3875
  "license": "MIT",
3876
  "funding": {
3877
  "url": "https://github.com/sponsors/ljharb"
 
3932
  "version": "1.3.0",
3933
  "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
3934
  "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
 
3935
  "license": "MIT",
3936
  "dependencies": {
3937
  "call-bind-apply-helpers": "^1.0.2",
 
3956
  "version": "1.0.1",
3957
  "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
3958
  "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
 
3959
  "license": "MIT",
3960
  "dependencies": {
3961
  "dunder-proto": "^1.0.1",
 
4043
  "version": "1.2.0",
4044
  "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
4045
  "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
 
4046
  "license": "MIT",
4047
  "engines": {
4048
  "node": ">= 0.4"
 
4121
  "version": "1.1.0",
4122
  "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
4123
  "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
 
4124
  "license": "MIT",
4125
  "engines": {
4126
  "node": ">= 0.4"
 
4133
  "version": "1.0.2",
4134
  "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
4135
  "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
 
4136
  "license": "MIT",
4137
  "dependencies": {
4138
  "has-symbols": "^1.0.3"
 
4148
  "version": "2.0.2",
4149
  "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
4150
  "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
 
4151
  "license": "MIT",
4152
  "dependencies": {
4153
  "function-bind": "^1.1.2"
 
4173
  "hermes-estree": "0.25.1"
4174
  }
4175
  },
4176
+ "node_modules/html-to-image": {
4177
+ "version": "1.11.13",
4178
+ "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
4179
+ "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
4180
+ "license": "MIT"
4181
+ },
4182
  "node_modules/ignore": {
4183
  "version": "5.3.2",
4184
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
 
4692
  "version": "4.0.0",
4693
  "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
4694
  "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
 
4695
  "license": "MIT"
4696
  },
4697
  "node_modules/js-yaml": {
 
5102
  "version": "1.4.0",
5103
  "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
5104
  "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
 
5105
  "license": "MIT",
5106
  "dependencies": {
5107
  "js-tokens": "^3.0.0 || ^4.0.0"
 
5120
  "yallist": "^3.0.2"
5121
  }
5122
  },
5123
+ "node_modules/lucide-react": {
5124
+ "version": "0.552.0",
5125
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.552.0.tgz",
5126
+ "integrity": "sha512-g9WCjmfwqbexSnZE+2cl21PCfXOcqnGeWeMTNAOGEfpPbm/ZF4YIq77Z8qWrxbu660EKuLB4nSLggoKnCb+isw==",
5127
+ "license": "ISC",
5128
+ "peerDependencies": {
5129
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
5130
+ }
5131
+ },
5132
  "node_modules/magic-string": {
5133
  "version": "0.30.21",
5134
  "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
 
5143
  "version": "1.1.0",
5144
  "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
5145
  "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
 
5146
  "license": "MIT",
5147
  "engines": {
5148
  "node": ">= 0.4"
 
5172
  "node": ">=8.6"
5173
  }
5174
  },
5175
+ "node_modules/mime-db": {
5176
+ "version": "1.52.0",
5177
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
5178
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
5179
+ "license": "MIT",
5180
+ "engines": {
5181
+ "node": ">= 0.6"
5182
+ }
5183
+ },
5184
+ "node_modules/mime-types": {
5185
+ "version": "2.1.35",
5186
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
5187
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
5188
+ "license": "MIT",
5189
+ "dependencies": {
5190
+ "mime-db": "1.52.0"
5191
+ },
5192
+ "engines": {
5193
+ "node": ">= 0.6"
5194
+ }
5195
+ },
5196
  "node_modules/minimatch": {
5197
  "version": "3.1.2",
5198
  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
 
5355
  "version": "4.1.1",
5356
  "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
5357
  "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
 
5358
  "license": "MIT",
5359
  "engines": {
5360
  "node": ">=0.10.0"
 
5653
  "version": "15.8.1",
5654
  "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
5655
  "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
 
5656
  "license": "MIT",
5657
  "dependencies": {
5658
  "loose-envify": "^1.4.0",
 
5660
  "react-is": "^16.13.1"
5661
  }
5662
  },
5663
+ "node_modules/proxy-from-env": {
5664
+ "version": "1.1.0",
5665
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
5666
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
5667
+ "license": "MIT"
5668
+ },
5669
  "node_modules/punycode": {
5670
  "version": "2.3.1",
5671
  "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
 
5676
  "node": ">=6"
5677
  }
5678
  },
5679
+ "node_modules/qr-code-styling": {
5680
+ "version": "1.9.2",
5681
+ "resolved": "https://registry.npmjs.org/qr-code-styling/-/qr-code-styling-1.9.2.tgz",
5682
+ "integrity": "sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==",
5683
+ "license": "MIT",
5684
+ "dependencies": {
5685
+ "qrcode-generator": "^1.4.4"
5686
+ },
5687
+ "engines": {
5688
+ "node": ">=18.18.0"
5689
+ }
5690
+ },
5691
+ "node_modules/qr-code-styling/node_modules/qrcode-generator": {
5692
+ "version": "1.5.2",
5693
+ "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.5.2.tgz",
5694
+ "integrity": "sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==",
5695
+ "license": "MIT"
5696
+ },
5697
+ "node_modules/qr.js": {
5698
+ "version": "0.0.0",
5699
+ "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz",
5700
+ "integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==",
5701
+ "license": "MIT"
5702
+ },
5703
+ "node_modules/qrcode-generator": {
5704
+ "version": "2.0.4",
5705
+ "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-2.0.4.tgz",
5706
+ "integrity": "sha512-mZSiP6RnbHl4xL2Ap5HfkjLnmxfKcPWpWe/c+5XxCuetEenqmNFf1FH/ftXPCtFG5/TDobjsjz6sSNL0Sr8Z9g==",
5707
+ "license": "MIT"
5708
+ },
5709
  "node_modules/queue-microtask": {
5710
  "version": "1.2.3",
5711
  "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
 
5752
  "version": "16.13.1",
5753
  "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
5754
  "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
 
5755
  "license": "MIT"
5756
  },
5757
+ "node_modules/react-qr-code": {
5758
+ "version": "2.0.18",
5759
+ "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.18.tgz",
5760
+ "integrity": "sha512-v1Jqz7urLMhkO6jkgJuBYhnqvXagzceg3qJUWayuCK/c6LTIonpWbwxR1f1APGd4xrW/QcQEovNrAojbUz65Tg==",
5761
+ "license": "MIT",
5762
+ "dependencies": {
5763
+ "prop-types": "^15.8.1",
5764
+ "qr.js": "0.0.0"
5765
+ },
5766
+ "peerDependencies": {
5767
+ "react": "*"
5768
+ }
5769
+ },
5770
  "node_modules/reflect.getprototypeof": {
5771
  "version": "1.0.10",
5772
  "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
 
6377
  "url": "https://github.com/sponsors/ljharb"
6378
  }
6379
  },
6380
+ "node_modules/tailwind-merge": {
6381
+ "version": "3.3.1",
6382
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
6383
+ "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
6384
+ "license": "MIT",
6385
+ "funding": {
6386
+ "type": "github",
6387
+ "url": "https://github.com/sponsors/dcastil"
6388
+ }
6389
+ },
6390
  "node_modules/tailwindcss": {
6391
  "version": "4.1.16",
6392
  "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
 
6514
  "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
6515
  "license": "0BSD"
6516
  },
6517
+ "node_modules/tw-animate-css": {
6518
+ "version": "1.4.0",
6519
+ "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
6520
+ "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==",
6521
+ "dev": true,
6522
+ "license": "MIT",
6523
+ "funding": {
6524
+ "url": "https://github.com/sponsors/Wombosvideo"
6525
+ }
6526
+ },
6527
  "node_modules/type-check": {
6528
  "version": "0.4.0",
6529
  "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
 
6755
  "punycode": "^2.1.0"
6756
  }
6757
  },
6758
+ "node_modules/use-sync-external-store": {
6759
+ "version": "1.6.0",
6760
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
6761
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
6762
+ "license": "MIT",
6763
+ "peerDependencies": {
6764
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
6765
+ }
6766
+ },
6767
  "node_modules/which": {
6768
  "version": "2.0.2",
6769
  "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
package.json CHANGED
@@ -9,18 +9,32 @@
9
  "lint": "eslint"
10
  },
11
  "dependencies": {
 
 
 
 
 
 
 
 
 
 
 
12
  "react": "19.2.0",
13
  "react-dom": "19.2.0",
14
- "next": "16.0.1"
 
15
  },
16
  "devDependencies": {
17
- "typescript": "^5",
 
18
  "@types/node": "^20",
19
  "@types/react": "^19",
20
  "@types/react-dom": "^19",
21
- "@tailwindcss/postcss": "^4",
22
- "tailwindcss": "^4",
23
  "eslint": "^9",
24
- "eslint-config-next": "16.0.1"
 
 
 
25
  }
26
- }
 
9
  "lint": "eslint"
10
  },
11
  "dependencies": {
12
+ "@radix-ui/react-avatar": "^1.1.10",
13
+ "@radix-ui/react-slot": "^1.2.3",
14
+ "axios": "^1.13.1",
15
+ "class-variance-authority": "^0.7.1",
16
+ "clsx": "^2.1.1",
17
+ "file-saver": "^2.0.5",
18
+ "html-to-image": "^1.11.13",
19
+ "lucide-react": "^0.552.0",
20
+ "next": "16.0.1",
21
+ "qr-code-styling": "^1.9.2",
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
  },
28
  "devDependencies": {
29
+ "@tailwindcss/postcss": "^4",
30
+ "@types/file-saver": "^2.0.7",
31
  "@types/node": "^20",
32
  "@types/react": "^19",
33
  "@types/react-dom": "^19",
 
 
34
  "eslint": "^9",
35
+ "eslint-config-next": "16.0.1",
36
+ "tailwindcss": "^4",
37
+ "tw-animate-css": "^1.4.0",
38
+ "typescript": "^5"
39
  }
40
+ }
styles/qr-generator.css ADDED
@@ -0,0 +1,584 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .qr-generator {
2
+ min-height: 100vh;
3
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
4
+ padding: 2rem;
5
+ display: flex;
6
+ flex-direction: column;
7
+ align-items: center;
8
+ }
9
+
10
+ .input-section {
11
+ width: 100%;
12
+ max-width: 800px;
13
+ background: white;
14
+ border-radius: 20px;
15
+ padding: 3rem;
16
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
17
+ margin-bottom: 2rem;
18
+ }
19
+
20
+ .title {
21
+ font-size: 2.5rem;
22
+ font-weight: 800;
23
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
24
+ -webkit-background-clip: text;
25
+ -webkit-text-fill-color: transparent;
26
+ background-clip: text;
27
+ text-align: center;
28
+ margin-bottom: 0.5rem;
29
+ }
30
+
31
+ .subtitle {
32
+ text-align: center;
33
+ color: #6b7280;
34
+ font-size: 1.1rem;
35
+ margin-bottom: 2rem;
36
+ }
37
+
38
+ .input-group {
39
+ display: flex;
40
+ gap: 1rem;
41
+ margin-bottom: 1rem;
42
+ }
43
+
44
+ .url-input {
45
+ flex: 1;
46
+ padding: 1rem 1.5rem;
47
+ font-size: 1rem;
48
+ border: 2px solid #e5e7eb;
49
+ border-radius: 12px;
50
+ transition: all 0.3s ease;
51
+ outline: none;
52
+ }
53
+
54
+ .url-input:focus {
55
+ border-color: #667eea;
56
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
57
+ }
58
+
59
+ .generate-btn {
60
+ padding: 1rem 2rem;
61
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
62
+ color: white;
63
+ border: none;
64
+ border-radius: 12px;
65
+ font-size: 1rem;
66
+ font-weight: 600;
67
+ cursor: pointer;
68
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
69
+ white-space: nowrap;
70
+ }
71
+
72
+ .generate-btn:hover:not(:disabled) {
73
+ transform: translateY(-2px);
74
+ box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
75
+ }
76
+
77
+ .generate-btn:disabled {
78
+ opacity: 0.6;
79
+ cursor: not-allowed;
80
+ }
81
+
82
+ .error-message {
83
+ background: #fee2e2;
84
+ color: #dc2626;
85
+ padding: 0.75rem 1rem;
86
+ border-radius: 8px;
87
+ margin-top: 1rem;
88
+ font-size: 0.95rem;
89
+ }
90
+
91
+ .result-section {
92
+ width: 100%;
93
+ max-width: 800px;
94
+ background: white;
95
+ border-radius: 20px;
96
+ padding: 3rem;
97
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
98
+ animation: slideUp 0.5s ease;
99
+ }
100
+
101
+ @keyframes slideUp {
102
+ from {
103
+ opacity: 0;
104
+ transform: translateY(20px);
105
+ }
106
+ to {
107
+ opacity: 1;
108
+ transform: translateY(0);
109
+ }
110
+ }
111
+
112
+ .profile-info {
113
+ display: flex;
114
+ align-items: center;
115
+ gap: 1.5rem;
116
+ margin-bottom: 2rem;
117
+ padding-bottom: 2rem;
118
+ border-bottom: 2px solid #f3f4f6;
119
+ }
120
+
121
+ .profile-avatar {
122
+ width: 80px;
123
+ height: 80px;
124
+ border-radius: 50%;
125
+ object-fit: cover;
126
+ border: 3px solid #667eea;
127
+ }
128
+
129
+ .profile-details h2 {
130
+ font-size: 1.5rem;
131
+ font-weight: 700;
132
+ color: #1f2937;
133
+ margin-bottom: 0.25rem;
134
+ }
135
+
136
+ .profile-username {
137
+ color: #6b7280;
138
+ font-size: 1rem;
139
+ margin-bottom: 0.5rem;
140
+ }
141
+
142
+ .profile-link {
143
+ color: #667eea;
144
+ text-decoration: none;
145
+ font-weight: 500;
146
+ display: inline-flex;
147
+ align-items: center;
148
+ gap: 0.25rem;
149
+ transition: gap 0.2s ease;
150
+ }
151
+
152
+ .profile-link:hover {
153
+ gap: 0.5rem;
154
+ }
155
+
156
+ .qr-code-section {
157
+ display: flex;
158
+ justify-content: center;
159
+ margin: 2rem 0;
160
+ padding: 0;
161
+ }
162
+
163
+ .qr-code-wrapper {
164
+ background: white;
165
+ border-radius: 16px;
166
+ overflow: hidden;
167
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
168
+ }
169
+
170
+ .qr-code-container {
171
+ background: white;
172
+ padding: 2rem;
173
+ display: flex;
174
+ justify-content: center;
175
+ align-items: center;
176
+ }
177
+
178
+ .profile-bar {
179
+ display: flex;
180
+ align-items: center;
181
+ gap: 1rem;
182
+ padding: 1.5rem 2rem;
183
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
184
+ color: white;
185
+ }
186
+
187
+ .profile-bar-avatar {
188
+ width: 50px;
189
+ height: 50px;
190
+ border-radius: 50%;
191
+ border: 3px solid white;
192
+ object-fit: cover;
193
+ }
194
+
195
+ .profile-bar-info {
196
+ flex: 1;
197
+ }
198
+
199
+ .profile-bar-name {
200
+ font-size: 1.1rem;
201
+ font-weight: 700;
202
+ margin: 0 0 0.25rem 0;
203
+ color: white;
204
+ }
205
+
206
+ .profile-bar-username {
207
+ font-size: 0.9rem;
208
+ opacity: 0.9;
209
+ margin: 0;
210
+ color: white;
211
+ }
212
+
213
+ .profile-bar-details {
214
+ display: flex;
215
+ align-items: center;
216
+ gap: 0.75rem;
217
+ flex-wrap: wrap;
218
+ }
219
+
220
+ .resource-type-badge {
221
+ display: inline-flex;
222
+ align-items: center;
223
+ padding: 0.2rem 0.5rem;
224
+ background: rgba(255, 255, 255, 0.2);
225
+ border-radius: 6px;
226
+ font-size: 0.8rem;
227
+ font-weight: 600;
228
+ color: white;
229
+ text-transform: capitalize;
230
+ }
231
+
232
+ .resource-name {
233
+ font-size: 0.9rem;
234
+ color: white;
235
+ font-weight: 500;
236
+ overflow: hidden;
237
+ text-overflow: ellipsis;
238
+ white-space: nowrap;
239
+ max-width: 200px;
240
+ }
241
+
242
+ .actions-section {
243
+ display: grid;
244
+ grid-template-columns: 1fr 1fr;
245
+ gap: 2rem;
246
+ margin-top: 2rem;
247
+ }
248
+
249
+ .download-actions h3,
250
+ .share-actions h3 {
251
+ font-size: 1.1rem;
252
+ font-weight: 600;
253
+ color: #374151;
254
+ margin-bottom: 1rem;
255
+ }
256
+
257
+ .button-group {
258
+ display: flex;
259
+ gap: 1rem;
260
+ flex-wrap: wrap;
261
+ }
262
+
263
+ .download-btn {
264
+ flex: 1;
265
+ padding: 0.75rem 1.5rem;
266
+ background: #6b7280;
267
+ color: white;
268
+ border: none;
269
+ border-radius: 8px;
270
+ font-weight: 500;
271
+ cursor: pointer;
272
+ transition: all 0.2s ease;
273
+ min-width: 120px;
274
+ }
275
+
276
+ .download-btn:hover {
277
+ background: #4b5563;
278
+ transform: translateY(-1px);
279
+ }
280
+
281
+ .download-btn.primary {
282
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
283
+ }
284
+
285
+ .download-btn.primary:hover {
286
+ background: linear-gradient(135deg, #5a67d8 0%, #6b42a0 100%);
287
+ transform: translateY(-1px);
288
+ }
289
+
290
+ .download-btn.secondary {
291
+ background: #6b7280;
292
+ }
293
+
294
+ .download-btn.secondary:hover {
295
+ background: #4b5563;
296
+ }
297
+
298
+ .share-btn {
299
+ flex: 1;
300
+ padding: 0.75rem 1rem;
301
+ border: none;
302
+ border-radius: 8px;
303
+ font-weight: 500;
304
+ cursor: pointer;
305
+ transition: all 0.2s ease;
306
+ color: white;
307
+ min-width: 100px;
308
+ font-size: 0.9rem;
309
+ }
310
+
311
+ .share-btn.twitter {
312
+ background: #1DA1F2;
313
+ }
314
+
315
+ .share-btn.twitter:hover {
316
+ background: #1a91da;
317
+ }
318
+
319
+ .share-btn.facebook {
320
+ background: #1877F2;
321
+ }
322
+
323
+ .share-btn.facebook:hover {
324
+ background: #166fe5;
325
+ }
326
+
327
+ .share-btn.linkedin {
328
+ background: #0A66C2;
329
+ }
330
+
331
+ .share-btn.linkedin:hover {
332
+ background: #095196;
333
+ }
334
+
335
+ /* Mobile responsiveness */
336
+ @media (max-width: 768px) {
337
+ .qr-generator {
338
+ padding: 1rem;
339
+ }
340
+
341
+ .input-section,
342
+ .result-section {
343
+ padding: 2rem 1.5rem;
344
+ }
345
+
346
+ .title {
347
+ font-size: 1.8rem;
348
+ }
349
+
350
+ .subtitle {
351
+ font-size: 1rem;
352
+ }
353
+
354
+ .input-group {
355
+ flex-direction: column;
356
+ }
357
+
358
+ .url-input,
359
+ .generate-btn {
360
+ width: 100%;
361
+ }
362
+
363
+ .profile-info {
364
+ flex-direction: column;
365
+ text-align: center;
366
+ }
367
+
368
+ .qr-code-wrapper {
369
+ width: 100%;
370
+ }
371
+
372
+ .profile-bar {
373
+ padding: 1rem 1.5rem;
374
+ }
375
+
376
+ .profile-bar-avatar {
377
+ width: 40px;
378
+ height: 40px;
379
+ }
380
+
381
+ .profile-bar-name {
382
+ font-size: 1rem;
383
+ }
384
+
385
+ .profile-bar-username {
386
+ font-size: 0.85rem;
387
+ }
388
+
389
+ .resource-type-badge {
390
+ font-size: 0.75rem;
391
+ padding: 0.15rem 0.4rem;
392
+ }
393
+
394
+ .resource-name {
395
+ font-size: 0.85rem;
396
+ max-width: 150px;
397
+ }
398
+
399
+ .actions-section {
400
+ grid-template-columns: 1fr;
401
+ gap: 1.5rem;
402
+ }
403
+
404
+ .button-group {
405
+ flex-direction: column;
406
+ }
407
+
408
+ .download-btn,
409
+ .share-btn {
410
+ width: 100%;
411
+ }
412
+ }
413
+
414
+ /* ===== New UI (Screenshot-inspired) ===== */
415
+ .hf-card {
416
+ width: 100%;
417
+ max-width: 720px;
418
+ background: #ffffff;
419
+ border-radius: 20px;
420
+ padding: 2.25rem;
421
+ box-shadow: 0 14px 40px rgba(0,0,0,0.08);
422
+ margin-bottom: 2rem;
423
+ }
424
+
425
+ .hf-header {
426
+ display: flex;
427
+ align-items: center;
428
+ gap: 0.75rem;
429
+ margin-bottom: 1.25rem;
430
+ }
431
+
432
+ .hf-emoji {
433
+ font-size: 2rem;
434
+ }
435
+
436
+ .hf-title-wrap { display:flex; flex-direction: column; }
437
+ .hf-title {
438
+ font-size: 1.5rem;
439
+ font-weight: 800;
440
+ color: #111827;
441
+ }
442
+ .hf-subtitle { color:#6b7280; font-size: 0.95rem; }
443
+
444
+ .hf-form { display:flex; flex-direction: column; gap: 1rem; }
445
+ .hf-field { display:flex; flex-direction: column; gap: 0.4rem; }
446
+ .hf-label { font-size: 0.75rem; letter-spacing: .05em; color:#6b7280; font-weight: 700; }
447
+
448
+ .hf-input-wrap { position: relative; display:flex; align-items: center; }
449
+ .hf-input {
450
+ width: 100%;
451
+ padding: 0.9rem 2.75rem 0.9rem 1rem;
452
+ border: 1.5px solid #e5e7eb;
453
+ border-radius: 12px;
454
+ font-size: 0.95rem;
455
+ }
456
+ .hf-input:focus { outline: none; border-color: #10b981; box-shadow: 0 0 0 3px rgba(16,185,129,.15); }
457
+ .hf-valid {
458
+ position: absolute;
459
+ right: 0.75rem;
460
+ color: #10b981;
461
+ opacity: 0;
462
+ transition: opacity .2s ease;
463
+ font-weight: 900;
464
+ }
465
+ .hf-valid.active { opacity: 1; }
466
+
467
+ .hf-select {
468
+ width: 220px;
469
+ padding: 0.7rem 0.85rem;
470
+ border: 1.5px solid #e5e7eb;
471
+ border-radius: 12px;
472
+ font-size: 0.95rem;
473
+ background: #fff;
474
+ }
475
+
476
+ .hf-cta {
477
+ align-self: flex-start;
478
+ margin-top: 0.25rem;
479
+ padding: 0.85rem 1.25rem;
480
+ border-radius: 9999px;
481
+ border: none;
482
+ background: #111827;
483
+ color: #fff;
484
+ font-weight: 600;
485
+ cursor: pointer;
486
+ }
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: 52px 18px 26px;
498
+ box-shadow: 0 12px 28px rgba(0,0,0,.15);
499
+ }
500
+
501
+ .qr-card-v2 {
502
+ position: relative;
503
+ background: #fff;
504
+ border-radius: 22px;
505
+ padding: 56px 18px 22px;
506
+ box-shadow: 0 8px 18px rgba(0,0,0,.08);
507
+ }
508
+
509
+ .qr-avatar-wrap {
510
+ position: absolute;
511
+ top: -28px;
512
+ left: 50%;
513
+ transform: translateX(-50%);
514
+ width: 56px; height: 56px;
515
+ border-radius: 9999px;
516
+ border: 3px solid #fff;
517
+ overflow: hidden;
518
+ box-shadow: 0 6px 12px rgba(0,0,0,.12);
519
+ }
520
+ .qr-avatar { width:100%; height:100%; object-fit: cover; }
521
+
522
+ .qr-card-inner { display:flex; flex-direction: column; align-items:center; gap: 12px; }
523
+ .qr-name { font-weight: 700; color:#111827; margin-top: -6px; }
524
+ .qr-code-holder { padding: 6px 0 2px; }
525
+ .qr-caption { color:#9ca3af; font-size: .8rem; text-align:center; }
526
+ .qr-brand { display:flex; align-items:center; gap:8px; color:#111827; font-weight:700; }
527
+ .qr-brand img { width:18px; height:18px; }
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: 48px 14px 22px; border-radius: 28px; }
532
+ .qr-card-v2 { padding: 52px 14px 18px; border-radius: 20px; }
533
+ }
534
+
535
+ /* Top icons (back + menu) */
536
+ .qr-topbar {
537
+ position: absolute;
538
+ top: 10px;
539
+ left: 10px;
540
+ right: 10px;
541
+ display: flex;
542
+ align-items: center;
543
+ justify-content: space-between;
544
+ color: rgba(255,255,255,0.95);
545
+ }
546
+ .qr-topbar button { background: transparent; border: 0; color: inherit; padding: 4px; cursor: default; }
547
+
548
+ /* Bottom share sheet */
549
+ .qr-share-sheet {
550
+ position: absolute;
551
+ left: 8px;
552
+ right: 8px;
553
+ bottom: 4px;
554
+ background: #fff;
555
+ border-top-left-radius: 18px;
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 {
566
+ position: absolute;
567
+ right: 10px;
568
+ top: 8px;
569
+ border: 0;
570
+ background: transparent;
571
+ color: #111827;
572
+ opacity: .7;
573
+ font-size: 18px;
574
+ }
575
+
576
+ .qr-download { display:flex; flex-direction: column; align-items: center; gap: 6px; }
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:flex; flex-direction: column; gap: 6px; }
582
+ .qr-share-label { font-size: 12px; color:#6b7280; margin-left: 8px; }
583
+ .qr-share-actions { display:flex; gap: 14px; align-items:center; }
584
+ .qr-share-texts { display:flex; gap: 18px; color:#6b7280; font-size: 12px; margin-left: 6px; }
types/html-to-image.d.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ declare module 'html-to-image' {
2
+ export interface Options {
3
+ quality?: number;
4
+ backgroundColor?: string;
5
+ pixelRatio?: number;
6
+ style?: Partial<CSSStyleDeclaration>;
7
+ filter?: (node: HTMLElement) => boolean;
8
+ width?: number;
9
+ height?: number;
10
+ }
11
+
12
+ export function toPng(node: HTMLElement, options?: Options): Promise<string>;
13
+ export function toJpeg(node: HTMLElement, options?: Options): Promise<string>;
14
+ export function toBlob(node: HTMLElement, options?: Options): Promise<Blob>;
15
+ export function toSvg(node: HTMLElement, options?: Options): Promise<string>;
16
+ export function toCanvas(node: HTMLElement, options?: Options): Promise<HTMLCanvasElement>;
17
+ }
types/qr-code-styling.d.ts ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ declare module 'qr-code-styling' {
2
+ export interface Options {
3
+ width?: number;
4
+ height?: number;
5
+ type?: 'canvas' | 'svg';
6
+ data?: string;
7
+ image?: string;
8
+ margin?: number;
9
+ qrOptions?: {
10
+ typeNumber?: number;
11
+ mode?: 'Numeric' | 'Alphanumeric' | 'Byte' | 'Kanji';
12
+ errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H';
13
+ };
14
+ imageOptions?: {
15
+ hideBackgroundDots?: boolean;
16
+ imageSize?: number;
17
+ margin?: number;
18
+ crossOrigin?: string;
19
+ };
20
+ dotsOptions?: {
21
+ color?: string;
22
+ type?: 'rounded' | 'dots' | 'classy' | 'classy-rounded' | 'square' | 'extra-rounded';
23
+ };
24
+ backgroundOptions?: {
25
+ color?: string;
26
+ };
27
+ cornersSquareOptions?: {
28
+ color?: string;
29
+ type?: 'dot' | 'square' | 'extra-rounded';
30
+ };
31
+ cornersDotOptions?: {
32
+ color?: string;
33
+ type?: 'dot' | 'square';
34
+ };
35
+ }
36
+
37
+ export default class QRCodeStyling {
38
+ constructor(options?: Options);
39
+ append(container: HTMLElement): void;
40
+ download(options?: {
41
+ name?: string;
42
+ extension?: 'png' | 'jpeg' | 'webp' | 'svg';
43
+ }): Promise<Blob>;
44
+ update(options?: Options): void;
45
+ }
46
+ }