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
217
PLAN.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# TapTrack Website — Development Plan
|
||||
|
||||
**Stack:** Vue 3 + TypeScript · Tailwind CSS v4 · Vite
|
||||
**Deployment:** Vercel / Netlify
|
||||
**Design source:** `pencil-new.pen`
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Project Scaffold & Foundation
|
||||
**Goal:** Runnable dev server with global styles, fonts, and design tokens in place.
|
||||
|
||||
### Tasks
|
||||
1. Scaffold project with `npm create vite@latest` (Vue + TypeScript)
|
||||
2. Install and configure Tailwind CSS v4
|
||||
3. Install `lucide-vue-next` for icons
|
||||
4. Add Inter font via Google Fonts (`index.html`)
|
||||
5. Define global CSS variables (colors, spacing) from design tokens
|
||||
6. Set up base `App.vue` shell with router placeholder
|
||||
7. Move `public/screenshots/` and `Reports.jpg` into `public/` asset tree
|
||||
8. Verify `npm run dev` runs clean with blank page
|
||||
|
||||
### Deliverable
|
||||
Clean Vite + Vue 3 + Tailwind v4 dev server at `localhost:5173`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Layout Shell & Navigation
|
||||
**Goal:** Full-width page shell with sticky nav and footer wired up.
|
||||
|
||||
### Tasks
|
||||
1. Create `AppNavbar.vue`
|
||||
- TapTrack logo mark (blue "T" box) + wordmark
|
||||
- Nav links: Features, How It Works, Services, Contact
|
||||
- "Request a Demo" CTA button (scrolls to contact section)
|
||||
- Sticky on scroll, white background, bottom border
|
||||
2. Create `AppFooter.vue`
|
||||
- Brand column with logo + tagline
|
||||
- 3 link columns: Product, Company, Legal
|
||||
- Bottom bar with copyright
|
||||
3. Wire `App.vue` → `AppNavbar` + `<router-view>` + `AppFooter`
|
||||
4. Create `LandingPage.vue` as the `/` route
|
||||
|
||||
### Deliverable
|
||||
Navigable page shell with working sticky header and footer
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Hero Section
|
||||
**Goal:** Above-the-fold hero with real screenshot mockup.
|
||||
|
||||
### Tasks
|
||||
1. Create `HeroSection.vue`
|
||||
- "Built for Philippine Schools" pill badge
|
||||
- H1: "Attendance That Runs Itself."
|
||||
- Subtitle paragraph
|
||||
- Two CTA buttons ("Request a Demo" + "See How It Works")
|
||||
- Trust row (two checkmark lines)
|
||||
- Right column: screenshot of Live Attendance (`public/screenshots/58IBI.png`) in a rounded frame with border
|
||||
- Caption below screenshot
|
||||
2. Responsive: stack to single column on mobile
|
||||
|
||||
### Deliverable
|
||||
Pixel-faithful hero section matching the Pencil design
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Privacy Banner & "See It Live" Section
|
||||
**Goal:** Two full-width sections between Hero and Features.
|
||||
|
||||
### Tasks
|
||||
1. Create `PrivacyBanner.vue`
|
||||
- Dark navy background
|
||||
- Left: "DATA PRIVACY" label + headline + subtitle
|
||||
- Right: 3 stat items (On-Premises, DepEd Aligned, Works Offline) with Lucide icons
|
||||
2. Create `SeeItLiveSection.vue`
|
||||
- Light blue background
|
||||
- Centered headline + subtitle
|
||||
- Gate View screenshot (`public/screenshots/gE861.png`) in rounded frame
|
||||
- Caption below
|
||||
|
||||
### Deliverable
|
||||
Two polished full-width sections
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Features Section
|
||||
**Goal:** 6-card features grid.
|
||||
|
||||
### Tasks
|
||||
1. Create reusable `FeatureCard.vue` component
|
||||
- Props: icon name, icon bg color, icon color, title, description
|
||||
2. Create `FeaturesSection.vue`
|
||||
- "WHAT TAPTRACK DOES" label + headline + subtitle
|
||||
- 2 rows × 3 cards using `FeatureCard`
|
||||
- Cards: NFC Tap-In, Parent SMS, Live Dashboard, Reports, Student Mgmt, SMS Credits
|
||||
|
||||
### Deliverable
|
||||
Full features grid, pixel-faithful to design
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — App Screenshots Section
|
||||
**Goal:** 2-column screenshot showcase with labels.
|
||||
|
||||
### Tasks
|
||||
1. Create `ScreenshotsSection.vue`
|
||||
- "INSIDE TAPTRACK" label + headline + subtitle
|
||||
- 2-column grid:
|
||||
- Student Directory card (`public/screenshots/4frZA.png`)
|
||||
- Reports & Analytics card (`Reports.jpg`)
|
||||
- Each card: screenshot image + title + description below
|
||||
2. Responsive: stack to single column on mobile
|
||||
|
||||
### Deliverable
|
||||
Screenshot showcase section
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 — How It Works Section
|
||||
**Goal:** 3-step explainer + billing card side panel.
|
||||
|
||||
### Tasks
|
||||
1. Create `HowItWorksSection.vue`
|
||||
- Left column: "HOW IT WORKS" label + headline + subtitle + 3 numbered steps
|
||||
- Right column: "How billing works" white card
|
||||
- One-Time Implementation Fee item
|
||||
- Prepaid SMS Credits item
|
||||
- "No subscription fee" green note
|
||||
|
||||
### Deliverable
|
||||
How It Works section with billing breakdown
|
||||
|
||||
---
|
||||
|
||||
## Phase 8 — Support Section
|
||||
**Goal:** 5 support cards + 1 quote card.
|
||||
|
||||
### Tasks
|
||||
1. Reuse `FeatureCard.vue` or create `SupportCard.vue`
|
||||
2. Create `SupportSection.vue`
|
||||
- "AFTER-SALES SUPPORT" label + headline + subtitle
|
||||
- Row 1: Dedicated Support, System Updates, Staff Re-training
|
||||
- Row 2: Remote Troubleshooting, Data Backup, Quote card (blue tint)
|
||||
|
||||
### Deliverable
|
||||
Full support section
|
||||
|
||||
---
|
||||
|
||||
## Phase 9 — Contact / CTA Section
|
||||
**Goal:** Dark contact section with embedded demo request form.
|
||||
|
||||
### Tasks
|
||||
1. Create `ContactSection.vue`
|
||||
- Dark navy background
|
||||
- Left: "GET IN TOUCH" label + headline + subtitle + 3 bullet points
|
||||
- Right: dark "Request a Demo" form card
|
||||
- Fields: School Name, Contact Person, Phone / Email
|
||||
- "Send Request" blue button
|
||||
2. Wire up basic form validation (required fields)
|
||||
3. Optional: integrate with Formspree or EmailJS for real submissions
|
||||
|
||||
### Deliverable
|
||||
Working contact/demo request form
|
||||
|
||||
---
|
||||
|
||||
## Phase 10 — Polish, Responsiveness & Deployment
|
||||
**Goal:** Production-ready site, fully responsive, deployed.
|
||||
|
||||
### Tasks
|
||||
1. Mobile responsiveness pass for all sections (breakpoints: sm, md, lg)
|
||||
2. Smooth scroll behavior for anchor nav links
|
||||
3. Scroll-triggered fade-in animations (Intersection Observer)
|
||||
4. Add `<meta>` SEO tags (title, description, og:image)
|
||||
5. Favicon using the TapTrack "T" blue mark
|
||||
6. Run `npm run build` — fix any TypeScript or Tailwind errors
|
||||
7. Deploy to Vercel (connect GitHub repo → auto deploy)
|
||||
8. Verify live URL renders correctly
|
||||
|
||||
### Deliverable
|
||||
Live production URL on Vercel/Netlify
|
||||
|
||||
---
|
||||
|
||||
## Section → Component Map
|
||||
|
||||
| Section | Component | Screenshot / Asset |
|
||||
|---|---|---|
|
||||
| Navigation | `AppNavbar.vue` | — |
|
||||
| Hero | `HeroSection.vue` | `58IBI.png` |
|
||||
| Privacy Banner | `PrivacyBanner.vue` | — |
|
||||
| See It Live | `SeeItLiveSection.vue` | `gE861.png` |
|
||||
| Features | `FeaturesSection.vue` + `FeatureCard.vue` | — |
|
||||
| App Screenshots | `ScreenshotsSection.vue` | `4frZA.png` + `Reports.jpg` |
|
||||
| How It Works | `HowItWorksSection.vue` | — |
|
||||
| Support | `SupportSection.vue` | — |
|
||||
| Contact / CTA | `ContactSection.vue` | — |
|
||||
| Footer | `AppFooter.vue` | — |
|
||||
|
||||
---
|
||||
|
||||
## Design Tokens
|
||||
|
||||
| Token | Value |
|
||||
|---|---|
|
||||
| Primary blue | `#1D4ED8` |
|
||||
| Dark navy | `#0F172A` |
|
||||
| Body text | `#475569` |
|
||||
| Muted text | `#64748B` |
|
||||
| Subtle text | `#94A3B8` |
|
||||
| Background alt | `#F8FAFF` |
|
||||
| Card background | `#F8FAFC` |
|
||||
| Border | `#E2E8F0` |
|
||||
| Font | Inter |
|
||||
| Icons | Lucide (lucide-vue-next) |
|
||||
| Page width | 1440px max, 100px padding |
|
||||
BIN
Reports.jpg
Normal file
|
After Width: | Height: | Size: 178 KiB |
39
index.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<!-- Primary SEO -->
|
||||
<title>TapTrack — Attendance That Runs Itself</title>
|
||||
<meta name="description" content="TapTrack is an on-premises NFC student attendance system built for Philippine schools. Students tap their ID card, parents get an SMS, records update instantly. No cloud. No subscription." />
|
||||
<meta name="keywords" content="NFC attendance, student attendance Philippines, RFID attendance system, DepEd attendance, school attendance automation, SMS parent notification" />
|
||||
<meta name="author" content="TapTrack" />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://taptrack.ph/" />
|
||||
<meta property="og:title" content="TapTrack — Attendance That Runs Itself" />
|
||||
<meta property="og:description" content="On-premises NFC attendance for Philippine schools. Students tap, parents get SMS, records update automatically." />
|
||||
<meta property="og:image" content="/screenshots/58IBI.png" />
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="TapTrack — Attendance That Runs Itself" />
|
||||
<meta name="twitter:description" content="On-premises NFC attendance for Philippine schools. Students tap, parents get SMS, records update automatically." />
|
||||
<meta name="twitter:image" content="/screenshots/58IBI.png" />
|
||||
|
||||
<!-- Inter Font -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
2170
package-lock.json
generated
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "taptrack-website",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-vue-next": "^0.511.0",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.3.3",
|
||||
"vue-tsc": "^2.2.8"
|
||||
}
|
||||
}
|
||||
2958
pencil-new.pen
Normal file
4
public/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="8" fill="#1D4ED8"/>
|
||||
<text x="16" y="23" font-family="Inter, Arial, sans-serif" font-size="18" font-weight="bold" fill="white" text-anchor="middle">T</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 261 B |
24
public/icons.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/screenshots/4frZA.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
public/screenshots/58IBI.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
public/screenshots/Reports.jpg
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
public/screenshots/gE861.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
public/screenshots/rj2JV.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
16
src/App.vue
Normal 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>
|
||||
99
src/components/AppFooter.vue
Normal 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>
|
||||
116
src/components/AppNavbar.vue
Normal 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>
|
||||
146
src/components/ContactSection.vue
Normal 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>
|
||||
29
src/components/FeatureCard.vue
Normal 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>
|
||||
105
src/components/FeaturesSection.vue
Normal 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>
|
||||
71
src/components/HeroSection.vue
Normal 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]">✓ Student data stays on your servers</span>
|
||||
<span class="text-[#64748B] text-[13px]">✓ 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>
|
||||
118
src/components/HowItWorksSection.vue
Normal 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>
|
||||
46
src/components/PrivacyBanner.vue
Normal 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>
|
||||
61
src/components/ScreenshotsSection.vue
Normal 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>
|
||||
38
src/components/SeeItLiveSection.vue
Normal 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>
|
||||
109
src/components/SupportSection.vue
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
14
tsconfig.app.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"types": ["vite/client"],
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
23
tsconfig.node.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
5
vercel.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{ "source": "/(.*)", "destination": "/" }
|
||||
]
|
||||
}
|
||||
11
vite.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
tailwindcss(),
|
||||
],
|
||||
})
|
||||