initial commit
This commit is contained in:
170
src/frontend/.gitignore
vendored
Normal file
170
src/frontend/.gitignore
vendored
Normal 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
12
src/frontend/index.html
Normal 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
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
18
src/frontend/package.json
Normal 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
115
src/frontend/src/App.vue
Normal 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
5
src/frontend/src/main.js
Normal 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
340
src/frontend/src/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
9
src/frontend/vite.config.js
Normal file
9
src/frontend/vite.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 5173
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user