TikTok api
This commit is contained in:
@@ -1,39 +1,39 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(docker-compose:*)",
|
"Bash(docker-compose:*)",
|
||||||
"Bash(docker container prune:*)",
|
"Bash(docker container prune:*)",
|
||||||
"Bash(npx prisma migrate dev:*)",
|
"Bash(npx prisma migrate dev:*)",
|
||||||
"Bash(npx prisma:*)",
|
"Bash(npx prisma:*)",
|
||||||
"Bash(npm run dev)",
|
"Bash(npm run dev)",
|
||||||
"Bash(timeout:*)",
|
"Bash(timeout:*)",
|
||||||
"Bash(taskkill:*)",
|
"Bash(taskkill:*)",
|
||||||
"Bash(npx kill-port:*)",
|
"Bash(npx kill-port:*)",
|
||||||
"Bash(docker compose:*)",
|
"Bash(docker compose:*)",
|
||||||
"Bash(curl -I https://fonts.googleapis.com)",
|
"Bash(curl -I https://fonts.googleapis.com)",
|
||||||
"Bash(wsl:*)",
|
"Bash(wsl:*)",
|
||||||
"Read(//c/Users/a931627/.ssh/**)",
|
"Read(//c/Users/a931627/.ssh/**)",
|
||||||
"Bash(ssh-keygen:*)",
|
"Bash(ssh-keygen:*)",
|
||||||
"Bash(cat:*)",
|
"Bash(cat:*)",
|
||||||
"Bash(git remote add:*)",
|
"Bash(git remote add:*)",
|
||||||
"Bash(git push:*)",
|
"Bash(git push:*)",
|
||||||
"Bash(git remote set-url:*)",
|
"Bash(git remote set-url:*)",
|
||||||
"Bash(npm install:*)",
|
"Bash(npm install:*)",
|
||||||
"Bash(npm run build:*)",
|
"Bash(npm run build:*)",
|
||||||
"Bash(ls:*)",
|
"Bash(ls:*)",
|
||||||
"Bash(curl:*)",
|
"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(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)",
|
"Skill(shadcn-ui)",
|
||||||
"Bash(find:*)",
|
"Bash(find:*)",
|
||||||
"Bash(ls -la \"/c/Users/User/Documents/QR-master/src/app/\\(main\\)/\\(marketing\\)/\")",
|
"Bash(ls -la \"/c/Users/User/Documents/QR-master/src/app/\\(main\\)/\\(marketing\\)/\")",
|
||||||
"Bash(npx tsc:*)"
|
"Bash(npx tsc:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
},
|
},
|
||||||
"enabledMcpjsonServers": [
|
"enabledMcpjsonServers": [
|
||||||
"firecrawl",
|
"firecrawl",
|
||||||
"apify"
|
"apify"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,131 +1,131 @@
|
|||||||
# QR Master Internal RevOps Export API
|
# QR Master Internal RevOps Export API
|
||||||
|
|
||||||
This endpoint exposes read-only QR Master admin/product data for trusted automations such as Hermes cron jobs.
|
This endpoint exposes read-only QR Master admin/product data for trusted automations such as Hermes cron jobs.
|
||||||
|
|
||||||
## Endpoint
|
## Endpoint
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /api/internal/revops-export
|
GET /api/internal/revops-export
|
||||||
Authorization: Bearer <INTERNAL_API_SECRET>
|
Authorization: Bearer <INTERNAL_API_SECRET>
|
||||||
```
|
```
|
||||||
|
|
||||||
The endpoint accepts `INTERNAL_API_SECRET` first, with `CRON_SECRET` as a fallback for compatibility.
|
The endpoint accepts `INTERNAL_API_SECRET` first, with `CRON_SECRET` as a fallback for compatibility.
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
QR Master only exports structured data. It does **not** perform AI analysis, web research, email drafting, or lead outreach.
|
QR Master only exports structured data. It does **not** perform AI analysis, web research, email drafting, or lead outreach.
|
||||||
|
|
||||||
Hermes is responsible for:
|
Hermes is responsible for:
|
||||||
|
|
||||||
- calling this API on a schedule,
|
- calling this API on a schedule,
|
||||||
- researching public websites/domains with its tools,
|
- researching public websites/domains with its tools,
|
||||||
- deciding which leads are worth manual outreach,
|
- deciding which leads are worth manual outreach,
|
||||||
- drafting personalized emails,
|
- drafting personalized emails,
|
||||||
- sending Timo an internal briefing,
|
- sending Timo an internal briefing,
|
||||||
- never auto-sending messages to QR Master users.
|
- never auto-sending messages to QR Master users.
|
||||||
|
|
||||||
## Query Parameters
|
## Query Parameters
|
||||||
|
|
||||||
| Parameter | Example | Notes |
|
| Parameter | Example | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `range` | `24h`, `7d`, `30d`, `all` | Defaults to `24h`. |
|
| `range` | `24h`, `7d`, `30d`, `all` | Defaults to `24h`. |
|
||||||
| `from` | `2026-06-30T00:00:00Z` | Optional explicit start. Overrides `range`. |
|
| `from` | `2026-06-30T00:00:00Z` | Optional explicit start. Overrides `range`. |
|
||||||
| `to` | `2026-07-01T00:00:00Z` | Optional explicit end. Defaults to now if `from` is set. |
|
| `to` | `2026-07-01T00:00:00Z` | Optional explicit end. Defaults to now if `from` is set. |
|
||||||
| `limit` | `100` | User/lead limit. Defaults to 100, max 500. |
|
| `limit` | `100` | User/lead limit. Defaults to 100, max 500. |
|
||||||
| `allUsers` | `true` | Exports all users up to `limit`; otherwise exports users active/relevant in range. |
|
| `allUsers` | `true` | Exports all users up to `limit`; otherwise exports users active/relevant in range. |
|
||||||
|
|
||||||
## Example Calls
|
## Example Calls
|
||||||
|
|
||||||
Daily Hermes lead research context:
|
Daily Hermes lead research context:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -H "Authorization: Bearer $INTERNAL_API_SECRET" \
|
curl -H "Authorization: Bearer $INTERNAL_API_SECRET" \
|
||||||
"https://www.qrmaster.net/api/internal/revops-export?range=24h&limit=100"
|
"https://www.qrmaster.net/api/internal/revops-export?range=24h&limit=100"
|
||||||
```
|
```
|
||||||
|
|
||||||
Weekly progress context:
|
Weekly progress context:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -H "Authorization: Bearer $INTERNAL_API_SECRET" \
|
curl -H "Authorization: Bearer $INTERNAL_API_SECRET" \
|
||||||
"https://www.qrmaster.net/api/internal/revops-export?range=7d&limit=200"
|
"https://www.qrmaster.net/api/internal/revops-export?range=7d&limit=200"
|
||||||
```
|
```
|
||||||
|
|
||||||
Local development:
|
Local development:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -H "Authorization: Bearer $INTERNAL_API_SECRET" \
|
curl -H "Authorization: Bearer $INTERNAL_API_SECRET" \
|
||||||
"http://localhost:3050/api/internal/revops-export?range=24h"
|
"http://localhost:3050/api/internal/revops-export?range=24h"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Response Shape
|
## Response Shape
|
||||||
|
|
||||||
Top-level fields:
|
Top-level fields:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"generatedAt": "2026-07-01T18:00:00.000Z",
|
"generatedAt": "2026-07-01T18:00:00.000Z",
|
||||||
"endpoint": "/api/internal/revops-export",
|
"endpoint": "/api/internal/revops-export",
|
||||||
"range": {
|
"range": {
|
||||||
"preset": "24h",
|
"preset": "24h",
|
||||||
"from": "2026-06-30T18:00:00.000Z",
|
"from": "2026-06-30T18:00:00.000Z",
|
||||||
"to": "2026-07-01T18:00:00.000Z"
|
"to": "2026-07-01T18:00:00.000Z"
|
||||||
},
|
},
|
||||||
"overview": {},
|
"overview": {},
|
||||||
"breakdowns": {},
|
"breakdowns": {},
|
||||||
"users": [],
|
"users": [],
|
||||||
"leads": [],
|
"leads": [],
|
||||||
"newsletterSubscriptions": []
|
"newsletterSubscriptions": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Each exported user includes:
|
Each exported user includes:
|
||||||
|
|
||||||
- identity: `id`, `name`, `email`, `emailDomain`, `createdAt`, `updatedAt`
|
- identity: `id`, `name`, `email`, `emailDomain`, `createdAt`, `updatedAt`
|
||||||
- plan/subscription flags
|
- plan/subscription flags
|
||||||
- attribution fields
|
- attribution fields
|
||||||
- onboarding fields
|
- onboarding fields
|
||||||
- lifecycle scores and timestamps
|
- lifecycle scores and timestamps
|
||||||
- aggregate stats
|
- aggregate stats
|
||||||
- QR codes, content, public path, and scan summaries
|
- QR codes, content, public path, and scan summaries
|
||||||
- integrations
|
- integrations
|
||||||
- recent lifecycle logs
|
- recent lifecycle logs
|
||||||
|
|
||||||
## Safety Rules for Hermes Jobs
|
## Safety Rules for Hermes Jobs
|
||||||
|
|
||||||
Hermes should treat this API as internal source data and follow these rules:
|
Hermes should treat this API as internal source data and follow these rules:
|
||||||
|
|
||||||
1. Do not send outreach emails directly to leads.
|
1. Do not send outreach emails directly to leads.
|
||||||
2. Send only an internal briefing/draft pack to Timo.
|
2. Send only an internal briefing/draft pack to Timo.
|
||||||
3. Use public web research only when it improves personalization.
|
3. Use public web research only when it improves personalization.
|
||||||
4. Do not tell leads they were researched.
|
4. Do not tell leads they were researched.
|
||||||
5. Avoid creepy language; frame personalization around their QR Master activity and likely workflow.
|
5. Avoid creepy language; frame personalization around their QR Master activity and likely workflow.
|
||||||
6. Never expose API secrets or raw sensitive internals in the briefing.
|
6. Never expose API secrets or raw sensitive internals in the briefing.
|
||||||
7. Prefer business-use signals: QR content type, destination, scans, onboarding use case, domain, company website, landing path.
|
7. Prefer business-use signals: QR content type, destination, scans, onboarding use case, domain, company website, landing path.
|
||||||
|
|
||||||
## Suggested Hermes Cron Jobs
|
## Suggested Hermes Cron Jobs
|
||||||
|
|
||||||
### Daily QR Master Lead Research
|
### Daily QR Master Lead Research
|
||||||
|
|
||||||
Schedule: daily, evening.
|
Schedule: daily, evening.
|
||||||
|
|
||||||
Prompt should:
|
Prompt should:
|
||||||
|
|
||||||
1. call `/api/internal/revops-export?range=24h`,
|
1. call `/api/internal/revops-export?range=24h`,
|
||||||
2. identify promising leads,
|
2. identify promising leads,
|
||||||
3. research public company/domain signals,
|
3. research public company/domain signals,
|
||||||
4. draft personalized outreach emails from Timo,
|
4. draft personalized outreach emails from Timo,
|
||||||
5. send Timo an internal email briefing,
|
5. send Timo an internal email briefing,
|
||||||
6. never send emails to leads.
|
6. never send emails to leads.
|
||||||
|
|
||||||
### Weekly QR Master Progress Review
|
### Weekly QR Master Progress Review
|
||||||
|
|
||||||
Schedule: Sunday evening.
|
Schedule: Sunday evening.
|
||||||
|
|
||||||
Prompt should:
|
Prompt should:
|
||||||
|
|
||||||
1. call `/api/internal/revops-export?range=7d`,
|
1. call `/api/internal/revops-export?range=7d`,
|
||||||
2. summarize users, QR creation, scans, activations, paid/upgrades,
|
2. summarize users, QR creation, scans, activations, paid/upgrades,
|
||||||
3. identify bottlenecks,
|
3. identify bottlenecks,
|
||||||
4. suggest next-week goals,
|
4. suggest next-week goals,
|
||||||
5. email Timo the review.
|
5. email Timo the review.
|
||||||
|
|||||||
19
env.example
19
env.example
@@ -27,12 +27,12 @@ REDIS_URL=redis://redis:6379
|
|||||||
# Used for hashing IP addresses in analytics
|
# Used for hashing IP addresses in analytics
|
||||||
IP_SALT=your-ip-salt-here-change-in-production
|
IP_SALT=your-ip-salt-here-change-in-production
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
ENABLE_DEMO=false
|
ENABLE_DEMO=false
|
||||||
|
|
||||||
# SEO Configuration
|
# SEO Configuration
|
||||||
# Set to 'true' in production to allow search engine indexing
|
# Set to 'true' in production to allow search engine indexing
|
||||||
NEXT_PUBLIC_INDEXABLE=true
|
NEXT_PUBLIC_INDEXABLE=true
|
||||||
|
|
||||||
# Stripe Payment Configuration (Optional - for subscription payments)
|
# Stripe Payment Configuration (Optional - for subscription payments)
|
||||||
# Get your keys from: https://dashboard.stripe.com/apikeys
|
# Get your keys from: https://dashboard.stripe.com/apikeys
|
||||||
@@ -48,3 +48,10 @@ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
|||||||
# Analytics (Optional - PostHog)
|
# Analytics (Optional - PostHog)
|
||||||
NEXT_PUBLIC_POSTHOG_KEY=
|
NEXT_PUBLIC_POSTHOG_KEY=
|
||||||
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
|
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
|
||||||
|
|
||||||
|
# TikTok Content Posting API (Hermes Agent automated posting)
|
||||||
|
TIKTOK_CLIENT_KEY=
|
||||||
|
TIKTOK_CLIENT_SECRET=
|
||||||
|
TIKTOK_REDIRECT_URI=https://qrmaster.net/api/tiktok/callback
|
||||||
|
# Optional: protects /api/tiktok/connect from being triggered by strangers
|
||||||
|
TIKTOK_ADMIN_KEY=
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ datasource db {
|
|||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
email String @unique
|
email String @unique
|
||||||
name String?
|
name String?
|
||||||
password String?
|
password String?
|
||||||
image String?
|
image String?
|
||||||
emailVerified DateTime?
|
emailVerified DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@@ -32,56 +32,56 @@ model User {
|
|||||||
resetPasswordToken String? @unique
|
resetPasswordToken String? @unique
|
||||||
resetPasswordExpires DateTime?
|
resetPasswordExpires DateTime?
|
||||||
|
|
||||||
// Retention email tracking
|
// Retention email tracking
|
||||||
activationNudgeSentAt DateTime?
|
activationNudgeSentAt DateTime?
|
||||||
upgradeNudgeSentAt DateTime?
|
upgradeNudgeSentAt DateTime?
|
||||||
thirtyDayNudgeSentAt DateTime?
|
thirtyDayNudgeSentAt DateTime?
|
||||||
|
|
||||||
// RevOps attribution
|
// RevOps attribution
|
||||||
signupSource String?
|
signupSource String?
|
||||||
signupSourceSelfReported String?
|
signupSourceSelfReported String?
|
||||||
signupMedium String?
|
signupMedium String?
|
||||||
signupCampaign String?
|
signupCampaign String?
|
||||||
signupContent String?
|
signupContent String?
|
||||||
signupTerm String?
|
signupTerm String?
|
||||||
signupReferrer String?
|
signupReferrer String?
|
||||||
signupLandingPath String?
|
signupLandingPath String?
|
||||||
signupFirstSeenAt DateTime?
|
signupFirstSeenAt DateTime?
|
||||||
emailDomain String?
|
emailDomain String?
|
||||||
|
|
||||||
// Onboarding and qualification
|
// Onboarding and qualification
|
||||||
primaryUseCase String?
|
primaryUseCase String?
|
||||||
primaryGoal String?
|
primaryGoal String?
|
||||||
jobRole String?
|
jobRole String?
|
||||||
companyName String?
|
companyName String?
|
||||||
companyWebsite String?
|
companyWebsite String?
|
||||||
teamSizeBucket String?
|
teamSizeBucket String?
|
||||||
onboardingStartedAt DateTime?
|
onboardingStartedAt DateTime?
|
||||||
sourceConfirmedAt DateTime?
|
sourceConfirmedAt DateTime?
|
||||||
useCaseSelectedAt DateTime?
|
useCaseSelectedAt DateTime?
|
||||||
goalSelectedAt DateTime?
|
goalSelectedAt DateTime?
|
||||||
profileCompletedAt DateTime?
|
profileCompletedAt DateTime?
|
||||||
firstQrCreatedAt DateTime?
|
firstQrCreatedAt DateTime?
|
||||||
firstDynamicQrAt DateTime?
|
firstDynamicQrAt DateTime?
|
||||||
firstStaticQrAt DateTime?
|
firstStaticQrAt DateTime?
|
||||||
firstScanAt DateTime?
|
firstScanAt DateTime?
|
||||||
activationAt DateTime?
|
activationAt DateTime?
|
||||||
onboardingCompletedAt DateTime?
|
onboardingCompletedAt DateTime?
|
||||||
|
|
||||||
// RevOps scoring
|
// RevOps scoring
|
||||||
fitScore Int @default(0)
|
fitScore Int @default(0)
|
||||||
intentScore Int @default(0)
|
intentScore Int @default(0)
|
||||||
leadScore Int @default(0)
|
leadScore Int @default(0)
|
||||||
lifecycleStage String @default("cold")
|
lifecycleStage String @default("cold")
|
||||||
lastQualifiedAt DateTime?
|
lastQualifiedAt DateTime?
|
||||||
lastScoredAt DateTime?
|
lastScoredAt DateTime?
|
||||||
|
|
||||||
qrCodes QRCode[]
|
qrCodes QRCode[]
|
||||||
integrations Integration[]
|
integrations Integration[]
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
lifecycleLogs UserLifecycleLog[]
|
lifecycleLogs UserLifecycleLog[]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Plan {
|
enum Plan {
|
||||||
FREE
|
FREE
|
||||||
@@ -189,7 +189,7 @@ model QRScan {
|
|||||||
@@index([qrId, ts])
|
@@index([qrId, ts])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Integration {
|
model Integration {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
userId String
|
userId String
|
||||||
provider String
|
provider String
|
||||||
@@ -198,22 +198,35 @@ model Integration {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
model UserLifecycleLog {
|
model TiktokIntegration {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
userId String
|
accountKey String @unique
|
||||||
fromStage String?
|
openId String
|
||||||
toStage String
|
accessToken String
|
||||||
fitScore Int @default(0)
|
refreshToken String
|
||||||
intentScore Int @default(0)
|
scope String?
|
||||||
leadScore Int @default(0)
|
accessTokenExpiresAt DateTime
|
||||||
reason String?
|
refreshTokenExpiresAt DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
}
|
||||||
}
|
|
||||||
|
model UserLifecycleLog {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
fromStage String?
|
||||||
|
toStage String
|
||||||
|
fitScore Int @default(0)
|
||||||
|
intentScore Int @default(0)
|
||||||
|
leadScore Int @default(0)
|
||||||
|
reason String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
model NewsletterSubscription {
|
model NewsletterSubscription {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
|||||||
@@ -1,147 +1,147 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
|
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Terms of Service | QR Master',
|
title: 'Terms of Service | QR Master',
|
||||||
description: 'Read the QR Master Terms of Service to understand the rules, rights, and responsibilities that apply when you use our QR code platform.',
|
description: 'Read the QR Master Terms of Service to understand the rules, rights, and responsibilities that apply when you use our QR code platform.',
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: 'Terms of Service | QR Master',
|
title: 'Terms of Service | QR Master',
|
||||||
description: 'Read the QR Master Terms of Service to understand the rules, rights, and responsibilities that apply when you use our QR code platform.',
|
description: 'Read the QR Master Terms of Service to understand the rules, rights, and responsibilities that apply when you use our QR code platform.',
|
||||||
url: 'https://www.qrmaster.net/terms',
|
url: 'https://www.qrmaster.net/terms',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
images: ['/og-image.png'],
|
images: ['/og-image.png'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TermsPage() {
|
export default function TermsPage() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white py-12">
|
<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="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<Link href="/" className="text-primary-600 hover:text-primary-700 font-medium">
|
<Link href="/" className="text-primary-600 hover:text-primary-700 font-medium">
|
||||||
← Back to Home
|
← Back to Home
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">Terms of Service</h1>
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">Terms of Service</h1>
|
||||||
<p className="text-gray-600 mb-8">Last updated: July 2026</p>
|
<p className="text-gray-600 mb-8">Last updated: July 2026</p>
|
||||||
|
|
||||||
<div className="prose prose-lg max-w-none">
|
<div className="prose prose-lg max-w-none">
|
||||||
<section className="mb-8">
|
<section className="mb-8">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">1. Acceptance of Terms</h2>
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">1. Acceptance of Terms</h2>
|
||||||
<p className="text-gray-700 mb-4">
|
<p className="text-gray-700 mb-4">
|
||||||
Welcome to QR Master ("we," "our," or "us"). By accessing or using our website and services, you agree to be
|
Welcome to QR Master ("we," "our," or "us"). By accessing or using our website and services, you agree to be
|
||||||
bound by these Terms of Service. If you do not agree to these terms, please do not use our services.
|
bound by these Terms of Service. If you do not agree to these terms, please do not use our services.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mb-8">
|
<section className="mb-8">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">2. Description of Service</h2>
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">2. Description of Service</h2>
|
||||||
<p className="text-gray-700 mb-4">
|
<p className="text-gray-700 mb-4">
|
||||||
QR Master provides a platform for creating and managing static and dynamic QR codes, including scan
|
QR Master provides a platform for creating and managing static and dynamic QR codes, including scan
|
||||||
analytics, customization tools, and bulk creation features. We may add, change, or remove features at any
|
analytics, customization tools, and bulk creation features. We may add, change, or remove features at any
|
||||||
time.
|
time.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mb-8">
|
<section className="mb-8">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">3. Accounts</h2>
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">3. Accounts</h2>
|
||||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||||
<li>You are responsible for maintaining the confidentiality of your account credentials and for all activity under your account.</li>
|
<li>You are responsible for maintaining the confidentiality of your account credentials and for all activity under your account.</li>
|
||||||
<li>You must be at least 16 years old to use our services.</li>
|
<li>You must be at least 16 years old to use our services.</li>
|
||||||
<li>You must provide accurate account information and keep it up to date.</li>
|
<li>You must provide accurate account information and keep it up to date.</li>
|
||||||
<li>You may delete your account at any time from your account settings.</li>
|
<li>You may delete your account at any time from your account settings.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mb-8">
|
<section className="mb-8">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">4. Subscriptions & Payments</h2>
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">4. Subscriptions & Payments</h2>
|
||||||
<p className="text-gray-700 mb-4">
|
<p className="text-gray-700 mb-4">
|
||||||
QR Master offers FREE, PRO, and BUSINESS subscription plans. Paid subscriptions are billed through Stripe
|
QR Master offers FREE, PRO, and BUSINESS subscription plans. Paid subscriptions are billed through Stripe
|
||||||
and renew automatically until cancelled. You can manage or cancel your subscription at any time through
|
and renew automatically until cancelled. You can manage or cancel your subscription at any time through
|
||||||
the customer billing portal in your account settings.
|
the customer billing portal in your account settings.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 mb-4">
|
<p className="text-gray-700 mb-4">
|
||||||
Fees are non-refundable except where required by applicable law. Downgrading or cancelling a plan may
|
Fees are non-refundable except where required by applicable law. Downgrading or cancelling a plan may
|
||||||
affect the availability of dynamic QR codes and other paid features.
|
affect the availability of dynamic QR codes and other paid features.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mb-8">
|
<section className="mb-8">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">5. Acceptable Use</h2>
|
<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 QR Master to:</p>
|
<p className="text-gray-700 mb-4">You agree not to use QR Master to:</p>
|
||||||
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
<ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2">
|
||||||
<li>Create or distribute QR codes linking to illegal, fraudulent, or harmful content</li>
|
<li>Create or distribute QR codes linking to illegal, fraudulent, or harmful content</li>
|
||||||
<li>Spam, phish, or mislead scanners about the destination of a QR code</li>
|
<li>Spam, phish, or mislead scanners about the destination of a QR code</li>
|
||||||
<li>Attempt to circumvent rate limits, security measures, or plan restrictions</li>
|
<li>Attempt to circumvent rate limits, security measures, or plan restrictions</li>
|
||||||
<li>Interfere with or disrupt the integrity or performance of our services</li>
|
<li>Interfere with or disrupt the integrity or performance of our services</li>
|
||||||
<li>Reverse engineer or resell access to our services without authorization</li>
|
<li>Reverse engineer or resell access to our services without authorization</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p className="text-gray-700 mb-4">
|
<p className="text-gray-700 mb-4">
|
||||||
We reserve the right to disable QR codes or accounts that violate this policy.
|
We reserve the right to disable QR codes or accounts that violate this policy.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mb-8">
|
<section className="mb-8">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">6. Intellectual Property</h2>
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">6. Intellectual Property</h2>
|
||||||
<p className="text-gray-700 mb-4">
|
<p className="text-gray-700 mb-4">
|
||||||
QR Master and its original content, features, and functionality are owned by us and are protected by
|
QR Master and its original content, features, and functionality are owned by us and are protected by
|
||||||
copyright, trademark, and other intellectual property laws. You retain all rights to the content and
|
copyright, trademark, and other intellectual property laws. You retain all rights to the content and
|
||||||
destinations you associate with your own QR codes.
|
destinations you associate with your own QR codes.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mb-8">
|
<section className="mb-8">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">7. Termination</h2>
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">7. Termination</h2>
|
||||||
<p className="text-gray-700 mb-4">
|
<p className="text-gray-700 mb-4">
|
||||||
We may suspend or terminate your access to QR Master at any time if you violate these terms or misuse our
|
We may suspend or terminate your access to QR Master at any time if you violate these terms or misuse our
|
||||||
services. You may stop using our services and delete your account at any time.
|
services. You may stop using our services and delete your account at any time.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mb-8">
|
<section className="mb-8">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">8. Disclaimers & Limitation of Liability</h2>
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">8. Disclaimers & Limitation of Liability</h2>
|
||||||
<p className="text-gray-700 mb-4">
|
<p className="text-gray-700 mb-4">
|
||||||
Our services are provided "as is" and "as available" without warranties of any kind. We do not control
|
Our services are provided "as is" and "as available" without warranties of any kind. We do not control
|
||||||
and are not responsible for the content that a QR code links to once you set its destination. To the
|
and are not responsible for the content that a QR code links to once you set its destination. To the
|
||||||
maximum extent permitted by law, QR Master shall not be liable for any indirect, incidental, or
|
maximum extent permitted by law, QR Master shall not be liable for any indirect, incidental, or
|
||||||
consequential damages arising from your use of the services.
|
consequential damages arising from your use of the services.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mb-8">
|
<section className="mb-8">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">9. Changes to These Terms</h2>
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">9. Changes to These Terms</h2>
|
||||||
<p className="text-gray-700 mb-4">
|
<p className="text-gray-700 mb-4">
|
||||||
We may update these Terms of Service from time to time. We will update the "Last updated" date above when
|
We may update these Terms of Service from time to time. We will update the "Last updated" date above when
|
||||||
changes are made. Continued use of our services after changes take effect constitutes acceptance of the
|
changes are made. Continued use of our services after changes take effect constitutes acceptance of the
|
||||||
revised terms.
|
revised terms.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mb-8">
|
<section className="mb-8">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">10. Contact Us</h2>
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">10. Contact Us</h2>
|
||||||
<p className="text-gray-700 mb-4">
|
<p className="text-gray-700 mb-4">
|
||||||
If you have questions about these Terms of Service, please contact us:
|
If you have questions about these Terms of Service, please contact us:
|
||||||
</p>
|
</p>
|
||||||
<div className="bg-gray-50 p-6 rounded-lg">
|
<div className="bg-gray-50 p-6 rounded-lg">
|
||||||
<p className="text-gray-700 mb-2">
|
<p className="text-gray-700 mb-2">
|
||||||
<strong>Email:</strong>{' '}
|
<strong>Email:</strong>{' '}
|
||||||
<ObfuscatedMailto email="support@qrmaster.net" className="text-primary-600 hover:text-primary-700" />
|
<ObfuscatedMailto email="support@qrmaster.net" className="text-primary-600 hover:text-primary-700" />
|
||||||
</p>
|
</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>
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-12 pt-8 border-t border-gray-200">
|
<div className="mt-12 pt-8 border-t border-gray-200">
|
||||||
<p className="text-gray-600 text-center">
|
<p className="text-gray-600 text-center">
|
||||||
<Link href="/" className="text-primary-600 hover:text-primary-700">
|
<Link href="/" className="text-primary-600 hover:text-primary-700">
|
||||||
Back to Home
|
Back to Home
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,463 +1,463 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getEmailDomain } from '@/lib/revops';
|
import { getEmailDomain } from '@/lib/revops';
|
||||||
import { getMetricSnapshot, getUpgradeCandidateBadges } from '@/lib/revops-server';
|
import { getMetricSnapshot, getUpgradeCandidateBadges } from '@/lib/revops-server';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
type RangePreset = '24h' | '7d' | '30d' | 'all';
|
type RangePreset = '24h' | '7d' | '30d' | 'all';
|
||||||
|
|
||||||
const DEFAULT_LIMIT = 100;
|
const DEFAULT_LIMIT = 100;
|
||||||
const MAX_LIMIT = 500;
|
const MAX_LIMIT = 500;
|
||||||
|
|
||||||
function isAuthorized(request: NextRequest): boolean {
|
function isAuthorized(request: NextRequest): boolean {
|
||||||
const authHeader = request.headers.get('authorization');
|
const authHeader = request.headers.get('authorization');
|
||||||
const token = process.env.INTERNAL_API_SECRET || process.env.CRON_SECRET;
|
const token = process.env.INTERNAL_API_SECRET || process.env.CRON_SECRET;
|
||||||
|
|
||||||
if (!token) return false;
|
if (!token) return false;
|
||||||
return authHeader === `Bearer ${token}`;
|
return authHeader === `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePositiveInt(value: string | null, fallback: number, max: number) {
|
function parsePositiveInt(value: string | null, fallback: number, max: number) {
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
||||||
return Math.min(Math.floor(parsed), max);
|
return Math.min(Math.floor(parsed), max);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDate(value: string | null): Date | null {
|
function parseDate(value: string | null): Date | null {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
const parsed = new Date(value);
|
const parsed = new Date(value);
|
||||||
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDateRange(request: NextRequest) {
|
function resolveDateRange(request: NextRequest) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const explicitFrom = parseDate(request.nextUrl.searchParams.get('from'));
|
const explicitFrom = parseDate(request.nextUrl.searchParams.get('from'));
|
||||||
const explicitTo = parseDate(request.nextUrl.searchParams.get('to'));
|
const explicitTo = parseDate(request.nextUrl.searchParams.get('to'));
|
||||||
const range = (request.nextUrl.searchParams.get('range') || '24h') as RangePreset;
|
const range = (request.nextUrl.searchParams.get('range') || '24h') as RangePreset;
|
||||||
|
|
||||||
if (explicitFrom || explicitTo) {
|
if (explicitFrom || explicitTo) {
|
||||||
return {
|
return {
|
||||||
from: explicitFrom,
|
from: explicitFrom,
|
||||||
to: explicitTo ?? now,
|
to: explicitTo ?? now,
|
||||||
preset: 'custom',
|
preset: 'custom',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (range === 'all') {
|
if (range === 'all') {
|
||||||
return { from: null, to: now, preset: range };
|
return { from: null, to: now, preset: range };
|
||||||
}
|
}
|
||||||
|
|
||||||
const hours = range === '7d' ? 24 * 7 : range === '30d' ? 24 * 30 : 24;
|
const hours = range === '7d' ? 24 * 7 : range === '30d' ? 24 * 30 : 24;
|
||||||
return {
|
return {
|
||||||
from: new Date(now.getTime() - hours * 60 * 60 * 1000),
|
from: new Date(now.getTime() - hours * 60 * 60 * 1000),
|
||||||
to: now,
|
to: now,
|
||||||
preset: range,
|
preset: range,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function withinRange(date: Date | null | undefined, from: Date | null, to: Date) {
|
function withinRange(date: Date | null | undefined, from: Date | null, to: Date) {
|
||||||
if (!date) return false;
|
if (!date) return false;
|
||||||
return (!from || date >= from) && date <= to;
|
return (!from || date >= from) && date <= to;
|
||||||
}
|
}
|
||||||
|
|
||||||
function jsonDate(value: Date | null | undefined) {
|
function jsonDate(value: Date | null | undefined) {
|
||||||
return value ? value.toISOString() : null;
|
return value ? value.toISOString() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeJson(value: unknown) {
|
function safeJson(value: unknown) {
|
||||||
return value ?? null;
|
return value ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function summarizeScans(scans: Array<{
|
function summarizeScans(scans: Array<{
|
||||||
ts: Date;
|
ts: Date;
|
||||||
isUnique: boolean;
|
isUnique: boolean;
|
||||||
country: string | null;
|
country: string | null;
|
||||||
device: string | null;
|
device: string | null;
|
||||||
os: string | null;
|
os: string | null;
|
||||||
referrer: string | null;
|
referrer: string | null;
|
||||||
utmSource: string | null;
|
utmSource: string | null;
|
||||||
utmMedium: string | null;
|
utmMedium: string | null;
|
||||||
utmCampaign: string | null;
|
utmCampaign: string | null;
|
||||||
}>) {
|
}>) {
|
||||||
const countries = new Map<string, number>();
|
const countries = new Map<string, number>();
|
||||||
const devices = new Map<string, number>();
|
const devices = new Map<string, number>();
|
||||||
const os = new Map<string, number>();
|
const os = new Map<string, number>();
|
||||||
const referrers = new Map<string, number>();
|
const referrers = new Map<string, number>();
|
||||||
const utmSources = new Map<string, number>();
|
const utmSources = new Map<string, number>();
|
||||||
const utmCampaigns = new Map<string, number>();
|
const utmCampaigns = new Map<string, number>();
|
||||||
const scansByDay = new Map<string, number>();
|
const scansByDay = new Map<string, number>();
|
||||||
|
|
||||||
for (const scan of scans) {
|
for (const scan of scans) {
|
||||||
const day = scan.ts.toISOString().slice(0, 10);
|
const day = scan.ts.toISOString().slice(0, 10);
|
||||||
scansByDay.set(day, (scansByDay.get(day) || 0) + 1);
|
scansByDay.set(day, (scansByDay.get(day) || 0) + 1);
|
||||||
|
|
||||||
const increment = (map: Map<string, number>, key?: string | null) => {
|
const increment = (map: Map<string, number>, key?: string | null) => {
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
map.set(key, (map.get(key) || 0) + 1);
|
map.set(key, (map.get(key) || 0) + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
increment(countries, scan.country);
|
increment(countries, scan.country);
|
||||||
increment(devices, scan.device);
|
increment(devices, scan.device);
|
||||||
increment(os, scan.os);
|
increment(os, scan.os);
|
||||||
increment(referrers, scan.referrer);
|
increment(referrers, scan.referrer);
|
||||||
increment(utmSources, scan.utmSource);
|
increment(utmSources, scan.utmSource);
|
||||||
increment(utmCampaigns, scan.utmCampaign);
|
increment(utmCampaigns, scan.utmCampaign);
|
||||||
}
|
}
|
||||||
|
|
||||||
const top = (map: Map<string, number>) =>
|
const top = (map: Map<string, number>) =>
|
||||||
Array.from(map.entries())
|
Array.from(map.entries())
|
||||||
.sort((a, b) => b[1] - a[1])
|
.sort((a, b) => b[1] - a[1])
|
||||||
.slice(0, 10)
|
.slice(0, 10)
|
||||||
.map(([key, count]) => ({ key, count }));
|
.map(([key, count]) => ({ key, count }));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total: scans.length,
|
total: scans.length,
|
||||||
unique: scans.filter((scan) => scan.isUnique).length,
|
unique: scans.filter((scan) => scan.isUnique).length,
|
||||||
firstScanAt: jsonDate(scans[0]?.ts),
|
firstScanAt: jsonDate(scans[0]?.ts),
|
||||||
lastScanAt: jsonDate(scans[scans.length - 1]?.ts),
|
lastScanAt: jsonDate(scans[scans.length - 1]?.ts),
|
||||||
byDay: Array.from(scansByDay.entries()).map(([date, count]) => ({ date, count })),
|
byDay: Array.from(scansByDay.entries()).map(([date, count]) => ({ date, count })),
|
||||||
topCountries: top(countries),
|
topCountries: top(countries),
|
||||||
topDevices: top(devices),
|
topDevices: top(devices),
|
||||||
topOperatingSystems: top(os),
|
topOperatingSystems: top(os),
|
||||||
topReferrers: top(referrers),
|
topReferrers: top(referrers),
|
||||||
topUtmSources: top(utmSources),
|
topUtmSources: top(utmSources),
|
||||||
topUtmCampaigns: top(utmCampaigns),
|
topUtmCampaigns: top(utmCampaigns),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
if (!isAuthorized(request)) {
|
if (!isAuthorized(request)) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { from, to, preset } = resolveDateRange(request);
|
const { from, to, preset } = resolveDateRange(request);
|
||||||
const limit = parsePositiveInt(request.nextUrl.searchParams.get('limit'), DEFAULT_LIMIT, MAX_LIMIT);
|
const limit = parsePositiveInt(request.nextUrl.searchParams.get('limit'), DEFAULT_LIMIT, MAX_LIMIT);
|
||||||
const includeAllUsers = request.nextUrl.searchParams.get('allUsers') === 'true' || preset === 'all';
|
const includeAllUsers = request.nextUrl.searchParams.get('allUsers') === 'true' || preset === 'all';
|
||||||
|
|
||||||
const users = await db.user.findMany({
|
const users = await db.user.findMany({
|
||||||
where: includeAllUsers || !from ? undefined : {
|
where: includeAllUsers || !from ? undefined : {
|
||||||
OR: [
|
OR: [
|
||||||
{ createdAt: { gte: from, lte: to } },
|
{ createdAt: { gte: from, lte: to } },
|
||||||
{ updatedAt: { gte: from, lte: to } },
|
{ updatedAt: { gte: from, lte: to } },
|
||||||
{ firstQrCreatedAt: { gte: from, lte: to } },
|
{ firstQrCreatedAt: { gte: from, lte: to } },
|
||||||
{ firstDynamicQrAt: { gte: from, lte: to } },
|
{ firstDynamicQrAt: { gte: from, lte: to } },
|
||||||
{ firstStaticQrAt: { gte: from, lte: to } },
|
{ firstStaticQrAt: { gte: from, lte: to } },
|
||||||
{ firstScanAt: { gte: from, lte: to } },
|
{ firstScanAt: { gte: from, lte: to } },
|
||||||
{ activationAt: { gte: from, lte: to } },
|
{ activationAt: { gte: from, lte: to } },
|
||||||
{ lastQualifiedAt: { gte: from, lte: to } },
|
{ lastQualifiedAt: { gte: from, lte: to } },
|
||||||
{ lastScoredAt: { gte: from, lte: to } },
|
{ lastScoredAt: { gte: from, lte: to } },
|
||||||
{ qrCodes: { some: { createdAt: { gte: from, lte: to } } } },
|
{ qrCodes: { some: { createdAt: { gte: from, lte: to } } } },
|
||||||
{ qrCodes: { some: { scans: { some: { ts: { gte: from, lte: to } } } } } },
|
{ qrCodes: { some: { scans: { some: { ts: { gte: from, lte: to } } } } } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
orderBy: [
|
orderBy: [
|
||||||
{ leadScore: 'desc' },
|
{ leadScore: 'desc' },
|
||||||
{ createdAt: 'desc' },
|
{ createdAt: 'desc' },
|
||||||
],
|
],
|
||||||
take: limit,
|
take: limit,
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
email: true,
|
email: true,
|
||||||
name: true,
|
name: true,
|
||||||
image: true,
|
image: true,
|
||||||
emailVerified: true,
|
emailVerified: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
plan: true,
|
plan: true,
|
||||||
stripeCustomerId: true,
|
stripeCustomerId: true,
|
||||||
stripeSubscriptionId: true,
|
stripeSubscriptionId: true,
|
||||||
stripePriceId: true,
|
stripePriceId: true,
|
||||||
stripeCurrentPeriodEnd: true,
|
stripeCurrentPeriodEnd: true,
|
||||||
signupSource: true,
|
signupSource: true,
|
||||||
signupSourceSelfReported: true,
|
signupSourceSelfReported: true,
|
||||||
signupMedium: true,
|
signupMedium: true,
|
||||||
signupCampaign: true,
|
signupCampaign: true,
|
||||||
signupContent: true,
|
signupContent: true,
|
||||||
signupTerm: true,
|
signupTerm: true,
|
||||||
signupReferrer: true,
|
signupReferrer: true,
|
||||||
signupLandingPath: true,
|
signupLandingPath: true,
|
||||||
signupFirstSeenAt: true,
|
signupFirstSeenAt: true,
|
||||||
emailDomain: true,
|
emailDomain: true,
|
||||||
primaryUseCase: true,
|
primaryUseCase: true,
|
||||||
primaryGoal: true,
|
primaryGoal: true,
|
||||||
jobRole: true,
|
jobRole: true,
|
||||||
companyName: true,
|
companyName: true,
|
||||||
companyWebsite: true,
|
companyWebsite: true,
|
||||||
teamSizeBucket: true,
|
teamSizeBucket: true,
|
||||||
onboardingStartedAt: true,
|
onboardingStartedAt: true,
|
||||||
sourceConfirmedAt: true,
|
sourceConfirmedAt: true,
|
||||||
useCaseSelectedAt: true,
|
useCaseSelectedAt: true,
|
||||||
goalSelectedAt: true,
|
goalSelectedAt: true,
|
||||||
profileCompletedAt: true,
|
profileCompletedAt: true,
|
||||||
firstQrCreatedAt: true,
|
firstQrCreatedAt: true,
|
||||||
firstDynamicQrAt: true,
|
firstDynamicQrAt: true,
|
||||||
firstStaticQrAt: true,
|
firstStaticQrAt: true,
|
||||||
firstScanAt: true,
|
firstScanAt: true,
|
||||||
activationAt: true,
|
activationAt: true,
|
||||||
onboardingCompletedAt: true,
|
onboardingCompletedAt: true,
|
||||||
fitScore: true,
|
fitScore: true,
|
||||||
intentScore: true,
|
intentScore: true,
|
||||||
leadScore: true,
|
leadScore: true,
|
||||||
lifecycleStage: true,
|
lifecycleStage: true,
|
||||||
lastQualifiedAt: true,
|
lastQualifiedAt: true,
|
||||||
lastScoredAt: true,
|
lastScoredAt: true,
|
||||||
qrCodes: {
|
qrCodes: {
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
take: 50,
|
take: 50,
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
title: true,
|
title: true,
|
||||||
type: true,
|
type: true,
|
||||||
contentType: true,
|
contentType: true,
|
||||||
content: true,
|
content: true,
|
||||||
tags: true,
|
tags: true,
|
||||||
status: true,
|
status: true,
|
||||||
style: true,
|
style: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
scans: {
|
scans: {
|
||||||
orderBy: { ts: 'asc' },
|
orderBy: { ts: 'asc' },
|
||||||
take: 500,
|
take: 500,
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
ts: true,
|
ts: true,
|
||||||
device: true,
|
device: true,
|
||||||
os: true,
|
os: true,
|
||||||
country: true,
|
country: true,
|
||||||
referrer: true,
|
referrer: true,
|
||||||
utmSource: true,
|
utmSource: true,
|
||||||
utmMedium: true,
|
utmMedium: true,
|
||||||
utmCampaign: true,
|
utmCampaign: true,
|
||||||
isUnique: true,
|
isUnique: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
_count: {
|
_count: {
|
||||||
select: { scans: true },
|
select: { scans: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
lifecycleLogs: {
|
lifecycleLogs: {
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
take: 10,
|
take: 10,
|
||||||
select: {
|
select: {
|
||||||
fromStage: true,
|
fromStage: true,
|
||||||
toStage: true,
|
toStage: true,
|
||||||
fitScore: true,
|
fitScore: true,
|
||||||
intentScore: true,
|
intentScore: true,
|
||||||
leadScore: true,
|
leadScore: true,
|
||||||
reason: true,
|
reason: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
integrations: {
|
integrations: {
|
||||||
select: {
|
select: {
|
||||||
provider: true,
|
provider: true,
|
||||||
status: true,
|
status: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const leads = await db.lead.findMany({
|
const leads = await db.lead.findMany({
|
||||||
where: from ? { createdAt: { gte: from, lte: to } } : undefined,
|
where: from ? { createdAt: { gte: from, lte: to } } : undefined,
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
take: limit,
|
take: limit,
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
email: true,
|
email: true,
|
||||||
source: true,
|
source: true,
|
||||||
reprintCost: true,
|
reprintCost: true,
|
||||||
updatesPerYear: true,
|
updatesPerYear: true,
|
||||||
annualSavings: true,
|
annualSavings: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const newsletterSubscriptions = await db.newsletterSubscription.findMany({
|
const newsletterSubscriptions = await db.newsletterSubscription.findMany({
|
||||||
where: from ? { createdAt: { gte: from, lte: to } } : undefined,
|
where: from ? { createdAt: { gte: from, lte: to } } : undefined,
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
take: limit,
|
take: limit,
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
email: true,
|
email: true,
|
||||||
source: true,
|
source: true,
|
||||||
status: true,
|
status: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const exportedUsers = users.map((user) => {
|
const exportedUsers = users.map((user) => {
|
||||||
const metrics = getMetricSnapshot(user.qrCodes);
|
const metrics = getMetricSnapshot(user.qrCodes);
|
||||||
const allScans = user.qrCodes.flatMap((qr) => qr.scans).sort((a, b) => a.ts.getTime() - b.ts.getTime());
|
const allScans = user.qrCodes.flatMap((qr) => qr.scans).sort((a, b) => a.ts.getTime() - b.ts.getTime());
|
||||||
const rangeScans = allScans.filter((scan) => withinRange(scan.ts, from, to));
|
const rangeScans = allScans.filter((scan) => withinRange(scan.ts, from, to));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
emailDomain: user.emailDomain || getEmailDomain(user.email),
|
emailDomain: user.emailDomain || getEmailDomain(user.email),
|
||||||
image: user.image,
|
image: user.image,
|
||||||
emailVerified: jsonDate(user.emailVerified),
|
emailVerified: jsonDate(user.emailVerified),
|
||||||
createdAt: user.createdAt.toISOString(),
|
createdAt: user.createdAt.toISOString(),
|
||||||
updatedAt: user.updatedAt.toISOString(),
|
updatedAt: user.updatedAt.toISOString(),
|
||||||
plan: user.plan,
|
plan: user.plan,
|
||||||
stripe: {
|
stripe: {
|
||||||
hasCustomer: Boolean(user.stripeCustomerId),
|
hasCustomer: Boolean(user.stripeCustomerId),
|
||||||
hasSubscription: Boolean(user.stripeSubscriptionId),
|
hasSubscription: Boolean(user.stripeSubscriptionId),
|
||||||
priceId: user.stripePriceId,
|
priceId: user.stripePriceId,
|
||||||
currentPeriodEnd: jsonDate(user.stripeCurrentPeriodEnd),
|
currentPeriodEnd: jsonDate(user.stripeCurrentPeriodEnd),
|
||||||
},
|
},
|
||||||
attribution: {
|
attribution: {
|
||||||
signupSource: user.signupSource,
|
signupSource: user.signupSource,
|
||||||
signupSourceSelfReported: user.signupSourceSelfReported,
|
signupSourceSelfReported: user.signupSourceSelfReported,
|
||||||
signupMedium: user.signupMedium,
|
signupMedium: user.signupMedium,
|
||||||
signupCampaign: user.signupCampaign,
|
signupCampaign: user.signupCampaign,
|
||||||
signupContent: user.signupContent,
|
signupContent: user.signupContent,
|
||||||
signupTerm: user.signupTerm,
|
signupTerm: user.signupTerm,
|
||||||
signupReferrer: user.signupReferrer,
|
signupReferrer: user.signupReferrer,
|
||||||
signupLandingPath: user.signupLandingPath,
|
signupLandingPath: user.signupLandingPath,
|
||||||
signupFirstSeenAt: jsonDate(user.signupFirstSeenAt),
|
signupFirstSeenAt: jsonDate(user.signupFirstSeenAt),
|
||||||
},
|
},
|
||||||
onboarding: {
|
onboarding: {
|
||||||
primaryUseCase: user.primaryUseCase,
|
primaryUseCase: user.primaryUseCase,
|
||||||
primaryGoal: user.primaryGoal,
|
primaryGoal: user.primaryGoal,
|
||||||
jobRole: user.jobRole,
|
jobRole: user.jobRole,
|
||||||
companyName: user.companyName,
|
companyName: user.companyName,
|
||||||
companyWebsite: user.companyWebsite,
|
companyWebsite: user.companyWebsite,
|
||||||
teamSizeBucket: user.teamSizeBucket,
|
teamSizeBucket: user.teamSizeBucket,
|
||||||
onboardingStartedAt: jsonDate(user.onboardingStartedAt),
|
onboardingStartedAt: jsonDate(user.onboardingStartedAt),
|
||||||
sourceConfirmedAt: jsonDate(user.sourceConfirmedAt),
|
sourceConfirmedAt: jsonDate(user.sourceConfirmedAt),
|
||||||
useCaseSelectedAt: jsonDate(user.useCaseSelectedAt),
|
useCaseSelectedAt: jsonDate(user.useCaseSelectedAt),
|
||||||
goalSelectedAt: jsonDate(user.goalSelectedAt),
|
goalSelectedAt: jsonDate(user.goalSelectedAt),
|
||||||
profileCompletedAt: jsonDate(user.profileCompletedAt),
|
profileCompletedAt: jsonDate(user.profileCompletedAt),
|
||||||
onboardingCompletedAt: jsonDate(user.onboardingCompletedAt),
|
onboardingCompletedAt: jsonDate(user.onboardingCompletedAt),
|
||||||
},
|
},
|
||||||
lifecycle: {
|
lifecycle: {
|
||||||
fitScore: user.fitScore,
|
fitScore: user.fitScore,
|
||||||
intentScore: user.intentScore,
|
intentScore: user.intentScore,
|
||||||
leadScore: user.leadScore,
|
leadScore: user.leadScore,
|
||||||
lifecycleStage: user.lifecycleStage,
|
lifecycleStage: user.lifecycleStage,
|
||||||
firstQrCreatedAt: jsonDate(user.firstQrCreatedAt),
|
firstQrCreatedAt: jsonDate(user.firstQrCreatedAt),
|
||||||
firstDynamicQrAt: jsonDate(user.firstDynamicQrAt),
|
firstDynamicQrAt: jsonDate(user.firstDynamicQrAt),
|
||||||
firstStaticQrAt: jsonDate(user.firstStaticQrAt),
|
firstStaticQrAt: jsonDate(user.firstStaticQrAt),
|
||||||
firstScanAt: jsonDate(user.firstScanAt),
|
firstScanAt: jsonDate(user.firstScanAt),
|
||||||
activationAt: jsonDate(user.activationAt),
|
activationAt: jsonDate(user.activationAt),
|
||||||
lastQualifiedAt: jsonDate(user.lastQualifiedAt),
|
lastQualifiedAt: jsonDate(user.lastQualifiedAt),
|
||||||
lastScoredAt: jsonDate(user.lastScoredAt),
|
lastScoredAt: jsonDate(user.lastScoredAt),
|
||||||
upgradeBadges: getUpgradeCandidateBadges(user, metrics),
|
upgradeBadges: getUpgradeCandidateBadges(user, metrics),
|
||||||
logs: user.lifecycleLogs.map((log) => ({
|
logs: user.lifecycleLogs.map((log) => ({
|
||||||
...log,
|
...log,
|
||||||
createdAt: log.createdAt.toISOString(),
|
createdAt: log.createdAt.toISOString(),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
stats: {
|
stats: {
|
||||||
qrCount: metrics.qrCount,
|
qrCount: metrics.qrCount,
|
||||||
dynamicQrCount: metrics.dynamicQrCount,
|
dynamicQrCount: metrics.dynamicQrCount,
|
||||||
staticQrCount: metrics.qrCount - metrics.dynamicQrCount,
|
staticQrCount: metrics.qrCount - metrics.dynamicQrCount,
|
||||||
contentTypeCount: metrics.contentTypeCount,
|
contentTypeCount: metrics.contentTypeCount,
|
||||||
businessishTypeCount: metrics.businessishTypeCount,
|
businessishTypeCount: metrics.businessishTypeCount,
|
||||||
scanCount: metrics.scanCount,
|
scanCount: metrics.scanCount,
|
||||||
scansInRange: rangeScans.length,
|
scansInRange: rangeScans.length,
|
||||||
uniqueScansInRange: rangeScans.filter((scan) => scan.isUnique).length,
|
uniqueScansInRange: rangeScans.filter((scan) => scan.isUnique).length,
|
||||||
},
|
},
|
||||||
scans: summarizeScans(allScans),
|
scans: summarizeScans(allScans),
|
||||||
scansInRange: summarizeScans(rangeScans),
|
scansInRange: summarizeScans(rangeScans),
|
||||||
qrCodes: user.qrCodes.map((qr) => {
|
qrCodes: user.qrCodes.map((qr) => {
|
||||||
const qrScansInRange = qr.scans.filter((scan) => withinRange(scan.ts, from, to));
|
const qrScansInRange = qr.scans.filter((scan) => withinRange(scan.ts, from, to));
|
||||||
return {
|
return {
|
||||||
id: qr.id,
|
id: qr.id,
|
||||||
title: qr.title,
|
title: qr.title,
|
||||||
type: qr.type,
|
type: qr.type,
|
||||||
contentType: qr.contentType,
|
contentType: qr.contentType,
|
||||||
content: safeJson(qr.content),
|
content: safeJson(qr.content),
|
||||||
tags: qr.tags,
|
tags: qr.tags,
|
||||||
status: qr.status,
|
status: qr.status,
|
||||||
style: safeJson(qr.style),
|
style: safeJson(qr.style),
|
||||||
slug: qr.slug,
|
slug: qr.slug,
|
||||||
publicPath: `/r/${qr.slug}`,
|
publicPath: `/r/${qr.slug}`,
|
||||||
createdAt: qr.createdAt.toISOString(),
|
createdAt: qr.createdAt.toISOString(),
|
||||||
updatedAt: qr.updatedAt.toISOString(),
|
updatedAt: qr.updatedAt.toISOString(),
|
||||||
scanCount: qr._count.scans,
|
scanCount: qr._count.scans,
|
||||||
scansInRange: qrScansInRange.length,
|
scansInRange: qrScansInRange.length,
|
||||||
firstScanAt: jsonDate(qr.scans[0]?.ts),
|
firstScanAt: jsonDate(qr.scans[0]?.ts),
|
||||||
lastScanAt: jsonDate(qr.scans[qr.scans.length - 1]?.ts),
|
lastScanAt: jsonDate(qr.scans[qr.scans.length - 1]?.ts),
|
||||||
scanSummary: summarizeScans(qr.scans),
|
scanSummary: summarizeScans(qr.scans),
|
||||||
scanSummaryInRange: summarizeScans(qrScansInRange),
|
scanSummaryInRange: summarizeScans(qrScansInRange),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
integrations: user.integrations.map((integration) => ({
|
integrations: user.integrations.map((integration) => ({
|
||||||
...integration,
|
...integration,
|
||||||
createdAt: integration.createdAt.toISOString(),
|
createdAt: integration.createdAt.toISOString(),
|
||||||
updatedAt: integration.updatedAt.toISOString(),
|
updatedAt: integration.updatedAt.toISOString(),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const allQrs = exportedUsers.flatMap((user) => user.qrCodes.map((qr) => ({ userId: user.id, ...qr })));
|
const allQrs = exportedUsers.flatMap((user) => user.qrCodes.map((qr) => ({ userId: user.id, ...qr })));
|
||||||
const qrsInRange = allQrs.filter((qr) => withinRange(new Date(qr.createdAt), from, to));
|
const qrsInRange = allQrs.filter((qr) => withinRange(new Date(qr.createdAt), from, to));
|
||||||
const scansInRange = exportedUsers.reduce((sum, user) => sum + user.stats.scansInRange, 0);
|
const scansInRange = exportedUsers.reduce((sum, user) => sum + user.stats.scansInRange, 0);
|
||||||
const uniqueScansInRange = exportedUsers.reduce((sum, user) => sum + user.stats.uniqueScansInRange, 0);
|
const uniqueScansInRange = exportedUsers.reduce((sum, user) => sum + user.stats.uniqueScansInRange, 0);
|
||||||
|
|
||||||
const contentTypeCounts = new Map<string, number>();
|
const contentTypeCounts = new Map<string, number>();
|
||||||
for (const qr of allQrs) {
|
for (const qr of allQrs) {
|
||||||
contentTypeCounts.set(qr.contentType, (contentTypeCounts.get(qr.contentType) || 0) + 1);
|
contentTypeCounts.set(qr.contentType, (contentTypeCounts.get(qr.contentType) || 0) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
generatedAt: new Date().toISOString(),
|
generatedAt: new Date().toISOString(),
|
||||||
endpoint: '/api/internal/revops-export',
|
endpoint: '/api/internal/revops-export',
|
||||||
range: {
|
range: {
|
||||||
preset,
|
preset,
|
||||||
from: jsonDate(from),
|
from: jsonDate(from),
|
||||||
to: to.toISOString(),
|
to: to.toISOString(),
|
||||||
},
|
},
|
||||||
limits: {
|
limits: {
|
||||||
requestedUserLimit: limit,
|
requestedUserLimit: limit,
|
||||||
maxUserLimit: MAX_LIMIT,
|
maxUserLimit: MAX_LIMIT,
|
||||||
qrLimitPerUser: 50,
|
qrLimitPerUser: 50,
|
||||||
scanLimitPerQr: 500,
|
scanLimitPerQr: 500,
|
||||||
},
|
},
|
||||||
overview: {
|
overview: {
|
||||||
exportedUsers: exportedUsers.length,
|
exportedUsers: exportedUsers.length,
|
||||||
newUsersInRange: exportedUsers.filter((user) => withinRange(new Date(user.createdAt), from, to)).length,
|
newUsersInRange: exportedUsers.filter((user) => withinRange(new Date(user.createdAt), from, to)).length,
|
||||||
activatedUsersInRange: exportedUsers.filter((user) => withinRange(user.lifecycle.activationAt ? new Date(user.lifecycle.activationAt) : null, from, to)).length,
|
activatedUsersInRange: exportedUsers.filter((user) => withinRange(user.lifecycle.activationAt ? new Date(user.lifecycle.activationAt) : null, from, to)).length,
|
||||||
paidUsers: exportedUsers.filter((user) => user.plan === 'PRO' || user.plan === 'BUSINESS').length,
|
paidUsers: exportedUsers.filter((user) => user.plan === 'PRO' || user.plan === 'BUSINESS').length,
|
||||||
hotUsers: exportedUsers.filter((user) => user.lifecycle.lifecycleStage === 'hot').length,
|
hotUsers: exportedUsers.filter((user) => user.lifecycle.lifecycleStage === 'hot').length,
|
||||||
upgradeCandidates: exportedUsers.filter((user) => user.lifecycle.lifecycleStage === 'upgrade_candidate').length,
|
upgradeCandidates: exportedUsers.filter((user) => user.lifecycle.lifecycleStage === 'upgrade_candidate').length,
|
||||||
qrCodesTotal: allQrs.length,
|
qrCodesTotal: allQrs.length,
|
||||||
qrCodesCreatedInRange: qrsInRange.length,
|
qrCodesCreatedInRange: qrsInRange.length,
|
||||||
scansInRange,
|
scansInRange,
|
||||||
uniqueScansInRange,
|
uniqueScansInRange,
|
||||||
reprintCalculatorLeadsInRange: leads.length,
|
reprintCalculatorLeadsInRange: leads.length,
|
||||||
newsletterSubscriptionsInRange: newsletterSubscriptions.length,
|
newsletterSubscriptionsInRange: newsletterSubscriptions.length,
|
||||||
},
|
},
|
||||||
breakdowns: {
|
breakdowns: {
|
||||||
qrContentTypes: Array.from(contentTypeCounts.entries())
|
qrContentTypes: Array.from(contentTypeCounts.entries())
|
||||||
.sort((a, b) => b[1] - a[1])
|
.sort((a, b) => b[1] - a[1])
|
||||||
.map(([contentType, count]) => ({ contentType, count })),
|
.map(([contentType, count]) => ({ contentType, count })),
|
||||||
},
|
},
|
||||||
users: exportedUsers,
|
users: exportedUsers,
|
||||||
leads: leads.map((lead) => ({
|
leads: leads.map((lead) => ({
|
||||||
...lead,
|
...lead,
|
||||||
createdAt: lead.createdAt.toISOString(),
|
createdAt: lead.createdAt.toISOString(),
|
||||||
updatedAt: lead.updatedAt.toISOString(),
|
updatedAt: lead.updatedAt.toISOString(),
|
||||||
})),
|
})),
|
||||||
newsletterSubscriptions: newsletterSubscriptions.map((subscription) => ({
|
newsletterSubscriptions: newsletterSubscriptions.map((subscription) => ({
|
||||||
...subscription,
|
...subscription,
|
||||||
createdAt: subscription.createdAt.toISOString(),
|
createdAt: subscription.createdAt.toISOString(),
|
||||||
updatedAt: subscription.updatedAt.toISOString(),
|
updatedAt: subscription.updatedAt.toISOString(),
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error exporting internal RevOps data:', error);
|
console.error('Error exporting internal RevOps data:', error);
|
||||||
return NextResponse.json({ error: 'Failed to export RevOps data' }, { status: 500 });
|
return NextResponse.json({ error: 'Failed to export RevOps data' }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
92
src/app/(main)/api/tiktok/callback/route.ts
Normal file
92
src/app/(main)/api/tiktok/callback/route.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { db } from '@/lib/db';
|
||||||
|
import { TIKTOK_ACCOUNT_KEY, TIKTOK_OAUTH_STATE_COOKIE_NAME } from '@/lib/tiktok';
|
||||||
|
|
||||||
|
const textResponse = (body: string, status: number) => {
|
||||||
|
const response = new NextResponse(body, {
|
||||||
|
status,
|
||||||
|
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
||||||
|
});
|
||||||
|
response.cookies.delete(TIKTOK_OAUTH_STATE_COOKIE_NAME);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const code = searchParams.get('code');
|
||||||
|
const state = searchParams.get('state');
|
||||||
|
const error = searchParams.get('error');
|
||||||
|
const errorDescription = searchParams.get('error_description');
|
||||||
|
const savedState = request.cookies.get(TIKTOK_OAUTH_STATE_COOKIE_NAME)?.value;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return textResponse(`TikTok authorization failed: ${errorDescription || error}`, 400);
|
||||||
|
}
|
||||||
|
if (!code) {
|
||||||
|
return textResponse('Missing authorization code.', 400);
|
||||||
|
}
|
||||||
|
if (!state || !savedState || state !== savedState) {
|
||||||
|
return textResponse('Invalid OAuth state. Start over at /api/tiktok/connect.', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientKey = process.env.TIKTOK_CLIENT_KEY;
|
||||||
|
const clientSecret = process.env.TIKTOK_CLIENT_SECRET;
|
||||||
|
if (!clientKey || !clientSecret) {
|
||||||
|
return textResponse('TikTok client credentials are not configured.', 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectUri =
|
||||||
|
process.env.TIKTOK_REDIRECT_URI || `${process.env.NEXT_PUBLIC_APP_URL}/api/tiktok/callback`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokenResponse = await fetch('https://open.tiktokapis.com/v2/oauth/token/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_key: clientKey,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
code,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokens = await tokenResponse.json();
|
||||||
|
if (!tokenResponse.ok || tokens.error) {
|
||||||
|
throw new Error(tokens.error_description || tokens.error || 'TikTok token exchange failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const accessTokenExpiresAt = new Date(now + Number(tokens.expires_in || 0) * 1000);
|
||||||
|
const refreshTokenExpiresAt = tokens.refresh_expires_in
|
||||||
|
? new Date(now + Number(tokens.refresh_expires_in) * 1000)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
await db.tiktokIntegration.upsert({
|
||||||
|
where: { accountKey: TIKTOK_ACCOUNT_KEY },
|
||||||
|
create: {
|
||||||
|
accountKey: TIKTOK_ACCOUNT_KEY,
|
||||||
|
openId: tokens.open_id,
|
||||||
|
accessToken: tokens.access_token,
|
||||||
|
refreshToken: tokens.refresh_token,
|
||||||
|
scope: tokens.scope || null,
|
||||||
|
accessTokenExpiresAt,
|
||||||
|
refreshTokenExpiresAt,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
openId: tokens.open_id,
|
||||||
|
accessToken: tokens.access_token,
|
||||||
|
refreshToken: tokens.refresh_token,
|
||||||
|
scope: tokens.scope || null,
|
||||||
|
accessTokenExpiresAt,
|
||||||
|
refreshTokenExpiresAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return textResponse('TikTok account connected. You can close this tab.', 200);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('TikTok callback error:', err);
|
||||||
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||||
|
return textResponse(`Failed to connect TikTok account: ${message}`, 502);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/app/(main)/api/tiktok/connect/route.ts
Normal file
41
src/app/(main)/api/tiktok/connect/route.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { TIKTOK_OAUTH_STATE_COOKIE_NAME } from '@/lib/tiktok';
|
||||||
|
|
||||||
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const adminKey = process.env.TIKTOK_ADMIN_KEY;
|
||||||
|
if (adminKey) {
|
||||||
|
const provided = request.nextUrl.searchParams.get('key');
|
||||||
|
if (provided !== adminKey) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientKey = process.env.TIKTOK_CLIENT_KEY;
|
||||||
|
if (!clientKey) {
|
||||||
|
return NextResponse.json({ error: 'TIKTOK_CLIENT_KEY not configured' }, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectUri =
|
||||||
|
process.env.TIKTOK_REDIRECT_URI || `${process.env.NEXT_PUBLIC_APP_URL}/api/tiktok/callback`;
|
||||||
|
|
||||||
|
const oauthState = crypto.randomUUID();
|
||||||
|
|
||||||
|
const authUrl = new URL('https://www.tiktok.com/v2/auth/authorize/');
|
||||||
|
authUrl.searchParams.set('client_key', clientKey);
|
||||||
|
authUrl.searchParams.set('scope', 'user.info.basic,video.publish');
|
||||||
|
authUrl.searchParams.set('response_type', 'code');
|
||||||
|
authUrl.searchParams.set('redirect_uri', redirectUri);
|
||||||
|
authUrl.searchParams.set('state', oauthState);
|
||||||
|
|
||||||
|
const response = NextResponse.redirect(authUrl);
|
||||||
|
response.cookies.set(TIKTOK_OAUTH_STATE_COOKIE_NAME, oauthState, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: isProduction,
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
maxAge: 60 * 10,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
41
src/app/(main)/api/tiktok/token/route.ts
Normal file
41
src/app/(main)/api/tiktok/token/route.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { getValidTiktokTokens } from '@/lib/tiktok';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const adminKey = process.env.TIKTOK_ADMIN_KEY;
|
||||||
|
if (!adminKey) {
|
||||||
|
// Unlike /connect, this endpoint hands out live credentials — never expose
|
||||||
|
// it without a configured key.
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'TIKTOK_ADMIN_KEY must be configured to expose tokens' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const provided =
|
||||||
|
request.headers.get('x-admin-key') || request.nextUrl.searchParams.get('key');
|
||||||
|
if (provided !== adminKey) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokens = await getValidTiktokTokens();
|
||||||
|
if (!tokens) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'No TikTok account connected. Visit /api/tiktok/connect first.' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
access_token: tokens.accessToken,
|
||||||
|
open_id: tokens.openId,
|
||||||
|
scope: tokens.scope,
|
||||||
|
expires_at: tokens.accessTokenExpiresAt.toISOString(),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('TikTok token endpoint error:', err);
|
||||||
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||||
|
return NextResponse.json({ error: message }, { status: 502 });
|
||||||
|
}
|
||||||
|
}
|
||||||
884
src/i18n/de.json
884
src/i18n/de.json
@@ -1,442 +1,442 @@
|
|||||||
{
|
{
|
||||||
"nav": {
|
"nav": {
|
||||||
"features": "Funktionen",
|
"features": "Funktionen",
|
||||||
"pricing": "Preise",
|
"pricing": "Preise",
|
||||||
"faq": "FAQ",
|
"faq": "FAQ",
|
||||||
"blog": "Blog",
|
"blog": "Blog",
|
||||||
"login": "Anmelden",
|
"login": "Anmelden",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"about": "Über uns",
|
"about": "Über uns",
|
||||||
"contact": "Kontakt",
|
"contact": "Kontakt",
|
||||||
"signup": "Registrieren",
|
"signup": "Registrieren",
|
||||||
"learn": "Lernen",
|
"learn": "Lernen",
|
||||||
"create_qr": "QR erstellen",
|
"create_qr": "QR erstellen",
|
||||||
"bulk_creation": "Massen-Erstellung",
|
"bulk_creation": "Massen-Erstellung",
|
||||||
"analytics": "Analytik",
|
"analytics": "Analytik",
|
||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
"cta": "Kostenlos starten",
|
"cta": "Kostenlos starten",
|
||||||
"tools": "Kostenlose Tools",
|
"tools": "Kostenlose Tools",
|
||||||
"all_free": "Alle Generatoren sind 100% kostenlos",
|
"all_free": "Alle Generatoren sind 100% kostenlos",
|
||||||
"resources": "Ressourcen",
|
"resources": "Ressourcen",
|
||||||
"all_industries": "Branchen"
|
"all_industries": "Branchen"
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "Kostenloser QR-Code-Generator",
|
"badge": "Kostenloser QR-Code-Generator",
|
||||||
"title": "Erstellen Sie QR-Codes, die überall funktionieren",
|
"title": "Erstellen Sie QR-Codes, die überall funktionieren",
|
||||||
"subtitle": "Generieren Sie statische und dynamische QR-Codes mit Tracking, individuellem Branding und Massen-Erstellung. Kostenlos für immer.",
|
"subtitle": "Generieren Sie statische und dynamische QR-Codes mit Tracking, individuellem Branding und Massen-Erstellung. Kostenlos für immer.",
|
||||||
"features": [
|
"features": [
|
||||||
"Keine Kreditkarte zum Starten erforderlich",
|
"Keine Kreditkarte zum Starten erforderlich",
|
||||||
"QR-Codes für immer kostenlos erstellen",
|
"QR-Codes für immer kostenlos erstellen",
|
||||||
"Erweiterte Verfolgung und Analytik",
|
"Erweiterte Verfolgung und Analytik",
|
||||||
"Individuelle Farben und Stile"
|
"Individuelle Farben und Stile"
|
||||||
],
|
],
|
||||||
"cta_primary": "QR-Code kostenlos erstellen",
|
"cta_primary": "QR-Code kostenlos erstellen",
|
||||||
"cta_secondary": "Preise ansehen",
|
"cta_secondary": "Preise ansehen",
|
||||||
"engagement_badge": "Kostenlos für immer",
|
"engagement_badge": "Kostenlos für immer",
|
||||||
"get_started": "Loslegen",
|
"get_started": "Loslegen",
|
||||||
"view_full_pricing": "Alle Preisdetails ansehen →"
|
"view_full_pricing": "Alle Preisdetails ansehen →"
|
||||||
},
|
},
|
||||||
"trust": {
|
"trust": {
|
||||||
"users": "Aktive Nutzer",
|
"users": "Aktive Nutzer",
|
||||||
"codes": "QR-Codes erstellt",
|
"codes": "QR-Codes erstellt",
|
||||||
"scans": "Scans verfolgt",
|
"scans": "Scans verfolgt",
|
||||||
"countries": "Länder"
|
"countries": "Länder"
|
||||||
},
|
},
|
||||||
"industries": {
|
"industries": {
|
||||||
"restaurant": "Restaurant-Kette",
|
"restaurant": "Restaurant-Kette",
|
||||||
"tech": "Tech-Startup",
|
"tech": "Tech-Startup",
|
||||||
"realestate": "Immobilien",
|
"realestate": "Immobilien",
|
||||||
"events": "Event-Agentur",
|
"events": "Event-Agentur",
|
||||||
"retail": "Einzelhandel",
|
"retail": "Einzelhandel",
|
||||||
"healthcare": "Gesundheitswesen"
|
"healthcare": "Gesundheitswesen"
|
||||||
},
|
},
|
||||||
"templates": {
|
"templates": {
|
||||||
"title": "Mit einer Vorlage beginnen",
|
"title": "Mit einer Vorlage beginnen",
|
||||||
"restaurant": "Restaurant-Menü",
|
"restaurant": "Restaurant-Menü",
|
||||||
"business": "Visitenkarte",
|
"business": "Visitenkarte",
|
||||||
"vcard": "Kontaktkarte",
|
"vcard": "Kontaktkarte",
|
||||||
"event": "Event-Ticket",
|
"event": "Event-Ticket",
|
||||||
"use_template": "Vorlage verwenden →"
|
"use_template": "Vorlage verwenden →"
|
||||||
},
|
},
|
||||||
"generator": {
|
"generator": {
|
||||||
"title": "Sofortiger QR-Code-Generator",
|
"title": "Sofortiger QR-Code-Generator",
|
||||||
"url_placeholder": "Geben Sie hier Ihre URL ein...",
|
"url_placeholder": "Geben Sie hier Ihre URL ein...",
|
||||||
"foreground": "Vordergrund",
|
"foreground": "Vordergrund",
|
||||||
"background": "Hintergrund",
|
"background": "Hintergrund",
|
||||||
"corners": "Ecken",
|
"corners": "Ecken",
|
||||||
"size": "Größe",
|
"size": "Größe",
|
||||||
"contrast_good": "Guter Kontrast",
|
"contrast_good": "Guter Kontrast",
|
||||||
"download_svg": "SVG herunterladen",
|
"download_svg": "SVG herunterladen",
|
||||||
"download_png": "PNG herunterladen",
|
"download_png": "PNG herunterladen",
|
||||||
"save_track": "Speichern & Verfolgen",
|
"save_track": "Speichern & Verfolgen",
|
||||||
"live_preview": "Live-Vorschau",
|
"live_preview": "Live-Vorschau",
|
||||||
"demo_note": "Dies ist ein Demo-QR-Code"
|
"demo_note": "Dies ist ein Demo-QR-Code"
|
||||||
},
|
},
|
||||||
"static_vs_dynamic": {
|
"static_vs_dynamic": {
|
||||||
"title": "Warum dynamische QR-Codes Ihnen Geld sparen",
|
"title": "Warum dynamische QR-Codes Ihnen Geld sparen",
|
||||||
"description": "Hören Sie auf, Materialien neu zu drucken. Wechseln Sie Ziele sofort und verfolgen Sie jeden Scan.",
|
"description": "Hören Sie auf, Materialien neu zu drucken. Wechseln Sie Ziele sofort und verfolgen Sie jeden Scan.",
|
||||||
"static": {
|
"static": {
|
||||||
"title": "Statische QR-Codes",
|
"title": "Statische QR-Codes",
|
||||||
"subtitle": "Immer kostenlos",
|
"subtitle": "Immer kostenlos",
|
||||||
"description": "Perfekt für permanente Inhalte, die sich nie ändern",
|
"description": "Perfekt für permanente Inhalte, die sich nie ändern",
|
||||||
"features": [
|
"features": [
|
||||||
"Inhalt kann nicht bearbeitet werden",
|
"Inhalt kann nicht bearbeitet werden",
|
||||||
"Keine Scan-Verfolgung",
|
"Keine Scan-Verfolgung",
|
||||||
"Funktioniert für immer",
|
"Funktioniert für immer",
|
||||||
"Kein Konto erforderlich"
|
"Kein Konto erforderlich"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dynamic": {
|
"dynamic": {
|
||||||
"title": "Dynamische QR-Codes",
|
"title": "Dynamische QR-Codes",
|
||||||
"subtitle": "Empfohlen",
|
"subtitle": "Empfohlen",
|
||||||
"description": "Volle Kontrolle mit Tracking- und Bearbeitungsfunktionen",
|
"description": "Volle Kontrolle mit Tracking- und Bearbeitungsfunktionen",
|
||||||
"features": [
|
"features": [
|
||||||
"Inhalt jederzeit bearbeiten",
|
"Inhalt jederzeit bearbeiten",
|
||||||
"Erweiterte Analytik",
|
"Erweiterte Analytik",
|
||||||
"Individuelles Branding",
|
"Individuelles Branding",
|
||||||
"Bulk-Operationen"
|
"Bulk-Operationen"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
"title": "Alles was Sie brauchen, um professionelle QR-Codes zu erstellen",
|
"title": "Alles was Sie brauchen, um professionelle QR-Codes zu erstellen",
|
||||||
"analytics": {
|
"analytics": {
|
||||||
"title": "Erweiterte Analytik",
|
"title": "Erweiterte Analytik",
|
||||||
"description": "Verfolgen Sie Scans, Standorte, Geräte und Nutzerverhalten mit detaillierten Einblicken."
|
"description": "Verfolgen Sie Scans, Standorte, Geräte und Nutzerverhalten mit detaillierten Einblicken."
|
||||||
},
|
},
|
||||||
"customization": {
|
"customization": {
|
||||||
"title": "Vollständige Anpassung",
|
"title": "Vollständige Anpassung",
|
||||||
"description": "Branden Sie Ihre QR-Codes mit individuellen Farben, Logos und Styling-Optionen."
|
"description": "Branden Sie Ihre QR-Codes mit individuellen Farben, Logos und Styling-Optionen."
|
||||||
},
|
},
|
||||||
"unlimited": {
|
"unlimited": {
|
||||||
"title": "Unbegrenzte statische QR-Codes",
|
"title": "Unbegrenzte statische QR-Codes",
|
||||||
"description": "Erstellen Sie so viele statische QR-Codes wie Sie benötigen. Kostenlos für immer, ohne Limits."
|
"description": "Erstellen Sie so viele statische QR-Codes wie Sie benötigen. Kostenlos für immer, ohne Limits."
|
||||||
},
|
},
|
||||||
"bulk": {
|
"bulk": {
|
||||||
"title": "Bulk-Operationen",
|
"title": "Bulk-Operationen",
|
||||||
"description": "Erstellen Sie hunderte von QR-Codes auf einmal mit CSV-Import und Batch-Verarbeitung."
|
"description": "Erstellen Sie hunderte von QR-Codes auf einmal mit CSV-Import und Batch-Verarbeitung."
|
||||||
},
|
},
|
||||||
"integrations": {
|
"integrations": {
|
||||||
"title": "Integrationen",
|
"title": "Integrationen",
|
||||||
"description": "Verbinden Sie sich mit Zapier, Airtable, Google Sheets und weiteren beliebten Tools."
|
"description": "Verbinden Sie sich mit Zapier, Airtable, Google Sheets und weiteren beliebten Tools."
|
||||||
},
|
},
|
||||||
"api": {
|
"api": {
|
||||||
"title": "Entwickler-API",
|
"title": "Entwickler-API",
|
||||||
"description": "Integrieren Sie QR-Code-Generierung in Ihre Anwendungen mit unserer REST-API."
|
"description": "Integrieren Sie QR-Code-Generierung in Ihre Anwendungen mit unserer REST-API."
|
||||||
},
|
},
|
||||||
"support": {
|
"support": {
|
||||||
"title": "24/7 Support",
|
"title": "24/7 Support",
|
||||||
"description": "Erhalten Sie Hilfe, wenn Sie sie brauchen, mit unserem dedizierten Kundensupport-Team."
|
"description": "Erhalten Sie Hilfe, wenn Sie sie brauchen, mit unserem dedizierten Kundensupport-Team."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pricing": {
|
"pricing": {
|
||||||
"title": "Wählen Sie Ihren Plan",
|
"title": "Wählen Sie Ihren Plan",
|
||||||
"subtitle": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
|
"subtitle": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
|
||||||
"choose_plan": "Wählen Sie Ihren Plan",
|
"choose_plan": "Wählen Sie Ihren Plan",
|
||||||
"select_plan": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
|
"select_plan": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
|
||||||
"current_plan": "Aktueller Plan",
|
"current_plan": "Aktueller Plan",
|
||||||
"upgrade_to": "Upgrade auf",
|
"upgrade_to": "Upgrade auf",
|
||||||
"downgrade_to_free": "Zu Kostenlos zurückstufen",
|
"downgrade_to_free": "Zu Kostenlos zurückstufen",
|
||||||
"most_popular": "Beliebteste",
|
"most_popular": "Beliebteste",
|
||||||
"all_plans_note": "Alle Pläne beinhalten unbegrenzte statische QR-Codes und Basis-Anpassung.",
|
"all_plans_note": "Alle Pläne beinhalten unbegrenzte statische QR-Codes und Basis-Anpassung.",
|
||||||
"free": {
|
"free": {
|
||||||
"title": "Kostenlos",
|
"title": "Kostenlos",
|
||||||
"name": "Free",
|
"name": "Free",
|
||||||
"price": "€0",
|
"price": "€0",
|
||||||
"period": "für immer",
|
"period": "für immer",
|
||||||
"features": [
|
"features": [
|
||||||
"3 aktive dynamische QR-Codes (8 Typen verfügbar)",
|
"3 aktive dynamische QR-Codes (8 Typen verfügbar)",
|
||||||
"Unbegrenzte statische QR-Codes",
|
"Unbegrenzte statische QR-Codes",
|
||||||
"Basis-Scan-Tracking",
|
"Basis-Scan-Tracking",
|
||||||
"Standard QR-Design-Vorlagen"
|
"Standard QR-Design-Vorlagen"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"pro": {
|
"pro": {
|
||||||
"title": "Pro",
|
"title": "Pro",
|
||||||
"name": "Pro",
|
"name": "Pro",
|
||||||
"price": "€9",
|
"price": "€9",
|
||||||
"period": "pro Monat",
|
"period": "pro Monat",
|
||||||
"badge": "Beliebteste",
|
"badge": "Beliebteste",
|
||||||
"features": [
|
"features": [
|
||||||
"50 dynamische QR-Codes",
|
"50 dynamische QR-Codes",
|
||||||
"Unbegrenzte statische QR-Codes",
|
"Unbegrenzte statische QR-Codes",
|
||||||
"Erweiterte Analytik (Scans, Geräte, Standorte)",
|
"Erweiterte Analytik (Scans, Geräte, Standorte)",
|
||||||
"Individuelles Branding (Farben)",
|
"Individuelles Branding (Farben)",
|
||||||
"Download als SVG/PNG"
|
"Download als SVG/PNG"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"business": {
|
"business": {
|
||||||
"title": "Business",
|
"title": "Business",
|
||||||
"name": "Business",
|
"name": "Business",
|
||||||
"price": "€29",
|
"price": "€29",
|
||||||
"period": "pro Monat",
|
"period": "pro Monat",
|
||||||
"features": [
|
"features": [
|
||||||
"500 dynamische QR-Codes",
|
"500 dynamische QR-Codes",
|
||||||
"Unbegrenzte statische QR-Codes",
|
"Unbegrenzte statische QR-Codes",
|
||||||
"Alles aus Pro",
|
"Alles aus Pro",
|
||||||
"Massen-QR-Erstellung (bis zu 1.000)",
|
"Massen-QR-Erstellung (bis zu 1.000)",
|
||||||
"Prioritäts-E-Mail-Support",
|
"Prioritäts-E-Mail-Support",
|
||||||
"Erweiterte Tracking & Insights"
|
"Erweiterte Tracking & Insights"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"enterprise": {
|
"enterprise": {
|
||||||
"title": "Enterprise",
|
"title": "Enterprise",
|
||||||
"name": "Enterprise",
|
"name": "Enterprise",
|
||||||
"price": "Individuell",
|
"price": "Individuell",
|
||||||
"period": "",
|
"period": "",
|
||||||
"features": [
|
"features": [
|
||||||
"∞ dynamische QR-Codes",
|
"∞ dynamische QR-Codes",
|
||||||
"Unbegrenzte statische QR-Codes",
|
"Unbegrenzte statische QR-Codes",
|
||||||
"Alles aus Business",
|
"Alles aus Business",
|
||||||
"Eigener Account Manager"
|
"Eigener Account Manager"
|
||||||
],
|
],
|
||||||
"contact": "Kontakt aufnehmen"
|
"contact": "Kontakt aufnehmen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"faq": {
|
"faq": {
|
||||||
"title": "Häufig gestellte Fragen",
|
"title": "Häufig gestellte Fragen",
|
||||||
"questions": {
|
"questions": {
|
||||||
"account": {
|
"account": {
|
||||||
"question": "Benötige ich ein Konto, um QR-Codes zu erstellen?",
|
"question": "Benötige ich ein Konto, um QR-Codes zu erstellen?",
|
||||||
"answer": "Für statische QR-Codes ist kein Konto erforderlich. Dynamische QR-Codes mit Tracking- und Bearbeitungsfunktionen erfordern jedoch ein kostenloses Konto."
|
"answer": "Für statische QR-Codes ist kein Konto erforderlich. Dynamische QR-Codes mit Tracking- und Bearbeitungsfunktionen erfordern jedoch ein kostenloses Konto."
|
||||||
},
|
},
|
||||||
"static_vs_dynamic": {
|
"static_vs_dynamic": {
|
||||||
"question": "Was ist der Unterschied zwischen statischen und dynamischen QR-Codes?",
|
"question": "Was ist der Unterschied zwischen statischen und dynamischen QR-Codes?",
|
||||||
"answer": "Statische QR-Codes enthalten feste Inhalte, die nicht geändert werden können. Dynamische QR-Codes können jederzeit bearbeitet werden und bieten detaillierte Analytik."
|
"answer": "Statische QR-Codes enthalten feste Inhalte, die nicht geändert werden können. Dynamische QR-Codes können jederzeit bearbeitet werden und bieten detaillierte Analytik."
|
||||||
},
|
},
|
||||||
"forever": {
|
"forever": {
|
||||||
"question": "Funktionieren meine QR-Codes für immer?",
|
"question": "Funktionieren meine QR-Codes für immer?",
|
||||||
"answer": "Statische QR-Codes funktionieren für immer, da der Inhalt direkt eingebettet ist. Dynamische QR-Codes funktionieren, solange Ihr Konto aktiv ist."
|
"answer": "Statische QR-Codes funktionieren für immer, da der Inhalt direkt eingebettet ist. Dynamische QR-Codes funktionieren, solange Ihr Konto aktiv ist."
|
||||||
},
|
},
|
||||||
"file_type": {
|
"file_type": {
|
||||||
"question": "Welchen Dateityp sollte ich zum Drucken verwenden?",
|
"question": "Welchen Dateityp sollte ich zum Drucken verwenden?",
|
||||||
"answer": "Für Druckmaterialien empfehlen wir das SVG-Format für Skalierbarkeit oder hochauflösendes PNG (300+ DPI) für beste Qualität."
|
"answer": "Für Druckmaterialien empfehlen wir das SVG-Format für Skalierbarkeit oder hochauflösendes PNG (300+ DPI) für beste Qualität."
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"question": "Kann ich einen QR-Code mit einem Passwort schützen?",
|
"question": "Kann ich einen QR-Code mit einem Passwort schützen?",
|
||||||
"answer": "Ja, Pro- und Business-Pläne beinhalten Passwortschutz und Zugriffskontrollfunktionen für Ihre QR-Codes."
|
"answer": "Ja, Pro- und Business-Pläne beinhalten Passwortschutz und Zugriffskontrollfunktionen für Ihre QR-Codes."
|
||||||
},
|
},
|
||||||
"analytics": {
|
"analytics": {
|
||||||
"question": "Wie funktioniert die Analytik?",
|
"question": "Wie funktioniert die Analytik?",
|
||||||
"answer": "Wir verfolgen Scans, Standorte, Geräte und Referrer unter Beachtung der Privatsphäre der Nutzer. Keine persönlichen Daten werden gespeichert."
|
"answer": "Wir verfolgen Scans, Standorte, Geräte und Referrer unter Beachtung der Privatsphäre der Nutzer. Keine persönlichen Daten werden gespeichert."
|
||||||
},
|
},
|
||||||
"privacy": {
|
"privacy": {
|
||||||
"question": "Verfolgen Sie persönliche Daten?",
|
"question": "Verfolgen Sie persönliche Daten?",
|
||||||
"answer": "Wir respektieren die Privatsphäre und sammeln nur anonyme Nutzungsdaten. IP-Adressen werden gehasht und wir respektieren Do-Not-Track-Header."
|
"answer": "Wir respektieren die Privatsphäre und sammeln nur anonyme Nutzungsdaten. IP-Adressen werden gehasht und wir respektieren Do-Not-Track-Header."
|
||||||
},
|
},
|
||||||
"bulk": {
|
"bulk": {
|
||||||
"question": "Kann ich Codes in großen Mengen mit meinen eigenen Daten erstellen?",
|
"question": "Kann ich Codes in großen Mengen mit meinen eigenen Daten erstellen?",
|
||||||
"answer": "Ja, Sie können CSV- oder Excel-Dateien hochladen, um mehrere QR-Codes auf einmal mit individueller Datenzuordnung zu erstellen."
|
"answer": "Ja, Sie können CSV- oder Excel-Dateien hochladen, um mehrere QR-Codes auf einmal mit individueller Datenzuordnung zu erstellen."
|
||||||
},
|
},
|
||||||
"no_account": {
|
"no_account": {
|
||||||
"question": "Kann ich QR-Codes auch ohne Anmeldung erstellen?",
|
"question": "Kann ich QR-Codes auch ohne Anmeldung erstellen?",
|
||||||
"answer": "Ja, Sie können unbegrenzt statische QR-Codes ohne Registrierung erstellen. Diese sind sofort einsatzbereit und funktionieren für immer. Für dynamische QR-Codes mit Tracking und Bearbeitung ist ein kostenloses Konto erforderlich."
|
"answer": "Ja, Sie können unbegrenzt statische QR-Codes ohne Registrierung erstellen. Diese sind sofort einsatzbereit und funktionieren für immer. Für dynamische QR-Codes mit Tracking und Bearbeitung ist ein kostenloses Konto erforderlich."
|
||||||
},
|
},
|
||||||
"restaurant_menu": {
|
"restaurant_menu": {
|
||||||
"question": "Kann ich eine digitale Speisekarte per QR-Code erstellen?",
|
"question": "Kann ich eine digitale Speisekarte per QR-Code erstellen?",
|
||||||
"answer": "Ja, mit dem PDF-QR-Code können Sie Ihre Speisekarte als digitale Version erstellen. Gäste scannen den Code und sehen die Speisekarte auf ihrem Handy. Änderungen an der Karte sind ohne Neudruck möglich."
|
"answer": "Ja, mit dem PDF-QR-Code können Sie Ihre Speisekarte als digitale Version erstellen. Gäste scannen den Code und sehen die Speisekarte auf ihrem Handy. Änderungen an der Karte sind ohne Neudruck möglich."
|
||||||
},
|
},
|
||||||
"scan_rate": {
|
"scan_rate": {
|
||||||
"question": "Was beeinflusst die Scan-Rate meines QR-Codes?",
|
"question": "Was beeinflusst die Scan-Rate meines QR-Codes?",
|
||||||
"answer": "Die Scan-Rate hängt von mehreren Faktoren ab: Platzierung (auf Augenhöhe), Größe (mindestens 2x2 cm), Kontrast (dunkel auf hell), Call-to-Action ('Scannen Sie hier') und Vorhandensein Ihres Logos für Vertrauen."
|
"answer": "Die Scan-Rate hängt von mehreren Faktoren ab: Platzierung (auf Augenhöhe), Größe (mindestens 2x2 cm), Kontrast (dunkel auf hell), Call-to-Action ('Scannen Sie hier') und Vorhandensein Ihres Logos für Vertrauen."
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"question": "Kann ich meinen QR-Code mit meinem Logo branden?",
|
"question": "Kann ich meinen QR-Code mit meinem Logo branden?",
|
||||||
"answer": "Ja, ab dem Pro-Plan können Sie Ihre QR-Codes mit Ihrem Firmenlogo, individuellen Farben und Stilen anpassen. Dies erhöht die Vertrauenswürdigkeit und Wiedererkennung."
|
"answer": "Ja, ab dem Pro-Plan können Sie Ihre QR-Codes mit Ihrem Firmenlogo, individuellen Farben und Stilen anpassen. Dies erhöht die Vertrauenswürdigkeit und Wiedererkennung."
|
||||||
},
|
},
|
||||||
"data_security": {
|
"data_security": {
|
||||||
"question": "Sind meine QR-Code-Daten in Deutschland sicher?",
|
"question": "Sind meine QR-Code-Daten in Deutschland sicher?",
|
||||||
"answer": "Wir hosten auf europäischen Servern und folgen der DSGVO. Scan-Daten werden anonymisiert gespeichert. Wir geben keine persönlichen Daten an Dritte weiter."
|
"answer": "Wir hosten auf europäischen Servern und folgen der DSGVO. Scan-Daten werden anonymisiert gespeichert. Wir geben keine persönlichen Daten an Dritte weiter."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
"subtitle": "Verwalten Sie Ihre QR-Codes und verfolgen Sie Ihre Performance",
|
"subtitle": "Verwalten Sie Ihre QR-Codes und verfolgen Sie Ihre Performance",
|
||||||
"stats": {
|
"stats": {
|
||||||
"total_scans": "Gesamte Scans",
|
"total_scans": "Gesamte Scans",
|
||||||
"active_codes": "Aktive QR-Codes",
|
"active_codes": "Aktive QR-Codes",
|
||||||
"conversion_rate": "Konversionsrate"
|
"conversion_rate": "Konversionsrate"
|
||||||
},
|
},
|
||||||
"recent_codes": "Aktuelle QR-Codes",
|
"recent_codes": "Aktuelle QR-Codes",
|
||||||
"blog_resources": "Blog & Ressourcen",
|
"blog_resources": "Blog & Ressourcen",
|
||||||
"menu": {
|
"menu": {
|
||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
"duplicate": "Duplizieren",
|
"duplicate": "Duplizieren",
|
||||||
"pause": "Pausieren",
|
"pause": "Pausieren",
|
||||||
"delete": "Löschen"
|
"delete": "Löschen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "QR-Code erstellen",
|
"title": "QR-Code erstellen",
|
||||||
"subtitle": "Generieren Sie dynamische und statische QR-Codes mit individuellem Branding",
|
"subtitle": "Generieren Sie dynamische und statische QR-Codes mit individuellem Branding",
|
||||||
"content": "Inhalt",
|
"content": "Inhalt",
|
||||||
"type": "QR-Code-Typ",
|
"type": "QR-Code-Typ",
|
||||||
"style": "Stil & Branding",
|
"style": "Stil & Branding",
|
||||||
"preview": "Live-Vorschau",
|
"preview": "Live-Vorschau",
|
||||||
"title_label": "Titel",
|
"title_label": "Titel",
|
||||||
"title_placeholder": "Mein QR-Code",
|
"title_placeholder": "Mein QR-Code",
|
||||||
"content_type": "Inhaltstyp",
|
"content_type": "Inhaltstyp",
|
||||||
"url_label": "URL",
|
"url_label": "URL",
|
||||||
"url_placeholder": "https://beispiel.de",
|
"url_placeholder": "https://beispiel.de",
|
||||||
"tags_label": "Tags (durch Komma getrennt)",
|
"tags_label": "Tags (durch Komma getrennt)",
|
||||||
"tags_placeholder": "marketing, kampagne, 2025",
|
"tags_placeholder": "marketing, kampagne, 2025",
|
||||||
"qr_code_type": "QR-Code-Typ",
|
"qr_code_type": "QR-Code-Typ",
|
||||||
"dynamic": "Dynamisch",
|
"dynamic": "Dynamisch",
|
||||||
"static": "Statisch",
|
"static": "Statisch",
|
||||||
"recommended": "Empfohlen",
|
"recommended": "Empfohlen",
|
||||||
"dynamic_description": "Dynamisch: Scans verfolgen, URL später bearbeiten, Analytik ansehen. QR enthält Tracking-Link.",
|
"dynamic_description": "Dynamisch: Scans verfolgen, URL später bearbeiten, Analytik ansehen. QR enthält Tracking-Link.",
|
||||||
"static_description": "Statisch: Direkt zum Inhalt, kein Tracking, nicht bearbeitbar. QR enthält tatsächlichen Inhalt.",
|
"static_description": "Statisch: Direkt zum Inhalt, kein Tracking, nicht bearbeitbar. QR enthält tatsächlichen Inhalt.",
|
||||||
"foreground_color": "Vordergrundfarbe",
|
"foreground_color": "Vordergrundfarbe",
|
||||||
"background_color": "Hintergrundfarbe",
|
"background_color": "Hintergrundfarbe",
|
||||||
"corner_style": "Eckenstil",
|
"corner_style": "Eckenstil",
|
||||||
"size": "Größe",
|
"size": "Größe",
|
||||||
"good_contrast": "Guter Kontrast",
|
"good_contrast": "Guter Kontrast",
|
||||||
"contrast_ratio": "Kontrastverhältnis",
|
"contrast_ratio": "Kontrastverhältnis",
|
||||||
"download_svg": "SVG herunterladen",
|
"download_svg": "SVG herunterladen",
|
||||||
"download_png": "PNG herunterladen",
|
"download_png": "PNG herunterladen",
|
||||||
"save_qr_code": "QR-Code speichern"
|
"save_qr_code": "QR-Code speichern"
|
||||||
},
|
},
|
||||||
"analytics": {
|
"analytics": {
|
||||||
"title": "Analytik",
|
"title": "Analytik",
|
||||||
"subtitle": "Verfolgen und analysieren Sie die Performance Ihrer QR-Codes",
|
"subtitle": "Verfolgen und analysieren Sie die Performance Ihrer QR-Codes",
|
||||||
"export_report": "Bericht exportieren",
|
"export_report": "Bericht exportieren",
|
||||||
"from_last_period": "vom letzten Zeitraum",
|
"from_last_period": "vom letzten Zeitraum",
|
||||||
"no_mobile_scans": "Keine mobilen Scans",
|
"no_mobile_scans": "Keine mobilen Scans",
|
||||||
"of_total": "der Gesamtmenge",
|
"of_total": "der Gesamtmenge",
|
||||||
"ranges": {
|
"ranges": {
|
||||||
"7d": "7 Tage",
|
"7d": "7 Tage",
|
||||||
"30d": "30 Tage",
|
"30d": "30 Tage",
|
||||||
"90d": "90 Tage"
|
"90d": "90 Tage"
|
||||||
},
|
},
|
||||||
"kpis": {
|
"kpis": {
|
||||||
"total_scans": "Gesamte Scans",
|
"total_scans": "Gesamte Scans",
|
||||||
"avg_scans": "Ø Scans/QR",
|
"avg_scans": "Ø Scans/QR",
|
||||||
"mobile_usage": "Mobile Nutzung",
|
"mobile_usage": "Mobile Nutzung",
|
||||||
"top_country": "Top Land"
|
"top_country": "Top Land"
|
||||||
},
|
},
|
||||||
"charts": {
|
"charts": {
|
||||||
"scans_over_time": "Scans über Zeit",
|
"scans_over_time": "Scans über Zeit",
|
||||||
"device_types": "Gerätetypen",
|
"device_types": "Gerätetypen",
|
||||||
"top_countries": "Top Länder"
|
"top_countries": "Top Länder"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"qr_code": "QR-Code",
|
"qr_code": "QR-Code",
|
||||||
"type": "Typ",
|
"type": "Typ",
|
||||||
"total_scans": "Gesamte Scans",
|
"total_scans": "Gesamte Scans",
|
||||||
"unique_scans": "Einzigartige Scans",
|
"unique_scans": "Einzigartige Scans",
|
||||||
"conversion": "Konversion",
|
"conversion": "Konversion",
|
||||||
"trend": "Trend",
|
"trend": "Trend",
|
||||||
"scans": "Scans",
|
"scans": "Scans",
|
||||||
"percentage": "Prozent",
|
"percentage": "Prozent",
|
||||||
"country": "Land",
|
"country": "Land",
|
||||||
"performance": "Performance",
|
"performance": "Performance",
|
||||||
"created": "Erstellt",
|
"created": "Erstellt",
|
||||||
"status": "Status"
|
"status": "Status"
|
||||||
},
|
},
|
||||||
"performance_title": "QR-Code-Performance"
|
"performance_title": "QR-Code-Performance"
|
||||||
},
|
},
|
||||||
"bulk": {
|
"bulk": {
|
||||||
"title": "Massen-Erstellung",
|
"title": "Massen-Erstellung",
|
||||||
"subtitle": "Erstellen Sie mehrere QR-Codes gleichzeitig aus CSV- oder Excel-Dateien",
|
"subtitle": "Erstellen Sie mehrere QR-Codes gleichzeitig aus CSV- oder Excel-Dateien",
|
||||||
"template_warning_title": "Bitte folgen Sie dem Vorlagenformat",
|
"template_warning_title": "Bitte folgen Sie dem Vorlagenformat",
|
||||||
"template_warning_text": "Laden Sie die Vorlage unten herunter und folgen Sie dem Format genau. Ihre CSV muss Spalten für Titel und Inhalt (URL) enthalten.",
|
"template_warning_text": "Laden Sie die Vorlage unten herunter und folgen Sie dem Format genau. Ihre CSV muss Spalten für Titel und Inhalt (URL) enthalten.",
|
||||||
"static_only_title": "Nur statische QR-Codes",
|
"static_only_title": "Nur statische QR-Codes",
|
||||||
"static_only_text": "Massen-Erstellung generiert statische QR-Codes, die nach der Erstellung nicht bearbeitet werden können. Diese QR-Codes beinhalten kein Tracking oder Analytik. Perfekt für Druckmaterialien und Offline-Nutzung.",
|
"static_only_text": "Massen-Erstellung generiert statische QR-Codes, die nach der Erstellung nicht bearbeitet werden können. Diese QR-Codes beinhalten kein Tracking oder Analytik. Perfekt für Druckmaterialien und Offline-Nutzung.",
|
||||||
"download_template": "Vorlage herunterladen",
|
"download_template": "Vorlage herunterladen",
|
||||||
"no_file_selected": "Keine ausgewählt",
|
"no_file_selected": "Keine ausgewählt",
|
||||||
"simple_format": "Einfaches Format",
|
"simple_format": "Einfaches Format",
|
||||||
"just_title_url": "Nur Titel & URL",
|
"just_title_url": "Nur Titel & URL",
|
||||||
"static_qr_codes": "Statische QR-Codes",
|
"static_qr_codes": "Statische QR-Codes",
|
||||||
"no_tracking": "Kein Tracking enthalten",
|
"no_tracking": "Kein Tracking enthalten",
|
||||||
"instant_download": "Sofortiger Download",
|
"instant_download": "Sofortiger Download",
|
||||||
"get_zip": "ZIP mit allen SVGs erhalten",
|
"get_zip": "ZIP mit allen SVGs erhalten",
|
||||||
"max_rows": "max 1.000 Zeilen",
|
"max_rows": "max 1.000 Zeilen",
|
||||||
"steps": {
|
"steps": {
|
||||||
"upload": "Datei hochladen",
|
"upload": "Datei hochladen",
|
||||||
"preview": "Vorschau & Zuordnung",
|
"preview": "Vorschau & Zuordnung",
|
||||||
"download": "Herunterladen"
|
"download": "Herunterladen"
|
||||||
},
|
},
|
||||||
"drag_drop": "Datei hier hinziehen",
|
"drag_drop": "Datei hier hinziehen",
|
||||||
"or_click": "oder klicken zum Durchsuchen",
|
"or_click": "oder klicken zum Durchsuchen",
|
||||||
"supported_formats": "Unterstützt CSV, XLS, XLSX (max 1.000 Zeilen)"
|
"supported_formats": "Unterstützt CSV, XLS, XLSX (max 1.000 Zeilen)"
|
||||||
},
|
},
|
||||||
"integrations": {
|
"integrations": {
|
||||||
"title": "Integrationen",
|
"title": "Integrationen",
|
||||||
"metrics": {
|
"metrics": {
|
||||||
"total_codes": "QR-Codes Gesamt",
|
"total_codes": "QR-Codes Gesamt",
|
||||||
"active_integrations": "Aktive Integrationen",
|
"active_integrations": "Aktive Integrationen",
|
||||||
"sync_status": "Sync-Status",
|
"sync_status": "Sync-Status",
|
||||||
"available_services": "Verfügbare Services"
|
"available_services": "Verfügbare Services"
|
||||||
},
|
},
|
||||||
"zapier": {
|
"zapier": {
|
||||||
"title": "Zapier",
|
"title": "Zapier",
|
||||||
"description": "Automatisieren Sie QR-Code-Erstellung mit 5000+ Apps",
|
"description": "Automatisieren Sie QR-Code-Erstellung mit 5000+ Apps",
|
||||||
"features": [
|
"features": [
|
||||||
"Trigger bei neuen QR-Codes",
|
"Trigger bei neuen QR-Codes",
|
||||||
"Codes aus anderen Apps erstellen",
|
"Codes aus anderen Apps erstellen",
|
||||||
"Scan-Daten synchronisieren"
|
"Scan-Daten synchronisieren"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"airtable": {
|
"airtable": {
|
||||||
"title": "Airtable",
|
"title": "Airtable",
|
||||||
"description": "Synchronisieren Sie QR-Codes mit Ihren Airtable-Basen",
|
"description": "Synchronisieren Sie QR-Codes mit Ihren Airtable-Basen",
|
||||||
"features": [
|
"features": [
|
||||||
"Bidirektionale Synchronisation",
|
"Bidirektionale Synchronisation",
|
||||||
"Individuelle Feldzuordnung",
|
"Individuelle Feldzuordnung",
|
||||||
"Echtzeit-Updates"
|
"Echtzeit-Updates"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"sheets": {
|
"sheets": {
|
||||||
"title": "Google Sheets",
|
"title": "Google Sheets",
|
||||||
"description": "Exportieren Sie Daten automatisch zu Google Sheets",
|
"description": "Exportieren Sie Daten automatisch zu Google Sheets",
|
||||||
"features": [
|
"features": [
|
||||||
"Automatisierte Exporte",
|
"Automatisierte Exporte",
|
||||||
"Individuelle Vorlagen",
|
"Individuelle Vorlagen",
|
||||||
"Geplante Updates"
|
"Geplante Updates"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"activate": "Aktivieren & Konfigurieren"
|
"activate": "Aktivieren & Konfigurieren"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Einstellungen",
|
"title": "Einstellungen",
|
||||||
"subtitle": "Verwalten Sie Ihre Kontoeinstellungen und Präferenzen",
|
"subtitle": "Verwalten Sie Ihre Kontoeinstellungen und Präferenzen",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"profile": "Profil",
|
"profile": "Profil",
|
||||||
"billing": "Abrechnung",
|
"billing": "Abrechnung",
|
||||||
"team": "Team & Rollen",
|
"team": "Team & Rollen",
|
||||||
"api": "API-Schlüssel",
|
"api": "API-Schlüssel",
|
||||||
"workspace": "Arbeitsbereich"
|
"workspace": "Arbeitsbereich"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
"create": "Erstellen",
|
"create": "Erstellen",
|
||||||
"loading": "Lädt...",
|
"loading": "Lädt...",
|
||||||
"error": "Ein Fehler ist aufgetreten",
|
"error": "Ein Fehler ist aufgetreten",
|
||||||
"success": "Erfolgreich!"
|
"success": "Erfolgreich!"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"product": "Produkt",
|
"product": "Produkt",
|
||||||
"features": "Funktionen",
|
"features": "Funktionen",
|
||||||
"pricing": "Preise",
|
"pricing": "Preise",
|
||||||
"faq": "FAQ",
|
"faq": "FAQ",
|
||||||
"blog": "Blog",
|
"blog": "Blog",
|
||||||
"resources": "Ressourcen",
|
"resources": "Ressourcen",
|
||||||
"full_pricing": "Alle Preise",
|
"full_pricing": "Alle Preise",
|
||||||
"all_questions": "Alle Fragen",
|
"all_questions": "Alle Fragen",
|
||||||
"all_articles": "Alle Artikel",
|
"all_articles": "Alle Artikel",
|
||||||
"learn": "Lernen",
|
"learn": "Lernen",
|
||||||
"get_started": "Loslegen",
|
"get_started": "Loslegen",
|
||||||
"legal": "Rechtliches",
|
"legal": "Rechtliches",
|
||||||
"industries": "Branchen",
|
"industries": "Branchen",
|
||||||
"privacy_policy": "Datenschutzerklärung",
|
"privacy_policy": "Datenschutzerklärung",
|
||||||
"terms_of_service": "Nutzungsbedingungen",
|
"terms_of_service": "Nutzungsbedingungen",
|
||||||
"tagline": "Erstellen Sie benutzerdefinierte QR-Codes in Sekunden mit erweitertem Tracking und Analysen.",
|
"tagline": "Erstellen Sie benutzerdefinierte QR-Codes in Sekunden mit erweitertem Tracking und Analysen.",
|
||||||
"newsletter": "Newsletter-Anmeldung",
|
"newsletter": "Newsletter-Anmeldung",
|
||||||
"rights_reserved": "QR Master. Alle Rechte vorbehalten."
|
"rights_reserved": "QR Master. Alle Rechte vorbehalten."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
824
src/i18n/en.json
824
src/i18n/en.json
@@ -1,412 +1,412 @@
|
|||||||
{
|
{
|
||||||
"nav": {
|
"nav": {
|
||||||
"features": "Features",
|
"features": "Features",
|
||||||
"pricing": "Pricing",
|
"pricing": "Pricing",
|
||||||
"faq": "FAQ",
|
"faq": "FAQ",
|
||||||
"blog": "Blog",
|
"blog": "Blog",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"signup": "Sign Up",
|
"signup": "Sign Up",
|
||||||
"learn": "Learn",
|
"learn": "Learn",
|
||||||
"create_qr": "Create QR",
|
"create_qr": "Create QR",
|
||||||
"bulk_creation": "Bulk Creation",
|
"bulk_creation": "Bulk Creation",
|
||||||
"analytics": "Analytics",
|
"analytics": "Analytics",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"cta": "Get Started Free",
|
"cta": "Get Started Free",
|
||||||
"tools": "Free Tools",
|
"tools": "Free Tools",
|
||||||
"all_free": "All generators are 100% free",
|
"all_free": "All generators are 100% free",
|
||||||
"resources": "Resources",
|
"resources": "Resources",
|
||||||
"all_industries": "Industries"
|
"all_industries": "Industries"
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "QR Master Free QR Code Generator",
|
"badge": "QR Master Free QR Code Generator",
|
||||||
"title": "QR Master: Free Dynamic QR Code Generator with Tracking",
|
"title": "QR Master: Free Dynamic QR Code Generator with Tracking",
|
||||||
"subtitle": "Create static and dynamic QR codes with editable destinations, scan tracking, custom branding, and bulk generation. Also searched as QRMaster and QR code master.",
|
"subtitle": "Create static and dynamic QR codes with editable destinations, scan tracking, custom branding, and bulk generation. Also searched as QRMaster and QR code master.",
|
||||||
"features": [
|
"features": [
|
||||||
"No credit card required to start",
|
"No credit card required to start",
|
||||||
"Create QR codes free forever",
|
"Create QR codes free forever",
|
||||||
"Advanced tracking and analytics",
|
"Advanced tracking and analytics",
|
||||||
"Custom colors and styles"
|
"Custom colors and styles"
|
||||||
],
|
],
|
||||||
"cta_primary": "Make a QR Code Free",
|
"cta_primary": "Make a QR Code Free",
|
||||||
"cta_secondary": "View Pricing",
|
"cta_secondary": "View Pricing",
|
||||||
"engagement_badge": "Free Forever"
|
"engagement_badge": "Free Forever"
|
||||||
},
|
},
|
||||||
"trust": {
|
"trust": {
|
||||||
"users": "Happy Users",
|
"users": "Happy Users",
|
||||||
"codes": "Active QR Codes",
|
"codes": "Active QR Codes",
|
||||||
"scans": "Total Scans",
|
"scans": "Total Scans",
|
||||||
"countries": "Countries"
|
"countries": "Countries"
|
||||||
},
|
},
|
||||||
"industries": {
|
"industries": {
|
||||||
"restaurant": "Restaurant Chain",
|
"restaurant": "Restaurant Chain",
|
||||||
"tech": "Tech Startup",
|
"tech": "Tech Startup",
|
||||||
"realestate": "Real Estate",
|
"realestate": "Real Estate",
|
||||||
"events": "Event Agency",
|
"events": "Event Agency",
|
||||||
"retail": "Retail Store",
|
"retail": "Retail Store",
|
||||||
"healthcare": "Healthcare"
|
"healthcare": "Healthcare"
|
||||||
},
|
},
|
||||||
"templates": {
|
"templates": {
|
||||||
"title": "Start with a Template",
|
"title": "Start with a Template",
|
||||||
"restaurant": "Restaurant Menu",
|
"restaurant": "Restaurant Menu",
|
||||||
"business": "Business Card",
|
"business": "Business Card",
|
||||||
"vcard": "Contact Card",
|
"vcard": "Contact Card",
|
||||||
"event": "Event Ticket",
|
"event": "Event Ticket",
|
||||||
"use_template": "Use template →"
|
"use_template": "Use template →"
|
||||||
},
|
},
|
||||||
"generator": {
|
"generator": {
|
||||||
"title": "Instant QR Code Generator",
|
"title": "Instant QR Code Generator",
|
||||||
"url_placeholder": "Enter your URL here...",
|
"url_placeholder": "Enter your URL here...",
|
||||||
"foreground": "Foreground",
|
"foreground": "Foreground",
|
||||||
"background": "Background",
|
"background": "Background",
|
||||||
"corners": "Corners",
|
"corners": "Corners",
|
||||||
"size": "Size",
|
"size": "Size",
|
||||||
"contrast_good": "Good contrast",
|
"contrast_good": "Good contrast",
|
||||||
"download_svg": "Download SVG",
|
"download_svg": "Download SVG",
|
||||||
"download_png": "Download PNG",
|
"download_png": "Download PNG",
|
||||||
"save_track": "Save & Track",
|
"save_track": "Save & Track",
|
||||||
"live_preview": "Live Preview",
|
"live_preview": "Live Preview",
|
||||||
"demo_note": "This is a demo QR code"
|
"demo_note": "This is a demo QR code"
|
||||||
},
|
},
|
||||||
"static_vs_dynamic": {
|
"static_vs_dynamic": {
|
||||||
"title": "Why Dynamic QR Codes Save You Money",
|
"title": "Why Dynamic QR Codes Save You Money",
|
||||||
"description": "Stop re-printing materials. Switch destinations instantly and track every scan.",
|
"description": "Stop re-printing materials. Switch destinations instantly and track every scan.",
|
||||||
"static": {
|
"static": {
|
||||||
"title": "Static QR Codes",
|
"title": "Static QR Codes",
|
||||||
"subtitle": "Always Free",
|
"subtitle": "Always Free",
|
||||||
"description": "Perfect for permanent content that never changes",
|
"description": "Perfect for permanent content that never changes",
|
||||||
"features": [
|
"features": [
|
||||||
"Content cannot be edited",
|
"Content cannot be edited",
|
||||||
"No scan tracking",
|
"No scan tracking",
|
||||||
"Works forever",
|
"Works forever",
|
||||||
"No account required"
|
"No account required"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dynamic": {
|
"dynamic": {
|
||||||
"title": "Dynamic QR Codes",
|
"title": "Dynamic QR Codes",
|
||||||
"subtitle": "Recommended",
|
"subtitle": "Recommended",
|
||||||
"description": "Full control with tracking and editing capabilities",
|
"description": "Full control with tracking and editing capabilities",
|
||||||
"features": [
|
"features": [
|
||||||
"Edit content anytime",
|
"Edit content anytime",
|
||||||
"Advanced analytics",
|
"Advanced analytics",
|
||||||
"Custom branding",
|
"Custom branding",
|
||||||
"Bulk operations"
|
"Bulk operations"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
"title": "Everything you need to create professional QR codes",
|
"title": "Everything you need to create professional QR codes",
|
||||||
"analytics": {
|
"analytics": {
|
||||||
"title": "Advanced Analytics",
|
"title": "Advanced Analytics",
|
||||||
"description": "Track scans, locations, devices, and user behavior with detailed insights."
|
"description": "Track scans, locations, devices, and user behavior with detailed insights."
|
||||||
},
|
},
|
||||||
"customization": {
|
"customization": {
|
||||||
"title": "Full Customization",
|
"title": "Full Customization",
|
||||||
"description": "Brand your QR codes with custom colors and styling options."
|
"description": "Brand your QR codes with custom colors and styling options."
|
||||||
},
|
},
|
||||||
"unlimited": {
|
"unlimited": {
|
||||||
"title": "Unlimited Static QR Codes",
|
"title": "Unlimited Static QR Codes",
|
||||||
"description": "Create as many static QR codes as you need. Free forever, no limits."
|
"description": "Create as many static QR codes as you need. Free forever, no limits."
|
||||||
},
|
},
|
||||||
"bulk": {
|
"bulk": {
|
||||||
"title": "Bulk Operations",
|
"title": "Bulk Operations",
|
||||||
"description": "Create hundreds of QR codes at once with CSV import and batch processing."
|
"description": "Create hundreds of QR codes at once with CSV import and batch processing."
|
||||||
},
|
},
|
||||||
"integrations": {
|
"integrations": {
|
||||||
"title": "Integrations",
|
"title": "Integrations",
|
||||||
"description": "Connect with Zapier, Airtable, Google Sheets, and more popular tools."
|
"description": "Connect with Zapier, Airtable, Google Sheets, and more popular tools."
|
||||||
},
|
},
|
||||||
"api": {
|
"api": {
|
||||||
"title": "Developer API",
|
"title": "Developer API",
|
||||||
"description": "Integrate QR code generation into your applications with our REST API."
|
"description": "Integrate QR code generation into your applications with our REST API."
|
||||||
},
|
},
|
||||||
"support": {
|
"support": {
|
||||||
"title": "24/7 Support",
|
"title": "24/7 Support",
|
||||||
"description": "Get help when you need it with our dedicated customer support team."
|
"description": "Get help when you need it with our dedicated customer support team."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pricing": {
|
"pricing": {
|
||||||
"title": "Choose Your Plan",
|
"title": "Choose Your Plan",
|
||||||
"subtitle": "Select the perfect plan for your QR code needs",
|
"subtitle": "Select the perfect plan for your QR code needs",
|
||||||
"choose_plan": "Choose Your Plan",
|
"choose_plan": "Choose Your Plan",
|
||||||
"select_plan": "Select the perfect plan for your QR code needs",
|
"select_plan": "Select the perfect plan for your QR code needs",
|
||||||
"current_plan": "Current Plan",
|
"current_plan": "Current Plan",
|
||||||
"upgrade_to": "Upgrade to",
|
"upgrade_to": "Upgrade to",
|
||||||
"downgrade_to_free": "Downgrade to Free",
|
"downgrade_to_free": "Downgrade to Free",
|
||||||
"most_popular": "Most Popular",
|
"most_popular": "Most Popular",
|
||||||
"all_plans_note": "All plans include unlimited static QR codes and basic customization.",
|
"all_plans_note": "All plans include unlimited static QR codes and basic customization.",
|
||||||
"free": {
|
"free": {
|
||||||
"title": "Free",
|
"title": "Free",
|
||||||
"name": "Free",
|
"name": "Free",
|
||||||
"price": "€0",
|
"price": "€0",
|
||||||
"period": "forever",
|
"period": "forever",
|
||||||
"features": [
|
"features": [
|
||||||
"3 active dynamic QR codes (8 types available)",
|
"3 active dynamic QR codes (8 types available)",
|
||||||
"Unlimited static QR codes",
|
"Unlimited static QR codes",
|
||||||
"Basic scan tracking",
|
"Basic scan tracking",
|
||||||
"Standard QR design templates",
|
"Standard QR design templates",
|
||||||
"Download as SVG/PNG"
|
"Download as SVG/PNG"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"pro": {
|
"pro": {
|
||||||
"title": "Pro",
|
"title": "Pro",
|
||||||
"name": "Pro",
|
"name": "Pro",
|
||||||
"price": "€9",
|
"price": "€9",
|
||||||
"period": "per month",
|
"period": "per month",
|
||||||
"badge": "Most Popular",
|
"badge": "Most Popular",
|
||||||
"features": [
|
"features": [
|
||||||
"50 dynamic QR codes",
|
"50 dynamic QR codes",
|
||||||
"Unlimited static QR codes",
|
"Unlimited static QR codes",
|
||||||
"Advanced analytics (scans, devices, locations)",
|
"Advanced analytics (scans, devices, locations)",
|
||||||
"Custom branding (colors & logos)"
|
"Custom branding (colors & logos)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"business": {
|
"business": {
|
||||||
"title": "Business",
|
"title": "Business",
|
||||||
"name": "Business",
|
"name": "Business",
|
||||||
"price": "€29",
|
"price": "€29",
|
||||||
"period": "per month",
|
"period": "per month",
|
||||||
"features": [
|
"features": [
|
||||||
"500 dynamic QR codes",
|
"500 dynamic QR codes",
|
||||||
"Unlimited static QR codes",
|
"Unlimited static QR codes",
|
||||||
"Everything from Pro",
|
"Everything from Pro",
|
||||||
"Bulk QR Creation (up to 1,000)",
|
"Bulk QR Creation (up to 1,000)",
|
||||||
"Priority email support",
|
"Priority email support",
|
||||||
"Advanced tracking & insights"
|
"Advanced tracking & insights"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"enterprise": {
|
"enterprise": {
|
||||||
"title": "Enterprise",
|
"title": "Enterprise",
|
||||||
"name": "Enterprise",
|
"name": "Enterprise",
|
||||||
"price": "Custom",
|
"price": "Custom",
|
||||||
"period": "",
|
"period": "",
|
||||||
"features": [
|
"features": [
|
||||||
"∞ dynamic QR codes",
|
"∞ dynamic QR codes",
|
||||||
"Unlimited static QR codes",
|
"Unlimited static QR codes",
|
||||||
"Everything from Business",
|
"Everything from Business",
|
||||||
"Dedicated Account Manager"
|
"Dedicated Account Manager"
|
||||||
],
|
],
|
||||||
"contact": "Contact Us"
|
"contact": "Contact Us"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"faq": {
|
"faq": {
|
||||||
"title": "Frequently Asked Questions",
|
"title": "Frequently Asked Questions",
|
||||||
"questions": {
|
"questions": {
|
||||||
"account": {
|
"account": {
|
||||||
"question": "Do I need an account to create QR codes?",
|
"question": "Do I need an account to create QR codes?",
|
||||||
"answer": "No account is required for static QR codes. However, dynamic QR codes with tracking and editing capabilities require a free account."
|
"answer": "No account is required for static QR codes. However, dynamic QR codes with tracking and editing capabilities require a free account."
|
||||||
},
|
},
|
||||||
"static_vs_dynamic": {
|
"static_vs_dynamic": {
|
||||||
"question": "What's the difference between static and dynamic QR codes?",
|
"question": "What's the difference between static and dynamic QR codes?",
|
||||||
"answer": "Static QR codes contain fixed content that cannot be changed. Dynamic QR codes can be edited anytime and provide detailed analytics."
|
"answer": "Static QR codes contain fixed content that cannot be changed. Dynamic QR codes can be edited anytime and provide detailed analytics."
|
||||||
},
|
},
|
||||||
"forever": {
|
"forever": {
|
||||||
"question": "Will my QR codes work forever?",
|
"question": "Will my QR codes work forever?",
|
||||||
"answer": "Static QR codes work forever as the content is embedded directly. Dynamic QR codes work as long as your account is active."
|
"answer": "Static QR codes work forever as the content is embedded directly. Dynamic QR codes work as long as your account is active."
|
||||||
},
|
},
|
||||||
"file_type": {
|
"file_type": {
|
||||||
"question": "What file type should I use for printing?",
|
"question": "What file type should I use for printing?",
|
||||||
"answer": "For print materials, we recommend SVG format for scalability or high-resolution PNG (300+ DPI) for best quality."
|
"answer": "For print materials, we recommend SVG format for scalability or high-resolution PNG (300+ DPI) for best quality."
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"question": "Can I password-protect a QR code?",
|
"question": "Can I password-protect a QR code?",
|
||||||
"answer": "Yes, Pro and Business plans include password protection and access control features for your QR codes."
|
"answer": "Yes, Pro and Business plans include password protection and access control features for your QR codes."
|
||||||
},
|
},
|
||||||
"analytics": {
|
"analytics": {
|
||||||
"question": "How do analytics work?",
|
"question": "How do analytics work?",
|
||||||
"answer": "We track scans, locations, devices, and referrers while respecting user privacy. No personal data is stored."
|
"answer": "We track scans, locations, devices, and referrers while respecting user privacy. No personal data is stored."
|
||||||
},
|
},
|
||||||
"privacy": {
|
"privacy": {
|
||||||
"question": "Do you track personal data?",
|
"question": "Do you track personal data?",
|
||||||
"answer": "We respect privacy and only collect anonymous usage data. IP addresses are hashed and we honor Do Not Track headers."
|
"answer": "We respect privacy and only collect anonymous usage data. IP addresses are hashed and we honor Do Not Track headers."
|
||||||
},
|
},
|
||||||
"bulk": {
|
"bulk": {
|
||||||
"question": "Can I bulk-create codes with my own data?",
|
"question": "Can I bulk-create codes with my own data?",
|
||||||
"answer": "Yes, you can upload CSV or Excel files to create multiple QR codes at once with custom data mapping."
|
"answer": "Yes, you can upload CSV or Excel files to create multiple QR codes at once with custom data mapping."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
"subtitle": "Manage your QR codes and track performance",
|
"subtitle": "Manage your QR codes and track performance",
|
||||||
"stats": {
|
"stats": {
|
||||||
"total_scans": "Total Scans",
|
"total_scans": "Total Scans",
|
||||||
"active_codes": "Active QR Codes",
|
"active_codes": "Active QR Codes",
|
||||||
"conversion_rate": "Conversion Rate"
|
"conversion_rate": "Conversion Rate"
|
||||||
},
|
},
|
||||||
"recent_codes": "Recent QR Codes",
|
"recent_codes": "Recent QR Codes",
|
||||||
"blog_resources": "Blog & Resources",
|
"blog_resources": "Blog & Resources",
|
||||||
"menu": {
|
"menu": {
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"duplicate": "Duplicate",
|
"duplicate": "Duplicate",
|
||||||
"pause": "Pause",
|
"pause": "Pause",
|
||||||
"delete": "Delete"
|
"delete": "Delete"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "Create QR Code",
|
"title": "Create QR Code",
|
||||||
"subtitle": "Generate dynamic and static QR codes with custom branding",
|
"subtitle": "Generate dynamic and static QR codes with custom branding",
|
||||||
"content": "Content",
|
"content": "Content",
|
||||||
"type": "QR Code Type",
|
"type": "QR Code Type",
|
||||||
"style": "Style & Branding",
|
"style": "Style & Branding",
|
||||||
"preview": "Live Preview",
|
"preview": "Live Preview",
|
||||||
"title_label": "Title",
|
"title_label": "Title",
|
||||||
"title_placeholder": "My QR Code",
|
"title_placeholder": "My QR Code",
|
||||||
"content_type": "Content Type",
|
"content_type": "Content Type",
|
||||||
"url_label": "URL",
|
"url_label": "URL",
|
||||||
"url_placeholder": "https://example.com",
|
"url_placeholder": "https://example.com",
|
||||||
"tags_label": "Tags (comma-separated)",
|
"tags_label": "Tags (comma-separated)",
|
||||||
"tags_placeholder": "marketing, campaign, 2025",
|
"tags_placeholder": "marketing, campaign, 2025",
|
||||||
"qr_code_type": "QR Code Type",
|
"qr_code_type": "QR Code Type",
|
||||||
"dynamic": "Dynamic",
|
"dynamic": "Dynamic",
|
||||||
"static": "Static",
|
"static": "Static",
|
||||||
"recommended": "Recommended",
|
"recommended": "Recommended",
|
||||||
"dynamic_description": "Dynamic: Track scans, edit URL later, view analytics. QR contains tracking link.",
|
"dynamic_description": "Dynamic: Track scans, edit URL later, view analytics. QR contains tracking link.",
|
||||||
"static_description": "Static: Direct to content, no tracking, cannot edit. QR contains actual content.",
|
"static_description": "Static: Direct to content, no tracking, cannot edit. QR contains actual content.",
|
||||||
"foreground_color": "Foreground Color",
|
"foreground_color": "Foreground Color",
|
||||||
"background_color": "Background Color",
|
"background_color": "Background Color",
|
||||||
"corner_style": "Corner Style",
|
"corner_style": "Corner Style",
|
||||||
"size": "Size",
|
"size": "Size",
|
||||||
"good_contrast": "Good contrast",
|
"good_contrast": "Good contrast",
|
||||||
"contrast_ratio": "Contrast ratio",
|
"contrast_ratio": "Contrast ratio",
|
||||||
"download_svg": "Download SVG",
|
"download_svg": "Download SVG",
|
||||||
"download_png": "Download PNG",
|
"download_png": "Download PNG",
|
||||||
"save_qr_code": "Save QR Code"
|
"save_qr_code": "Save QR Code"
|
||||||
},
|
},
|
||||||
"analytics": {
|
"analytics": {
|
||||||
"title": "Analytics",
|
"title": "Analytics",
|
||||||
"subtitle": "Track and analyze your QR code performance",
|
"subtitle": "Track and analyze your QR code performance",
|
||||||
"export_report": "Export Report",
|
"export_report": "Export Report",
|
||||||
"from_last_period": "from last period",
|
"from_last_period": "from last period",
|
||||||
"no_mobile_scans": "No mobile scans",
|
"no_mobile_scans": "No mobile scans",
|
||||||
"of_total": "of total",
|
"of_total": "of total",
|
||||||
"ranges": {
|
"ranges": {
|
||||||
"7d": "7 Days",
|
"7d": "7 Days",
|
||||||
"30d": "30 Days",
|
"30d": "30 Days",
|
||||||
"90d": "90 Days"
|
"90d": "90 Days"
|
||||||
},
|
},
|
||||||
"kpis": {
|
"kpis": {
|
||||||
"total_scans": "Total Scans",
|
"total_scans": "Total Scans",
|
||||||
"avg_scans": "Avg Scans/QR",
|
"avg_scans": "Avg Scans/QR",
|
||||||
"mobile_usage": "Mobile Usage",
|
"mobile_usage": "Mobile Usage",
|
||||||
"top_country": "Top Country"
|
"top_country": "Top Country"
|
||||||
},
|
},
|
||||||
"charts": {
|
"charts": {
|
||||||
"scans_over_time": "Scans Over Time",
|
"scans_over_time": "Scans Over Time",
|
||||||
"device_types": "Device Types",
|
"device_types": "Device Types",
|
||||||
"top_countries": "Top Countries"
|
"top_countries": "Top Countries"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"qr_code": "QR Code",
|
"qr_code": "QR Code",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"total_scans": "Total Scans",
|
"total_scans": "Total Scans",
|
||||||
"unique_scans": "Unique Scans",
|
"unique_scans": "Unique Scans",
|
||||||
"conversion": "Conversion",
|
"conversion": "Conversion",
|
||||||
"trend": "Trend",
|
"trend": "Trend",
|
||||||
"scans": "Scans",
|
"scans": "Scans",
|
||||||
"percentage": "Percentage",
|
"percentage": "Percentage",
|
||||||
"country": "Country",
|
"country": "Country",
|
||||||
"performance": "Performance",
|
"performance": "Performance",
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
"status": "Status"
|
"status": "Status"
|
||||||
},
|
},
|
||||||
"performance_title": "QR Code Performance"
|
"performance_title": "QR Code Performance"
|
||||||
},
|
},
|
||||||
"bulk": {
|
"bulk": {
|
||||||
"title": "Bulk Creation",
|
"title": "Bulk Creation",
|
||||||
"subtitle": "Create multiple QR codes at once from CSV or Excel files",
|
"subtitle": "Create multiple QR codes at once from CSV or Excel files",
|
||||||
"template_warning_title": "Please Follow the Template Format",
|
"template_warning_title": "Please Follow the Template Format",
|
||||||
"template_warning_text": "Download the template below and follow the format exactly. Your CSV must include columns for title and content (URL).",
|
"template_warning_text": "Download the template below and follow the format exactly. Your CSV must include columns for title and content (URL).",
|
||||||
"static_only_title": "Static QR Codes Only",
|
"static_only_title": "Static QR Codes Only",
|
||||||
"static_only_text": "Bulk creation generates static QR codes that cannot be edited after creation. These QR codes do not include tracking or analytics. Perfect for print materials and offline use.",
|
"static_only_text": "Bulk creation generates static QR codes that cannot be edited after creation. These QR codes do not include tracking or analytics. Perfect for print materials and offline use.",
|
||||||
"download_template": "Download Template",
|
"download_template": "Download Template",
|
||||||
"no_file_selected": "No file selected",
|
"no_file_selected": "No file selected",
|
||||||
"simple_format": "Simple Format",
|
"simple_format": "Simple Format",
|
||||||
"just_title_url": "Just title & URL",
|
"just_title_url": "Just title & URL",
|
||||||
"static_qr_codes": "Static QR Codes",
|
"static_qr_codes": "Static QR Codes",
|
||||||
"no_tracking": "No tracking included",
|
"no_tracking": "No tracking included",
|
||||||
"instant_download": "Instant Download",
|
"instant_download": "Instant Download",
|
||||||
"get_zip": "Get ZIP with all SVGs",
|
"get_zip": "Get ZIP with all SVGs",
|
||||||
"max_rows": "max 1,000 rows",
|
"max_rows": "max 1,000 rows",
|
||||||
"steps": {
|
"steps": {
|
||||||
"upload": "Upload File",
|
"upload": "Upload File",
|
||||||
"preview": "Preview & Map",
|
"preview": "Preview & Map",
|
||||||
"download": "Download"
|
"download": "Download"
|
||||||
},
|
},
|
||||||
"drag_drop": "Drag & drop your file here",
|
"drag_drop": "Drag & drop your file here",
|
||||||
"or_click": "or click to browse",
|
"or_click": "or click to browse",
|
||||||
"supported_formats": "Supports CSV, XLS, XLSX (max 1,000 rows)"
|
"supported_formats": "Supports CSV, XLS, XLSX (max 1,000 rows)"
|
||||||
},
|
},
|
||||||
"integrations": {
|
"integrations": {
|
||||||
"title": "Integrations",
|
"title": "Integrations",
|
||||||
"metrics": {
|
"metrics": {
|
||||||
"total_codes": "QR Codes Total",
|
"total_codes": "QR Codes Total",
|
||||||
"active_integrations": "Active Integrations",
|
"active_integrations": "Active Integrations",
|
||||||
"sync_status": "Sync Status",
|
"sync_status": "Sync Status",
|
||||||
"available_services": "Available Services"
|
"available_services": "Available Services"
|
||||||
},
|
},
|
||||||
"zapier": {
|
"zapier": {
|
||||||
"title": "Zapier",
|
"title": "Zapier",
|
||||||
"description": "Automate QR code creation with 5000+ apps",
|
"description": "Automate QR code creation with 5000+ apps",
|
||||||
"features": [
|
"features": [
|
||||||
"Trigger on new QR codes",
|
"Trigger on new QR codes",
|
||||||
"Create codes from other apps",
|
"Create codes from other apps",
|
||||||
"Sync scan data"
|
"Sync scan data"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"airtable": {
|
"airtable": {
|
||||||
"title": "Airtable",
|
"title": "Airtable",
|
||||||
"description": "Sync QR codes with your Airtable bases",
|
"description": "Sync QR codes with your Airtable bases",
|
||||||
"features": ["Two-way sync", "Custom field mapping", "Real-time updates"]
|
"features": ["Two-way sync", "Custom field mapping", "Real-time updates"]
|
||||||
},
|
},
|
||||||
"sheets": {
|
"sheets": {
|
||||||
"title": "Google Sheets",
|
"title": "Google Sheets",
|
||||||
"description": "Export data to Google Sheets automatically",
|
"description": "Export data to Google Sheets automatically",
|
||||||
"features": ["Automated exports", "Custom templates", "Scheduled updates"]
|
"features": ["Automated exports", "Custom templates", "Scheduled updates"]
|
||||||
},
|
},
|
||||||
"activate": "Activate & Configure"
|
"activate": "Activate & Configure"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"subtitle": "Manage your account settings and preferences",
|
"subtitle": "Manage your account settings and preferences",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"billing": "Billing",
|
"billing": "Billing",
|
||||||
"team": "Team & Roles",
|
"team": "Team & Roles",
|
||||||
"api": "API Keys",
|
"api": "API Keys",
|
||||||
"workspace": "Workspace"
|
"workspace": "Workspace"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"error": "An error occurred",
|
"error": "An error occurred",
|
||||||
"success": "Success!"
|
"success": "Success!"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"product": "Product",
|
"product": "Product",
|
||||||
"features": "Features",
|
"features": "Features",
|
||||||
"pricing": "Pricing",
|
"pricing": "Pricing",
|
||||||
"faq": "FAQ",
|
"faq": "FAQ",
|
||||||
"blog": "Blog",
|
"blog": "Blog",
|
||||||
"resources": "Resources",
|
"resources": "Resources",
|
||||||
"full_pricing": "Full Pricing",
|
"full_pricing": "Full Pricing",
|
||||||
"all_questions": "All Questions",
|
"all_questions": "All Questions",
|
||||||
"all_articles": "All Articles",
|
"all_articles": "All Articles",
|
||||||
"learn": "Learn",
|
"learn": "Learn",
|
||||||
"get_started": "Get Started",
|
"get_started": "Get Started",
|
||||||
"legal": "Legal",
|
"legal": "Legal",
|
||||||
"industries": "Industries",
|
"industries": "Industries",
|
||||||
"privacy_policy": "Privacy Policy",
|
"privacy_policy": "Privacy Policy",
|
||||||
"terms_of_service": "Terms of Service",
|
"terms_of_service": "Terms of Service",
|
||||||
"tagline": "Create custom QR codes in seconds with advanced tracking and analytics.",
|
"tagline": "Create custom QR codes in seconds with advanced tracking and analytics.",
|
||||||
"newsletter": "Newsletter signup",
|
"newsletter": "Newsletter signup",
|
||||||
"rights_reserved": "QR Master. All rights reserved."
|
"rights_reserved": "QR Master. All rights reserved."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/lib/tiktok.ts
Normal file
59
src/lib/tiktok.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { db } from '@/lib/db';
|
||||||
|
|
||||||
|
export const TIKTOK_OAUTH_STATE_COOKIE_NAME = 'tiktok_oauth_state';
|
||||||
|
export const TIKTOK_ACCOUNT_KEY = 'hermes-agent';
|
||||||
|
|
||||||
|
// Refresh when the access token expires within this window, so Hermes never
|
||||||
|
// receives a token that dies mid-upload.
|
||||||
|
const REFRESH_BUFFER_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
export async function getValidTiktokTokens() {
|
||||||
|
const integration = await db.tiktokIntegration.findUnique({
|
||||||
|
where: { accountKey: TIKTOK_ACCOUNT_KEY },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!integration) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integration.accessTokenExpiresAt.getTime() - Date.now() > REFRESH_BUFFER_MS) {
|
||||||
|
return integration;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientKey = process.env.TIKTOK_CLIENT_KEY;
|
||||||
|
const clientSecret = process.env.TIKTOK_CLIENT_SECRET;
|
||||||
|
if (!clientKey || !clientSecret) {
|
||||||
|
throw new Error('TikTok client credentials are not configured.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('https://open.tiktokapis.com/v2/oauth/token/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_key: clientKey,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token: integration.refreshToken,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokens = await response.json();
|
||||||
|
if (!response.ok || tokens.error) {
|
||||||
|
throw new Error(tokens.error_description || tokens.error || 'TikTok token refresh failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
return db.tiktokIntegration.update({
|
||||||
|
where: { accountKey: TIKTOK_ACCOUNT_KEY },
|
||||||
|
data: {
|
||||||
|
openId: tokens.open_id,
|
||||||
|
accessToken: tokens.access_token,
|
||||||
|
refreshToken: tokens.refresh_token,
|
||||||
|
scope: tokens.scope || null,
|
||||||
|
accessTokenExpiresAt: new Date(now + Number(tokens.expires_in || 0) * 1000),
|
||||||
|
refreshTokenExpiresAt: tokens.refresh_expires_in
|
||||||
|
? new Date(now + Number(tokens.refresh_expires_in) * 1000)
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user