Impeccable
This commit is contained in:
39
.github/pull_request_template.md
vendored
Normal file
39
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
## Summary
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
## Change Type
|
||||||
|
|
||||||
|
- [ ] QRMaster SEO page
|
||||||
|
- [ ] QRMaster landing/tool page
|
||||||
|
- [ ] QRMaster conversion/pricing change
|
||||||
|
- [ ] GreenLens content/ASO workflow
|
||||||
|
- [ ] Bug fix
|
||||||
|
- [ ] Other
|
||||||
|
|
||||||
|
## SEO / Content Review
|
||||||
|
|
||||||
|
- [ ] Primary search intent is clear.
|
||||||
|
- [ ] Metadata is present and specific.
|
||||||
|
- [ ] Exactly one H1 is rendered for each new or changed page.
|
||||||
|
- [ ] Internal links are added to relevant money pages.
|
||||||
|
- [ ] CTA is specific to the page/use case.
|
||||||
|
- [ ] Duplicate or thin content risk was checked.
|
||||||
|
- [ ] Schema/structured data was added or intentionally skipped.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- [ ] Build passes.
|
||||||
|
- [ ] Lint passes.
|
||||||
|
- [ ] Links/CTAs checked.
|
||||||
|
- [ ] Screenshots or notes included for UI changes.
|
||||||
|
|
||||||
|
## Codex Review Prompt
|
||||||
|
|
||||||
|
For QRMaster SEO/page changes, run:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Use docs/automations/qrmaster-pr-seo-review.md and review this PR for SEO,
|
||||||
|
conversion, internal linking, duplicate content, schema, and build/lint risk.
|
||||||
|
```
|
||||||
|
|
||||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on: [push]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -23,4 +23,4 @@ jobs:
|
|||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
|
|||||||
1
.impeccable-live.json
Normal file
1
.impeccable-live.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"pid":23720,"port":8400,"token":"99ca8ad6-3aa6-44f6-9b64-25921f55724b"}
|
||||||
51
PRODUCT.md
Normal file
51
PRODUCT.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Product
|
||||||
|
|
||||||
|
## Register
|
||||||
|
|
||||||
|
brand
|
||||||
|
|
||||||
|
## Users
|
||||||
|
|
||||||
|
Two primary personas, both aesthetically-driven professionals who have outgrown generic tools:
|
||||||
|
|
||||||
|
**Marketing / Agency Lead** — manages multiple client campaigns simultaneously. Needs organized folders, detailed analytics, and design output that doesn't betray itself as "internet freeware." They evaluate tools by how their outputs look to clients, not just how the tool works internally.
|
||||||
|
|
||||||
|
**Modern Restaurateur** — owner of a boutique restaurant, cafe, or hotel. Their physical space is carefully designed; their digital touchpoints must match. They rely on Dynamic QR codes to swap menu PDFs and URLs seasonally without reprinting expensive acrylic table stands. Brand consistency between print and digital is non-negotiable for them.
|
||||||
|
|
||||||
|
**Arrival context:** Both land on QR Master after being burned by "free" tools that expired, injected third-party ads, or looked visually cheap. They are actively comparing options and arrive skeptical. They're not discovering QR tools for the first time — they're looking for a permanent, professional home.
|
||||||
|
|
||||||
|
## Product Purpose
|
||||||
|
|
||||||
|
QR Master is a precision QR code platform — creation, dynamic editing, and analytics — for professionals who refuse to compromise on aesthetics. Success means users choose QR Master not because they have to, but because they want to: the tool itself feels like an extension of their creative workflow, not a clunky utility they tolerate.
|
||||||
|
|
||||||
|
## Brand Personality
|
||||||
|
|
||||||
|
**Confident, Minimal, Crafted.**
|
||||||
|
|
||||||
|
"The Leica of QR Generators." A precision instrument that earns trust through intentionality, not decoration. Every pixel deliberate. Think Linear's pro-tool clarity, Raycast's utilitarian beauty, Framer's implied creative freedom — combined into something that feels high-performance without performing.
|
||||||
|
|
||||||
|
Tone of voice: direct and assured. No hedging, no exclamation points for emphasis, no "Amazing!" copy. Let the product speak. Copy is short, specific, and treats the user as a professional.
|
||||||
|
|
||||||
|
## Anti-references
|
||||||
|
|
||||||
|
- **SEO ad farms** (QR-Code-Generator.com style): cluttered sidebars, aggressive upsell banners, walls of keyword-stuffed text. The opposite of QR Master.
|
||||||
|
- **Bubbly link-in-bio tools**: neon gradients, Gen-Z playfulness, the Linktree aesthetic. QR Master is for businesses, not social profiles.
|
||||||
|
- **Legacy enterprise software**: cold gray, mechanical, joyless. High-end is not the same as corporate. QR Master should feel premium, not bureaucratic.
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
1. **Precision over decoration.** Every element earns its place. No UX furniture — no gratuitous dividers, decorative gradients, or filler icons. If removing it doesn't hurt, remove it.
|
||||||
|
|
||||||
|
2. **Show, don't explain.** The product's quality is demonstrated by how the interface looks and behaves, not described in marketing copy. A beautifully rendered QR preview communicates more than three bullet points about "custom branding."
|
||||||
|
|
||||||
|
3. **Premium through restraint.** Sophistication comes from what's removed. More whitespace, fewer words, tighter hierarchy. The instinct to add is the enemy of craft.
|
||||||
|
|
||||||
|
4. **Trustworthy at a glance.** Clarity and professionalism must be instantaneous — users arriving skeptical decide in 5 seconds. No clever puzzles, no mystery-meat navigation. Confidence is expressed through legibility.
|
||||||
|
|
||||||
|
5. **Tool-like beauty.** Functional elegance, like a well-made physical instrument. Interactions should feel responsive and precise. Animations are subtle cues, not performances. The UI should feel fast even when it isn't.
|
||||||
|
|
||||||
|
## Accessibility & Inclusion
|
||||||
|
|
||||||
|
- **Standard**: WCAG 2.1 AA
|
||||||
|
- **Reduced motion**: Full `prefers-reduced-motion` support. Animations (where used) default to subtle fades or scale shifts; no motion for motion's sake.
|
||||||
|
- **Color contrast**: Critical for a QR creation tool. The dashboard must warn users when foreground/background color combinations produce insufficient contrast — both for readability and QR scannability. This is a functional requirement, not a nice-to-have.
|
||||||
45
docs/automations/README.md
Normal file
45
docs/automations/README.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Codex Automation System
|
||||||
|
|
||||||
|
This folder defines reusable Codex workflows for QRMaster and GreenLens Pro.
|
||||||
|
Use these as operating playbooks when asking Codex to run growth, SEO, content,
|
||||||
|
or app-store work.
|
||||||
|
|
||||||
|
## Active Automations
|
||||||
|
|
||||||
|
### QRMaster
|
||||||
|
|
||||||
|
1. `qrmaster-pr-seo-review.md`
|
||||||
|
- Purpose: review every SEO, landing page, and conversion change before it is merged.
|
||||||
|
- Primary plugins/tools: GitHub, Codex.
|
||||||
|
- Primary skills: `ai-seo`, `content-strategy`, `careful`, `qa`.
|
||||||
|
|
||||||
|
2. `qrmaster-seo-sprint-machine.md`
|
||||||
|
- Purpose: plan and produce a weekly SEO sprint from keyword backlog to PR-ready work.
|
||||||
|
- Primary plugins/tools: GitHub, Coupler or CSV exports, Codex.
|
||||||
|
- Primary skills: `content-strategy`, `ai-seo`, `copywriting`, `qa`.
|
||||||
|
|
||||||
|
3. `qrmaster-broken-link-cta-checker.md`
|
||||||
|
- Purpose: catch broken internal links and broken CTAs after direct `main`
|
||||||
|
branch changes.
|
||||||
|
- Primary plugins/tools: Codex, GitHub Actions or local npm script.
|
||||||
|
- Primary skills: `qa`, `ai-seo`.
|
||||||
|
|
||||||
|
### GreenLens Pro
|
||||||
|
|
||||||
|
1. `greenlens-pain-mining-machine.md`
|
||||||
|
- Purpose: turn reviews, comments, competitor messaging, and search questions into
|
||||||
|
product, ASO, and content opportunities.
|
||||||
|
- Primary plugins/tools: Codex, Gmail, Coupler or CSV exports.
|
||||||
|
- Primary skills: `app-store-aso`, `content-strategy`, `copywriting`.
|
||||||
|
|
||||||
|
2. `greenlens-viral-slideshow-machine.md`
|
||||||
|
- Purpose: turn validated plant pains into TikTok, Instagram, and Canva-ready
|
||||||
|
slideshow assets.
|
||||||
|
- Primary plugins/tools: Canva, Codex, Gmail/Fyxer for creator briefs.
|
||||||
|
- Primary skills: `content-strategy`, `copywriting`, `ad-creative`, `app-store-aso`.
|
||||||
|
|
||||||
|
## Operating Rule
|
||||||
|
|
||||||
|
Do not automate publishing directly. Automate drafts, PRs, reviews, and packaged
|
||||||
|
outputs first. A human should approve live SEO pages, store metadata, influencer
|
||||||
|
messages, and paid/conversion changes.
|
||||||
160
docs/automations/greenlens-pain-mining-machine.md
Normal file
160
docs/automations/greenlens-pain-mining-machine.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# GreenLens Pro Pain Mining Machine
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Turn real plant-owner pains into content, ASO, influencer, landing page, and
|
||||||
|
product opportunities.
|
||||||
|
|
||||||
|
## Why This Exists
|
||||||
|
|
||||||
|
GreenLens Pro should be driven by what users actually worry about:
|
||||||
|
yellow leaves, brown spots, root rot, overwatering, underwatering, pests,
|
||||||
|
curling leaves, and not knowing what to do next.
|
||||||
|
|
||||||
|
## Plugins And Skills
|
||||||
|
|
||||||
|
| Need | Use |
|
||||||
|
|---|---|
|
||||||
|
| Review/comment exports | Coupler, CSV exports, Codex |
|
||||||
|
| App Store optimization | `app-store-aso` skill |
|
||||||
|
| Content clustering | `content-strategy` skill |
|
||||||
|
| Copy and hooks | `copywriting` skill |
|
||||||
|
| Support/outreach drafting | Gmail/Fyxer plugin |
|
||||||
|
| Product issue creation | GitHub plugin |
|
||||||
|
|
||||||
|
## Data Sources
|
||||||
|
|
||||||
|
Use any available source, but label the source for every pain:
|
||||||
|
|
||||||
|
- App Store competitor reviews
|
||||||
|
- Google Play competitor reviews
|
||||||
|
- Reddit plant-care threads
|
||||||
|
- TikTok or Instagram comments
|
||||||
|
- Google autocomplete or People Also Ask exports
|
||||||
|
- Support emails or user feedback
|
||||||
|
- Existing GreenLens analytics or onboarding responses
|
||||||
|
|
||||||
|
## Pain Taxonomy
|
||||||
|
|
||||||
|
Cluster each item into one primary category:
|
||||||
|
|
||||||
|
- Yellow leaves
|
||||||
|
- Brown spots
|
||||||
|
- Root rot
|
||||||
|
- Overwatering
|
||||||
|
- Underwatering
|
||||||
|
- Curling leaves
|
||||||
|
- Drooping leaves
|
||||||
|
- Pests
|
||||||
|
- Light problems
|
||||||
|
- Soil and repotting
|
||||||
|
- Beginner confusion
|
||||||
|
- Diagnosis trust
|
||||||
|
- Price/paywall objection
|
||||||
|
- App usability issue
|
||||||
|
|
||||||
|
## Scoring Model
|
||||||
|
|
||||||
|
Score each pain from 0-100:
|
||||||
|
|
||||||
|
| Factor | Weight |
|
||||||
|
|---|---:|
|
||||||
|
| User urgency | 30 |
|
||||||
|
| App fit | 25 |
|
||||||
|
| Content virality | 20 |
|
||||||
|
| ASO/search value | 15 |
|
||||||
|
| Product learning value | 10 |
|
||||||
|
|
||||||
|
Prioritize urgent, visual, diagnosis-driven pains where GreenLens can credibly
|
||||||
|
help the user decide what to check next.
|
||||||
|
|
||||||
|
## Weekly Output
|
||||||
|
|
||||||
|
Produce:
|
||||||
|
|
||||||
|
1. Top 20 pains.
|
||||||
|
2. Top 10 social hooks.
|
||||||
|
3. Top 5 ASO keyword opportunities.
|
||||||
|
4. Top 5 blog or landing page ideas.
|
||||||
|
5. Top 5 product issues or feature hypotheses.
|
||||||
|
6. Top 10 influencer angles.
|
||||||
|
|
||||||
|
## Codex Pain Mining Prompt
|
||||||
|
|
||||||
|
```text
|
||||||
|
Run the GreenLens Pro Pain Mining Machine.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- docs/automations/greenlens-pain-mining-machine.md
|
||||||
|
- app-store-aso skill
|
||||||
|
- content-strategy skill
|
||||||
|
|
||||||
|
Input source: [reviews/comments/export/pasted text]
|
||||||
|
Market: [US / DE / global]
|
||||||
|
Platform focus: [iOS / Android / both]
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
1. Extract raw plant-owner pains.
|
||||||
|
2. Cluster them into the GreenLens pain taxonomy.
|
||||||
|
3. Score each pain by urgency, app fit, virality, ASO value, and product learning.
|
||||||
|
4. Convert winners into:
|
||||||
|
- social hooks
|
||||||
|
- ASO keyword ideas
|
||||||
|
- blog/landing page ideas
|
||||||
|
- product issues
|
||||||
|
- influencer outreach angles
|
||||||
|
|
||||||
|
Do not invent source quotes. If evidence is weak, label it as hypothesis.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# GreenLens Pain Mining Report
|
||||||
|
|
||||||
|
## Source Summary
|
||||||
|
|
||||||
|
## Top Pains
|
||||||
|
| Rank | Pain | Source | Score | Why it matters |
|
||||||
|
|---|---|---|---:|---|
|
||||||
|
|
||||||
|
## Hook Backlog
|
||||||
|
|
||||||
|
## ASO Opportunities
|
||||||
|
|
||||||
|
## Product Issues
|
||||||
|
|
||||||
|
## Influencer Angles
|
||||||
|
|
||||||
|
## Next Actions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Product Issue Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Title: [Feature or improvement]
|
||||||
|
|
||||||
|
User pain:
|
||||||
|
[What the user is struggling with]
|
||||||
|
|
||||||
|
Hypothesis:
|
||||||
|
If GreenLens [change], users will [outcome].
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
- [criterion]
|
||||||
|
- [criterion]
|
||||||
|
- [criterion]
|
||||||
|
|
||||||
|
Measurement:
|
||||||
|
- activation
|
||||||
|
- scan completion
|
||||||
|
- paywall conversion
|
||||||
|
- retention
|
||||||
|
```
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- Every recommendation traces back to a real pain or explicitly marked hypothesis.
|
||||||
|
- Top pains can feed both ASO and social content.
|
||||||
|
- Product issues are concrete enough for GitHub.
|
||||||
|
|
||||||
139
docs/automations/greenlens-viral-slideshow-machine.md
Normal file
139
docs/automations/greenlens-viral-slideshow-machine.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# GreenLens Pro Viral Slideshow Machine
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Convert validated plant pains into TikTok, Instagram, and Canva-ready slideshow
|
||||||
|
assets that drive awareness and app downloads.
|
||||||
|
|
||||||
|
## Plugins And Skills
|
||||||
|
|
||||||
|
| Need | Use |
|
||||||
|
|---|---|
|
||||||
|
| Creative generation | Canva plugin |
|
||||||
|
| Hook and caption writing | `copywriting`, `ad-creative` skills |
|
||||||
|
| Content planning | `content-strategy` skill |
|
||||||
|
| ASO alignment | `app-store-aso` skill |
|
||||||
|
| Creator briefs and outreach | Gmail/Fyxer plugin |
|
||||||
|
|
||||||
|
## Required Input
|
||||||
|
|
||||||
|
Use outputs from `greenlens-pain-mining-machine.md`:
|
||||||
|
|
||||||
|
- pain cluster
|
||||||
|
- urgency score
|
||||||
|
- source evidence
|
||||||
|
- target audience
|
||||||
|
- desired CTA
|
||||||
|
- app positioning angle
|
||||||
|
|
||||||
|
## Content Pillars
|
||||||
|
|
||||||
|
- Diagnosis before guessing
|
||||||
|
- Overwatering mistakes
|
||||||
|
- Yellow leaves
|
||||||
|
- Brown spots
|
||||||
|
- Root rot warnings
|
||||||
|
- Beginner plant rescue
|
||||||
|
- Plant symptoms explained
|
||||||
|
- "Do not water yet" warnings
|
||||||
|
- App scan/use-case demos
|
||||||
|
|
||||||
|
## Slideshow Formula
|
||||||
|
|
||||||
|
1. Hook: direct warning, contradiction, or curiosity.
|
||||||
|
2. Problem: show the common wrong assumption.
|
||||||
|
3. Explanation: simple plant-care reason.
|
||||||
|
4. Check: what the user should inspect first.
|
||||||
|
5. Risk: what happens if they guess.
|
||||||
|
6. GreenLens bridge: scan or diagnose before acting.
|
||||||
|
7. CTA: download, scan, or save.
|
||||||
|
|
||||||
|
## Hook Patterns
|
||||||
|
|
||||||
|
- "Do not water your plant before checking this."
|
||||||
|
- "Yellow leaves do not always mean your plant is thirsty."
|
||||||
|
- "Brown spots can mean more than sunburn."
|
||||||
|
- "Your plant was warning you before it started dying."
|
||||||
|
- "Overwatering often looks like underwatering."
|
||||||
|
- "Scan before you guess."
|
||||||
|
|
||||||
|
## Canva Direction
|
||||||
|
|
||||||
|
Use GreenLens as a calm diagnosis-first plant app:
|
||||||
|
|
||||||
|
- natural plant photography or close-up symptom imagery
|
||||||
|
- clear readable overlay text
|
||||||
|
- botanical but not decorative-only
|
||||||
|
- show symptoms clearly
|
||||||
|
- app screenshot or phone mockup only when it explains the action
|
||||||
|
- avoid vague wellness aesthetics that do not show the plant problem
|
||||||
|
|
||||||
|
## Codex Slideshow Prompt
|
||||||
|
|
||||||
|
```text
|
||||||
|
Run the GreenLens Pro Viral Slideshow Machine.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- docs/automations/greenlens-pain-mining-machine.md
|
||||||
|
- docs/automations/greenlens-viral-slideshow-machine.md
|
||||||
|
- app-store-aso skill
|
||||||
|
- content-strategy skill
|
||||||
|
- copywriting/ad-creative skills
|
||||||
|
|
||||||
|
Input pain cluster: [pain]
|
||||||
|
Audience: [beginner plant owners / plant rescue followers / houseplant collectors]
|
||||||
|
CTA: [Download GreenLens Pro / Scan your plant / Save this checklist]
|
||||||
|
Channel: [TikTok / Instagram / both]
|
||||||
|
Quantity: [number]
|
||||||
|
|
||||||
|
Return for each concept:
|
||||||
|
1. hook
|
||||||
|
2. 5-7 slide script
|
||||||
|
3. visual direction per slide
|
||||||
|
4. Canva prompt
|
||||||
|
5. caption
|
||||||
|
6. hashtags
|
||||||
|
7. ASO keyword tie-in
|
||||||
|
8. creator brief version
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Keep claims educational, not medical/certain beyond evidence.
|
||||||
|
- Do not promise perfect diagnosis.
|
||||||
|
- Make the symptom visually inspectable.
|
||||||
|
- The app CTA should feel like the next practical step, not a hard sell.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# GreenLens Slideshow Pack: [Pain Cluster]
|
||||||
|
|
||||||
|
## Concept 1: [Hook]
|
||||||
|
|
||||||
|
### Slides
|
||||||
|
1. [text] -- [visual]
|
||||||
|
2. [text] -- [visual]
|
||||||
|
3. [text] -- [visual]
|
||||||
|
4. [text] -- [visual]
|
||||||
|
5. [text] -- [visual]
|
||||||
|
6. [text] -- [visual]
|
||||||
|
7. [text] -- [visual]
|
||||||
|
|
||||||
|
### Canva Prompt
|
||||||
|
|
||||||
|
### Caption
|
||||||
|
|
||||||
|
### Hashtags
|
||||||
|
|
||||||
|
### ASO Tie-In
|
||||||
|
|
||||||
|
### Creator Brief
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quality Bar
|
||||||
|
|
||||||
|
- The first slide must be understandable in under 2 seconds.
|
||||||
|
- Every slide should be shorter than 12 words when possible.
|
||||||
|
- The visual must show the symptom or action, not just a plant mood shot.
|
||||||
|
- The final CTA should match the pain: scan, check, save, or download.
|
||||||
|
|
||||||
57
docs/automations/qrmaster-broken-link-cta-checker.md
Normal file
57
docs/automations/qrmaster-broken-link-cta-checker.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# QRMaster Broken Link + CTA Checker
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Catch broken internal links and broken conversion CTAs on `main`, especially
|
||||||
|
after direct edits without a pull request.
|
||||||
|
|
||||||
|
## What It Checks
|
||||||
|
|
||||||
|
- Static internal `href` values in source files.
|
||||||
|
- Static `router.push("/...")` destinations.
|
||||||
|
- Internal links against known Next.js app routes and files in `public/`.
|
||||||
|
- CTA-like links such as "Get started", "Create QR", "Start free",
|
||||||
|
"Generate QR", "Pricing", and "Upgrade".
|
||||||
|
- Pages that appear to have no obvious CTA link.
|
||||||
|
|
||||||
|
## Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run check:links
|
||||||
|
```
|
||||||
|
|
||||||
|
The command prints a JSON report. It exits with a non-zero status if broken
|
||||||
|
internal links or broken CTA links are found.
|
||||||
|
|
||||||
|
## Known Limits
|
||||||
|
|
||||||
|
- Dynamic CMS/blog slugs are allowed by prefix and not fully validated.
|
||||||
|
- Runtime-only links built from variables are skipped.
|
||||||
|
- External links are not checked by this local script.
|
||||||
|
- This is a fast safety check, not a full crawl of the deployed website.
|
||||||
|
|
||||||
|
## Codex Automation Prompt
|
||||||
|
|
||||||
|
```text
|
||||||
|
Run the QRMaster Broken Link + CTA Checker.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- docs/automations/qrmaster-broken-link-cta-checker.md
|
||||||
|
- scripts/check-links-and-ctas.js
|
||||||
|
|
||||||
|
Run npm run check:links. Review the JSON report and summarize:
|
||||||
|
1. broken internal links
|
||||||
|
2. broken CTA links
|
||||||
|
3. important pages without obvious CTAs
|
||||||
|
4. concrete fixes with file paths
|
||||||
|
|
||||||
|
If the script fails, inspect the listed files and propose the smallest safe fix.
|
||||||
|
Do not modify production configuration automatically.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- No broken internal links.
|
||||||
|
- No broken CTA links.
|
||||||
|
- Important marketing, tool, and pricing pages have a clear CTA.
|
||||||
|
|
||||||
163
docs/automations/qrmaster-pr-seo-review.md
Normal file
163
docs/automations/qrmaster-pr-seo-review.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# QRMaster PR SEO Review
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Catch SEO, conversion, content-quality, and technical issues before a QRMaster
|
||||||
|
page change is merged.
|
||||||
|
|
||||||
|
## Use When
|
||||||
|
|
||||||
|
- A PR changes landing pages, tool pages, comparison pages, blog posts, metadata,
|
||||||
|
schema, sitemap behavior, internal links, pricing copy, or CTAs.
|
||||||
|
- Codex generated new SEO pages from `marketing/programmatic-seo-top-50.md`.
|
||||||
|
- Existing pages were refreshed from Google Search Console or keyword data.
|
||||||
|
|
||||||
|
## Plugins And Skills
|
||||||
|
|
||||||
|
| Need | Use |
|
||||||
|
|---|---|
|
||||||
|
| Diff and PR review | GitHub plugin |
|
||||||
|
| Code/content inspection | Codex |
|
||||||
|
| SEO/AEO review | `ai-seo` skill |
|
||||||
|
| Content intent and cluster fit | `content-strategy` skill |
|
||||||
|
| Build/lint verification | GitHub Actions, `qa` skill |
|
||||||
|
| Careful merge decision | `careful` skill |
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
|
||||||
|
### 1. Technical SEO
|
||||||
|
|
||||||
|
- Exactly one H1 on the rendered page.
|
||||||
|
- Meta title exists and is specific to the page intent.
|
||||||
|
- Meta title places the primary keyword near the start where natural.
|
||||||
|
- Meta title stays under roughly 60 characters unless there is a clear reason.
|
||||||
|
- Meta description exists and promises the right outcome.
|
||||||
|
- Meta description includes the target keyword naturally, states the user benefit,
|
||||||
|
and stays concise enough to avoid likely truncation.
|
||||||
|
- Canonical URL is correct.
|
||||||
|
- Page is not accidentally noindexed.
|
||||||
|
- FAQ, Article, Product, Breadcrumb, or HowTo schema is valid where used.
|
||||||
|
- Sitemap and internal routing include the page where required.
|
||||||
|
- No broken internal links or CTA links.
|
||||||
|
- Language and locale are consistent.
|
||||||
|
- URL slug is short, descriptive, hyphenated, and does not include stale dates.
|
||||||
|
- Meaningful images have descriptive alt text and useful filenames where local
|
||||||
|
image handling allows it.
|
||||||
|
- Large visual assets are compressed or already optimized.
|
||||||
|
- Mobile layout is readable and CTAs are tappable.
|
||||||
|
- The page does not introduce obvious Core Web Vitals risks.
|
||||||
|
- Robots rules do not block important pages or desired AI/search crawlers.
|
||||||
|
|
||||||
|
### 2. Search Intent
|
||||||
|
|
||||||
|
- The first screen makes it obvious the page answers the target query.
|
||||||
|
- The opening paragraph states the problem directly.
|
||||||
|
- The page matches one primary intent only.
|
||||||
|
- The content is not a generic rewrite of another QRMaster page.
|
||||||
|
- The page includes concrete examples for its audience or use case.
|
||||||
|
- The target keyword intent is labeled as informational, commercial,
|
||||||
|
transactional, or navigational.
|
||||||
|
- The page covers the related subtopics a search or AI system would fan out to
|
||||||
|
for the main query.
|
||||||
|
- Each H2/H3 section answers the heading directly in the first sentence before
|
||||||
|
adding background or nuance.
|
||||||
|
|
||||||
|
### 3. QRMaster Conversion Fit
|
||||||
|
|
||||||
|
- Primary CTA is visible early.
|
||||||
|
- CTA copy matches the use case, not just generic "Get started".
|
||||||
|
- The page explains why dynamic QR codes matter when links change after printing.
|
||||||
|
- Scan analytics or tracking is mentioned when relevant.
|
||||||
|
- Privacy/GDPR positioning is included where tracking is discussed.
|
||||||
|
- The copy avoids unsupported claims.
|
||||||
|
|
||||||
|
### 4. Internal Linking
|
||||||
|
|
||||||
|
- New page links to the relevant money page:
|
||||||
|
- `/dynamic-qr-code-generator`
|
||||||
|
- `/qr-code-tracking`
|
||||||
|
- `/bulk-qr-code-generator`
|
||||||
|
- `/pricing`
|
||||||
|
- relevant `/tools/...` page
|
||||||
|
- Existing related pages should link back to the new page.
|
||||||
|
- Anchor text is natural and varied.
|
||||||
|
|
||||||
|
### 5. AI Search Extractability
|
||||||
|
|
||||||
|
- Important answer blocks are self-contained.
|
||||||
|
- Comparison content uses tables where useful.
|
||||||
|
- FAQ questions are written in natural user language.
|
||||||
|
- Definitions answer the query in 40-60 words when possible.
|
||||||
|
- Claims that need evidence include a source or are framed as product positioning.
|
||||||
|
- Sections are focused on one question or subtopic at a time.
|
||||||
|
- Bullet lists, tables, and short paragraphs are used where they improve
|
||||||
|
extraction and scanning.
|
||||||
|
- The page can be cited by AI systems without relying on surrounding context.
|
||||||
|
|
||||||
|
### 6. E-E-A-T And Quality
|
||||||
|
|
||||||
|
- Content is accurate, current, and not copied from competitors.
|
||||||
|
- Any competitor, pricing, legal, privacy, or compliance claim is verified or
|
||||||
|
clearly avoided.
|
||||||
|
- The page adds QRMaster-specific value, examples, workflows, or product context.
|
||||||
|
- The tone stays direct, useful, and trustworthy.
|
||||||
|
|
||||||
|
## Codex Review Prompt
|
||||||
|
|
||||||
|
```text
|
||||||
|
Review this QRMaster PR as an SEO, conversion, and technical quality gate.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- docs/automations/qrmaster-pr-seo-review.md
|
||||||
|
- .agents/product-marketing-context.md
|
||||||
|
- marketing/programmatic-seo-top-50.md
|
||||||
|
|
||||||
|
Check the diff for:
|
||||||
|
1. technical SEO issues
|
||||||
|
2. search intent mismatch
|
||||||
|
3. weak or missing CTA
|
||||||
|
4. duplicate/thin content
|
||||||
|
5. missing internal links
|
||||||
|
6. invalid or missing schema
|
||||||
|
7. weak AI/agentic-search extractability
|
||||||
|
8. missing visual/mobile/performance considerations
|
||||||
|
9. build/lint risks
|
||||||
|
|
||||||
|
Return findings ordered by severity. For each finding include:
|
||||||
|
- file path
|
||||||
|
- exact line if possible
|
||||||
|
- why it matters
|
||||||
|
- concrete fix
|
||||||
|
|
||||||
|
Also include:
|
||||||
|
- merge recommendation: approve / request changes
|
||||||
|
- required follow-up tasks
|
||||||
|
```
|
||||||
|
|
||||||
|
## Review Output Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## QRMaster PR SEO Review
|
||||||
|
|
||||||
|
Decision: Request changes
|
||||||
|
|
||||||
|
### Findings
|
||||||
|
1. [High] Missing internal link to `/dynamic-qr-code-generator`
|
||||||
|
2. [Medium] CTA is too generic for the target intent
|
||||||
|
3. [Low] FAQ question overlaps with another page
|
||||||
|
|
||||||
|
### Required Fixes
|
||||||
|
- Add contextual link from the "after printing" section to `/dynamic-qr-code-generator`.
|
||||||
|
- Change CTA from "Get started" to "Create an editable QR code for your flyer".
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- Build:
|
||||||
|
- Lint:
|
||||||
|
- Link/schema check:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- No high-severity SEO or conversion findings remain.
|
||||||
|
- Build and lint pass.
|
||||||
|
- The PR has a clear human approval before merge.
|
||||||
211
docs/automations/qrmaster-seo-sprint-machine.md
Normal file
211
docs/automations/qrmaster-seo-sprint-machine.md
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
# QRMaster SEO Sprint Machine
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Run a weekly controlled SEO sprint that chooses the right pages, creates or
|
||||||
|
updates them, adds internal links, and ships through a reviewed PR.
|
||||||
|
|
||||||
|
## Why This Exists
|
||||||
|
|
||||||
|
QRMaster should not publish random daily content. The goal is to build
|
||||||
|
commercially useful SEO clusters around dynamic QR codes, tracking, tool pages,
|
||||||
|
comparison pages, and industry workflows.
|
||||||
|
|
||||||
|
## Plugins And Skills
|
||||||
|
|
||||||
|
| Need | Use |
|
||||||
|
|---|---|
|
||||||
|
| Repository changes and PRs | GitHub plugin |
|
||||||
|
| Keyword and performance imports | Coupler, CSV exports, Google Search Console export |
|
||||||
|
| Page creation and refactors | Codex |
|
||||||
|
| SEO/content planning | `content-strategy`, `ai-seo` skills |
|
||||||
|
| Copy generation | `copywriting` skill |
|
||||||
|
| Verification | GitHub Actions, `qa` skill |
|
||||||
|
|
||||||
|
## Weekly Inputs
|
||||||
|
|
||||||
|
- Current keyword backlog:
|
||||||
|
- `marketing/programmatic-seo-top-50.md`
|
||||||
|
- `marketing/keyword-strategy-seo-plan.md`
|
||||||
|
- `seo-keywords.csv`
|
||||||
|
- Existing product positioning:
|
||||||
|
- `.agents/product-marketing-context.md`
|
||||||
|
- Performance data when available:
|
||||||
|
- Google Search Console export
|
||||||
|
- signup/conversion report
|
||||||
|
- top landing pages by traffic
|
||||||
|
- Sprint focus:
|
||||||
|
- Dynamic QR
|
||||||
|
- Tracking/analytics
|
||||||
|
- Restaurant/menu QR
|
||||||
|
- Print marketing
|
||||||
|
- Bulk QR
|
||||||
|
- Comparison/alternatives
|
||||||
|
|
||||||
|
## Scoring Model
|
||||||
|
|
||||||
|
Score each candidate from 0-100:
|
||||||
|
|
||||||
|
| Factor | Weight |
|
||||||
|
|---|---:|
|
||||||
|
| Product fit | 30 |
|
||||||
|
| Commercial intent | 25 |
|
||||||
|
| Differentiation potential | 15 |
|
||||||
|
| Cluster leverage | 10 |
|
||||||
|
| Search winability | 10 |
|
||||||
|
| Production effort | 10 |
|
||||||
|
|
||||||
|
Do not select pages only because they have volume. Prefer pages where QRMaster
|
||||||
|
can naturally sell dynamic QR, scan tracking, bulk creation, or privacy-first
|
||||||
|
analytics.
|
||||||
|
|
||||||
|
## On-Page And Agentic Search Rules
|
||||||
|
|
||||||
|
Every new or refreshed page must follow these checks before review:
|
||||||
|
|
||||||
|
- Identify the primary keyword and intent type: informational, commercial,
|
||||||
|
transactional, or navigational.
|
||||||
|
- Cover the query fan-out: list the related subtopics an AI/search system would
|
||||||
|
need to answer the query well.
|
||||||
|
- Put the primary keyword naturally near the start of the title tag and H1.
|
||||||
|
- Keep title tags under roughly 60 characters when possible.
|
||||||
|
- Keep meta descriptions concise, benefit-led, and naturally keyword-aligned.
|
||||||
|
- Use one clear H1 and a logical H2/H3 hierarchy.
|
||||||
|
- Start each section with a direct answer to the heading.
|
||||||
|
- Use short paragraphs, bullets, and comparison tables where they improve
|
||||||
|
scanning and AI extraction.
|
||||||
|
- Add descriptive internal links with natural anchor text.
|
||||||
|
- Add useful visuals, screenshots, or examples where the page needs them.
|
||||||
|
- Add schema when the page type supports it.
|
||||||
|
- Check mobile readability, CTA tap targets, and obvious speed risks.
|
||||||
|
- Verify robots/indexing assumptions for important SEO pages.
|
||||||
|
|
||||||
|
## Default Weekly Sprint
|
||||||
|
|
||||||
|
1. Select one cluster.
|
||||||
|
2. Create 3 new pages.
|
||||||
|
3. Refresh 2 existing pages with impressions, weak CTR, or position 8-20.
|
||||||
|
4. Add internal links in both directions.
|
||||||
|
5. Create one GitHub PR.
|
||||||
|
6. Run `qrmaster-pr-seo-review.md`.
|
||||||
|
7. Produce social and outreach drafts after the PR is ready.
|
||||||
|
|
||||||
|
## Page Types
|
||||||
|
|
||||||
|
### Tool Pages
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `/tools/pdf-qr-code`
|
||||||
|
- `/tools/vcard-qr-code`
|
||||||
|
- `/tools/wifi-qr-code`
|
||||||
|
- `/tools/menu-qr-code`
|
||||||
|
- `/tools/google-review-qr-code`
|
||||||
|
|
||||||
|
Must include:
|
||||||
|
|
||||||
|
- direct tool-oriented hero
|
||||||
|
- use cases
|
||||||
|
- dynamic vs static guidance
|
||||||
|
- FAQ
|
||||||
|
- CTA into the app
|
||||||
|
- internal links to related use cases
|
||||||
|
|
||||||
|
### Industry Workflow Pages
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `/qr-code-for/restaurants/menu-updates`
|
||||||
|
- `/qr-code-for/events/check-in`
|
||||||
|
- `/qr-code-for/real-estate/open-house-flyers`
|
||||||
|
|
||||||
|
Must include:
|
||||||
|
|
||||||
|
- specific audience pain
|
||||||
|
- example workflow
|
||||||
|
- print-risk or tracking angle
|
||||||
|
- CTA matching the industry
|
||||||
|
|
||||||
|
### Comparison Pages
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `/compare/dynamic-vs-static-qr-codes`
|
||||||
|
- `/compare/free-vs-paid-qr-code-generator`
|
||||||
|
- `/compare/flowcode-alternative`
|
||||||
|
|
||||||
|
Must include:
|
||||||
|
|
||||||
|
- comparison table
|
||||||
|
- fair positioning
|
||||||
|
- current facts verified before publishing
|
||||||
|
- "who this is best for" section
|
||||||
|
- CTA to the best-fit QRMaster feature
|
||||||
|
|
||||||
|
## Codex Sprint Prompt
|
||||||
|
|
||||||
|
```text
|
||||||
|
Run the QRMaster SEO Sprint Machine.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- docs/automations/qrmaster-seo-sprint-machine.md
|
||||||
|
- docs/automations/qrmaster-pr-seo-review.md
|
||||||
|
- .agents/product-marketing-context.md
|
||||||
|
- marketing/programmatic-seo-top-50.md
|
||||||
|
- marketing/keyword-strategy-seo-plan.md
|
||||||
|
|
||||||
|
Sprint focus: [cluster]
|
||||||
|
Target output: 3 new SEO/tool pages, 2 page refreshes, internal links, and one PR-ready diff.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Follow existing code and page patterns.
|
||||||
|
- Do not invent competitor pricing or claims.
|
||||||
|
- Prioritize dynamic QR, edit-after-print, analytics, bulk, and privacy-first messaging.
|
||||||
|
- Add metadata, FAQ/schema where local patterns support it.
|
||||||
|
- Apply the on-page and agentic search rules from the automation doc.
|
||||||
|
- Keep pages specific enough to avoid thin programmatic content.
|
||||||
|
- Run build/lint or explain why not.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
1. selected pages and scoring
|
||||||
|
2. target keyword, intent, and fan-out subtopics per page
|
||||||
|
3. files changed
|
||||||
|
4. internal links added
|
||||||
|
5. PR summary
|
||||||
|
6. SEO review status
|
||||||
|
7. follow-up social/outreach package
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sprint Output Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# QRMaster SEO Sprint: [Cluster]
|
||||||
|
|
||||||
|
## Selected Work
|
||||||
|
| Type | URL | Score | Reason |
|
||||||
|
|---|---|---:|---|
|
||||||
|
|
||||||
|
## Keyword Intent And Fan-Out
|
||||||
|
|
||||||
|
| URL | Primary keyword | Intent | Fan-out subtopics |
|
||||||
|
|---|---|---|---|
|
||||||
|
|
||||||
|
## New Pages
|
||||||
|
|
||||||
|
## Updated Pages
|
||||||
|
|
||||||
|
## Internal Links
|
||||||
|
|
||||||
|
## PR Summary
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
## Social/Outreach Follow-Up
|
||||||
|
```
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- Each new page has clear commercial intent or cluster leverage.
|
||||||
|
- Refreshed pages have a measurable reason for the update.
|
||||||
|
- Internal links support money pages.
|
||||||
|
- PR SEO Review passes before merge.
|
||||||
23
meta-fix.js
Normal file
23
meta-fix.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { config } from 'dotenv';
|
||||||
|
config();
|
||||||
|
|
||||||
|
const TOKEN = process.env.META_ACCESS_TOKEN;
|
||||||
|
const BASE = 'https://graph.facebook.com/v21.0';
|
||||||
|
|
||||||
|
async function api(path, method = 'GET', body) {
|
||||||
|
const url = new URL(`${BASE}/${path}`);
|
||||||
|
url.searchParams.set('access_token', TOKEN);
|
||||||
|
const res = await fetch(url.toString(), {
|
||||||
|
method,
|
||||||
|
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(JSON.stringify(data));
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Pausing orphaned ad sets...');
|
||||||
|
await api('6968509692127', 'POST', { status: 'PAUSED' });
|
||||||
|
await api('6958800756527', 'POST', { status: 'PAUSED' });
|
||||||
|
console.log('Done: Paused 2 orphaned ad sets (New Sales Ad Set, New Sales ad set)');
|
||||||
BIN
quora_suche.txt
Normal file
BIN
quora_suche.txt
Normal file
Binary file not shown.
49
read-inbox.mjs
Normal file
49
read-inbox.mjs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import tls from 'node:tls';
|
||||||
|
|
||||||
|
function readMessages(seqs) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const socket = tls.connect({ host: 'imap.qrmaster.net', port: 993 }, () => {
|
||||||
|
let buf = '';
|
||||||
|
let step = 0;
|
||||||
|
const results = {};
|
||||||
|
|
||||||
|
socket.on('data', (chunk) => {
|
||||||
|
buf += chunk.toString();
|
||||||
|
const lines = buf.split('\r\n');
|
||||||
|
buf = lines.pop();
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (step === 0 && line.includes('* OK')) {
|
||||||
|
socket.write(`A1 LOGIN timo@qrmaster.net fiesta\r\n`);
|
||||||
|
step = 1;
|
||||||
|
} else if (step === 1 && line.startsWith('A1 OK')) {
|
||||||
|
socket.write(`A2 SELECT INBOX\r\n`);
|
||||||
|
step = 2;
|
||||||
|
} else if (step === 2 && line.startsWith('A2 OK')) {
|
||||||
|
socket.write(`A3 FETCH ${seqs.join(',')} (BODY.PEEK[1])\r\n`);
|
||||||
|
step = 3;
|
||||||
|
} else if (step === 3) {
|
||||||
|
if (line.startsWith('A3 OK')) {
|
||||||
|
socket.write(`A4 LOGOUT\r\n`);
|
||||||
|
resolve(results);
|
||||||
|
}
|
||||||
|
const m = line.match(/^\* (\d+) FETCH/);
|
||||||
|
if (m) results[m[1]] = { body: '' };
|
||||||
|
const curr = Object.keys(results).at(-1);
|
||||||
|
if (curr && line && !line.match(/^\* \d+ FETCH/) && !line.startsWith('A3') && line !== ')') {
|
||||||
|
results[curr].body += line + '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', reject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = await readMessages([954, 990, 997]);
|
||||||
|
for (const [seq, msg] of Object.entries(r)) {
|
||||||
|
console.log(`\n=== SEQ ${seq} ===`);
|
||||||
|
console.log(msg.body.slice(0, 1500));
|
||||||
|
}
|
||||||
249
scripts/check-links-and-ctas.js
Normal file
249
scripts/check-links-and-ctas.js
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const root = process.cwd();
|
||||||
|
const scanDirs = ["src", "marketing", "articles", "blog-posts-improved"];
|
||||||
|
const sourceExtensions = new Set([".ts", ".tsx", ".js", ".jsx", ".md", ".mdx"]);
|
||||||
|
const publicDir = path.join(root, "public");
|
||||||
|
const appDir = path.join(root, "src", "app");
|
||||||
|
|
||||||
|
const ignoredPrefixes = [
|
||||||
|
"/api/",
|
||||||
|
"/_next/",
|
||||||
|
"/auth/",
|
||||||
|
"/r/",
|
||||||
|
"/qr/",
|
||||||
|
"/scan/",
|
||||||
|
];
|
||||||
|
|
||||||
|
const knownDynamicPrefixes = [
|
||||||
|
"/blog/",
|
||||||
|
"/learn/",
|
||||||
|
"/authors/",
|
||||||
|
"/qr-code-for/",
|
||||||
|
"/use-cases/",
|
||||||
|
];
|
||||||
|
|
||||||
|
const ctaPatterns = [
|
||||||
|
/get started/i,
|
||||||
|
/start free/i,
|
||||||
|
/try free/i,
|
||||||
|
/create.*qr/i,
|
||||||
|
/generate.*qr/i,
|
||||||
|
/sign up/i,
|
||||||
|
/pricing/i,
|
||||||
|
/upgrade/i,
|
||||||
|
/create.*free/i,
|
||||||
|
/start tracking/i,
|
||||||
|
/create.*editable/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
const nonConversionPageParts = [
|
||||||
|
"/contact/",
|
||||||
|
"/cookie-policy/",
|
||||||
|
"/privacy/",
|
||||||
|
"/press/",
|
||||||
|
"/authors/",
|
||||||
|
"/blog/",
|
||||||
|
"/newsletter/",
|
||||||
|
];
|
||||||
|
|
||||||
|
const findings = [];
|
||||||
|
const ctas = [];
|
||||||
|
|
||||||
|
function walk(dir, files = []) {
|
||||||
|
if (!fs.existsSync(dir)) return files;
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
if (entry.name === "node_modules" || entry.name === ".next" || entry.name === ".git") continue;
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walk(fullPath, files);
|
||||||
|
} else {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPosix(value) {
|
||||||
|
return value.split(path.sep).join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function routeFromPageFile(file) {
|
||||||
|
const rel = toPosix(path.relative(appDir, file));
|
||||||
|
if (!rel.endsWith("/page.tsx") && !rel.endsWith("/page.ts") && !rel.endsWith("/route.ts")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = rel.split("/");
|
||||||
|
parts.pop();
|
||||||
|
const routeParts = parts.filter((part) => {
|
||||||
|
if (!part) return false;
|
||||||
|
if (part.startsWith("(") && part.endsWith(")")) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (routeParts.some((part) => part.startsWith("[") && part.endsWith("]"))) return null;
|
||||||
|
return "/" + routeParts.join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectRoutes() {
|
||||||
|
const routes = new Set(["/"]);
|
||||||
|
for (const file of walk(appDir)) {
|
||||||
|
const route = routeFromPageFile(file);
|
||||||
|
if (route) routes.add(route === "/" ? "/" : route.replace(/\/$/, ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of walk(publicDir)) {
|
||||||
|
const rel = "/" + toPosix(path.relative(publicDir, file));
|
||||||
|
routes.add(rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHref(rawHref) {
|
||||||
|
if (!rawHref) return null;
|
||||||
|
let href = rawHref.trim();
|
||||||
|
if (!href || href.startsWith("#")) return null;
|
||||||
|
if (/^(https?:|mailto:|tel:|sms:|javascript:|data:)/i.test(href)) return null;
|
||||||
|
|
||||||
|
if (!href.startsWith("/")) return null;
|
||||||
|
href = href.split("#")[0].split("?")[0];
|
||||||
|
if (href.length > 1) href = href.replace(/\/$/, "");
|
||||||
|
return href || "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAllowedDynamicHref(href) {
|
||||||
|
if (ignoredPrefixes.some((prefix) => href.startsWith(prefix))) return true;
|
||||||
|
if (href.includes("[") || href.includes("${") || href.includes("`")) return true;
|
||||||
|
return knownDynamicPrefixes.some((prefix) => href.startsWith(prefix) && href !== prefix.replace(/\/$/, ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineNumber(content, index) {
|
||||||
|
return content.slice(0, index).split(/\r?\n/).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractHrefMatches(content) {
|
||||||
|
const matches = [];
|
||||||
|
const patterns = [
|
||||||
|
/href\s*=\s*["']([^"']+)["']/g,
|
||||||
|
/href\s*=\s*{\s*["']([^"']+)["']\s*}/g,
|
||||||
|
/router\.push\(\s*["']([^"']+)["']\s*\)/g,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
let match;
|
||||||
|
while ((match = pattern.exec(content)) !== null) {
|
||||||
|
matches.push({ href: match[1], index: match.index });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAnchors(content) {
|
||||||
|
const anchors = [];
|
||||||
|
const linkPattern = /<Link\b[\s\S]*?href\s*=\s*(?:["']([^"']+)["']|{\s*["']([^"']+)["']\s*})[\s\S]*?>([\s\S]*?)<\/Link>/g;
|
||||||
|
const anchorPattern = /<a\b[\s\S]*?href\s*=\s*(?:["']([^"']+)["']|{\s*["']([^"']+)["']\s*})[\s\S]*?>([\s\S]*?)<\/a>/g;
|
||||||
|
|
||||||
|
for (const pattern of [linkPattern, anchorPattern]) {
|
||||||
|
let match;
|
||||||
|
while ((match = pattern.exec(content)) !== null) {
|
||||||
|
const href = match[1] || match[2];
|
||||||
|
const text = match[3]
|
||||||
|
.replace(/<[^>]*>/g, " ")
|
||||||
|
.replace(/\{[^}]*\}/g, " ")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
anchors.push({ href, text, index: match.index });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return anchors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceFiles() {
|
||||||
|
const files = [];
|
||||||
|
for (const dir of scanDirs) {
|
||||||
|
for (const file of walk(path.join(root, dir))) {
|
||||||
|
if (sourceExtensions.has(path.extname(file))) files.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function check() {
|
||||||
|
const routes = collectRoutes();
|
||||||
|
|
||||||
|
for (const file of sourceFiles()) {
|
||||||
|
const content = fs.readFileSync(file, "utf8");
|
||||||
|
const rel = toPosix(path.relative(root, file));
|
||||||
|
|
||||||
|
for (const item of extractHrefMatches(content)) {
|
||||||
|
const href = normalizeHref(item.href);
|
||||||
|
if (!href) continue;
|
||||||
|
if (routes.has(href) || isAllowedDynamicHref(href)) continue;
|
||||||
|
|
||||||
|
findings.push({
|
||||||
|
type: "broken-internal-link",
|
||||||
|
file: rel,
|
||||||
|
line: lineNumber(content, item.index),
|
||||||
|
href,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const anchor of extractAnchors(content)) {
|
||||||
|
const text = anchor.text || "";
|
||||||
|
if (!ctaPatterns.some((pattern) => pattern.test(text))) continue;
|
||||||
|
|
||||||
|
const href = normalizeHref(anchor.href);
|
||||||
|
const status = !href
|
||||||
|
? "external-or-non-http"
|
||||||
|
: routes.has(href) || isAllowedDynamicHref(href)
|
||||||
|
? "ok"
|
||||||
|
: "broken";
|
||||||
|
|
||||||
|
ctas.push({
|
||||||
|
file: rel,
|
||||||
|
line: lineNumber(content, anchor.index),
|
||||||
|
text: text.slice(0, 100),
|
||||||
|
href: anchor.href,
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const brokenCtas = ctas.filter((cta) => cta.status === "broken");
|
||||||
|
const weakFiles = sourceFiles().filter((file) => {
|
||||||
|
const rel = toPosix(path.relative(root, file));
|
||||||
|
if (!rel.includes("src/app/") || !rel.endsWith("/page.tsx")) return false;
|
||||||
|
if (!rel.includes("(marketing)")) return false;
|
||||||
|
if (rel.includes("[")) return false;
|
||||||
|
if (nonConversionPageParts.some((part) => rel.includes(part))) return false;
|
||||||
|
|
||||||
|
const content = fs.readFileSync(file, "utf8");
|
||||||
|
return !extractAnchors(content).some((anchor) =>
|
||||||
|
ctaPatterns.some((pattern) => pattern.test(anchor.text || "")),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const report = {
|
||||||
|
checkedAt: new Date().toISOString(),
|
||||||
|
routeCount: routes.size,
|
||||||
|
filesChecked: sourceFiles().length,
|
||||||
|
brokenInternalLinks: findings,
|
||||||
|
ctaSummary: {
|
||||||
|
total: ctas.length,
|
||||||
|
broken: brokenCtas.length,
|
||||||
|
sample: ctas.slice(0, 50),
|
||||||
|
},
|
||||||
|
pagesWithoutObviousCta: weakFiles.map((file) => toPosix(path.relative(root, file))).slice(0, 100),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(JSON.stringify(report, null, 2));
|
||||||
|
|
||||||
|
if (findings.length > 0 || brokenCtas.length > 0) {
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check();
|
||||||
@@ -1004,49 +1004,51 @@ export default function CreatePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Foreground Color
|
Foreground Color
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
value={foregroundColor}
|
value={foregroundColor}
|
||||||
onChange={(e) => setForegroundColor(e.target.value)}
|
onChange={(e) => setForegroundColor(e.target.value)}
|
||||||
className="w-12 h-10 rounded border border-gray-300"
|
className="w-12 h-10 rounded border border-gray-300"
|
||||||
disabled={!canCustomizeColors}
|
disabled={!canCustomizeColors}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={foregroundColor}
|
value={foregroundColor}
|
||||||
onChange={(e) => setForegroundColor(e.target.value)}
|
onChange={(e) => setForegroundColor(e.target.value)}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
disabled={!canCustomizeColors}
|
disabled={!canCustomizeColors}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Background Color
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={backgroundColor}
|
|
||||||
onChange={(e) => setBackgroundColor(e.target.value)}
|
|
||||||
className="w-12 h-10 rounded border border-gray-300"
|
|
||||||
disabled={!canCustomizeColors}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={backgroundColor}
|
|
||||||
onChange={(e) => setBackgroundColor(e.target.value)}
|
|
||||||
className="flex-1"
|
|
||||||
disabled={!canCustomizeColors}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Background Color
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={backgroundColor}
|
||||||
|
onChange={(e) => setBackgroundColor(e.target.value)}
|
||||||
|
className="w-12 h-10 rounded border border-gray-300"
|
||||||
|
disabled={!canCustomizeColors}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={backgroundColor}
|
||||||
|
onChange={(e) => setBackgroundColor(e.target.value)}
|
||||||
|
className="flex-1"
|
||||||
|
disabled={!canCustomizeColors}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ export default function RootLayout({
|
|||||||
{children}
|
{children}
|
||||||
</Providers>
|
</Providers>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</body>
|
{/* impeccable-live-start */}
|
||||||
|
<script src="http://localhost:8400/live.js"></script>
|
||||||
|
{/* impeccable-live-end */}
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,280 +1,124 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { motion, Variants } from 'framer-motion';
|
import { motion, Variants } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
Link as LinkIcon,
|
Link as LinkIcon,
|
||||||
User,
|
User,
|
||||||
Mail,
|
Mail,
|
||||||
Calendar,
|
Calendar,
|
||||||
Facebook,
|
Facebook,
|
||||||
Instagram,
|
Instagram,
|
||||||
Phone,
|
Phone,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Type,
|
Type,
|
||||||
Music,
|
Music,
|
||||||
Twitter,
|
Twitter,
|
||||||
MessageCircle,
|
MessageCircle,
|
||||||
Wifi,
|
Wifi,
|
||||||
Youtube,
|
Youtube,
|
||||||
Bitcoin,
|
Bitcoin,
|
||||||
MapPin,
|
MapPin,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Video,
|
Video,
|
||||||
Users,
|
Users,
|
||||||
Barcode,
|
Barcode,
|
||||||
Star
|
Star
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const TOOLS = [
|
const TOOLS = [
|
||||||
{
|
{ icon: LinkIcon, name: 'URL', description: 'Open any website', href: '/tools/url-qr-code', color: 'text-blue-500', bg: 'bg-blue-50' },
|
||||||
icon: LinkIcon,
|
{ icon: User, name: 'vCard', description: 'Share contact details', href: '/tools/vcard-qr-code', color: 'text-rose-500', bg: 'bg-rose-50' },
|
||||||
name: 'URL',
|
{ icon: Type, name: 'Text', description: 'Display plain text', href: '/tools/text-qr-code', color: 'text-slate-600', bg: 'bg-slate-50' },
|
||||||
description: 'Open any website',
|
{ icon: Mail, name: 'Email', description: 'Send an email', href: '/tools/email-qr-code', color: 'text-red-500', bg: 'bg-red-50' },
|
||||||
href: '/tools/url-qr-code',
|
{ icon: MessageSquare, name: 'SMS', description: 'Send a text message', href: '/tools/sms-qr-code', color: 'text-green-500', bg: 'bg-green-50' },
|
||||||
color: 'text-blue-500',
|
{ icon: Wifi, name: 'WiFi', description: 'Connect to WiFi', href: '/tools/wifi-qr-code', color: 'text-indigo-500', bg: 'bg-indigo-50' },
|
||||||
bg: 'bg-blue-50'
|
{ icon: Bitcoin, name: 'Crypto', description: 'Receive payments', href: '/tools/crypto-qr-code', color: 'text-orange-500', bg: 'bg-orange-50' },
|
||||||
},
|
{ icon: Calendar, name: 'Event', description: 'Save calendar event', href: '/tools/event-qr-code', color: 'text-violet-500', bg: 'bg-violet-50' },
|
||||||
{
|
{ icon: Facebook, name: 'Facebook', description: 'Open Facebook page', href: '/tools/facebook-qr-code', color: 'text-blue-600', bg: 'bg-blue-50' },
|
||||||
icon: User,
|
{ icon: Instagram, name: 'Instagram', description: 'Open Instagram profile', href: '/tools/instagram-qr-code', color: 'text-pink-500', bg: 'bg-pink-50' },
|
||||||
name: 'vCard',
|
{ icon: Twitter, name: 'Twitter', description: 'Open Twitter profile', href: '/tools/twitter-qr-code', color: 'text-sky-500', bg: 'bg-sky-50' },
|
||||||
description: 'Share contact details',
|
{ icon: Youtube, name: 'YouTube', description: 'Open YouTube video', href: '/tools/youtube-qr-code', color: 'text-red-600', bg: 'bg-red-50' },
|
||||||
href: '/tools/vcard-qr-code',
|
{ icon: MessageCircle, name: 'WhatsApp', description: 'Send WhatsApp message', href: '/tools/whatsapp-qr-code', color: 'text-green-600', bg: 'bg-green-50' },
|
||||||
color: 'text-rose-500',
|
{ icon: Music, name: 'TikTok', description: 'Open TikTok profile', href: '/tools/tiktok-qr-code', color: 'text-slate-900', bg: 'bg-slate-100' },
|
||||||
bg: 'bg-rose-50'
|
{ icon: MapPin, name: 'Location', description: 'Share GPS coordinates', href: '/tools/location-qr-code', color: 'text-emerald-500', bg: 'bg-emerald-50' },
|
||||||
},
|
{ icon: Phone, name: 'Call', description: 'Start a phone call', href: '/tools/phone-qr-code', color: 'text-teal-500', bg: 'bg-teal-50' },
|
||||||
{
|
];
|
||||||
icon: Type,
|
|
||||||
name: 'Text',
|
const containerVariants: Variants = {
|
||||||
description: 'Display plain text',
|
hidden: { opacity: 0 },
|
||||||
href: '/tools/text-qr-code',
|
visible: {
|
||||||
color: 'text-slate-600',
|
opacity: 1,
|
||||||
bg: 'bg-slate-50'
|
transition: { staggerChildren: 0.05 }
|
||||||
},
|
}
|
||||||
{
|
};
|
||||||
icon: Mail,
|
|
||||||
name: 'Email',
|
const itemVariants: Variants = {
|
||||||
description: 'Send an email',
|
hidden: { opacity: 0, y: 16 },
|
||||||
href: '/tools/email-qr-code',
|
visible: {
|
||||||
color: 'text-red-500',
|
opacity: 1,
|
||||||
bg: 'bg-red-50'
|
y: 0,
|
||||||
},
|
transition: { duration: 0.4 }
|
||||||
{
|
}
|
||||||
icon: MessageSquare,
|
};
|
||||||
name: 'SMS',
|
|
||||||
description: 'Send a text message',
|
export function FreeToolsGrid() {
|
||||||
href: '/tools/sms-qr-code',
|
return (
|
||||||
color: 'text-green-500',
|
<section id="tools" className="py-24 bg-slate-50/50 border-t border-slate-100">
|
||||||
bg: 'bg-green-50'
|
<style dangerouslySetInnerHTML={{__html:`
|
||||||
},
|
.ftg-card { transition: transform 0.22s ease-out, box-shadow 0.22s ease-out, border-color 0.22s ease-out; }
|
||||||
{
|
.ftg-card:hover { transform: translateY(-8px); box-shadow: rgba(83,58,253,0.2) 0px 20px 40px -12px, rgba(0,0,0,0.08) 0px 8px 16px -8px; border-color: #533afd; }
|
||||||
icon: Wifi,
|
.ftg-icon { transition: transform 0.25s ease-out; }
|
||||||
name: 'WiFi',
|
.ftg-card:hover .ftg-icon { transform: rotate(12deg) scale(1.1); }
|
||||||
description: 'Connect to WiFi',
|
@media (prefers-reduced-motion: reduce) { .ftg-card, .ftg-icon { transition: none !important; } }
|
||||||
href: '/tools/wifi-qr-code',
|
`}}/>
|
||||||
color: 'text-indigo-500',
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
|
||||||
bg: 'bg-indigo-50'
|
<motion.div
|
||||||
},
|
initial={{ opacity: 0, y: 20 }}
|
||||||
{
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
icon: Bitcoin,
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
name: 'Crypto',
|
transition={{ duration: 0.5 }}
|
||||||
description: 'Receive payments',
|
className="text-center mb-16"
|
||||||
href: '/tools/crypto-qr-code',
|
>
|
||||||
color: 'text-orange-500',
|
<div className="flex flex-col md:flex-row items-center justify-center gap-3 mb-4">
|
||||||
bg: 'bg-orange-50'
|
<h2 className="text-3xl lg:text-4xl font-bold text-slate-900">More Free QR Code Tools</h2>
|
||||||
},
|
<div className="bg-gradient-to-r from-emerald-500 to-green-500 text-white px-3 py-1 rounded-full text-xs md:text-sm font-semibold shadow-lg shadow-emerald-500/20 flex items-center gap-2">
|
||||||
{
|
<span className="relative flex h-2 w-2">
|
||||||
icon: Calendar,
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-75"></span>
|
||||||
name: 'Event',
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-white"></span>
|
||||||
description: 'Save calendar event',
|
</span>
|
||||||
href: '/tools/event-qr-code',
|
Free Forever
|
||||||
color: 'text-violet-500',
|
</div>
|
||||||
bg: 'bg-violet-50'
|
</div>
|
||||||
},
|
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
|
||||||
{
|
Create specialized QR codes for every need. Completely free and no signup required.
|
||||||
icon: Facebook,
|
</p>
|
||||||
name: 'Facebook',
|
</motion.div>
|
||||||
description: 'Open Facebook page',
|
|
||||||
href: '/tools/facebook-qr-code',
|
<motion.div
|
||||||
color: 'text-blue-600',
|
variants={containerVariants}
|
||||||
bg: 'bg-blue-50'
|
initial="hidden"
|
||||||
},
|
whileInView="visible"
|
||||||
{
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
icon: Instagram,
|
className="grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6"
|
||||||
name: 'Instagram',
|
>
|
||||||
description: 'Open Instagram profile',
|
{TOOLS.map((tool) => (
|
||||||
href: '/tools/instagram-qr-code',
|
<motion.div key={tool.name} variants={itemVariants}>
|
||||||
color: 'text-pink-500',
|
<Link
|
||||||
bg: 'bg-pink-50'
|
href={tool.href}
|
||||||
},
|
className="ftg-card group flex flex-col items-center p-5 md:p-6 rounded-2xl border border-[#e5edf5] bg-white"
|
||||||
{
|
>
|
||||||
icon: Twitter,
|
<div className={`ftg-icon w-12 h-12 md:w-14 md:h-14 rounded-xl ${tool.bg} flex items-center justify-center mb-3 md:mb-4`}>
|
||||||
name: 'Twitter',
|
<tool.icon className={`w-6 h-6 md:w-7 md:h-7 ${tool.color}`} aria-hidden="true" />
|
||||||
description: 'Open Twitter profile',
|
</div>
|
||||||
href: '/tools/twitter-qr-code',
|
<h3 className="text-base md:text-lg font-semibold text-[#061b31] mb-0.5">{tool.name}</h3>
|
||||||
color: 'text-sky-500',
|
<p className="text-xs md:text-sm text-[#64748d] text-center">{tool.description}</p>
|
||||||
bg: 'bg-sky-50'
|
</Link>
|
||||||
},
|
</motion.div>
|
||||||
{
|
))}
|
||||||
icon: Youtube,
|
</motion.div>
|
||||||
name: 'YouTube',
|
</div>
|
||||||
description: 'Open YouTube video',
|
</section>
|
||||||
href: '/tools/youtube-qr-code',
|
);
|
||||||
color: 'text-red-600',
|
}
|
||||||
bg: 'bg-red-50'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: MessageCircle,
|
|
||||||
name: 'WhatsApp',
|
|
||||||
description: 'Send WhatsApp message',
|
|
||||||
href: '/tools/whatsapp-qr-code',
|
|
||||||
color: 'text-green-600',
|
|
||||||
bg: 'bg-green-50'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Music,
|
|
||||||
name: 'TikTok',
|
|
||||||
description: 'Open TikTok profile',
|
|
||||||
href: '/tools/tiktok-qr-code',
|
|
||||||
color: 'text-pink-600',
|
|
||||||
bg: 'bg-pink-50'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: MapPin,
|
|
||||||
name: 'Location',
|
|
||||||
description: 'Share GPS coordinates',
|
|
||||||
href: '/tools/geolocation-qr-code',
|
|
||||||
color: 'text-emerald-500',
|
|
||||||
bg: 'bg-emerald-50'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Phone,
|
|
||||||
name: 'Call',
|
|
||||||
description: 'Start a phone call',
|
|
||||||
href: '/tools/call-qr-code-generator',
|
|
||||||
color: 'text-violet-500',
|
|
||||||
bg: 'bg-violet-50'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: CreditCard,
|
|
||||||
name: 'PayPal',
|
|
||||||
description: 'Receive PayPal payments',
|
|
||||||
href: '/tools/paypal-qr-code',
|
|
||||||
color: 'text-blue-700',
|
|
||||||
bg: 'bg-blue-50'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Video,
|
|
||||||
name: 'Zoom',
|
|
||||||
description: 'Join Zoom meeting',
|
|
||||||
href: '/tools/zoom-qr-code',
|
|
||||||
color: 'text-sky-500',
|
|
||||||
bg: 'bg-sky-50'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Users,
|
|
||||||
name: 'Teams',
|
|
||||||
description: 'Join Teams meeting',
|
|
||||||
href: '/tools/teams-qr-code',
|
|
||||||
color: 'text-violet-500',
|
|
||||||
bg: 'bg-violet-50'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Barcode,
|
|
||||||
name: 'Barcode',
|
|
||||||
description: 'Create standard barcodes',
|
|
||||||
href: '/tools/barcode-generator',
|
|
||||||
color: 'text-slate-900',
|
|
||||||
bg: 'bg-slate-100'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Star,
|
|
||||||
name: 'Google Review',
|
|
||||||
description: 'Get more Google reviews',
|
|
||||||
href: '/tools/google-review-qr-code',
|
|
||||||
color: 'text-yellow-500',
|
|
||||||
bg: 'bg-yellow-50'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Animation variants
|
|
||||||
const containerVariants: Variants = {
|
|
||||||
hidden: { opacity: 0 },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
transition: {
|
|
||||||
staggerChildren: 0.05
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const itemVariants: Variants = {
|
|
||||||
hidden: { opacity: 0, y: 20 },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
transition: {
|
|
||||||
duration: 0.4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function FreeToolsGrid() {
|
|
||||||
return (
|
|
||||||
<section id="tools" className="py-24 bg-slate-50/50 border-t border-slate-100">
|
|
||||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true, margin: "-100px" }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
className="text-center mb-16"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col md:flex-row items-center justify-center gap-3 mb-4">
|
|
||||||
<h2 className="text-3xl lg:text-4xl font-bold text-slate-900">
|
|
||||||
More Free QR Code Tools
|
|
||||||
</h2>
|
|
||||||
<div className="bg-gradient-to-r from-emerald-500 to-green-500 text-white px-3 py-1 rounded-full text-xs md:text-sm font-semibold shadow-lg shadow-emerald-500/20 flex items-center gap-2">
|
|
||||||
<span className="relative flex h-2 w-2">
|
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-75"></span>
|
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-white"></span>
|
|
||||||
</span>
|
|
||||||
Free Forever
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
|
|
||||||
Create specialized QR codes for every need. Completely free and no signup required.
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
variants={containerVariants}
|
|
||||||
initial="hidden"
|
|
||||||
whileInView="visible"
|
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
|
||||||
className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-4 gap-4 md:gap-6"
|
|
||||||
>
|
|
||||||
{TOOLS.map((tool) => (
|
|
||||||
<motion.div key={tool.name} variants={itemVariants}>
|
|
||||||
<Link
|
|
||||||
href={tool.href}
|
|
||||||
className="group flex flex-col items-center p-5 md:p-6 rounded-2xl border border-slate-200/80 bg-white hover:border-primary-200 hover:shadow-xl hover:shadow-primary-500/10 transition-all duration-300"
|
|
||||||
>
|
|
||||||
<div className={`w-12 h-12 md:w-14 md:h-14 rounded-xl ${tool.bg} flex items-center justify-center mb-3 md:mb-4 group-hover:scale-110 transition-transform duration-300`}>
|
|
||||||
<tool.icon className={`w-6 h-6 md:w-7 md:h-7 ${tool.color}`} />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-base md:text-lg font-semibold text-slate-900 mb-0.5">
|
|
||||||
{tool.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs md:text-sm text-slate-600 text-center">
|
|
||||||
{tool.description}
|
|
||||||
</p>
|
|
||||||
</Link>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,212 +1,456 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { motion } from 'framer-motion';
|
||||||
import { motion } from 'framer-motion';
|
import { Globe, User, MapPin, Phone, FileText, Ticket, Smartphone, Star } from 'lucide-react';
|
||||||
import { Globe, User, MapPin, Phone, CheckCircle2, ArrowRight, FileText, Ticket, Smartphone, Star } from 'lucide-react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
const FlippingCard = ({ front, back, delay }: { front: any, back: any, delay: number }) => {
|
||||||
// Sub-component for the flipping effect
|
const [isFlipped, setIsFlipped] = useState(false);
|
||||||
const FlippingCard = ({ front, back, delay }: { front: any, back: any, delay: number }) => {
|
|
||||||
const [isFlipped, setIsFlipped] = useState(false);
|
useEffect(() => {
|
||||||
|
const initialTimeout = setTimeout(() => {
|
||||||
useEffect(() => {
|
setIsFlipped(true);
|
||||||
// Initial delay
|
const interval = setInterval(() => {
|
||||||
const initialTimeout = setTimeout(() => {
|
setIsFlipped(prev => !prev);
|
||||||
setIsFlipped(true); // First flip
|
}, 8000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
// Setup interval for subsequent flips
|
}, delay * 1000);
|
||||||
const interval = setInterval(() => {
|
return () => clearTimeout(initialTimeout);
|
||||||
setIsFlipped(prev => !prev);
|
}, [delay]);
|
||||||
}, 8000); // Toggle every 8 seconds to prevent overlap (4 cards * 2s gap)
|
|
||||||
|
return (
|
||||||
return () => clearInterval(interval);
|
<div className="relative h-32 w-full perspective-[1000px] group cursor-pointer">
|
||||||
}, delay * 1000);
|
<motion.div
|
||||||
|
animate={{ rotateY: isFlipped ? 180 : 0 }}
|
||||||
return () => clearTimeout(initialTimeout);
|
transition={{ duration: 0.6, type: "spring", stiffness: 260, damping: 20 }}
|
||||||
}, [delay]);
|
className="relative w-full h-full preserve-3d"
|
||||||
|
style={{ transformStyle: 'preserve-3d' }}
|
||||||
return (
|
>
|
||||||
<div className="relative h-32 w-full perspective-[1000px] group cursor-pointer">
|
<div
|
||||||
<motion.div
|
className="absolute inset-0 backface-hidden"
|
||||||
animate={{ rotateY: isFlipped ? 180 : 0 }}
|
style={{ backfaceVisibility: 'hidden', WebkitBackfaceVisibility: 'hidden' }}
|
||||||
transition={{ duration: 0.6, type: "spring", stiffness: 260, damping: 20 }}
|
>
|
||||||
className="relative w-full h-full preserve-3d"
|
<Card className="w-full h-full backdrop-blur-xl bg-white/70 border-white/50 shadow-xl shadow-gray-200/50 p-4 flex flex-col items-center justify-center hover:scale-105 transition-all duration-300">
|
||||||
style={{ transformStyle: 'preserve-3d' }}
|
<div className={`w-10 h-10 mb-3 rounded-xl ${front.color} flex items-center justify-center`}>
|
||||||
>
|
<front.icon className="w-5 h-5" />
|
||||||
{/* Front Face */}
|
</div>
|
||||||
<div
|
<p className="font-semibold text-gray-800 text-sm">{front.title}</p>
|
||||||
className="absolute inset-0 backface-hidden"
|
</Card>
|
||||||
style={{ backfaceVisibility: 'hidden', WebkitBackfaceVisibility: 'hidden' }}
|
</div>
|
||||||
>
|
|
||||||
<Card className="w-full h-full backdrop-blur-xl bg-white/70 border-white/50 shadow-xl shadow-gray-200/50 p-4 flex flex-col items-center justify-center hover:scale-105 transition-all duration-300">
|
<div
|
||||||
<div className={`w-10 h-10 mb-3 rounded-xl ${front.color} flex items-center justify-center`}>
|
className="absolute inset-0 backface-hidden"
|
||||||
<front.icon className="w-5 h-5" />
|
style={{
|
||||||
</div>
|
backfaceVisibility: 'hidden',
|
||||||
<p className="font-semibold text-gray-800 text-sm">{front.title}</p>
|
WebkitBackfaceVisibility: 'hidden',
|
||||||
</Card>
|
transform: 'rotateY(180deg)'
|
||||||
</div>
|
}}
|
||||||
|
>
|
||||||
{/* Back Face */}
|
<Card className="w-full h-full backdrop-blur-xl bg-white/80 border-white/60 shadow-xl shadow-blue-200/50 p-4 flex flex-col items-center justify-center hover:scale-105 transition-all duration-300">
|
||||||
<div
|
<div className={`w-10 h-10 mb-3 rounded-xl ${back.color} flex items-center justify-center`}>
|
||||||
className="absolute inset-0 backface-hidden"
|
<back.icon className="w-5 h-5" />
|
||||||
style={{
|
</div>
|
||||||
backfaceVisibility: 'hidden',
|
<p className="font-semibold text-gray-900 text-sm">{back.title}</p>
|
||||||
WebkitBackfaceVisibility: 'hidden',
|
</Card>
|
||||||
transform: 'rotateY(180deg)'
|
</div>
|
||||||
}}
|
</motion.div>
|
||||||
>
|
</div>
|
||||||
<Card className="w-full h-full backdrop-blur-xl bg-white/80 border-white/60 shadow-xl shadow-blue-200/50 p-4 flex flex-col items-center justify-center hover:scale-105 transition-all duration-300">
|
);
|
||||||
<div className={`w-10 h-10 mb-3 rounded-xl ${back.color} flex items-center justify-center`}>
|
};
|
||||||
<back.icon className="w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<p className="font-semibold text-gray-900 text-sm">{back.title}</p>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface HeroProps {
|
interface HeroProps {
|
||||||
t: any; // i18n translation function
|
t: any;
|
||||||
headingAs?: 'h1' | 'div';
|
headingAs?: 'h1' | 'div';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Hero: React.FC<HeroProps> = ({ t, headingAs = 'h1' }) => {
|
export const Hero: React.FC<HeroProps> = ({ t, headingAs = 'h1' }) => {
|
||||||
const HeadingTag = headingAs;
|
const HeadingTag = headingAs;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 pt-12 pb-20">
|
||||||
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
|
<style dangerouslySetInnerHTML={{__html:`
|
||||||
|
@keyframes dotFloat{0%{transform:translateY(0) translateX(0);opacity:0}20%{opacity:1}80%{opacity:1}100%{transform:translateY(calc(var(--dy)*1px)) translateX(calc(var(--dx)*1px));opacity:0}}
|
||||||
|
.hero-dot{position:absolute;border-radius:50%;animation:dotFloat linear infinite;}
|
||||||
|
@media(prefers-reduced-motion:reduce){.hero-dot{animation:none}}
|
||||||
|
`}}/>
|
||||||
|
{([
|
||||||
|
{size:4,x:15,y:80,dx:-30,dy:-120,dur:12,delay:0,color:'rgba(96,165,250,0.5)'},
|
||||||
|
{size:3,x:35,y:90,dx:20,dy:-100,dur:15,delay:-3,color:'rgba(167,139,250,0.4)'},
|
||||||
|
{size:5,x:55,y:85,dx:-10,dy:-130,dur:10,delay:-6,color:'rgba(96,165,250,0.3)'},
|
||||||
|
{size:2,x:70,y:95,dx:30,dy:-110,dur:18,delay:-2,color:'rgba(192,132,252,0.5)'},
|
||||||
|
{size:4,x:85,y:80,dx:-20,dy:-90,dur:14,delay:-8,color:'rgba(147,197,253,0.4)'},
|
||||||
|
{size:3,x:25,y:70,dx:15,dy:-140,dur:11,delay:-5,color:'rgba(216,180,254,0.4)'},
|
||||||
|
{size:6,x:60,y:75,dx:-25,dy:-100,dur:16,delay:-1,color:'rgba(96,165,250,0.25)'},
|
||||||
|
{size:2,x:45,y:88,dx:10,dy:-120,dur:13,delay:-9,color:'rgba(167,139,250,0.5)'},
|
||||||
|
{size:5,x:80,y:60,dx:-15,dy:-80,dur:20,delay:-4,color:'rgba(99,102,241,0.3)'},
|
||||||
|
{size:3,x:10,y:50,dx:25,dy:-110,dur:17,delay:-7,color:'rgba(192,132,252,0.35)'},
|
||||||
|
] as {size:number,x:number,y:number,dx:number,dy:number,dur:number,delay:number,color:string}[]).map((p,i)=>(
|
||||||
|
<div key={i} className="hero-dot" style={{
|
||||||
|
width:p.size,height:p.size,
|
||||||
|
left:`${p.x}%`,top:`${p.y}%`,
|
||||||
|
background:p.color,
|
||||||
|
'--dx':p.dx,'--dy':p.dy,
|
||||||
|
animationDuration:`${p.dur}s`,
|
||||||
|
animationDelay:`${p.delay}s`,
|
||||||
|
} as React.CSSProperties}/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
const containerjs = {
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl relative z-10">
|
||||||
hidden: { opacity: 0 },
|
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||||
show: {
|
{/* Left Content */}
|
||||||
opacity: 1,
|
<div className="space-y-6">
|
||||||
transition: {
|
<div className="flex items-center gap-2.5">
|
||||||
staggerChildren: 0.1
|
<div className="w-6 h-0.5 bg-[#533afd] shrink-0" />
|
||||||
}
|
<span className="text-[10px] font-semibold tracking-[0.1em] uppercase text-[#533afd]">{t.hero.badge}</span>
|
||||||
}
|
</div>
|
||||||
};
|
|
||||||
|
<motion.div
|
||||||
const itemjs = {
|
initial={{ opacity: 0, y: 20 }}
|
||||||
hidden: { opacity: 0, y: 20 },
|
animate={{ opacity: 1, y: 0 }}
|
||||||
show: { opacity: 1, y: 0 }
|
transition={{ duration: 0.5 }}
|
||||||
};
|
className="space-y-4"
|
||||||
|
>
|
||||||
return (
|
<HeadingTag
|
||||||
<section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 pt-12 pb-20">
|
className="font-extrabold text-[#061b31] leading-[0.92]"
|
||||||
{/* Animated Background Orbs */}
|
style={{ fontSize: 'clamp(2.75rem, 6vw, 5rem)' }}
|
||||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
>
|
||||||
{/* Orb 1 - Blue (top-left) */}
|
|
||||||
<div className="absolute -top-24 -left-24 w-96 h-96 bg-blue-400/30 rounded-full blur-3xl animate-blob" />
|
|
||||||
|
|
||||||
{/* Orb 2 - Purple (top-right) */}
|
|
||||||
<div className="absolute -top-12 -right-12 w-96 h-96 bg-purple-400/30 rounded-full blur-3xl animate-blob animation-delay-2000" />
|
|
||||||
|
|
||||||
{/* Orb 3 - Pink (bottom-left) */}
|
|
||||||
<div className="absolute -bottom-24 -left-12 w-96 h-96 bg-pink-400/20 rounded-full blur-3xl animate-blob animation-delay-4000" />
|
|
||||||
|
|
||||||
{/* Orb 4 - Cyan (center-right) */}
|
|
||||||
<div className="absolute top-1/2 -right-24 w-80 h-80 bg-cyan-400/20 rounded-full blur-3xl animate-blob animation-delay-6000" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl relative z-10">
|
|
||||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
|
||||||
{/* Left Content */}
|
|
||||||
<div className="space-y-8">
|
|
||||||
<Badge variant="info" className="inline-flex items-center space-x-2">
|
|
||||||
<span>{t.hero.badge}</span>
|
|
||||||
</Badge>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
className="space-y-6"
|
|
||||||
>
|
|
||||||
<HeadingTag className="text-5xl lg:text-6xl font-bold text-gray-900 leading-tight">
|
|
||||||
{t.hero.title}
|
{t.hero.title}
|
||||||
</HeadingTag>
|
</HeadingTag>
|
||||||
|
|
||||||
<p className="text-xl text-gray-600 leading-relaxed max-w-2xl">
|
<p className="text-[0.9375rem] text-[#64748d] leading-[1.55] max-w-[44ch]">
|
||||||
{t.hero.subtitle}
|
{t.hero.subtitle}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="space-y-3 pt-2">
|
<div className="flex flex-col gap-1.5 pt-1">
|
||||||
{t.hero.features.map((feature: string, index: number) => (
|
{t.hero.features.map((feature: string, index: number) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={index}
|
key={index}
|
||||||
initial={{ opacity: 0, x: -20 }}
|
initial={{ opacity: 0, x: -20 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ delay: 0.2 + (index * 0.1) }}
|
transition={{ delay: 0.2 + (index * 0.1) }}
|
||||||
className="flex items-center space-x-3"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0 w-6 h-6 bg-emerald-100 rounded-full flex items-center justify-center">
|
<div className="w-1 h-1 rounded-full bg-[#533afd] shrink-0" />
|
||||||
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
|
<span className="text-[13px] text-[#273951] font-medium">{feature}</span>
|
||||||
</div>
|
</motion.div>
|
||||||
<span className="text-gray-700 font-medium">{feature}</span>
|
))}
|
||||||
</motion.div>
|
</div>
|
||||||
))}
|
</motion.div>
|
||||||
</div>
|
|
||||||
</motion.div>
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<motion.div
|
animate={{ opacity: 1, y: 0 }}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
transition={{ delay: 0.5 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
className="flex gap-4 items-center flex-wrap pt-1"
|
||||||
transition={{ delay: 0.5 }}
|
>
|
||||||
className="flex flex-col sm:flex-row gap-4 pt-4"
|
<Link href="/signup">
|
||||||
>
|
<Button size="lg" style={{ padding: '14px 36px', fontSize: '1.0625rem', boxShadow: 'rgba(83,58,253,0.3) 0px 10px 28px -8px' }}>
|
||||||
<Link href="/signup">
|
{t.hero.cta_primary}
|
||||||
<Button size="lg" className="text-lg px-8 py-6 w-full sm:w-auto shadow-lg shadow-blue-500/25 hover:shadow-blue-500/40 transition-all duration-300">
|
</Button>
|
||||||
{t.hero.cta_primary}
|
</Link>
|
||||||
</Button>
|
<Link
|
||||||
</Link>
|
href="/#pricing"
|
||||||
<Link href="/#pricing">
|
style={{ fontSize: '0.875rem', fontWeight: 500, color: '#533afd', textDecoration: 'underline', textUnderlineOffset: '3px' }}
|
||||||
<Button variant="outline" size="lg" className="text-lg px-8 py-6 w-full sm:w-auto backdrop-blur-sm bg-white/50 border-gray-200 hover:bg-white/80 transition-all duration-300">
|
>
|
||||||
{t.hero.cta_secondary}
|
{t.hero.cta_secondary}
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
</motion.div>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Right Preview Widget */}
|
||||||
{/* Right Preview Widget */}
|
<div className="relative">
|
||||||
<div className="relative">
|
<div className="relative perspective-[1000px]">
|
||||||
<div className="relative perspective-[1000px]">
|
{/* impeccable-variants-start eb49f55c */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div data-impeccable-variants="eb49f55c" data-impeccable-variant-count="2" style={{ display: "contents" }}>
|
||||||
{[
|
{/* Original */}
|
||||||
{
|
<div data-impeccable-variant="original">
|
||||||
front: { title: 'URL/Website', color: 'bg-blue-500/10 text-blue-600', icon: Globe },
|
<div className="grid grid-cols-2 gap-4">
|
||||||
back: { title: 'PDF / Menu', color: 'bg-orange-500/10 text-orange-600', icon: FileText },
|
{[
|
||||||
delay: 3 // Starts at 3s
|
{
|
||||||
},
|
front: { title: 'URL/Website', color: 'bg-blue-500/10 text-blue-600', icon: Globe },
|
||||||
{
|
back: { title: 'PDF / Menu', color: 'bg-orange-500/10 text-orange-600', icon: FileText },
|
||||||
front: { title: 'Contact Card', color: 'bg-purple-500/10 text-purple-600', icon: User },
|
delay: 3
|
||||||
back: { title: 'Coupon / Deals', color: 'bg-red-500/10 text-red-600', icon: Ticket },
|
},
|
||||||
delay: 5 // +2s
|
{
|
||||||
},
|
front: { title: 'Contact Card', color: 'bg-purple-500/10 text-purple-600', icon: User },
|
||||||
{
|
back: { title: 'Coupon / Deals', color: 'bg-red-500/10 text-red-600', icon: Ticket },
|
||||||
front: { title: 'Location', color: 'bg-green-500/10 text-green-600', icon: MapPin },
|
delay: 5
|
||||||
back: { title: 'App Store', color: 'bg-sky-500/10 text-sky-600', icon: Smartphone },
|
},
|
||||||
delay: 7 // +2s
|
{
|
||||||
},
|
front: { title: 'Location', color: 'bg-green-500/10 text-green-600', icon: MapPin },
|
||||||
{
|
back: { title: 'App Store', color: 'bg-sky-500/10 text-sky-600', icon: Smartphone },
|
||||||
front: { title: 'Phone Number', color: 'bg-pink-500/10 text-pink-600', icon: Phone },
|
delay: 7
|
||||||
back: { title: 'Feedback', color: 'bg-yellow-500/10 text-yellow-600', icon: Star },
|
},
|
||||||
delay: 9 // +2s
|
{
|
||||||
},
|
front: { title: 'Phone Number', color: 'bg-pink-500/10 text-pink-600', icon: Phone },
|
||||||
].map((card, index) => (
|
back: { title: 'Feedback', color: 'bg-yellow-500/10 text-yellow-600', icon: Star },
|
||||||
<FlippingCard key={index} {...card} />
|
delay: 9
|
||||||
))}
|
},
|
||||||
</div>
|
].map((card, index) => (
|
||||||
</div>
|
<FlippingCard key={index} {...card} />
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Variants: insert below this line */}
|
||||||
{/* Smooth Gradient Fade Transition */}
|
<style data-impeccable-css="eb49f55c">{`
|
||||||
<div className="absolute bottom-0 left-0 w-full h-32 bg-gradient-to-b from-transparent to-gray-50 pointer-events-none" />
|
@scope ([data-impeccable-variant="1"]) {
|
||||||
</section >
|
.qr-instrument-grid {
|
||||||
);
|
--accent-strength: var(--p-accent-strength, 0.58);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid oklch(91% 0.018 260);
|
||||||
|
border-radius: 8px;
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, oklch(99% 0.006 260), oklch(96% 0.018 278)),
|
||||||
|
repeating-linear-gradient(90deg, oklch(93% 0.012 260) 0 1px, transparent 1px 44px);
|
||||||
|
box-shadow:
|
||||||
|
rgba(50, 50, 93, 0.24) 0 30px 45px -30px,
|
||||||
|
rgba(3, 3, 39, 0.10) 0 18px 36px -18px;
|
||||||
|
}
|
||||||
|
.qr-instrument-tile {
|
||||||
|
position: relative;
|
||||||
|
min-height: 126px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid oklch(88% 0.018 260);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: oklch(99% 0.004 260 / 0.92);
|
||||||
|
color: oklch(22% 0.07 260);
|
||||||
|
box-shadow: rgba(23, 23, 23, 0.06) 0 3px 6px;
|
||||||
|
transition: transform 220ms cubic-bezier(0.25, 1, 0.5, 1), border-color 220ms cubic-bezier(0.25, 1, 0.5, 1), box-shadow 220ms cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
|
}
|
||||||
|
.qr-instrument-tile:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
border-color: color-mix(in oklch, oklch(55% 0.25 280) calc(var(--accent-strength) * 100%), oklch(88% 0.018 260));
|
||||||
|
box-shadow:
|
||||||
|
rgba(50, 50, 93, 0.26) 0 24px 40px -26px,
|
||||||
|
rgba(83, 58, 253, calc(var(--accent-strength) * 0.16)) 0 10px 24px -16px;
|
||||||
|
}
|
||||||
|
.qr-instrument-face {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: grid;
|
||||||
|
align-content: space-between;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
.qr-instrument-code {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
color: oklch(55% 0.04 260);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.qr-instrument-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: oklch(52% 0.24 280);
|
||||||
|
background: color-mix(in oklch, oklch(62% 0.25 280) calc(var(--accent-strength) * 16%), oklch(97% 0.01 260));
|
||||||
|
}
|
||||||
|
.qr-instrument-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.15;
|
||||||
|
font-weight: 600;
|
||||||
|
color: oklch(20% 0.065 260);
|
||||||
|
}
|
||||||
|
.qr-instrument-spark {
|
||||||
|
width: 34px;
|
||||||
|
height: 12px;
|
||||||
|
background: linear-gradient(90deg, transparent 0 12%, oklch(58% 0.24 280 / calc(var(--accent-strength) * 0.55)) 12% 18%, transparent 18% 34%, oklch(58% 0.24 280 / calc(var(--accent-strength) * 0.75)) 34% 44%, transparent 44% 100%);
|
||||||
|
transform: skewX(-18deg);
|
||||||
|
}
|
||||||
|
.qr-instrument-tile:hover .qr-instrument-spark {
|
||||||
|
animation: qrInstrumentPulse 720ms cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
|
}
|
||||||
|
@keyframes qrInstrumentPulse {
|
||||||
|
from { opacity: 0.25; transform: translateX(-4px) skewX(-18deg); }
|
||||||
|
to { opacity: 1; transform: translateX(0) skewX(-18deg); }
|
||||||
|
}
|
||||||
|
:scope[data-p-density="compact"] .qr-instrument-grid { gap: 8px; padding: 8px; }
|
||||||
|
:scope[data-p-density="compact"] .qr-instrument-tile { min-height: 114px; }
|
||||||
|
:scope[data-p-density="open"] .qr-instrument-grid { gap: 14px; padding: 12px; }
|
||||||
|
:scope[data-p-density="open"] .qr-instrument-tile { min-height: 136px; }
|
||||||
|
}
|
||||||
|
@scope ([data-impeccable-variant="2"]) {
|
||||||
|
.qr-rhythm-grid {
|
||||||
|
--motion-depth: var(--p-motion-depth, 0.45);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.qr-rhythm-tile {
|
||||||
|
position: relative;
|
||||||
|
min-height: 126px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid oklch(90% 0.018 260);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: oklch(99% 0.004 260 / 0.86);
|
||||||
|
box-shadow:
|
||||||
|
rgba(50, 50, 93, 0.18) 0 24px 34px -28px,
|
||||||
|
rgba(0, 0, 0, 0.08) 0 14px 24px -18px;
|
||||||
|
transition: transform 260ms cubic-bezier(0.25, 1, 0.5, 1), box-shadow 260ms cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
|
}
|
||||||
|
.qr-rhythm-tile:hover {
|
||||||
|
transform: translateY(calc(var(--motion-depth) * -8px));
|
||||||
|
box-shadow:
|
||||||
|
rgba(50, 50, 93, 0.26) 0 30px 45px -30px,
|
||||||
|
rgba(83, 58, 253, 0.14) 0 16px 30px -22px;
|
||||||
|
}
|
||||||
|
.qr-rhythm-tile::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, oklch(55% 0.25 280 / 0.10), transparent 42%),
|
||||||
|
repeating-linear-gradient(90deg, oklch(68% 0.16 280 / 0.32) 0 3px, transparent 3px 8px);
|
||||||
|
clip-path: inset(72% 0 10% 0);
|
||||||
|
opacity: calc(0.36 + var(--motion-depth) * 0.5);
|
||||||
|
}
|
||||||
|
.qr-rhythm-content {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 11px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.qr-rhythm-icon {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: oklch(48% 0.24 280);
|
||||||
|
background: oklch(96% 0.025 280);
|
||||||
|
box-shadow: inset oklch(100% 0.004 260 / 0.82) 0 1px 0;
|
||||||
|
transition: transform 260ms cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
|
}
|
||||||
|
.qr-rhythm-tile:hover .qr-rhythm-icon {
|
||||||
|
transform: translateY(calc(var(--motion-depth) * -5px)) scale(calc(1 + var(--motion-depth) * 0.05));
|
||||||
|
}
|
||||||
|
.qr-rhythm-label {
|
||||||
|
color: oklch(20% 0.065 260);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.12;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.qr-rhythm-meta {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 10px;
|
||||||
|
color: oklch(58% 0.04 260);
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 1;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
:scope[data-p-signal="quiet"] .qr-rhythm-tile::before { opacity: 0.18; }
|
||||||
|
:scope[data-p-signal="bright"] .qr-rhythm-tile::before { opacity: 0.78; }
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.qr-rhythm-tile,
|
||||||
|
.qr-rhythm-icon,
|
||||||
|
.qr-instrument-tile,
|
||||||
|
.qr-instrument-spark {
|
||||||
|
animation: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
<div
|
||||||
|
data-impeccable-variant="1"
|
||||||
|
data-impeccable-params='[
|
||||||
|
{"id":"accent-strength","kind":"range","min":0.25,"max":0.9,"step":0.05,"default":0.58,"label":"Accent strength"},
|
||||||
|
{"id":"density","kind":"steps","default":"balanced","label":"Density","options":[
|
||||||
|
{"value":"compact","label":"Compact"},
|
||||||
|
{"value":"balanced","label":"Balanced"},
|
||||||
|
{"value":"open","label":"Open"}
|
||||||
|
]}
|
||||||
|
]'
|
||||||
|
>
|
||||||
|
<div className="qr-instrument-grid">
|
||||||
|
{[
|
||||||
|
{ code: '01', title: 'URL/Website', icon: Globe },
|
||||||
|
{ code: '02', title: 'PDF / Menu', icon: FileText },
|
||||||
|
{ code: '03', title: 'Contact Card', icon: User },
|
||||||
|
{ code: '04', title: 'Coupon / Deals', icon: Ticket },
|
||||||
|
].map((card) => (
|
||||||
|
<div className="qr-instrument-tile" key={card.title}>
|
||||||
|
<div className="qr-instrument-face">
|
||||||
|
<div className="qr-instrument-code">
|
||||||
|
<span>QR-{card.code}</span>
|
||||||
|
<span>READY</span>
|
||||||
|
</div>
|
||||||
|
<div className="qr-instrument-icon">
|
||||||
|
<card.icon className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div className="qr-instrument-label">
|
||||||
|
<span>{card.title}</span>
|
||||||
|
<span className="qr-instrument-spark" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-impeccable-variant="2"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
data-impeccable-params='[
|
||||||
|
{"id":"motion-depth","kind":"range","min":0,"max":1,"step":0.05,"default":0.45,"label":"Motion depth"},
|
||||||
|
{"id":"signal","kind":"steps","default":"calm","label":"Signal","options":[
|
||||||
|
{"value":"quiet","label":"Quiet"},
|
||||||
|
{"value":"calm","label":"Calm"},
|
||||||
|
{"value":"bright","label":"Bright"}
|
||||||
|
]}
|
||||||
|
]'
|
||||||
|
>
|
||||||
|
<div className="qr-rhythm-grid">
|
||||||
|
{[
|
||||||
|
{ code: 'URL', title: 'URL/Website', icon: Globe },
|
||||||
|
{ code: 'PDF', title: 'PDF / Menu', icon: FileText },
|
||||||
|
{ code: 'LOC', title: 'Location', icon: MapPin },
|
||||||
|
{ code: 'APP', title: 'App Store', icon: Smartphone },
|
||||||
|
].map((card) => (
|
||||||
|
<div className="qr-rhythm-tile" key={card.title}>
|
||||||
|
<span className="qr-rhythm-meta">{card.code}</span>
|
||||||
|
<div className="qr-rhythm-content">
|
||||||
|
<div className="qr-rhythm-icon">
|
||||||
|
<card.icon className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<span className="qr-rhythm-label">{card.title}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* impeccable-variants-end eb49f55c */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-0 left-0 w-full h-32 bg-gradient-to-b from-transparent to-gray-50 pointer-events-none" />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,48 +9,41 @@ export const ReprintCalculatorTeaser: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<section className="py-24 bg-white relative overflow-hidden">
|
<section className="py-24 bg-white relative overflow-hidden">
|
||||||
<div className="container mx-auto px-4 relative z-10">
|
<div className="container mx-auto px-4 relative z-10">
|
||||||
<div className="bg-gradient-to-br from-indigo-50 to-white border border-indigo-100 rounded-3xl p-8 md:p-12 lg:p-16 flex flex-col md:flex-row items-center justify-between gap-12 shadow-sm hover:shadow-md transition-shadow duration-500">
|
<div className="flex flex-col md:flex-row items-center justify-between gap-12 bg-white border border-[#e5edf5] rounded-lg p-8 md:p-12 lg:p-16" style={{boxShadow:'rgba(50,50,93,0.12) 0px 8px 20px -8px'}}>
|
||||||
|
|
||||||
<div className="flex-1 text-center md:text-left">
|
<div className="flex-1 text-center md:text-left">
|
||||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-indigo-100 text-indigo-700 text-sm font-medium mb-6">
|
<div className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded border border-[rgba(83,58,253,0.15)] mb-5" style={{background:'oklch(0.94 0.02 270)',color:'#533afd',fontSize:11,fontWeight:500,letterSpacing:'0.06em',textTransform:'uppercase'}}>
|
||||||
<TrendingUp className="w-4 h-4" />
|
<TrendingUp style={{width:12,height:12}} aria-hidden="true" />
|
||||||
<span>ROI Calculator</span>
|
<span>ROI Calculator</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-slate-900 mb-6 leading-tight">
|
<h2 style={{fontSize:'clamp(1.875rem,4vw,3rem)',fontWeight:700,lineHeight:1.1,color:'#061b31',marginBottom:'1.25rem',letterSpacing:'-0.02em'}}>
|
||||||
Are you burning budget on <br />
|
Are you burning budget on{' '}
|
||||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-indigo-600 to-blue-600">Static Reprints?</span>
|
<span style={{color:'#533afd'}}>Static Reprints?</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="text-slate-600 text-lg md:text-xl max-w-xl mx-auto md:mx-0 leading-relaxed mb-8">
|
<p style={{color:'#64748d',fontSize:'1.0625rem',lineHeight:1.6,maxWidth:'52ch',margin:'0 auto 2rem'}}>
|
||||||
Find out exactly how much you can save by switching to dynamic QR codes. Our calculator reveals your savings potential in seconds.
|
Find out exactly how much you can save by switching to dynamic QR codes. Our calculator reveals your savings potential in seconds.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Link href="/reprint-calculator">
|
<Link href="/reprint-calculator">
|
||||||
<Button
|
<Button size="lg" className="h-14 px-8">
|
||||||
size="lg"
|
Calculate Savings <ArrowRight className="w-4 h-4 ml-2" aria-hidden="true" />
|
||||||
variant="primary"
|
|
||||||
className="h-14 px-8 text-lg hover:translate-x-1 transition-transform"
|
|
||||||
>
|
|
||||||
Calculate Savings <ArrowRight className="w-5 h-5 ml-2" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-shrink-0 w-full md:w-auto flex justify-center md:justify-end">
|
<div className="flex-shrink-0 w-full md:w-auto flex justify-center md:justify-end">
|
||||||
<div className="relative group">
|
<div className="bg-white border border-[#e5edf5] rounded-lg p-7 w-full max-w-[260px] text-center" style={{boxShadow:'rgba(50,50,93,0.25) 0px 20px 40px -20px,rgba(0,0,0,0.08) 0px 10px 20px -10px'}}>
|
||||||
<div className="absolute -inset-1 bg-gradient-to-r from-indigo-500 to-blue-500 rounded-2xl blur opacity-20 group-hover:opacity-40 transition duration-1000 group-hover:duration-200"></div>
|
<div className="flex items-center justify-center mx-auto mb-4 text-[#533afd]" style={{width:44,height:44,background:'oklch(0.94 0.02 270)',borderRadius:6}}>
|
||||||
<div className="relative bg-white rounded-xl p-8 border border-slate-100 w-full max-w-xs text-center shadow-lg">
|
<Calculator style={{width:22,height:22}} aria-hidden="true" />
|
||||||
<div className="w-16 h-16 bg-indigo-50 rounded-2xl flex items-center justify-center mx-auto mb-6 text-indigo-600">
|
</div>
|
||||||
<Calculator className="w-8 h-8" />
|
<h3 className="font-semibold text-[#061b31] mb-2" style={{fontSize:'0.9375rem'}}>Cost Analysis</h3>
|
||||||
</div>
|
<p className="text-[#64748d] leading-relaxed" style={{fontSize:'0.8125rem'}}>
|
||||||
<h3 className="text-lg font-bold text-slate-900 mb-2">Cost Analysis</h3>
|
Enter your print volume and update frequency to see your hidden costs.
|
||||||
<p className="text-slate-500 text-sm mb-6">
|
</p>
|
||||||
Enter your print volume and update frequency to see your hidden costs.
|
<div className="mt-5 h-0.5 w-full bg-[#e5edf5] rounded-sm overflow-hidden">
|
||||||
</p>
|
<div className="h-full w-2/3 bg-[#533afd] rounded-sm" />
|
||||||
<div className="h-1.5 w-full bg-slate-100 rounded-full overflow-hidden">
|
|
||||||
<div className="h-full w-2/3 bg-indigo-500 rounded-full animate-pulse"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
5
tasks/lessons.md
Normal file
5
tasks/lessons.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Lesson:
|
||||||
|
For lead scraping, do not rely only on pre-enrichment dedupe. Website crawling can assign the same corporate email to multiple locations, so exports must be deduplicated again by final email before use.
|
||||||
|
|
||||||
|
Lesson:
|
||||||
|
Large API scraping runs should write incremental output or use smaller controlled batches. A long Overpass workflow can hang or rate-limit without producing files, making it hard to recover useful partial results.
|
||||||
135
urls.txt
135
urls.txt
@@ -1,135 +0,0 @@
|
|||||||
https://www.qrmaster.net
|
|
||||||
https://www.qrmaster.net/newsletter
|
|
||||||
https://www.qrmaster.net/qr-code-erstellen
|
|
||||||
https://www.qrmaster.net/qr-code-tracking
|
|
||||||
https://www.qrmaster.net/qr-code-analytics
|
|
||||||
https://www.qrmaster.net/reprint-calculator
|
|
||||||
https://www.qrmaster.net/dynamic-qr-code-generator
|
|
||||||
https://www.qrmaster.net/dynamic-barcode-generator
|
|
||||||
https://www.qrmaster.net/bulk-qr-code-generator
|
|
||||||
https://www.qrmaster.net/custom-qr-code-generator
|
|
||||||
https://www.qrmaster.net/pricing
|
|
||||||
https://www.qrmaster.net/tools
|
|
||||||
https://www.qrmaster.net/features
|
|
||||||
https://www.qrmaster.net/faq
|
|
||||||
https://www.qrmaster.net/blog
|
|
||||||
https://www.qrmaster.net/privacy
|
|
||||||
https://www.qrmaster.net/contact
|
|
||||||
https://www.qrmaster.net/about
|
|
||||||
https://www.qrmaster.net/press
|
|
||||||
https://www.qrmaster.net/testimonials
|
|
||||||
https://www.qrmaster.net/tools/url-qr-code
|
|
||||||
https://www.qrmaster.net/tools/vcard-qr-code
|
|
||||||
https://www.qrmaster.net/tools/text-qr-code
|
|
||||||
https://www.qrmaster.net/tools/email-qr-code
|
|
||||||
https://www.qrmaster.net/tools/sms-qr-code
|
|
||||||
https://www.qrmaster.net/tools/wifi-qr-code
|
|
||||||
https://www.qrmaster.net/tools/crypto-qr-code
|
|
||||||
https://www.qrmaster.net/tools/event-qr-code
|
|
||||||
https://www.qrmaster.net/tools/facebook-qr-code
|
|
||||||
https://www.qrmaster.net/tools/instagram-qr-code
|
|
||||||
https://www.qrmaster.net/tools/twitter-qr-code
|
|
||||||
https://www.qrmaster.net/tools/youtube-qr-code
|
|
||||||
https://www.qrmaster.net/tools/whatsapp-qr-code
|
|
||||||
https://www.qrmaster.net/tools/tiktok-qr-code
|
|
||||||
https://www.qrmaster.net/tools/geolocation-qr-code
|
|
||||||
https://www.qrmaster.net/tools/call-qr-code-generator
|
|
||||||
https://www.qrmaster.net/tools/paypal-qr-code
|
|
||||||
https://www.qrmaster.net/tools/zoom-qr-code
|
|
||||||
https://www.qrmaster.net/tools/teams-qr-code
|
|
||||||
https://www.qrmaster.net/tools/barcode-generator
|
|
||||||
https://www.qrmaster.net/blog/static-vs-dynamic-qr-code
|
|
||||||
https://www.qrmaster.net/blog/microsoft-teams-qr-code
|
|
||||||
https://www.qrmaster.net/blog/qr-code-restaurant-menu
|
|
||||||
https://www.qrmaster.net/blog/vcard-qr-code-generator
|
|
||||||
https://www.qrmaster.net/blog/qr-code-small-business
|
|
||||||
https://www.qrmaster.net/blog/qr-code-print-size-guide
|
|
||||||
https://www.qrmaster.net/blog/qr-code-tracking-guide-2025
|
|
||||||
https://www.qrmaster.net/blog/dynamic-vs-static-qr-codes
|
|
||||||
https://www.qrmaster.net/blog/bulk-qr-code-generator-excel
|
|
||||||
https://www.qrmaster.net/blog/qr-code-analytics
|
|
||||||
https://www.qrmaster.net/blog/barcode-generator-tool
|
|
||||||
https://www.qrmaster.net/blog/spotify-code-generator-guide
|
|
||||||
https://www.qrmaster.net/blog/whatsapp-qr-code-generator
|
|
||||||
https://www.qrmaster.net/blog/instagram-qr-code-generator
|
|
||||||
https://www.qrmaster.net/blog/trackable-qr-codes
|
|
||||||
https://www.qrmaster.net/blog/utm-parameter-qr-codes
|
|
||||||
https://www.qrmaster.net/blog/qr-code-scan-statistics-2026
|
|
||||||
https://www.qrmaster.net/blog/qr-code-events
|
|
||||||
https://www.qrmaster.net/blog/business-card-qr-code
|
|
||||||
https://www.qrmaster.net/blog/qr-code-marketing
|
|
||||||
https://www.qrmaster.net/blog/qr-code-security
|
|
||||||
https://www.qrmaster.net/blog/qr-code-api-documentation
|
|
||||||
https://www.qrmaster.net/blog/free-vs-paid-qr-generator
|
|
||||||
https://www.qrmaster.net/blog/best-qr-code-generator-2026
|
|
||||||
https://www.qrmaster.net/learn
|
|
||||||
https://www.qrmaster.net/learn/basics
|
|
||||||
https://www.qrmaster.net/learn/tracking
|
|
||||||
https://www.qrmaster.net/learn/use-cases
|
|
||||||
https://www.qrmaster.net/learn/security
|
|
||||||
https://www.qrmaster.net/learn/developer
|
|
||||||
https://www.qrmaster.net/use-cases
|
|
||||||
https://www.qrmaster.net/qr-code-for
|
|
||||||
https://www.qrmaster.net/use-cases/restaurant-menu-qr-codes
|
|
||||||
https://www.qrmaster.net/use-cases/business-card-qr-codes
|
|
||||||
https://www.qrmaster.net/use-cases/event-qr-codes
|
|
||||||
https://www.qrmaster.net/use-cases/flyer-qr-codes
|
|
||||||
https://www.qrmaster.net/use-cases/packaging-qr-codes
|
|
||||||
https://www.qrmaster.net/use-cases/real-estate-sign-qr-codes
|
|
||||||
https://www.qrmaster.net/use-cases/feedback-qr-codes
|
|
||||||
https://www.qrmaster.net/use-cases/payment-qr-codes
|
|
||||||
https://www.qrmaster.net/use-cases/coupon-qr-codes
|
|
||||||
https://www.qrmaster.net/qr-code-for-marketing-campaigns
|
|
||||||
https://www.qrmaster.net/qr-code-for/restaurants
|
|
||||||
https://www.qrmaster.net/qr-code-for/cafes
|
|
||||||
https://www.qrmaster.net/qr-code-for/hotels
|
|
||||||
https://www.qrmaster.net/qr-code-for/real-estate
|
|
||||||
https://www.qrmaster.net/qr-code-for/gyms
|
|
||||||
https://www.qrmaster.net/qr-code-for/doctors-dentists
|
|
||||||
https://www.qrmaster.net/qr-code-for/retail
|
|
||||||
https://www.qrmaster.net/qr-code-for/events
|
|
||||||
https://www.qrmaster.net/qr-code-for/bars
|
|
||||||
https://www.qrmaster.net/qr-code-for/food-trucks
|
|
||||||
https://www.qrmaster.net/qr-code-for/bakeries
|
|
||||||
https://www.qrmaster.net/qr-code-for/breweries
|
|
||||||
https://www.qrmaster.net/qr-code-for/nightclubs
|
|
||||||
https://www.qrmaster.net/qr-code-for/catering
|
|
||||||
https://www.qrmaster.net/qr-code-for/wineries
|
|
||||||
https://www.qrmaster.net/qr-code-for/yoga-studios
|
|
||||||
https://www.qrmaster.net/qr-code-for/spas
|
|
||||||
https://www.qrmaster.net/qr-code-for/beauty-salons
|
|
||||||
https://www.qrmaster.net/qr-code-for/barbershops
|
|
||||||
https://www.qrmaster.net/qr-code-for/nail-salons
|
|
||||||
https://www.qrmaster.net/qr-code-for/tattoo-studios
|
|
||||||
https://www.qrmaster.net/qr-code-for/pharmacies
|
|
||||||
https://www.qrmaster.net/qr-code-for/clothing-stores
|
|
||||||
https://www.qrmaster.net/qr-code-for/car-dealerships
|
|
||||||
https://www.qrmaster.net/qr-code-for/florists
|
|
||||||
https://www.qrmaster.net/qr-code-for/pet-stores
|
|
||||||
https://www.qrmaster.net/qr-code-for/electronics-stores
|
|
||||||
https://www.qrmaster.net/qr-code-for/jewelry-stores
|
|
||||||
https://www.qrmaster.net/qr-code-for/hardware-stores
|
|
||||||
https://www.qrmaster.net/qr-code-for/bookstores
|
|
||||||
https://www.qrmaster.net/qr-code-for/universities
|
|
||||||
https://www.qrmaster.net/qr-code-for/schools
|
|
||||||
https://www.qrmaster.net/qr-code-for/museums
|
|
||||||
https://www.qrmaster.net/qr-code-for/libraries
|
|
||||||
https://www.qrmaster.net/qr-code-for/theaters
|
|
||||||
https://www.qrmaster.net/qr-code-for/cinemas
|
|
||||||
https://www.qrmaster.net/qr-code-for/churches
|
|
||||||
https://www.qrmaster.net/qr-code-for/art-galleries
|
|
||||||
https://www.qrmaster.net/qr-code-for/stadiums
|
|
||||||
https://www.qrmaster.net/qr-code-for/wedding-planners
|
|
||||||
https://www.qrmaster.net/qr-code-for/photographers
|
|
||||||
https://www.qrmaster.net/qr-code-for/trade-shows
|
|
||||||
https://www.qrmaster.net/qr-code-for/law-firms
|
|
||||||
https://www.qrmaster.net/qr-code-for/accountants
|
|
||||||
https://www.qrmaster.net/qr-code-for/insurance-agencies
|
|
||||||
https://www.qrmaster.net/qr-code-for/travel-agencies
|
|
||||||
https://www.qrmaster.net/qr-code-for/coworking-spaces
|
|
||||||
https://www.qrmaster.net/qr-code-for/property-management
|
|
||||||
https://www.qrmaster.net/qr-code-for/airports
|
|
||||||
https://www.qrmaster.net/qr-code-for/dentists
|
|
||||||
https://www.qrmaster.net/qr-code-for/pet-grooming
|
|
||||||
https://www.qrmaster.net/qr-code-for/veterinarians
|
|
||||||
https://www.qrmaster.net/authors/timo
|
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user