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

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