Reubencf commited on
Commit
26cefad
·
1 Parent(s): fa4d7a4
.claude/settings.local.json CHANGED
@@ -7,7 +7,8 @@
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": []
 
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
+ "Bash(npm run dev:*)"
12
  ],
13
  "deny": [],
14
  "ask": []
.dockerignore ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules
3
+ npm-debug.log
4
+ yarn-error.log
5
+ yarn.lock
6
+ package-lock.json
7
+
8
+ # Next.js build output
9
+ .next
10
+ out
11
+
12
+ # Production
13
+ build
14
+ dist
15
+
16
+ # Misc
17
+ .DS_Store
18
+ *.pem
19
+ .env*.local
20
+
21
+ # Debug
22
+ npm-debug.log*
23
+ yarn-debug.log*
24
+ yarn-error.log*
25
+
26
+ # Vercel
27
+ .vercel
28
+
29
+ # Typescript
30
+ *.tsbuildinfo
31
+
32
+ # Git
33
+ .git
34
+ .gitignore
35
+
36
+ # Documentation
37
+ README.md
38
+ docs
39
+
40
+ # Testing
41
+ coverage
42
+ .nyc_output
43
+
44
+ # Editor directories and files
45
+ .idea
46
+ .vscode
47
+ *.swp
48
+ *.swo
49
+ *~
50
+
51
+ # OS files
52
+ Thumbs.db
Dockerfile ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Node.js 20 Alpine as base image
2
+ FROM node:20-alpine AS base
3
+
4
+ # Install dependencies only when needed
5
+ FROM base AS deps
6
+ # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
7
+ RUN apk add --no-cache libc6-compat
8
+ WORKDIR /app
9
+
10
+ # Install dependencies based on the preferred package manager
11
+ COPY package.json package-lock.json* ./
12
+ RUN npm ci --only=production && \
13
+ npm ci --only=development
14
+
15
+ # Rebuild the source code only when needed
16
+ FROM base AS builder
17
+ WORKDIR /app
18
+ COPY --from=deps /app/node_modules ./node_modules
19
+ COPY . .
20
+
21
+ # Next.js collects completely anonymous telemetry data about general usage.
22
+ # Learn more here: https://nextjs.org/telemetry
23
+ # Uncomment the following line in case you want to disable telemetry during the build.
24
+ ENV NEXT_TELEMETRY_DISABLED 1
25
+
26
+ # Build the Next.js application
27
+ RUN npm run build
28
+
29
+ # Production image, copy all the files and run next
30
+ FROM base AS runner
31
+ WORKDIR /app
32
+
33
+ ENV NODE_ENV production
34
+ # Uncomment the following line in case you want to disable telemetry during runtime.
35
+ ENV NEXT_TELEMETRY_DISABLED 1
36
+
37
+ # Create a non-root user
38
+ RUN addgroup --system --gid 1001 nodejs
39
+ RUN adduser --system --uid 1001 nextjs
40
+
41
+ # Copy built application
42
+ COPY --from=builder /app/public ./public
43
+ COPY --from=builder /app/.next/standalone ./
44
+ COPY --from=builder /app/.next/static ./.next/static
45
+
46
+ # Set the correct permission for prerender cache
47
+ RUN mkdir -p .next
48
+ RUN chown nextjs:nodejs .next
49
+
50
+ # Switch to the non-root user
51
+ USER nextjs
52
+
53
+ # Expose port 7860 (Hugging Face Spaces default)
54
+ EXPOSE 7860
55
+
56
+ # Set environment variable for the port
57
+ ENV PORT 7860
58
+ ENV HOSTNAME "0.0.0.0"
59
+
60
+ # Start the application
61
+ CMD ["node", "server.js"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Hugging Face QR Code Generator
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,36 +1,156 @@
1
- This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
 
 
 
 
 
 
 
 
 
2
 
3
- ## Getting Started
4
 
5
- First, run the development server:
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  ```bash
8
  npm run dev
9
- # or
10
- yarn dev
11
- # or
12
- pnpm dev
13
- # or
14
- bun dev
15
  ```
16
 
17
- Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
- You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
 
 
 
 
20
 
21
- This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
 
23
- ## Learn More
24
 
25
- To learn more about Next.js, take a look at the following resources:
26
 
27
- - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
- - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
 
 
29
 
30
- You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
 
32
- ## Deploy on Vercel
 
 
33
 
34
- The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
 
36
- Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
 
1
+ ---
2
+ title: Hugging Face QR Code Generator
3
+ emoji: 🤗
4
+ colorFrom: yellow
5
+ colorTo: orange
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ app_port: 7860
10
+ ---
11
 
12
+ # Hugging Face QR Code Generator 🤗
13
 
14
+ A beautiful and responsive web application that generates QR codes for Hugging Face profiles, models, datasets, and spaces. Create stunning, shareable QR codes with custom designs that perfectly represent your Hugging Face identity.
15
 
16
+ ## Features ✨
17
+
18
+ - **Profile QR Codes**: Generate QR codes for any Hugging Face user profile
19
+ - **Resource Support**: Create QR codes for models, datasets, and spaces
20
+ - **Beautiful Design**: Modern, clean interface with animated gradients
21
+ - **Responsive**: Works perfectly on all devices - desktop, tablet, and mobile
22
+ - **Multiple Export Options**: Download as image, copy to clipboard, or share directly
23
+ - **Social Sharing**: Built-in sharing to Twitter/X, LinkedIn, and Facebook
24
+ - **Dark Mode Support**: Automatically adapts to your system preferences
25
+
26
+ ## How to Use 📖
27
+
28
+ 1. **Enter a Hugging Face URL or username**:
29
+ - Full URL: `https://huggingface.co/username`
30
+ - Just username: `username`
31
+ - Model URL: `https://huggingface.co/username/model-name`
32
+ - Dataset URL: `https://huggingface.co/datasets/username/dataset-name`
33
+ - Space URL: `https://huggingface.co/spaces/username/space-name`
34
+
35
+ 2. **Click "Generate QR Code"** to create your custom QR code
36
+
37
+ 3. **Customize and Share**:
38
+ - Click the background to cycle through different gradient colors
39
+ - Download the complete design or just the QR code
40
+ - Copy to clipboard for easy sharing
41
+ - Share directly to social media
42
+
43
+ ## Technology Stack 🛠️
44
+
45
+ - **Framework**: Next.js 16 with App Router
46
+ - **Language**: TypeScript
47
+ - **Styling**: Tailwind CSS + Custom CSS
48
+ - **UI Components**: shadcn/ui
49
+ - **QR Generation**: qrcode.js
50
+ - **Image Export**: html-to-image
51
+ - **Icons**: Lucide React + React Icons
52
+
53
+ ## Local Development 💻
54
+
55
+ ### Prerequisites
56
+ - Node.js 20 or higher
57
+ - npm or yarn package manager
58
+
59
+ ### Installation
60
+
61
+ 1. Clone the repository:
62
+ ```bash
63
+ git clone https://github.com/yourusername/huggingface-qr-generator.git
64
+ cd huggingface-qr-generator
65
+ ```
66
+
67
+ 2. Install dependencies:
68
+ ```bash
69
+ npm install
70
+ ```
71
+
72
+ 3. Run the development server:
73
  ```bash
74
  npm run dev
 
 
 
 
 
 
75
  ```
76
 
77
+ 4. Open [http://localhost:3000](http://localhost:3000) in your browser
78
+
79
+ ### Build for Production
80
+
81
+ ```bash
82
+ npm run build
83
+ npm run start
84
+ ```
85
+
86
+ ## Docker Deployment 🐳
87
+
88
+ ### Build and Run with Docker
89
+
90
+ 1. Build the Docker image:
91
+ ```bash
92
+ docker build -t hf-qr-generator .
93
+ ```
94
+
95
+ 2. Run the container:
96
+ ```bash
97
+ docker run -p 7860:7860 hf-qr-generator
98
+ ```
99
+
100
+ 3. Access the application at [http://localhost:7860](http://localhost:7860)
101
+
102
+ ### Deploy to Hugging Face Spaces
103
+
104
+ This application is configured for easy deployment to Hugging Face Spaces:
105
+
106
+ 1. Create a new Space on Hugging Face
107
+ 2. Choose "Docker" as the SDK
108
+ 3. Push this repository to your Space
109
+ 4. The application will automatically build and deploy
110
+
111
+ ## Environment Variables 🔐
112
+
113
+ No environment variables are required for basic functionality. The application uses Next.js API routes to handle CORS and proxy requests.
114
+
115
+ ## API Routes 🌐
116
+
117
+ - `/api/huggingface` - Fetches Hugging Face profile data
118
+ - `/api/proxy-image` - Proxies avatar images to avoid CORS issues
119
+
120
+ ## Browser Support 🌍
121
+
122
+ - Chrome/Edge (latest 2 versions)
123
+ - Firefox (latest 2 versions)
124
+ - Safari (latest 2 versions)
125
+ - Mobile browsers (iOS Safari, Chrome Mobile)
126
+
127
+ ## Contributing 🤝
128
+
129
+ Contributions are welcome! Please feel free to submit a Pull Request.
130
 
131
+ 1. Fork the project
132
+ 2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
133
+ 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
134
+ 4. Push to the branch (`git push origin feature/AmazingFeature`)
135
+ 5. Open a Pull Request
136
 
137
+ ## License 📄
138
 
139
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
140
 
141
+ ## Acknowledgments 🙏
142
 
143
+ - Hugging Face for the amazing platform and API
144
+ - shadcn/ui for the beautiful UI components
145
+ - The Next.js team for the excellent framework
146
+ - All contributors and users of this application
147
 
148
+ ## Support 💬
149
 
150
+ If you encounter any issues or have questions:
151
+ - Open an issue on GitHub
152
+ - Contact through Hugging Face Spaces discussions
153
 
154
+ ---
155
 
156
+ Made with ❤️ for the Hugging Face community
app/api/huggingface/route.js CHANGED
@@ -27,11 +27,13 @@ export async function POST(request) {
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
 
27
 
28
  // Extract the CDN avatar URL from the HTML
29
  // Look for pattern: https://cdn-avatars.huggingface.co/...
30
+ // Make sure to stop at the first quote, space, or HTML entity
31
+ const avatarMatch = html.match(/https:\/\/cdn-avatars\.huggingface\.co\/[^"'\s&<>]+?\.(png|jpg|jpeg|webp)/);
32
 
33
  let avatarUrl = null;
34
  if (avatarMatch && avatarMatch[0]) {
35
+ // Clean the URL - remove any HTML entities or extra characters
36
+ avatarUrl = avatarMatch[0].split('&')[0].split('"')[0].split("'")[0];
37
  }
38
 
39
  // If no CDN avatar found, try to find any avatar image
app/api/proxy-image/route.js CHANGED
@@ -4,7 +4,7 @@ import axios from 'axios';
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(
@@ -13,12 +13,30 @@ export async function GET(request) {
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
@@ -35,10 +53,21 @@ export async function GET(request) {
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
  }
 
4
  export async function GET(request) {
5
  try {
6
  const { searchParams } = new URL(request.url);
7
+ let imageUrl = searchParams.get('url');
8
 
9
  if (!imageUrl) {
10
  return NextResponse.json(
 
13
  );
14
  }
15
 
16
+ // Decode the URL properly and clean it
17
+ imageUrl = decodeURIComponent(imageUrl);
18
+
19
+ // Remove any extra encoded characters or HTML entities that might have been appended
20
+ // Clean up common issues with malformed URLs
21
+ imageUrl = imageUrl.split('&quot')[0].split('&#')[0].split('"')[0].split("'")[0];
22
+
23
+ // Validate that it's a proper image URL
24
+ if (!imageUrl.match(/^https?:\/\/.+\.(png|jpg|jpeg|gif|webp|svg)$/i)) {
25
+ console.error('Invalid image URL format:', imageUrl);
26
+ return NextResponse.json(
27
+ { error: 'Invalid image URL format' },
28
+ { status: 400 }
29
+ );
30
+ }
31
+
32
  // Fetch the image
33
  const response = await axios.get(imageUrl, {
34
  responseType: 'arraybuffer',
35
  headers: {
36
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
37
+ 'Accept': 'image/*'
38
+ },
39
+ timeout: 10000 // 10 second timeout
40
  });
41
 
42
  // Get content type from response
 
53
  });
54
 
55
  } catch (error) {
56
+ console.error('Error proxying image:', {
57
+ message: error.message,
58
+ status: error.response?.status,
59
+ url: error.config?.url
60
+ });
61
+
62
+ // If it's a 403, try to return a default avatar
63
+ if (error.response?.status === 403 || error.response?.status === 404) {
64
+ // Return a redirect to the default Hugging Face logo
65
+ return NextResponse.redirect('https://huggingface.co/front/assets/huggingface_logo-noborder.svg');
66
+ }
67
+
68
  return NextResponse.json(
69
+ { error: 'Failed to fetch image', details: error.message },
70
+ { status: error.response?.status || 500 }
71
  );
72
  }
73
  }
app/page.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import HuggingFaceQRGenerator from '../components/HuggingFaceQRGenerator.tsx';
2
 
3
  export default function Home() {
4
  return <HuggingFaceQRGenerator />;
 
1
+ import HuggingFaceQRGenerator from '../components/HuggingFaceQRGenerator';
2
 
3
  export default function Home() {
4
  return <HuggingFaceQRGenerator />;
components/HuggingFaceQRGenerator.tsx CHANGED
@@ -11,7 +11,7 @@ import { Input } from '@/components/ui/input';
11
  import { Label } from '@/components/ui/label';
12
  import { Badge } from '@/components/ui/badge';
13
  import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
14
- import { Download, ChevronLeft, Copy } from 'lucide-react';
15
  import { FaFacebook, FaLinkedin } from 'react-icons/fa';
16
  import { FaSquareXTwitter } from 'react-icons/fa6';
17
 
@@ -23,12 +23,29 @@ const HuggingFaceQRGenerator = () => {
23
  const [qrCodeInstance, setQrCodeInstance] = useState<any>(null);
24
  const [showQR, setShowQR] = useState(false);
25
  const [gradientIndex, setGradientIndex] = useState(0);
 
 
 
26
  const gradients = [
27
- ['#f1c40f', '#f39c12'],
28
- ['#34d399', '#10b981'],
29
- ['#60a5fa', '#6366f1'],
30
- ['#fb7185', '#f472b6'],
31
- ['#f59e0b', '#ef4444']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  ];
33
 
34
  const handleGenerate = async () => {
@@ -84,6 +101,26 @@ const HuggingFaceQRGenerator = () => {
84
 
85
  const handleCycleBackground = () => {
86
  setGradientIndex((prev) => (prev + 1) % gradients.length);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  };
88
 
89
  const phoneRef = useRef<HTMLDivElement | null>(null);
@@ -246,22 +283,22 @@ const HuggingFaceQRGenerator = () => {
246
  <div className="min-h-screen bg-linear-to-br from-purple-50 via-pink-50 to-blue-50 dark:from-gray-900 dark:via-purple-900 dark:to-gray-900">
247
  {/* Input form - hidden when QR is shown */}
248
  {!showQR && (
249
- <div className="min-h-screen grid place-items-center p-6 md:p-10 bg-white/80">
250
  <div className="w-full max-w-2xl mx-auto">
251
  {/* Input card */}
252
- <Card className="shadow-xl" style={{ padding: 15, fontFamily: 'var(--font-inter)', boxShadow: '0 10px 40px rgba(0, 0, 0, 0.1), 0 2px 8px rgba(0, 0, 0, 0.06)' }}>
253
- <CardHeader className="pb-4">
254
  <div className="flex flex-col gap-3">
255
- <img src="/logo.svg" alt="Hugging Face" className="h-7 w-auto" />
256
- <CardDescription className="text-muted-foreground" style={{ fontFamily: 'var(--font-inter)' }}>Generate a clean QR code for any Hugging Face profile or resource.</CardDescription>
257
  </div>
258
  </CardHeader>
259
- <CardContent className="p-6 md:p-8 space-y-7">
260
  <div className="space-y-3">
261
  <Label className="text-xs tracking-wider font-medium" style={{ fontFamily: 'var(--font-inter)' }}>HUGGING FACE USERNAME</Label>
262
  <div className="relative">
263
- <div className="flex items-stretch overflow-hidden rounded-md border">
264
- <span className="hidden sm:inline-flex items-center text-sm text-muted-foreground select-none" style={{ paddingLeft: '8px', paddingRight: '5px', backgroundColor: '#f5f5f5', fontFamily: 'var(--font-inter)' }}>
265
  https://huggingface.co/
266
  </span>
267
  <Input
@@ -269,8 +306,8 @@ const HuggingFaceQRGenerator = () => {
269
  value={inputUrl}
270
  onChange={(e) => setInputUrl(e.target.value)}
271
  placeholder="Reubencf"
272
- className="h-12 border-0 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0"
273
- style={{ paddingLeft: '3px', paddingRight: '12px', fontFamily: 'var(--font-inter)' }}
274
  onKeyPress={(e) => e.key === 'Enter' && handleGenerate()}
275
  disabled={loading}
276
  aria-invalid={inputIsInvalid}
@@ -279,15 +316,15 @@ const HuggingFaceQRGenerator = () => {
279
  />
280
  </div>
281
  </div>
282
- <p id="hf-input-help" className="text-xs text-muted-foreground" style={{ paddingTop: '5px', paddingBottom: '4px', fontFamily: 'var(--font-inter)' }}>Paste a full URL or just the username, e.g. <span className="font-mono">reubencf</span>.</p>
283
  </div>
284
 
285
- <div className="pt-2 flex justify-end">
286
  <Button
287
  onClick={handleGenerate}
288
  disabled={!inputUrl || loading}
289
- className="rounded-md h-auto bg-neutral-900 text-white hover:bg-neutral-800 disabled:bg-neutral-700 disabled:opacity-100"
290
- style={{ padding: '12px' }}
291
  aria-label="Generate QR Code"
292
  >
293
  {loading ? 'Generating…' : 'Generate QR Code'}
@@ -309,7 +346,7 @@ const HuggingFaceQRGenerator = () => {
309
  {showQR && profileData && (
310
  <div
311
  className="fixed inset-0 bg-linear-to-br from-purple-50 via-pink-50 to-blue-50 dark:from-gray-900 dark:via-purple-900 dark:to-gray-900 z-50 p-4 md:p-6 overflow-y-auto"
312
- style={{ background: `linear-gradient(135deg, ${gradients[gradientIndex][0]}, ${gradients[gradientIndex][1]})` }}
313
  >
314
  {/* Back button moved outside of the phone so it won't appear in exports */}
315
  <div className="qr-topbar">
@@ -319,7 +356,7 @@ const HuggingFaceQRGenerator = () => {
319
  <div
320
  className="qr-phone-bg"
321
  ref={phoneRef}
322
- style={{ background: `linear-gradient(135deg, ${gradients[gradientIndex][0]}, ${gradients[gradientIndex][1]})` }}
323
  onClick={handleCycleBackground}
324
  >
325
  <div className="qr-card-v2" id="qr-card" ref={cardRef} onClick={(e) => e.stopPropagation()}>
@@ -375,6 +412,12 @@ const HuggingFaceQRGenerator = () => {
375
  </button>
376
  <span className="qr-action-text">Copy</span>
377
  </div>
 
 
 
 
 
 
378
  </div>
379
  </div>
380
  <div className="qr-share-group">
@@ -386,6 +429,46 @@ const HuggingFaceQRGenerator = () => {
386
  </div>
387
  </div>
388
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  </div>
390
  </div>
391
  )}
 
11
  import { Label } from '@/components/ui/label';
12
  import { Badge } from '@/components/ui/badge';
13
  import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
14
+ import { Download, ChevronLeft, Copy, Palette } from 'lucide-react';
15
  import { FaFacebook, FaLinkedin } from 'react-icons/fa';
16
  import { FaSquareXTwitter } from 'react-icons/fa6';
17
 
 
23
  const [qrCodeInstance, setQrCodeInstance] = useState<any>(null);
24
  const [showQR, setShowQR] = useState(false);
25
  const [gradientIndex, setGradientIndex] = useState(0);
26
+ const [showColorPicker, setShowColorPicker] = useState(false);
27
+ const [customColor, setCustomColor] = useState<string | null>(null);
28
+
29
  const gradients = [
30
+ { name: 'Sunset Orange', colors: ['#f1c40f', '#f39c12'], type: 'gradient' },
31
+ { name: 'Ocean Blue', colors: ['#60a5fa', '#3b82f6'], type: 'gradient' },
32
+ { name: 'Emerald Green', colors: ['#34d399', '#10b981'], type: 'gradient' },
33
+ { name: 'Purple Dream', colors: ['#a78bfa', '#7c3aed'], type: 'gradient' },
34
+ { name: 'Rose Pink', colors: ['#fb7185', '#f472b6'], type: 'gradient' },
35
+ { name: 'Amber Gold', colors: ['#fbbf24', '#f59e0b'], type: 'gradient' },
36
+ { name: 'Teal Breeze', colors: ['#2dd4bf', '#14b8a6'], type: 'gradient' },
37
+ { name: 'Indigo Night', colors: ['#6366f1', '#4f46e5'], type: 'gradient' },
38
+ ];
39
+
40
+ const solidColors = [
41
+ { name: 'Orange', color: '#f59e0b' },
42
+ { name: 'Blue', color: '#3b82f6' },
43
+ { name: 'Green', color: '#10b981' },
44
+ { name: 'Purple', color: '#8b5cf6' },
45
+ { name: 'Pink', color: '#ec4899' },
46
+ { name: 'Red', color: '#ef4444' },
47
+ { name: 'Yellow', color: '#eab308' },
48
+ { name: 'Gray', color: '#6b7280' },
49
  ];
50
 
51
  const handleGenerate = async () => {
 
101
 
102
  const handleCycleBackground = () => {
103
  setGradientIndex((prev) => (prev + 1) % gradients.length);
104
+ setCustomColor(null);
105
+ };
106
+
107
+ const handleSelectGradient = (index: number) => {
108
+ setGradientIndex(index);
109
+ setCustomColor(null);
110
+ setShowColorPicker(false);
111
+ };
112
+
113
+ const handleSelectSolidColor = (color: string) => {
114
+ setCustomColor(color);
115
+ setShowColorPicker(false);
116
+ };
117
+
118
+ const getBackgroundStyle = () => {
119
+ if (customColor) {
120
+ return { background: customColor };
121
+ }
122
+ const gradient = gradients[gradientIndex];
123
+ return { background: `linear-gradient(135deg, ${gradient.colors[0]}, ${gradient.colors[1]})` };
124
  };
125
 
126
  const phoneRef = useRef<HTMLDivElement | null>(null);
 
283
  <div className="min-h-screen bg-linear-to-br from-purple-50 via-pink-50 to-blue-50 dark:from-gray-900 dark:via-purple-900 dark:to-gray-900">
284
  {/* Input form - hidden when QR is shown */}
285
  {!showQR && (
286
+ <div className="min-h-screen grid place-items-center p-4 sm:p-6 md:p-10 bg-white/80">
287
  <div className="w-full max-w-2xl mx-auto">
288
  {/* Input card */}
289
+ <Card className="shadow-xl" style={{ padding: '1rem', fontFamily: 'var(--font-inter)', boxShadow: '0 10px 40px rgba(0, 0, 0, 0.1), 0 2px 8px rgba(0, 0, 0, 0.06)' }}>
290
+ <CardHeader className="pb-4 px-4 sm:px-6">
291
  <div className="flex flex-col gap-3">
292
+ <img src="/logo.svg" alt="Hugging Face" className="h-6 sm:h-7 w-auto" />
293
+ <CardDescription className="text-muted-foreground text-sm sm:text-base" style={{ fontFamily: 'var(--font-inter)' }}>Generate a clean QR code for any Hugging Face profile or resource.</CardDescription>
294
  </div>
295
  </CardHeader>
296
+ <CardContent className="p-4 sm:p-6 md:p-8 space-y-5 sm:space-y-7">
297
  <div className="space-y-3">
298
  <Label className="text-xs tracking-wider font-medium" style={{ fontFamily: 'var(--font-inter)' }}>HUGGING FACE USERNAME</Label>
299
  <div className="relative">
300
+ <div className="flex flex-col sm:flex-row items-stretch overflow-hidden rounded-md border">
301
+ <span className="hidden sm:inline-flex items-center text-xs sm:text-sm text-muted-foreground select-none" style={{ paddingLeft: '8px', paddingRight: '5px', backgroundColor: '#f5f5f5', fontFamily: 'var(--font-inter)' }}>
302
  https://huggingface.co/
303
  </span>
304
  <Input
 
306
  value={inputUrl}
307
  onChange={(e) => setInputUrl(e.target.value)}
308
  placeholder="Reubencf"
309
+ className="h-10 sm:h-12 border-0 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0 text-sm sm:text-base"
310
+ style={{ paddingLeft: '12px', paddingRight: '12px', fontFamily: 'var(--font-inter)' }}
311
  onKeyPress={(e) => e.key === 'Enter' && handleGenerate()}
312
  disabled={loading}
313
  aria-invalid={inputIsInvalid}
 
316
  />
317
  </div>
318
  </div>
319
+ <p id="hf-input-help" className="text-xs text-muted-foreground" style={{ paddingTop: '5px', paddingBottom: '4px', fontFamily: 'var(--font-inter)' }}>Paste a full URL or just the username, e.g. <span className="font-mono text-xs">reubencf</span>.</p>
320
  </div>
321
 
322
+ <div className="pt-2 flex justify-center sm:justify-end">
323
  <Button
324
  onClick={handleGenerate}
325
  disabled={!inputUrl || loading}
326
+ className="rounded-md h-auto bg-neutral-900 text-white hover:bg-neutral-800 disabled:bg-neutral-700 disabled:opacity-100 w-full sm:w-auto text-sm sm:text-base"
327
+ style={{ padding: '10px 16px' }}
328
  aria-label="Generate QR Code"
329
  >
330
  {loading ? 'Generating…' : 'Generate QR Code'}
 
346
  {showQR && profileData && (
347
  <div
348
  className="fixed inset-0 bg-linear-to-br from-purple-50 via-pink-50 to-blue-50 dark:from-gray-900 dark:via-purple-900 dark:to-gray-900 z-50 p-4 md:p-6 overflow-y-auto"
349
+ style={getBackgroundStyle()}
350
  >
351
  {/* Back button moved outside of the phone so it won't appear in exports */}
352
  <div className="qr-topbar">
 
356
  <div
357
  className="qr-phone-bg"
358
  ref={phoneRef}
359
+ style={getBackgroundStyle()}
360
  onClick={handleCycleBackground}
361
  >
362
  <div className="qr-card-v2" id="qr-card" ref={cardRef} onClick={(e) => e.stopPropagation()}>
 
412
  </button>
413
  <span className="qr-action-text">Copy</span>
414
  </div>
415
+ <div className="qr-download-item">
416
+ <button onClick={() => setShowColorPicker(!showColorPicker)} className="qr-circle" aria-label="Change Color">
417
+ <Palette size={18} />
418
+ </button>
419
+ <span className="qr-action-text">Color</span>
420
+ </div>
421
  </div>
422
  </div>
423
  <div className="qr-share-group">
 
429
  </div>
430
  </div>
431
  </div>
432
+
433
+ {/* Color Picker Popup */}
434
+ {showColorPicker && (
435
+ <div className="qr-color-picker" onClick={(e) => e.stopPropagation()}>
436
+ <div className="qr-color-picker-header">
437
+ <h3>Choose Background</h3>
438
+ <button onClick={() => setShowColorPicker(false)} className="qr-color-close">×</button>
439
+ </div>
440
+
441
+ <div className="qr-color-section">
442
+ <h4>Gradients</h4>
443
+ <div className="qr-color-grid">
444
+ {gradients.map((gradient, index) => (
445
+ <button
446
+ key={index}
447
+ className={`qr-color-swatch ${gradientIndex === index && !customColor ? 'active' : ''}`}
448
+ style={{ background: `linear-gradient(135deg, ${gradient.colors[0]}, ${gradient.colors[1]})` }}
449
+ onClick={() => handleSelectGradient(index)}
450
+ title={gradient.name}
451
+ />
452
+ ))}
453
+ </div>
454
+ </div>
455
+
456
+ <div className="qr-color-section">
457
+ <h4>Solid Colors</h4>
458
+ <div className="qr-color-grid">
459
+ {solidColors.map((color, index) => (
460
+ <button
461
+ key={index}
462
+ className={`qr-color-swatch ${customColor === color.color ? 'active' : ''}`}
463
+ style={{ background: color.color }}
464
+ onClick={() => handleSelectSolidColor(color.color)}
465
+ title={color.name}
466
+ />
467
+ ))}
468
+ </div>
469
+ </div>
470
+ </div>
471
+ )}
472
  </div>
473
  </div>
474
  )}
next.config.ts CHANGED
@@ -1,7 +1,23 @@
1
  import type { NextConfig } from "next";
2
 
3
  const nextConfig: NextConfig = {
4
- /* config options here */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  };
6
 
7
  export default nextConfig;
 
1
  import type { NextConfig } from "next";
2
 
3
  const nextConfig: NextConfig = {
4
+ output: 'standalone',
5
+ images: {
6
+ remotePatterns: [
7
+ {
8
+ protocol: 'https',
9
+ hostname: 'huggingface.co',
10
+ port: '',
11
+ pathname: '/**',
12
+ },
13
+ {
14
+ protocol: 'https',
15
+ hostname: 'aeiljuispo.cloudimg.io',
16
+ port: '',
17
+ pathname: '/**',
18
+ },
19
+ ],
20
+ },
21
  };
22
 
23
  export default nextConfig;
styles/qr-generator.css CHANGED
@@ -332,7 +332,34 @@
332
  background: #095196;
333
  }
334
 
335
- /* Mobile responsiveness */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  @media (max-width: 768px) {
337
  .qr-generator {
338
  padding: 1rem;
@@ -341,6 +368,7 @@
341
  .input-section,
342
  .result-section {
343
  padding: 2rem 1.5rem;
 
344
  }
345
 
346
  .title {
@@ -409,6 +437,114 @@
409
  .share-btn {
410
  width: 100%;
411
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  }
413
 
414
  /* ===== New UI (Screenshot-inspired) ===== */
@@ -543,8 +679,10 @@
543
  align-items: center;
544
  justify-content: space-between;
545
  color: rgba(255,255,255,0.95);
 
546
  }
547
- .qr-topbar button { background: transparent; border: 0; color: inherit; padding: 4px; cursor: default; }
 
548
 
549
  /* Bottom share sheet */
550
  .qr-share-sheet {
@@ -586,3 +724,105 @@
586
  .qr-share-group { display:flex; flex-direction: column; gap: 8px; }
587
  .qr-share-label { font-size: 12px; color:#6b7280; margin-left: 0; text-align: left; letter-spacing: .02em; font-weight: 600; }
588
  .qr-share-actions { display:flex; gap: 14px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  background: #095196;
333
  }
334
 
335
+ /* Responsive Design for All Devices */
336
+
337
+ /* Tablet - Large (iPad Pro) */
338
+ @media (max-width: 1024px) {
339
+ .qr-generator {
340
+ padding: 1.5rem;
341
+ }
342
+
343
+ .input-section,
344
+ .result-section {
345
+ max-width: 700px;
346
+ padding: 2.5rem 2rem;
347
+ }
348
+
349
+ .title {
350
+ font-size: 2.2rem;
351
+ }
352
+
353
+ .qr-phone-bg {
354
+ width: 360px;
355
+ }
356
+
357
+ .qr-share-sheet {
358
+ width: 360px;
359
+ }
360
+ }
361
+
362
+ /* Tablet - Standard */
363
  @media (max-width: 768px) {
364
  .qr-generator {
365
  padding: 1rem;
 
368
  .input-section,
369
  .result-section {
370
  padding: 2rem 1.5rem;
371
+ max-width: 100%;
372
  }
373
 
374
  .title {
 
437
  .share-btn {
438
  width: 100%;
439
  }
440
+
441
+ .qr-phone-bg {
442
+ width: 340px;
443
+ }
444
+
445
+ .qr-share-sheet {
446
+ width: 340px;
447
+ }
448
+ }
449
+
450
+ /* Mobile - Large (iPhone Plus/Max) */
451
+ @media (max-width: 414px) {
452
+ .qr-generator {
453
+ padding: 0.75rem;
454
+ }
455
+
456
+ .input-section,
457
+ .result-section {
458
+ padding: 1.5rem 1rem;
459
+ border-radius: 16px;
460
+ }
461
+
462
+ .title {
463
+ font-size: 1.5rem;
464
+ }
465
+
466
+ .subtitle {
467
+ font-size: 0.9rem;
468
+ }
469
+
470
+ .qr-phone-bg {
471
+ width: 100%;
472
+ max-width: 340px;
473
+ padding: 30px 12px 12px;
474
+ }
475
+
476
+ .qr-card-v2 {
477
+ padding: 45px 12px 16px;
478
+ }
479
+
480
+ .qr-share-sheet {
481
+ width: 100%;
482
+ max-width: 340px;
483
+ padding: 10px 12px;
484
+ }
485
+
486
+ .hf-card {
487
+ padding: 1.5rem;
488
+ }
489
+ }
490
+
491
+ /* Mobile - Standard (iPhone, Android) */
492
+ @media (max-width: 375px) {
493
+ .qr-phone-bg {
494
+ width: 100%;
495
+ max-width: 320px;
496
+ }
497
+
498
+ .qr-share-sheet {
499
+ width: 100%;
500
+ max-width: 320px;
501
+ }
502
+
503
+ .qr-avatar-wrap {
504
+ width: 48px;
505
+ height: 48px;
506
+ top: -24px;
507
+ }
508
+
509
+ .qr-name {
510
+ font-size: 0.95rem;
511
+ }
512
+
513
+ .qr-caption {
514
+ font-size: 0.75rem;
515
+ }
516
+ }
517
+
518
+ /* Mobile - Small */
519
+ @media (max-width: 320px) {
520
+ .input-section,
521
+ .result-section {
522
+ padding: 1.25rem 0.75rem;
523
+ }
524
+
525
+ .title {
526
+ font-size: 1.3rem;
527
+ }
528
+
529
+ .qr-phone-bg {
530
+ width: 100%;
531
+ padding: 25px 10px 10px;
532
+ }
533
+
534
+ .qr-card-v2 {
535
+ padding: 40px 10px 14px;
536
+ }
537
+
538
+ .qr-share-sheet {
539
+ width: 100%;
540
+ grid-template-columns: 1fr;
541
+ gap: 10px;
542
+ }
543
+
544
+ .qr-share-actions,
545
+ .qr-download-actions {
546
+ justify-content: center;
547
+ }
548
  }
549
 
550
  /* ===== New UI (Screenshot-inspired) ===== */
 
679
  align-items: center;
680
  justify-content: space-between;
681
  color: rgba(255,255,255,0.95);
682
+ padding: 8px;
683
  }
684
+ .qr-topbar button { background: transparent; border: 0; color: inherit; padding: 8px; cursor: pointer; transition: opacity 0.2s; }
685
+ .qr-topbar button:hover { opacity: 0.7; }
686
 
687
  /* Bottom share sheet */
688
  .qr-share-sheet {
 
724
  .qr-share-group { display:flex; flex-direction: column; gap: 8px; }
725
  .qr-share-label { font-size: 12px; color:#6b7280; margin-left: 0; text-align: left; letter-spacing: .02em; font-weight: 600; }
726
  .qr-share-actions { display:flex; gap: 14px; }
727
+
728
+ /* Color Picker */
729
+ .qr-color-picker {
730
+ position: relative;
731
+ width: 380px;
732
+ max-width: 100%;
733
+ margin: 12px auto 0;
734
+ background: #fff;
735
+ border-radius: 18px;
736
+ box-shadow: 0 -8px 24px rgba(0,0,0,.22);
737
+ padding: 16px;
738
+ font-family: var(--font-inter), var(--font-geist-sans), system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
739
+ animation: slideUp 0.3s ease;
740
+ }
741
+
742
+ .qr-color-picker-header {
743
+ display: flex;
744
+ justify-content: space-between;
745
+ align-items: center;
746
+ margin-bottom: 16px;
747
+ }
748
+
749
+ .qr-color-picker-header h3 {
750
+ font-size: 16px;
751
+ font-weight: 700;
752
+ color: #111827;
753
+ margin: 0;
754
+ }
755
+
756
+ .qr-color-close {
757
+ background: transparent;
758
+ border: none;
759
+ font-size: 24px;
760
+ color: #6b7280;
761
+ cursor: pointer;
762
+ padding: 0;
763
+ width: 24px;
764
+ height: 24px;
765
+ display: flex;
766
+ align-items: center;
767
+ justify-content: center;
768
+ transition: color 0.2s;
769
+ }
770
+
771
+ .qr-color-close:hover {
772
+ color: #111827;
773
+ }
774
+
775
+ .qr-color-section {
776
+ margin-bottom: 16px;
777
+ }
778
+
779
+ .qr-color-section:last-child {
780
+ margin-bottom: 0;
781
+ }
782
+
783
+ .qr-color-section h4 {
784
+ font-size: 13px;
785
+ font-weight: 600;
786
+ color: #6b7280;
787
+ margin: 0 0 10px 0;
788
+ text-transform: uppercase;
789
+ letter-spacing: 0.05em;
790
+ }
791
+
792
+ .qr-color-grid {
793
+ display: grid;
794
+ grid-template-columns: repeat(4, 1fr);
795
+ gap: 10px;
796
+ }
797
+
798
+ .qr-color-swatch {
799
+ width: 100%;
800
+ aspect-ratio: 1;
801
+ border-radius: 12px;
802
+ border: 2px solid transparent;
803
+ cursor: pointer;
804
+ transition: all 0.2s;
805
+ position: relative;
806
+ }
807
+
808
+ .qr-color-swatch:hover {
809
+ transform: scale(1.05);
810
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
811
+ }
812
+
813
+ .qr-color-swatch.active {
814
+ border-color: #111827;
815
+ box-shadow: 0 0 0 2px #fff, 0 0 0 4px #111827;
816
+ }
817
+
818
+ .qr-color-swatch.active::after {
819
+ content: '✓';
820
+ position: absolute;
821
+ top: 50%;
822
+ left: 50%;
823
+ transform: translate(-50%, -50%);
824
+ color: white;
825
+ font-size: 18px;
826
+ font-weight: bold;
827
+ text-shadow: 0 1px 3px rgba(0,0,0,0.5);
828
+ }