ganeshotsav-2025-forms
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Viewing readonly version of main branch: v235View latest version
The quiz system integrates with the existing Ganeshotsav 2025 platform, extending the current Val Town-based architecture with new components for team-based registrations.
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ │ │ │ │ │
│ Frontend │ │ Backend │ │ External │
│ Components │◄──►│ Services │◄──►│ Services │
│ │ │ │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │ │
├─ Quiz Form ├─ Quiz API Routes ├─ OpenAI API
├─ Team Management ├─ Database Layer ├─ Val Town Runtime
├─ Admin Interface ├─ Validation └─ SQLite
└─ Language Support └─ Authentication
- Serverless Runtime: No persistent processes or local state
- Cold Start Latency: First request may have higher latency
- Memory Limits: Function execution memory constraints
- File System: Read-only file system, no local file storage
- Database: SQLite only, no external database connections
- Request Timeout: Maximum execution time limits
- TypeScript/TSX only for Val Town compatibility
- ESM import syntax required (
https://esm.sh/
for npm packages) - No Node.js specific APIs (use Deno APIs instead)
- Browser compatibility for shared code modules
-- Quiz Teams Table CREATE TABLE IF NOT EXISTS quiz_teams ( team_name TEXT PRIMARY KEY, parent_name TEXT NOT NULL, contact_number TEXT NOT NULL CHECK(LENGTH(contact_number) = 10), members TEXT NOT NULL, -- JSON array: [{name: string, age: number}] member_count INTEGER NOT NULL CHECK(member_count >= 1 AND member_count <= 4), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(contact_number) -- Prevent duplicate registrations by same contact ); -- Index for admin queries CREATE INDEX IF NOT EXISTS idx_quiz_teams_created ON quiz_teams(created_at); CREATE INDEX IF NOT EXISTS idx_quiz_teams_member_count ON quiz_teams(member_count);
interface QuizTeamMember {
name: string; // 2-50 characters, alphabetic with spaces
age: number; // 6-18 years for quiz eligibility
}
interface QuizTeamData {
team_name: string; // 3-30 characters, unique
parent_name: string; // 2-100 characters
contact_number: string; // 10-digit Indian mobile number
members: QuizTeamMember[]; // 1-4 members
member_count: number; // Computed field for queries
}
interface QuizRegistrationResponse {
success: boolean;
error?: string;
message?: string;
team_name?: string;
}
// Migration function to be run during initialization
export async function initializeQuizDatabase(): Promise<void> {
await sqlite.execute(`
CREATE TABLE IF NOT EXISTS quiz_teams (
team_name TEXT PRIMARY KEY,
parent_name TEXT NOT NULL,
contact_number TEXT NOT NULL CHECK(LENGTH(contact_number) = 10),
members TEXT NOT NULL,
member_count INTEGER NOT NULL CHECK(member_count >= 1 AND member_count <= 4),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(contact_number)
)
`);
// Create indexes for performance
await sqlite.execute(`
CREATE INDEX IF NOT EXISTS idx_quiz_teams_created ON quiz_teams(created_at)
`);
await sqlite.execute(`
CREATE INDEX IF NOT EXISTS idx_quiz_teams_member_count ON quiz_teams(member_count)
`);
}
POST /quiz-registration
Content-Type: application/json
Request Body:
{
"team_name": "भारत के वीर",
"parent_name": "राजेश शर्मा",
"contact_number": "9876543210",
"members": [
{"name": "आर्यन", "age": 12},
{"name": "प्रिया", "age": 10}
]
}
Response (200 OK):
{
"success": true,
"message": "Team registered successfully",
"team_name": "भारत के वीर"
}
Response (400 Bad Request):
{
"success": false,
"error": "Team name already exists"
}
GET /admin/quiz?key={ADMIN_KEY}
Response: HTML page with quiz registrations table
GET /admin/quiz/data?key={ADMIN_KEY}
Response (200 OK):
{
"success": true,
"teams": [...],
"summary": {
"total_teams": 45,
"total_members": 156,
"avg_team_size": 3.47
}
}
POST /ai/suggest-team-names
Content-Type: application/json
Request Body:
{
"language": "hindi",
"context": "cultural quiz competition"
}
Response (200 OK):
{
"success": true,
"suggestions": [
"भारत गौरव",
"ज्ञान सागर",
"संस्कृति रत्न",
"विद्या वीर",
"राष्ट्र गर्व"
]
}
DELETE /admin/quiz/{team_name}?key={ADMIN_KEY}
Response (200 OK):
{
"success": true,
"message": "Team deleted successfully"
}
export function validateQuizRegistration(data: any): {
valid: boolean;
errors: string[];
} {
const errors: string[] = [];
// Team name validation
if (!data.team_name || data.team_name.length < 3 || data.team_name.length > 30) {
errors.push("Team name must be 3-30 characters");
}
// Parent name validation
if (!data.parent_name || data.parent_name.length < 2 || data.parent_name.length > 100) {
errors.push("Parent name must be 2-100 characters");
}
// Contact number validation
if (!data.contact_number || !/^[6-9]\d{9}$/.test(data.contact_number)) {
errors.push("Contact number must be a valid 10-digit Indian mobile number");
}
// Members validation
if (!Array.isArray(data.members) || data.members.length === 0 || data.members.length > 4) {
errors.push("Team must have 1-4 members");
}
data.members?.forEach((member: any, index: number) => {
if (!member.name || member.name.length < 2 || member.name.length > 50) {
errors.push(`Member ${index + 1}: Name must be 2-50 characters`);
}
if (!member.age || member.age < 6 || member.age > 18) {
errors.push(`Member ${index + 1}: Age must be between 6-18 years`);
}
});
return { valid: errors.length === 0, errors };
}
- Extend existing
database.ts
with quiz-specific functions - Reuse SQLite connection and error handling patterns
- Maintain transaction consistency with existing tables
- Extend
shared/translations.ts
with quiz-specific keys - Reuse existing language switching functionality
- Maintain consistency with existing translation patterns
- Extend existing
admin-template.ts
with quiz tab - Reuse admin authentication mechanism
- Maintain consistent UI patterns and styling
- Extend existing form validation patterns
- Reuse error handling and response formatting
- Maintain consistent user feedback mechanisms
// Addition to existing TranslationKeys interface
interface QuizTranslationKeys extends TranslationKeys {
// Quiz-specific keys
quizRegistration: string;
bharatEkKhoj: string;
teamName: string;
teamMembers: string;
memberName: string;
memberAge: string;
addMember: string;
removeMember: string;
suggestTeamName: string;
teamNameTaken: string;
minTeamSize: string;
maxTeamSize: string;
ageRange: string;
generateSuggestions: string;
usingSuggestion: string;
quizTab: string;
totalTeams: string;
averageTeamSize: string;
teamComposition: string;
}
import { OpenAI } from "https://esm.town/v/std/openai";
export async function generateTeamNameSuggestions(
language: 'english' | 'hindi' | 'gujarati',
userContext?: string
): Promise<string[]> {
const openai = new OpenAI();
const prompts = {
english: `Generate 5 culturally appropriate team names for an Indian cultural knowledge quiz competition called "Bharat Ek Khoj" (Discovery of India). Names should be inspiring, easy to remember, and reflect Indian heritage. Format as a simple list.`,
hindi: `"भारत एक खोज" सांस्कृतिक प्रतियोगिता के लिए 5 उपयुक्त टीम नाम सुझाएं। नाम प्रेरणादायक, याद रखने योग्य और भारतीय संस्कृति को दर्शाने वाले होने चाहिए। सूची के रूप में दें।`,
gujarati: `"ભારત એક ખોજ" સાંસ્કૃતિક સ્પર્ધા માટે 5 યોગ્ય ટીમના નામ સૂચવો. નામો પ્રેરણાદાયક, યાદ રાખવા યોગ્ય અને ભારતીય સંસ્કૃતિને દર્શાવતા હોવા જોઈએ. યાદીના સ્વરૂપમાં આપો.`
};
try {
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{
role: "user",
content: prompts[language] + (userContext ? `\n\nAdditional context: ${userContext}` : '')
}
],
max_tokens: 150,
temperature: 0.8
});
const response = completion.choices[0]?.message?.content || "";
// Parse response into array of suggestions
return response
.split('\n')
.filter(line => line.trim())
.map(line => line.replace(/^\d+\.\s*/, '').trim())
.filter(name => name.length > 0)
.slice(0, 5);
} catch (error) {
console.error("OpenAI API error:", error);
return getFallbackSuggestions(language);
}
}
function getFallbackSuggestions(language: string): string[] {
const fallbacks = {
english: ["Bharat Ratna", "Cultural Warriors", "Heritage Heroes", "Knowledge Seekers", "Tradition Keepers"],
hindi: ["भारत रत्न", "संस्कृति वीर", "ज्ञान खोजी", "विरासत रक्षक", "राष्ट्र गौरव"],
gujarati: ["ભારત રત્ન", "સંસ્કૃતિ વીર", "જ્ઞાન શોધક", "વિરાસત રક્ષક", "રાષ્ટ્ર ગૌરવ"]
};
return fallbacks[language] || fallbacks.english;
}
// Simple in-memory rate limiting (per session)
const rateLimiter = new Map<string, { count: number; resetTime: number }>();
export function checkRateLimit(ip: string, maxRequests: number = 5, windowMs: number = 300000): boolean {
const now = Date.now();
const userLimit = rateLimiter.get(ip);
if (!userLimit || now > userLimit.resetTime) {
rateLimiter.set(ip, { count: 1, resetTime: now + windowMs });
return true;
}
if (userLimit.count >= maxRequests) {
return false;
}
userLimit.count++;
return true;
}
- Quiz Registration Form Load: < 2 seconds
- Form Submission Processing: < 1.5 seconds
- AI Team Name Generation: < 5 seconds
- Admin Interface Load: < 3 seconds with 100+ teams
// Efficient query patterns
export async function getQuizTeamsSummary(): Promise<{
totalTeams: number;
totalMembers: number;
averageTeamSize: number;
sizeDistribution: Record<number, number>;
}> {
// Single query to get all stats
const result = await sqlite.execute(`
SELECT
COUNT(*) as total_teams,
SUM(member_count) as total_members,
AVG(member_count) as avg_team_size,
member_count,
COUNT(member_count) as count_by_size
FROM quiz_teams
GROUP BY member_count
`);
// Process results efficiently
return processStatsResult(result);
}
- Lazy load AI suggestions only when requested
- Debounce team name validation checks
- Progressive form validation (validate on blur, not on input)
- Minimize re-renders with efficient state management
// Simple in-memory caching for admin data
const adminCache = new Map<string, { data: any; expiry: number }>();
export function getCachedAdminData(key: string, ttlMs: number = 60000) {
const cached = adminCache.get(key);
if (cached && Date.now() < cached.expiry) {
return cached.data;
}
adminCache.delete(key);
return null;
}
export function setCachedAdminData(key: string, data: any, ttlMs: number = 60000) {
adminCache.set(key, {
data,
expiry: Date.now() + ttlMs
});
}
export function sanitizeInput(input: string): string {
return input
.trim()
.replace(/[<>]/g, '') // Remove potential HTML
.replace(/['"]/g, '') // Remove quotes that could break SQL
.substring(0, 200); // Limit length
}
export function validateTeamName(name: string): boolean {
// Allow letters, numbers, spaces, and common punctuation in multiple languages
const validPattern = /^[\w\s\u0900-\u097F\u0A80-\u0AFF\u0A00-\u0A7F\-\.,!]+$/;
return validPattern.test(name) && name.length >= 3 && name.length <= 30;
}
- Use parameterized queries exclusively
- Validate all inputs before database operations
- Escape special characters in dynamic queries
export function validateAdminAccess(request: Request): boolean {
const url = new URL(request.url);
const providedKey = url.searchParams.get("key");
const adminKey = Deno.env.get("ADMIN_KEY");
return providedKey === adminKey && adminKey !== null && adminKey.length > 0;
}
export function createErrorResponse(
error: string,
statusCode: number = 400,
details?: any
): Response {
const errorResponse = {
success: false,
error,
...(details && { details }),
timestamp: new Date().toISOString()
};
console.error(`Quiz API Error [${statusCode}]:`, errorResponse);
return new Response(JSON.stringify(errorResponse), {
status: statusCode,
headers: { 'Content-Type': 'application/json' }
});
}
export function logQuizActivity(
action: string,
details: any,
level: 'info' | 'warn' | 'error' = 'info'
) {
const logEntry = {
timestamp: new Date().toISOString(),
component: 'quiz-system',
action,
details,
level
};
console[level]('Quiz Activity:', logEntry);
}
- Database operations (CRUD functions)
- Validation functions
- API endpoint handlers
- Translation key resolution
- Error handling scenarios
- Complete registration flow
- Admin interface interactions
- Multi-language form submission
- AI service integration
- Error recovery flows
- Load testing with 100 concurrent registrations
- Database performance with 1000+ teams
- AI API response time under load
- Memory usage monitoring
// Required environment variables
interface QuizEnvironment {
ADMIN_KEY: string; // Admin access key
OPENAI_API_KEY?: string; // OpenAI API key (optional)
QUIZ_ENABLED?: string; // Feature flag
DEBUG_MODE?: string; // Debug logging
}
export async function healthCheck(): Promise<{
status: 'healthy' | 'degraded' | 'unhealthy';
services: Record<string, 'up' | 'down'>;
timestamp: string;
}> {
const checks = {
database: await checkDatabase(),
openai: await checkOpenAI(),
quiz_system: 'up' as const
};
const allUp = Object.values(checks).every(status => status === 'up');
const someUp = Object.values(checks).some(status => status === 'up');
return {
status: allUp ? 'healthy' : someUp ? 'degraded' : 'unhealthy',
services: checks,
timestamp: new Date().toISOString()
};
}
- Registration success/failure rates
- API response times
- Database query performance
- AI service usage and latency
- Error frequency and types
- SQLite database automatic backups via Val Town platform
- Export functionality for admin users
- Regular data integrity checks
- Database corruption recovery
- Service degradation handling
- Roll-back procedures for problematic deployments
Document Version: 1.0
Last Updated: August 17, 2025
Technical Review: Required before implementation