Branch-Aware Search
Search members by phone number with branch filtering (BSD/Kuningan) and automatic retry functionality
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
| Technology | Version | Purpose |
|---|---|---|
| Next.js | 15.5.3 | React framework with App Router and static export |
| React | 19.1.0 | UI library for component architecture |
| TypeScript | 5.x | Type-safe development |
| Tailwind CSS | 4.x | Utility-first CSS framework |
| shadcn/ui | Latest | Radix UI + Tailwind component library |
| Lucide React | 0.544.0 | Icon library |
| Sonner | 2.0.7 | Toast notifications |
| Technology | Purpose |
|---|---|
| Google Apps Script | Serverless API with LockService for concurrency |
| Google Sheets | Database with three-sheet architecture |
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)Clone the repository
git clone <repository-url>cd term-formInstall dependencies
npm installbun installConfigure environment
Create .env.local in the project root:
NEXT_PUBLIC_APPS_SCRIPT_URL=https://script.googleusercontent.com/macros/echo?user_content_key=YOUR_KEY&lib=YOUR_LIBStart development server
npm run devOpen http://localhost:3000
Create .env.local in the project root:
# Google Apps Script Web App URLNEXT_PUBLIC_APPS_SCRIPT_URL=https://script.googleusercontent.com/macros/echo?user_content_key=YOUR_CONTENT_KEY&lib=YOUR_LIB_IDThe 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
| Column | Type | Description |
|---|---|---|
member_id | String | Unique identifier (e.g., “001-2024-15684”) |
branch | String | Branch location (“BSD” or “Kuningan”) |
name | String | Member full name |
birthdate | Date | Birth date (YYYY-MM-DD) |
parent_name | String | Parent/guardian name |
contact | String | Phone number (normalized) |
registration_status | String | Current status (Active/Inactive) |
Purpose: Registration records (one row per token)
| Column | Type | Description |
|---|---|---|
member_id | String | Reference to list_member |
branch | String | Branch location |
name | String | Member full name |
birthdate | Date | Birth date |
activity_name | String | Selected activity |
parent_name | String | Parent name |
contact | String | Phone number |
token | Number | Token number (1-5) |
timestamp | DateTime | Registration timestamp |
Purpose: Activity schedules and availability tracking
| Column | Type | Description |
|---|---|---|
activity_id | String | Unique activity identifier |
branch | String | Branch location |
class_category | String | Category (Tennis, Basketball, etc.) |
activity_name | String | Full activity description |
total_slot | Number | Maximum capacity |
booked_slot | Number | Currently booked |
available_slot | Number | Remaining slots |
Create Google Sheets
Set up three sheets (list_member, form, schedule) with exact column names
Open Apps Script Editor Extensions → Apps Script in your Google Sheet
Copy Backend Code
Copy code from gas-backend/Code.gs to the editor
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', }};Deploy as Web App
Copy Web App URL
Add the URL to your .env.local file
The Google Apps Script backend provides these endpoints:
GET /execResponse: {"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 /execBody: {"member": {...}, "selections": [...]}Response: {"ok": true}How It Works:
Technical Implementation:
useMemberSearch hook with debouncing using useRefBusiness Rules:
activity_id)available_slot > 0 can be selectedTechnical Implementation:
useSelections hook manages sparse array (5 slots)setSelectionAtPosition for token-specific updatesactivity_id comparisonFeatures:
Technical Implementation:
useSchedules hook with setInterval for pollingDisplay:
Technical Implementation:
getCategoryCounts returns category statistics// From page.tsx - Enhanced validationconst 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:
| Error Type | User Message | Action |
|---|---|---|
| Network Error | ”Failed to connect to server” | Retry button appears |
| Invalid Phone | No results returned | Try different number |
| No Available Slots | ”Session full” | Select different activity |
| Category Maxed | Category grayed out | Choose different category |
| Duplicate Session | Selection rejected silently | Choose different time |
Verify environment
Ensure .env.local has correct NEXT_PUBLIC_APPS_SCRIPT_URL
Build static export
npm run buildThis runs next build and creates out/ directory
Test locally
npx serve outVisit 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@v4Setup:
NEXT_PUBLIC_APPS_SCRIPT_URL to repository secrets# Install gh-pagesnpm install -D gh-pages
# Add to package.json scripts"deploy": "gh-pages -d out"
# Build and deploynpm run buildnpm run deployBackend Health Check
curl "YOUR_WEB_APP_URL"Expected: {"ok":true,"msg":"API ready"}
Member Search
Token Selection
Submission
See gas-backend/TESTING.md for curl commands to test each endpoint:
# Search memberscurl "YOUR_URL?fn=search&branch=bsd&phone=081234567"
# Load schedulescurl "YOUR_URL?fn=schedules&branch=bsd"
# Submit registrationcurl -X POST "YOUR_URL" \ -H "Content-Type: application/json" \ -d '{"member":{...},"selections":[...]}'Problem: net::ERR_ABORTED errors
Solutions:
.env.localcurl YOUR_URL.env.localProblem: No search results
Solutions:
list_member sheet has datacurl "YOUR_URL?fn=search&branch=bsd&phone=081234567"Problem: Cannot select certain activities
Solutions:
available_slot > 0Problem: Registration not submitting
Solutions:
form sheet has write permissionscurl -X POST "YOUR_URL" \ -H "Content-Type: application/json" \ -d '{"member":{...},"selections":[...]}'| Browser | Minimum Version | Notes |
|---|---|---|
| Chrome | Latest | Full support |
| Firefox | Latest | Full support |
| Safari | 14+ | Full support |
| Edge | Latest | Full support |
| Mobile Safari (iOS) | 14+ | Touch optimized |
| Chrome Mobile (Android) | 10+ | Touch optimized |
Requirements:
.env.local (not committed)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