done
Browse files- .claude/settings.local.json +2 -1
- .dockerignore +52 -0
- Dockerfile +61 -0
- LICENSE +21 -0
- README.md +140 -20
- app/api/huggingface/route.js +4 -2
- app/api/proxy-image/route.js +35 -6
- app/page.tsx +1 -1
- components/HuggingFaceQRGenerator.tsx +105 -22
- next.config.ts +17 -1
- styles/qr-generator.css +242 -2
.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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
|
| 23 |
-
|
| 24 |
|
| 25 |
-
|
| 26 |
|
| 27 |
-
-
|
| 28 |
-
-
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
|
| 31 |
|
| 32 |
-
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
|
| 35 |
|
| 36 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 31 |
|
| 32 |
let avatarUrl = null;
|
| 33 |
if (avatarMatch && avatarMatch[0]) {
|
| 34 |
-
|
|
|
|
| 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 |
-
|
| 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:',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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('"')[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
|
| 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 |
-
['#
|
| 29 |
-
['#
|
| 30 |
-
['#
|
| 31 |
-
['#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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: '
|
| 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: '
|
| 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={
|
| 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={
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
|
|
|
| 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 |
+
}
|