From 33804043c67fc38a3ac7409cbf9bcc71e12cd866 Mon Sep 17 00:00:00 2001 From: blaisadmin Date: Sun, 12 Oct 2025 20:42:56 -0400 Subject: [PATCH] initial config --- README.md | 95 +++++++++++++++++++++++++++++++++++++ backend/Dockerfile | 13 +++++ backend/database/init.sql | 85 +++++++++++++++++++++++++++++++++ backend/package.json | 42 ++++++++++++++++ backend/src/app.ts | 66 ++++++++++++++++++++++++++ backend/tsconfig.json | 20 ++++++++ docker-compose.yml | 83 ++++++++++++++++++++++++++++++++ frontend/Dockerfile.dev | 13 +++++ frontend/index.html | 12 +++++ frontend/package.json | 29 +++++++++++ frontend/postcss.config.js | 6 +++ frontend/src/App.tsx | 31 ++++++++++++ frontend/src/index.css | 22 +++++++++ frontend/src/main.tsx | 10 ++++ frontend/src/pages/Home.tsx | 22 +++++++++ frontend/tailwind.config.js | 11 +++++ frontend/tsconfig.json | 18 +++++++ frontend/tsconfig.node.json | 10 ++++ frontend/vite.config.ts | 10 ++++ 19 files changed, 598 insertions(+) create mode 100644 backend/Dockerfile create mode 100644 backend/database/init.sql create mode 100644 backend/package.json create mode 100644 backend/src/app.ts create mode 100644 backend/tsconfig.json create mode 100644 docker-compose.yml create mode 100644 frontend/Dockerfile.dev create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/Home.tsx create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts diff --git a/README.md b/README.md index e69de29..832e5a9 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,95 @@ +# 🏹 FletchIQ - Archery Scoring Application + +A modern, secure, and extensible archery tournament scoring system built with React, TypeScript, Node.js, and PostgreSQL. + +## Quick Start + +### Prerequisites +- Ubuntu/Linux +- Docker and Docker Compose +- Git + +### Setup + +1. **Navigate to project** +```bash +cd fletchiq +``` + +2. **Start all services** +```bash +docker-compose up +``` + +3. **Access the application** +- Frontend: http://localhost:3000 +- Backend API: http://localhost:5000/api +- PostgreSQL: localhost:5432 +- Redis: localhost:6379 + +### Development Commands + +```bash +# View logs +docker-compose logs -f backend +docker-compose logs -f frontend + +# Stop all services +docker-compose down + +# Rebuild containers +docker-compose build --no-cache + +# Reset database +docker-compose down -v +docker-compose up +``` + +## Project Structure + +``` +fletchiq/ +├── backend/ # Node.js/Express API +│ ├── src/ +│ │ ├── routes/ # API endpoints +│ │ ├── controllers/ # Request handlers +│ │ ├── services/ # Business logic +│ │ ├── middleware/ # Express middleware +│ │ ├── models/ # TypeORM entities +│ │ └── utils/ # Helper functions +│ ├── database/ # SQL migrations +│ └── tests/ # Unit & integration tests +├── frontend/ # React application +│ ├── src/ +│ │ ├── components/ # React components +│ │ ├── pages/ # Page components +│ │ ├── hooks/ # Custom React hooks +│ │ ├── services/ # API client +│ │ └── types/ # TypeScript types +│ └── public/ # Static assets +└── docker-compose.yml # Service orchestration +``` + +## Next Steps + +1. Implement authentication routes +2. Create tournament CRUD endpoints +3. Build scoring input UI +4. Add real-time leaderboard with WebSockets +5. Implement analytics dashboard + +## Security + +- JWT token-based authentication +- Bcrypt password hashing +- CORS and rate limiting +- Input validation with Zod +- Row-level security in database + +## Contributing + +Follow the architecture guide and ensure all code is: +- Type-safe (TypeScript) +- Well-tested +- Documented +- Following the existing patterns diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..f262e0b --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install --legacy-peer-deps + +COPY . . + +EXPOSE 5000 + +CMD ["npm", "run", "dev"] diff --git a/backend/database/init.sql b/backend/database/init.sql new file mode 100644 index 0000000..cdd7124 --- /dev/null +++ b/backend/database/init.sql @@ -0,0 +1,85 @@ +-- Users table +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_users_email ON users(email); + +-- Shooting styles table +CREATE TABLE IF NOT EXISTS shooting_styles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL UNIQUE, + description TEXT, + scoring_format VARCHAR(50) NOT NULL, + max_points_per_arrow INT NOT NULL DEFAULT 10, + configuration JSONB DEFAULT '{}', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Tournaments table +CREATE TABLE IF NOT EXISTS tournaments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + shooting_style_id UUID NOT NULL REFERENCES shooting_styles(id), + status VARCHAR(50) DEFAULT 'draft', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_tournaments_user_id ON tournaments(user_id); +CREATE INDEX idx_tournaments_status ON tournaments(status); + +-- Archers table +CREATE TABLE IF NOT EXISTS archers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tournament_id UUID NOT NULL REFERENCES tournaments(id) ON DELETE CASCADE, + user_id UUID REFERENCES users(id) ON DELETE SET NULL, + name VARCHAR(255) NOT NULL, + bib_number VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_archers_tournament_id ON archers(tournament_id); + +-- Rounds table +CREATE TABLE IF NOT EXISTS rounds ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tournament_id UUID NOT NULL REFERENCES tournaments(id) ON DELETE CASCADE, + round_number INT NOT NULL, + distance_meters INT, + arrow_count INT NOT NULL DEFAULT 72, + max_score_per_arrow INT NOT NULL DEFAULT 10, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_rounds_tournament_id ON rounds(tournament_id); + +-- Shots table +CREATE TABLE IF NOT EXISTS shots ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + round_id UUID NOT NULL REFERENCES rounds(id) ON DELETE CASCADE, + archer_id UUID NOT NULL REFERENCES archers(id) ON DELETE CASCADE, + arrow_number INT NOT NULL, + score INT NOT NULL, + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_shots_round_id ON shots(round_id); +CREATE INDEX idx_shots_archer_id ON shots(archer_id); + +-- Insert default shooting styles +INSERT INTO shooting_styles (name, description, scoring_format, max_points_per_arrow, configuration) +VALUES + ('Olympic Recurve', 'Standard Olympic archery format', 'point_based', 10, '{"rounds": 1, "arrows_per_round": 72}'), + ('3D Field', 'Field archery with variable distances', 'zone_based', 12, '{"variable_distance": true}'), + ('Indoor 18m', 'Indoor competition at 18 meters', 'point_based', 10, '{"distance_meters": 18, "arrows": 60}'), + ('Compound', 'Compound bow format', 'point_based', 10, '{"bow_type": "compound"}') +ON CONFLICT (name) DO NOTHING; diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..54167f0 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,42 @@ +{ + "name": "archery-scoring-backend", + "version": "1.0.0", + "description": "Backend for archery scoring application", + "main": "dist/app.js", + "type": "module", + "scripts": { + "dev": "node --import tsx src/app.ts", + "build": "tsc", + "start": "node dist/app.js", + "migrate": "node --import tsx src/database/migrate.ts", + "seed": "node --import tsx src/database/seed.ts", + "test": "vitest", + "lint": "eslint src/" + }, + "dependencies": { + "express": "4.18.2", + "cors": "2.8.5", + "dotenv": "16.3.1", + "pg": "8.11.3", + "redis": "4.6.12", + "jsonwebtoken": "9.0.2", + "bcryptjs": "2.4.3", + "zod": "3.22.4", + "uuid": "9.0.1", + "express-rate-limit": "7.1.5", + "helmet": "7.1.0", + "morgan": "1.10.0", + "axios": "1.6.2" + }, + "devDependencies": { + "@types/node": "20.10.6", + "@types/express": "4.17.21", + "@types/bcryptjs": "2.4.6", + "@types/jsonwebtoken": "9.0.7", + "typescript": "5.3.3", + "tsx": "4.7.0", + "vitest": "1.1.0", + "supertest": "6.3.3", + "@types/supertest": "6.0.2" + } +} diff --git a/backend/src/app.ts b/backend/src/app.ts new file mode 100644 index 0000000..ec38c8e --- /dev/null +++ b/backend/src/app.ts @@ -0,0 +1,66 @@ +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import morgan from 'morgan'; +import rateLimit from 'express-rate-limit'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const app = express(); +const PORT = process.env.PORT || 5000; + +// Security middleware +app.use(helmet()); +app.use(cors({ + origin: process.env.NODE_ENV === 'production' + ? process.env.FRONTEND_URL + : ['http://localhost:3000', 'http://localhost:5000'], + credentials: true +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100, + message: 'Too many requests from this IP' +}); +app.use(limiter); + +// Logging +app.use(morgan('combined')); + +// Body parser +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// Health check +app.get('/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// Routes (placeholder) +app.get('/api', (req, res) => { + res.json({ message: 'Archery Scoring API v1.0' }); +}); + +// 404 handler +app.use((req, res) => { + res.status(404).json({ error: 'Route not found' }); +}); + +// Error handler +app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { + console.error(err); + res.status(err.status || 500).json({ + error: process.env.NODE_ENV === 'production' + ? 'Internal server error' + : err.message + }); +}); + +app.listen(PORT, () => { + console.log(`🏹 Server running on http://localhost:${PORT}`); +}); + +export default app; diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..08624f8 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..51446a9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,83 @@ +version: '3.9' + +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_INITDB_ARGS: "-U ${POSTGRES_USER}" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./backend/database/init.sql:/docker-entrypoint-initdb.d/01-init.sql + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - archery_network + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - archery_network + + backend: + build: + context: ./backend + dockerfile: Dockerfile + ports: + - "5000:5000" + environment: + DATABASE_URL: postgresql://${POSTGRES_USER}:${DB_PASSWORD}@postgres:5432/${POSTGRES_DB} + REDIS_URL: redis://redis:6379 + NODE_ENV: ${NODE_ENV} + JWT_SECRET: ${JWT_SECRET} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - ./backend/src:/app/src + - ./backend/database:/app/database + networks: + - archery_network + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile.dev + ports: + - "3000:3000" + environment: + REACT_APP_API_URL: ${REACT_APP_API_URL} + volumes: + - ./frontend/src:/app/src + - ./frontend/public:/app/public + - ./frontend/index.html:/app/index.html + depends_on: + - backend + networks: + - archery_network + +volumes: + postgres_data: + redis_data: + +networks: + archery_network: + driver: bridge diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev new file mode 100644 index 0000000..9ea1be1 --- /dev/null +++ b/frontend/Dockerfile.dev @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install --legacy-peer-deps + +COPY . . + +EXPOSE 3000 + +CMD ["npm", "run", "dev", "--", "--host"] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..a3f7a41 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Archery Scoring + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..dbb8436 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "archery-scoring-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "dependencies": { + "react": "18.2.0", + "react-dom": "18.2.0", + "react-router-dom": "6.20.1", + "axios": "1.6.2", + "tailwindcss": "3.4.1", + "lucide-react": "0.263.1", + "zod": "3.22.4" + }, + "devDependencies": { + "@types/react": "18.2.37", + "@types/react-dom": "18.2.15", + "@vitejs/plugin-react": "4.2.1", + "vite": "5.0.8", + "typescript": "5.3.3", + "postcss": "8.4.32", + "autoprefixer": "10.4.16" + }, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..68bcb83 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,31 @@ +import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom' +import { ArcheryTarget } from 'lucide-react' +import Home from './pages/Home' + +function App() { + return ( + +
+ + + + } /> + +
+
+ ) +} + +export default App diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..8970c1b --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,22 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..964aeb4 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx new file mode 100644 index 0000000..f934a65 --- /dev/null +++ b/frontend/src/pages/Home.tsx @@ -0,0 +1,22 @@ +export default function Home() { + return ( +
+
+

+ Welcome to Archery Scoring +

+

+ Track tournament scores with precision and ease +

+
+ + +
+
+
+ ) +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..dca8ba0 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..99c4e52 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "esModuleInterop": true, + "strict": true, + "resolveJsonModule": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..891932f --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + host: true, + }, +})