You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
837 lines
27 KiB
Markdown
837 lines
27 KiB
Markdown
# Know Foolery - Technical Architecture Documentation
|
|
|
|
## Architecture Overview
|
|
|
|
Know Foolery follows a microservices architecture with clear separation between frontend presentation, backend services, and data persistence layers. The system is designed for cross-platform compatibility, scalability, and maintainability.
|
|
|
|
## System Architecture Diagram
|
|
|
|
```
|
|
┌────────────────────────────────────────────────────────────────────────────┐
|
|
│ Client Layer │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ Web App │ │ Mobile iOS │ │ Mobile Androi│ │ Desktop ctr│ │
|
|
│ │ (React) │ │(React Native)│ │(React Native)│ │ (Wails) │ │
|
|
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
└────────────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
HTTPS/WSS
|
|
│
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ API Gateway Layer │
|
|
│ ┌────────────────────────────────────────────────────────────────────────┐ │
|
|
│ │ NGINX + API Gateway Service (Go) │ │
|
|
│ │ • Authentication & Authorization │ │
|
|
│ │ • Rate Limiting & CORS │ │
|
|
│ │ • Request Routing & Load Balancing │ │
|
|
│ │ • Security Headers & Input Validation │ │
|
|
│ └────────────────────────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
gRPC/HTTP
|
|
│
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ Microservices Layer │
|
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
│ │ Game │ │ Question │ │ User │ │ Leaderboard │ │
|
|
│ │ Service │ │ Service │ │ Service │ │ Service │ │
|
|
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
│ ┌─────────────┐ ┌─────────────┐ │
|
|
│ │ Session │ │ Admin │ │
|
|
│ │ Service │ │ Service │ │
|
|
│ └─────────────┘ └─────────────┘ │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
Database Connections
|
|
│
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ Data & External Layer │
|
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
│ │ PostgreSQL │ │ Redis │ │ Zitadel │ │ Observabi- │ │
|
|
│ │ Primary │ │ Cache & │ │ OAuth │ │ lity │ │
|
|
│ │ Database │ │ Sessions │ │ Provider │ │ Stack │ │
|
|
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## Technology Stack
|
|
|
|
### Frontend Technologies
|
|
```yaml
|
|
Web Application:
|
|
Framework: React 18.2+
|
|
Language: TypeScript 5.0+
|
|
Build Tool: Vite 4.0+
|
|
UI Library: Gluestack UI
|
|
Styling: NativeWind/Tailwind CSS 3.0+
|
|
State Management: React Context + useReducer
|
|
Testing: Jest + React Testing Library + Playwright
|
|
|
|
Mobile Applications:
|
|
Framework: React Native 0.81+
|
|
Language: TypeScript 5.9+
|
|
UI Library: Gluestack UI (Native)
|
|
Navigation: React Navigation 6+
|
|
State Management: React Context + useReducer
|
|
Testing: Jest + React Native Testing Library
|
|
|
|
Desktop Application:
|
|
Framework: Wails 2.10+
|
|
Template: wails-vite-react-ts-tailwind-template
|
|
```
|
|
|
|
### Backend Technologies
|
|
```yaml
|
|
Microservices:
|
|
Language: Go 1.25+
|
|
HTTP Framework: Fiber 3.0+
|
|
gRPC: Protocol Buffers + gRPC-Go
|
|
Database ORM: Ent (Facebook)
|
|
Authentication: JWT + Zitadel Integration
|
|
Testing: Go testing + testcontainers
|
|
|
|
API Gateway:
|
|
Reverse Proxy: NGINX
|
|
Gateway Service: Go + Fiber
|
|
Load Balancing: Round-robin + Health checks
|
|
Rate Limiting: Redis-based sliding window
|
|
```
|
|
|
|
### Data Technologies
|
|
```yaml
|
|
Primary Database:
|
|
Engine: PostgreSQL 15+
|
|
Connection Pooling: pgxpool
|
|
Migrations: Ent migrations
|
|
Backup: pg_dump + Object Storage
|
|
|
|
Cache Layer:
|
|
Engine: Redis 7+
|
|
Use Cases: Sessions, Rate limiting, Cache
|
|
Clustering: Redis Cluster (Production)
|
|
Persistence: RDB + AOF
|
|
|
|
Authentication:
|
|
Provider: Zitadel (Self-hosted)
|
|
Protocol: OAuth 2.0 + OpenID Connect
|
|
Tokens: JWT with RS256 signing
|
|
MFA: TOTP + WebAuthn
|
|
```
|
|
|
|
### Infrastructure Technologies
|
|
```yaml
|
|
Containerization:
|
|
Runtime: Docker 24+
|
|
Orchestration: Kubernetes 1.28+
|
|
Registry: Private Docker Registry
|
|
|
|
Observability:
|
|
Metrics: Prometheus + Node Exporter
|
|
Visualization: Grafana + Custom Dashboards
|
|
Tracing: Jaeger + OpenTelemetry
|
|
Logging: Structured logging + Loki
|
|
|
|
CI/CD:
|
|
Platform: GitHub Actions
|
|
Testing: Automated test suites
|
|
Security: SAST/DAST scanning
|
|
Deployment: GitOps with ArgoCD
|
|
```
|
|
|
|
## Microservices Design
|
|
|
|
### Service Boundaries
|
|
|
|
#### 1. Game Service
|
|
```go
|
|
// Responsibilities
|
|
type GameService struct {
|
|
// Core game logic and session management
|
|
SessionManager *SessionManager
|
|
ScoreCalculator *ScoreCalculator
|
|
AttemptTracker *AttemptTracker
|
|
TimerManager *TimerManager
|
|
}
|
|
|
|
// API Endpoints
|
|
POST /api/v1/game/start // Start new game session
|
|
GET /api/v1/game/session/{id} // Get session details
|
|
POST /api/v1/game/answer // Submit answer
|
|
POST /api/v1/game/hint // Request hint
|
|
POST /api/v1/game/end // End session
|
|
GET /api/v1/game/status/{id} // Get session status
|
|
```
|
|
|
|
#### 2. Question Service
|
|
```go
|
|
// Responsibilities
|
|
type QuestionService struct {
|
|
QuestionRepo *QuestionRepository
|
|
ThemeManager *ThemeManager
|
|
Randomizer *QuestionRandomizer
|
|
Validator *AnswerValidator
|
|
}
|
|
|
|
// API Endpoints
|
|
GET /api/v1/questions/random // Get random question
|
|
GET /api/v1/questions/{id} // Get specific question
|
|
GET /api/v1/questions/themes // List available themes
|
|
POST /api/v1/questions // Create question (admin)
|
|
PUT /api/v1/questions/{id} // Update question (admin)
|
|
DELETE /api/v1/questions/{id} // Delete question (admin)
|
|
```
|
|
|
|
#### 3. User Service
|
|
```go
|
|
// Responsibilities
|
|
type UserService struct {
|
|
UserRepo *UserRepository
|
|
ProfileManager *ProfileManager
|
|
SessionTracker *SessionTracker
|
|
}
|
|
|
|
// API Endpoints
|
|
POST /api/v1/users/register // Register new player
|
|
GET /api/v1/users/profile // Get user profile
|
|
PUT /api/v1/users/profile // Update profile
|
|
GET /api/v1/users/sessions // Get session history
|
|
DELETE /api/v1/users/profile // Delete account (GDPR)
|
|
```
|
|
|
|
#### 4. Leaderboard Service
|
|
```go
|
|
// Responsibilities
|
|
type LeaderboardService struct {
|
|
ScoreAggregator *ScoreAggregator
|
|
RankingEngine *RankingEngine
|
|
StatsCalculator *StatsCalculator
|
|
}
|
|
|
|
// API Endpoints
|
|
GET /api/v1/leaderboard/top10 // Get top 10 scores
|
|
GET /api/v1/leaderboard/stats // Get game statistics
|
|
GET /api/v1/leaderboard/player/{id} // Get player ranking
|
|
POST /api/v1/leaderboard/update // Update scores (internal)
|
|
```
|
|
|
|
#### 5. Session Service
|
|
```go
|
|
// Responsibilities
|
|
type SessionService struct {
|
|
SessionStore *SessionStore
|
|
TimerManager *TimerManager
|
|
StateManager *StateManager
|
|
}
|
|
|
|
// API Endpoints
|
|
POST /api/v1/sessions // Create session
|
|
GET /api/v1/sessions/{id} // Get session
|
|
PUT /api/v1/sessions/{id} // Update session
|
|
DELETE /api/v1/sessions/{id} // End session
|
|
GET /api/v1/sessions/{id}/timer // Get timer status
|
|
```
|
|
|
|
#### 6. Admin Service
|
|
```go
|
|
// Responsibilities
|
|
type AdminService struct {
|
|
AuthManager *AuthManager
|
|
QuestionMgmt *QuestionManagement
|
|
UserMgmt *UserManagement
|
|
AuditLogger *AuditLogger
|
|
}
|
|
|
|
// API Endpoints
|
|
POST /api/v1/admin/auth // Admin authentication
|
|
GET /api/v1/admin/dashboard // Dashboard data
|
|
GET /api/v1/admin/questions // List all questions
|
|
POST /api/v1/admin/questions/bulk // Bulk question import
|
|
GET /api/v1/admin/users // User management
|
|
GET /api/v1/admin/audit // Audit logs
|
|
```
|
|
|
|
### Inter-Service Communication
|
|
|
|
#### Service Mesh Architecture
|
|
```yaml
|
|
Communication Patterns:
|
|
Synchronous: gRPC for real-time operations
|
|
Asynchronous: Event-driven via message queues (future)
|
|
|
|
Service Discovery:
|
|
Registry: Kubernetes DNS
|
|
Health Checks: HTTP /health endpoints
|
|
Load Balancing: Round-robin with health awareness
|
|
|
|
Circuit Breaker:
|
|
Pattern: Hystrix-style circuit breakers
|
|
Fallback: Graceful degradation
|
|
Timeout: Context-based timeouts
|
|
```
|
|
|
|
#### gRPC Service Definitions
|
|
```protobuf
|
|
// game_service.proto
|
|
service GameService {
|
|
rpc StartGame(StartGameRequest) returns (StartGameResponse);
|
|
rpc SubmitAnswer(SubmitAnswerRequest) returns (SubmitAnswerResponse);
|
|
rpc GetSession(GetSessionRequest) returns (GetSessionResponse);
|
|
rpc EndGame(EndGameRequest) returns (EndGameResponse);
|
|
}
|
|
|
|
// question_service.proto
|
|
service QuestionService {
|
|
rpc GetRandomQuestion(RandomQuestionRequest) returns (Question);
|
|
rpc ValidateAnswer(ValidateAnswerRequest) returns (ValidationResponse);
|
|
rpc GetQuestionHint(HintRequest) returns (HintResponse);
|
|
}
|
|
|
|
// leaderboard_service.proto
|
|
service LeaderboardService {
|
|
rpc UpdateScore(UpdateScoreRequest) returns (UpdateScoreResponse);
|
|
rpc GetTopScores(TopScoresRequest) returns (TopScoresResponse);
|
|
rpc GetPlayerRank(PlayerRankRequest) returns (PlayerRankResponse);
|
|
}
|
|
```
|
|
|
|
## Database Architecture
|
|
|
|
### Data Model Design
|
|
|
|
#### Core Entities (Ent Schemas)
|
|
```go
|
|
// Question Entity
|
|
type Question struct {
|
|
ent.Schema
|
|
}
|
|
|
|
func (Question) Fields() []ent.Field {
|
|
return []ent.Field{
|
|
field.String("theme").NotEmpty(),
|
|
field.Text("text").NotEmpty(),
|
|
field.String("answer").NotEmpty(),
|
|
field.Text("hint").Optional(),
|
|
field.Enum("difficulty").Values("easy", "medium", "hard").Default("medium"),
|
|
field.Bool("is_active").Default(true),
|
|
field.Time("created_at").Default(time.Now),
|
|
field.Time("updated_at").Default(time.Now).UpdateDefault(time.Now),
|
|
}
|
|
}
|
|
|
|
func (Question) Indexes() []ent.Index {
|
|
return []ent.Index{
|
|
index.Fields("theme"),
|
|
index.Fields("difficulty"),
|
|
index.Fields("is_active"),
|
|
index.Fields("created_at"),
|
|
}
|
|
}
|
|
|
|
// GameSession Entity
|
|
type GameSession struct {
|
|
ent.Schema
|
|
}
|
|
|
|
func (GameSession) Fields() []ent.Field {
|
|
return []ent.Field{
|
|
field.String("player_name").NotEmpty(),
|
|
field.Int("total_score").Default(0),
|
|
field.Int("questions_asked").Default(0),
|
|
field.Int("questions_correct").Default(0),
|
|
field.Int("hints_used").Default(0),
|
|
field.Time("start_time").Default(time.Now),
|
|
field.Time("end_time").Optional().Nillable(),
|
|
field.Enum("status").Values("active", "completed", "timeout", "abandoned").Default("active"),
|
|
field.String("current_question_id").Optional(),
|
|
field.Int("current_attempts").Default(0),
|
|
}
|
|
}
|
|
|
|
func (GameSession) Edges() []ent.Edge {
|
|
return []ent.Edge{
|
|
edge.To("attempts", QuestionAttempt.Type),
|
|
edge.To("current_question", Question.Type).Unique().Field("current_question_id"),
|
|
}
|
|
}
|
|
|
|
// QuestionAttempt Entity
|
|
type QuestionAttempt struct {
|
|
ent.Schema
|
|
}
|
|
|
|
func (QuestionAttempt) Fields() []ent.Field {
|
|
return []ent.Field{
|
|
field.String("session_id"),
|
|
field.String("question_id"),
|
|
field.Int("attempt_number"),
|
|
field.String("submitted_answer"),
|
|
field.Bool("is_correct"),
|
|
field.Bool("used_hint"),
|
|
field.Int("points_awarded"),
|
|
field.Time("submitted_at").Default(time.Now),
|
|
field.Duration("time_taken"),
|
|
}
|
|
}
|
|
|
|
func (QuestionAttempt) Edges() []ent.Edge {
|
|
return []ent.Edge{
|
|
edge.From("session", GameSession.Type).Ref("attempts").Unique().Field("session_id"),
|
|
edge.To("question", Question.Type).Unique().Field("question_id"),
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Database Performance Optimization
|
|
```sql
|
|
-- Indexes for optimal query performance
|
|
CREATE INDEX idx_questions_theme_active ON questions(theme, is_active);
|
|
CREATE INDEX idx_sessions_status_start_time ON game_sessions(status, start_time);
|
|
CREATE INDEX idx_attempts_session_question ON question_attempts(session_id, question_id);
|
|
CREATE INDEX idx_leaderboard_score_time ON game_sessions(total_score DESC, end_time ASC) WHERE status = 'completed';
|
|
|
|
-- Partitioning for large datasets (future scaling)
|
|
CREATE TABLE game_sessions_y2024m01 PARTITION OF game_sessions
|
|
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
|
|
```
|
|
|
|
### Database Connection Management
|
|
```go
|
|
// Database configuration
|
|
type DatabaseConfig struct {
|
|
Primary DatabaseInstance
|
|
Replicas []DatabaseInstance
|
|
Pool PoolConfig
|
|
}
|
|
|
|
type DatabaseInstance struct {
|
|
Host string
|
|
Port int
|
|
Database string
|
|
Username string
|
|
Password string
|
|
SSLMode string
|
|
}
|
|
|
|
type PoolConfig struct {
|
|
MaxOpenConns int // 25
|
|
MaxIdleConns int // 5
|
|
ConnMaxLifetime time.Duration // 30 minutes
|
|
ConnMaxIdleTime time.Duration // 15 minutes
|
|
}
|
|
|
|
// Connection pooling with read/write splitting
|
|
func NewDatabaseClient(config DatabaseConfig) (*ent.Client, error) {
|
|
primaryDSN := formatDSN(config.Primary)
|
|
|
|
// Primary database for writes
|
|
primaryDB, err := sql.Open("postgres", primaryDSN)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
configurePools(primaryDB, config.Pool)
|
|
|
|
// Create Ent client with instrumented driver
|
|
drv := entsql.OpenDB("postgres", primaryDB)
|
|
return ent.NewClient(ent.Driver(drv)), nil
|
|
}
|
|
```
|
|
|
|
## Security Architecture
|
|
|
|
### Authentication Flow
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant C as Client
|
|
participant G as API Gateway
|
|
participant Z as Zitadel
|
|
participant S as Service
|
|
|
|
C->>G: Request with credentials
|
|
G->>Z: Validate credentials
|
|
Z->>G: Return JWT token
|
|
G->>C: Return token + user info
|
|
C->>G: API request with JWT
|
|
G->>G: Validate JWT signature
|
|
G->>S: Forward request with user context
|
|
S->>G: Return response
|
|
G->>C: Return response
|
|
```
|
|
|
|
### Authorization Strategy
|
|
```go
|
|
// Role-based access control
|
|
type Role string
|
|
|
|
const (
|
|
RolePlayer Role = "player"
|
|
RoleAdmin Role = "admin"
|
|
)
|
|
|
|
type Permissions struct {
|
|
CanPlayGame bool
|
|
CanViewScores bool
|
|
CanManageQuestions bool
|
|
CanViewAuditLogs bool
|
|
CanManageUsers bool
|
|
}
|
|
|
|
var RolePermissions = map[Role]Permissions{
|
|
RolePlayer: {
|
|
CanPlayGame: true,
|
|
CanViewScores: true,
|
|
},
|
|
RoleAdmin: {
|
|
CanPlayGame: true,
|
|
CanViewScores: true,
|
|
CanManageQuestions: true,
|
|
CanViewAuditLogs: true,
|
|
CanManageUsers: true,
|
|
},
|
|
}
|
|
|
|
// JWT middleware with role checking
|
|
func AuthMiddleware() fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
token := extractToken(c.Get("Authorization"))
|
|
|
|
claims, err := validateJWT(token)
|
|
if err != nil {
|
|
return c.Status(401).JSON(fiber.Map{"error": "Invalid token"})
|
|
}
|
|
|
|
c.Locals("user_id", claims.Subject)
|
|
c.Locals("user_role", claims.Role)
|
|
c.Locals("permissions", RolePermissions[Role(claims.Role)])
|
|
|
|
return c.Next()
|
|
}
|
|
}
|
|
|
|
func RequirePermission(permission func(Permissions) bool) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
perms := c.Locals("permissions").(Permissions)
|
|
if !permission(perms) {
|
|
return c.Status(403).JSON(fiber.Map{"error": "Insufficient permissions"})
|
|
}
|
|
return c.Next()
|
|
}
|
|
}
|
|
```
|
|
|
|
### Input Validation & Security
|
|
```go
|
|
// Comprehensive input validation
|
|
type InputValidator struct {
|
|
maxAnswerLength int
|
|
maxNameLength int
|
|
allowedChars *regexp.Regexp
|
|
}
|
|
|
|
func NewInputValidator() *InputValidator {
|
|
return &InputValidator{
|
|
maxAnswerLength: 500,
|
|
maxNameLength: 50,
|
|
allowedChars: regexp.MustCompile(`^[a-zA-Z0-9\s\-_.]+$`),
|
|
}
|
|
}
|
|
|
|
func (v *InputValidator) ValidateAnswer(answer string) error {
|
|
if len(answer) == 0 {
|
|
return errors.New("answer cannot be empty")
|
|
}
|
|
|
|
if len(answer) > v.maxAnswerLength {
|
|
return errors.New("answer too long")
|
|
}
|
|
|
|
// Sanitize HTML and potential XSS
|
|
clean := html.EscapeString(strings.TrimSpace(answer))
|
|
if clean != answer {
|
|
return errors.New("invalid characters in answer")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *InputValidator) ValidatePlayerName(name string) error {
|
|
if len(name) < 2 || len(name) > v.maxNameLength {
|
|
return errors.New("name must be 2-50 characters")
|
|
}
|
|
|
|
if !v.allowedChars.MatchString(name) {
|
|
return errors.New("name contains invalid characters")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Rate limiting implementation
|
|
type RateLimiter struct {
|
|
redis *redis.Client
|
|
limits map[string]RateLimit
|
|
}
|
|
|
|
type RateLimit struct {
|
|
Requests int // Number of requests
|
|
Window time.Duration // Time window
|
|
}
|
|
|
|
func (rl *RateLimiter) Allow(key string, limit RateLimit) bool {
|
|
current, err := rl.redis.Incr(key).Result()
|
|
if err != nil {
|
|
return false // Fail closed
|
|
}
|
|
|
|
if current == 1 {
|
|
rl.redis.Expire(key, limit.Window)
|
|
}
|
|
|
|
return current <= int64(limit.Requests)
|
|
}
|
|
|
|
// Usage in middleware
|
|
func RateLimitMiddleware() fiber.Handler {
|
|
limiter := NewRateLimiter()
|
|
|
|
return func(c *fiber.Ctx) error {
|
|
userID := c.Locals("user_id").(string)
|
|
clientIP := c.IP()
|
|
|
|
// Per-user rate limiting
|
|
userKey := fmt.Sprintf("rate_limit:user:%s", userID)
|
|
if !limiter.Allow(userKey, RateLimit{Requests: 60, Window: time.Minute}) {
|
|
return c.Status(429).JSON(fiber.Map{"error": "Rate limit exceeded"})
|
|
}
|
|
|
|
// Per-IP rate limiting
|
|
ipKey := fmt.Sprintf("rate_limit:ip:%s", clientIP)
|
|
if !limiter.Allow(ipKey, RateLimit{Requests: 100, Window: time.Minute}) {
|
|
return c.Status(429).JSON(fiber.Map{"error": "Rate limit exceeded"})
|
|
}
|
|
|
|
return c.Next()
|
|
}
|
|
}
|
|
```
|
|
|
|
## Cross-Platform Frontend Architecture
|
|
|
|
### Gluestack UI Integration
|
|
```typescript
|
|
// Shared component library structure
|
|
// packages/ui-components/src/index.ts
|
|
|
|
export { GameCard } from './GameCard'
|
|
export { Leaderboard } from './Leaderboard'
|
|
export { Timer } from './Timer'
|
|
export { ScoreDisplay } from './ScoreDisplay'
|
|
export { AdminPanel } from './AdminPanel'
|
|
|
|
// Component example with Gluestack UI
|
|
// packages/ui-components/src/GameCard/GameCard.tsx
|
|
import {
|
|
Card,
|
|
VStack,
|
|
HStack,
|
|
Text,
|
|
Input,
|
|
Button,
|
|
Badge,
|
|
Progress,
|
|
Box
|
|
} from '@gluestack-ui/themed'
|
|
|
|
export interface GameCardProps {
|
|
question: string
|
|
theme: string
|
|
timeRemaining: number
|
|
attemptsLeft: number
|
|
currentScore: number
|
|
onSubmitAnswer: (answer: string) => void
|
|
onRequestHint: () => void
|
|
isLoading?: boolean
|
|
}
|
|
|
|
export const GameCard: React.FC<GameCardProps> = ({
|
|
question,
|
|
theme,
|
|
timeRemaining,
|
|
attemptsLeft,
|
|
currentScore,
|
|
onSubmitAnswer,
|
|
onRequestHint,
|
|
isLoading = false
|
|
}) => {
|
|
const [answer, setAnswer] = useState('')
|
|
|
|
const handleSubmit = () => {
|
|
if (answer.trim()) {
|
|
onSubmitAnswer(answer.trim())
|
|
setAnswer('')
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Card size="lg" variant="elevated" m="$4">
|
|
<VStack space="md" p="$4">
|
|
{/* Header with theme and timer */}
|
|
<HStack justifyContent="space-between" alignItems="center">
|
|
<Badge size="sm" variant="solid" action="info">
|
|
<Text color="$white" fontSize="$sm" fontWeight="$semibold">
|
|
{theme}
|
|
</Text>
|
|
</Badge>
|
|
<HStack space="sm" alignItems="center">
|
|
<Text fontSize="$sm" color="$textLight600">
|
|
⏱️ {Math.floor(timeRemaining / 60)}:{(timeRemaining % 60).toString().padStart(2, '0')}
|
|
</Text>
|
|
</HStack>
|
|
</HStack>
|
|
|
|
{/* Progress indicator */}
|
|
<Progress value={((30 * 60 - timeRemaining) / (30 * 60)) * 100} size="sm">
|
|
<ProgressFilledTrack />
|
|
</Progress>
|
|
|
|
{/* Question */}
|
|
<Box>
|
|
<Text fontSize="$lg" fontWeight="$semibold" lineHeight="$xl">
|
|
{question}
|
|
</Text>
|
|
</Box>
|
|
|
|
{/* Answer input */}
|
|
<Input size="lg" isDisabled={isLoading}>
|
|
<InputField
|
|
placeholder="Enter your answer..."
|
|
value={answer}
|
|
onChangeText={setAnswer}
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
/>
|
|
</Input>
|
|
|
|
{/* Game stats */}
|
|
<HStack justifyContent="space-between" alignItems="center">
|
|
<Text fontSize="$sm" color="$textLight600">
|
|
Attempts left: {attemptsLeft}/3
|
|
</Text>
|
|
<Text fontSize="$sm" color="$textLight600">
|
|
Score: {currentScore} points
|
|
</Text>
|
|
</HStack>
|
|
|
|
{/* Action buttons */}
|
|
<HStack space="sm">
|
|
<Button
|
|
size="lg"
|
|
variant="solid"
|
|
action="primary"
|
|
flex={1}
|
|
onPress={handleSubmit}
|
|
isDisabled={!answer.trim() || isLoading}
|
|
>
|
|
<ButtonText>Submit Answer</ButtonText>
|
|
</Button>
|
|
<Button
|
|
size="lg"
|
|
variant="outline"
|
|
action="secondary"
|
|
onPress={onRequestHint}
|
|
isDisabled={isLoading}
|
|
>
|
|
<ButtonText>💡 Hint</ButtonText>
|
|
</Button>
|
|
</HStack>
|
|
</VStack>
|
|
</Card>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Platform-Specific Adaptations
|
|
```typescript
|
|
// Platform detection and adaptation
|
|
// packages/shared-logic/src/platform.ts
|
|
|
|
export interface PlatformCapabilities {
|
|
hasTouch: boolean
|
|
hasKeyboard: boolean
|
|
hasCamera: boolean
|
|
canVibrate: boolean
|
|
supportsNotifications: boolean
|
|
isOfflineCapable: boolean
|
|
}
|
|
|
|
export const getPlatformCapabilities = (): PlatformCapabilities => {
|
|
// Web platform
|
|
if (typeof window !== 'undefined') {
|
|
return {
|
|
hasTouch: 'ontouchstart' in window,
|
|
hasKeyboard: true,
|
|
hasCamera: !!navigator.mediaDevices?.getUserMedia,
|
|
canVibrate: !!navigator.vibrate,
|
|
supportsNotifications: 'Notification' in window,
|
|
isOfflineCapable: 'serviceWorker' in navigator
|
|
}
|
|
}
|
|
|
|
// React Native platform
|
|
if (typeof require !== 'undefined') {
|
|
try {
|
|
const { Platform } = require('react-native')
|
|
return {
|
|
hasTouch: true,
|
|
hasKeyboard: Platform.OS === 'ios' || Platform.OS === 'android',
|
|
hasCamera: true,
|
|
canVibrate: true,
|
|
supportsNotifications: true,
|
|
isOfflineCapable: true
|
|
}
|
|
} catch {
|
|
// Fallback for other environments
|
|
}
|
|
}
|
|
|
|
// Default capabilities
|
|
return {
|
|
hasTouch: false,
|
|
hasKeyboard: true,
|
|
hasCamera: false,
|
|
canVibrate: false,
|
|
supportsNotifications: false,
|
|
isOfflineCapable: false
|
|
}
|
|
}
|
|
|
|
// Responsive design utilities
|
|
export const useResponsiveValue = <T>(values: {
|
|
mobile: T
|
|
tablet: T
|
|
desktop: T
|
|
}) => {
|
|
const [screenSize, setScreenSize] = useState<'mobile' | 'tablet' | 'desktop'>('desktop')
|
|
|
|
useEffect(() => {
|
|
const updateScreenSize = () => {
|
|
const width = window.innerWidth
|
|
if (width < 768) {
|
|
setScreenSize('mobile')
|
|
} else if (width < 1024) {
|
|
setScreenSize('tablet')
|
|
} else {
|
|
setScreenSize('desktop')
|
|
}
|
|
}
|
|
|
|
updateScreenSize()
|
|
window.addEventListener('resize', updateScreenSize)
|
|
return () => window.removeEventListener('resize', updateScreenSize)
|
|
}, [])
|
|
|
|
return values[screenSize]
|
|
}
|
|
```
|
|
|
|
This technical architecture provides a solid foundation for building a scalable, secure, and maintainable quiz game platform with cross-platform capabilities. |