Skip to content

Activity Registration Form

A modern Next.js 15 registration system for KYZN activity tokens, featuring branch-aware member search, real-time schedule updates, and smart token selection with duplicate prevention. Built with shadcn/ui and powered by Google Apps Script serverless backend.

The KYZN Activity Registration Form is a serverless registration system that combines a modern React frontend with Google Apps Script backend. Users can search for members, view available activities, and register for up to 5 activity tokens with intelligent validation.

Branch-Aware Search

Search members by phone number with branch filtering (BSD/Kuningan) and automatic retry functionality

Smart Token Selection

Select up to 5 activity tokens with automatic duplicate prevention and category limits (max 2 per category)

Real-Time Updates

Live schedule updates with manual refresh option and automatic availability tracking

Serverless Architecture

Google Apps Script backend with zero hosting costs and automatic scaling

Progress Tracking

Real-time progress banner showing token usage and category distribution

Mobile Responsive

Mobile-first design with responsive layout and touch-optimized controls


TechnologyVersionPurpose
Next.js15.5.3React framework with App Router and static export
React19.1.0UI library for component architecture
TypeScript5.xType-safe development
Tailwind CSS4.xUtility-first CSS framework
shadcn/uiLatestRadix UI + Tailwind component library
Lucide React0.544.0Icon library
Sonner2.0.7Toast notifications
TechnologyPurpose
Google Apps ScriptServerless API with LockService for concurrency
Google SheetsDatabase with three-sheet architecture
  • Next.js 15: App Router for optimal performance, static export for GitHub Pages deployment
  • shadcn/ui: Accessible components built on Radix UI primitives with full customization
  • Google Apps Script: No server costs, direct Sheets integration, automatic scaling
  • TypeScript: Type safety prevents runtime errors and improves developer experience

  • Directoryterm-form/
    • Directorysrc/
      • Directoryapp/
        • globals.css # Global Tailwind styles
        • layout.tsx # Root layout with Toaster provider
        • page.tsx # Main registration page (client component)
      • Directorycomponents/
        • Directoryui/ # shadcn/ui base components
          • alert.tsx
          • badge.tsx
          • button.tsx
          • card.tsx
          • command.tsx
          • dialog.tsx
          • input.tsx
          • progress.tsx
          • select.tsx
          • separator.tsx
          • skeleton.tsx
          • sonner.tsx
          • table.tsx
          • tooltip.tsx
        • IdentityCard.tsx # Member information display
        • NameSearchCard.tsx # Phone search with branch selection
        • ProgressBanner.tsx # Token usage progress tracker
        • SubmitCard.tsx # Registration submission
        • TokenSelectionCard.tsx # Individual token selector
        • WhatsAppButton.tsx # WhatsApp contact button
        • index.ts # Component barrel exports
      • Directoryhooks/
        • useMemberSearch.ts # Member search with debouncing
        • useSchedules.ts # Schedule fetching with real-time updates
        • useSelections.ts # Token selection state management
        • index.ts # Hook barrel exports
      • Directorylib/
        • utils.ts # Utility functions (cn helper, etc.)
    • Directorygas-backend/
      • Code.gs # Google Apps Script backend
      • DEPLOYMENT.md # Backend deployment guide
      • TESTING.md # Testing guide with curl examples
      • TROUBLESHOOTING.md # Common issues and solutions
    • Directorydocs/
      • api-contract.md # Complete API specifications
      • integration-testing.md # Integration testing guide
      • qa-checklist.md # Quality assurance checklist
    • Directorypublic/ # Static assets (if any)
    • .env.local # Environment variables (create this)
    • components.json # shadcn/ui configuration
    • next.config.js # Next.js static export config
    • package.json # Dependencies and scripts
    • tsconfig.json # TypeScript configuration
    • README.md # Project documentation
page.tsx (Main Registration Flow)
├── PhoneSearchCard
│ └── useMemberSearch hook (debounced search)
├── IdentityCard
│ └── Display member information
├── ProgressBanner
│ └── useSelections hook (progress tracking)
├── TokenSelectionCard (×5)
│ ├── useSchedules hook (fetch schedules)
│ └── useSelections hook (validate selections)
├── SubmitCard
│ └── Submit registration data
└── WhatsAppButton (floating button)

  • Node.js 18+ or Bun runtime
  • Git for version control
  • Google account (for backend deployment)
  1. Clone the repository

    Terminal window
    git clone <repository-url>
    cd term-form
  2. Install dependencies

    Terminal window
    npm install
  3. Configure environment Create .env.local in the project root:

    Terminal window
    NEXT_PUBLIC_APPS_SCRIPT_URL=https://script.googleusercontent.com/macros/echo?user_content_key=YOUR_KEY&lib=YOUR_LIB
  4. Start development server

    Terminal window
    npm run dev

    Open http://localhost:3000


Create .env.local in the project root:

.env.local
# Google Apps Script Web App URL
NEXT_PUBLIC_APPS_SCRIPT_URL=https://script.googleusercontent.com/macros/echo?user_content_key=YOUR_CONTENT_KEY&lib=YOUR_LIB_ID

The next.config.js is configured for static export:

const nextConfig = {
output: 'export', // Static HTML export
trailingSlash: true, // Add trailing slashes
images: {
unoptimized: true // Disable image optimization for static export
}
}

For GitHub Pages subdirectory deployment, uncomment and configure:

basePath: '/your-repo-name',
assetPrefix: '/your-repo-name/',

The backend uses a three-sheet architecture for optimal performance:

Purpose: Master members table for fast search operations

ColumnTypeDescription
member_idStringUnique identifier (e.g., “001-2024-15684”)
branchStringBranch location (“BSD” or “Kuningan”)
nameStringMember full name
birthdateDateBirth date (YYYY-MM-DD)
parent_nameStringParent/guardian name
contactStringPhone number (normalized)
registration_statusStringCurrent status (Active/Inactive)
  1. Create Google Sheets Set up three sheets (list_member, form, schedule) with exact column names

  2. Open Apps Script Editor Extensions → Apps Script in your Google Sheet

  3. Copy Backend Code Copy code from gas-backend/Code.gs to the editor

  4. Configure Sheet IDs

    const CONFIG = {
    bsd: {
    FORM_SHEET_ID: 'YOUR_BSD_SHEET_ID_HERE',
    SCHEDULE_SHEET_ID: 'YOUR_BSD_SHEET_ID_HERE',
    LIST_MEMBER_SHEET_ID: 'YOUR_BSD_SHEET_ID_HERE',
    },
    kuningan: {
    FORM_SHEET_ID: 'YOUR_KUNINGAN_SHEET_ID_HERE',
    SCHEDULE_SHEET_ID: 'YOUR_KUNINGAN_SHEET_ID_HERE',
    LIST_MEMBER_SHEET_ID: 'YOUR_KUNINGAN_SHEET_ID_HERE',
    }
    };
  5. Deploy as Web App

    • Click Deploy → New deployment
    • Type: Web app
    • Execute as: Me
    • Who has access: Anyone
    • Click Deploy and authorize
  6. Copy Web App URL Add the URL to your .env.local file


The Google Apps Script backend provides these endpoints:

GET /exec
Response: {"ok": true, "msg": "API ready"}
GET /exec?fn=search&branch={branch}&phone={phone}
Response: {"ok": true, "results": [...]}
GET /exec?fn=schedules&branch={branch}
Response: {"ok": true, "items": [...]}
POST /exec
Body: {"member": {...}, "selections": [...]}
Response: {"ok": true}

How It Works:

  • User enters phone number and selects branch (BSD/Kuningan)
  • Automatic debounced search after 500ms of typing
  • Manual “Search” button for immediate retry
  • Results filtered by branch for data isolation

Technical Implementation:

  • useMemberSearch hook with debouncing using useRef
  • AbortController for request cancellation
  • Phone number normalization (removes non-digits)
  • Minimum 9 digits validation

Business Rules:

  • Maximum 5 tokens total per registration
  • Maximum 2 tokens per category
  • No duplicate sessions (same activity_id)
  • Only sessions with available_slot > 0 can be selected

Technical Implementation:

  • useSelections hook manages sparse array (5 slots)
  • setSelectionAtPosition for token-specific updates
  • Real-time validation with category count tracking
  • Duplicate detection using activity_id comparison

Features:

  • Toggle between Live (auto-refresh every 30s) and Manual mode
  • Manual refresh button for on-demand updates
  • Last updated timestamp display
  • Live indicator with animated pulse

Technical Implementation:

  • useSchedules hook with setInterval for polling
  • Branch-aware schedule caching
  • Automatic cleanup on unmount
  • AbortController for request management

Display:

  • Total tokens used (e.g., “3/5 tokens”)
  • Category breakdown with badges
  • Max reached indicators for categories at limit
  • Visual progress bar

Technical Implementation:

  • getCategoryCounts returns category statistics
  • Dynamic badge colors (green for available, gray for maxed)
  • Real-time updates on selection changes

// From page.tsx - Enhanced validation
const isValidSelections = () => {
const validSelections = selections.filter(s => s !== null)
// Check minimum and maximum
if (validSelections.length === 0) return false
if (validSelections.length > 5) return false
// Validate required fields
for (const selection of validSelections) {
if (!selection.class_category ||
!selection.activity_id ||
!selection.activity_name) {
return false
}
}
// Check category limits
const categoryCount = new Map()
for (const selection of validSelections) {
const count = (categoryCount.get(selection.class_category) || 0) + 1
if (count > 2) return false
categoryCount.set(selection.class_category, count)
}
return true
}

The Google Apps Script backend performs:

  • Member data validation (required fields)
  • Selections array validation (min 1, max 5)
  • Category limit validation (max 2 per category)
  • Activity availability validation
  • Race condition prevention using LockService
Error TypeUser MessageAction
Network Error”Failed to connect to server”Retry button appears
Invalid PhoneNo results returnedTry different number
No Available Slots”Session full”Select different activity
Category MaxedCategory grayed outChoose different category
Duplicate SessionSelection rejected silentlyChoose different time

  1. Verify environment Ensure .env.local has correct NEXT_PUBLIC_APPS_SCRIPT_URL

  2. Build static export

    Terminal window
    npm run build

    This runs next build and creates out/ directory

  3. Test locally

    Terminal window
    npx serve out

    Visit http://localhost:3000 to test

Create .github/workflows/deploy.yml:

name: Deploy to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
NEXT_PUBLIC_APPS_SCRIPT_URL: ${{ secrets.NEXT_PUBLIC_APPS_SCRIPT_URL }}
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./out
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

Setup:

  1. Add NEXT_PUBLIC_APPS_SCRIPT_URL to repository secrets
  2. Enable GitHub Pages in repository settings
  3. Push to main branch to trigger deployment

  1. Backend Health Check

    Terminal window
    curl "YOUR_WEB_APP_URL"

    Expected: {"ok":true,"msg":"API ready"}

  2. Member Search

    • Enter phone number
    • Select branch
    • Verify results display
    • Test manual retry button
  3. Token Selection

    • Select activities across different categories
    • Verify category limits (max 2 per category)
    • Try selecting duplicate session (should be prevented)
    • Test clearing selections
  4. Submission

    • Fill all 5 tokens
    • Submit registration
    • Verify success message
    • Check Google Sheet for new records

See gas-backend/TESTING.md for curl commands to test each endpoint:

Terminal window
# Search members
curl "YOUR_URL?fn=search&branch=bsd&phone=081234567"
# Load schedules
curl "YOUR_URL?fn=schedules&branch=bsd"
# Submit registration
curl -X POST "YOUR_URL" \
-H "Content-Type: application/json" \
-d '{"member":{...},"selections":[...]}'

Problem: net::ERR_ABORTED errors

Solutions:

  1. Verify backend URL is correct in .env.local
  2. Test backend directly: curl YOUR_URL
  3. Check Google Apps Script deployment:
    • Deployed as Web App
    • Access set to “Anyone”
    • Using latest version
  4. Restart dev server after updating .env.local

BrowserMinimum VersionNotes
ChromeLatestFull support
FirefoxLatestFull support
Safari14+Full support
EdgeLatestFull support
Mobile Safari (iOS)14+Touch optimized
Chrome Mobile (Android)10+Touch optimized

Requirements:

  • JavaScript enabled
  • Cookies enabled (for session storage)
  • Modern ES2017+ support

  • Code Splitting: Automatic with Next.js App Router
  • Static Export: Pre-rendered HTML for fast initial load
  • Debounced Search: Reduces unnecessary API calls (500ms delay)
  • Request Cancellation: AbortController prevents race conditions
  • Component Memoization: Prevents unnecessary re-renders
  • LockService: Prevents race conditions on concurrent submissions
  • Batch Operations: Processes multiple tokens efficiently
  • Schedule Caching: Frontend caches for 60 seconds
  • Branch Filtering: Reduces data transfer size
  • First Load: < 2 seconds (static HTML)
  • Search: < 500ms average
  • Schedule Load: < 1 second average
  • Submission: < 2 seconds average

  • Environment Variables: API URL stored in .env.local (not committed)
  • Input Sanitization: Phone numbers normalized, trimmed
  • Client-Side Validation: Prevents invalid data submission
  • HTTPS Only: All API calls use secure connections
  • Public Access: Web App set to “Anyone” for public registration
  • Rate Limiting: 100 requests/minute per user (configurable)
  • LockService: Prevents concurrent modification issues
  • Data Validation: Comprehensive server-side validation
  • Audit Trail: Timestamps on all registrations

API Documentation

Complete API contract with request/response examples
docs/api-contract.md

Backend Deployment

Step-by-step Google Apps Script setup guide
gas-backend/DEPLOYMENT.md

Testing Guide

Manual and automated testing procedures
gas-backend/TESTING.md

Troubleshooting

Common issues and solutions
gas-backend/TROUBLESHOOTING.md

Integration Testing

End-to-end testing scenarios
docs/integration-testing.md

QA Checklist

Quality assurance checklist for production
docs/qa-checklist.md


This project is for internal KYZN use. Contact the development team for licensing inquiries.


Documentation last updated: January 2025
Project version: 0.1.0