qrmaster.net
This commit is contained in:
parent
424c61a176
commit
8c5e2fa58e
|
|
@ -23,7 +23,8 @@
|
|||
"Bash(ls:*)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(echo \"\n\n## CSRF Debug aktiviert!\n\nBitte teste jetzt:\n1. Browser zu http://localhost:3050/create\n2. Dynamic QR Code erstellen versuchen\n3. Server-Logs zeigen jetzt [CSRF Debug] Output\n\nIch sehe dann:\n- Ob headerToken vorhanden ist\n- Ob cookieToken vorhanden ist \n- Ob sie übereinstimmen\n\n---\n\nStripe Portal 500 Error ist separates Problem:\nhttps://dashboard.stripe.com/test/settings/billing/portal\n→ Customer Portal Configuration muss erstellt werden\n\")",
|
||||
"Bash(pkill:*)"
|
||||
"Bash(pkill:*)",
|
||||
"Skill(shadcn-ui)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
69
README.md
69
README.md
|
|
@ -42,43 +42,53 @@ A production-ready SaaS application for creating and managing QR codes with adva
|
|||
Run database in Docker, app on host machine:
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/qr-master.git
|
||||
cd qr-master
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Copy and configure environment:
|
||||
|
||||
```bash
|
||||
cp env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` and set:
|
||||
- `NEXTAUTH_SECRET` (generate: `openssl rand -base64 32`)
|
||||
- `IP_SALT` (generate: `openssl rand -base64 32`)
|
||||
- (Optional) Google OAuth credentials
|
||||
|
||||
- `NEXTAUTH_SECRET` (generate: `openssl rand -base64 32`)
|
||||
- `IP_SALT` (generate: `openssl rand -base64 32`)
|
||||
- (Optional) Google OAuth credentials
|
||||
|
||||
4. Start database services:
|
||||
|
||||
```bash
|
||||
npm run docker:dev
|
||||
```
|
||||
|
||||
5. Run database migrations and seed:
|
||||
|
||||
```bash
|
||||
npx prisma migrate dev
|
||||
npm run db:seed
|
||||
```
|
||||
|
||||
> **Note**: If you get migration errors, you can reset the database:
|
||||
>
|
||||
> ```bash
|
||||
> npx prisma migrate reset
|
||||
> ```
|
||||
>
|
||||
> This will drop the database, recreate it, run all migrations, and seed data.
|
||||
|
||||
6. Start development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
|
@ -94,6 +104,7 @@ npm run dev
|
|||
Run everything in Docker:
|
||||
|
||||
1. Clone and setup:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/qr-master.git
|
||||
cd qr-master
|
||||
|
|
@ -102,11 +113,13 @@ cp env.example .env
|
|||
```
|
||||
|
||||
2. Build and start:
|
||||
|
||||
```bash
|
||||
npm run docker:prod
|
||||
```
|
||||
|
||||
3. Run migrations:
|
||||
|
||||
```bash
|
||||
docker-compose exec web npx prisma migrate deploy
|
||||
```
|
||||
|
|
@ -158,6 +171,7 @@ npm run docker:backup # Backup database to SQL file
|
|||
### Local Development (without Docker)
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
|
@ -165,17 +179,20 @@ npm install
|
|||
2. Set up PostgreSQL and Redis locally
|
||||
|
||||
3. Configure `.env` with local database URL:
|
||||
|
||||
```env
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5435/qrmaster?schema=public
|
||||
```
|
||||
|
||||
4. Run migrations and seed:
|
||||
|
||||
```bash
|
||||
npx prisma migrate dev
|
||||
npm run db:seed
|
||||
```
|
||||
|
||||
5. Start dev server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
|
@ -194,6 +211,7 @@ npm run db:seed # Then seed manually
|
|||
```
|
||||
|
||||
This is useful when:
|
||||
|
||||
- Schema has changed significantly
|
||||
- You have migration conflicts
|
||||
- You want to start fresh with clean data
|
||||
|
|
@ -224,11 +242,13 @@ qr-master/
|
|||
## API Endpoints
|
||||
|
||||
### Authentication
|
||||
|
||||
- `POST /api/auth/signin` - Sign in with credentials
|
||||
- `POST /api/auth/signout` - Sign out
|
||||
- `GET /api/auth/session` - Get current session
|
||||
|
||||
### QR Codes
|
||||
|
||||
- `GET /api/qrs` - List all QR codes
|
||||
- `POST /api/qrs` - Create a new QR code (dynamic or static)
|
||||
- `POST /api/qrs/static` - Create a static QR code
|
||||
|
|
@ -238,9 +258,11 @@ qr-master/
|
|||
- `DELETE /api/qrs/delete-all` - Delete all user's QR codes
|
||||
|
||||
### Analytics
|
||||
|
||||
- `GET /api/analytics/summary` - Get analytics summary for a QR code
|
||||
|
||||
### User & Settings
|
||||
|
||||
- `GET /api/user/plan` - Get current user plan
|
||||
- `GET /api/user/stats` - Get user statistics
|
||||
- `POST /api/user/password` - Update password
|
||||
|
|
@ -248,31 +270,33 @@ qr-master/
|
|||
- `DELETE /api/user/delete` - Delete account
|
||||
|
||||
### Stripe Payments
|
||||
|
||||
- `POST /api/stripe/checkout` - Create checkout session
|
||||
- `POST /api/stripe/portal` - Create customer portal session
|
||||
- `POST /api/stripe/webhook` - Handle Stripe webhooks
|
||||
- `POST /api/stripe/cancel-subscription` - Cancel subscription
|
||||
|
||||
### Public Redirect
|
||||
|
||||
- `GET /r/[slug]` - Redirect and track QR code scan
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Required | Default |
|
||||
|----------|-------------|----------|---------|
|
||||
| `DATABASE_URL` | PostgreSQL connection string | Yes | `postgresql://postgres:postgres@localhost:5435/qrmaster?schema=public` |
|
||||
| `NEXTAUTH_URL` | Application URL | Yes | `http://localhost:3050` |
|
||||
| `NEXTAUTH_SECRET` | Secret for JWT encryption | Yes | Generate with `openssl rand -base64 32` |
|
||||
| `IP_SALT` | Salt for IP hashing (privacy) | Yes | Generate with `openssl rand -base64 32` |
|
||||
| `GOOGLE_CLIENT_ID` | Google OAuth client ID | No | - |
|
||||
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | No | - |
|
||||
| `STRIPE_SECRET_KEY` | Stripe secret key | No | - |
|
||||
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret | No | - |
|
||||
| `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` | Stripe public key | No | - |
|
||||
| `NEXT_PUBLIC_INDEXABLE` | Allow search engine indexing | No | `false` (set to `true` in production) |
|
||||
| `REDIS_URL` | Redis connection string | No | `redis://redis:6379` |
|
||||
| `NEXT_PUBLIC_POSTHOG_KEY` | PostHog analytics key | No | - |
|
||||
| `NEXT_PUBLIC_POSTHOG_HOST` | PostHog host URL | No | - |
|
||||
| Variable | Description | Required | Default |
|
||||
| ------------------------------------ | ----------------------------- | -------- | ---------------------------------------------------------------------- |
|
||||
| `DATABASE_URL` | PostgreSQL connection string | Yes | `postgresql://postgres:postgres@localhost:5435/qrmaster?schema=public` |
|
||||
| `NEXTAUTH_URL` | Application URL | Yes | `http://localhost:3050` |
|
||||
| `NEXTAUTH_SECRET` | Secret for JWT encryption | Yes | Generate with `openssl rand -base64 32` |
|
||||
| `IP_SALT` | Salt for IP hashing (privacy) | Yes | Generate with `openssl rand -base64 32` |
|
||||
| `GOOGLE_CLIENT_ID` | Google OAuth client ID | No | - |
|
||||
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | No | - |
|
||||
| `STRIPE_SECRET_KEY` | Stripe secret key | No | - |
|
||||
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret | No | - |
|
||||
| `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` | Stripe public key | No | - |
|
||||
| `NEXT_PUBLIC_INDEXABLE` | Allow search engine indexing | No | `false` (set to `true` in production) |
|
||||
| `REDIS_URL` | Redis connection string | No | `redis://redis:6379` |
|
||||
| `NEXT_PUBLIC_POSTHOG_KEY` | PostHog analytics key | No | - |
|
||||
| `NEXT_PUBLIC_POSTHOG_HOST` | PostHog host URL | No | - |
|
||||
|
||||
**Note**: Copy `env.example` to `.env` and update the values before starting.
|
||||
|
||||
|
|
@ -356,12 +380,14 @@ For detailed deployment instructions, see [DOCKER_SETUP.md](DOCKER_SETUP.md).
|
|||
### Database Issues
|
||||
|
||||
**Problem**: Migration errors or schema conflicts
|
||||
|
||||
```bash
|
||||
# Solution: Reset the database
|
||||
npx prisma migrate reset
|
||||
```
|
||||
|
||||
**Problem**: "Error: P1001: Can't reach database server"
|
||||
|
||||
```bash
|
||||
# Check if Docker containers are running
|
||||
docker ps
|
||||
|
|
@ -372,12 +398,14 @@ npm run docker:dev
|
|||
```
|
||||
|
||||
**Problem**: Prisma Client out of sync
|
||||
|
||||
```bash
|
||||
# Regenerate Prisma Client
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
**Problem**: Need to start completely fresh
|
||||
|
||||
```bash
|
||||
# Stop all Docker containers
|
||||
npm run docker:dev:stop
|
||||
|
|
@ -394,6 +422,7 @@ npm run db:seed
|
|||
### Port Already in Use
|
||||
|
||||
If port 3050 is already in use:
|
||||
|
||||
```bash
|
||||
# Find and kill the process (Windows)
|
||||
netstat -ano | findstr :3050
|
||||
|
|
@ -406,12 +435,14 @@ taskkill /PID <PID> /F
|
|||
### Docker Issues
|
||||
|
||||
**Problem**: Permission denied errors
|
||||
|
||||
```bash
|
||||
# Windows: Run PowerShell as Administrator
|
||||
# Linux/Mac: Use sudo for docker commands
|
||||
```
|
||||
|
||||
**Problem**: Out of disk space
|
||||
|
||||
```bash
|
||||
# Clean up Docker
|
||||
docker system prune -a
|
||||
|
|
@ -431,7 +462,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|||
|
||||
## Support
|
||||
|
||||
For support, email support@qrmaster.com or open an issue on GitHub.
|
||||
For support, email support@qrmaster.net or open an issue on GitHub.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/** @type {import('next-sitemap').IConfig} */
|
||||
module.exports = {
|
||||
siteUrl: 'https://www.qrmaster.com',
|
||||
siteUrl: 'https://www.qrmaster.net',
|
||||
generateRobotsTxt: true,
|
||||
robotsTxtOptions: {
|
||||
policies: [
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ const nextConfig = {
|
|||
output: 'standalone',
|
||||
images: {
|
||||
unoptimized: false,
|
||||
domains: ['www.qrmaster.com', 'qrmaster.com', 'images.qrmaster.com'],
|
||||
domains: ['www.qrmaster.net', 'qrmaster.net', 'images.qrmaster.net'],
|
||||
formats: ['image/webp', 'image/avif'],
|
||||
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
||||
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
||||
},
|
||||
experimental: {
|
||||
serverComponentsExternalPackages: ['@prisma/client', 'bcryptjs']
|
||||
}
|
||||
}
|
||||
serverComponentsExternalPackages: ['@prisma/client', 'bcryptjs'],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig
|
||||
export default nextConfig;
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ async function main() {
|
|||
const hashedPassword = await bcrypt.hash('demo123', 12);
|
||||
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email: 'demo@qrmaster.com' },
|
||||
where: { email: 'demo@qrmaster.net' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'demo@qrmaster.com',
|
||||
email: 'demo@qrmaster.net',
|
||||
name: 'Demo User',
|
||||
password: hashedPassword,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://www.qrmaster.com/</loc>
|
||||
<loc>https://www.qrmaster.net/</loc>
|
||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.qrmaster.com/blog</loc>
|
||||
<loc>https://www.qrmaster.net/blog</loc>
|
||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.qrmaster.com/pricing</loc>
|
||||
<loc>https://www.qrmaster.net/pricing</loc>
|
||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.qrmaster.com/faq</loc>
|
||||
<loc>https://www.qrmaster.net/faq</loc>
|
||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.qrmaster.com/blog/qr-code-analytics</loc>
|
||||
<loc>https://www.qrmaster.net/blog/qr-code-analytics</loc>
|
||||
<lastmod>2025-10-16T00:00:00Z</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
|
|
|
|||
|
|
@ -344,7 +344,13 @@ export default function AnalyticsPage() {
|
|||
<td className="px-4 py-4 align-middle">{country.count.toLocaleString()}</td>
|
||||
<td className="px-4 py-4 align-middle">{country.percentage}%</td>
|
||||
<td className="px-4 py-4 align-middle">
|
||||
<Badge variant="success">↑</Badge>
|
||||
<Badge variant={
|
||||
country.trend === 'up' ? 'success' :
|
||||
country.trend === 'down' ? 'destructive' :
|
||||
'default'
|
||||
}>
|
||||
{country.trend === 'up' ? '↑' : country.trend === 'down' ? '↓' : '→'} {country.trendPercentage}%
|
||||
</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
@ -387,8 +393,12 @@ export default function AnalyticsPage() {
|
|||
<td className="px-4 py-4 align-middle">{qr.uniqueScans.toLocaleString()}</td>
|
||||
<td className="px-4 py-4 align-middle">{qr.conversion}%</td>
|
||||
<td className="px-4 py-4 align-middle">
|
||||
<Badge variant={qr.totalScans > 0 ? 'success' : 'default'}>
|
||||
{qr.totalScans > 0 ? '↑' : '—'}
|
||||
<Badge variant={
|
||||
qr.trend === 'up' ? 'success' :
|
||||
qr.trend === 'down' ? 'destructive' :
|
||||
'default'
|
||||
}>
|
||||
{qr.trend === 'up' ? '↑' : qr.trend === 'down' ? '↓' : '→'} {qr.trendPercentage}%
|
||||
</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ interface QRCodeData {
|
|||
contentType: string;
|
||||
content?: any;
|
||||
slug: string;
|
||||
status: 'ACTIVE' | 'PAUSED';
|
||||
createdAt: string;
|
||||
scans: number;
|
||||
style?: any;
|
||||
|
|
@ -219,36 +218,6 @@ export default function DashboardPage() {
|
|||
router.push(`/qr/${id}/edit`);
|
||||
};
|
||||
|
||||
const handlePause = async (id: string) => {
|
||||
try {
|
||||
const qr = qrCodes.find(q => q.id === id);
|
||||
if (!qr) return;
|
||||
|
||||
const newStatus = qr.status === 'ACTIVE' ? 'PAUSED' : 'ACTIVE';
|
||||
|
||||
const response = await fetch(`/api/qrs/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ status: newStatus }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Update local state
|
||||
setQrCodes(qrCodes.map(q =>
|
||||
q.id === id ? { ...q, status: newStatus } : q
|
||||
));
|
||||
showToast(`QR code ${newStatus === 'ACTIVE' ? 'resumed' : 'paused'}!`, 'success');
|
||||
} else {
|
||||
throw new Error('Failed to update status');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating QR status:', error);
|
||||
showToast('Failed to update QR code status', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Are you sure you want to delete this QR code? This action cannot be undone.')) {
|
||||
return;
|
||||
|
|
@ -393,7 +362,6 @@ export default function DashboardPage() {
|
|||
key={qr.id}
|
||||
qr={qr}
|
||||
onEdit={handleEdit}
|
||||
onPause={handlePause}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,14 @@ import { Button } from '@/components/ui/Button';
|
|||
import { Badge } from '@/components/ui/Badge';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { BillingToggle } from '@/components/ui/BillingToggle';
|
||||
|
||||
export default function PricingPage() {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState<string | null>(null);
|
||||
const [currentPlan, setCurrentPlan] = useState<string>('FREE');
|
||||
const [currentInterval, setCurrentInterval] = useState<'month' | 'year' | null>(null);
|
||||
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch current user plan
|
||||
|
|
@ -20,6 +23,7 @@ export default function PricingPage() {
|
|||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setCurrentPlan(data.plan || 'FREE');
|
||||
setCurrentInterval(data.interval || null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching user plan:', error);
|
||||
|
|
@ -40,7 +44,7 @@ export default function PricingPage() {
|
|||
},
|
||||
body: JSON.stringify({
|
||||
plan,
|
||||
billingInterval: 'month',
|
||||
billingInterval: billingPeriod === 'month' ? 'month' : 'year',
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -95,17 +99,31 @@ export default function PricingPage() {
|
|||
}
|
||||
};
|
||||
|
||||
// Helper function to check if this is the user's exact current plan (plan + interval)
|
||||
const isCurrentPlanWithInterval = (planType: string, interval: 'month' | 'year') => {
|
||||
return currentPlan === planType && currentInterval === interval;
|
||||
};
|
||||
|
||||
// Helper function to check if user has this plan but different interval
|
||||
const hasPlanDifferentInterval = (planType: string) => {
|
||||
return currentPlan === planType && currentInterval && currentInterval !== billingPeriod;
|
||||
};
|
||||
|
||||
const selectedInterval = billingPeriod === 'month' ? 'month' : 'year';
|
||||
|
||||
const plans = [
|
||||
{
|
||||
key: 'free',
|
||||
name: 'Free',
|
||||
price: '€0',
|
||||
period: 'forever',
|
||||
showDiscount: false,
|
||||
features: [
|
||||
'3 dynamic QR codes',
|
||||
'Unlimited static QR codes',
|
||||
'Basic scan tracking',
|
||||
'Standard QR design templates',
|
||||
'Download as SVG/PNG',
|
||||
],
|
||||
buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Downgrade to Free',
|
||||
buttonVariant: 'outline' as const,
|
||||
|
|
@ -116,26 +134,31 @@ export default function PricingPage() {
|
|||
{
|
||||
key: 'pro',
|
||||
name: 'Pro',
|
||||
price: '€9',
|
||||
period: 'per month',
|
||||
price: billingPeriod === 'month' ? '€9' : '€90',
|
||||
period: billingPeriod === 'month' ? 'per month' : 'per year',
|
||||
showDiscount: billingPeriod === 'year',
|
||||
features: [
|
||||
'50 dynamic QR codes',
|
||||
'Unlimited static QR codes',
|
||||
'Advanced analytics (scans, devices, locations)',
|
||||
'Custom branding (colors)',
|
||||
'Download as SVG/PNG',
|
||||
],
|
||||
buttonText: currentPlan === 'PRO' ? 'Current Plan' : 'Upgrade to Pro',
|
||||
buttonText: isCurrentPlanWithInterval('PRO', selectedInterval)
|
||||
? 'Current Plan'
|
||||
: hasPlanDifferentInterval('PRO')
|
||||
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
|
||||
: 'Upgrade to Pro',
|
||||
buttonVariant: 'primary' as const,
|
||||
disabled: currentPlan === 'PRO',
|
||||
disabled: isCurrentPlanWithInterval('PRO', selectedInterval),
|
||||
popular: true,
|
||||
onUpgrade: () => handleUpgrade('PRO'),
|
||||
},
|
||||
{
|
||||
key: 'business',
|
||||
name: 'Business',
|
||||
price: '€29',
|
||||
period: 'per month',
|
||||
price: billingPeriod === 'month' ? '€29' : '€290',
|
||||
period: billingPeriod === 'month' ? 'per month' : 'per year',
|
||||
showDiscount: billingPeriod === 'year',
|
||||
features: [
|
||||
'500 dynamic QR codes',
|
||||
'Unlimited static QR codes',
|
||||
|
|
@ -144,9 +167,13 @@ export default function PricingPage() {
|
|||
'Priority email support',
|
||||
'Advanced tracking & insights',
|
||||
],
|
||||
buttonText: currentPlan === 'BUSINESS' ? 'Current Plan' : 'Upgrade to Business',
|
||||
buttonText: isCurrentPlanWithInterval('BUSINESS', selectedInterval)
|
||||
? 'Current Plan'
|
||||
: hasPlanDifferentInterval('BUSINESS')
|
||||
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
|
||||
: 'Upgrade to Business',
|
||||
buttonVariant: 'primary' as const,
|
||||
disabled: currentPlan === 'BUSINESS',
|
||||
disabled: isCurrentPlanWithInterval('BUSINESS', selectedInterval),
|
||||
popular: false,
|
||||
onUpgrade: () => handleUpgrade('BUSINESS'),
|
||||
},
|
||||
|
|
@ -163,6 +190,10 @@ export default function PricingPage() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mb-8">
|
||||
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto">
|
||||
{plans.map((plan) => (
|
||||
<Card
|
||||
|
|
@ -181,13 +212,20 @@ export default function PricingPage() {
|
|||
<CardTitle className="text-2xl mb-4">
|
||||
{plan.name}
|
||||
</CardTitle>
|
||||
<div className="flex items-baseline justify-center">
|
||||
<span className="text-4xl font-bold">
|
||||
{plan.price}
|
||||
</span>
|
||||
<span className="text-gray-600 ml-2">
|
||||
{plan.period}
|
||||
</span>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex items-baseline justify-center">
|
||||
<span className="text-4xl font-bold">
|
||||
{plan.price}
|
||||
</span>
|
||||
<span className="text-gray-600 ml-2">
|
||||
{plan.period}
|
||||
</span>
|
||||
</div>
|
||||
{plan.showDiscount && (
|
||||
<Badge variant="success" className="mt-2">
|
||||
Save 16%
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
|
|
@ -222,7 +260,7 @@ export default function PricingPage() {
|
|||
All plans include unlimited static QR codes and basic customization.
|
||||
</p>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Need help choosing? <a href="mailto:support@qrmaster.com" className="text-primary-600 hover:text-primary-700 underline">Contact our team</a>
|
||||
Need help choosing? <a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -65,6 +65,9 @@ export default function LoginPage() {
|
|||
</Link>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1>
|
||||
<p className="text-gray-600 mt-2">Sign in to your account</p>
|
||||
<Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors">
|
||||
← Back to Home
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
|
|
@ -158,10 +161,6 @@ export default function LoginPage() {
|
|||
|
||||
<p className="text-center text-sm text-gray-500 mt-6">
|
||||
By signing in, you agree to our{' '}
|
||||
<Link href="/terms" className="text-primary-600 hover:text-primary-700">
|
||||
Terms of Service
|
||||
</Link>{' '}
|
||||
and{' '}
|
||||
<Link href="/privacy" className="text-primary-600 hover:text-primary-700">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -77,6 +77,9 @@ export default function SignupPage() {
|
|||
</Link>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Create Account</h1>
|
||||
<p className="text-gray-600 mt-2">Start creating QR codes in seconds</p>
|
||||
<Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors">
|
||||
← Back to Home
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
|
|
@ -124,26 +127,6 @@ export default function SignupPage() {
|
|||
required
|
||||
/>
|
||||
|
||||
<div className="flex items-start">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-2 mt-1"
|
||||
required
|
||||
onInvalid={(e) => (e.target as HTMLInputElement).setCustomValidity('Please check this box if you want to proceed')}
|
||||
onInput={(e) => (e.target as HTMLInputElement).setCustomValidity('')}
|
||||
/>
|
||||
<label className="text-sm text-gray-600">
|
||||
I agree to the{' '}
|
||||
<Link href="/terms" className="text-primary-600 hover:text-primary-700">
|
||||
Terms of Service
|
||||
</Link>{' '}
|
||||
and{' '}
|
||||
<Link href="/privacy" className="text-primary-600 hover:text-primary-700">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" loading={loading}>
|
||||
Create Account
|
||||
</Button>
|
||||
|
|
@ -195,6 +178,13 @@ export default function SignupPage() {
|
|||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<p className="text-center text-sm text-gray-500 mt-6">
|
||||
By signing up, you agree to our{' '}
|
||||
<Link href="/privacy" className="text-primary-600 hover:text-primary-700">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const blogPosts: Record<string, BlogPostData> = {
|
|||
image: '/blog/4-hero.png',
|
||||
imageAlt: 'Smartphone displaying QR code scan with modern tech aesthetic',
|
||||
author: 'QR Master Team',
|
||||
authorUrl: 'https://www.qrmaster.com/about',
|
||||
authorUrl: 'https://www.qrmaster.net/about',
|
||||
answer: 'QR code analytics empowers marketers to track scan rates, user behavior, and campaign ROI through real-time dashboards, enabling data-driven optimization of dynamic QR codes and branded marketing campaigns.',
|
||||
howTo: {
|
||||
name: 'How to Track QR Code Scans',
|
||||
|
|
@ -50,7 +50,7 @@ const blogPosts: Record<string, BlogPostData> = {
|
|||
{
|
||||
name: 'Create a Dynamic QR Code',
|
||||
text: 'Log into your QR Master dashboard and select "Create Dynamic QR Code". Enter your destination URL and customize design options.',
|
||||
url: 'https://www.qrmaster.com/create',
|
||||
url: 'https://www.qrmaster.net/create',
|
||||
},
|
||||
{
|
||||
name: 'Enable UTM Tracking',
|
||||
|
|
@ -59,7 +59,7 @@ const blogPosts: Record<string, BlogPostData> = {
|
|||
{
|
||||
name: 'Access Analytics Dashboard',
|
||||
text: 'Navigate to Dashboard → Analytics to view real-time scan data, geographic distribution, and device breakdowns.',
|
||||
url: 'https://www.qrmaster.com/analytics',
|
||||
url: 'https://www.qrmaster.net/analytics',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -156,7 +156,7 @@ const blogPosts: Record<string, BlogPostData> = {
|
|||
image: '/blog/1-hero.png',
|
||||
imageAlt: 'QR code tracking and analytics visualization',
|
||||
author: 'QR Master Team',
|
||||
authorUrl: 'https://www.qrmaster.com/about',
|
||||
authorUrl: 'https://www.qrmaster.net/about',
|
||||
answer: 'QR code tracking allows you to monitor scan metrics including location, device type, time, and user behavior using dynamic QR codes. Only dynamic QR codes can be tracked—static codes cannot provide analytics. Use tools like QR Master, Google Analytics with UTM parameters, or URL shorteners to track scans and measure campaign ROI effectively.',
|
||||
howTo: {
|
||||
name: 'How to Set Up QR Code Tracking',
|
||||
|
|
@ -166,7 +166,7 @@ const blogPosts: Record<string, BlogPostData> = {
|
|||
{
|
||||
name: 'Create a Dynamic QR Code',
|
||||
text: 'Sign up for QR Master and create a dynamic QR code. Enter your destination URL and customize the design with your brand colors and logo.',
|
||||
url: 'https://www.qrmaster.com/signup',
|
||||
url: 'https://www.qrmaster.net/signup',
|
||||
},
|
||||
{
|
||||
name: 'Add UTM Parameters',
|
||||
|
|
@ -179,7 +179,7 @@ const blogPosts: Record<string, BlogPostData> = {
|
|||
{
|
||||
name: 'Monitor Analytics Dashboard',
|
||||
text: 'Access your QR Master dashboard to view real-time scan data: total scans, unique users, geographic location, device types, and scan timestamps.',
|
||||
url: 'https://www.qrmaster.com/analytics',
|
||||
url: 'https://www.qrmaster.net/analytics',
|
||||
},
|
||||
{
|
||||
name: 'Optimize Based on Data',
|
||||
|
|
@ -212,7 +212,7 @@ const blogPosts: Record<string, BlogPostData> = {
|
|||
|
||||
<p><strong>Static QR Codes:</strong> These encode the destination URL directly into the QR code pattern. Once generated, the content cannot be changed, and no tracking is possible. The QR code reader goes directly to the encoded destination without any intermediate server.</p>
|
||||
|
||||
<p><strong>Dynamic QR Codes:</strong> These contain a short redirect URL (like qrmaster.com/abc123) that points to a server. The server logs the scan data and then redirects to your actual destination URL. This enables tracking AND allows you to change the destination URL anytime—even after printing thousands of codes.</p>
|
||||
<p><strong>Dynamic QR Codes:</strong> These contain a short redirect URL (like qrmaster.net/abc123) that points to a server. The server logs the scan data and then redirects to your actual destination URL. This enables tracking AND allows you to change the destination URL anytime—even after printing thousands of codes.</p>
|
||||
|
||||
<div class="overflow-x-auto my-8">
|
||||
<table class="min-w-full border-collapse border border-gray-300">
|
||||
|
|
@ -276,7 +276,7 @@ const blogPosts: Record<string, BlogPostData> = {
|
|||
|
||||
<h4>Step-by-Step with QR Master:</h4>
|
||||
<ol>
|
||||
<li><strong>Sign up for free:</strong> Create your QR Master account at <a href="https://www.qrmaster.com/signup">qrmaster.com/signup</a></li>
|
||||
<li><strong>Sign up for free:</strong> Create your QR Master account at <a href="https://www.qrmaster.net/signup">qrmaster.net/signup</a></li>
|
||||
<li><strong>Create dynamic QR code:</strong> Click "Create QR Code" and select "Dynamic QR"</li>
|
||||
<li><strong>Enter destination URL:</strong> Add the website, landing page, or content you want to link</li>
|
||||
<li><strong>Customize design:</strong> Add your logo, brand colors, and custom frame</li>
|
||||
|
|
@ -554,10 +554,10 @@ app.get('/qr/:id', async (req, res) => {
|
|||
<h3>Multi-Channel Attribution</h3>
|
||||
<p>Use unique QR codes for each marketing channel to measure which drives the best results:</p>
|
||||
<ul>
|
||||
<li>Billboard: <code>qrmaster.com/billboard-nyc</code></li>
|
||||
<li>Magazine ad: <code>qrmaster.com/magazine-vogue</code></li>
|
||||
<li>Product packaging: <code>qrmaster.com/packaging-productA</code></li>
|
||||
<li>Business card: <code>qrmaster.com/card-john</code></li>
|
||||
<li>Billboard: <code>qrmaster.net/billboard-nyc</code></li>
|
||||
<li>Magazine ad: <code>qrmaster.net/magazine-vogue</code></li>
|
||||
<li>Product packaging: <code>qrmaster.net/packaging-productA</code></li>
|
||||
<li>Business card: <code>qrmaster.net/card-john</code></li>
|
||||
</ul>
|
||||
<p>Track scans separately to calculate ROI per channel.</p>
|
||||
|
||||
|
|
@ -680,7 +680,7 @@ app.get('/qr/:id', async (req, res) => {
|
|||
image: '/blog/2-hero.png',
|
||||
imageAlt: 'Two QR codes side by side showing static and dynamic comparison',
|
||||
author: 'QR Master Team',
|
||||
authorUrl: 'https://www.qrmaster.com/about',
|
||||
authorUrl: 'https://www.qrmaster.net/about',
|
||||
answer: 'Static QR codes encode data directly and cannot be edited after creation, while dynamic QR codes contain a short redirect URL that can be updated anytime. Dynamic QR codes also provide tracking analytics, making them ideal for marketing campaigns. Static QR codes work forever without subscriptions, perfect for permanent content like contact cards or fixed URLs.',
|
||||
content: `<div class="blog-content">
|
||||
<p>Choosing between static and dynamic QR codes is one of the most important decisions when implementing a QR code strategy. According to <a href="https://en.wikipedia.org/wiki/QR_code" target="_blank" rel="noopener">Wikipedia</a>, QR codes were invented in 1994 by Masahiro Hara at Denso Wave for automotive part tracking. Today, QR codes have evolved into sophisticated marketing tools, with dynamic QR codes offering features unimaginable in their original static form.</p>
|
||||
|
|
@ -751,7 +751,7 @@ No Tracking | Cannot Edit | Works Forever
|
|||
<h3>How Dynamic QR Codes Work</h3>
|
||||
<p>Think of a dynamic QR code like a phone forwarding service. When someone calls your forwarding number (the short URL in the QR code), the service logs the call and forwards it to your real phone (the destination URL). You can change your real phone number anytime without changing the forwarding number people dial.</p>
|
||||
|
||||
<p><strong>Example:</strong> A dynamic QR code might contain <code>qrmaster.com/abc123</code>. When scanned, this redirects to your actual URL: <code>https://www.yourwebsite.com/summer-sale-2025</code>. Later, you can change it to <code>https://www.yourwebsite.com/fall-sale-2025</code> without reprinting.</p>
|
||||
<p><strong>Example:</strong> A dynamic QR code might contain <code>qrmaster.net/abc123</code>. When scanned, this redirects to your actual URL: <code>https://www.yourwebsite.com/summer-sale-2025</code>. Later, you can change it to <code>https://www.yourwebsite.com/fall-sale-2025</code> without reprinting.</p>
|
||||
|
||||
<h3>Common Uses for Dynamic QR Codes</h3>
|
||||
<ul>
|
||||
|
|
@ -769,7 +769,7 @@ No Tracking | Cannot Edit | Works Forever
|
|||
<li><strong>✅ Edit destination anytime:</strong> Change URL without reprinting QR codes—save thousands in reprint costs.</li>
|
||||
<li><strong>✅ Full analytics:</strong> Track scans, geographic location, device types, time patterns, and user behavior.</li>
|
||||
<li><strong>✅ A/B testing:</strong> Test different landing pages to optimize conversion rates.</li>
|
||||
<li><strong>✅ Short, clean URLs:</strong> QR code contains <code>qrmaster.com/abc123</code> instead of long ugly URLs.</li>
|
||||
<li><strong>✅ Short, clean URLs:</strong> QR code contains <code>qrmaster.net/abc123</code> instead of long ugly URLs.</li>
|
||||
<li><strong>✅ Set expiration dates:</strong> Configure codes to stop working after campaigns end.</li>
|
||||
<li><strong>✅ Password protection:</strong> Require password to access destination content.</li>
|
||||
<li><strong>✅ Retargeting pixels:</strong> Add Facebook Pixel, Google Ads tracking for remarketing.</li>
|
||||
|
|
@ -791,13 +791,13 @@ No Tracking | Cannot Edit | Works Forever
|
|||
<h3>Visual Example: Dynamic QR Code Data Flow</h3>
|
||||
<pre class="bg-gray-100 p-4 rounded-lg my-6 overflow-x-auto">
|
||||
Dynamic QR Code Content:
|
||||
qrmaster.com/abc123
|
||||
qrmaster.net/abc123
|
||||
|
||||
User Scans QR Code
|
||||
↓
|
||||
QR Scanner Decodes Pattern
|
||||
↓
|
||||
Contacts: qrmaster.com/abc123
|
||||
Contacts: qrmaster.net/abc123
|
||||
↓
|
||||
Server Logs: Device, Location, Time, User Agent
|
||||
↓
|
||||
|
|
@ -1197,7 +1197,7 @@ Will the destination URL ever change?
|
|||
image: '/blog/3-hero.png',
|
||||
imageAlt: 'Multiple QR codes arranged in organized grid pattern',
|
||||
author: 'QR Master Team',
|
||||
authorUrl: 'https://www.qrmaster.com/about',
|
||||
authorUrl: 'https://www.qrmaster.net/about',
|
||||
answer: 'Bulk QR code generation from Excel allows you to create hundreds or thousands of QR codes simultaneously by uploading a CSV or Excel file. The file should contain columns for name, URL, and optional metadata. Tools like QR Master Pro can process 1,000+ codes in minutes, saving hours of manual work. Perfect for product labels, event tickets, asset tracking, and marketing campaigns.',
|
||||
howTo: {
|
||||
name: 'How to Generate Bulk QR Codes from Excel',
|
||||
|
|
@ -1211,7 +1211,7 @@ Will the destination URL ever change?
|
|||
{
|
||||
name: 'Sign Up for QR Master Business',
|
||||
text: 'Create a QR Master account and upgrade to Business plan for bulk upload feature (supports up to 500 codes).',
|
||||
url: 'https://www.qrmaster.com/signup',
|
||||
url: 'https://www.qrmaster.net/signup',
|
||||
},
|
||||
{
|
||||
name: 'Upload Your File',
|
||||
|
|
@ -1224,7 +1224,7 @@ Will the destination URL ever change?
|
|||
{
|
||||
name: 'Generate and Download',
|
||||
text: 'Click Generate All. Processing takes 2-4 minutes for 1,000 codes. Download the ZIP file with all QR codes organized by name.',
|
||||
url: 'https://www.qrmaster.com/bulk-qr-code-generator',
|
||||
url: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -1439,7 +1439,7 @@ Event Ticket 1 | https://event.com/ticket/1 | events, tickets
|
|||
|
||||
<h3>Step 2: Sign Up for QR Master</h3>
|
||||
<ol>
|
||||
<li>Go to <a href="https://www.qrmaster.com/signup">qrmaster.com/signup</a></li>
|
||||
<li>Go to <a href="https://www.qrmaster.net/signup">qrmaster.net/signup</a></li>
|
||||
<li>Create free account (email + password)</li>
|
||||
<li>Verify your email</li>
|
||||
<li><strong>Free plan:</strong> Up to 3 dynamic QR codes (no bulk upload)</li>
|
||||
|
|
@ -1770,7 +1770,7 @@ Chicago-Store,https://promo.com?location=chicago,illinois retail
|
|||
<p>For recurring bulk generation needs, use QR Master's API:</p>
|
||||
<pre class="bg-gray-100 p-4 rounded-lg my-4">
|
||||
// Example: Node.js API call
|
||||
const response = await fetch('https://api.qrmaster.com/v1/bulk', {
|
||||
const response = await fetch('https://api.qrmaster.net/v1/bulk', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer YOUR_API_KEY',
|
||||
|
|
@ -1864,16 +1864,16 @@ export async function generateMetadata({ params }: { params: { slug: string } })
|
|||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: `https://www.qrmaster.com/blog/${params.slug}`,
|
||||
canonical: `https://www.qrmaster.net/blog/${params.slug}`,
|
||||
languages: {
|
||||
'x-default': `https://www.qrmaster.com/blog/${params.slug}`,
|
||||
en: `https://www.qrmaster.com/blog/${params.slug}`,
|
||||
'x-default': `https://www.qrmaster.net/blog/${params.slug}`,
|
||||
en: `https://www.qrmaster.net/blog/${params.slug}`,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: `https://www.qrmaster.com/blog/${params.slug}`,
|
||||
url: `https://www.qrmaster.net/blog/${params.slug}`,
|
||||
type: 'article',
|
||||
publishedTime: post.datePublished,
|
||||
modifiedTime: post.dateModified,
|
||||
|
|
|
|||
|
|
@ -26,16 +26,16 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.com/blog',
|
||||
canonical: 'https://www.qrmaster.net/blog',
|
||||
languages: {
|
||||
'x-default': 'https://www.qrmaster.com/blog',
|
||||
en: 'https://www.qrmaster.com/blog',
|
||||
'x-default': 'https://www.qrmaster.net/blog',
|
||||
en: 'https://www.qrmaster.net/blog',
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: 'https://www.qrmaster.com/blog',
|
||||
url: 'https://www.qrmaster.net/blog',
|
||||
type: 'website',
|
||||
},
|
||||
twitter: {
|
||||
|
|
|
|||
|
|
@ -12,16 +12,16 @@ export const metadata: Metadata = {
|
|||
description: 'Generate hundreds of QR codes at once from CSV or Excel files. Create URLs, vCards, locations, phone numbers, and text QR codes in bulk. Perfect for products, events, inventory management.',
|
||||
keywords: 'bulk qr code generator, batch qr code, qr code from excel, csv qr code generator, mass qr code generation, bulk vcard qr code, bulk qr codes free',
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.com/bulk-qr-code-generator',
|
||||
canonical: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
||||
languages: {
|
||||
'x-default': 'https://www.qrmaster.com/bulk-qr-code-generator',
|
||||
en: 'https://www.qrmaster.com/bulk-qr-code-generator',
|
||||
'x-default': 'https://www.qrmaster.net/bulk-qr-code-generator',
|
||||
en: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel',
|
||||
description: 'Generate hundreds of QR codes at once from CSV or Excel files. Perfect for products, events, and inventory.',
|
||||
url: 'https://www.qrmaster.com/bulk-qr-code-generator',
|
||||
url: 'https://www.qrmaster.net/bulk-qr-code-generator',
|
||||
type: 'website',
|
||||
},
|
||||
twitter: {
|
||||
|
|
@ -171,7 +171,7 @@ export default function BulkQRCodeGeneratorPage() {
|
|||
const softwareSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
'@id': 'https://www.qrmaster.com/bulk-qr-code-generator#software',
|
||||
'@id': 'https://www.qrmaster.net/bulk-qr-code-generator#software',
|
||||
name: 'QR Master - Bulk QR Code Generator',
|
||||
applicationCategory: 'BusinessApplication',
|
||||
operatingSystem: 'Web Browser',
|
||||
|
|
@ -202,7 +202,7 @@ export default function BulkQRCodeGeneratorPage() {
|
|||
const howToSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'HowTo',
|
||||
'@id': 'https://www.qrmaster.com/bulk-qr-code-generator#howto',
|
||||
'@id': 'https://www.qrmaster.net/bulk-qr-code-generator#howto',
|
||||
name: 'How to Generate Bulk QR Codes from Excel',
|
||||
description: 'Learn how to create hundreds of QR codes from an Excel or CSV file',
|
||||
totalTime: 'PT10M',
|
||||
|
|
@ -218,7 +218,7 @@ export default function BulkQRCodeGeneratorPage() {
|
|||
position: 2,
|
||||
name: 'Upload File',
|
||||
text: 'Log into QR Master and upload your file to the bulk generator',
|
||||
url: 'https://www.qrmaster.com/bulk-creation',
|
||||
url: 'https://www.qrmaster.net/bulk-creation',
|
||||
},
|
||||
{
|
||||
'@type': 'HowToStep',
|
||||
|
|
@ -244,7 +244,7 @@ export default function BulkQRCodeGeneratorPage() {
|
|||
const faqSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
'@id': 'https://www.qrmaster.com/bulk-qr-code-generator#faq',
|
||||
'@id': 'https://www.qrmaster.net/bulk-qr-code-generator#faq',
|
||||
mainEntity: [
|
||||
{
|
||||
'@type': 'Question',
|
||||
|
|
|
|||
|
|
@ -12,16 +12,16 @@ export const metadata: Metadata = {
|
|||
description: 'Create dynamic QR codes that can be edited after printing. Change destination URL, track scans, and update content without reprinting. Free dynamic QR code generator.',
|
||||
keywords: 'dynamic qr code generator, editable qr code, dynamic qr code, free dynamic qr code, qr code generator dynamic, best dynamic qr code generator',
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.com/dynamic-qr-code-generator',
|
||||
canonical: 'https://www.qrmaster.net/dynamic-qr-code-generator',
|
||||
languages: {
|
||||
'x-default': 'https://www.qrmaster.com/dynamic-qr-code-generator',
|
||||
en: 'https://www.qrmaster.com/dynamic-qr-code-generator',
|
||||
'x-default': 'https://www.qrmaster.net/dynamic-qr-code-generator',
|
||||
en: 'https://www.qrmaster.net/dynamic-qr-code-generator',
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master',
|
||||
description: 'Create dynamic QR codes that can be edited after printing. Change URLs, track scans, and update content anytime.',
|
||||
url: 'https://www.qrmaster.com/dynamic-qr-code-generator',
|
||||
url: 'https://www.qrmaster.net/dynamic-qr-code-generator',
|
||||
type: 'website',
|
||||
},
|
||||
twitter: {
|
||||
|
|
@ -132,7 +132,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
|||
const softwareSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
'@id': 'https://www.qrmaster.com/dynamic-qr-code-generator#software',
|
||||
'@id': 'https://www.qrmaster.net/dynamic-qr-code-generator#software',
|
||||
name: 'QR Master - Dynamic QR Code Generator',
|
||||
applicationCategory: 'BusinessApplication',
|
||||
operatingSystem: 'Web Browser',
|
||||
|
|
@ -163,7 +163,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
|||
const howToSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'HowTo',
|
||||
'@id': 'https://www.qrmaster.com/dynamic-qr-code-generator#howto',
|
||||
'@id': 'https://www.qrmaster.net/dynamic-qr-code-generator#howto',
|
||||
name: 'How to Create a Dynamic QR Code',
|
||||
description: 'Learn how to create editable QR codes that can be updated after printing',
|
||||
totalTime: 'PT3M',
|
||||
|
|
@ -173,14 +173,14 @@ export default function DynamicQRCodeGeneratorPage() {
|
|||
position: 1,
|
||||
name: 'Sign Up Free',
|
||||
text: 'Create a free QR Master account to start generating dynamic QR codes',
|
||||
url: 'https://www.qrmaster.com/signup',
|
||||
url: 'https://www.qrmaster.net/signup',
|
||||
},
|
||||
{
|
||||
'@type': 'HowToStep',
|
||||
position: 2,
|
||||
name: 'Generate QR Code',
|
||||
text: 'Enter your destination URL and customize the design with your branding',
|
||||
url: 'https://www.qrmaster.com/create',
|
||||
url: 'https://www.qrmaster.net/create',
|
||||
},
|
||||
{
|
||||
'@type': 'HowToStep',
|
||||
|
|
@ -193,7 +193,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
|||
position: 4,
|
||||
name: 'Update Anytime',
|
||||
text: 'Log into your dashboard to change the destination URL whenever needed - no reprinting required',
|
||||
url: 'https://www.qrmaster.com/dashboard',
|
||||
url: 'https://www.qrmaster.net/dashboard',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -201,7 +201,7 @@ export default function DynamicQRCodeGeneratorPage() {
|
|||
const faqSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
'@id': 'https://www.qrmaster.com/dynamic-qr-code-generator#faq',
|
||||
'@id': 'https://www.qrmaster.net/dynamic-qr-code-generator#faq',
|
||||
mainEntity: [
|
||||
{
|
||||
'@type': 'Question',
|
||||
|
|
|
|||
|
|
@ -22,16 +22,16 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.com/faq',
|
||||
canonical: 'https://www.qrmaster.net/faq',
|
||||
languages: {
|
||||
'x-default': 'https://www.qrmaster.com/faq',
|
||||
en: 'https://www.qrmaster.com/faq',
|
||||
'x-default': 'https://www.qrmaster.net/faq',
|
||||
en: 'https://www.qrmaster.net/faq',
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: 'https://www.qrmaster.com/faq',
|
||||
url: 'https://www.qrmaster.net/faq',
|
||||
type: 'website',
|
||||
},
|
||||
twitter: {
|
||||
|
|
@ -129,8 +129,8 @@ export default function FAQPage() {
|
|||
</h2>
|
||||
<p className="text-lg text-gray-700 mb-6 leading-relaxed">
|
||||
Our support team is here to help. Contact us at{' '}
|
||||
<a href="mailto:support@qrmaster.com" className="text-blue-600 hover:text-blue-700 font-semibold">
|
||||
support@qrmaster.com
|
||||
<a href="mailto:support@qrmaster.net" className="text-blue-600 hover:text-blue-700 font-semibold">
|
||||
support@qrmaster.net
|
||||
</a>{' '}
|
||||
or reach out through our live chat.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -1,189 +0,0 @@
|
|||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Legal Notice | QR Master',
|
||||
description: 'Legal notice and company information for QR Master',
|
||||
};
|
||||
|
||||
export default function ImpressumPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-white py-12">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl">
|
||||
<div className="mb-8">
|
||||
<Link href="/" className="text-primary-600 hover:text-primary-700 font-medium">
|
||||
← Back to Home
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">Legal Notice</h1>
|
||||
<p className="text-gray-600 mb-8">Information according to § 5 TMG (German Telemedia Act)</p>
|
||||
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Service Provider</h2>
|
||||
<div className="bg-gray-50 p-6 rounded-lg mb-4">
|
||||
<p className="text-gray-700 mb-2"><strong>Company Name:</strong> [Your Company / Your Name]</p>
|
||||
<p className="text-gray-700 mb-2"><strong>Legal Form:</strong> [e.g., Sole Proprietorship, LLC, Corporation]</p>
|
||||
<p className="text-gray-700 mb-2"><strong>Address:</strong></p>
|
||||
<p className="text-gray-700 ml-4 mb-2">
|
||||
[Street and Number]<br />
|
||||
[Postal Code City]<br />
|
||||
[Country]
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Contact</h2>
|
||||
<div className="bg-gray-50 p-6 rounded-lg">
|
||||
<p className="text-gray-700 mb-2">
|
||||
<strong>Email:</strong>{' '}
|
||||
<a href="mailto:info@qrmaster.com" className="text-primary-600 hover:text-primary-700">
|
||||
info@qrmaster.com
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-gray-700 mb-2"><strong>Phone:</strong> [Your Phone Number]</p>
|
||||
<p className="text-gray-700 mb-2"><strong>Website:</strong> <a href="/" className="text-primary-600 hover:text-primary-700">www.qrmaster.com</a></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Authorized Representative</h2>
|
||||
<div className="bg-gray-50 p-6 rounded-lg">
|
||||
<p className="text-gray-700 mb-2"><strong>Managing Director / Owner:</strong> [Name]</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Commercial Register</h2>
|
||||
<div className="bg-gray-50 p-6 rounded-lg">
|
||||
<p className="text-gray-700 mb-2"><strong>Register Court:</strong> [e.g., Munich District Court]</p>
|
||||
<p className="text-gray-700 mb-2"><strong>Registration Number:</strong> [e.g., HRB 123456]</p>
|
||||
<p className="text-gray-400 text-sm mt-2">(If applicable)</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">VAT Identification Number</h2>
|
||||
<div className="bg-gray-50 p-6 rounded-lg">
|
||||
<p className="text-gray-700 mb-2">
|
||||
<strong>VAT ID according to § 27a UStG:</strong> [Your VAT ID]
|
||||
</p>
|
||||
<p className="text-gray-400 text-sm mt-2">(If applicable)</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Responsible for Content</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
Responsible for content according to § 55 para. 2 RStV:
|
||||
</p>
|
||||
<div className="bg-gray-50 p-6 rounded-lg">
|
||||
<p className="text-gray-700 mb-2"><strong>Name:</strong> [Name]</p>
|
||||
<p className="text-gray-700 mb-2"><strong>Address:</strong> [Same as above]</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">EU Dispute Resolution</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
The European Commission provides a platform for online dispute resolution (ODR):
|
||||
{' '}<a
|
||||
href="https://ec.europa.eu/consumers/odr/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary-600 hover:text-primary-700"
|
||||
>
|
||||
https://ec.europa.eu/consumers/odr/
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-gray-700 mb-4">
|
||||
You can find our email address in the contact section above.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
Consumer Dispute Resolution / Universal Arbitration Board
|
||||
</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
We are not willing or obliged to participate in dispute resolution proceedings before a
|
||||
consumer arbitration board.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Liability for Content</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
As a service provider, we are responsible for our own content on these pages in accordance with § 7 para. 1 TMG
|
||||
under general law. However, according to §§ 8 to 10 TMG, as a service provider we are not obligated to
|
||||
monitor transmitted or stored third-party information or to investigate circumstances that indicate illegal activity.
|
||||
</p>
|
||||
<p className="text-gray-700 mb-4">
|
||||
Obligations to remove or block the use of information according to general laws remain unaffected. However,
|
||||
liability in this regard is only possible from the time of knowledge of a specific legal violation. Upon
|
||||
becoming aware of corresponding legal violations, we will remove this content immediately.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Liability for Links</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
Our website contains links to external third-party websites over whose content we have no influence.
|
||||
Therefore, we cannot assume any liability for this external content. The respective provider or operator
|
||||
of the pages is always responsible for the content of the linked pages. The linked pages were checked for
|
||||
possible legal violations at the time of linking. Illegal content was not recognizable at the time of linking.
|
||||
</p>
|
||||
<p className="text-gray-700 mb-4">
|
||||
However, permanent monitoring of the content of linked pages is not reasonable without concrete evidence
|
||||
of a legal violation. Upon becoming aware of legal violations, we will remove such links immediately.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Copyright</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
The content and works created by the site operators on these pages are subject to German copyright law.
|
||||
Duplication, processing, distribution, and any kind of exploitation outside the limits of copyright law
|
||||
require the written consent of the respective author or creator. Downloads and copies of this site are
|
||||
only permitted for private, non-commercial use.
|
||||
</p>
|
||||
<p className="text-gray-700 mb-4">
|
||||
Insofar as the content on this site was not created by the operator, the copyrights of third parties
|
||||
are respected. In particular, third-party content is identified as such. Should you nevertheless become
|
||||
aware of a copyright infringement, please inform us accordingly. Upon becoming aware of legal violations,
|
||||
we will remove such content immediately.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Image Credits</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
Images and graphics used on this website are from:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li>Our own creation</li>
|
||||
<li>License-free image sources (e.g., Unsplash, Pexels)</li>
|
||||
<li>Licensed stock photo services</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 pt-8 border-t border-gray-200">
|
||||
<p className="text-gray-600 text-center">
|
||||
<Link href="/privacy" className="text-primary-600 hover:text-primary-700 mr-4">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link href="/terms" className="text-primary-600 hover:text-primary-700 mr-4">
|
||||
Terms of Service
|
||||
</Link>
|
||||
<Link href="/" className="text-primary-600 hover:text-primary-700">
|
||||
Back to Home
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import en from '@/i18n/en.json';
|
||||
|
||||
|
|
@ -11,7 +10,6 @@ export default function MarketingLayout({
|
|||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
// Always use English for marketing pages
|
||||
|
|
@ -142,10 +140,9 @@ export default function MarketingLayout({
|
|||
<h3 className="font-semibold mb-4">Legal</h3>
|
||||
<ul className="space-y-2 text-gray-400">
|
||||
<li><Link href="/privacy" className="hover:text-white">Privacy Policy</Link></li>
|
||||
<li><Link href="/terms" className="hover:text-white">Terms of Service</Link></li>
|
||||
<li><Link href="/impressum" className="hover:text-white">Legal Notice</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
|
||||
|
|
|
|||
|
|
@ -22,16 +22,16 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.com/',
|
||||
canonical: 'https://www.qrmaster.net/',
|
||||
languages: {
|
||||
'x-default': 'https://www.qrmaster.com/',
|
||||
en: 'https://www.qrmaster.com/',
|
||||
'x-default': 'https://www.qrmaster.net/',
|
||||
en: 'https://www.qrmaster.net/',
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: 'https://www.qrmaster.com/',
|
||||
url: 'https://www.qrmaster.net/',
|
||||
type: 'website',
|
||||
},
|
||||
twitter: {
|
||||
|
|
|
|||
|
|
@ -24,199 +24,104 @@ export default function PrivacyPage() {
|
|||
<h2 className="text-2xl font-bold text-gray-900 mb-4">1. Introduction</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
Welcome to QR Master ("we," "our," or "us"). We respect your privacy and are committed to protecting your personal data.
|
||||
This privacy policy will inform you about how we look after your personal data when you visit our website and use our services,
|
||||
and tell you about your privacy rights and how the law protects you.
|
||||
This privacy policy explains how we collect, use, and protect your information when you use our services.
|
||||
</p>
|
||||
<p className="text-gray-700 mb-4">
|
||||
We implement appropriate security measures including secure HTTPS transmission, password hashing, database access controls,
|
||||
and CSRF protection to keep your data safe.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">2. Information We Collect</h2>
|
||||
<p className="text-gray-700 mb-4">We collect and process the following data about you:</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">2.1 Information You Provide</h3>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">Information You Provide</h3>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li><strong>Account Information:</strong> Name, email address, and password when you create an account</li>
|
||||
<li><strong>Payment Information:</strong> Payment details processed securely through Stripe (we do not store credit card information)</li>
|
||||
<li><strong>QR Code Data:</strong> Content, URLs, and customization settings for QR codes you create</li>
|
||||
<li><strong>Profile Information:</strong> Any additional information you choose to provide</li>
|
||||
<li><strong>Account Information:</strong> Name, email address, and password</li>
|
||||
<li><strong>Payment Information:</strong> Processed securely through Stripe (we do not store credit card information)</li>
|
||||
<li><strong>QR Code Content:</strong> URLs, text, and customization settings for your QR codes</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">2.2 Information We Collect Automatically</h3>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">Information Collected Automatically</h3>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li><strong>Usage Data:</strong> QR code scans, analytics data, and interaction with our services</li>
|
||||
<li><strong>Technical Data:</strong> IP address, browser type, device information, and operating system</li>
|
||||
<li><strong>Analytics Data:</strong> Website usage analytics collected via PostHog (only with your consent)</li>
|
||||
<li><strong>Cookies:</strong> We use cookies to improve your experience (see our Cookie Policy below)</li>
|
||||
<li><strong>Usage Data:</strong> QR code scans and analytics</li>
|
||||
<li><strong>Technical Data:</strong> IP address, browser type, and device information</li>
|
||||
<li><strong>Cookies:</strong> Essential cookies for authentication and optional analytics cookies (PostHog) with your consent</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">3. How We Use Your Information</h2>
|
||||
<p className="text-gray-700 mb-4">We use your personal data for the following purposes:</p>
|
||||
<p className="text-gray-700 mb-4">We use your data to:</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li>To provide and maintain our QR code generation and analytics services</li>
|
||||
<li>To process your payments and manage your subscription</li>
|
||||
<li>To provide customer support and respond to your inquiries</li>
|
||||
<li>To improve our services and develop new features</li>
|
||||
<li>To detect and prevent fraud and abuse</li>
|
||||
<li>To comply with legal obligations</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">4. Legal Basis for Processing (GDPR)</h2>
|
||||
<p className="text-gray-700 mb-4">We process your personal data under the following legal bases:</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li><strong>Contract Performance:</strong> Processing necessary to provide our services to you</li>
|
||||
<li><strong>Consent:</strong> Where you have given clear consent for specific purposes</li>
|
||||
<li><strong>Legitimate Interests:</strong> For improving our services, security, and fraud prevention</li>
|
||||
<li><strong>Legal Obligation:</strong> To comply with applicable laws and regulations</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">5. Data Sharing and Third Parties</h2>
|
||||
<p className="text-gray-700 mb-4">We may share your data with the following third parties:</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li><strong>Stripe:</strong> Payment processing (subject to Stripe's privacy policy)</li>
|
||||
<li><strong>PostHog:</strong> Website analytics platform for tracking user behavior and improving our services (only with your consent, subject to PostHog's privacy policy)</li>
|
||||
<li><strong>Cloud Hosting:</strong> Vercel and database providers for hosting our services</li>
|
||||
<li><strong>Service Providers:</strong> Companies that help us provide our services (under strict confidentiality agreements)</li>
|
||||
<li><strong>Legal Requirements:</strong> When required by law or to protect our rights</li>
|
||||
<li>Provide and maintain our QR code services</li>
|
||||
<li>Process payments and manage subscriptions</li>
|
||||
<li>Provide customer support</li>
|
||||
<li>Improve our services and develop new features</li>
|
||||
<li>Detect and prevent fraud</li>
|
||||
</ul>
|
||||
<p className="text-gray-700 mb-4">
|
||||
We do not sell your personal data to third parties. PostHog analytics are only activated if you accept analytics cookies,
|
||||
and we use privacy-friendly settings including respecting Do Not Track (DNT) headers.
|
||||
We retain your data while your account is active. Upon account deletion, most data is removed immediately,
|
||||
though some may be retained for legal compliance. Aggregated, anonymized analytics may be kept indefinitely.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">6. Data Security</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
We implement appropriate technical and organizational measures to protect your personal data, including:
|
||||
</p>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">4. Data Sharing</h2>
|
||||
<p className="text-gray-700 mb-4">We may share your data with:</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li>Secure HTTPS transmission for data in transit</li>
|
||||
<li>Secure password hashing using bcrypt</li>
|
||||
<li>Database security and access controls</li>
|
||||
<li>Cookie-based authentication with HttpOnly flags</li>
|
||||
<li>CSRF protection for sensitive operations</li>
|
||||
<li>Rate limiting to prevent abuse</li>
|
||||
<li><strong>Stripe:</strong> Payment processing</li>
|
||||
<li><strong>PostHog:</strong> Analytics (only with your consent, respects Do Not Track)</li>
|
||||
<li><strong>Vercel:</strong> Cloud hosting provider</li>
|
||||
<li><strong>Legal Requirements:</strong> When required by law</li>
|
||||
</ul>
|
||||
<p className="text-gray-700 mb-4">
|
||||
We do not sell your personal data. Analytics are only activated if you accept optional cookies.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">7. Data Retention</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
We retain your personal data only for as long as necessary to fulfill the purposes outlined in this policy:
|
||||
</p>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">5. Your Rights (GDPR)</h2>
|
||||
<p className="text-gray-700 mb-4">You have the right to:</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li><strong>Active Accounts:</strong> Data retained while your account is active</li>
|
||||
<li><strong>Deleted Accounts:</strong> Most data deleted immediately upon account deletion</li>
|
||||
<li><strong>Legal Requirements:</strong> Some data may be retained to comply with legal obligations</li>
|
||||
<li><strong>Analytics Data:</strong> Aggregated, anonymized data may be retained indefinitely</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">8. Your Rights (GDPR)</h2>
|
||||
<p className="text-gray-700 mb-4">Under GDPR, you have the following rights:</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li><strong>Right to Access:</strong> Request a copy of your personal data</li>
|
||||
<li><strong>Right to Rectification:</strong> Correct inaccurate or incomplete data (edit name in settings)</li>
|
||||
<li><strong>Right to Erasure:</strong> Request deletion of your data (account deletion available in settings)</li>
|
||||
<li><strong>Right to Restriction:</strong> Request limitation of processing</li>
|
||||
<li><strong>Right to Data Portability:</strong> Receive your data in a portable format (available upon request)</li>
|
||||
<li><strong>Right to Object:</strong> Object to processing based on legitimate interests</li>
|
||||
<li><strong>Right to Withdraw Consent:</strong> Withdraw consent at any time</li>
|
||||
<li><strong>Access:</strong> Request a copy of your personal data</li>
|
||||
<li><strong>Rectification:</strong> Correct inaccurate data (update in account settings)</li>
|
||||
<li><strong>Erasure:</strong> Delete your data (account deletion available in settings)</li>
|
||||
<li><strong>Data Portability:</strong> Receive your data in a portable format</li>
|
||||
<li><strong>Object:</strong> Object to processing based on legitimate interests</li>
|
||||
<li><strong>Withdraw Consent:</strong> Withdraw cookie consent at any time</li>
|
||||
</ul>
|
||||
<p className="text-gray-700 mb-4">
|
||||
To exercise these rights, please contact us at{' '}
|
||||
<a href="mailto:privacy@qrmaster.com" className="text-primary-600 hover:text-primary-700">
|
||||
privacy@qrmaster.com
|
||||
To exercise these rights, contact us at{' '}
|
||||
<a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700">
|
||||
support@qrmaster.net
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">9. Cookies</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
We use cookies to improve your experience on our website. Cookies are small text files stored on your device.
|
||||
</p>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">Types of Cookies We Use:</h3>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li><strong>Essential Cookies:</strong> Required for authentication and basic functionality (userId, CSRF token)</li>
|
||||
<li><strong>Preference Cookies:</strong> Remember your settings and cookie consent preferences (cookieConsent)</li>
|
||||
<li><strong>Analytics Cookies:</strong> PostHog analytics cookies to track page views, user behavior, and improve our services (only with your consent)</li>
|
||||
</ul>
|
||||
<p className="text-gray-700 mb-4">
|
||||
You can control cookies through your browser settings and our cookie banner. Analytics cookies are only set if you accept them
|
||||
through our cookie banner. Essential cookies are required for the website to function and cannot be disabled.
|
||||
</p>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">PostHog Analytics:</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
PostHog is our analytics platform that helps us understand how users interact with our website. When you accept analytics cookies:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li>PostHog tracks page views, clicks, and user journeys</li>
|
||||
<li>We collect device type, browser, operating system, and referral source</li>
|
||||
<li>PostHog respects Do Not Track (DNT) browser settings</li>
|
||||
<li>No personally identifiable information (PII) is sent to PostHog without explicit identification</li>
|
||||
<li>Data is processed in accordance with PostHog's privacy policy</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">10. International Data Transfers</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
Your data may be transferred to and processed in countries outside the European Economic Area (EEA).
|
||||
We ensure appropriate safeguards are in place, including Standard Contractual Clauses (SCCs) and
|
||||
adequacy decisions by the European Commission.
|
||||
Our service is for users 16 years and older. If you're in the EEA and have concerns,
|
||||
you may lodge a complaint with your local data protection authority.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">11. Children's Privacy</h2>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">6. Contact Us</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
Our services are not intended for children under 16 years of age. We do not knowingly collect personal data
|
||||
from children. If you believe we have collected data from a child, please contact us immediately.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">12. Changes to This Policy</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
We may update this privacy policy from time to time. We will notify you of significant changes through
|
||||
a prominent notice on our website. Continued use of our services after changes constitutes
|
||||
acceptance of the updated policy.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">13. Contact Us</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
If you have any questions about this privacy policy or our data practices, please contact us:
|
||||
If you have questions about this privacy policy, please contact us:
|
||||
</p>
|
||||
<div className="bg-gray-50 p-6 rounded-lg">
|
||||
<p className="text-gray-700 mb-2"><strong>Email:</strong> privacy@qrmaster.com</p>
|
||||
<p className="text-gray-700 mb-2"><strong>Website:</strong> <a href="/" className="text-primary-600 hover:text-primary-700">qrmaster.com</a></p>
|
||||
<p className="text-gray-700 mb-2">
|
||||
<strong>Email:</strong>{' '}
|
||||
<a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700">
|
||||
support@qrmaster.net
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-gray-700 mb-2"><strong>Website:</strong> <a href="/" className="text-primary-600 hover:text-primary-700">qrmaster.net</a></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">14. Supervisory Authority</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
If you are located in the EEA and believe we have not addressed your concerns adequately,
|
||||
you have the right to lodge a complaint with your local data protection supervisory authority.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 pt-8 border-t border-gray-200">
|
||||
<p className="text-gray-600 text-center">
|
||||
<Link href="/terms" className="text-primary-600 hover:text-primary-700 mr-4">
|
||||
Terms of Service
|
||||
</Link>
|
||||
<Link href="/" className="text-primary-600 hover:text-primary-700">
|
||||
Back to Home
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -12,16 +12,16 @@ export const metadata: Metadata = {
|
|||
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior. Free QR code tracking software with detailed reports.',
|
||||
keywords: 'qr code tracking, qr code analytics, track qr scans, qr code statistics, free qr tracking, qr code monitoring',
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.com/qr-code-tracking',
|
||||
canonical: 'https://www.qrmaster.net/qr-code-tracking',
|
||||
languages: {
|
||||
'x-default': 'https://www.qrmaster.com/qr-code-tracking',
|
||||
en: 'https://www.qrmaster.com/qr-code-tracking',
|
||||
'x-default': 'https://www.qrmaster.net/qr-code-tracking',
|
||||
en: 'https://www.qrmaster.net/qr-code-tracking',
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master',
|
||||
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.',
|
||||
url: 'https://www.qrmaster.com/qr-code-tracking',
|
||||
url: 'https://www.qrmaster.net/qr-code-tracking',
|
||||
type: 'website',
|
||||
},
|
||||
twitter: {
|
||||
|
|
@ -100,7 +100,7 @@ export default function QRCodeTrackingPage() {
|
|||
const softwareSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
'@id': 'https://www.qrmaster.com/qr-code-tracking#software',
|
||||
'@id': 'https://www.qrmaster.net/qr-code-tracking#software',
|
||||
name: 'QR Master - QR Code Tracking & Analytics',
|
||||
applicationCategory: 'BusinessApplication',
|
||||
operatingSystem: 'Web Browser, iOS, Android',
|
||||
|
|
@ -131,7 +131,7 @@ export default function QRCodeTrackingPage() {
|
|||
const howToSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'HowTo',
|
||||
'@id': 'https://www.qrmaster.com/qr-code-tracking#howto',
|
||||
'@id': 'https://www.qrmaster.net/qr-code-tracking#howto',
|
||||
name: 'How to Track QR Code Scans',
|
||||
description: 'Learn how to track and analyze QR code scans with real-time analytics',
|
||||
totalTime: 'PT5M',
|
||||
|
|
@ -141,7 +141,7 @@ export default function QRCodeTrackingPage() {
|
|||
position: 1,
|
||||
name: 'Create QR Code',
|
||||
text: 'Sign up for free and create a dynamic QR code with tracking enabled',
|
||||
url: 'https://www.qrmaster.com/signup',
|
||||
url: 'https://www.qrmaster.net/signup',
|
||||
},
|
||||
{
|
||||
'@type': 'HowToStep',
|
||||
|
|
@ -154,7 +154,7 @@ export default function QRCodeTrackingPage() {
|
|||
position: 3,
|
||||
name: 'Monitor Analytics',
|
||||
text: 'View real-time scan data including location, device, and time patterns in your dashboard',
|
||||
url: 'https://www.qrmaster.com/analytics',
|
||||
url: 'https://www.qrmaster.net/analytics',
|
||||
},
|
||||
{
|
||||
'@type': 'HowToStep',
|
||||
|
|
|
|||
|
|
@ -1,304 +0,0 @@
|
|||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Terms of Service | QR Master',
|
||||
description: 'Terms of Service and usage conditions for QR Master',
|
||||
};
|
||||
|
||||
export default function TermsPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-white py-12">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl">
|
||||
<div className="mb-8">
|
||||
<Link href="/" className="text-primary-600 hover:text-primary-700 font-medium">
|
||||
← Back to Home
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">Terms of Service</h1>
|
||||
<p className="text-gray-600 mb-8">Last updated: January 2025</p>
|
||||
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">1. Agreement to Terms</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
By accessing or using QR Master ("Service"), you agree to be bound by these Terms of Service ("Terms").
|
||||
If you disagree with any part of these terms, you may not access the Service.
|
||||
</p>
|
||||
<p className="text-gray-700 mb-4">
|
||||
These Terms apply to all visitors, users, and others who access or use the Service.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">2. Description of Service</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
QR Master is a SaaS platform that provides:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li>QR code generation (static and dynamic)</li>
|
||||
<li>QR code customization and branding</li>
|
||||
<li>Analytics and tracking for dynamic QR codes</li>
|
||||
<li>Bulk QR code creation</li>
|
||||
<li>QR code management dashboard</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">3. Account Registration</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">3.1 Account Creation</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
To use certain features, you must create an account. You agree to:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li>Provide accurate, current, and complete information</li>
|
||||
<li>Maintain and update your information to keep it accurate</li>
|
||||
<li>Maintain the security of your password</li>
|
||||
<li>Accept responsibility for all activities under your account</li>
|
||||
<li>Notify us immediately of any unauthorized use</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">3.2 Account Eligibility</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
You must be at least 16 years old to use this Service. By creating an account, you represent that you meet this requirement.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">3.3 Account Termination</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
We reserve the right to suspend or terminate your account at any time for violations of these Terms,
|
||||
fraudulent activity, or other reasons at our sole discretion.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">4. Subscription Plans and Payments</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">4.1 Plans</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
We offer multiple subscription tiers:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li><strong>Free Plan:</strong> Limited features with usage restrictions</li>
|
||||
<li><strong>Pro Plan:</strong> Enhanced features and higher limits</li>
|
||||
<li><strong>Business Plan:</strong> Full features with maximum limits</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">4.2 Billing</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
Paid subscriptions are billed monthly in advance. By subscribing, you agree to:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li>Pay all fees associated with your chosen plan</li>
|
||||
<li>Provide current, complete, and accurate billing information</li>
|
||||
<li>Update payment information promptly to avoid service interruption</li>
|
||||
<li>Accept that fees are non-refundable except as required by law</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">4.3 Payment Processing</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
All payments are processed securely through Stripe. We do not store your credit card information.
|
||||
Your payment information is subject to Stripe's Terms of Service and Privacy Policy.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">4.4 Automatic Renewal</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
Subscriptions automatically renew at the end of each billing period unless cancelled.
|
||||
You may cancel your subscription at any time through your account settings or the Stripe Customer Portal.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">4.5 Cancellation and Refunds</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
You may cancel your subscription at any time. Cancellation takes effect at the end of the current billing period.
|
||||
No refunds will be provided for partial months or unused services, except as required by applicable law.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">4.6 Price Changes</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
We reserve the right to change our pricing. We will provide at least 30 days' notice before any price changes take effect.
|
||||
Continued use of the Service after a price change constitutes acceptance of the new pricing.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">5. Acceptable Use</h2>
|
||||
|
||||
<p className="text-gray-700 mb-4">You agree NOT to use the Service to:</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li>Violate any laws or regulations</li>
|
||||
<li>Infringe on intellectual property rights</li>
|
||||
<li>Transmit malware, viruses, or harmful code</li>
|
||||
<li>Engage in phishing, spam, or fraudulent activities</li>
|
||||
<li>Create QR codes linking to illegal, harmful, or malicious content</li>
|
||||
<li>Harass, abuse, or harm others</li>
|
||||
<li>Impersonate any person or entity</li>
|
||||
<li>Interfere with or disrupt the Service</li>
|
||||
<li>Attempt to gain unauthorized access to our systems</li>
|
||||
<li>Use automated tools to access the Service without permission</li>
|
||||
<li>Resell or redistribute the Service without authorization</li>
|
||||
</ul>
|
||||
|
||||
<p className="text-gray-700 mb-4">
|
||||
We reserve the right to investigate and take appropriate legal action against anyone who violates these provisions.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">6. Content and Intellectual Property</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">6.1 Your Content</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
You retain all rights to the content you create using our Service (URLs, text, images in QR codes, etc.).
|
||||
By using our Service, you grant us a limited license to store, process, and display your content solely
|
||||
for the purpose of providing the Service to you.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">6.2 Our Content</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
The Service and its original content (excluding user-generated content), features, and functionality
|
||||
are owned by QR Master and are protected by international copyright, trademark, and other intellectual property laws.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">6.3 Generated QR Codes</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
QR codes you generate using our Service are yours to use as you wish. However, you are responsible for ensuring
|
||||
that the content encoded in QR codes complies with these Terms and applicable laws.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">7. Service Availability and Modifications</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">7.1 Service Availability</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
We strive to provide reliable service but do not guarantee uninterrupted access. The Service may be temporarily
|
||||
unavailable due to maintenance, updates, or circumstances beyond our control.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">7.2 Modifications</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
We reserve the right to modify, suspend, or discontinue any part of the Service at any time with or without notice.
|
||||
We will not be liable to you or any third party for any modification, suspension, or discontinuation of the Service.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">8. Data and Privacy</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
Your use of the Service is also governed by our Privacy Policy. Please review our{' '}
|
||||
<Link href="/privacy" className="text-primary-600 hover:text-primary-700">Privacy Policy</Link>
|
||||
{' '}to understand how we collect, use, and protect your data.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">9. Disclaimers and Limitations of Liability</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">9.1 Disclaimer of Warranties</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">9.2 Limitation of Liability</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
TO THE MAXIMUM EXTENT PERMITTED BY LAW, QR MASTER SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL,
|
||||
CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS OR REVENUES, WHETHER INCURRED DIRECTLY OR INDIRECTLY,
|
||||
OR ANY LOSS OF DATA, USE, GOODWILL, OR OTHER INTANGIBLE LOSSES.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">9.3 Maximum Liability</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
Our total liability to you for all claims arising from your use of the Service shall not exceed the amount
|
||||
you paid us in the 12 months preceding the claim.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">10. Indemnification</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
You agree to indemnify and hold harmless QR Master and its officers, directors, employees, and agents from any
|
||||
claims, damages, losses, liabilities, and expenses (including legal fees) arising from:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li>Your use of the Service</li>
|
||||
<li>Your violation of these Terms</li>
|
||||
<li>Your violation of any rights of another party</li>
|
||||
<li>Content you create or share using the Service</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">11. Governing Law and Dispute Resolution</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">11.1 Governing Law</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
These Terms shall be governed by and construed in accordance with the laws of the jurisdiction in which
|
||||
QR Master operates, without regard to conflict of law provisions.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">11.2 Dispute Resolution</h3>
|
||||
<p className="text-gray-700 mb-4">
|
||||
Any disputes arising from these Terms or your use of the Service shall first be attempted to be resolved
|
||||
through good-faith negotiation. If unresolved, disputes may be brought in the courts of competent jurisdiction.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">12. Changes to Terms</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
We reserve the right to modify these Terms at any time. We will provide notice of significant changes by:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||
<li>Posting the updated Terms with a new "Last Updated" date</li>
|
||||
<li>Displaying a prominent notice on our website</li>
|
||||
</ul>
|
||||
<p className="text-gray-700 mb-4">
|
||||
Your continued use of the Service after changes take effect constitutes acceptance of the revised Terms.
|
||||
If you do not agree to the new Terms, you must stop using the Service and may cancel your account.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">13. Severability</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
If any provision of these Terms is found to be unenforceable or invalid, that provision will be limited
|
||||
or eliminated to the minimum extent necessary, and the remaining provisions will remain in full force and effect.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">14. Entire Agreement</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
These Terms, together with our Privacy Policy, constitute the entire agreement between you and QR Master
|
||||
regarding the Service and supersede all prior agreements and understandings.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">15. Contact Information</h2>
|
||||
<p className="text-gray-700 mb-4">
|
||||
If you have questions about these Terms, please contact us:
|
||||
</p>
|
||||
<div className="bg-gray-50 p-6 rounded-lg">
|
||||
<p className="text-gray-700 mb-2"><strong>Email:</strong> legal@qrmaster.com</p>
|
||||
<p className="text-gray-700 mb-2"><strong>Website:</strong> <a href="/" className="text-primary-600 hover:text-primary-700">qrmaster.com</a></p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 pt-8 border-t border-gray-200">
|
||||
<p className="text-gray-600 text-center">
|
||||
<Link href="/privacy" className="text-primary-600 hover:text-primary-700 mr-4">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link href="/" className="text-primary-600 hover:text-primary-700">
|
||||
Back to Home
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,6 +5,20 @@ import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
|||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// Helper function to calculate trend
|
||||
function calculateTrend(current: number, previous: number): { trend: 'up' | 'down' | 'flat'; percentage: number } {
|
||||
if (previous === 0) {
|
||||
return current > 0 ? { trend: 'up', percentage: 100 } : { trend: 'flat', percentage: 0 };
|
||||
}
|
||||
|
||||
const change = ((current - previous) / previous) * 100;
|
||||
const percentage = Math.round(Math.abs(change));
|
||||
|
||||
if (change > 5) return { trend: 'up', percentage };
|
||||
if (change < -5) return { trend: 'down', percentage };
|
||||
return { trend: 'flat', percentage };
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
|
@ -33,20 +47,61 @@ export async function GET(request: NextRequest) {
|
|||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get user's QR codes
|
||||
// Get date range from query params (default: last 30 days)
|
||||
const { searchParams } = request.nextUrl;
|
||||
const range = searchParams.get('range') || '30';
|
||||
const daysInRange = parseInt(range, 10);
|
||||
|
||||
// Calculate current and previous period dates
|
||||
const now = new Date();
|
||||
const currentPeriodStart = new Date();
|
||||
currentPeriodStart.setDate(now.getDate() - daysInRange);
|
||||
|
||||
const previousPeriodEnd = new Date(currentPeriodStart);
|
||||
const previousPeriodStart = new Date(previousPeriodEnd);
|
||||
previousPeriodStart.setDate(previousPeriodEnd.getDate() - daysInRange);
|
||||
|
||||
// Get user's QR codes with scans filtered by period
|
||||
const qrCodes = await db.qRCode.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
scans: true,
|
||||
scans: {
|
||||
where: {
|
||||
ts: {
|
||||
gte: currentPeriodStart,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Calculate stats
|
||||
// Get previous period scans for comparison
|
||||
const qrCodesWithPreviousScans = await db.qRCode.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
scans: {
|
||||
where: {
|
||||
ts: {
|
||||
gte: previousPeriodStart,
|
||||
lt: previousPeriodEnd,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Calculate current period stats
|
||||
const totalScans = qrCodes.reduce((sum, qr) => sum + qr.scans.length, 0);
|
||||
const uniqueScans = qrCodes.reduce((sum, qr) =>
|
||||
sum + qr.scans.filter(s => s.isUnique).length, 0
|
||||
);
|
||||
|
||||
// Calculate previous period stats for comparison
|
||||
const previousTotalScans = qrCodesWithPreviousScans.reduce((sum, qr) => sum + qr.scans.length, 0);
|
||||
const previousUniqueScans = qrCodesWithPreviousScans.reduce((sum, qr) =>
|
||||
sum + qr.scans.filter(s => s.isUnique).length, 0
|
||||
);
|
||||
|
||||
// Device stats
|
||||
const deviceStats = qrCodes.flatMap(qr => qr.scans)
|
||||
.reduce((acc, scan) => {
|
||||
|
|
@ -60,7 +115,7 @@ export async function GET(request: NextRequest) {
|
|||
? Math.round((mobileScans / totalScans) * 100)
|
||||
: 0;
|
||||
|
||||
// Country stats
|
||||
// Country stats (current period)
|
||||
const countryStats = qrCodes.flatMap(qr => qr.scans)
|
||||
.reduce((acc, scan) => {
|
||||
const country = scan.country || 'Unknown';
|
||||
|
|
@ -68,18 +123,19 @@ export async function GET(request: NextRequest) {
|
|||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
// Country stats (previous period)
|
||||
const previousCountryStats = qrCodesWithPreviousScans.flatMap(qr => qr.scans)
|
||||
.reduce((acc, scan) => {
|
||||
const country = scan.country || 'Unknown';
|
||||
acc[country] = (acc[country] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
const topCountry = Object.entries(countryStats)
|
||||
.sort(([,a], [,b]) => b - a)[0];
|
||||
|
||||
// Time-based stats (last 30 days)
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
const recentScans = qrCodes.flatMap(qr => qr.scans)
|
||||
.filter(scan => new Date(scan.ts) > thirtyDaysAgo);
|
||||
|
||||
// Daily scan counts for chart
|
||||
const dailyScans = recentScans.reduce((acc, scan) => {
|
||||
// Daily scan counts for chart (current period)
|
||||
const dailyScans = qrCodes.flatMap(qr => qr.scans).reduce((acc, scan) => {
|
||||
const date = new Date(scan.ts).toISOString().split('T')[0];
|
||||
acc[date] = (acc[date] || 0) + 1;
|
||||
return acc;
|
||||
|
|
@ -88,16 +144,30 @@ export async function GET(request: NextRequest) {
|
|||
// QR performance (only show DYNAMIC QR codes since STATIC don't track scans)
|
||||
const qrPerformance = qrCodes
|
||||
.filter(qr => qr.type === 'DYNAMIC')
|
||||
.map(qr => ({
|
||||
id: qr.id,
|
||||
title: qr.title,
|
||||
type: qr.type,
|
||||
totalScans: qr.scans.length,
|
||||
uniqueScans: qr.scans.filter(s => s.isUnique).length,
|
||||
conversion: qr.scans.length > 0
|
||||
? Math.round((qr.scans.filter(s => s.isUnique).length / qr.scans.length) * 100)
|
||||
: 0,
|
||||
}))
|
||||
.map(qr => {
|
||||
const currentTotal = qr.scans.length;
|
||||
const currentUnique = qr.scans.filter(s => s.isUnique).length;
|
||||
|
||||
// Find previous period data for this QR code
|
||||
const previousQR = qrCodesWithPreviousScans.find(prev => prev.id === qr.id);
|
||||
const previousTotal = previousQR ? previousQR.scans.length : 0;
|
||||
|
||||
// Calculate trend
|
||||
const trendData = calculateTrend(currentTotal, previousTotal);
|
||||
|
||||
return {
|
||||
id: qr.id,
|
||||
title: qr.title,
|
||||
type: qr.type,
|
||||
totalScans: currentTotal,
|
||||
uniqueScans: currentUnique,
|
||||
conversion: currentTotal > 0
|
||||
? Math.round((currentUnique / currentTotal) * 100)
|
||||
: 0,
|
||||
trend: trendData.trend,
|
||||
trendPercentage: trendData.percentage,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.totalScans - a.totalScans);
|
||||
|
||||
return NextResponse.json({
|
||||
|
|
@ -117,13 +187,20 @@ export async function GET(request: NextRequest) {
|
|||
countryStats: Object.entries(countryStats)
|
||||
.sort(([,a], [,b]) => b - a)
|
||||
.slice(0, 10)
|
||||
.map(([country, count]) => ({
|
||||
country,
|
||||
count,
|
||||
percentage: totalScans > 0
|
||||
? Math.round((count / totalScans) * 100)
|
||||
: 0,
|
||||
})),
|
||||
.map(([country, count]) => {
|
||||
const previousCount = previousCountryStats[country] || 0;
|
||||
const trendData = calculateTrend(count, previousCount);
|
||||
|
||||
return {
|
||||
country,
|
||||
count,
|
||||
percentage: totalScans > 0
|
||||
? Math.round((count / totalScans) * 100)
|
||||
: 0,
|
||||
trend: trendData.trend,
|
||||
trendPercentage: trendData.percentage,
|
||||
};
|
||||
}),
|
||||
dailyScans,
|
||||
qrPerformance: qrPerformance.slice(0, 10),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ const updateQRSchema = z.object({
|
|||
content: z.any().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
style: z.any().optional(),
|
||||
status: z.enum(['ACTIVE', 'PAUSED']).optional(),
|
||||
});
|
||||
|
||||
// GET /api/qrs/[id] - Get a single QR code
|
||||
|
|
@ -120,7 +119,6 @@ export async function PATCH(
|
|||
...(data.content && { content: data.content }),
|
||||
...(data.tags && { tags: data.tags }),
|
||||
...(data.style && { style: data.style }),
|
||||
...(data.status && { status: data.status }),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
import { STRIPE_PLANS } from '@/lib/stripe';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
|
@ -28,8 +29,21 @@ export async function GET(request: NextRequest) {
|
|||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Determine billing interval from stripePriceId
|
||||
let interval: 'month' | 'year' | null = null;
|
||||
|
||||
if (user.stripePriceId) {
|
||||
// Check if the current price ID matches any yearly price ID
|
||||
const isYearly =
|
||||
user.stripePriceId === STRIPE_PLANS.PRO.priceIdYearly ||
|
||||
user.stripePriceId === STRIPE_PLANS.BUSINESS.priceIdYearly;
|
||||
|
||||
interval = isYearly ? 'year' : 'month';
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
plan: user.plan || 'FREE',
|
||||
interval,
|
||||
currentPeriodEnd: user.stripeCurrentPeriodEnd,
|
||||
priceId: user.stripePriceId,
|
||||
stripeCustomerId: user.stripeCustomerId,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import CookieBanner from '@/components/CookieBanner';
|
|||
const isIndexable = process.env.NEXT_PUBLIC_INDEXABLE === 'true';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL('https://www.qrmaster.com'),
|
||||
metadataBase: new URL('https://www.qrmaster.net'),
|
||||
title: {
|
||||
default: 'QR Master – Smart QR Generator & Analytics',
|
||||
template: '%s | QR Master',
|
||||
|
|
@ -28,17 +28,17 @@ export const metadata: Metadata = {
|
|||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
site: '@qrmaster',
|
||||
images: ['https://www.qrmaster.com/static/og-image.png'],
|
||||
images: ['https://www.qrmaster.net/static/og-image.png'],
|
||||
},
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
siteName: 'QR Master',
|
||||
title: 'QR Master – Smart QR Generator & Analytics',
|
||||
description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics.',
|
||||
url: 'https://www.qrmaster.com',
|
||||
url: 'https://www.qrmaster.net',
|
||||
images: [
|
||||
{
|
||||
url: 'https://www.qrmaster.com/static/og-image.png',
|
||||
url: 'https://www.qrmaster.net/static/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'QR Master - Dynamic QR Code Generator and Analytics Platform',
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ export async function GET(
|
|||
where: { slug },
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
content: true,
|
||||
contentType: true,
|
||||
},
|
||||
|
|
@ -24,10 +23,6 @@ export async function GET(
|
|||
return new NextResponse('QR Code not found', { status: 404 });
|
||||
}
|
||||
|
||||
if (qrCode.status === 'PAUSED') {
|
||||
return new NextResponse('QR Code is paused', { status: 404 });
|
||||
}
|
||||
|
||||
// Track scan (fire and forget)
|
||||
trackScan(qrCode.id, request).catch(console.error);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { MetadataRoute } from 'next';
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
const baseUrl = 'https://www.qrmaster.com';
|
||||
const baseUrl = 'https://www.qrmaster.net';
|
||||
|
||||
return {
|
||||
rules: [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { MetadataRoute } from 'next';
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const baseUrl = 'https://www.qrmaster.com';
|
||||
const baseUrl = 'https://www.qrmaster.net';
|
||||
|
||||
return [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,20 +15,17 @@ interface QRCodeCardProps {
|
|||
contentType: string;
|
||||
content?: any;
|
||||
slug: string;
|
||||
status: 'ACTIVE' | 'PAUSED';
|
||||
createdAt: string;
|
||||
scans?: number;
|
||||
style?: any;
|
||||
};
|
||||
onEdit: (id: string) => void;
|
||||
onPause: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
export const QRCodeCard: React.FC<QRCodeCardProps> = ({
|
||||
qr,
|
||||
onEdit,
|
||||
onPause,
|
||||
onDelete,
|
||||
}) => {
|
||||
// For dynamic QR codes, use the redirect URL for tracking
|
||||
|
|
@ -172,9 +169,6 @@ END:VCARD`;
|
|||
<Badge variant={qr.type === 'DYNAMIC' ? 'info' : 'default'}>
|
||||
{qr.type}
|
||||
</Badge>
|
||||
<Badge variant={qr.status === 'ACTIVE' ? 'success' : 'warning'}>
|
||||
{qr.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -193,11 +187,6 @@ END:VCARD`;
|
|||
{qr.type === 'DYNAMIC' && (
|
||||
<DropdownItem onClick={() => onEdit(qr.id)}>Edit</DropdownItem>
|
||||
)}
|
||||
{qr.type === 'DYNAMIC' && (
|
||||
<DropdownItem onClick={() => onPause(qr.id)}>
|
||||
{qr.status === 'ACTIVE' ? 'Pause' : 'Resume'}
|
||||
</DropdownItem>
|
||||
)}
|
||||
<DropdownItem onClick={() => onDelete(qr.id)} className="text-red-600">
|
||||
Delete
|
||||
</DropdownItem>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,22 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
|
|||
|
||||
return (
|
||||
<section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 pt-12 pb-20">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
|
||||
{/* Animated Background Orbs */}
|
||||
<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">
|
||||
|
|
@ -83,6 +98,9 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Smooth Gradient Fade Transition */}
|
||||
<div className="absolute bottom-0 left-0 w-full h-32 bg-gradient-to-b from-transparent to-gray-50 pointer-events-none" />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,16 +1,19 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { BillingToggle } from '@/components/ui/BillingToggle';
|
||||
|
||||
interface PricingProps {
|
||||
t: any; // i18n translation function
|
||||
}
|
||||
|
||||
export const Pricing: React.FC<PricingProps> = ({ t }) => {
|
||||
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
|
||||
|
||||
const plans = [
|
||||
{
|
||||
key: 'free',
|
||||
|
|
@ -38,6 +41,10 @@ export const Pricing: React.FC<PricingProps> = ({ t }) => {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mb-8">
|
||||
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
||||
{plans.map((plan) => (
|
||||
<Card
|
||||
|
|
@ -56,13 +63,30 @@ export const Pricing: React.FC<PricingProps> = ({ t }) => {
|
|||
<CardTitle className="text-2xl mb-4">
|
||||
{t.pricing[plan.key].title}
|
||||
</CardTitle>
|
||||
<div className="flex items-baseline justify-center">
|
||||
<span className="text-4xl font-bold">
|
||||
{t.pricing[plan.key].price}
|
||||
</span>
|
||||
<span className="text-gray-600 ml-2">
|
||||
{t.pricing[plan.key].period}
|
||||
</span>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex items-baseline justify-center">
|
||||
<span className="text-4xl font-bold">
|
||||
{plan.key === 'free'
|
||||
? t.pricing[plan.key].price
|
||||
: billingPeriod === 'month'
|
||||
? t.pricing[plan.key].price
|
||||
: plan.key === 'pro'
|
||||
? '€90'
|
||||
: '€290'}
|
||||
</span>
|
||||
<span className="text-gray-600 ml-2">
|
||||
{plan.key === 'free'
|
||||
? t.pricing[plan.key].period
|
||||
: billingPeriod === 'month'
|
||||
? t.pricing[plan.key].period
|
||||
: 'per year'}
|
||||
</span>
|
||||
</div>
|
||||
{billingPeriod === 'year' && plan.key !== 'free' && (
|
||||
<Badge variant="success" className="mt-2">
|
||||
Save 16%
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface BillingToggleProps {
|
||||
value: 'month' | 'year';
|
||||
onChange: (value: 'month' | 'year') => void;
|
||||
}
|
||||
|
||||
export const BillingToggle: React.FC<BillingToggleProps> = ({ value, onChange }) => {
|
||||
return (
|
||||
<div className="inline-flex items-center rounded-lg border border-gray-300 bg-gray-50 p-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange('month')}
|
||||
className={cn(
|
||||
'px-6 py-2 text-sm font-medium rounded-md transition-all duration-200',
|
||||
value === 'month'
|
||||
? 'bg-primary-600 text-white shadow-sm'
|
||||
: 'bg-transparent text-gray-700 hover:bg-gray-100'
|
||||
)}
|
||||
>
|
||||
Monthly
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange('year')}
|
||||
className={cn(
|
||||
'px-6 py-2 text-sm font-medium rounded-md transition-all duration-200',
|
||||
value === 'year'
|
||||
? 'bg-primary-600 text-white shadow-sm'
|
||||
: 'bg-transparent text-gray-700 hover:bg-gray-100'
|
||||
)}
|
||||
>
|
||||
Yearly
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -135,7 +135,8 @@
|
|||
"3 dynamic QR codes",
|
||||
"Unlimited static QR codes",
|
||||
"Basic scan tracking",
|
||||
"Standard QR design templates"
|
||||
"Standard QR design templates",
|
||||
"Download as SVG/PNG"
|
||||
]
|
||||
},
|
||||
"pro": {
|
||||
|
|
@ -148,8 +149,7 @@
|
|||
"50 dynamic QR codes",
|
||||
"Unlimited static QR codes",
|
||||
"Advanced analytics (scans, devices, locations)",
|
||||
"Custom branding (colors)",
|
||||
"Download as SVG/PNG"
|
||||
"Custom branding (colors)"
|
||||
]
|
||||
},
|
||||
"business": {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export async function sendPasswordResetEmail(email: string, resetToken: string)
|
|||
try {
|
||||
await resend.emails.send({
|
||||
from: 'QR Master <onboarding@resend.dev>', // Use Resend's testing domain
|
||||
replyTo: 'support@qrmaster.net',
|
||||
to: email,
|
||||
subject: 'Reset Your Password - QR Master',
|
||||
html: `
|
||||
|
|
|
|||
|
|
@ -44,12 +44,12 @@ export function organizationSchema() {
|
|||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
'@id': 'https://www.qrmaster.com/#organization',
|
||||
'@id': 'https://www.qrmaster.net/#organization',
|
||||
name: 'QR Master',
|
||||
url: 'https://www.qrmaster.com',
|
||||
url: 'https://www.qrmaster.net',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: 'https://www.qrmaster.com/static/og-image.png',
|
||||
url: 'https://www.qrmaster.net/static/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
|
|
@ -59,11 +59,11 @@ export function organizationSchema() {
|
|||
contactPoint: {
|
||||
'@type': 'ContactPoint',
|
||||
contactType: 'Customer Support',
|
||||
email: 'support@qrmaster.com',
|
||||
email: 'support@qrmaster.net',
|
||||
},
|
||||
description: 'Dynamic QR code generator with analytics, branding, and bulk generation for modern marketing campaigns.',
|
||||
inLanguage: 'en',
|
||||
mainEntityOfPage: 'https://www.qrmaster.com',
|
||||
mainEntityOfPage: 'https://www.qrmaster.net',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -71,19 +71,19 @@ export function websiteSchema() {
|
|||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
'@id': 'https://www.qrmaster.com/#website',
|
||||
'@id': 'https://www.qrmaster.net/#website',
|
||||
name: 'QR Master',
|
||||
url: 'https://www.qrmaster.com',
|
||||
url: 'https://www.qrmaster.net',
|
||||
inLanguage: 'en',
|
||||
mainEntityOfPage: 'https://www.qrmaster.com',
|
||||
mainEntityOfPage: 'https://www.qrmaster.net',
|
||||
publisher: {
|
||||
'@id': 'https://www.qrmaster.com/#organization',
|
||||
'@id': 'https://www.qrmaster.net/#organization',
|
||||
},
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: {
|
||||
'@type': 'EntryPoint',
|
||||
urlTemplate: 'https://www.qrmaster.com/blog?q={search_term_string}',
|
||||
urlTemplate: 'https://www.qrmaster.net/blog?q={search_term_string}',
|
||||
},
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
|
|
@ -94,14 +94,14 @@ export function breadcrumbSchema(items: BreadcrumbItem[]) {
|
|||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
'@id': `https://www.qrmaster.com${items[items.length - 1]?.url}#breadcrumb`,
|
||||
'@id': `https://www.qrmaster.net${items[items.length - 1]?.url}#breadcrumb`,
|
||||
inLanguage: 'en',
|
||||
mainEntityOfPage: `https://www.qrmaster.com${items[items.length - 1]?.url}`,
|
||||
mainEntityOfPage: `https://www.qrmaster.net${items[items.length - 1]?.url}`,
|
||||
itemListElement: items.map((item, index) => ({
|
||||
'@type': 'ListItem',
|
||||
position: index + 1,
|
||||
name: item.name,
|
||||
item: `https://www.qrmaster.com${item.url}`,
|
||||
item: `https://www.qrmaster.net${item.url}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
|
@ -110,14 +110,14 @@ export function blogPostingSchema(post: BlogPost) {
|
|||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BlogPosting',
|
||||
'@id': `https://www.qrmaster.com/blog/${post.slug}#article`,
|
||||
'@id': `https://www.qrmaster.net/blog/${post.slug}#article`,
|
||||
headline: post.title,
|
||||
description: post.description,
|
||||
image: post.image,
|
||||
datePublished: post.datePublished,
|
||||
dateModified: post.dateModified,
|
||||
inLanguage: 'en',
|
||||
mainEntityOfPage: `https://www.qrmaster.com/blog/${post.slug}`,
|
||||
mainEntityOfPage: `https://www.qrmaster.net/blog/${post.slug}`,
|
||||
author: {
|
||||
'@type': 'Person',
|
||||
name: post.author,
|
||||
|
|
@ -126,19 +126,19 @@ export function blogPostingSchema(post: BlogPost) {
|
|||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: 'QR Master',
|
||||
url: 'https://www.qrmaster.com',
|
||||
url: 'https://www.qrmaster.net',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: 'https://www.qrmaster.com/static/og-image.png',
|
||||
url: 'https://www.qrmaster.net/static/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
},
|
||||
isPartOf: {
|
||||
'@type': 'Blog',
|
||||
'@id': 'https://www.qrmaster.com/blog#blog',
|
||||
'@id': 'https://www.qrmaster.net/blog#blog',
|
||||
name: 'QR Master Blog',
|
||||
url: 'https://www.qrmaster.com/blog',
|
||||
url: 'https://www.qrmaster.net/blog',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -147,9 +147,9 @@ export function faqPageSchema(faqs: FAQItem[]) {
|
|||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
'@id': 'https://www.qrmaster.com/faq#faqpage',
|
||||
'@id': 'https://www.qrmaster.net/faq#faqpage',
|
||||
inLanguage: 'en',
|
||||
mainEntityOfPage: 'https://www.qrmaster.com/faq',
|
||||
mainEntityOfPage: 'https://www.qrmaster.net/faq',
|
||||
mainEntity: faqs.map((faq) => ({
|
||||
'@type': 'Question',
|
||||
name: faq.question,
|
||||
|
|
@ -165,11 +165,11 @@ export function productSchema(product: { name: string; description: string; offe
|
|||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Product',
|
||||
'@id': 'https://www.qrmaster.com/pricing#product',
|
||||
'@id': 'https://www.qrmaster.net/pricing#product',
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
inLanguage: 'en',
|
||||
mainEntityOfPage: 'https://www.qrmaster.com/pricing',
|
||||
mainEntityOfPage: 'https://www.qrmaster.net/pricing',
|
||||
brand: {
|
||||
'@type': 'Organization',
|
||||
name: 'QR Master',
|
||||
|
|
@ -189,11 +189,11 @@ export function howToSchema(task: HowToTask) {
|
|||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'HowTo',
|
||||
'@id': `https://www.qrmaster.com/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}#howto`,
|
||||
'@id': `https://www.qrmaster.net/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}#howto`,
|
||||
name: task.name,
|
||||
description: task.description,
|
||||
inLanguage: 'en',
|
||||
mainEntityOfPage: `https://www.qrmaster.com/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}`,
|
||||
mainEntityOfPage: `https://www.qrmaster.net/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}`,
|
||||
totalTime: task.totalTime || 'PT5M',
|
||||
step: task.steps.map((step, index) => ({
|
||||
'@type': 'HowToStep',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,40 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer utilities {
|
||||
/* Floating blob animation for hero background */
|
||||
@keyframes blob {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
25% {
|
||||
transform: translate(20px, -30px) scale(1.1);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-20px, 20px) scale(0.9);
|
||||
}
|
||||
75% {
|
||||
transform: translate(30px, 10px) scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-blob {
|
||||
animation: blob 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animation-delay-2000 {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.animation-delay-4000 {
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
.animation-delay-6000 {
|
||||
animation-delay: 6s;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
|
|
|
|||
Loading…
Reference in New Issue