diff --git a/package-lock.json b/package-lock.json index 212d284..a6c7161 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "dayjs": "^1.11.10", "exceljs": "^4.4.0", "file-saver": "^2.0.5", + "framer-motion": "^12.24.10", "i18next": "^23.7.6", "ioredis": "^5.3.2", "jszip": "^3.10.1", @@ -5098,6 +5099,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.24.10", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.24.10.tgz", + "integrity": "sha512-8yoyMkCn2RmV9UB9mfmMuzKyenQe909hRQRl0yGBhbZJjZZ9bSU87NIGAruqCXCuTNCA0qHw2LWLrcXLL9GF6A==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.24.10", + "motion-utils": "^12.24.10", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -6607,6 +6635,21 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/motion-dom": { + "version": "12.24.10", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.24.10.tgz", + "integrity": "sha512-H3HStYaJ6wANoZVNT0ZmYZHGvrpvi9pKJRzsgNEHkdITR4Qd9FFu2e9sH4e2Phr4tKCmyyloex6SOSmv0Tlq+g==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.24.10" + } + }, + "node_modules/motion-utils": { + "version": "12.24.10", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.24.10.tgz", + "integrity": "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index 193ee1c..8305a0e 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "dayjs": "^1.11.10", "exceljs": "^4.4.0", "file-saver": "^2.0.5", + "framer-motion": "^12.24.10", "i18next": "^23.7.6", "ioredis": "^5.3.2", "jszip": "^3.10.1", diff --git a/src/app/(marketing)/layout.tsx b/src/app/(marketing)/layout.tsx index 2790aff..cb38211 100644 --- a/src/app/(marketing)/layout.tsx +++ b/src/app/(marketing)/layout.tsx @@ -3,6 +3,7 @@ import React, { useState } from 'react'; import Link from 'next/link'; import { Button } from '@/components/ui/Button'; +import { Footer } from '@/components/ui/Footer'; import en from '@/i18n/en.json'; export default function MarketingLayout({ @@ -11,6 +12,16 @@ export default function MarketingLayout({ children: React.ReactNode; }) { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const [showStickyCTA, setShowStickyCTA] = useState(false); + + React.useEffect(() => { + const handleScroll = () => { + setShowStickyCTA(window.scrollY > 400); + }; + + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, []); // Always use English for marketing pages const t = en; @@ -25,7 +36,7 @@ export default function MarketingLayout({ return (
{/* Header */} -
+
{/* Main Content */} -
{children}
+
{children}
{/* Footer */} -
-
-
-
- - QR Master - QR Master - -

- Create custom QR codes in seconds with advanced tracking and analytics. -

-
- -
-

Product

-
    -
  • Features
  • -
  • Pricing
  • -
  • FAQ
  • -
  • Blog
  • -
-
- -
-

Resources

-
    -
  • Full Pricing
  • -
  • All Questions
  • -
  • Blog
  • -
  • Get Started
  • -
-
- -
-

Legal

-
    -
  • Privacy Policy
  • -
-
- -
- -
- - • - -

© 2025 QR Master. All rights reserved.

-
-
-
-
+
); } \ No newline at end of file diff --git a/src/components/marketing/AIComingSoonBanner.tsx b/src/components/marketing/AIComingSoonBanner.tsx index 04a5c40..32be33d 100644 --- a/src/components/marketing/AIComingSoonBanner.tsx +++ b/src/components/marketing/AIComingSoonBanner.tsx @@ -3,6 +3,7 @@ import React, { useState } from 'react'; import { Sparkles, Brain, TrendingUp, MessageSquare, Palette, ArrowRight, Mail, CheckCircle2, Lock } from 'lucide-react'; import Link from 'next/link'; +import { motion } from 'framer-motion'; const AIComingSoonBanner = () => { const [email, setEmail] = useState(''); @@ -93,8 +94,15 @@ const AIComingSoonBanner = () => {
{/* Header */} -
-
+ {/* Header */} + +
Coming Soon @@ -111,14 +119,18 @@ const AIComingSoonBanner = () => {

Revolutionary AI features to transform how you create, manage, and optimize QR codes

-
+
{/* Features Grid */}
{features.map((feature, index) => ( -
@@ -136,12 +148,18 @@ const AIComingSoonBanner = () => { ))} -
+ ))}
{/* Email Capture */} -
+ {!submitted ? ( <>
@@ -163,12 +181,12 @@ const AIComingSoonBanner = () => { @@ -188,7 +206,7 @@ const AIComingSoonBanner = () => {
)} -
+
); diff --git a/src/components/marketing/FAQ.tsx b/src/components/marketing/FAQ.tsx index dec7e35..3fa7b6b 100644 --- a/src/components/marketing/FAQ.tsx +++ b/src/components/marketing/FAQ.tsx @@ -1,6 +1,7 @@ 'use client'; import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; import { Card } from '@/components/ui/Card'; interface FAQProps { @@ -21,38 +22,62 @@ export const FAQ: React.FC = ({ t }) => { return (
-
+

{t.faq.title}

-
+
{questions.map((key, index) => ( - setOpenIndex(openIndex === index ? null : index)}> -
-
-

- {t.faq.questions[key].question} -

- -
- - {openIndex === index && ( -
- {t.faq.questions[key].answer} + + setOpenIndex(openIndex === index ? null : index)}> +
+
+

+ {t.faq.questions[key].question} +

+
- )} -
-
+ + + {openIndex === index && ( + +
+ {t.faq.questions[key].answer} +
+
+ )} +
+
+ + ))}
diff --git a/src/components/marketing/Features.tsx b/src/components/marketing/Features.tsx index 5ad53a1..0a7b532 100644 --- a/src/components/marketing/Features.tsx +++ b/src/components/marketing/Features.tsx @@ -1,6 +1,7 @@ 'use client'; import React from 'react'; +import { motion } from 'framer-motion'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; interface FeaturesProps { @@ -41,27 +42,41 @@ export const Features: React.FC = ({ t }) => { return (
-
+

{t.features.title}

-
+
- {features.map((feature) => ( - - -
- {feature.icon} -
- {t.features[feature.key].title} -
- -

- {t.features[feature.key].description} -

-
-
+ {features.map((feature, index) => ( + + + +
+ {feature.icon} +
+ {t.features[feature.key].title} +
+ +

+ {t.features[feature.key].description} +

+
+
+
))}
diff --git a/src/components/marketing/Hero.tsx b/src/components/marketing/Hero.tsx index 3cf45db..b5bceb7 100644 --- a/src/components/marketing/Hero.tsx +++ b/src/components/marketing/Hero.tsx @@ -5,6 +5,8 @@ import Link from 'next/link'; import { Button } from '@/components/ui/Button'; import { Badge } from '@/components/ui/Badge'; import { Card } from '@/components/ui/Card'; +import { motion } from 'framer-motion'; +import { Globe, User, MapPin, Phone, CheckCircle2, ArrowRight } from 'lucide-react'; interface HeroProps { t: any; // i18n translation function @@ -12,12 +14,27 @@ interface HeroProps { export const Hero: React.FC = ({ t }) => { const templateCards = [ - { title: 'URL/Website', color: 'bg-blue-100', icon: '🌐' }, - { title: 'Contact Card', color: 'bg-purple-100', icon: '👤' }, - { title: 'Location', color: 'bg-green-100', icon: '📍' }, - { title: 'Phone Number', color: 'bg-pink-100', icon: '📞' }, + { title: 'URL/Website', color: 'bg-blue-500/10 text-blue-600', icon: Globe }, + { title: 'Contact Card', color: 'bg-purple-500/10 text-purple-600', icon: User }, + { title: 'Location', color: 'bg-green-500/10 text-green-600', icon: MapPin }, + { title: 'Phone Number', color: 'bg-pink-500/10 text-pink-600', icon: Phone }, ]; + const containerjs = { + hidden: { opacity: 0 }, + show: { + opacity: 1, + transition: { + staggerChildren: 0.1 + } + } + }; + + const itemjs = { + hidden: { opacity: 0, y: 20 }, + show: { opacity: 1, y: 0 } + }; + return (
{/* Animated Background Orbs */} @@ -43,64 +60,96 @@ export const Hero: React.FC = ({ t }) => { {t.hero.badge} -
+

{t.hero.title}

-

+

{t.hero.subtitle}

-
+
{t.hero.features.map((feature: string, index: number) => ( -
-
- - - + +
+
- {feature} -
+ {feature} + ))}
+ -
- - - - - - -
-
+ + + + + + + +
{/* Right Preview Widget */}
-
+ {templateCards.map((card, index) => ( - -
{card.icon}
-

{card.title}

-
+ + +
+ +
+

{card.title}

+
+
))} -
+ {/* Floating Badge */} -
+ + + + + {t.hero.engagement_badge} -
+
{/* Smooth Gradient Fade Transition */}
-
+ ); }; \ No newline at end of file diff --git a/src/components/marketing/HomePageClient.tsx b/src/components/marketing/HomePageClient.tsx index 633ce57..b857be1 100644 --- a/src/components/marketing/HomePageClient.tsx +++ b/src/components/marketing/HomePageClient.tsx @@ -30,8 +30,14 @@ export default function HomePageClient() { return ( <> - + + {/* Main Interaction: Generator */} + +
+ +
+ diff --git a/src/components/marketing/InstantGenerator.tsx b/src/components/marketing/InstantGenerator.tsx index eaeb985..fdab8e0 100644 --- a/src/components/marketing/InstantGenerator.tsx +++ b/src/components/marketing/InstantGenerator.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { QRCodeSVG } from 'qrcode.react'; +import { motion } from 'framer-motion'; import { Card } from '@/components/ui/Card'; import { Input } from '@/components/ui/Input'; import { Button } from '@/components/ui/Button'; @@ -73,139 +74,183 @@ export const InstantGenerator: React.FC = ({ t }) => { }; return ( -
+
-
+

{t.generator.title}

-
+
{/* Left Form */} - - setUrl(e.target.value)} - placeholder={t.generator.url_placeholder} - /> + + + setUrl(e.target.value)} + placeholder={t.generator.url_placeholder} + className="transition-all focus:ring-2 focus:ring-primary-500/20" + /> -
-
- -
- setForegroundColor(e.target.value)} - className="w-12 h-10 rounded border border-gray-300" - aria-label="Foreground color picker" - /> - setForegroundColor(e.target.value)} - className="flex-1" - aria-label="Foreground color hex value" - /> +
+
+ +
+ setForegroundColor(e.target.value)} + className="w-12 h-10 rounded border border-gray-300 cursor-pointer" + aria-label="Foreground color picker" + /> + setForegroundColor(e.target.value)} + className="flex-1" + aria-label="Foreground color hex value" + /> +
+
+ +
+ +
+ setBackgroundColor(e.target.value)} + className="w-12 h-10 rounded border border-gray-300 cursor-pointer" + aria-label="Background color picker" + /> + setBackgroundColor(e.target.value)} + className="flex-1" + aria-label="Background color hex value" + /> +
-
- -
+
+
+ + +
+ +
+ setBackgroundColor(e.target.value)} - className="w-12 h-10 rounded border border-gray-300" - aria-label="Background color picker" - /> - setBackgroundColor(e.target.value)} - className="flex-1" - aria-label="Background color hex value" + id="qr-size" + type="range" + min="100" + max="400" + value={size} + onChange={(e) => setSize(Number(e.target.value))} + className="w-full accent-primary-600" + aria-label={`QR code size: ${size} pixels`} /> +
-
-
-
- - +
+ + {hasGoodContrast ? t.generator.contrast_good : 'Low contrast'} + +
+ Contrast: {contrast.toFixed(1)}:1 +
-
- - setSize(Number(e.target.value))} - className="w-full" - aria-label={`QR code size: ${size} pixels`} - /> - +
+ +
-
-
- - {hasGoodContrast ? t.generator.contrast_good : 'Low contrast'} - -
- Contrast: {contrast.toFixed(1)}:1 -
-
- -
- - -
- - - + + {/* Right Preview */} -
- -

{t.generator.live_preview}

-
+ + {/* Artistic Curved Lines Background */} +
+ + + + + + + + + + + + + + + +
+
+ + {/* Decorative Orbs */} +
+
+ +
+

{t.generator.live_preview}

+
{url ? ( -
+
= ({ t }) => {
) : (
Enter URL
)}
-
URL
-
{t.generator.demo_note}
- -
+
+ {url || 'https://example.com'} +
+
{t.generator.demo_note}
+
+
diff --git a/src/components/marketing/Pricing.tsx b/src/components/marketing/Pricing.tsx index b7be3fa..911b8fc 100644 --- a/src/components/marketing/Pricing.tsx +++ b/src/components/marketing/Pricing.tsx @@ -1,6 +1,7 @@ 'use client'; import React, { useState } from 'react'; +import { motion } from 'framer-motion'; import Link from 'next/link'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; @@ -32,89 +33,106 @@ export const Pricing: React.FC = ({ t }) => { return (
-
+

{t.pricing.title}

{t.pricing.subtitle}

-
+
- {plans.map((plan) => ( - ( + - {plan.popular && ( -
- - {t.pricing[plan.key].badge} - -
- )} - - - - {t.pricing[plan.key].title} - -
-
- - {plan.key === 'free' - ? t.pricing[plan.key].price - : billingPeriod === 'month' - ? t.pricing[plan.key].price - : plan.key === 'pro' - ? '€90' - : '€290'} - - - {plan.key === 'free' - ? t.pricing[plan.key].period - : billingPeriod === 'month' - ? t.pricing[plan.key].period - : 'per year'} - -
- {billingPeriod === 'year' && plan.key !== 'free' && ( - - Save 16% + + {plan.popular && ( +
+ + {t.pricing[plan.key].badge} - )} -
- +
+ )} - -
    - {t.pricing[plan.key].features.map((feature: string, index: number) => ( -
  • - - - - {feature} -
  • - ))} -
+ + + {t.pricing[plan.key].title} + +
+
+ + {plan.key === 'free' + ? t.pricing[plan.key].price + : billingPeriod === 'month' + ? t.pricing[plan.key].price + : plan.key === 'pro' + ? '€90' + : '€290'} + + + {plan.key === 'free' + ? t.pricing[plan.key].period + : billingPeriod === 'month' + ? t.pricing[plan.key].period + : 'per year'} + +
+ {billingPeriod === 'year' && plan.key !== 'free' && ( + + Save 16% + + )} +
+
-
- - - -
-
-
+ +
    + {t.pricing[plan.key].features.map((feature: string, index: number) => ( +
  • + + + + {feature} +
  • + ))} +
+ +
+ + + +
+
+ + ))}
diff --git a/src/components/marketing/StaticVsDynamic.tsx b/src/components/marketing/StaticVsDynamic.tsx index 2208f01..2579cf7 100644 --- a/src/components/marketing/StaticVsDynamic.tsx +++ b/src/components/marketing/StaticVsDynamic.tsx @@ -1,6 +1,8 @@ 'use client'; import React from 'react'; +import { motion } from 'framer-motion'; +import { CheckCircle2 } from 'lucide-react'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Badge } from '@/components/ui/Badge'; @@ -12,56 +14,83 @@ export const StaticVsDynamic: React.FC = ({ t }) => { return (
+
+

+ {t.static_vs_dynamic.title} +

+

+ {t.static_vs_dynamic.description} +

+
+
{/* Static QR Codes */} - - -
- {t.static_vs_dynamic.static.title} - {t.static_vs_dynamic.static.subtitle} -
-

{t.static_vs_dynamic.static.description}

-
- -
    - {t.static_vs_dynamic.static.features.map((feature: string, index: number) => ( -
  • -
    - - - -
    - {feature} -
  • - ))} -
-
-
+ + + +
+ {t.static_vs_dynamic.static.title} + {t.static_vs_dynamic.static.subtitle} +
+

{t.static_vs_dynamic.static.description}

+
+ +
    + {t.static_vs_dynamic.static.features.map((feature: string, index: number) => ( +
  • +
    + +
    + {feature} +
  • + ))} +
+
+
+
{/* Dynamic QR Codes */} - - -
- {t.static_vs_dynamic.dynamic.title} - {t.static_vs_dynamic.dynamic.subtitle} + + +
+
+ + + + +
-

{t.static_vs_dynamic.dynamic.description}

- - -
    - {t.static_vs_dynamic.dynamic.features.map((feature: string, index: number) => ( -
  • -
    - - - -
    - {feature} -
  • - ))} -
-
-
+ +
+ {t.static_vs_dynamic.dynamic.title} + {t.static_vs_dynamic.dynamic.subtitle} +
+

{t.static_vs_dynamic.dynamic.description}

+
+ +
    + {t.static_vs_dynamic.dynamic.features.map((feature: string, index: number) => ( +
  • +
    + +
    + {feature} +
  • + ))} +
+
+ +
diff --git a/src/components/marketing/StatsStrip.tsx b/src/components/marketing/StatsStrip.tsx index fc5cebc..82aa87f 100644 --- a/src/components/marketing/StatsStrip.tsx +++ b/src/components/marketing/StatsStrip.tsx @@ -8,22 +8,22 @@ interface StatsStripProps { export const StatsStrip: React.FC = ({ t }) => { const stats = [ - { key: 'users', value: '10,000+', label: t.trust.users }, - { key: 'codes', value: '500,000+', label: t.trust.codes }, - { key: 'scans', value: '50M+', label: t.trust.scans }, + { key: 'users', value: '1,240+', label: t.trust.users }, + { key: 'codes', value: '8,500+', label: t.trust.codes }, + { key: 'scans', value: '1.2M+', label: t.trust.scans }, { key: 'countries', value: '120+', label: t.trust.countries }, ]; return ( -
+
{stats.map((stat, index) => ( -
-
+
+
{stat.value}
-
+
{stat.label}
diff --git a/src/components/ui/Footer.tsx b/src/components/ui/Footer.tsx index e9837f8..cceeaf3 100644 --- a/src/components/ui/Footer.tsx +++ b/src/components/ui/Footer.tsx @@ -26,7 +26,7 @@ export function Footer({ variant = 'marketing' }: FooterProps) {
  • Features
  • Pricing
  • -
  • FAQ
  • +
  • FAQ
  • Blog
@@ -34,7 +34,7 @@ export function Footer({ variant = 'marketing' }: FooterProps) {

Resources

    -
  • Full Pricing
  • +
  • Full Pricing
  • All Questions
  • Blog
  • Get Started
  • diff --git a/src/i18n/en.json b/src/i18n/en.json index e3437ef..1d34218 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -26,10 +26,10 @@ "engagement_badge": "Free Forever" }, "trust": { - "users": "Trusted by small businesses", - "codes": "Simple QR code creation", - "scans": "Track every scan", - "countries": "Works worldwide" + "users": "Happy Users", + "codes": "Active QR Codes", + "scans": "Total Scans", + "countries": "Countries" }, "industries": { "restaurant": "Restaurant Chain", @@ -62,6 +62,8 @@ "demo_note": "This is a demo QR code" }, "static_vs_dynamic": { + "title": "Why Dynamic QR Codes Save You Money", + "description": "Stop re-printing materials. Switch destinations instantly and track every scan.", "static": { "title": "Static QR Codes", "subtitle": "Always Free",