feat: complete TapTrack landing page — Phases 1-10

- Vite + Vue 3 + TypeScript + Tailwind CSS v4 scaffold
- lucide-vue-next icons, Inter font, design tokens
- AppNavbar: sticky, scroll shadow, mobile hamburger
- AppFooter: dark navy, 3 link columns, copyright bar
- HeroSection: pill badge, H1, CTAs, trust row, screenshot
- PrivacyBanner: dark, 3 stat icons (Server/Shield/WifiOff)
- SeeItLiveSection: gate kiosk screenshot showcase
- FeaturesSection: 6-card grid with Lucide icons
- ScreenshotsSection: Student Directory + Reports.jpg cards
- HowItWorksSection: 3 steps + billing card
- SupportSection: 5 support cards + quote card
- ContactSection: form with Formspree, validation, success state
- v-reveal directive: Intersection Observer scroll animations
- SEO meta tags + Open Graph + Twitter Card
- vercel.json SPA rewrite rule
- Build: 100KB JS / 19KB CSS, 0 errors
This commit is contained in:
kevin-asprec
2026-03-17 04:01:44 +08:00
parent 8c37e5bb45
commit 5376d1b1d5
34 changed files with 6619 additions and 0 deletions

16
src/App.vue Normal file
View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import AppNavbar from './components/AppNavbar.vue'
import AppFooter from './components/AppFooter.vue'
import LandingPage from './pages/LandingPage.vue'
</script>
<template>
<div class="min-h-screen bg-white font-sans">
<AppNavbar />
<!-- pt-[68px] offsets the fixed navbar height -->
<div class="pt-[68px]">
<LandingPage />
</div>
<AppFooter />
</div>
</template>

View File

@@ -0,0 +1,99 @@
<script setup lang="ts">
const productLinks = [
{ label: 'Features', href: '#features' },
{ label: 'How It Works', href: '#how-it-works' },
{ label: 'Services', href: '#support' },
]
const companyLinks = [
{ label: 'About Us', href: '#' },
{ label: 'Contact', href: '#contact' },
{ label: 'Blog', href: '#' },
]
const legalLinks = [
{ label: 'Privacy Policy', href: '#' },
{ label: 'Terms of Service', href: '#' },
]
</script>
<template>
<footer class="bg-[#020617] text-white">
<div class="page-container py-14 pb-7">
<!-- Top row: brand + nav columns -->
<div class="flex flex-col md:flex-row gap-12 md:gap-16 pb-10 border-b border-[#1E293B]">
<!-- Brand column -->
<div class="flex flex-col gap-3.5 w-full md:w-[260px] shrink-0">
<div class="flex items-center gap-2">
<div class="w-7 h-7 rounded-[6px] bg-[#1D4ED8] flex items-center justify-center shrink-0">
<span class="text-white font-bold text-sm leading-none">T</span>
</div>
<span class="text-white font-bold text-base">TapTrack</span>
</div>
<p class="text-[#475569] text-[13px] leading-[1.65] max-w-[220px]">
On-premises NFC attendance for Philippine schools. Student data stays where it belongs with you.
</p>
</div>
<!-- Spacer -->
<div class="hidden md:block flex-1" />
<!-- Nav columns -->
<div class="grid grid-cols-2 sm:grid-cols-3 gap-8 md:gap-16">
<!-- Product -->
<div class="flex flex-col gap-3">
<span class="text-[#D1D5DB] font-semibold text-[13px]">Product</span>
<a
v-for="link in productLinks"
:key="link.label"
:href="link.href"
class="text-[#6B7280] text-[13px] hover:text-white transition-colors duration-150"
>
{{ link.label }}
</a>
</div>
<!-- Company -->
<div class="flex flex-col gap-3">
<span class="text-[#D1D5DB] font-semibold text-[13px]">Company</span>
<a
v-for="link in companyLinks"
:key="link.label"
:href="link.href"
class="text-[#6B7280] text-[13px] hover:text-white transition-colors duration-150"
>
{{ link.label }}
</a>
</div>
<!-- Legal -->
<div class="flex flex-col gap-3">
<span class="text-[#D1D5DB] font-semibold text-[13px]">Legal</span>
<a
v-for="link in legalLinks"
:key="link.label"
:href="link.href"
class="text-[#6B7280] text-[13px] hover:text-white transition-colors duration-150"
>
{{ link.label }}
</a>
</div>
</div>
</div>
<!-- Bottom bar -->
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 pt-5">
<p class="text-[#374151] text-xs">
© 2025 TapTrack. All rights reserved. Built in the Philippines.
</p>
<p class="text-[#374151] text-xs">
Student data never leaves your premises.
</p>
</div>
</div>
</footer>
</template>

View File

@@ -0,0 +1,116 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
const isScrolled = ref(false)
const isMobileMenuOpen = ref(false)
const navLinks = [
{ label: 'Features', href: '#features' },
{ label: 'How It Works', href: '#how-it-works' },
{ label: 'Services', href: '#support' },
{ label: 'Contact', href: '#contact' },
]
function handleScroll() {
isScrolled.value = window.scrollY > 10
}
function toggleMobileMenu() {
isMobileMenuOpen.value = !isMobileMenuOpen.value
}
function closeMobileMenu() {
isMobileMenuOpen.value = false
}
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
onMounted(() => window.addEventListener('scroll', handleScroll))
onUnmounted(() => window.removeEventListener('scroll', handleScroll))
</script>
<template>
<header
class="fixed top-0 left-0 right-0 z-50 bg-white transition-shadow duration-200"
:class="isScrolled ? 'shadow-sm' : 'border-b border-[#E2E8F0]'"
>
<div class="page-container flex items-center gap-12 h-[68px]">
<!-- Logo -->
<a href="#" class="flex items-center gap-2 shrink-0" @click.prevent="scrollToTop">
<div class="w-8 h-8 rounded-lg bg-[#1D4ED8] flex items-center justify-center">
<span class="text-white font-bold text-base leading-none">T</span>
</div>
<span class="text-[#0F172A] font-bold text-lg">TapTrack</span>
</a>
<!-- Spacer -->
<div class="flex-1" />
<!-- Desktop Nav Links -->
<nav class="hidden md:flex items-center gap-9">
<a
v-for="link in navLinks"
:key="link.href"
:href="link.href"
class="text-[#475569] font-medium text-sm hover:text-[#1D4ED8] transition-colors duration-150"
>
{{ link.label }}
</a>
</nav>
<!-- CTA Button -->
<a
href="#contact"
class="hidden md:inline-flex items-center bg-[#1D4ED8] hover:bg-[#1A43C0] text-white font-semibold text-sm px-[22px] py-[10px] rounded-lg transition-colors duration-150 shrink-0"
>
Request a Demo
</a>
<!-- Mobile Hamburger -->
<button
class="md:hidden flex flex-col gap-[5px] p-2 -mr-2"
:aria-label="isMobileMenuOpen ? 'Close menu' : 'Open menu'"
@click="toggleMobileMenu"
>
<span
class="block w-5 h-0.5 bg-[#475569] transition-all duration-200"
:class="isMobileMenuOpen ? 'rotate-45 translate-y-[7px]' : ''"
/>
<span
class="block w-5 h-0.5 bg-[#475569] transition-all duration-200"
:class="isMobileMenuOpen ? 'opacity-0' : ''"
/>
<span
class="block w-5 h-0.5 bg-[#475569] transition-all duration-200"
:class="isMobileMenuOpen ? '-rotate-45 -translate-y-[7px]' : ''"
/>
</button>
</div>
<!-- Mobile Menu Dropdown -->
<div
v-if="isMobileMenuOpen"
class="md:hidden bg-white border-t border-[#E2E8F0] px-6 py-4 flex flex-col gap-4"
>
<a
v-for="link in navLinks"
:key="link.href"
:href="link.href"
class="text-[#475569] font-medium text-sm hover:text-[#1D4ED8] transition-colors duration-150 py-1"
@click="closeMobileMenu"
>
{{ link.label }}
</a>
<a
href="#contact"
class="inline-flex items-center justify-center bg-[#1D4ED8] hover:bg-[#1A43C0] text-white font-semibold text-sm px-5 py-2.5 rounded-lg transition-colors duration-150 mt-1"
@click="closeMobileMenu"
>
Request a Demo
</a>
</div>
</header>
</template>

View File

@@ -0,0 +1,146 @@
<script setup lang="ts">
import { ref } from 'vue'
const form = ref({
school: '',
contact: '',
phone: '',
})
const submitted = ref(false)
const submitting = ref(false)
const error = ref('')
const bulletPoints = [
'✓ Free consultation call with our team',
'✓ Custom quote based on your school size',
'✓ Demo of the live system before you commit',
]
// Replace with your actual Formspree form ID: https://formspree.io
const FORMSPREE_URL = 'https://formspree.io/f/YOUR_FORM_ID'
async function handleSubmit() {
error.value = ''
if (!form.value.school || !form.value.contact || !form.value.phone) {
error.value = 'Please fill in all fields before submitting.'
return
}
submitting.value = true
try {
const res = await fetch(FORMSPREE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
school: form.value.school,
contact: form.value.contact,
phone: form.value.phone,
}),
})
if (res.ok) {
submitted.value = true
} else {
error.value = 'Something went wrong. Please try again or email us directly.'
}
} catch {
error.value = 'Network error. Please check your connection and try again.'
} finally {
submitting.value = false
}
}
</script>
<template>
<section id="contact" class="bg-[#0F172A]">
<div class="page-container flex flex-col lg:flex-row items-start lg:items-center gap-16 xl:gap-20 py-20">
<!-- Left: Copy -->
<div v-reveal class="flex-1 flex flex-col gap-5 min-w-0">
<span class="text-[#60A5FA] font-semibold text-[11px] tracking-[2px] uppercase">
Get in Touch
</span>
<h2 class="text-white font-bold text-[clamp(28px,3vw,40px)] leading-[1.15] max-w-[520px]">
Ready to bring TapTrack<br class="hidden sm:block" /> to your school?
</h2>
<p class="text-[#94A3B8] text-[16px] leading-[1.65] max-w-[520px]">
Tell us about your school and we'll get back to you with a tailored proposal — including implementation scope, NFC hardware, and SMS credit packages that fit your needs.
</p>
<div class="flex flex-col gap-2.5 mt-1">
<span
v-for="point in bulletPoints"
:key="point"
class="text-[#CBD5E1] text-[14px]"
>
{{ point }}
</span>
</div>
</div>
<!-- Right: Form card -->
<div v-reveal="{ delay: 150 }" class="w-full lg:w-[480px] shrink-0">
<div class="bg-[#1E293B] rounded-2xl p-8 lg:p-9 flex flex-col gap-5">
<div>
<h3 class="text-white font-bold text-xl">Request a Demo</h3>
<p class="text-[#64748B] text-[13px] mt-1">Fill this out and we'll reach you within 24 hours.</p>
</div>
<!-- Success state -->
<div v-if="submitted" class="flex flex-col items-center gap-3 py-6 text-center">
<div class="w-12 h-12 rounded-full bg-[#059669] flex items-center justify-center">
<span class="text-white font-bold text-xl"></span>
</div>
<p class="text-white font-semibold text-base">Request sent!</p>
<p class="text-[#94A3B8] text-sm">We'll get back to you within 24 hours.</p>
</div>
<!-- Form -->
<form v-else class="flex flex-col gap-4" @submit.prevent="handleSubmit">
<div class="flex flex-col gap-1.5">
<label class="text-[#CBD5E1] font-medium text-[13px]">School Name</label>
<input
v-model="form.school"
type="text"
placeholder="e.g. San Pedro Elementary School"
class="w-full h-[42px] bg-[#0F172A] border border-[#334155] rounded-lg px-3 text-[13px] text-white placeholder:text-[#475569] focus:outline-none focus:border-[#1D4ED8] transition-colors"
/>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-[#CBD5E1] font-medium text-[13px]">Contact Person</label>
<input
v-model="form.contact"
type="text"
placeholder="Principal / Administrator name"
class="w-full h-[42px] bg-[#0F172A] border border-[#334155] rounded-lg px-3 text-[13px] text-white placeholder:text-[#475569] focus:outline-none focus:border-[#1D4ED8] transition-colors"
/>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-[#CBD5E1] font-medium text-[13px]">Phone / Email</label>
<input
v-model="form.phone"
type="text"
placeholder="+63 9XX XXX XXXX or email@school.edu.ph"
class="w-full h-[42px] bg-[#0F172A] border border-[#334155] rounded-lg px-3 text-[13px] text-white placeholder:text-[#475569] focus:outline-none focus:border-[#1D4ED8] transition-colors"
/>
</div>
<p v-if="error" class="text-red-400 text-[13px]">{{ error }}</p>
<button
type="submit"
:disabled="submitting"
class="w-full bg-[#1D4ED8] hover:bg-[#1A43C0] disabled:opacity-60 text-white font-semibold text-[15px] py-[13px] rounded-lg transition-colors duration-150 mt-1 cursor-pointer"
>
{{ submitting ? 'Sending' : 'Send Request' }}
</button>
</form>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import type { Component } from 'vue'
defineProps<{
icon: Component
iconColor: string
iconBg: string
title: string
description: string
}>()
</script>
<template>
<div class="flex flex-col gap-3.5 bg-[#F8FAFC] border border-[#E2E8F0] rounded-[14px] p-7 pt-8">
<!-- Icon box -->
<div
class="w-11 h-11 rounded-[10px] flex items-center justify-center shrink-0"
:style="{ backgroundColor: iconBg }"
>
<component :is="icon" :size="22" :stroke-width="1.75" :style="{ color: iconColor }" />
</div>
<!-- Title -->
<h3 class="text-[#0F172A] font-semibold text-base leading-snug">{{ title }}</h3>
<!-- Description -->
<p class="text-[#64748B] text-[13px] leading-[1.65]">{{ description }}</p>
</div>
</template>

View File

@@ -0,0 +1,105 @@
<script setup lang="ts">
import {
CreditCard,
MessageSquare,
LayoutDashboard,
BookOpen,
Users,
Activity,
} from 'lucide-vue-next'
import FeatureCard from './FeatureCard.vue'
const features = [
{
icon: CreditCard,
iconColor: '#1D4ED8',
iconBg: '#EFF6FF',
title: 'NFC Tap-In Attendance',
description:
'Students tap their NFC ID card on the reader. Attendance is recorded instantly — no queuing, no manual entry, no errors.',
},
{
icon: MessageSquare,
iconColor: '#059669',
iconBg: '#ECFDF5',
title: 'Instant Parent SMS Alerts',
description:
'Parents receive an automatic SMS the moment their child arrives or is marked absent. No app required on their end.',
},
{
icon: LayoutDashboard,
iconColor: '#CA8A04',
iconBg: '#FEF9C3',
title: 'Live Attendance Dashboard',
description:
'Principals and advisers see real-time attendance, late counts, and absent students — all from a single screen.',
},
{
icon: BookOpen,
iconColor: '#7C3AED',
iconBg: '#F3E8FF',
title: 'Automated Attendance Reports',
description:
'Monthly attendance summaries are generated and sent automatically to administrators — with per-student and per-class breakdowns.',
},
{
icon: Users,
iconColor: '#E11D48',
iconBg: '#FFF1F2',
title: 'Student & Section Management',
description:
"Manage your full student roster, assign sections and advisers, and link each student to their parent's contact number.",
},
{
icon: Activity,
iconColor: '#0284C7',
iconBg: '#E0F2FE',
title: 'SMS Credit Monitoring',
description:
'Track your prepaid SMS credit balance in real time. Low-credit alerts let you top up before you run out — no interruptions to parent notifications.',
},
]
</script>
<template>
<section id="features" class="bg-white">
<div class="page-container flex flex-col gap-[72px] py-24">
<!-- Section header -->
<div v-reveal class="flex flex-col items-center gap-2.5">
<span class="text-[#1D4ED8] font-semibold text-[11px] tracking-[2px] uppercase">
What TapTrack Does
</span>
<h2 class="text-[#0F172A] font-bold text-[clamp(28px,3vw,40px)] leading-[1.15] text-center max-w-[560px]">
Every feature your school<br class="hidden sm:block" /> actually needs.
</h2>
<p class="text-[#64748B] text-[17px] leading-[1.6] text-center max-w-[560px]">
Designed around the real workflow of Filipino schools from homeroom attendance to parent communication.
</p>
</div>
<!-- Cards grid: 2 rows × 3 cols -->
<div class="flex flex-col gap-6">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="(feature, i) in features.slice(0, 3)"
:key="feature.title"
v-reveal="{ delay: i * 80 }"
>
<FeatureCard v-bind="feature" />
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="(feature, i) in features.slice(3)"
:key="feature.title"
v-reveal="{ delay: i * 80 }"
>
<FeatureCard v-bind="feature" />
</div>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
// No props needed — all content is static copy from design
</script>
<template>
<section class="bg-[#F8FAFF] min-h-[640px] flex items-center">
<div class="page-container w-full flex flex-col lg:flex-row items-center gap-16 xl:gap-20 py-16 lg:py-0">
<!-- Left Column -->
<div v-reveal class="flex-1 flex flex-col gap-7 min-w-0">
<!-- Pill badge -->
<div class="inline-flex items-center gap-1.5 bg-[#EFF6FF] rounded-full px-3 py-[5px] w-fit">
<span class="w-1.5 h-1.5 rounded-full bg-[#1D4ED8] shrink-0"></span>
<span class="text-[#1D4ED8] font-semibold text-xs">Built for Philippine Schools</span>
</div>
<!-- H1 -->
<h1 class="text-[#0F172A] font-bold text-[clamp(38px,4.5vw,58px)] leading-[1.1] max-w-[560px]">
Attendance That<br>Runs Itself.
</h1>
<!-- Subtitle -->
<p class="text-[#475569] text-[17px] leading-[1.7] max-w-[520px]">
TapTrack is an on-premises NFC attendance system students tap their ID card, parents get an SMS, and your records are updated automatically. No manual registers. No paper trails.
</p>
<!-- CTA Buttons -->
<div class="flex flex-wrap items-center gap-3">
<a
href="#contact"
class="inline-flex items-center bg-[#1D4ED8] hover:bg-[#1A43C0] text-white font-semibold text-[15px] px-[26px] py-[13px] rounded-lg transition-colors duration-150 whitespace-nowrap"
>
Request a Demo
</a>
<a
href="#how-it-works"
class="inline-flex items-center bg-white hover:bg-[#F8FAFF] text-[#334155] font-medium text-[15px] px-[26px] py-[13px] rounded-lg border border-[#CBD5E1] transition-colors duration-150 whitespace-nowrap"
>
See How It Works
</a>
</div>
<!-- Trust row -->
<div class="flex flex-wrap items-center gap-5">
<span class="text-[#64748B] text-[13px]">&nbsp; Student data stays on your servers</span>
<span class="text-[#64748B] text-[13px]">&nbsp; No cloud lock-in</span>
</div>
</div>
<!-- Right Column -->
<div v-reveal="{ delay: 150 }" class="w-full lg:w-[580px] shrink-0 flex flex-col gap-4 items-center">
<!-- Screenshot frame -->
<div class="w-full max-w-[580px] h-[300px] sm:h-[380px] rounded-2xl overflow-hidden border-2 border-[#E2E8F0] shadow-xl">
<img
src="/screenshots/58IBI.png"
alt="TapTrack live attendance dashboard"
class="w-full h-full object-cover object-top"
/>
</div>
<!-- Caption -->
<p class="text-[#94A3B8] text-xs text-center max-w-[520px] leading-relaxed">
Live dashboard running on your school's own server data never leaves your premises.
</p>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,118 @@
<script setup lang="ts">
import { Wrench, Zap, CircleCheck } from 'lucide-vue-next'
const steps = [
{
num: '1',
title: "We install TapTrack on your school's hardware",
description:
'Our team sets up the on-premises software and NFC reader. You pay a one-time implementation fee and you own the setup.',
},
{
num: '2',
title: 'We enrol your students and train your staff',
description:
'We import your student list, assign NFC ID cards, add parent numbers, and run a full walkthrough with your administrators.',
},
{
num: '3',
title: 'Top up SMS credits monthly as needed',
description:
'No recurring subscription. You purchase prepaid SMS credits when you need them. The system keeps sending alerts as long as your credits last.',
},
]
const billingItems = [
{
icon: Wrench,
iconColor: '#1D4ED8',
iconBg: '#EFF6FF',
title: 'One-Time Implementation Fee',
description: 'Covers installation, student enrolment, NFC reader setup, and staff training. Paid once.',
},
{
icon: Zap,
iconColor: '#059669',
iconBg: '#ECFDF5',
title: 'Prepaid SMS Credits',
description: 'Purchase credits when you need them. Each parent notification consumes one credit. Top up anytime.',
},
]
</script>
<template>
<section id="how-it-works" class="bg-[#F8FAFF]">
<div class="page-container flex flex-col lg:flex-row gap-16 xl:gap-20 py-24">
<!-- Left: Steps -->
<div v-reveal class="flex-1 flex flex-col gap-10 min-w-0">
<div class="flex flex-col gap-2.5">
<span class="text-[#1D4ED8] font-semibold text-[11px] tracking-[2px] uppercase">
How It Works
</span>
<h2 class="text-[#0F172A] font-bold text-[clamp(28px,3vw,38px)] leading-[1.15] max-w-[480px]">
Simple to set up.<br />Simple to run.
</h2>
<p class="text-[#64748B] text-[16px] leading-[1.6] max-w-[460px]">
Our team handles the full installation. Once you're live, the system runs on its own.
</p>
</div>
<div class="flex flex-col gap-8">
<div
v-for="(step, i) in steps"
:key="step.num"
v-reveal="{ delay: i * 100 }"
class="flex items-start gap-5"
>
<div class="w-9 h-9 rounded-full bg-[#1D4ED8] flex items-center justify-center shrink-0 mt-0.5">
<span class="text-white font-bold text-[15px] leading-none">{{ step.num }}</span>
</div>
<div class="flex flex-col gap-1.5 min-w-0">
<h3 class="text-[#0F172A] font-semibold text-[15px] leading-snug">{{ step.title }}</h3>
<p class="text-[#64748B] text-[13px] leading-[1.6]">{{ step.description }}</p>
</div>
</div>
</div>
</div>
<!-- Right: Billing card -->
<div v-reveal="{ delay: 150 }" class="w-full lg:w-[460px] shrink-0">
<div class="bg-white border border-[#E2E8F0] rounded-[20px] p-8 lg:p-10 flex flex-col gap-6">
<h3 class="text-[#0F172A] font-bold text-xl">How billing works</h3>
<p class="text-[#64748B] text-[14px] leading-[1.6]">
Transparent, no surprises. You only pay for what you actually use.
</p>
<div class="flex flex-col">
<div
v-for="(item, i) in billingItems"
:key="item.title"
class="flex items-start gap-3.5 py-4"
:class="i === 0 ? 'border-b border-[#F1F5F9]' : ''"
>
<div
class="w-9 h-9 rounded-lg flex items-center justify-center shrink-0"
:style="{ backgroundColor: item.iconBg }"
>
<component :is="item.icon" :size="18" :stroke-width="1.75" :style="{ color: item.iconColor }" />
</div>
<div class="flex flex-col gap-0.5 min-w-0">
<span class="text-[#0F172A] font-semibold text-[14px]">{{ item.title }}</span>
<span class="text-[#64748B] text-[13px] leading-[1.5]">{{ item.description }}</span>
</div>
</div>
</div>
<div class="flex items-start gap-2.5 bg-[#F0FDF4] rounded-lg px-3.5 py-3">
<CircleCheck :size="16" :stroke-width="2" class="text-[#16A34A] shrink-0 mt-0.5" />
<p class="text-[#15803D] text-[13px] leading-[1.5]">
No monthly subscription fee. No lock-in contract. Pricing shared upon request.
</p>
</div>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,46 @@
<script setup lang="ts">
import { Server, Shield, WifiOff } from 'lucide-vue-next'
const stats = [
{ icon: Server, label: 'On-Premises', sub: 'Your server only' },
{ icon: Shield, label: 'DepEd Aligned', sub: 'Data privacy ready' },
{ icon: WifiOff, label: 'Works Offline', sub: 'No internet required' },
]
</script>
<template>
<section class="bg-[#0F172A]">
<div class="page-container flex flex-col lg:flex-row items-start lg:items-center gap-12 lg:gap-20 py-14">
<!-- Left: Text content -->
<div v-reveal class="flex-1 flex flex-col gap-3 min-w-0">
<div class="w-fit bg-[#1E3A5F] rounded px-[10px] py-1">
<span class="text-[#60A5FA] font-semibold text-[11px] tracking-[1px] uppercase">
Data Privacy
</span>
</div>
<h2 class="text-white font-bold text-[28px] leading-[1.2] max-w-[500px]">
Student data never leaves your school.
</h2>
<p class="text-[#94A3B8] text-[15px] leading-[1.6] max-w-[500px]">
TapTrack runs entirely on your own server. No student records, attendance logs, or personal data are ever uploaded to a third-party cloud.
</p>
</div>
<!-- Right: Stat items -->
<div class="flex flex-row flex-wrap gap-10 shrink-0">
<div
v-for="(stat, i) in stats"
:key="stat.label"
v-reveal="{ delay: i * 100 }"
class="flex flex-col items-center gap-1.5 min-w-[90px]"
>
<component :is="stat.icon" class="text-[#60A5FA]" :size="28" :stroke-width="1.75" />
<span class="text-white font-semibold text-[13px]">{{ stat.label }}</span>
<span class="text-[#64748B] text-[12px]">{{ stat.sub }}</span>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,61 @@
<script setup lang="ts">
const cards = [
{
img: '/screenshots/4frZA.png',
alt: 'TapTrack Student Directory screen',
title: 'Student Directory',
description:
'Manage your full roster — NFC ID assignments, parent numbers, section placement, and enrolment status all in one place.',
},
{
img: '/screenshots/Reports.jpg',
alt: 'TapTrack Reports & Analytics screen',
title: 'Reports & Analytics',
description:
'Generate attendance summaries by class, date range, and section. Export to PDF or Excel for DepEd compliance and parent meetings.',
},
]
</script>
<template>
<section class="bg-white">
<div class="page-container flex flex-col gap-14 py-24">
<!-- Header -->
<div v-reveal class="flex flex-col items-center gap-2.5">
<span class="text-[#1D4ED8] font-semibold text-[11px] tracking-[2px] uppercase">
Inside TapTrack
</span>
<h2 class="text-[#0F172A] font-bold text-[clamp(26px,3vw,38px)] leading-[1.15] text-center max-w-[600px]">
A system built for how your school actually works.
</h2>
<p class="text-[#64748B] text-[16px] leading-[1.6] text-center max-w-[580px]">
Every screen is purpose-built for school administrators, advisers, and principals not generic enterprise software.
</p>
</div>
<!-- Screenshot cards grid -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-7">
<div
v-for="(card, i) in cards"
:key="card.title"
v-reveal="{ delay: i * 120 }"
class="flex flex-col rounded-[14px] border border-[#E2E8F0] overflow-hidden"
>
<div class="h-[240px] sm:h-[300px] lg:h-[340px] overflow-hidden">
<img
:src="card.img"
:alt="card.alt"
class="w-full h-full object-cover object-top"
/>
</div>
<div class="flex flex-col gap-1.5 px-6 py-5">
<h3 class="text-[#0F172A] font-semibold text-base">{{ card.title }}</h3>
<p class="text-[#64748B] text-[13px] leading-[1.6]">{{ card.description }}</p>
</div>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
// Gate View kiosk screenshot section
</script>
<template>
<section class="bg-[#F8FAFF]">
<div class="page-container flex flex-col items-center gap-10 py-24">
<!-- Header -->
<div v-reveal class="flex flex-col items-center gap-2.5 w-full">
<span class="text-[#1D4ED8] font-semibold text-[11px] tracking-[2px] uppercase">
The System in Action
</span>
<h2 class="text-[#0F172A] font-bold text-[clamp(28px,3vw,38px)] leading-[1.15] text-center max-w-[600px]">
The gate display students see every morning.
</h2>
<p class="text-[#64748B] text-[16px] leading-[1.6] text-center max-w-[600px]">
A full-screen kiosk view runs at your school gate students tap their NFC ID card and instantly see their name, section, and status confirmed.
</p>
</div>
<!-- Gate View Screenshot -->
<div v-reveal="{ delay: 100 }" class="w-full max-w-[960px] rounded-2xl overflow-hidden border border-[#E2E8F0] shadow-lg">
<img
src="/screenshots/gE861.png"
alt="TapTrack gate kiosk display"
class="w-full h-auto object-cover"
/>
</div>
<!-- Caption -->
<p v-reveal="{ delay: 150 }" class="text-[#94A3B8] text-[13px] text-center max-w-[700px] leading-relaxed">
Full-screen gate display running on a monitor at your school entrance. No login required for students just tap and go.
</p>
</div>
</section>
</template>

View File

@@ -0,0 +1,109 @@
<script setup lang="ts">
import {
Headphones,
Settings,
GraduationCap,
Monitor,
DatabaseBackup,
} from 'lucide-vue-next'
import FeatureCard from './FeatureCard.vue'
const supportCards = [
{
icon: Headphones,
iconColor: '#1D4ED8',
iconBg: '#EFF6FF',
title: 'Dedicated Support Channel',
description:
'Get direct access to our technical team via chat or phone. No ticket queues, no bots — real people who know your setup.',
},
{
icon: Settings,
iconColor: '#CA8A04',
iconBg: '#FEF9C3',
title: 'System Updates & Maintenance',
description:
'We push updates to fix bugs and add improvements. Your system stays current without you having to do anything.',
},
{
icon: GraduationCap,
iconColor: '#059669',
iconBg: '#ECFDF5',
title: 'Staff Re-training Assistance',
description:
"New school year, new staff? We'll get your team up to speed. Re-training sessions available on request.",
},
{
icon: Monitor,
iconColor: '#7C3AED',
iconBg: '#F3E8FF',
title: 'Remote Troubleshooting',
description:
'Most issues can be resolved remotely. Our team can connect to your system and fix problems without an on-site visit.',
},
{
icon: DatabaseBackup,
iconColor: '#E11D48',
iconBg: '#FFF1F2',
title: 'Data Backup Assistance',
description:
'We help you set up regular attendance data backups so your records are always safe, even if hardware fails.',
},
]
</script>
<template>
<section id="support" class="bg-white">
<div class="page-container flex flex-col gap-16 py-24">
<!-- Header -->
<div v-reveal class="flex flex-col items-center gap-2.5">
<span class="text-[#1D4ED8] font-semibold text-[11px] tracking-[2px] uppercase">
After-Sales Support
</span>
<h2 class="text-[#0F172A] font-bold text-[clamp(28px,3vw,38px)] leading-[1.15] text-center max-w-[560px]">
We don't just install and leave.
</h2>
<p class="text-[#64748B] text-[17px] leading-[1.6] text-center max-w-[560px]">
Our team stays with you long after go-live. If something breaks or your staff needs help, we're one message away.
</p>
</div>
<!-- Row 1: 3 cards -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="(card, i) in supportCards.slice(0, 3)"
:key="card.title"
v-reveal="{ delay: i * 80 }"
>
<FeatureCard v-bind="card" />
</div>
</div>
<!-- Row 2: 2 cards + quote card -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="(card, i) in supportCards.slice(3)"
:key="card.title"
v-reveal="{ delay: i * 80 }"
>
<FeatureCard v-bind="card" />
</div>
<!-- Quote card -->
<div v-reveal="{ delay: 160 }" class="flex flex-col gap-4 bg-[#EFF6FF] border border-[#BFDBFE] rounded-[14px] p-7 pt-9">
<p class="text-[#1E3A5F] font-medium text-base leading-[1.65]">
"Think of us as your school's IT partner for attendance — not just a vendor who sold you something."
</p>
<div class="flex items-center gap-2.5">
<div class="w-9 h-9 rounded-full bg-[#1D4ED8] flex items-center justify-center shrink-0">
<span class="text-white font-bold text-xs">TT</span>
</div>
<span class="text-[#1D4ED8] font-semibold text-[13px]">TapTrack Support Team</span>
</div>
</div>
</div>
</div>
</section>
</template>

39
src/directives/reveal.ts Normal file
View File

@@ -0,0 +1,39 @@
import type { Directive } from 'vue'
// v-reveal — fades + slides element up when it enters the viewport
// Usage: <div v-reveal> or <div v-reveal="{ delay: 200 }">
interface RevealOptions {
delay?: number // ms delay before animation starts (default 0)
duration?: number // ms animation duration (default 600)
y?: number // px to translate from (default 24)
}
const reveal: Directive<HTMLElement, RevealOptions | undefined> = {
mounted(el, binding) {
const { delay = 0, duration = 600, y = 24 } = binding.value ?? {}
// Set initial invisible state
el.style.opacity = '0'
el.style.transform = `translateY(${y}px)`
el.style.transition = `opacity ${duration}ms ease, transform ${duration}ms ease`
el.style.transitionDelay = `${delay}ms`
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
el.style.opacity = '1'
el.style.transform = 'translateY(0)'
observer.unobserve(el)
}
})
},
{ threshold: 0.12 },
)
observer.observe(el)
},
}
export default reveal

8
src/main.ts Normal file
View File

@@ -0,0 +1,8 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import reveal from './directives/reveal'
const app = createApp(App)
app.directive('reveal', reveal)
app.mount('#app')

23
src/pages/LandingPage.vue Normal file
View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import HeroSection from '../components/HeroSection.vue'
import PrivacyBanner from '../components/PrivacyBanner.vue'
import SeeItLiveSection from '../components/SeeItLiveSection.vue'
import FeaturesSection from '../components/FeaturesSection.vue'
import ScreenshotsSection from '../components/ScreenshotsSection.vue'
import HowItWorksSection from '../components/HowItWorksSection.vue'
import SupportSection from '../components/SupportSection.vue'
import ContactSection from '../components/ContactSection.vue'
</script>
<template>
<main>
<HeroSection />
<PrivacyBanner />
<SeeItLiveSection />
<FeaturesSection />
<ScreenshotsSection />
<HowItWorksSection />
<SupportSection />
<ContactSection />
</main>
</template>

99
src/style.css Normal file
View File

@@ -0,0 +1,99 @@
@import "tailwindcss";
/* ─── Design Tokens ─────────────────────────────────────── */
@theme {
/* Colors — Primary */
--color-primary: #1D4ED8;
--color-primary-hover: #1A43C0;
--color-primary-light: #EFF6FF;
--color-primary-dark: #1E3A5F;
/* Colors — Navy / Dark backgrounds */
--color-navy: #0F172A;
--color-navy-mid: #1E293B;
--color-navy-light: #334155;
/* Colors — Text */
--color-text-heading: #0F172A;
--color-text-body: #475569;
--color-text-muted: #64748B;
--color-text-subtle: #94A3B8;
--color-text-faint: #CBD5E1;
/* Colors — Backgrounds */
--color-bg-white: #FFFFFF;
--color-bg-alt: #F8FAFF;
--color-bg-card: #F8FAFC;
/* Colors — Borders */
--color-border: #E2E8F0;
--color-border-dark: #334155;
/* Colors — Status */
--color-success: #059669;
--color-success-bg: #ECFDF5;
--color-success-text: #4ADE80;
--color-success-dark: #14532D;
--color-warning: #CA8A04;
--color-warning-bg: #FEF9C3;
--color-danger: #E11D48;
--color-danger-bg: #FFF1F2;
/* Colors — Accent */
--color-purple: #7C3AED;
--color-purple-bg: #F3E8FF;
--color-sky: #0284C7;
--color-sky-bg: #E0F2FE;
--color-blue-400: #60A5FA;
/* Typography */
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
}
/* ─── Base Reset ────────────────────────────────────────── */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-sans);
color: #475569;
background-color: #ffffff;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ─── Page Container ────────────────────────────────────── */
.page-container {
max-width: 1440px;
margin-inline: auto;
padding-inline: 100px;
}
@media (max-width: 1280px) {
.page-container {
padding-inline: 64px;
}
}
@media (max-width: 1024px) {
.page-container {
padding-inline: 40px;
}
}
@media (max-width: 640px) {
.page-container {
padding-inline: 24px;
}
}