TikTok api

This commit is contained in:
Timo Knuth
2026-07-02 13:06:50 +02:00
parent b0b70640ab
commit 0b9c8d2a8f
12 changed files with 1965 additions and 1712 deletions

View File

@@ -1,39 +1,39 @@
{
"permissions": {
"allow": [
"Bash(docker-compose:*)",
"Bash(docker container prune:*)",
"Bash(npx prisma migrate dev:*)",
"Bash(npx prisma:*)",
"Bash(npm run dev)",
"Bash(timeout:*)",
"Bash(taskkill:*)",
"Bash(npx kill-port:*)",
"Bash(docker compose:*)",
"Bash(curl -I https://fonts.googleapis.com)",
"Bash(wsl:*)",
"Read(//c/Users/a931627/.ssh/**)",
"Bash(ssh-keygen:*)",
"Bash(cat:*)",
"Bash(git remote add:*)",
"Bash(git push:*)",
"Bash(git remote set-url:*)",
"Bash(npm install:*)",
"Bash(npm run build:*)",
"Bash(ls:*)",
"Bash(curl:*)",
"Bash(echo \"\n\n## CSRF Debug aktiviert!\n\nBitte teste jetzt:\n1. Browser zu http://localhost:3050/create\n2. Dynamic QR Code erstellen versuchen\n3. Server-Logs zeigen jetzt [CSRF Debug] Output\n\nIch sehe dann:\n- Ob headerToken vorhanden ist\n- Ob cookieToken vorhanden ist \n- Ob sie übereinstimmen\n\n---\n\nStripe Portal 500 Error ist separates Problem:\nhttps://dashboard.stripe.com/test/settings/billing/portal\n→ Customer Portal Configuration muss erstellt werden\n\")",
"Bash(pkill:*)",
"Skill(shadcn-ui)",
"Bash(find:*)",
"Bash(ls -la \"/c/Users/User/Documents/QR-master/src/app/\\(main\\)/\\(marketing\\)/\")",
"Bash(npx tsc:*)"
],
"deny": [],
"ask": []
},
"enabledMcpjsonServers": [
"firecrawl",
"apify"
]
}
{
"permissions": {
"allow": [
"Bash(docker-compose:*)",
"Bash(docker container prune:*)",
"Bash(npx prisma migrate dev:*)",
"Bash(npx prisma:*)",
"Bash(npm run dev)",
"Bash(timeout:*)",
"Bash(taskkill:*)",
"Bash(npx kill-port:*)",
"Bash(docker compose:*)",
"Bash(curl -I https://fonts.googleapis.com)",
"Bash(wsl:*)",
"Read(//c/Users/a931627/.ssh/**)",
"Bash(ssh-keygen:*)",
"Bash(cat:*)",
"Bash(git remote add:*)",
"Bash(git push:*)",
"Bash(git remote set-url:*)",
"Bash(npm install:*)",
"Bash(npm run build:*)",
"Bash(ls:*)",
"Bash(curl:*)",
"Bash(echo \"\n\n## CSRF Debug aktiviert!\n\nBitte teste jetzt:\n1. Browser zu http://localhost:3050/create\n2. Dynamic QR Code erstellen versuchen\n3. Server-Logs zeigen jetzt [CSRF Debug] Output\n\nIch sehe dann:\n- Ob headerToken vorhanden ist\n- Ob cookieToken vorhanden ist \n- Ob sie übereinstimmen\n\n---\n\nStripe Portal 500 Error ist separates Problem:\nhttps://dashboard.stripe.com/test/settings/billing/portal\n→ Customer Portal Configuration muss erstellt werden\n\")",
"Bash(pkill:*)",
"Skill(shadcn-ui)",
"Bash(find:*)",
"Bash(ls -la \"/c/Users/User/Documents/QR-master/src/app/\\(main\\)/\\(marketing\\)/\")",
"Bash(npx tsc:*)"
],
"deny": [],
"ask": []
},
"enabledMcpjsonServers": [
"firecrawl",
"apify"
]
}

View File

@@ -1,131 +1,131 @@
# QR Master Internal RevOps Export API
This endpoint exposes read-only QR Master admin/product data for trusted automations such as Hermes cron jobs.
## Endpoint
```http
GET /api/internal/revops-export
Authorization: Bearer <INTERNAL_API_SECRET>
```
The endpoint accepts `INTERNAL_API_SECRET` first, with `CRON_SECRET` as a fallback for compatibility.
## Purpose
QR Master only exports structured data. It does **not** perform AI analysis, web research, email drafting, or lead outreach.
Hermes is responsible for:
- calling this API on a schedule,
- researching public websites/domains with its tools,
- deciding which leads are worth manual outreach,
- drafting personalized emails,
- sending Timo an internal briefing,
- never auto-sending messages to QR Master users.
## Query Parameters
| Parameter | Example | Notes |
|---|---|---|
| `range` | `24h`, `7d`, `30d`, `all` | Defaults to `24h`. |
| `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. |
| `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. |
## Example Calls
Daily Hermes lead research context:
```bash
curl -H "Authorization: Bearer $INTERNAL_API_SECRET" \
"https://www.qrmaster.net/api/internal/revops-export?range=24h&limit=100"
```
Weekly progress context:
```bash
curl -H "Authorization: Bearer $INTERNAL_API_SECRET" \
"https://www.qrmaster.net/api/internal/revops-export?range=7d&limit=200"
```
Local development:
```bash
curl -H "Authorization: Bearer $INTERNAL_API_SECRET" \
"http://localhost:3050/api/internal/revops-export?range=24h"
```
## Response Shape
Top-level fields:
```json
{
"ok": true,
"generatedAt": "2026-07-01T18:00:00.000Z",
"endpoint": "/api/internal/revops-export",
"range": {
"preset": "24h",
"from": "2026-06-30T18:00:00.000Z",
"to": "2026-07-01T18:00:00.000Z"
},
"overview": {},
"breakdowns": {},
"users": [],
"leads": [],
"newsletterSubscriptions": []
}
```
Each exported user includes:
- identity: `id`, `name`, `email`, `emailDomain`, `createdAt`, `updatedAt`
- plan/subscription flags
- attribution fields
- onboarding fields
- lifecycle scores and timestamps
- aggregate stats
- QR codes, content, public path, and scan summaries
- integrations
- recent lifecycle logs
## Safety Rules for Hermes Jobs
Hermes should treat this API as internal source data and follow these rules:
1. Do not send outreach emails directly to leads.
2. Send only an internal briefing/draft pack to Timo.
3. Use public web research only when it improves personalization.
4. Do not tell leads they were researched.
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.
7. Prefer business-use signals: QR content type, destination, scans, onboarding use case, domain, company website, landing path.
## Suggested Hermes Cron Jobs
### Daily QR Master Lead Research
Schedule: daily, evening.
Prompt should:
1. call `/api/internal/revops-export?range=24h`,
2. identify promising leads,
3. research public company/domain signals,
4. draft personalized outreach emails from Timo,
5. send Timo an internal email briefing,
6. never send emails to leads.
### Weekly QR Master Progress Review
Schedule: Sunday evening.
Prompt should:
1. call `/api/internal/revops-export?range=7d`,
2. summarize users, QR creation, scans, activations, paid/upgrades,
3. identify bottlenecks,
4. suggest next-week goals,
5. email Timo the review.
# QR Master Internal RevOps Export API
This endpoint exposes read-only QR Master admin/product data for trusted automations such as Hermes cron jobs.
## Endpoint
```http
GET /api/internal/revops-export
Authorization: Bearer <INTERNAL_API_SECRET>
```
The endpoint accepts `INTERNAL_API_SECRET` first, with `CRON_SECRET` as a fallback for compatibility.
## Purpose
QR Master only exports structured data. It does **not** perform AI analysis, web research, email drafting, or lead outreach.
Hermes is responsible for:
- calling this API on a schedule,
- researching public websites/domains with its tools,
- deciding which leads are worth manual outreach,
- drafting personalized emails,
- sending Timo an internal briefing,
- never auto-sending messages to QR Master users.
## Query Parameters
| Parameter | Example | Notes |
|---|---|---|
| `range` | `24h`, `7d`, `30d`, `all` | Defaults to `24h`. |
| `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. |
| `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. |
## Example Calls
Daily Hermes lead research context:
```bash
curl -H "Authorization: Bearer $INTERNAL_API_SECRET" \
"https://www.qrmaster.net/api/internal/revops-export?range=24h&limit=100"
```
Weekly progress context:
```bash
curl -H "Authorization: Bearer $INTERNAL_API_SECRET" \
"https://www.qrmaster.net/api/internal/revops-export?range=7d&limit=200"
```
Local development:
```bash
curl -H "Authorization: Bearer $INTERNAL_API_SECRET" \
"http://localhost:3050/api/internal/revops-export?range=24h"
```
## Response Shape
Top-level fields:
```json
{
"ok": true,
"generatedAt": "2026-07-01T18:00:00.000Z",
"endpoint": "/api/internal/revops-export",
"range": {
"preset": "24h",
"from": "2026-06-30T18:00:00.000Z",
"to": "2026-07-01T18:00:00.000Z"
},
"overview": {},
"breakdowns": {},
"users": [],
"leads": [],
"newsletterSubscriptions": []
}
```
Each exported user includes:
- identity: `id`, `name`, `email`, `emailDomain`, `createdAt`, `updatedAt`
- plan/subscription flags
- attribution fields
- onboarding fields
- lifecycle scores and timestamps
- aggregate stats
- QR codes, content, public path, and scan summaries
- integrations
- recent lifecycle logs
## Safety Rules for Hermes Jobs
Hermes should treat this API as internal source data and follow these rules:
1. Do not send outreach emails directly to leads.
2. Send only an internal briefing/draft pack to Timo.
3. Use public web research only when it improves personalization.
4. Do not tell leads they were researched.
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.
7. Prefer business-use signals: QR content type, destination, scans, onboarding use case, domain, company website, landing path.
## Suggested Hermes Cron Jobs
### Daily QR Master Lead Research
Schedule: daily, evening.
Prompt should:
1. call `/api/internal/revops-export?range=24h`,
2. identify promising leads,
3. research public company/domain signals,
4. draft personalized outreach emails from Timo,
5. send Timo an internal email briefing,
6. never send emails to leads.
### Weekly QR Master Progress Review
Schedule: Sunday evening.
Prompt should:
1. call `/api/internal/revops-export?range=7d`,
2. summarize users, QR creation, scans, activations, paid/upgrades,
3. identify bottlenecks,
4. suggest next-week goals,
5. email Timo the review.

View File

@@ -27,12 +27,12 @@ REDIS_URL=redis://redis:6379
# Used for hashing IP addresses in analytics
IP_SALT=your-ip-salt-here-change-in-production
# Features
ENABLE_DEMO=false
# SEO Configuration
# Set to 'true' in production to allow search engine indexing
NEXT_PUBLIC_INDEXABLE=true
# Features
ENABLE_DEMO=false
# SEO Configuration
# Set to 'true' in production to allow search engine indexing
NEXT_PUBLIC_INDEXABLE=true
# Stripe Payment Configuration (Optional - for subscription payments)
# Get your keys from: https://dashboard.stripe.com/apikeys
@@ -48,3 +48,10 @@ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
# Analytics (Optional - PostHog)
NEXT_PUBLIC_POSTHOG_KEY=
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=

View File

@@ -11,11 +11,11 @@ datasource db {
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
password String?
model User {
id String @id @default(cuid())
email String @unique
name String?
password String?
image String?
emailVerified DateTime?
createdAt DateTime @default(now())
@@ -32,56 +32,56 @@ model User {
resetPasswordToken String? @unique
resetPasswordExpires DateTime?
// Retention email tracking
activationNudgeSentAt DateTime?
upgradeNudgeSentAt DateTime?
thirtyDayNudgeSentAt DateTime?
// RevOps attribution
signupSource String?
signupSourceSelfReported String?
signupMedium String?
signupCampaign String?
signupContent String?
signupTerm String?
signupReferrer String?
signupLandingPath String?
signupFirstSeenAt DateTime?
emailDomain String?
// Onboarding and qualification
primaryUseCase String?
primaryGoal String?
jobRole String?
companyName String?
companyWebsite String?
teamSizeBucket String?
onboardingStartedAt DateTime?
sourceConfirmedAt DateTime?
useCaseSelectedAt DateTime?
goalSelectedAt DateTime?
profileCompletedAt DateTime?
firstQrCreatedAt DateTime?
firstDynamicQrAt DateTime?
firstStaticQrAt DateTime?
firstScanAt DateTime?
activationAt DateTime?
onboardingCompletedAt DateTime?
// RevOps scoring
fitScore Int @default(0)
intentScore Int @default(0)
leadScore Int @default(0)
lifecycleStage String @default("cold")
lastQualifiedAt DateTime?
lastScoredAt DateTime?
qrCodes QRCode[]
integrations Integration[]
accounts Account[]
sessions Session[]
lifecycleLogs UserLifecycleLog[]
}
// Retention email tracking
activationNudgeSentAt DateTime?
upgradeNudgeSentAt DateTime?
thirtyDayNudgeSentAt DateTime?
// RevOps attribution
signupSource String?
signupSourceSelfReported String?
signupMedium String?
signupCampaign String?
signupContent String?
signupTerm String?
signupReferrer String?
signupLandingPath String?
signupFirstSeenAt DateTime?
emailDomain String?
// Onboarding and qualification
primaryUseCase String?
primaryGoal String?
jobRole String?
companyName String?
companyWebsite String?
teamSizeBucket String?
onboardingStartedAt DateTime?
sourceConfirmedAt DateTime?
useCaseSelectedAt DateTime?
goalSelectedAt DateTime?
profileCompletedAt DateTime?
firstQrCreatedAt DateTime?
firstDynamicQrAt DateTime?
firstStaticQrAt DateTime?
firstScanAt DateTime?
activationAt DateTime?
onboardingCompletedAt DateTime?
// RevOps scoring
fitScore Int @default(0)
intentScore Int @default(0)
leadScore Int @default(0)
lifecycleStage String @default("cold")
lastQualifiedAt DateTime?
lastScoredAt DateTime?
qrCodes QRCode[]
integrations Integration[]
accounts Account[]
sessions Session[]
lifecycleLogs UserLifecycleLog[]
}
enum Plan {
FREE
@@ -189,7 +189,7 @@ model QRScan {
@@index([qrId, ts])
}
model Integration {
model Integration {
id String @id @default(cuid())
userId String
provider String
@@ -198,22 +198,35 @@ model Integration {
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)
}
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model TiktokIntegration {
id String @id @default(cuid())
accountKey String @unique
openId String
accessToken String
refreshToken String
scope String?
accessTokenExpiresAt DateTime
refreshTokenExpiresAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
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 {
id String @id @default(cuid())

View File

@@ -1,147 +1,147 @@
import React from 'react';
import Link from 'next/link';
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
export const metadata = {
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.',
openGraph: {
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.',
url: 'https://www.qrmaster.net/terms',
type: 'website',
images: ['/og-image.png'],
},
};
export default function TermsPage() {
return (
<div className="min-h-screen bg-white py-12">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl">
<div className="mb-8">
<Link href="/" className="text-primary-600 hover:text-primary-700 font-medium">
Back to Home
</Link>
</div>
<h1 className="text-4xl font-bold text-gray-900 mb-4">Terms of Service</h1>
<p className="text-gray-600 mb-8">Last updated: July 2026</p>
<div className="prose prose-lg max-w-none">
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">1. Acceptance of Terms</h2>
<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
bound by these Terms of Service. If you do not agree to these terms, please do not use our services.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">2. Description of Service</h2>
<p className="text-gray-700 mb-4">
QR Master 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
time.
</p>
</section>
<section className="mb-8">
<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">
<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 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>
</ul>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">4. Subscriptions &amp; Payments</h2>
<p className="text-gray-700 mb-4">
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
the customer billing portal in your account settings.
</p>
<p className="text-gray-700 mb-4">
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.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">5. Acceptable Use</h2>
<p className="text-gray-700 mb-4">You agree not to use QR Master to:</p>
<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>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>Interfere with or disrupt the integrity or performance of our services</li>
<li>Reverse engineer or resell access to our services without authorization</li>
</ul>
<p className="text-gray-700 mb-4">
We reserve the right to disable QR codes or accounts that violate this policy.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">6. Intellectual Property</h2>
<p className="text-gray-700 mb-4">
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
destinations you associate with your own QR codes.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">7. Termination</h2>
<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
services. You may stop using our services and delete your account at any time.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">8. Disclaimers &amp; Limitation of Liability</h2>
<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
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
consequential damages arising from your use of the services.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">9. Changes to These Terms</h2>
<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
changes are made. Continued use of our services after changes take effect constitutes acceptance of the
revised terms.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">10. Contact Us</h2>
<p className="text-gray-700 mb-4">
If you have questions about these Terms of Service, please contact us:
</p>
<div className="bg-gray-50 p-6 rounded-lg">
<p className="text-gray-700 mb-2">
<strong>Email:</strong>{' '}
<ObfuscatedMailto email="support@qrmaster.net" className="text-primary-600 hover:text-primary-700" />
</p>
<p className="text-gray-700 mb-2"><strong>Website:</strong> <a href="/" className="text-primary-600 hover:text-primary-700">qrmaster.net</a></p>
</div>
</section>
</div>
<div className="mt-12 pt-8 border-t border-gray-200">
<p className="text-gray-600 text-center">
<Link href="/" className="text-primary-600 hover:text-primary-700">
Back to Home
</Link>
</p>
</div>
</div>
</div>
);
}
import React from 'react';
import Link from 'next/link';
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
export const metadata = {
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.',
openGraph: {
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.',
url: 'https://www.qrmaster.net/terms',
type: 'website',
images: ['/og-image.png'],
},
};
export default function TermsPage() {
return (
<div className="min-h-screen bg-white py-12">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl">
<div className="mb-8">
<Link href="/" className="text-primary-600 hover:text-primary-700 font-medium">
Back to Home
</Link>
</div>
<h1 className="text-4xl font-bold text-gray-900 mb-4">Terms of Service</h1>
<p className="text-gray-600 mb-8">Last updated: July 2026</p>
<div className="prose prose-lg max-w-none">
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">1. Acceptance of Terms</h2>
<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
bound by these Terms of Service. If you do not agree to these terms, please do not use our services.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">2. Description of Service</h2>
<p className="text-gray-700 mb-4">
QR Master 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
time.
</p>
</section>
<section className="mb-8">
<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">
<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 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>
</ul>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">4. Subscriptions &amp; Payments</h2>
<p className="text-gray-700 mb-4">
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
the customer billing portal in your account settings.
</p>
<p className="text-gray-700 mb-4">
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.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">5. Acceptable Use</h2>
<p className="text-gray-700 mb-4">You agree not to use QR Master to:</p>
<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>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>Interfere with or disrupt the integrity or performance of our services</li>
<li>Reverse engineer or resell access to our services without authorization</li>
</ul>
<p className="text-gray-700 mb-4">
We reserve the right to disable QR codes or accounts that violate this policy.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">6. Intellectual Property</h2>
<p className="text-gray-700 mb-4">
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
destinations you associate with your own QR codes.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">7. Termination</h2>
<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
services. You may stop using our services and delete your account at any time.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">8. Disclaimers &amp; Limitation of Liability</h2>
<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
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
consequential damages arising from your use of the services.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">9. Changes to These Terms</h2>
<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
changes are made. Continued use of our services after changes take effect constitutes acceptance of the
revised terms.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">10. Contact Us</h2>
<p className="text-gray-700 mb-4">
If you have questions about these Terms of Service, please contact us:
</p>
<div className="bg-gray-50 p-6 rounded-lg">
<p className="text-gray-700 mb-2">
<strong>Email:</strong>{' '}
<ObfuscatedMailto email="support@qrmaster.net" className="text-primary-600 hover:text-primary-700" />
</p>
<p className="text-gray-700 mb-2"><strong>Website:</strong> <a href="/" className="text-primary-600 hover:text-primary-700">qrmaster.net</a></p>
</div>
</section>
</div>
<div className="mt-12 pt-8 border-t border-gray-200">
<p className="text-gray-600 text-center">
<Link href="/" className="text-primary-600 hover:text-primary-700">
Back to Home
</Link>
</p>
</div>
</div>
</div>
);
}

View File

@@ -1,463 +1,463 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getEmailDomain } from '@/lib/revops';
import { getMetricSnapshot, getUpgradeCandidateBadges } from '@/lib/revops-server';
export const dynamic = 'force-dynamic';
type RangePreset = '24h' | '7d' | '30d' | 'all';
const DEFAULT_LIMIT = 100;
const MAX_LIMIT = 500;
function isAuthorized(request: NextRequest): boolean {
const authHeader = request.headers.get('authorization');
const token = process.env.INTERNAL_API_SECRET || process.env.CRON_SECRET;
if (!token) return false;
return authHeader === `Bearer ${token}`;
}
function parsePositiveInt(value: string | null, fallback: number, max: number) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
return Math.min(Math.floor(parsed), max);
}
function parseDate(value: string | null): Date | null {
if (!value) return null;
const parsed = new Date(value);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
function resolveDateRange(request: NextRequest) {
const now = new Date();
const explicitFrom = parseDate(request.nextUrl.searchParams.get('from'));
const explicitTo = parseDate(request.nextUrl.searchParams.get('to'));
const range = (request.nextUrl.searchParams.get('range') || '24h') as RangePreset;
if (explicitFrom || explicitTo) {
return {
from: explicitFrom,
to: explicitTo ?? now,
preset: 'custom',
};
}
if (range === 'all') {
return { from: null, to: now, preset: range };
}
const hours = range === '7d' ? 24 * 7 : range === '30d' ? 24 * 30 : 24;
return {
from: new Date(now.getTime() - hours * 60 * 60 * 1000),
to: now,
preset: range,
};
}
function withinRange(date: Date | null | undefined, from: Date | null, to: Date) {
if (!date) return false;
return (!from || date >= from) && date <= to;
}
function jsonDate(value: Date | null | undefined) {
return value ? value.toISOString() : null;
}
function safeJson(value: unknown) {
return value ?? null;
}
function summarizeScans(scans: Array<{
ts: Date;
isUnique: boolean;
country: string | null;
device: string | null;
os: string | null;
referrer: string | null;
utmSource: string | null;
utmMedium: string | null;
utmCampaign: string | null;
}>) {
const countries = new Map<string, number>();
const devices = new Map<string, number>();
const os = new Map<string, number>();
const referrers = new Map<string, number>();
const utmSources = new Map<string, number>();
const utmCampaigns = new Map<string, number>();
const scansByDay = new Map<string, number>();
for (const scan of scans) {
const day = scan.ts.toISOString().slice(0, 10);
scansByDay.set(day, (scansByDay.get(day) || 0) + 1);
const increment = (map: Map<string, number>, key?: string | null) => {
if (!key) return;
map.set(key, (map.get(key) || 0) + 1);
};
increment(countries, scan.country);
increment(devices, scan.device);
increment(os, scan.os);
increment(referrers, scan.referrer);
increment(utmSources, scan.utmSource);
increment(utmCampaigns, scan.utmCampaign);
}
const top = (map: Map<string, number>) =>
Array.from(map.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([key, count]) => ({ key, count }));
return {
total: scans.length,
unique: scans.filter((scan) => scan.isUnique).length,
firstScanAt: jsonDate(scans[0]?.ts),
lastScanAt: jsonDate(scans[scans.length - 1]?.ts),
byDay: Array.from(scansByDay.entries()).map(([date, count]) => ({ date, count })),
topCountries: top(countries),
topDevices: top(devices),
topOperatingSystems: top(os),
topReferrers: top(referrers),
topUtmSources: top(utmSources),
topUtmCampaigns: top(utmCampaigns),
};
}
export async function GET(request: NextRequest) {
try {
if (!isAuthorized(request)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { from, to, preset } = resolveDateRange(request);
const limit = parsePositiveInt(request.nextUrl.searchParams.get('limit'), DEFAULT_LIMIT, MAX_LIMIT);
const includeAllUsers = request.nextUrl.searchParams.get('allUsers') === 'true' || preset === 'all';
const users = await db.user.findMany({
where: includeAllUsers || !from ? undefined : {
OR: [
{ createdAt: { gte: from, lte: to } },
{ updatedAt: { gte: from, lte: to } },
{ firstQrCreatedAt: { gte: from, lte: to } },
{ firstDynamicQrAt: { gte: from, lte: to } },
{ firstStaticQrAt: { gte: from, lte: to } },
{ firstScanAt: { gte: from, lte: to } },
{ activationAt: { gte: from, lte: to } },
{ lastQualifiedAt: { gte: from, lte: to } },
{ lastScoredAt: { gte: from, lte: to } },
{ qrCodes: { some: { createdAt: { gte: from, lte: to } } } },
{ qrCodes: { some: { scans: { some: { ts: { gte: from, lte: to } } } } } },
],
},
orderBy: [
{ leadScore: 'desc' },
{ createdAt: 'desc' },
],
take: limit,
select: {
id: true,
email: true,
name: true,
image: true,
emailVerified: true,
createdAt: true,
updatedAt: true,
plan: true,
stripeCustomerId: true,
stripeSubscriptionId: true,
stripePriceId: true,
stripeCurrentPeriodEnd: true,
signupSource: true,
signupSourceSelfReported: true,
signupMedium: true,
signupCampaign: true,
signupContent: true,
signupTerm: true,
signupReferrer: true,
signupLandingPath: true,
signupFirstSeenAt: true,
emailDomain: true,
primaryUseCase: true,
primaryGoal: true,
jobRole: true,
companyName: true,
companyWebsite: true,
teamSizeBucket: true,
onboardingStartedAt: true,
sourceConfirmedAt: true,
useCaseSelectedAt: true,
goalSelectedAt: true,
profileCompletedAt: true,
firstQrCreatedAt: true,
firstDynamicQrAt: true,
firstStaticQrAt: true,
firstScanAt: true,
activationAt: true,
onboardingCompletedAt: true,
fitScore: true,
intentScore: true,
leadScore: true,
lifecycleStage: true,
lastQualifiedAt: true,
lastScoredAt: true,
qrCodes: {
orderBy: { createdAt: 'desc' },
take: 50,
select: {
id: true,
title: true,
type: true,
contentType: true,
content: true,
tags: true,
status: true,
style: true,
slug: true,
createdAt: true,
updatedAt: true,
scans: {
orderBy: { ts: 'asc' },
take: 500,
select: {
id: true,
ts: true,
device: true,
os: true,
country: true,
referrer: true,
utmSource: true,
utmMedium: true,
utmCampaign: true,
isUnique: true,
},
},
_count: {
select: { scans: true },
},
},
},
lifecycleLogs: {
orderBy: { createdAt: 'desc' },
take: 10,
select: {
fromStage: true,
toStage: true,
fitScore: true,
intentScore: true,
leadScore: true,
reason: true,
createdAt: true,
},
},
integrations: {
select: {
provider: true,
status: true,
createdAt: true,
updatedAt: true,
},
},
},
});
const leads = await db.lead.findMany({
where: from ? { createdAt: { gte: from, lte: to } } : undefined,
orderBy: { createdAt: 'desc' },
take: limit,
select: {
id: true,
email: true,
source: true,
reprintCost: true,
updatesPerYear: true,
annualSavings: true,
createdAt: true,
updatedAt: true,
},
});
const newsletterSubscriptions = await db.newsletterSubscription.findMany({
where: from ? { createdAt: { gte: from, lte: to } } : undefined,
orderBy: { createdAt: 'desc' },
take: limit,
select: {
id: true,
email: true,
source: true,
status: true,
createdAt: true,
updatedAt: true,
},
});
const exportedUsers = users.map((user) => {
const metrics = getMetricSnapshot(user.qrCodes);
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));
return {
id: user.id,
name: user.name,
email: user.email,
emailDomain: user.emailDomain || getEmailDomain(user.email),
image: user.image,
emailVerified: jsonDate(user.emailVerified),
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString(),
plan: user.plan,
stripe: {
hasCustomer: Boolean(user.stripeCustomerId),
hasSubscription: Boolean(user.stripeSubscriptionId),
priceId: user.stripePriceId,
currentPeriodEnd: jsonDate(user.stripeCurrentPeriodEnd),
},
attribution: {
signupSource: user.signupSource,
signupSourceSelfReported: user.signupSourceSelfReported,
signupMedium: user.signupMedium,
signupCampaign: user.signupCampaign,
signupContent: user.signupContent,
signupTerm: user.signupTerm,
signupReferrer: user.signupReferrer,
signupLandingPath: user.signupLandingPath,
signupFirstSeenAt: jsonDate(user.signupFirstSeenAt),
},
onboarding: {
primaryUseCase: user.primaryUseCase,
primaryGoal: user.primaryGoal,
jobRole: user.jobRole,
companyName: user.companyName,
companyWebsite: user.companyWebsite,
teamSizeBucket: user.teamSizeBucket,
onboardingStartedAt: jsonDate(user.onboardingStartedAt),
sourceConfirmedAt: jsonDate(user.sourceConfirmedAt),
useCaseSelectedAt: jsonDate(user.useCaseSelectedAt),
goalSelectedAt: jsonDate(user.goalSelectedAt),
profileCompletedAt: jsonDate(user.profileCompletedAt),
onboardingCompletedAt: jsonDate(user.onboardingCompletedAt),
},
lifecycle: {
fitScore: user.fitScore,
intentScore: user.intentScore,
leadScore: user.leadScore,
lifecycleStage: user.lifecycleStage,
firstQrCreatedAt: jsonDate(user.firstQrCreatedAt),
firstDynamicQrAt: jsonDate(user.firstDynamicQrAt),
firstStaticQrAt: jsonDate(user.firstStaticQrAt),
firstScanAt: jsonDate(user.firstScanAt),
activationAt: jsonDate(user.activationAt),
lastQualifiedAt: jsonDate(user.lastQualifiedAt),
lastScoredAt: jsonDate(user.lastScoredAt),
upgradeBadges: getUpgradeCandidateBadges(user, metrics),
logs: user.lifecycleLogs.map((log) => ({
...log,
createdAt: log.createdAt.toISOString(),
})),
},
stats: {
qrCount: metrics.qrCount,
dynamicQrCount: metrics.dynamicQrCount,
staticQrCount: metrics.qrCount - metrics.dynamicQrCount,
contentTypeCount: metrics.contentTypeCount,
businessishTypeCount: metrics.businessishTypeCount,
scanCount: metrics.scanCount,
scansInRange: rangeScans.length,
uniqueScansInRange: rangeScans.filter((scan) => scan.isUnique).length,
},
scans: summarizeScans(allScans),
scansInRange: summarizeScans(rangeScans),
qrCodes: user.qrCodes.map((qr) => {
const qrScansInRange = qr.scans.filter((scan) => withinRange(scan.ts, from, to));
return {
id: qr.id,
title: qr.title,
type: qr.type,
contentType: qr.contentType,
content: safeJson(qr.content),
tags: qr.tags,
status: qr.status,
style: safeJson(qr.style),
slug: qr.slug,
publicPath: `/r/${qr.slug}`,
createdAt: qr.createdAt.toISOString(),
updatedAt: qr.updatedAt.toISOString(),
scanCount: qr._count.scans,
scansInRange: qrScansInRange.length,
firstScanAt: jsonDate(qr.scans[0]?.ts),
lastScanAt: jsonDate(qr.scans[qr.scans.length - 1]?.ts),
scanSummary: summarizeScans(qr.scans),
scanSummaryInRange: summarizeScans(qrScansInRange),
};
}),
integrations: user.integrations.map((integration) => ({
...integration,
createdAt: integration.createdAt.toISOString(),
updatedAt: integration.updatedAt.toISOString(),
})),
};
});
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 scansInRange = exportedUsers.reduce((sum, user) => sum + user.stats.scansInRange, 0);
const uniqueScansInRange = exportedUsers.reduce((sum, user) => sum + user.stats.uniqueScansInRange, 0);
const contentTypeCounts = new Map<string, number>();
for (const qr of allQrs) {
contentTypeCounts.set(qr.contentType, (contentTypeCounts.get(qr.contentType) || 0) + 1);
}
return NextResponse.json({
ok: true,
generatedAt: new Date().toISOString(),
endpoint: '/api/internal/revops-export',
range: {
preset,
from: jsonDate(from),
to: to.toISOString(),
},
limits: {
requestedUserLimit: limit,
maxUserLimit: MAX_LIMIT,
qrLimitPerUser: 50,
scanLimitPerQr: 500,
},
overview: {
exportedUsers: exportedUsers.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,
paidUsers: exportedUsers.filter((user) => user.plan === 'PRO' || user.plan === 'BUSINESS').length,
hotUsers: exportedUsers.filter((user) => user.lifecycle.lifecycleStage === 'hot').length,
upgradeCandidates: exportedUsers.filter((user) => user.lifecycle.lifecycleStage === 'upgrade_candidate').length,
qrCodesTotal: allQrs.length,
qrCodesCreatedInRange: qrsInRange.length,
scansInRange,
uniqueScansInRange,
reprintCalculatorLeadsInRange: leads.length,
newsletterSubscriptionsInRange: newsletterSubscriptions.length,
},
breakdowns: {
qrContentTypes: Array.from(contentTypeCounts.entries())
.sort((a, b) => b[1] - a[1])
.map(([contentType, count]) => ({ contentType, count })),
},
users: exportedUsers,
leads: leads.map((lead) => ({
...lead,
createdAt: lead.createdAt.toISOString(),
updatedAt: lead.updatedAt.toISOString(),
})),
newsletterSubscriptions: newsletterSubscriptions.map((subscription) => ({
...subscription,
createdAt: subscription.createdAt.toISOString(),
updatedAt: subscription.updatedAt.toISOString(),
})),
});
} catch (error) {
console.error('Error exporting internal RevOps data:', error);
return NextResponse.json({ error: 'Failed to export RevOps data' }, { status: 500 });
}
}
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getEmailDomain } from '@/lib/revops';
import { getMetricSnapshot, getUpgradeCandidateBadges } from '@/lib/revops-server';
export const dynamic = 'force-dynamic';
type RangePreset = '24h' | '7d' | '30d' | 'all';
const DEFAULT_LIMIT = 100;
const MAX_LIMIT = 500;
function isAuthorized(request: NextRequest): boolean {
const authHeader = request.headers.get('authorization');
const token = process.env.INTERNAL_API_SECRET || process.env.CRON_SECRET;
if (!token) return false;
return authHeader === `Bearer ${token}`;
}
function parsePositiveInt(value: string | null, fallback: number, max: number) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
return Math.min(Math.floor(parsed), max);
}
function parseDate(value: string | null): Date | null {
if (!value) return null;
const parsed = new Date(value);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
function resolveDateRange(request: NextRequest) {
const now = new Date();
const explicitFrom = parseDate(request.nextUrl.searchParams.get('from'));
const explicitTo = parseDate(request.nextUrl.searchParams.get('to'));
const range = (request.nextUrl.searchParams.get('range') || '24h') as RangePreset;
if (explicitFrom || explicitTo) {
return {
from: explicitFrom,
to: explicitTo ?? now,
preset: 'custom',
};
}
if (range === 'all') {
return { from: null, to: now, preset: range };
}
const hours = range === '7d' ? 24 * 7 : range === '30d' ? 24 * 30 : 24;
return {
from: new Date(now.getTime() - hours * 60 * 60 * 1000),
to: now,
preset: range,
};
}
function withinRange(date: Date | null | undefined, from: Date | null, to: Date) {
if (!date) return false;
return (!from || date >= from) && date <= to;
}
function jsonDate(value: Date | null | undefined) {
return value ? value.toISOString() : null;
}
function safeJson(value: unknown) {
return value ?? null;
}
function summarizeScans(scans: Array<{
ts: Date;
isUnique: boolean;
country: string | null;
device: string | null;
os: string | null;
referrer: string | null;
utmSource: string | null;
utmMedium: string | null;
utmCampaign: string | null;
}>) {
const countries = new Map<string, number>();
const devices = new Map<string, number>();
const os = new Map<string, number>();
const referrers = new Map<string, number>();
const utmSources = new Map<string, number>();
const utmCampaigns = new Map<string, number>();
const scansByDay = new Map<string, number>();
for (const scan of scans) {
const day = scan.ts.toISOString().slice(0, 10);
scansByDay.set(day, (scansByDay.get(day) || 0) + 1);
const increment = (map: Map<string, number>, key?: string | null) => {
if (!key) return;
map.set(key, (map.get(key) || 0) + 1);
};
increment(countries, scan.country);
increment(devices, scan.device);
increment(os, scan.os);
increment(referrers, scan.referrer);
increment(utmSources, scan.utmSource);
increment(utmCampaigns, scan.utmCampaign);
}
const top = (map: Map<string, number>) =>
Array.from(map.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([key, count]) => ({ key, count }));
return {
total: scans.length,
unique: scans.filter((scan) => scan.isUnique).length,
firstScanAt: jsonDate(scans[0]?.ts),
lastScanAt: jsonDate(scans[scans.length - 1]?.ts),
byDay: Array.from(scansByDay.entries()).map(([date, count]) => ({ date, count })),
topCountries: top(countries),
topDevices: top(devices),
topOperatingSystems: top(os),
topReferrers: top(referrers),
topUtmSources: top(utmSources),
topUtmCampaigns: top(utmCampaigns),
};
}
export async function GET(request: NextRequest) {
try {
if (!isAuthorized(request)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { from, to, preset } = resolveDateRange(request);
const limit = parsePositiveInt(request.nextUrl.searchParams.get('limit'), DEFAULT_LIMIT, MAX_LIMIT);
const includeAllUsers = request.nextUrl.searchParams.get('allUsers') === 'true' || preset === 'all';
const users = await db.user.findMany({
where: includeAllUsers || !from ? undefined : {
OR: [
{ createdAt: { gte: from, lte: to } },
{ updatedAt: { gte: from, lte: to } },
{ firstQrCreatedAt: { gte: from, lte: to } },
{ firstDynamicQrAt: { gte: from, lte: to } },
{ firstStaticQrAt: { gte: from, lte: to } },
{ firstScanAt: { gte: from, lte: to } },
{ activationAt: { gte: from, lte: to } },
{ lastQualifiedAt: { gte: from, lte: to } },
{ lastScoredAt: { gte: from, lte: to } },
{ qrCodes: { some: { createdAt: { gte: from, lte: to } } } },
{ qrCodes: { some: { scans: { some: { ts: { gte: from, lte: to } } } } } },
],
},
orderBy: [
{ leadScore: 'desc' },
{ createdAt: 'desc' },
],
take: limit,
select: {
id: true,
email: true,
name: true,
image: true,
emailVerified: true,
createdAt: true,
updatedAt: true,
plan: true,
stripeCustomerId: true,
stripeSubscriptionId: true,
stripePriceId: true,
stripeCurrentPeriodEnd: true,
signupSource: true,
signupSourceSelfReported: true,
signupMedium: true,
signupCampaign: true,
signupContent: true,
signupTerm: true,
signupReferrer: true,
signupLandingPath: true,
signupFirstSeenAt: true,
emailDomain: true,
primaryUseCase: true,
primaryGoal: true,
jobRole: true,
companyName: true,
companyWebsite: true,
teamSizeBucket: true,
onboardingStartedAt: true,
sourceConfirmedAt: true,
useCaseSelectedAt: true,
goalSelectedAt: true,
profileCompletedAt: true,
firstQrCreatedAt: true,
firstDynamicQrAt: true,
firstStaticQrAt: true,
firstScanAt: true,
activationAt: true,
onboardingCompletedAt: true,
fitScore: true,
intentScore: true,
leadScore: true,
lifecycleStage: true,
lastQualifiedAt: true,
lastScoredAt: true,
qrCodes: {
orderBy: { createdAt: 'desc' },
take: 50,
select: {
id: true,
title: true,
type: true,
contentType: true,
content: true,
tags: true,
status: true,
style: true,
slug: true,
createdAt: true,
updatedAt: true,
scans: {
orderBy: { ts: 'asc' },
take: 500,
select: {
id: true,
ts: true,
device: true,
os: true,
country: true,
referrer: true,
utmSource: true,
utmMedium: true,
utmCampaign: true,
isUnique: true,
},
},
_count: {
select: { scans: true },
},
},
},
lifecycleLogs: {
orderBy: { createdAt: 'desc' },
take: 10,
select: {
fromStage: true,
toStage: true,
fitScore: true,
intentScore: true,
leadScore: true,
reason: true,
createdAt: true,
},
},
integrations: {
select: {
provider: true,
status: true,
createdAt: true,
updatedAt: true,
},
},
},
});
const leads = await db.lead.findMany({
where: from ? { createdAt: { gte: from, lte: to } } : undefined,
orderBy: { createdAt: 'desc' },
take: limit,
select: {
id: true,
email: true,
source: true,
reprintCost: true,
updatesPerYear: true,
annualSavings: true,
createdAt: true,
updatedAt: true,
},
});
const newsletterSubscriptions = await db.newsletterSubscription.findMany({
where: from ? { createdAt: { gte: from, lte: to } } : undefined,
orderBy: { createdAt: 'desc' },
take: limit,
select: {
id: true,
email: true,
source: true,
status: true,
createdAt: true,
updatedAt: true,
},
});
const exportedUsers = users.map((user) => {
const metrics = getMetricSnapshot(user.qrCodes);
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));
return {
id: user.id,
name: user.name,
email: user.email,
emailDomain: user.emailDomain || getEmailDomain(user.email),
image: user.image,
emailVerified: jsonDate(user.emailVerified),
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString(),
plan: user.plan,
stripe: {
hasCustomer: Boolean(user.stripeCustomerId),
hasSubscription: Boolean(user.stripeSubscriptionId),
priceId: user.stripePriceId,
currentPeriodEnd: jsonDate(user.stripeCurrentPeriodEnd),
},
attribution: {
signupSource: user.signupSource,
signupSourceSelfReported: user.signupSourceSelfReported,
signupMedium: user.signupMedium,
signupCampaign: user.signupCampaign,
signupContent: user.signupContent,
signupTerm: user.signupTerm,
signupReferrer: user.signupReferrer,
signupLandingPath: user.signupLandingPath,
signupFirstSeenAt: jsonDate(user.signupFirstSeenAt),
},
onboarding: {
primaryUseCase: user.primaryUseCase,
primaryGoal: user.primaryGoal,
jobRole: user.jobRole,
companyName: user.companyName,
companyWebsite: user.companyWebsite,
teamSizeBucket: user.teamSizeBucket,
onboardingStartedAt: jsonDate(user.onboardingStartedAt),
sourceConfirmedAt: jsonDate(user.sourceConfirmedAt),
useCaseSelectedAt: jsonDate(user.useCaseSelectedAt),
goalSelectedAt: jsonDate(user.goalSelectedAt),
profileCompletedAt: jsonDate(user.profileCompletedAt),
onboardingCompletedAt: jsonDate(user.onboardingCompletedAt),
},
lifecycle: {
fitScore: user.fitScore,
intentScore: user.intentScore,
leadScore: user.leadScore,
lifecycleStage: user.lifecycleStage,
firstQrCreatedAt: jsonDate(user.firstQrCreatedAt),
firstDynamicQrAt: jsonDate(user.firstDynamicQrAt),
firstStaticQrAt: jsonDate(user.firstStaticQrAt),
firstScanAt: jsonDate(user.firstScanAt),
activationAt: jsonDate(user.activationAt),
lastQualifiedAt: jsonDate(user.lastQualifiedAt),
lastScoredAt: jsonDate(user.lastScoredAt),
upgradeBadges: getUpgradeCandidateBadges(user, metrics),
logs: user.lifecycleLogs.map((log) => ({
...log,
createdAt: log.createdAt.toISOString(),
})),
},
stats: {
qrCount: metrics.qrCount,
dynamicQrCount: metrics.dynamicQrCount,
staticQrCount: metrics.qrCount - metrics.dynamicQrCount,
contentTypeCount: metrics.contentTypeCount,
businessishTypeCount: metrics.businessishTypeCount,
scanCount: metrics.scanCount,
scansInRange: rangeScans.length,
uniqueScansInRange: rangeScans.filter((scan) => scan.isUnique).length,
},
scans: summarizeScans(allScans),
scansInRange: summarizeScans(rangeScans),
qrCodes: user.qrCodes.map((qr) => {
const qrScansInRange = qr.scans.filter((scan) => withinRange(scan.ts, from, to));
return {
id: qr.id,
title: qr.title,
type: qr.type,
contentType: qr.contentType,
content: safeJson(qr.content),
tags: qr.tags,
status: qr.status,
style: safeJson(qr.style),
slug: qr.slug,
publicPath: `/r/${qr.slug}`,
createdAt: qr.createdAt.toISOString(),
updatedAt: qr.updatedAt.toISOString(),
scanCount: qr._count.scans,
scansInRange: qrScansInRange.length,
firstScanAt: jsonDate(qr.scans[0]?.ts),
lastScanAt: jsonDate(qr.scans[qr.scans.length - 1]?.ts),
scanSummary: summarizeScans(qr.scans),
scanSummaryInRange: summarizeScans(qrScansInRange),
};
}),
integrations: user.integrations.map((integration) => ({
...integration,
createdAt: integration.createdAt.toISOString(),
updatedAt: integration.updatedAt.toISOString(),
})),
};
});
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 scansInRange = exportedUsers.reduce((sum, user) => sum + user.stats.scansInRange, 0);
const uniqueScansInRange = exportedUsers.reduce((sum, user) => sum + user.stats.uniqueScansInRange, 0);
const contentTypeCounts = new Map<string, number>();
for (const qr of allQrs) {
contentTypeCounts.set(qr.contentType, (contentTypeCounts.get(qr.contentType) || 0) + 1);
}
return NextResponse.json({
ok: true,
generatedAt: new Date().toISOString(),
endpoint: '/api/internal/revops-export',
range: {
preset,
from: jsonDate(from),
to: to.toISOString(),
},
limits: {
requestedUserLimit: limit,
maxUserLimit: MAX_LIMIT,
qrLimitPerUser: 50,
scanLimitPerQr: 500,
},
overview: {
exportedUsers: exportedUsers.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,
paidUsers: exportedUsers.filter((user) => user.plan === 'PRO' || user.plan === 'BUSINESS').length,
hotUsers: exportedUsers.filter((user) => user.lifecycle.lifecycleStage === 'hot').length,
upgradeCandidates: exportedUsers.filter((user) => user.lifecycle.lifecycleStage === 'upgrade_candidate').length,
qrCodesTotal: allQrs.length,
qrCodesCreatedInRange: qrsInRange.length,
scansInRange,
uniqueScansInRange,
reprintCalculatorLeadsInRange: leads.length,
newsletterSubscriptionsInRange: newsletterSubscriptions.length,
},
breakdowns: {
qrContentTypes: Array.from(contentTypeCounts.entries())
.sort((a, b) => b[1] - a[1])
.map(([contentType, count]) => ({ contentType, count })),
},
users: exportedUsers,
leads: leads.map((lead) => ({
...lead,
createdAt: lead.createdAt.toISOString(),
updatedAt: lead.updatedAt.toISOString(),
})),
newsletterSubscriptions: newsletterSubscriptions.map((subscription) => ({
...subscription,
createdAt: subscription.createdAt.toISOString(),
updatedAt: subscription.updatedAt.toISOString(),
})),
});
} catch (error) {
console.error('Error exporting internal RevOps data:', error);
return NextResponse.json({ error: 'Failed to export RevOps data' }, { status: 500 });
}
}

View 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);
}
}

View 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;
}

View 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 });
}
}

View File

@@ -1,442 +1,442 @@
{
"nav": {
"features": "Funktionen",
"pricing": "Preise",
"faq": "FAQ",
"blog": "Blog",
"login": "Anmelden",
"dashboard": "Dashboard",
"about": "Über uns",
"contact": "Kontakt",
"signup": "Registrieren",
"learn": "Lernen",
"create_qr": "QR erstellen",
"bulk_creation": "Massen-Erstellung",
"analytics": "Analytik",
"settings": "Einstellungen",
"cta": "Kostenlos starten",
"tools": "Kostenlose Tools",
"all_free": "Alle Generatoren sind 100% kostenlos",
"resources": "Ressourcen",
"all_industries": "Branchen"
},
"hero": {
"badge": "Kostenloser QR-Code-Generator",
"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.",
"features": [
"Keine Kreditkarte zum Starten erforderlich",
"QR-Codes für immer kostenlos erstellen",
"Erweiterte Verfolgung und Analytik",
"Individuelle Farben und Stile"
],
"cta_primary": "QR-Code kostenlos erstellen",
"cta_secondary": "Preise ansehen",
"engagement_badge": "Kostenlos für immer",
"get_started": "Loslegen",
"view_full_pricing": "Alle Preisdetails ansehen →"
},
"trust": {
"users": "Aktive Nutzer",
"codes": "QR-Codes erstellt",
"scans": "Scans verfolgt",
"countries": "Länder"
},
"industries": {
"restaurant": "Restaurant-Kette",
"tech": "Tech-Startup",
"realestate": "Immobilien",
"events": "Event-Agentur",
"retail": "Einzelhandel",
"healthcare": "Gesundheitswesen"
},
"templates": {
"title": "Mit einer Vorlage beginnen",
"restaurant": "Restaurant-Menü",
"business": "Visitenkarte",
"vcard": "Kontaktkarte",
"event": "Event-Ticket",
"use_template": "Vorlage verwenden →"
},
"generator": {
"title": "Sofortiger QR-Code-Generator",
"url_placeholder": "Geben Sie hier Ihre URL ein...",
"foreground": "Vordergrund",
"background": "Hintergrund",
"corners": "Ecken",
"size": "Größe",
"contrast_good": "Guter Kontrast",
"download_svg": "SVG herunterladen",
"download_png": "PNG herunterladen",
"save_track": "Speichern & Verfolgen",
"live_preview": "Live-Vorschau",
"demo_note": "Dies ist ein Demo-QR-Code"
},
"static_vs_dynamic": {
"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.",
"static": {
"title": "Statische QR-Codes",
"subtitle": "Immer kostenlos",
"description": "Perfekt für permanente Inhalte, die sich nie ändern",
"features": [
"Inhalt kann nicht bearbeitet werden",
"Keine Scan-Verfolgung",
"Funktioniert für immer",
"Kein Konto erforderlich"
]
},
"dynamic": {
"title": "Dynamische QR-Codes",
"subtitle": "Empfohlen",
"description": "Volle Kontrolle mit Tracking- und Bearbeitungsfunktionen",
"features": [
"Inhalt jederzeit bearbeiten",
"Erweiterte Analytik",
"Individuelles Branding",
"Bulk-Operationen"
]
}
},
"features": {
"title": "Alles was Sie brauchen, um professionelle QR-Codes zu erstellen",
"analytics": {
"title": "Erweiterte Analytik",
"description": "Verfolgen Sie Scans, Standorte, Geräte und Nutzerverhalten mit detaillierten Einblicken."
},
"customization": {
"title": "Vollständige Anpassung",
"description": "Branden Sie Ihre QR-Codes mit individuellen Farben, Logos und Styling-Optionen."
},
"unlimited": {
"title": "Unbegrenzte statische QR-Codes",
"description": "Erstellen Sie so viele statische QR-Codes wie Sie benötigen. Kostenlos für immer, ohne Limits."
},
"bulk": {
"title": "Bulk-Operationen",
"description": "Erstellen Sie hunderte von QR-Codes auf einmal mit CSV-Import und Batch-Verarbeitung."
},
"integrations": {
"title": "Integrationen",
"description": "Verbinden Sie sich mit Zapier, Airtable, Google Sheets und weiteren beliebten Tools."
},
"api": {
"title": "Entwickler-API",
"description": "Integrieren Sie QR-Code-Generierung in Ihre Anwendungen mit unserer REST-API."
},
"support": {
"title": "24/7 Support",
"description": "Erhalten Sie Hilfe, wenn Sie sie brauchen, mit unserem dedizierten Kundensupport-Team."
}
},
"pricing": {
"title": "Wählen Sie Ihren Plan",
"subtitle": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
"choose_plan": "Wählen Sie Ihren Plan",
"select_plan": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
"current_plan": "Aktueller Plan",
"upgrade_to": "Upgrade auf",
"downgrade_to_free": "Zu Kostenlos zurückstufen",
"most_popular": "Beliebteste",
"all_plans_note": "Alle Pläne beinhalten unbegrenzte statische QR-Codes und Basis-Anpassung.",
"free": {
"title": "Kostenlos",
"name": "Free",
"price": "€0",
"period": "für immer",
"features": [
"3 aktive dynamische QR-Codes (8 Typen verfügbar)",
"Unbegrenzte statische QR-Codes",
"Basis-Scan-Tracking",
"Standard QR-Design-Vorlagen"
]
},
"pro": {
"title": "Pro",
"name": "Pro",
"price": "€9",
"period": "pro Monat",
"badge": "Beliebteste",
"features": [
"50 dynamische QR-Codes",
"Unbegrenzte statische QR-Codes",
"Erweiterte Analytik (Scans, Geräte, Standorte)",
"Individuelles Branding (Farben)",
"Download als SVG/PNG"
]
},
"business": {
"title": "Business",
"name": "Business",
"price": "€29",
"period": "pro Monat",
"features": [
"500 dynamische QR-Codes",
"Unbegrenzte statische QR-Codes",
"Alles aus Pro",
"Massen-QR-Erstellung (bis zu 1.000)",
"Prioritäts-E-Mail-Support",
"Erweiterte Tracking & Insights"
]
},
"enterprise": {
"title": "Enterprise",
"name": "Enterprise",
"price": "Individuell",
"period": "",
"features": [
"∞ dynamische QR-Codes",
"Unbegrenzte statische QR-Codes",
"Alles aus Business",
"Eigener Account Manager"
],
"contact": "Kontakt aufnehmen"
}
},
"faq": {
"title": "Häufig gestellte Fragen",
"questions": {
"account": {
"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."
},
"static_vs_dynamic": {
"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."
},
"forever": {
"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."
},
"file_type": {
"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."
},
"password": {
"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."
},
"analytics": {
"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."
},
"privacy": {
"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."
},
"bulk": {
"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."
},
"no_account": {
"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."
},
"restaurant_menu": {
"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."
},
"scan_rate": {
"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."
},
"branding": {
"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."
},
"data_security": {
"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."
}
}
},
"dashboard": {
"title": "Dashboard",
"subtitle": "Verwalten Sie Ihre QR-Codes und verfolgen Sie Ihre Performance",
"stats": {
"total_scans": "Gesamte Scans",
"active_codes": "Aktive QR-Codes",
"conversion_rate": "Konversionsrate"
},
"recent_codes": "Aktuelle QR-Codes",
"blog_resources": "Blog & Ressourcen",
"menu": {
"edit": "Bearbeiten",
"duplicate": "Duplizieren",
"pause": "Pausieren",
"delete": "Löschen"
}
},
"create": {
"title": "QR-Code erstellen",
"subtitle": "Generieren Sie dynamische und statische QR-Codes mit individuellem Branding",
"content": "Inhalt",
"type": "QR-Code-Typ",
"style": "Stil & Branding",
"preview": "Live-Vorschau",
"title_label": "Titel",
"title_placeholder": "Mein QR-Code",
"content_type": "Inhaltstyp",
"url_label": "URL",
"url_placeholder": "https://beispiel.de",
"tags_label": "Tags (durch Komma getrennt)",
"tags_placeholder": "marketing, kampagne, 2025",
"qr_code_type": "QR-Code-Typ",
"dynamic": "Dynamisch",
"static": "Statisch",
"recommended": "Empfohlen",
"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.",
"foreground_color": "Vordergrundfarbe",
"background_color": "Hintergrundfarbe",
"corner_style": "Eckenstil",
"size": "Größe",
"good_contrast": "Guter Kontrast",
"contrast_ratio": "Kontrastverhältnis",
"download_svg": "SVG herunterladen",
"download_png": "PNG herunterladen",
"save_qr_code": "QR-Code speichern"
},
"analytics": {
"title": "Analytik",
"subtitle": "Verfolgen und analysieren Sie die Performance Ihrer QR-Codes",
"export_report": "Bericht exportieren",
"from_last_period": "vom letzten Zeitraum",
"no_mobile_scans": "Keine mobilen Scans",
"of_total": "der Gesamtmenge",
"ranges": {
"7d": "7 Tage",
"30d": "30 Tage",
"90d": "90 Tage"
},
"kpis": {
"total_scans": "Gesamte Scans",
"avg_scans": "Ø Scans/QR",
"mobile_usage": "Mobile Nutzung",
"top_country": "Top Land"
},
"charts": {
"scans_over_time": "Scans über Zeit",
"device_types": "Gerätetypen",
"top_countries": "Top Länder"
},
"table": {
"qr_code": "QR-Code",
"type": "Typ",
"total_scans": "Gesamte Scans",
"unique_scans": "Einzigartige Scans",
"conversion": "Konversion",
"trend": "Trend",
"scans": "Scans",
"percentage": "Prozent",
"country": "Land",
"performance": "Performance",
"created": "Erstellt",
"status": "Status"
},
"performance_title": "QR-Code-Performance"
},
"bulk": {
"title": "Massen-Erstellung",
"subtitle": "Erstellen Sie mehrere QR-Codes gleichzeitig aus CSV- oder Excel-Dateien",
"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.",
"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.",
"download_template": "Vorlage herunterladen",
"no_file_selected": "Keine ausgewählt",
"simple_format": "Einfaches Format",
"just_title_url": "Nur Titel & URL",
"static_qr_codes": "Statische QR-Codes",
"no_tracking": "Kein Tracking enthalten",
"instant_download": "Sofortiger Download",
"get_zip": "ZIP mit allen SVGs erhalten",
"max_rows": "max 1.000 Zeilen",
"steps": {
"upload": "Datei hochladen",
"preview": "Vorschau & Zuordnung",
"download": "Herunterladen"
},
"drag_drop": "Datei hier hinziehen",
"or_click": "oder klicken zum Durchsuchen",
"supported_formats": "Unterstützt CSV, XLS, XLSX (max 1.000 Zeilen)"
},
"integrations": {
"title": "Integrationen",
"metrics": {
"total_codes": "QR-Codes Gesamt",
"active_integrations": "Aktive Integrationen",
"sync_status": "Sync-Status",
"available_services": "Verfügbare Services"
},
"zapier": {
"title": "Zapier",
"description": "Automatisieren Sie QR-Code-Erstellung mit 5000+ Apps",
"features": [
"Trigger bei neuen QR-Codes",
"Codes aus anderen Apps erstellen",
"Scan-Daten synchronisieren"
]
},
"airtable": {
"title": "Airtable",
"description": "Synchronisieren Sie QR-Codes mit Ihren Airtable-Basen",
"features": [
"Bidirektionale Synchronisation",
"Individuelle Feldzuordnung",
"Echtzeit-Updates"
]
},
"sheets": {
"title": "Google Sheets",
"description": "Exportieren Sie Daten automatisch zu Google Sheets",
"features": [
"Automatisierte Exporte",
"Individuelle Vorlagen",
"Geplante Updates"
]
},
"activate": "Aktivieren & Konfigurieren"
},
"settings": {
"title": "Einstellungen",
"subtitle": "Verwalten Sie Ihre Kontoeinstellungen und Präferenzen",
"tabs": {
"profile": "Profil",
"billing": "Abrechnung",
"team": "Team & Rollen",
"api": "API-Schlüssel",
"workspace": "Arbeitsbereich"
}
},
"common": {
"save": "Speichern",
"cancel": "Abbrechen",
"delete": "Löschen",
"edit": "Bearbeiten",
"create": "Erstellen",
"loading": "Lädt...",
"error": "Ein Fehler ist aufgetreten",
"success": "Erfolgreich!"
},
"footer": {
"product": "Produkt",
"features": "Funktionen",
"pricing": "Preise",
"faq": "FAQ",
"blog": "Blog",
"resources": "Ressourcen",
"full_pricing": "Alle Preise",
"all_questions": "Alle Fragen",
"all_articles": "Alle Artikel",
"learn": "Lernen",
"get_started": "Loslegen",
"legal": "Rechtliches",
"industries": "Branchen",
"privacy_policy": "Datenschutzerklärung",
"terms_of_service": "Nutzungsbedingungen",
"tagline": "Erstellen Sie benutzerdefinierte QR-Codes in Sekunden mit erweitertem Tracking und Analysen.",
"newsletter": "Newsletter-Anmeldung",
"rights_reserved": "QR Master. Alle Rechte vorbehalten."
}
}
{
"nav": {
"features": "Funktionen",
"pricing": "Preise",
"faq": "FAQ",
"blog": "Blog",
"login": "Anmelden",
"dashboard": "Dashboard",
"about": "Über uns",
"contact": "Kontakt",
"signup": "Registrieren",
"learn": "Lernen",
"create_qr": "QR erstellen",
"bulk_creation": "Massen-Erstellung",
"analytics": "Analytik",
"settings": "Einstellungen",
"cta": "Kostenlos starten",
"tools": "Kostenlose Tools",
"all_free": "Alle Generatoren sind 100% kostenlos",
"resources": "Ressourcen",
"all_industries": "Branchen"
},
"hero": {
"badge": "Kostenloser QR-Code-Generator",
"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.",
"features": [
"Keine Kreditkarte zum Starten erforderlich",
"QR-Codes für immer kostenlos erstellen",
"Erweiterte Verfolgung und Analytik",
"Individuelle Farben und Stile"
],
"cta_primary": "QR-Code kostenlos erstellen",
"cta_secondary": "Preise ansehen",
"engagement_badge": "Kostenlos für immer",
"get_started": "Loslegen",
"view_full_pricing": "Alle Preisdetails ansehen →"
},
"trust": {
"users": "Aktive Nutzer",
"codes": "QR-Codes erstellt",
"scans": "Scans verfolgt",
"countries": "Länder"
},
"industries": {
"restaurant": "Restaurant-Kette",
"tech": "Tech-Startup",
"realestate": "Immobilien",
"events": "Event-Agentur",
"retail": "Einzelhandel",
"healthcare": "Gesundheitswesen"
},
"templates": {
"title": "Mit einer Vorlage beginnen",
"restaurant": "Restaurant-Menü",
"business": "Visitenkarte",
"vcard": "Kontaktkarte",
"event": "Event-Ticket",
"use_template": "Vorlage verwenden →"
},
"generator": {
"title": "Sofortiger QR-Code-Generator",
"url_placeholder": "Geben Sie hier Ihre URL ein...",
"foreground": "Vordergrund",
"background": "Hintergrund",
"corners": "Ecken",
"size": "Größe",
"contrast_good": "Guter Kontrast",
"download_svg": "SVG herunterladen",
"download_png": "PNG herunterladen",
"save_track": "Speichern & Verfolgen",
"live_preview": "Live-Vorschau",
"demo_note": "Dies ist ein Demo-QR-Code"
},
"static_vs_dynamic": {
"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.",
"static": {
"title": "Statische QR-Codes",
"subtitle": "Immer kostenlos",
"description": "Perfekt für permanente Inhalte, die sich nie ändern",
"features": [
"Inhalt kann nicht bearbeitet werden",
"Keine Scan-Verfolgung",
"Funktioniert für immer",
"Kein Konto erforderlich"
]
},
"dynamic": {
"title": "Dynamische QR-Codes",
"subtitle": "Empfohlen",
"description": "Volle Kontrolle mit Tracking- und Bearbeitungsfunktionen",
"features": [
"Inhalt jederzeit bearbeiten",
"Erweiterte Analytik",
"Individuelles Branding",
"Bulk-Operationen"
]
}
},
"features": {
"title": "Alles was Sie brauchen, um professionelle QR-Codes zu erstellen",
"analytics": {
"title": "Erweiterte Analytik",
"description": "Verfolgen Sie Scans, Standorte, Geräte und Nutzerverhalten mit detaillierten Einblicken."
},
"customization": {
"title": "Vollständige Anpassung",
"description": "Branden Sie Ihre QR-Codes mit individuellen Farben, Logos und Styling-Optionen."
},
"unlimited": {
"title": "Unbegrenzte statische QR-Codes",
"description": "Erstellen Sie so viele statische QR-Codes wie Sie benötigen. Kostenlos für immer, ohne Limits."
},
"bulk": {
"title": "Bulk-Operationen",
"description": "Erstellen Sie hunderte von QR-Codes auf einmal mit CSV-Import und Batch-Verarbeitung."
},
"integrations": {
"title": "Integrationen",
"description": "Verbinden Sie sich mit Zapier, Airtable, Google Sheets und weiteren beliebten Tools."
},
"api": {
"title": "Entwickler-API",
"description": "Integrieren Sie QR-Code-Generierung in Ihre Anwendungen mit unserer REST-API."
},
"support": {
"title": "24/7 Support",
"description": "Erhalten Sie Hilfe, wenn Sie sie brauchen, mit unserem dedizierten Kundensupport-Team."
}
},
"pricing": {
"title": "Wählen Sie Ihren Plan",
"subtitle": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
"choose_plan": "Wählen Sie Ihren Plan",
"select_plan": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
"current_plan": "Aktueller Plan",
"upgrade_to": "Upgrade auf",
"downgrade_to_free": "Zu Kostenlos zurückstufen",
"most_popular": "Beliebteste",
"all_plans_note": "Alle Pläne beinhalten unbegrenzte statische QR-Codes und Basis-Anpassung.",
"free": {
"title": "Kostenlos",
"name": "Free",
"price": "€0",
"period": "für immer",
"features": [
"3 aktive dynamische QR-Codes (8 Typen verfügbar)",
"Unbegrenzte statische QR-Codes",
"Basis-Scan-Tracking",
"Standard QR-Design-Vorlagen"
]
},
"pro": {
"title": "Pro",
"name": "Pro",
"price": "€9",
"period": "pro Monat",
"badge": "Beliebteste",
"features": [
"50 dynamische QR-Codes",
"Unbegrenzte statische QR-Codes",
"Erweiterte Analytik (Scans, Geräte, Standorte)",
"Individuelles Branding (Farben)",
"Download als SVG/PNG"
]
},
"business": {
"title": "Business",
"name": "Business",
"price": "€29",
"period": "pro Monat",
"features": [
"500 dynamische QR-Codes",
"Unbegrenzte statische QR-Codes",
"Alles aus Pro",
"Massen-QR-Erstellung (bis zu 1.000)",
"Prioritäts-E-Mail-Support",
"Erweiterte Tracking & Insights"
]
},
"enterprise": {
"title": "Enterprise",
"name": "Enterprise",
"price": "Individuell",
"period": "",
"features": [
"∞ dynamische QR-Codes",
"Unbegrenzte statische QR-Codes",
"Alles aus Business",
"Eigener Account Manager"
],
"contact": "Kontakt aufnehmen"
}
},
"faq": {
"title": "Häufig gestellte Fragen",
"questions": {
"account": {
"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."
},
"static_vs_dynamic": {
"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."
},
"forever": {
"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."
},
"file_type": {
"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."
},
"password": {
"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."
},
"analytics": {
"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."
},
"privacy": {
"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."
},
"bulk": {
"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."
},
"no_account": {
"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."
},
"restaurant_menu": {
"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."
},
"scan_rate": {
"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."
},
"branding": {
"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."
},
"data_security": {
"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."
}
}
},
"dashboard": {
"title": "Dashboard",
"subtitle": "Verwalten Sie Ihre QR-Codes und verfolgen Sie Ihre Performance",
"stats": {
"total_scans": "Gesamte Scans",
"active_codes": "Aktive QR-Codes",
"conversion_rate": "Konversionsrate"
},
"recent_codes": "Aktuelle QR-Codes",
"blog_resources": "Blog & Ressourcen",
"menu": {
"edit": "Bearbeiten",
"duplicate": "Duplizieren",
"pause": "Pausieren",
"delete": "Löschen"
}
},
"create": {
"title": "QR-Code erstellen",
"subtitle": "Generieren Sie dynamische und statische QR-Codes mit individuellem Branding",
"content": "Inhalt",
"type": "QR-Code-Typ",
"style": "Stil & Branding",
"preview": "Live-Vorschau",
"title_label": "Titel",
"title_placeholder": "Mein QR-Code",
"content_type": "Inhaltstyp",
"url_label": "URL",
"url_placeholder": "https://beispiel.de",
"tags_label": "Tags (durch Komma getrennt)",
"tags_placeholder": "marketing, kampagne, 2025",
"qr_code_type": "QR-Code-Typ",
"dynamic": "Dynamisch",
"static": "Statisch",
"recommended": "Empfohlen",
"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.",
"foreground_color": "Vordergrundfarbe",
"background_color": "Hintergrundfarbe",
"corner_style": "Eckenstil",
"size": "Größe",
"good_contrast": "Guter Kontrast",
"contrast_ratio": "Kontrastverhältnis",
"download_svg": "SVG herunterladen",
"download_png": "PNG herunterladen",
"save_qr_code": "QR-Code speichern"
},
"analytics": {
"title": "Analytik",
"subtitle": "Verfolgen und analysieren Sie die Performance Ihrer QR-Codes",
"export_report": "Bericht exportieren",
"from_last_period": "vom letzten Zeitraum",
"no_mobile_scans": "Keine mobilen Scans",
"of_total": "der Gesamtmenge",
"ranges": {
"7d": "7 Tage",
"30d": "30 Tage",
"90d": "90 Tage"
},
"kpis": {
"total_scans": "Gesamte Scans",
"avg_scans": "Ø Scans/QR",
"mobile_usage": "Mobile Nutzung",
"top_country": "Top Land"
},
"charts": {
"scans_over_time": "Scans über Zeit",
"device_types": "Gerätetypen",
"top_countries": "Top Länder"
},
"table": {
"qr_code": "QR-Code",
"type": "Typ",
"total_scans": "Gesamte Scans",
"unique_scans": "Einzigartige Scans",
"conversion": "Konversion",
"trend": "Trend",
"scans": "Scans",
"percentage": "Prozent",
"country": "Land",
"performance": "Performance",
"created": "Erstellt",
"status": "Status"
},
"performance_title": "QR-Code-Performance"
},
"bulk": {
"title": "Massen-Erstellung",
"subtitle": "Erstellen Sie mehrere QR-Codes gleichzeitig aus CSV- oder Excel-Dateien",
"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.",
"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.",
"download_template": "Vorlage herunterladen",
"no_file_selected": "Keine ausgewählt",
"simple_format": "Einfaches Format",
"just_title_url": "Nur Titel & URL",
"static_qr_codes": "Statische QR-Codes",
"no_tracking": "Kein Tracking enthalten",
"instant_download": "Sofortiger Download",
"get_zip": "ZIP mit allen SVGs erhalten",
"max_rows": "max 1.000 Zeilen",
"steps": {
"upload": "Datei hochladen",
"preview": "Vorschau & Zuordnung",
"download": "Herunterladen"
},
"drag_drop": "Datei hier hinziehen",
"or_click": "oder klicken zum Durchsuchen",
"supported_formats": "Unterstützt CSV, XLS, XLSX (max 1.000 Zeilen)"
},
"integrations": {
"title": "Integrationen",
"metrics": {
"total_codes": "QR-Codes Gesamt",
"active_integrations": "Aktive Integrationen",
"sync_status": "Sync-Status",
"available_services": "Verfügbare Services"
},
"zapier": {
"title": "Zapier",
"description": "Automatisieren Sie QR-Code-Erstellung mit 5000+ Apps",
"features": [
"Trigger bei neuen QR-Codes",
"Codes aus anderen Apps erstellen",
"Scan-Daten synchronisieren"
]
},
"airtable": {
"title": "Airtable",
"description": "Synchronisieren Sie QR-Codes mit Ihren Airtable-Basen",
"features": [
"Bidirektionale Synchronisation",
"Individuelle Feldzuordnung",
"Echtzeit-Updates"
]
},
"sheets": {
"title": "Google Sheets",
"description": "Exportieren Sie Daten automatisch zu Google Sheets",
"features": [
"Automatisierte Exporte",
"Individuelle Vorlagen",
"Geplante Updates"
]
},
"activate": "Aktivieren & Konfigurieren"
},
"settings": {
"title": "Einstellungen",
"subtitle": "Verwalten Sie Ihre Kontoeinstellungen und Präferenzen",
"tabs": {
"profile": "Profil",
"billing": "Abrechnung",
"team": "Team & Rollen",
"api": "API-Schlüssel",
"workspace": "Arbeitsbereich"
}
},
"common": {
"save": "Speichern",
"cancel": "Abbrechen",
"delete": "Löschen",
"edit": "Bearbeiten",
"create": "Erstellen",
"loading": "Lädt...",
"error": "Ein Fehler ist aufgetreten",
"success": "Erfolgreich!"
},
"footer": {
"product": "Produkt",
"features": "Funktionen",
"pricing": "Preise",
"faq": "FAQ",
"blog": "Blog",
"resources": "Ressourcen",
"full_pricing": "Alle Preise",
"all_questions": "Alle Fragen",
"all_articles": "Alle Artikel",
"learn": "Lernen",
"get_started": "Loslegen",
"legal": "Rechtliches",
"industries": "Branchen",
"privacy_policy": "Datenschutzerklärung",
"terms_of_service": "Nutzungsbedingungen",
"tagline": "Erstellen Sie benutzerdefinierte QR-Codes in Sekunden mit erweitertem Tracking und Analysen.",
"newsletter": "Newsletter-Anmeldung",
"rights_reserved": "QR Master. Alle Rechte vorbehalten."
}
}

View File

@@ -1,412 +1,412 @@
{
"nav": {
"features": "Features",
"pricing": "Pricing",
"faq": "FAQ",
"blog": "Blog",
"login": "Login",
"dashboard": "Dashboard",
"about": "About",
"contact": "Contact",
"signup": "Sign Up",
"learn": "Learn",
"create_qr": "Create QR",
"bulk_creation": "Bulk Creation",
"analytics": "Analytics",
"settings": "Settings",
"cta": "Get Started Free",
"tools": "Free Tools",
"all_free": "All generators are 100% free",
"resources": "Resources",
"all_industries": "Industries"
},
"hero": {
"badge": "QR Master Free QR Code Generator",
"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.",
"features": [
"No credit card required to start",
"Create QR codes free forever",
"Advanced tracking and analytics",
"Custom colors and styles"
],
"cta_primary": "Make a QR Code Free",
"cta_secondary": "View Pricing",
"engagement_badge": "Free Forever"
},
"trust": {
"users": "Happy Users",
"codes": "Active QR Codes",
"scans": "Total Scans",
"countries": "Countries"
},
"industries": {
"restaurant": "Restaurant Chain",
"tech": "Tech Startup",
"realestate": "Real Estate",
"events": "Event Agency",
"retail": "Retail Store",
"healthcare": "Healthcare"
},
"templates": {
"title": "Start with a Template",
"restaurant": "Restaurant Menu",
"business": "Business Card",
"vcard": "Contact Card",
"event": "Event Ticket",
"use_template": "Use template →"
},
"generator": {
"title": "Instant QR Code Generator",
"url_placeholder": "Enter your URL here...",
"foreground": "Foreground",
"background": "Background",
"corners": "Corners",
"size": "Size",
"contrast_good": "Good contrast",
"download_svg": "Download SVG",
"download_png": "Download PNG",
"save_track": "Save & Track",
"live_preview": "Live Preview",
"demo_note": "This is a demo QR code"
},
"static_vs_dynamic": {
"title": "Why Dynamic QR Codes Save You Money",
"description": "Stop re-printing materials. Switch destinations instantly and track every scan.",
"static": {
"title": "Static QR Codes",
"subtitle": "Always Free",
"description": "Perfect for permanent content that never changes",
"features": [
"Content cannot be edited",
"No scan tracking",
"Works forever",
"No account required"
]
},
"dynamic": {
"title": "Dynamic QR Codes",
"subtitle": "Recommended",
"description": "Full control with tracking and editing capabilities",
"features": [
"Edit content anytime",
"Advanced analytics",
"Custom branding",
"Bulk operations"
]
}
},
"features": {
"title": "Everything you need to create professional QR codes",
"analytics": {
"title": "Advanced Analytics",
"description": "Track scans, locations, devices, and user behavior with detailed insights."
},
"customization": {
"title": "Full Customization",
"description": "Brand your QR codes with custom colors and styling options."
},
"unlimited": {
"title": "Unlimited Static QR Codes",
"description": "Create as many static QR codes as you need. Free forever, no limits."
},
"bulk": {
"title": "Bulk Operations",
"description": "Create hundreds of QR codes at once with CSV import and batch processing."
},
"integrations": {
"title": "Integrations",
"description": "Connect with Zapier, Airtable, Google Sheets, and more popular tools."
},
"api": {
"title": "Developer API",
"description": "Integrate QR code generation into your applications with our REST API."
},
"support": {
"title": "24/7 Support",
"description": "Get help when you need it with our dedicated customer support team."
}
},
"pricing": {
"title": "Choose Your Plan",
"subtitle": "Select the perfect plan for your QR code needs",
"choose_plan": "Choose Your Plan",
"select_plan": "Select the perfect plan for your QR code needs",
"current_plan": "Current Plan",
"upgrade_to": "Upgrade to",
"downgrade_to_free": "Downgrade to Free",
"most_popular": "Most Popular",
"all_plans_note": "All plans include unlimited static QR codes and basic customization.",
"free": {
"title": "Free",
"name": "Free",
"price": "€0",
"period": "forever",
"features": [
"3 active dynamic QR codes (8 types available)",
"Unlimited static QR codes",
"Basic scan tracking",
"Standard QR design templates",
"Download as SVG/PNG"
]
},
"pro": {
"title": "Pro",
"name": "Pro",
"price": "€9",
"period": "per month",
"badge": "Most Popular",
"features": [
"50 dynamic QR codes",
"Unlimited static QR codes",
"Advanced analytics (scans, devices, locations)",
"Custom branding (colors & logos)"
]
},
"business": {
"title": "Business",
"name": "Business",
"price": "€29",
"period": "per month",
"features": [
"500 dynamic QR codes",
"Unlimited static QR codes",
"Everything from Pro",
"Bulk QR Creation (up to 1,000)",
"Priority email support",
"Advanced tracking & insights"
]
},
"enterprise": {
"title": "Enterprise",
"name": "Enterprise",
"price": "Custom",
"period": "",
"features": [
"∞ dynamic QR codes",
"Unlimited static QR codes",
"Everything from Business",
"Dedicated Account Manager"
],
"contact": "Contact Us"
}
},
"faq": {
"title": "Frequently Asked Questions",
"questions": {
"account": {
"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."
},
"static_vs_dynamic": {
"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."
},
"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."
},
"file_type": {
"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."
},
"password": {
"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."
},
"analytics": {
"question": "How do analytics work?",
"answer": "We track scans, locations, devices, and referrers while respecting user privacy. No personal data is stored."
},
"privacy": {
"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."
},
"bulk": {
"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."
}
}
},
"dashboard": {
"title": "Dashboard",
"subtitle": "Manage your QR codes and track performance",
"stats": {
"total_scans": "Total Scans",
"active_codes": "Active QR Codes",
"conversion_rate": "Conversion Rate"
},
"recent_codes": "Recent QR Codes",
"blog_resources": "Blog & Resources",
"menu": {
"edit": "Edit",
"duplicate": "Duplicate",
"pause": "Pause",
"delete": "Delete"
}
},
"create": {
"title": "Create QR Code",
"subtitle": "Generate dynamic and static QR codes with custom branding",
"content": "Content",
"type": "QR Code Type",
"style": "Style & Branding",
"preview": "Live Preview",
"title_label": "Title",
"title_placeholder": "My QR Code",
"content_type": "Content Type",
"url_label": "URL",
"url_placeholder": "https://example.com",
"tags_label": "Tags (comma-separated)",
"tags_placeholder": "marketing, campaign, 2025",
"qr_code_type": "QR Code Type",
"dynamic": "Dynamic",
"static": "Static",
"recommended": "Recommended",
"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.",
"foreground_color": "Foreground Color",
"background_color": "Background Color",
"corner_style": "Corner Style",
"size": "Size",
"good_contrast": "Good contrast",
"contrast_ratio": "Contrast ratio",
"download_svg": "Download SVG",
"download_png": "Download PNG",
"save_qr_code": "Save QR Code"
},
"analytics": {
"title": "Analytics",
"subtitle": "Track and analyze your QR code performance",
"export_report": "Export Report",
"from_last_period": "from last period",
"no_mobile_scans": "No mobile scans",
"of_total": "of total",
"ranges": {
"7d": "7 Days",
"30d": "30 Days",
"90d": "90 Days"
},
"kpis": {
"total_scans": "Total Scans",
"avg_scans": "Avg Scans/QR",
"mobile_usage": "Mobile Usage",
"top_country": "Top Country"
},
"charts": {
"scans_over_time": "Scans Over Time",
"device_types": "Device Types",
"top_countries": "Top Countries"
},
"table": {
"qr_code": "QR Code",
"type": "Type",
"total_scans": "Total Scans",
"unique_scans": "Unique Scans",
"conversion": "Conversion",
"trend": "Trend",
"scans": "Scans",
"percentage": "Percentage",
"country": "Country",
"performance": "Performance",
"created": "Created",
"status": "Status"
},
"performance_title": "QR Code Performance"
},
"bulk": {
"title": "Bulk Creation",
"subtitle": "Create multiple QR codes at once from CSV or Excel files",
"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).",
"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.",
"download_template": "Download Template",
"no_file_selected": "No file selected",
"simple_format": "Simple Format",
"just_title_url": "Just title & URL",
"static_qr_codes": "Static QR Codes",
"no_tracking": "No tracking included",
"instant_download": "Instant Download",
"get_zip": "Get ZIP with all SVGs",
"max_rows": "max 1,000 rows",
"steps": {
"upload": "Upload File",
"preview": "Preview & Map",
"download": "Download"
},
"drag_drop": "Drag & drop your file here",
"or_click": "or click to browse",
"supported_formats": "Supports CSV, XLS, XLSX (max 1,000 rows)"
},
"integrations": {
"title": "Integrations",
"metrics": {
"total_codes": "QR Codes Total",
"active_integrations": "Active Integrations",
"sync_status": "Sync Status",
"available_services": "Available Services"
},
"zapier": {
"title": "Zapier",
"description": "Automate QR code creation with 5000+ apps",
"features": [
"Trigger on new QR codes",
"Create codes from other apps",
"Sync scan data"
]
},
"airtable": {
"title": "Airtable",
"description": "Sync QR codes with your Airtable bases",
"features": ["Two-way sync", "Custom field mapping", "Real-time updates"]
},
"sheets": {
"title": "Google Sheets",
"description": "Export data to Google Sheets automatically",
"features": ["Automated exports", "Custom templates", "Scheduled updates"]
},
"activate": "Activate & Configure"
},
"settings": {
"title": "Settings",
"subtitle": "Manage your account settings and preferences",
"tabs": {
"profile": "Profile",
"billing": "Billing",
"team": "Team & Roles",
"api": "API Keys",
"workspace": "Workspace"
}
},
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"create": "Create",
"loading": "Loading...",
"error": "An error occurred",
"success": "Success!"
},
"footer": {
"product": "Product",
"features": "Features",
"pricing": "Pricing",
"faq": "FAQ",
"blog": "Blog",
"resources": "Resources",
"full_pricing": "Full Pricing",
"all_questions": "All Questions",
"all_articles": "All Articles",
"learn": "Learn",
"get_started": "Get Started",
"legal": "Legal",
"industries": "Industries",
"privacy_policy": "Privacy Policy",
"terms_of_service": "Terms of Service",
"tagline": "Create custom QR codes in seconds with advanced tracking and analytics.",
"newsletter": "Newsletter signup",
"rights_reserved": "QR Master. All rights reserved."
}
}
{
"nav": {
"features": "Features",
"pricing": "Pricing",
"faq": "FAQ",
"blog": "Blog",
"login": "Login",
"dashboard": "Dashboard",
"about": "About",
"contact": "Contact",
"signup": "Sign Up",
"learn": "Learn",
"create_qr": "Create QR",
"bulk_creation": "Bulk Creation",
"analytics": "Analytics",
"settings": "Settings",
"cta": "Get Started Free",
"tools": "Free Tools",
"all_free": "All generators are 100% free",
"resources": "Resources",
"all_industries": "Industries"
},
"hero": {
"badge": "QR Master Free QR Code Generator",
"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.",
"features": [
"No credit card required to start",
"Create QR codes free forever",
"Advanced tracking and analytics",
"Custom colors and styles"
],
"cta_primary": "Make a QR Code Free",
"cta_secondary": "View Pricing",
"engagement_badge": "Free Forever"
},
"trust": {
"users": "Happy Users",
"codes": "Active QR Codes",
"scans": "Total Scans",
"countries": "Countries"
},
"industries": {
"restaurant": "Restaurant Chain",
"tech": "Tech Startup",
"realestate": "Real Estate",
"events": "Event Agency",
"retail": "Retail Store",
"healthcare": "Healthcare"
},
"templates": {
"title": "Start with a Template",
"restaurant": "Restaurant Menu",
"business": "Business Card",
"vcard": "Contact Card",
"event": "Event Ticket",
"use_template": "Use template →"
},
"generator": {
"title": "Instant QR Code Generator",
"url_placeholder": "Enter your URL here...",
"foreground": "Foreground",
"background": "Background",
"corners": "Corners",
"size": "Size",
"contrast_good": "Good contrast",
"download_svg": "Download SVG",
"download_png": "Download PNG",
"save_track": "Save & Track",
"live_preview": "Live Preview",
"demo_note": "This is a demo QR code"
},
"static_vs_dynamic": {
"title": "Why Dynamic QR Codes Save You Money",
"description": "Stop re-printing materials. Switch destinations instantly and track every scan.",
"static": {
"title": "Static QR Codes",
"subtitle": "Always Free",
"description": "Perfect for permanent content that never changes",
"features": [
"Content cannot be edited",
"No scan tracking",
"Works forever",
"No account required"
]
},
"dynamic": {
"title": "Dynamic QR Codes",
"subtitle": "Recommended",
"description": "Full control with tracking and editing capabilities",
"features": [
"Edit content anytime",
"Advanced analytics",
"Custom branding",
"Bulk operations"
]
}
},
"features": {
"title": "Everything you need to create professional QR codes",
"analytics": {
"title": "Advanced Analytics",
"description": "Track scans, locations, devices, and user behavior with detailed insights."
},
"customization": {
"title": "Full Customization",
"description": "Brand your QR codes with custom colors and styling options."
},
"unlimited": {
"title": "Unlimited Static QR Codes",
"description": "Create as many static QR codes as you need. Free forever, no limits."
},
"bulk": {
"title": "Bulk Operations",
"description": "Create hundreds of QR codes at once with CSV import and batch processing."
},
"integrations": {
"title": "Integrations",
"description": "Connect with Zapier, Airtable, Google Sheets, and more popular tools."
},
"api": {
"title": "Developer API",
"description": "Integrate QR code generation into your applications with our REST API."
},
"support": {
"title": "24/7 Support",
"description": "Get help when you need it with our dedicated customer support team."
}
},
"pricing": {
"title": "Choose Your Plan",
"subtitle": "Select the perfect plan for your QR code needs",
"choose_plan": "Choose Your Plan",
"select_plan": "Select the perfect plan for your QR code needs",
"current_plan": "Current Plan",
"upgrade_to": "Upgrade to",
"downgrade_to_free": "Downgrade to Free",
"most_popular": "Most Popular",
"all_plans_note": "All plans include unlimited static QR codes and basic customization.",
"free": {
"title": "Free",
"name": "Free",
"price": "€0",
"period": "forever",
"features": [
"3 active dynamic QR codes (8 types available)",
"Unlimited static QR codes",
"Basic scan tracking",
"Standard QR design templates",
"Download as SVG/PNG"
]
},
"pro": {
"title": "Pro",
"name": "Pro",
"price": "€9",
"period": "per month",
"badge": "Most Popular",
"features": [
"50 dynamic QR codes",
"Unlimited static QR codes",
"Advanced analytics (scans, devices, locations)",
"Custom branding (colors & logos)"
]
},
"business": {
"title": "Business",
"name": "Business",
"price": "€29",
"period": "per month",
"features": [
"500 dynamic QR codes",
"Unlimited static QR codes",
"Everything from Pro",
"Bulk QR Creation (up to 1,000)",
"Priority email support",
"Advanced tracking & insights"
]
},
"enterprise": {
"title": "Enterprise",
"name": "Enterprise",
"price": "Custom",
"period": "",
"features": [
"∞ dynamic QR codes",
"Unlimited static QR codes",
"Everything from Business",
"Dedicated Account Manager"
],
"contact": "Contact Us"
}
},
"faq": {
"title": "Frequently Asked Questions",
"questions": {
"account": {
"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."
},
"static_vs_dynamic": {
"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."
},
"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."
},
"file_type": {
"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."
},
"password": {
"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."
},
"analytics": {
"question": "How do analytics work?",
"answer": "We track scans, locations, devices, and referrers while respecting user privacy. No personal data is stored."
},
"privacy": {
"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."
},
"bulk": {
"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."
}
}
},
"dashboard": {
"title": "Dashboard",
"subtitle": "Manage your QR codes and track performance",
"stats": {
"total_scans": "Total Scans",
"active_codes": "Active QR Codes",
"conversion_rate": "Conversion Rate"
},
"recent_codes": "Recent QR Codes",
"blog_resources": "Blog & Resources",
"menu": {
"edit": "Edit",
"duplicate": "Duplicate",
"pause": "Pause",
"delete": "Delete"
}
},
"create": {
"title": "Create QR Code",
"subtitle": "Generate dynamic and static QR codes with custom branding",
"content": "Content",
"type": "QR Code Type",
"style": "Style & Branding",
"preview": "Live Preview",
"title_label": "Title",
"title_placeholder": "My QR Code",
"content_type": "Content Type",
"url_label": "URL",
"url_placeholder": "https://example.com",
"tags_label": "Tags (comma-separated)",
"tags_placeholder": "marketing, campaign, 2025",
"qr_code_type": "QR Code Type",
"dynamic": "Dynamic",
"static": "Static",
"recommended": "Recommended",
"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.",
"foreground_color": "Foreground Color",
"background_color": "Background Color",
"corner_style": "Corner Style",
"size": "Size",
"good_contrast": "Good contrast",
"contrast_ratio": "Contrast ratio",
"download_svg": "Download SVG",
"download_png": "Download PNG",
"save_qr_code": "Save QR Code"
},
"analytics": {
"title": "Analytics",
"subtitle": "Track and analyze your QR code performance",
"export_report": "Export Report",
"from_last_period": "from last period",
"no_mobile_scans": "No mobile scans",
"of_total": "of total",
"ranges": {
"7d": "7 Days",
"30d": "30 Days",
"90d": "90 Days"
},
"kpis": {
"total_scans": "Total Scans",
"avg_scans": "Avg Scans/QR",
"mobile_usage": "Mobile Usage",
"top_country": "Top Country"
},
"charts": {
"scans_over_time": "Scans Over Time",
"device_types": "Device Types",
"top_countries": "Top Countries"
},
"table": {
"qr_code": "QR Code",
"type": "Type",
"total_scans": "Total Scans",
"unique_scans": "Unique Scans",
"conversion": "Conversion",
"trend": "Trend",
"scans": "Scans",
"percentage": "Percentage",
"country": "Country",
"performance": "Performance",
"created": "Created",
"status": "Status"
},
"performance_title": "QR Code Performance"
},
"bulk": {
"title": "Bulk Creation",
"subtitle": "Create multiple QR codes at once from CSV or Excel files",
"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).",
"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.",
"download_template": "Download Template",
"no_file_selected": "No file selected",
"simple_format": "Simple Format",
"just_title_url": "Just title & URL",
"static_qr_codes": "Static QR Codes",
"no_tracking": "No tracking included",
"instant_download": "Instant Download",
"get_zip": "Get ZIP with all SVGs",
"max_rows": "max 1,000 rows",
"steps": {
"upload": "Upload File",
"preview": "Preview & Map",
"download": "Download"
},
"drag_drop": "Drag & drop your file here",
"or_click": "or click to browse",
"supported_formats": "Supports CSV, XLS, XLSX (max 1,000 rows)"
},
"integrations": {
"title": "Integrations",
"metrics": {
"total_codes": "QR Codes Total",
"active_integrations": "Active Integrations",
"sync_status": "Sync Status",
"available_services": "Available Services"
},
"zapier": {
"title": "Zapier",
"description": "Automate QR code creation with 5000+ apps",
"features": [
"Trigger on new QR codes",
"Create codes from other apps",
"Sync scan data"
]
},
"airtable": {
"title": "Airtable",
"description": "Sync QR codes with your Airtable bases",
"features": ["Two-way sync", "Custom field mapping", "Real-time updates"]
},
"sheets": {
"title": "Google Sheets",
"description": "Export data to Google Sheets automatically",
"features": ["Automated exports", "Custom templates", "Scheduled updates"]
},
"activate": "Activate & Configure"
},
"settings": {
"title": "Settings",
"subtitle": "Manage your account settings and preferences",
"tabs": {
"profile": "Profile",
"billing": "Billing",
"team": "Team & Roles",
"api": "API Keys",
"workspace": "Workspace"
}
},
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"create": "Create",
"loading": "Loading...",
"error": "An error occurred",
"success": "Success!"
},
"footer": {
"product": "Product",
"features": "Features",
"pricing": "Pricing",
"faq": "FAQ",
"blog": "Blog",
"resources": "Resources",
"full_pricing": "Full Pricing",
"all_questions": "All Questions",
"all_articles": "All Articles",
"learn": "Learn",
"get_started": "Get Started",
"legal": "Legal",
"industries": "Industries",
"privacy_policy": "Privacy Policy",
"terms_of_service": "Terms of Service",
"tagline": "Create custom QR codes in seconds with advanced tracking and analytics.",
"newsletter": "Newsletter signup",
"rights_reserved": "QR Master. All rights reserved."
}
}

59
src/lib/tiktok.ts Normal file
View 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,
},
});
}