initial commit

This commit is contained in:
2026-01-27 13:47:56 -05:00
commit d88f19500b
22 changed files with 2723 additions and 0 deletions

13
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/.idea.trakqr.iml
/projectSettingsUpdater.xml
/contentModel.xml
/modules.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

4
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

8
.idea/indexLayout.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

43
AGENTS.md Normal file
View File

@@ -0,0 +1,43 @@
# Repository Guidelines
## Project Structure & Module Organization
This repository is currently a minimal skeleton with no source or test directories committed yet. As you add code, follow a clear top-level layout such as:
- `src/` for application code
- `tests/` for automated tests
- `scripts/` for maintenance or build utilities
- `docs/` for documentation and design notes
Keep new directories shallow and self-explanatory.
## Build, Test, and Development Commands
- Frontend (Vue, Vite):
- `cd src/frontend && npm install` installs dependencies.
- `cd src/frontend && npm run dev` starts the dev server.
- `cd src/frontend && npm run build` creates a production build.
- `cd src/frontend && npm run preview` previews the production build.
- API (.NET):
- `dotnet restore src/api` restores NuGet dependencies.
- `dotnet build src/api` builds the API.
- `dotnet run --project src/api` runs the API.
- Dev shell:
- `scripts/dev-tmux.sh` starts a tmux session with API and frontend windows and attaches to it, or adds windows to the current session if already inside tmux.
## Coding Style & Naming Conventions
- Indentation: 2 spaces for JS/Vue, 4 spaces for C#.
- Naming: `camelCase` for JS, `PascalCase` for C# types and `camelCase` for locals.
- Formatting: no tooling configured yet.
## Testing Guidelines
No testing framework is configured yet.
## Commit & Pull Request Guidelines
The repository has no commit history available yet, so there is no established commit message convention. When you start committing:
- Use short, imperative subject lines (e.g., "Add tracker API")
- Include context in the body when changes are non-trivial
For pull requests, include a brief summary, any linked issues, and screenshots for UI changes.
## Configuration & Secrets
Do not commit secrets. Use environment variables and provide sample configuration files such as `.env.example` when needed.

283
docs/spec.md Normal file
View File

@@ -0,0 +1,283 @@
# QR-First URL Shortener SaaS (Designer + Short Links + Tracking)
Note: These specs are a draft and need review.
## 0) Product Definition
One-liner: Create branded short links and highly customizable QR codes, then track scans/clicks with actionable analytics.
Primary users:
- Solo creators / small businesses
- Marketing teams in SMB/PME
- Agencies managing multiple clients
Core value: Beautiful QR designs + brandable short domains + trustworthy tracking in one place.
## 1) MVP Scope (what ships first)
### 1.1 User Capabilities (MVP)
Auth & Account
- Sign up / sign in (email + password; optional SSO later)
- Email verification
- Password reset
- Basic account settings
Projects / Workspaces
- Default workspace per user
- Create “Projects” to organize links/QRs (e.g., “Restaurant menus”, “Flyers Q1”)
Short Link Creation
- Create short link: https://d.om/abc123 or custom slug …/menu
- Destination URL validation
- Optional UTM builder (preset templates)
- Enable/disable link
- Expiration date (optional)
- Password protection (optional) — may be “Pro” if you want
QR Code Designer
- Generate QR from a short link (default) or direct URL
- Styling:
- Colors (foreground/background)
- Error correction level (L/M/Q/H)
- Quiet zone padding
- Shape presets (modules/eyes) (start with a few presets)
- Center logo upload (PNG/SVG) with size + margin controls
- Export:
- PNG and SVG
- Size presets (e.g., 256/512/1024/2048)
- Print-ready options (e.g., “high contrast” toggle)
Tracking & Analytics (MVP)
- Track events: click (short link) and scan (QR)
- Dashboard:
- Total events, uniques, last 24h / 7d / 30d
- Time series
- Top referrers (for clicks)
- Geo (country) and device (desktop/mobile) high-level
- Per-link analytics and per-QR analytics
Basic Admin
- Subscription status
- Usage quotas (links/QRs/events)
## 2) Non-Goals for MVP (explicitly out)
- Team roles/permissions (RBAC) beyond “owner”
- A/B routing, smart rules, rotation, geo routing
- Deep campaign automation
- Enterprise SSO, SCIM
- Offline QR scan tracking (impossible without network in most cases)
## 3) Plans & Monetization (recommended)
Free
- 1 workspace
- 25 short links
- 25 QR designs
- 10k events/month
- 1 custom QR logo upload (or allow unlimited but watermark exports)
Pro (individual/SMB)
- Custom domains (13)
- Higher limits
- No watermark
- UTM templates
- Expiring links / password links (if Pro)
Business
- Multiple workspaces
- Team seats (later)
- Higher retention and export presets
## 4) Core Entities (Data Model)
### 4.1 Entities
User
- id, email, password_hash, verified_at, created_at
Workspace
- id, owner_user_id, name, plan, created_at
Project
- id, workspace_id, name, created_at
Domain
- id, workspace_id, hostname, status (pending/verified/active), verification_token, created_at
ShortLink
- id
- workspace_id, project_id (nullable)
- domain_id (nullable; else default platform domain)
- slug
- destination_url
- title (nullable)
- status (active/disabled)
- expires_at (nullable)
- password_hash (nullable)
- created_at, updated_at
QRCodeDesign
- id
- workspace_id, project_id (nullable)
- shortlink_id (nullable; recommended default)
- style_json (colors, shapes, ecc level, etc.)
- logo_asset_id (nullable)
- created_at, updated_at
Event
- id (or bigint)
- workspace_id
- shortlink_id
- qrcode_id (nullable but strongly recommended to tag scans)
- type: click | scan
- ts
- ip_hash (privacy-safe)
- user_agent
- referrer
- country_code (nullable)
- device_type (nullable)
- dedupe_key (nullable)
- raw_json (optional for debug, or drop)
Asset
- id, workspace_id, type (logo), storage_key, mime, size, created_at
### 4.2 Key Design Choice
How do we distinguish “scan” vs “click”?
When exporting a QR, embed a URL like:
https://d.om/s/abc123?qr=<qrcode_id>
The redirect endpoint records scan when it detects qr=<id>, then redirects to destination, which will also produce a click unless you decide “scan implies click” and record only one.
Recommendation: record one event per redirect request:
- If qr present → type=scan
- Else → type=click
## 5) System Behavior (Routes & Flows)
### 5.1 Public Redirect
GET /{slug}
Resolve domain + slug → short link
Validate:
- exists
- active
- not expired
- if password-protected → show password page
Log event (scan/click)
Redirect 301/302 (configurable later; MVP use 302)
### 5.2 QR Export
QR code is generated from:
Redirect URL including qrcode id: https://{domain}/{slug}?qr={qrcode_id}
## 6) Functional Requirements (MVP checklist)
Link Management
- Create, edit, disable, delete (soft delete preferred)
- Slug uniqueness per domain
- Auto-slug generator (base62)
- Destination URL allowlist/denylist (prevent abuse)
QR Designer
- Live preview
- Save design
- Export SVG/PNG
- Logo upload with validation (size/mime)
Analytics
- Views for:
- Workspace overview
- Project overview
- Per short link
- Per QR design
- Time filters: 24h / 7d / 30d / custom range
- Unique definition:
- Unique per day per link based on ip_hash + UA hash (privacy-safe and approximate)
## 7) Non-Functional Requirements
Performance
- Redirect endpoint P95 < 100ms (excluding DNS/TLS)
- Event write must not block redirect (use async queue if possible)
Availability
- Redirect is the critical path; should stay up even if dashboard is down
- Graceful degradation: if analytics store is down, still redirect
Security
- Rate limit public endpoints
- Abuse prevention: phishing/malware reporting flow (later), basic filters now
- Domain verification to prevent takeover
- Strict CSP on app pages
Privacy & Compliance (Canada / Quebec friendly baseline)
- Avoid storing raw IP; store hashed IP with rotating salt (e.g., monthly)
- Provide retention configuration per plan (e.g., 30/180/365 days)
## 8) Architecture (pragmatic MVP)
Components
- Web App: dashboard + designer (Vue/React)
- API: CRUD for links/qr/projects/domains, analytics queries
- Redirect Edge: fastest path for /{slug} (can be same API initially)
Storage
- PostgreSQL for core entities
- Analytics:
- MVP: PostgreSQL events table partitioned by month
- Later: ClickHouse/BigQuery for scale
Background Jobs
- Domain verification checks
- Event enrichment (geo/device parsing)
- Cleanup & retention tasks
## 9) API Surface (minimal)
- POST /auth/register|login|forgot|reset
- GET/POST /workspaces
- GET/POST /projects
- GET/POST /links
- GET/POST /qrcodes
- POST /domains + verification status
- GET /analytics/overview
- GET /analytics/link/{id}
- GET /analytics/qrcode/{id}
## 10) UI Pages (MVP)
- Login / Register / Reset
- Workspace switcher
- Projects list
- Links list + create/edit
- QR designer (create/edit) with preview
- Analytics dashboard (overview + per link + per QR)
- Domains page (add/verify)
## 11) Pricing/Quotas Enforcement
Enforce at API level:
- max links, max QR codes, max events/month, max custom domains
Stripe integration later; MVP can be “manual Pro” toggle or Stripe from day 1 if you want.
## 12) Implementation Notes (key decisions)
- Use one redirect URL as canonical; QR adds ?qr= for attribution.
- Event logging should be non-blocking:
- MVP: write to DB async (background queue) or “fire-and-forget” with retry
- Plan for domain verification:
- Require DNS TXT record or CNAME to verify ownership
- Short link collision:
- Slug uniqueness per domain enforced in DB

30
scripts/dev-tmux.sh Executable file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
if ! command -v tmux >/dev/null 2>&1; then
echo "tmux is required but was not found in PATH." >&2
exit 1
fi
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
session_name="trakqr"
if [[ -n "${TMUX-}" ]]; then
current_session=$(tmux display-message -p "#S")
tmux new-window -t "${current_session}" -n api "cd \"${repo_root}\" && dotnet run --project src/api"
tmux new-window -t "${current_session}" -n frontend "cd \"${repo_root}\"/src/frontend && npm run dev"
tmux select-window -t "${current_session}:api"
echo "Added windows 'api' and 'frontend' to session '${current_session}'."
exit 0
fi
if tmux has-session -t "${session_name}" 2>/dev/null; then
echo "Session '${session_name}' already exists. Attaching..."
tmux attach -t "${session_name}"
exit 0
fi
tmux new-session -d -s "${session_name}" -n api "cd \"${repo_root}\" && dotnet watch --project src/api"
tmux new-window -t "${session_name}" -n frontend "cd \"${repo_root}\"/src/frontend && npm run dev"
tmux attach -t "${session_name}"

482
src/api/.gitignore vendored Normal file
View File

@@ -0,0 +1,482 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from `dotnet new gitignore`
# dotenv files
.env
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET
project.lock.json
project.fragment.lock.json
artifacts/
# Tye
.tye/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
.idea/
##
## Visual studio for Mac
##
# globs
Makefile.in
*.userprefs
*.usertasks
config.make
config.status
aclocal.m4
install-sh
autom4te.cache/
*.tar.gz
tarballs/
test-results/
# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# Vim temporary swap files
*.swp

46
src/api/Program.cs Normal file
View File

@@ -0,0 +1,46 @@
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi().CacheOutput();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/openapi/v1.json", "v1");
});
}
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

View File

@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://127.0.0.1:0",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

14
src/api/api.csproj Normal file
View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.0" />
</ItemGroup>
</Project>

6
src/api/api.http Normal file
View File

@@ -0,0 +1,6 @@
@api_HostAddress = http://localhost:0
GET {{api_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
src/api/appsettings.json Normal file
View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

170
src/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,170 @@
# Created by https://www.toptal.com/developers/gitignore/api/vue,node,linux
# Edit at https://www.toptal.com/developers/gitignore?templates=vue,node,linux
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
### Vue ###
# gitignore template for Vue.js projects
#
# Recommended template: Node.gitignore
# TODO: where does this rule come from?
docs/_book
# TODO: where does this rule come from?
test/
# End of https://www.toptal.com/developers/gitignore/api/vue,node,linux

12
src/frontend/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TrakQR</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1088
src/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
src/frontend/package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "trakqr-frontend",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.21"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"vite": "^5.2.0"
}
}

115
src/frontend/src/App.vue Normal file
View File

@@ -0,0 +1,115 @@
<template>
<div class="page">
<header class="topbar">
<div class="brand">
<span class="brand-mark">TQ</span>
<span class="brand-name">TrakQR</span>
</div>
<nav class="nav">
<a href="#features">Features</a>
<a href="#analytics">Analytics</a>
<a href="#designer">Designer</a>
</nav>
<button class="cta">Get Started</button>
</header>
<main>
<section class="hero">
<div class="hero-copy">
<p class="eyebrow">QR-first link intelligence</p>
<h1>Design bold QR codes. Track every scan.</h1>
<p class="subhead">
Build branded short links, craft beautiful QR designs, and turn scans into
actionable analytics for your projects.
</p>
<div class="hero-actions">
<button class="cta">Start free</button>
<button class="ghost">View demo</button>
</div>
<div class="hero-metrics">
<div>
<p class="metric">10k+</p>
<p class="label">Events per month on free tier</p>
</div>
<div>
<p class="metric">3</p>
<p class="label">Designer presets included</p>
</div>
</div>
</div>
<div class="hero-panel">
<div class="panel-header">
<div>
<p class="panel-title">Live QR Preview</p>
<p class="panel-sub">Updated from your short link</p>
</div>
<span class="status">Active</span>
</div>
<div class="qr-preview">
<div class="qr-grid"></div>
</div>
<div class="panel-footer">
<div>
<p class="label">https://tq.link/menu</p>
<p class="hint">Scan-ready, export PNG or SVG</p>
</div>
<button class="ghost small">Export</button>
</div>
</div>
</section>
<section id="features" class="feature-grid">
<article class="feature">
<h3>Branded short links</h3>
<p>Custom slugs, expiring links, and password protection built in.</p>
</article>
<article class="feature">
<h3>Designer-grade QR</h3>
<p>Control shapes, colors, quiet zones, and add logos with precision.</p>
</article>
<article class="feature">
<h3>Trustworthy analytics</h3>
<p>See scans, clicks, referrers, and device mix across every project.</p>
</article>
</section>
<section id="analytics" class="split">
<div>
<h2>Analytics that guide your next campaign.</h2>
<p>
Track events by link and by QR design. Spot spikes in real time and
attribute every scan with confidence.
</p>
<ul class="list">
<li>Live timelines with 24h, 7d, 30d filters</li>
<li>Device and geo snapshots for quick decisions</li>
<li>Workspace and project rollups</li>
</ul>
</div>
<div class="chart-card">
<div class="chart-header">
<p class="panel-title">Events last 7 days</p>
<p class="panel-sub">Scans + clicks</p>
</div>
<div class="chart">
<span style="height: 30%"></span>
<span style="height: 45%"></span>
<span style="height: 65%"></span>
<span style="height: 50%"></span>
<span style="height: 80%"></span>
<span style="height: 60%"></span>
<span style="height: 70%"></span>
</div>
</div>
</section>
<section id="designer" class="callout">
<div>
<h2>Launch your first QR in minutes.</h2>
<p>Start with presets or build your own design system.</p>
</div>
<button class="cta">Create a project</button>
</section>
</main>
</div>
</template>

5
src/frontend/src/main.js Normal file
View File

@@ -0,0 +1,5 @@
import { createApp } from "vue";
import App from "./App.vue";
import "./style.css";
createApp(App).mount("#app");

340
src/frontend/src/style.css Normal file
View File

@@ -0,0 +1,340 @@
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500;600&display=swap");
:root {
color-scheme: light;
--ink: #0f172a;
--muted: #475569;
--surface: #ffffff;
--accent: #ff6a3d;
--accent-dark: #d94b22;
--glow: #ffd1c2;
--line: rgba(15, 23, 42, 0.12);
--bg: #f5f1eb;
--shadow: 0 24px 60px rgba(15, 23, 42, 0.18);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: "Space Grotesk", "IBM Plex Sans", sans-serif;
background:
radial-gradient(circle at top left, #fff0e6 0%, transparent 40%),
radial-gradient(circle at top right, #eaf0ff 0%, transparent 45%),
var(--bg);
color: var(--ink);
}
a {
color: inherit;
text-decoration: none;
}
.page {
min-height: 100vh;
display: flex;
flex-direction: column;
padding: 32px clamp(20px, 4vw, 64px) 80px;
gap: 48px;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
font-weight: 600;
letter-spacing: 0.02em;
}
.brand-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 12px;
background: var(--ink);
color: #fff4ec;
font-weight: 700;
}
.brand-name {
font-size: 1.1rem;
}
.nav {
display: flex;
gap: 20px;
color: var(--muted);
font-size: 0.95rem;
}
.cta {
background: var(--accent);
color: white;
border: none;
padding: 12px 20px;
border-radius: 999px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 12px 20px rgba(255, 106, 61, 0.25);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.cta:hover {
transform: translateY(-1px);
box-shadow: 0 18px 28px rgba(255, 106, 61, 0.35);
}
.ghost {
background: transparent;
border: 1px solid var(--line);
color: var(--ink);
padding: 12px 20px;
border-radius: 999px;
font-weight: 600;
cursor: pointer;
}
.ghost.small {
padding: 8px 14px;
font-size: 0.85rem;
}
.hero {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 32px;
align-items: center;
}
.hero-copy h1 {
font-size: clamp(2.4rem, 4vw, 3.6rem);
line-height: 1.05;
margin-bottom: 16px;
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.22em;
font-size: 0.75rem;
color: var(--muted);
margin-bottom: 16px;
}
.subhead {
color: var(--muted);
font-size: 1.1rem;
line-height: 1.6;
margin-bottom: 24px;
}
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 28px;
}
.hero-metrics {
display: flex;
gap: 32px;
color: var(--muted);
}
.metric {
font-size: 1.4rem;
font-weight: 600;
color: var(--ink);
}
.hero-panel {
background: var(--surface);
border-radius: 28px;
padding: 24px;
box-shadow: var(--shadow);
border: 1px solid rgba(255, 255, 255, 0.6);
display: grid;
gap: 20px;
animation: floatIn 0.8s ease;
}
.panel-header,
.panel-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.panel-title {
font-weight: 600;
}
.panel-sub {
color: var(--muted);
font-size: 0.85rem;
}
.status {
padding: 6px 12px;
border-radius: 999px;
background: var(--glow);
color: var(--accent-dark);
font-size: 0.75rem;
font-weight: 600;
}
.qr-preview {
background: linear-gradient(135deg, #fff6f1, #f1f5ff);
border-radius: 20px;
padding: 32px;
display: grid;
place-items: center;
}
.qr-grid {
width: 180px;
height: 180px;
background: repeating-linear-gradient(
90deg,
rgba(15, 23, 42, 0.9) 0 8px,
transparent 8px 16px
),
repeating-linear-gradient(
0deg,
rgba(15, 23, 42, 0.9) 0 8px,
transparent 8px 16px
);
border-radius: 16px;
box-shadow: inset 0 0 0 10px #fff;
}
.hint,
.label {
color: var(--muted);
font-size: 0.85rem;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.feature {
background: rgba(255, 255, 255, 0.85);
border-radius: 20px;
padding: 18px 20px;
border: 1px solid var(--line);
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
transition: transform 0.2s ease;
}
.feature:hover {
transform: translateY(-4px);
}
.feature h3 {
margin-bottom: 8px;
}
.split {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 32px;
align-items: center;
}
.list {
margin-top: 16px;
display: grid;
gap: 10px;
color: var(--muted);
}
.chart-card {
background: var(--surface);
border-radius: 24px;
padding: 20px;
box-shadow: var(--shadow);
}
.chart {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 10px;
margin-top: 24px;
align-items: end;
height: 180px;
}
.chart span {
display: block;
background: var(--accent);
border-radius: 12px;
box-shadow: 0 8px 18px rgba(255, 106, 61, 0.25);
animation: rise 0.8s ease;
}
.callout {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
padding: 28px 32px;
border-radius: 30px;
background: linear-gradient(135deg, #fff1e8, #e8efff);
border: 1px solid rgba(15, 23, 42, 0.08);
}
@keyframes floatIn {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes rise {
from {
transform: scaleY(0.6);
}
to {
transform: scaleY(1);
}
}
@media (max-width: 900px) {
.topbar {
flex-direction: column;
align-items: flex-start;
}
.nav {
flex-wrap: wrap;
}
.hero-metrics {
flex-direction: column;
gap: 12px;
}
.callout {
flex-direction: column;
align-items: flex-start;
}
}

View File

@@ -0,0 +1,9 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
server: {
port: 5173
}
});