From dfb20b571a74f6f25f64da0c5cfd9d50f8a98583 Mon Sep 17 00:00:00 2001 From: Davide Piccinini Date: Fri, 31 Jul 2026 09:22:40 +0200 Subject: [PATCH 1/4] chore: refresh README, add Netlify MCP, prune old deploys on release - README: rewrite to match the current app (19 content templates, international phone input, i18n en/it/ro, color presets, correct tech stack, Node 22+, real project structure and CI/CD deployment) - Add .mcp.json with the Netlify MCP server - CI: add a cleanup job that prunes Netlify deploys after an official (tagged) release, keeping the last 3 production releases and only the latest preview; skipped on preview deploys from main Co-Authored-By: Claude AI --- .github/workflows/build-and-deploy.yml | 44 ++++++++ .mcp.json | 8 ++ README.md | 144 +++++++++++++------------ 3 files changed, 127 insertions(+), 69 deletions(-) create mode 100644 .mcp.json diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 066ec2f..53448a9 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -140,3 +140,47 @@ jobs: NETLIFY_AUTH_TOKEN: "${{ secrets.NETLIFY_AUTH_TOKEN }}" NETLIFY_SITE_ID: "${{ secrets.NETLIFY_SITE_ID }}" timeout-minutes: 5 + + cleanup: + name: Prune old Netlify deploys + runs-on: ubuntu-latest + needs: deploy-production + # Only after publishing an OFFICIAL release (tag) β€” never on preview deploys. + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Keep last 3 releases + latest preview, delete the rest + env: + NETLIFY_AUTH_TOKEN: "${{ secrets.NETLIFY_AUTH_TOKEN }}" + NETLIFY_SITE_ID: "${{ secrets.NETLIFY_SITE_ID }}" + KEEP_STABLE: "3" # tagged production deploys to retain (incl. the one just released) + KEEP_PREVIEW: "1" # most recent preview (draft) deploy to retain + run: | + set -euo pipefail + api="https://api.netlify.com/api/v1" + auth="Authorization: Bearer ${NETLIFY_AUTH_TOKEN}" + + deploys="$(curl -fsS -H "$auth" "$api/sites/${NETLIFY_SITE_ID}/deploys?per_page=100")" + + # IDs to delete: + # - production (draft=false, ready) beyond the newest $KEEP_STABLE + # - previews (draft=true) beyond the newest $KEEP_PREVIEW + ids="$(printf '%s' "$deploys" | jq -r \ + --argjson keep_stable "$KEEP_STABLE" \ + --argjson keep_preview "$KEEP_PREVIEW" ' + ( [ .[] | select(.draft == false and .state == "ready") ] + | sort_by(.created_at) | reverse | .[$keep_stable:] | .[].id ), + ( [ .[] | select(.draft == true) ] + | sort_by(.created_at) | reverse | .[$keep_preview:] | .[].id ) + ')" + + if [ -z "$ids" ]; then echo "Nothing to prune."; exit 0; fi + + while IFS= read -r id; do + [ -n "$id" ] || continue + code="$(curl -sS -o /dev/null -w '%{http_code}' -X DELETE -H "$auth" "$api/deploys/${id}")" + if [ "$code" = "200" ] || [ "$code" = "204" ]; then + echo "Deleted $id" + else + echo "::warning::Could not delete $id (HTTP $code)" + fi + done <<< "$ids" diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..19e350a --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "netlify": { + "command": "npx", + "args": ["-y", "@netlify/mcp"] + } + } +} diff --git a/README.md b/README.md index 7ea5f9b..bdeb7e1 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,132 @@ -# QR Code Builder +

+ QR Code Builder +

-A modern, customizable QR code builder built with React, TypeScript, and Vite. Features a beautiful dark/light theme, persistent settings, and multiple export formats. +# QR Code Builder -🌐 **Live Demo:** [https://free-qr-builder.netlify.app](https://free-qr-builder.netlify.app) +A modern, fully customizable QR code builder built with React, TypeScript, and Vite. Generate styled QR codes for 19 content types, personalize every detail, localize the UI, and export to PNG, SVG, or PDF β€” all client-side, no account required. -![QR Code Builder](https://via.placeholder.com/800x400?text=QR+Code+Builder) +🌐 **Live demo:** [https://free-qr-builder.netlify.app](https://free-qr-builder.netlify.app) ## ✨ Features -- **Fully Customizable QR Codes** - - Adjust size (100-500px) and margin - - Choose from 6 dot styles (square, dots, rounded, extra-rounded, classy, classy-rounded) - - Select corner square and corner dot styles - - Solid colors or gradients for dots +- **19 content templates**, grouped by category: + - **Links & utilities** β€” URL, WiFi credentials, calendar event, geo location + - **Contact & messaging** β€” vCard, email, SMS, phone call, WhatsApp, Telegram + - **Social** β€” Instagram, Twitter/X, LinkedIn, TikTok, YouTube, Facebook, Snapchat + - **Payment** β€” PayPal, Bitcoin + - Each template renders a dedicated form and builds a spec-compliant payload (e.g. `WIFI:`, `BEGIN:VCARD`, `BEGIN:VEVENT`, `geo:`, `bitcoin:`), with proper escaping/encoding. + +- **International phone input** β€” dialing-code selector with country flags (powered by `libphonenumber-js`) for phone, SMS, and WhatsApp templates. + +- **Multi-language UI** β€” English, Italiano, and RomΓ’nΔƒ, switchable from the header. + +- **Fully customizable styling** + - Adjustable size (100–500px) and margin + - 6 dot styles (square, rounded, dots, classy, classy-rounded, extra-rounded) + - Independent corner-square and corner-dot styles + - Solid color or linear/radial gradient for dots - Transparent or colored background + - **Color presets** (Classic, Ocean, Forest, Sunset, Purple, Monochrome) plus your own saved presets -- **Center Logo Support** - - Upload custom logo images - - Adjustable size (20-30% of QR code) - - Configurable margin around logo +- **Center logo support** β€” upload a custom image, tune its size and the margin around it. -- **Error Correction Levels** - - L, M, Q, H levels with helpful tooltips - - Higher levels recommended when using logos +- **Error correction levels** β€” L, M, Q, H with inline guidance (higher levels recommended when embedding a logo). -- **Multiple Export Formats** - - PNG for general use - - SVG for scalable graphics - - PDF for print-ready output +- **Multiple export formats** β€” PNG for general use, SVG for scalable graphics, PDF for print-ready output. -- **Modern UI/UX** - - Dark/Light theme toggle - - Settings persistence via localStorage - - Reset to default functionality - - Visual style pickers with previews - - Responsive design for mobile and desktop +- **Modern UX** β€” dark/light theme toggle (dark by default), settings and presets persisted in `localStorage`, reset-to-default, visual style pickers with SVG previews, responsive layout for mobile and desktop. ## πŸš€ Getting Started ### Prerequisites -- Node.js 18+ -- npm or yarn +- Node.js 22+ +- npm ### Installation ```bash # Clone the repository -git clone https://github.com/your-username/qr-code-builder.git +git clone https://github.com/davide-pi/qr-code-builder.git cd qr-code-builder # Install dependencies npm install -# Start development server +# Start the development server npm run dev ``` -### Build for Production +### Build for production ```bash npm run build -npm run preview # Preview the production build +npm run preview # Preview the production build locally ``` ## πŸ› οΈ Tech Stack -- **React 19** - UI framework -- **TypeScript** - Type safety -- **Vite 7** - Build tool and dev server -- **qr-code-styling** - QR code generation library -- **jsPDF** - PDF export functionality +- **React 19** β€” UI framework +- **TypeScript** β€” type safety +- **Vite 7** β€” build tool and dev server +- **qr-code-styling** β€” QR code generation and styling +- **jsPDF** β€” PDF export +- **libphonenumber-js** + **flag-icons** β€” international dialing codes and flags +- **lucide-react** β€” icons +- **react-select** β€” accessible dropdowns ## πŸ“ Project Structure ``` src/ β”œβ”€β”€ components/ -β”‚ β”œβ”€β”€ Header/ -β”‚ β”‚ β”œβ”€β”€ Header.tsx # Fixed header with logo and theme toggle -β”‚ β”‚ └── Header.css -β”‚ β”œβ”€β”€ Footer/ -β”‚ β”‚ β”œβ”€β”€ Footer.tsx # Fixed footer with credits -β”‚ β”‚ └── Footer.css -β”‚ β”œβ”€β”€ QRCodeGenerator/ -β”‚ β”‚ β”œβ”€β”€ QRCodeGenerator.tsx # Main orchestrator component -β”‚ β”‚ └── QRCodeGenerator.css -β”‚ β”œβ”€β”€ QROptions/ -β”‚ β”‚ β”œβ”€β”€ QROptions.tsx # All customization controls -β”‚ β”‚ └── QROptions.css -β”‚ β”œβ”€β”€ QRPreview/ -β”‚ β”‚ β”œβ”€β”€ QRPreview.tsx # QR preview and download buttons -β”‚ β”‚ └── QRPreview.css -β”‚ └── StylePicker/ -β”‚ └── StylePicker.tsx # Reusable style picker with SVG previews +β”‚ β”œβ”€β”€ Header/ # App header (logo, theme toggle, language switch) +β”‚ β”œβ”€β”€ Footer/ # Footer with credits +β”‚ β”œβ”€β”€ QRCodeGenerator/ # Main orchestrator component +β”‚ β”œβ”€β”€ QRDataInput/ # Content-type templates and their forms +β”‚ β”œβ”€β”€ QROptions/ # Styling controls (size, colors, dots, gradient, logo…) +β”‚ β”œβ”€β”€ QRPreview/ # Live preview and PNG/SVG/PDF export +β”‚ β”œβ”€β”€ StylePicker/ # Reusable style picker with SVG previews +β”‚ β”œβ”€β”€ CountryCodeSelect/ # International dialing-code selector +β”‚ └── Flags/ # Flag icons for the country selector +β”œβ”€β”€ i18n/ # Localization (en, it, ro) and LanguageProvider β”œβ”€β”€ types/ -β”‚ └── qr.ts # Shared types and constants -β”œβ”€β”€ App.tsx/css # Main app with theme logic -└── main.tsx # Entry point +β”‚ └── qr.ts # Shared types, template definitions, presets, constants +β”œβ”€β”€ App.tsx / App.css # Root app and theme logic +└── main.tsx # Entry point ``` ## 🌐 Deployment -This project is configured for easy deployment to Netlify: +The app deploys to **Netlify** through the GitHub Actions pipeline in +[`.github/workflows/build-and-deploy.yml`](.github/workflows/build-and-deploy.yml): + +1. **Lint** β†’ **Build** (versioned with [GitVersion](https://gitversion.net/)). +2. **Preview deploy** β€” every push to `main` publishes a Netlify **draft** deploy. +3. **Production deploy** β€” pushing a `vX.Y.Z` tag publishes to production. +4. **Cleanup** β€” after an official (tagged) release, an automated step prunes old Netlify + deploys, keeping the **last 3 production releases** and only the **latest preview**. + +The pipeline expects two repository secrets: `NETLIFY_AUTH_TOKEN` and `NETLIFY_SITE_ID`. -1. Connect your GitHub repository to Netlify -2. Netlify will automatically detect Vite configuration -3. Build command: `npm run build` -4. Publish directory: `dist` +> An MCP server for Netlify is configured in [`.mcp.json`](.mcp.json) for use with +> MCP-compatible AI tooling. ## πŸ“ Scripts -- `npm run dev` - Start development server -- `npm run build` - Build for production -- `npm run preview` - Preview production build -- `npm run lint` - Run ESLint +- `npm run dev` β€” start the development server +- `npm run build` β€” type-check and build for production +- `npm run preview` β€” preview the production build +- `npm run lint` β€” run ESLint ## πŸ“„ License -MIT License - feel free to use this project for personal or commercial purposes. +MIT License Β© 2025 Davide Piccinini β€” free to use for personal or commercial purposes. See [LICENSE](LICENSE). ## πŸ™ Credits -- [qr-code-styling](https://github.com/kozakdenys/qr-code-styling) - QR code generation -- [jsPDF](https://github.com/parallax/jsPDF) - PDF export +- [qr-code-styling](https://github.com/kozakdenys/qr-code-styling) β€” QR code generation +- [jsPDF](https://github.com/parallax/jsPDF) β€” PDF export +- [libphonenumber-js](https://github.com/catamphetamine/libphonenumber-js) β€” phone number handling +- [lucide](https://lucide.dev/) β€” icons From 2d64e37c1afabb3b380280e06bae349af0fc0d6f Mon Sep 17 00:00:00 2001 From: Davide Piccinini Date: Fri, 31 Jul 2026 09:42:09 +0200 Subject: [PATCH 2/4] docs: correct Vite version in README (7 -> 8) Align with the current main, where Vite is ^8.x. Co-Authored-By: Claude AI --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bdeb7e1..617d515 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ npm run preview # Preview the production build locally - **React 19** β€” UI framework - **TypeScript** β€” type safety -- **Vite 7** β€” build tool and dev server +- **Vite 8** β€” build tool and dev server - **qr-code-styling** β€” QR code generation and styling - **jsPDF** β€” PDF export - **libphonenumber-js** + **flag-icons** β€” international dialing codes and flags From cf7294e42480da1b8e1e5023bb5d8ff85a50c174 Mon Sep 17 00:00:00 2001 From: Davide Piccinini Date: Fri, 31 Jul 2026 10:09:21 +0200 Subject: [PATCH 3/4] Harden Netlify prune job - keep only a preview whose state is "ready" (mirror the stable filter), so a failed draft deploy can't be retained over a working one - append `|| echo 000` to the DELETE curl so a network-level failure on one deploy no longer trips errexit and aborts the whole prune loop Co-Authored-By: Claude AI --- .github/workflows/build-and-deploy.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 53448a9..48ff36f 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -163,13 +163,13 @@ jobs: # IDs to delete: # - production (draft=false, ready) beyond the newest $KEEP_STABLE - # - previews (draft=true) beyond the newest $KEEP_PREVIEW + # - previews (draft=true, ready) beyond the newest $KEEP_PREVIEW ids="$(printf '%s' "$deploys" | jq -r \ --argjson keep_stable "$KEEP_STABLE" \ --argjson keep_preview "$KEEP_PREVIEW" ' ( [ .[] | select(.draft == false and .state == "ready") ] | sort_by(.created_at) | reverse | .[$keep_stable:] | .[].id ), - ( [ .[] | select(.draft == true) ] + ( [ .[] | select(.draft == true and .state == "ready") ] | sort_by(.created_at) | reverse | .[$keep_preview:] | .[].id ) ')" @@ -177,7 +177,8 @@ jobs: while IFS= read -r id; do [ -n "$id" ] || continue - code="$(curl -sS -o /dev/null -w '%{http_code}' -X DELETE -H "$auth" "$api/deploys/${id}")" + # `|| echo 000` so a network-level curl failure doesn't trip errexit and abort the loop + code="$(curl -sS -o /dev/null -w '%{http_code}' -X DELETE -H "$auth" "$api/deploys/${id}" || echo 000)" if [ "$code" = "200" ] || [ "$code" = "204" ]; then echo "Deleted $id" else From 7f1e654c3d9a9954a852ef65d9be228bb66f545d Mon Sep 17 00:00:00 2001 From: Davide Piccinini Date: Fri, 31 Jul 2026 10:22:46 +0200 Subject: [PATCH 4/4] Rework Netlify prune: age-based retention, run on every deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prune now runs after every deploy (preview on main, production on a tag), not only on tagged releases. A deploy is deleted only when it is BOTH older than MAX_AGE_DAYS (7) AND beyond its group minimum β€” newest KEEP_STABLE (3) stable and KEEP_PREVIEW (2) previews. So everything younger than a week is always kept, and each group keeps a floor regardless of age. Co-Authored-By: Claude AI --- .github/workflows/build-and-deploy.yml | 32 ++++++++++++++++---------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 48ff36f..342c718 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -144,33 +144,41 @@ jobs: cleanup: name: Prune old Netlify deploys runs-on: ubuntu-latest - needs: deploy-production - # Only after publishing an OFFICIAL release (tag) β€” never on preview deploys. - if: startsWith(github.ref, 'refs/tags/') + needs: [deploy-preview, deploy-production] + # Runs after every deploy β€” preview (push to main) or production (tag). + # Skipped on PRs, where nothing is deployed. + if: ${{ always() && (needs.deploy-preview.result == 'success' || needs.deploy-production.result == 'success') }} + permissions: + contents: read steps: - - name: Keep last 3 releases + latest preview, delete the rest + - name: Keep recent deploys plus a minimum of stable/preview, delete the rest env: NETLIFY_AUTH_TOKEN: "${{ secrets.NETLIFY_AUTH_TOKEN }}" NETLIFY_SITE_ID: "${{ secrets.NETLIFY_SITE_ID }}" - KEEP_STABLE: "3" # tagged production deploys to retain (incl. the one just released) - KEEP_PREVIEW: "1" # most recent preview (draft) deploy to retain + KEEP_STABLE: "3" # minimum stable (production) deploys to always retain + KEEP_PREVIEW: "2" # minimum preview (draft) deploys to always retain + MAX_AGE_DAYS: "7" # additionally, retain every deploy younger than this run: | set -euo pipefail api="https://api.netlify.com/api/v1" auth="Authorization: Bearer ${NETLIFY_AUTH_TOKEN}" + cutoff="$(( $(date +%s) - MAX_AGE_DAYS * 24 * 3600 ))" deploys="$(curl -fsS -H "$auth" "$api/sites/${NETLIFY_SITE_ID}/deploys?per_page=100")" - # IDs to delete: - # - production (draft=false, ready) beyond the newest $KEEP_STABLE - # - previews (draft=true, ready) beyond the newest $KEEP_PREVIEW + # Delete a deploy only if it is BOTH older than the cutoff AND beyond its group's + # minimum (newest $KEEP_STABLE stable / newest $KEEP_PREVIEW previews). So every + # deploy younger than $MAX_AGE_DAYS is kept, and each group keeps at least its + # minimum regardless of age. ids="$(printf '%s' "$deploys" | jq -r \ --argjson keep_stable "$KEEP_STABLE" \ - --argjson keep_preview "$KEEP_PREVIEW" ' + --argjson keep_preview "$KEEP_PREVIEW" \ + --argjson cutoff "$cutoff" ' + def isold: (.created_at | sub("\\.[0-9]+";"") | fromdateiso8601) < $cutoff; ( [ .[] | select(.draft == false and .state == "ready") ] - | sort_by(.created_at) | reverse | .[$keep_stable:] | .[].id ), + | sort_by(.created_at) | reverse | .[$keep_stable:] | .[] | select(isold) | .id ), ( [ .[] | select(.draft == true and .state == "ready") ] - | sort_by(.created_at) | reverse | .[$keep_preview:] | .[].id ) + | sort_by(.created_at) | reverse | .[$keep_preview:] | .[] | select(isold) | .id ) ')" if [ -z "$ids" ]; then echo "Nothing to prune."; exit 0; fi