# Project export: LifeTap

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: Press one button and your exact location fires to your emergency contact over WhatsApp with a map link, while an 80dB alarm sounds. No phone, no app, no dialing. Help knows where you are in seconds.
- Devpost: https://devpost.com/software/lifetap
- GitHub: https://github.com/ShadowEsu/LifeTap-website
- Demo: https://life-tap-website.vercel.app/
- Video: https://www.youtube.com/embed/Rey7MmR0J4I?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Preston Susanto (1 commits)

## Devpost submission (written by the team)

### Inspiration

We come from Indonesia, a country that sits on the collision of two tectonic plates. Natural disasters are not abstract to us, they are part of life. Some of the deadliest events in human history happened in our region. The 1883 eruption of Krakatoa altered the global climate. The 1931 Yangtze River floods killed an estimated several million people, one of the deadliest disasters ever recorded. The 2010 Haiti earthquake killed roughly 300,000. Behind every one of those numbers is a family that lost someone, often because help could not find them in time. That is the gap we kept coming back to. In a disaster, the problem is rarely that no one cares. It is that no one knows where you are. Pair that with the rise in kidnappings across the US and Asia, and the same truth holds: the faster someone knows your exact location, the better your odds. We wanted to build the simplest possible tool that closes the distance between "something is wrong" and "someone knows where I am."

### What it does

LifeTap is a physical panic button built from an Arduino, a Raspberry Pi 3, a buzzer, and a GPS module. When you are in trouble, you press the button. Two things happen at once. An 80 decibel buzzer sounds for 30 seconds to draw immediate attention and disorient a potential attacker. At the same time, the device captures your GPS coordinates and sends a pinpoint location to your emergency contact over WhatsApp, complete with a Google Maps link so they can navigate straight to you. It doesnt need a user to unlock a phone. No opening an app. No dialing. One press, and the people who can help you know exactly where you are.

### How we built it

The Raspberry Pi 3 is the brain. It runs the main logic, handles the GPS processing, and acts as the transmitter that sends the alert out over WhatsApp. The Arduino is the receiver layer, reading the GPS coordinates and the button state and passing them to the Pi. A green status LED pulses to signal that the device is connected and working, so the user always knows it is armed and ready. The flow is simple by design. The GPS module acquires a satellite fix and feeds coordinates to the Arduino. The Arduino relays the button press and the coordinates to the Pi over a serial connection. The Pi packages the location into a message and sends it out. The buzzer fires the instant the button is pressed, before any of the networking happens, so the alarm never waits on a signal.

### Challenges we ran into

We forgot a dedicated transmitter module. Rather than stall, we improvised, wiring the Raspberry Pi and Arduino together over USB serial and using that as our communication link. It worked, and it actually made the system simpler. We hit complications getting a Twilio phone number provisioned in time, so we pivoted from SMS to WhatsApp for the alert delivery. WhatsApp turned out to be easier to integrate and works internationally, which fits our disaster-response use case better anyway. GPS was harder than we expected. Getting a reliable satellite fix, especially indoors at the venue, taught us a lot about the limits of consumer GPS hardware and forced us to think about fallback behavior.

### Accomplishments we're proud of

We built a complete, working system in 24 hours. The connections hold, the button triggers the buzzer, the GPS acquires coordinates, and the alert reaches a phone over WhatsApp with a working map link. Every time we hit a wall, we improvised a path through it instead of cutting the feature. Walking up to the table with hardware that actually fires end-to-end, on our own wiring, is the thing we are proudest of.

### What we learned

GPS and satellite connectivity are genuinely hard. Getting a fix depends on conditions you do not control, and building around that uncertainty is its own engineering problem. We also learned that WhatsApp integration is far simpler than carrier SMS for getting a location message onto someone's phone quickly, especially across borders. Most of all, we learned that improvising under a deadline is a skill, and that a simpler design that works beats an elegant design that does not.

### What's next

We want LifeTap to connect directly to the nearest emergency services and post a live, continuously updating location, which means working with local government and dispatch systems rather than just private contacts. On the hardware side, we want to make the device smaller, cheaper, and longer-lasting on a single charge, and add a true satellite uplink so it works in the remote and disaster-struck areas where cell coverage disappears exactly when it matters most.

## README (from the GitHub repository)

# LifeTap Website

Public launch site for **LifeTap** — a hardware panic button that sends GPS coordinates via SMS on one press.

Built for Y Combinator outreach, investor demos, waitlist signups, and product documentation.

## Features

- Scroll-driven **Two Futures** story (with / without LifeTap)
- Hardware prototype section with specs, blueprints, and architecture diagrams
- Crisis stats, SMS mockup, and 911 comparison table
- Docs hub (`/docs`) with hackathon guide, architecture, and roadmap
- Waitlist API stub, Calendly demo booking, mobile-first nav

## Stack

- **Next.js 16** (App Router)
- **TypeScript**
- **Tailwind CSS 4**
- **Framer Motion** + **GSAP ScrollTrigger** + **Lenis**
- **Poppins** via `next/font`

## Quick start

```bash
npm install
cp .env.example .env.local
npm run dev
```

Open [http://localhost:3000](http://localhost:3000).

## Scripts

| Command | Description |
|---------|-------------|
| `npm run dev` | Development server |
| `npm run build` | Production build |
| `npm run start` | Serve production build |
| `npm run lint` | ESLint |

## Project structure

```
lifetap_website/
├── content/              # Markdown source for docs pages
├── docs/                 # Internal project documentation
├── public/images/        # Hero, prototype blueprints, OG image
├── src/
│   ├── app/              # Routes, layout, API, docs
│   ├── components/
│   │   ├── layout/       # Nav, footer, smooth scroll
│   │   ├── sections/     # Homepage sections
│   │   └── ui/           # Reusable UI pieces
│   ├── constants/        # Copy, stats, image paths
│   ├── lib/              # Utilities
│   └── types/            # Shared TypeScript types
└── .env.example          # Environment template
```

## Environment variables

| Variable | Description |
|----------|-------------|
| `NEXT_PUBLIC_SITE_URL` | Canonical site URL (OG tags, sitemap) |

See `.env.example` for waitlist provider placeholders (Resend, Supabase, webhook).

## Waitlist API

`POST /api/waitlist` with `{ "email": "..." }` — stub ready for your email provider or database.

## Docs

- Live docs: `/docs` on the site
- [Architecture](./docs/ARCHITECTURE.md)
- [Assets guide](./docs/ASSETS.md)
- [Content & copy](./docs/CONTENT.md)
- [Deployment](./docs/DEPLOYMENT.md)
- [Roadmap](./docs/ROADMAP.md)

## Deploy from GitHub

This repo is ready to host. **Recommended: Vercel** (free, built for Next.js).

1. Push this repo to GitHub (see below).
2. Go to [vercel.com/new](https://vercel.com/new) → **Import Git Repository**.
3. Select **LifeTap-Website** → Deploy (defaults work).
4. In Vercel → **Settings → Environment Variables**, add:
   ```
   NEXT_PUBLIC_SITE_URL=https://your-domain.vercel.app
   ```
5. Redeploy. Your site is live.

GitHub Actions runs `npm run build` on every push to `main` so broken builds are caught before deploy.

**Note:** GitHub Pages alone does not support this app (Next.js API routes + server features). Use Vercel, Netlify, or any Node.js host.

See [docs/DEPLOYMENT.md](./docs/DEPLOYMENT.md) for custom domains and waitlist setup.

## Push to GitHub

```bash
gh auth login
./scripts/publish-github.sh
```

Or manually:

```bash
git remote add origin git@github.com:YOUR_USERNAME/LifeTap-Website.git
git push -u origin main
```

## Deploy (other hosts)

## License

All rights reserved — LifeTap Technologies © 2026


## Detected evidence (automated analysis)

Indexed codebase: 74 recognized source files, 180 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- C++ (language) — claimed on Devpost, not found in the code
- HTML (language) — claimed on Devpost, not found in the code
- Python (language) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (85 of 85)

```
.env.example
.github/workflows/ci.yml
.gitignore
AGENTS.md
CLAUDE.md
content/ARCHITECTURE.md
content/ASSETS.md
content/CONTENT.md
content/DEPLOYMENT.md
content/hackathon-guide.md
content/ROADMAP.md
docs/ARCHITECTURE.md
docs/ASSETS.md
docs/CONTENT.md
docs/DEPLOYMENT.md
docs/ROADMAP.md
eslint.config.mjs
next.config.ts
package.json
postcss.config.mjs
public/images/.gitkeep
public/images/hero/.gitkeep
public/images/og/.gitkeep
public/images/product/.gitkeep
public/images/README.md
public/images/sms/.gitkeep
README.md
scripts/publish-github.sh
src/app/api/waitlist/route.ts
src/app/docs/[slug]/page.tsx
src/app/docs/page.tsx
src/app/globals.css
src/app/layout.tsx
src/app/page.tsx
src/app/robots.ts
src/app/sitemap.ts
src/components/docs/DocsShell.tsx
src/components/docs/MarkdownArticle.tsx
src/components/layout/CustomCursor.tsx
src/components/layout/Footer.tsx
src/components/layout/MobileBottomBar.tsx
src/components/layout/Navigation.tsx
src/components/layout/SmoothScrollProvider.tsx
src/components/layout/TopBanner.tsx
src/components/sections/ComparisonTable.tsx
src/components/sections/DocsPreview.tsx
src/components/sections/HardwareBlueprint.tsx
src/components/sections/Hero.tsx
src/components/sections/HowItWorks.tsx
src/components/sections/ProblemSection.tsx
src/components/sections/PrototypeSection.tsx
src/components/sections/ResultSection.tsx
src/components/sections/SignalJourney.tsx
src/components/sections/SMSMockup.tsx
src/components/sections/StatsSection.tsx
src/components/sections/TwoFutures.tsx
src/components/sections/TwoFuturesMobile.tsx
src/components/sections/WaitlistBenefits.tsx
src/components/sections/WaitlistCTA.tsx
src/components/ui/AlertBeacon.tsx
src/components/ui/CountUp.tsx
src/components/ui/DiagramTabs.tsx
src/components/ui/HeroSignalFlow.tsx
src/components/ui/HighlightHeadline.tsx
src/components/ui/HighlightMark.tsx
src/components/ui/ImageSlot.tsx
src/components/ui/Loader.tsx
src/components/ui/MagneticButton.tsx
src/components/ui/ParallaxBackground.tsx
src/components/ui/ProductButton.tsx
src/components/ui/ScrollReveal.tsx
src/components/ui/SplitReveal.tsx
src/components/ui/TickerMarquee.tsx
src/components/ui/WordReveal.tsx
src/components/ui/YCBadge.tsx
src/constants/copy.ts
src/constants/docs.ts
src/constants/images.ts
src/constants/index.ts
src/constants/stats.ts
src/lib/docs.ts
src/lib/gsap.ts
src/lib/utils.ts
src/types/index.ts
tsconfig.json
```

### Dependencies

- package.json: @gsap/react@^2.1.2, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, eslint@^9, eslint-config-next@16.2.9, framer-motion@^12.40.0, gsap@^3.15.0, lenis@^1.3.23, next@16.2.9, react@19.2.4, react-dom@19.2.4, server-only@^0.0.1, tailwindcss@^4, typescript@^5

### Recent commits (newest first)

- first commit

## Key source files (fetched from GitHub, selected and truncated for size)

### AGENTS.md

```markdown
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->

```

### CLAUDE.md

```markdown
# LifeTap — agent notes

Launch marketing site. Read `README.md` and `docs/` before making changes.

## Conventions

- **Font**: Poppins only — no extra display/mono fonts
- **Copy**: Short. Edit `src/constants/` not scattered strings
- **Images**: `public/images/` — see `public/images/README.md`
- **Colors**: CSS vars in `globals.css` — warm cream, amber accent, Ditto highlights
- **Scope**: Minimal diffs. Don't re-add removed sections without asking

## Key paths

| What | Where |
|------|-------|
| Page order | `src/app/page.tsx` |
| Site copy | `src/constants/copy.ts` |
| Image paths | `src/constants/images.ts` |
| Waitlist API | `src/app/api/waitlist/route.ts` |

## Stack

Next.js App Router · Tailwind 4 · Framer Motion · GSAP · Lenis

@AGENTS.md

```

### package.json

```
{
  "name": "lifetap_website",
  "version": "0.1.0",
  "private": true,
  "engines": {
    "node": ">=20"
  },
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@gsap/react": "^2.1.2",
    "framer-motion": "^12.40.0",
    "gsap": "^3.15.0",
    "lenis": "^1.3.23",
    "next": "16.2.9",
    "react": "19.2.4",
    "react-dom": "19.2.4",
    "server-only": "^0.0.1"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.2.9",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### src/constants/index.ts

```typescript
export * from "./copy";
export * from "./docs";
export * from "./images";
export * from "./stats";

```

### src/types/index.ts

```typescript
export interface Frame {
  time: string;
  head: string;
  body: string;
  signal?: boolean;
  badge?: string;
  bad?: boolean;
}

export interface WaitlistPayload {
  email: string;
  source?: string;
}

```

### src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Poppins } from "next/font/google";
import "./globals.css";

const poppins = Poppins({
  variable: "--font-poppins",
  subsets: ["latin"],
  weight: ["400", "500", "600", "700", "800"],
});

export const metadata: Metadata = {
  title: "LifeTap — One Press. Help On The Way.",
  description:
    "A hardware panic button that sends GPS coordinates via SMS the instant it's pressed.",
};

export const viewport = {
  width: "device-width",
  initialScale: 1,
  viewportFit: "cover",
  themeColor: "#f9f8f3",
};

export default function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en" className={`${poppins.variable} h-full antialiased`}>
      <body className="min-h-full bg-cream font-sans text-charcoal">{children}</body>
    </html>
  );
}

```

### src/app/page.tsx

```typescript
"use client";

import { useState, useCallback } from "react";
import Loader from "@/components/ui/Loader";
import SmoothScrollProvider from "@/components/layout/SmoothScrollProvider";
import TopBanner from "@/components/layout/TopBanner";
import Navigation from "@/components/layout/Navigation";
import Footer from "@/components/layout/Footer";
import Hero from "@/components/sections/Hero";
import TickerMarquee from "@/components/ui/TickerMarquee";
import ProblemSection from "@/components/sections/ProblemSection";
import TwoFutures from "@/components/sections/TwoFutures";
import TwoFuturesMobile from "@/components/sections/TwoFuturesMobile";
import MobileBottomBar from "@/components/layout/MobileBottomBar";
import AlertBeacon from "@/components/ui/AlertBeacon";
import HowItWorks from "@/components/sections/HowItWorks";
import PrototypeSection from "@/components/sections/PrototypeSection";
import SMSMockup from "@/components/sections/SMSMockup";
import ComparisonTable from "@/components/sections/ComparisonTable";
import WaitlistCTA from "@/components/sections/WaitlistCTA";

export default function HomePage() {
  const [loading, setLoading] = useState(true);
  const [visible, setVisible] = useState(false);

  const onLoaderDone = useCallback(() => {
    setLoading(false);
    setTimeout(() => setVisible(true), 40);
  }, []);

  return (
    <SmoothScrollProvider enabled={!loading}>
      {loading && <Loader onComplete={onLoaderDone} />}
      <div className="mobile-page pb-safe transition-opacity duration-500" style={{ opacity: visible ? 1 : 0 }}>
        <TopBanner visible={visible} />
        <Navigation visible={visible} />
        <AlertBeacon visible={visible} />
        <main>
          <Hero animate={visible} />
          <TickerMarquee />
          <ProblemSection />
          <div id="two-futures" className="scroll-anchor" aria-hidden />
          <TwoFuturesMobile />
          <TwoFutures />
          <HowItWorks />
          <PrototypeSection />
          <SMSMockup />
          <ComparisonTable />
          <WaitlistCTA />
        </main>
        <Footer />
        <MobileBottomBar />
      </div>
    </SmoothScrollProvider>
  );
}

```

### src/app/docs/page.tsx

```typescript
import Link from "next/link";
import Image from "next/image";
import { DocsShell } from "@/components/docs/DocsShell";
import { DOC_PAGES } from "@/constants/docs";

export default function DocsIndexPage() {
  return (
    <DocsShell fullWidth>
      <div className="grid gap-4 sm:grid-cols-2">
        {DOC_PAGES.map((doc) => (
          <Link
            key={doc.slug}
            href={`/docs/${doc.slug}`}
            className="group overflow-hidden rounded-2xl border border-charcoal/[0.06] bg-cream/50 transition-all hover:-translate-y-0.5 hover:shadow-[0_12px_40px_rgba(10,10,10,0.08)]"
          >
            <div className="relative h-36 overflow-hidden" style={{ background: doc.accent }}>
              <Image
                src={doc.image}
                alt={doc.title}
                fill
                className="object-cover opacity-90 transition-transform duration-500 group-hover:scale-[1.03]"
                sizes="(max-width:768px) 100vw, 400px"
              />
              <div className="absolute inset-0 bg-gradient-to-t from-charcoal/40 to-transparent" />
              <span
                className="absolute bottom-3 left-3 rounded-full px-2.5 py-1 text-[10px] font-extrabold uppercase tracking-[0.1em] text-white"
                style={{ background: doc.accentDark }}
              >
                {doc.category}
              </span>
            </div>
            <div className="p-5">
              <h2 className="text-lg font-extrabold text-charcoal group-hover:underline">{doc.title}</h2>
              <p className="mt-1.5 text-sm leading-relaxed text-charcoal/55">{doc.summary}</p>
              <div className="mt-3 flex flex-wrap gap-1.5">
                {doc.highlights.slice(0, 3).map((h) => (
                  <span key={h} className="rounded-full bg-white px-2 py-0.5 text-[10px] font-bold text-charcoal/50">
                    {h}
                  </span>
                ))}
              </div>
              <p className="mt-3 text-xs font-semibold text-charcoal/35">{doc.readTime} →</p>
            </div>
          </Link>
        ))}
      </div>
    </DocsShell>
  );
}

```

### src/app/api/waitlist/route.ts

```typescript
import { NextResponse } from "next/server";
import { isValidEmail } from "@/lib/utils";
import type { WaitlistPayload } from "@/types";

export async function POST(request: Request) {
  try {
    const body = (await request.json()) as WaitlistPayload;
    const email = body.email?.trim().toLowerCase();

    if (!email || !isValidEmail(email)) {
      return NextResponse.json({ error: "Valid email required" }, { status: 400 });
    }

    // TODO: connect Resend, Supabase, or webhook
    // const webhook = process.env.WAITLIST_WEBHOOK_URL;
    // if (webhook) await fetch(webhook, { method: "POST", body: JSON.stringify({ email }) });

    console.info("[waitlist]", email);

    return NextResponse.json({ ok: true });
  } catch {
    return NextResponse.json({ error: "Something went wrong" }, { status: 500 });
  }
}

```

### src/app/docs/[slug]/page.tsx

```typescript
import Link from "next/link";
import { notFound } from "next/navigation";
import { MarkdownArticle } from "@/components/docs/MarkdownArticle";
import { DocsShell } from "@/components/docs/DocsShell";
import { getDocContent, getDocSlugs } from "@/lib/docs";

export function generateStaticParams() {
  return getDocSlugs().map((slug) => ({ slug }));
}

export default async function DocPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const doc = getDocContent(slug as Parameters<typeof getDocContent>[0]);
  if (!doc) notFound();

  return (
    <DocsShell slug={slug}>
      <MarkdownArticle content={doc.content} />
      <div className="mt-12 flex flex-wrap gap-3 border-t border-charcoal/[0.06] pt-8">
        <Link
          href="/"
          className="rounded-full bg-charcoal px-5 py-2.5 text-sm font-bold text-cream hover:bg-charcoal/90"
        >
          ← Back to site
        </Link>
        <Link
          href="/docs"
          className="rounded-full border border-charcoal/10 px-5 py-2.5 text-sm font-bold text-charcoal/60 hover:text-charcoal"
        >
          All docs
        </Link>
      </div>
    </DocsShell>
  );
}

```

[63 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]