remove misplaced code
This commit is contained in:
@@ -28,6 +28,22 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Heartbeat: FRE-5286 Google Display & Meta Static Image Asset Production (Recovery)
|
||||||
|
|
||||||
|
### Work Completed
|
||||||
|
- Recovered from stalled/cancelled prior run — produced all 11 static image assets:
|
||||||
|
- **Google Display (3):** Square 1200×1200 (3 Protections), Landscape 1200×628 (Family + Shield), Portrait 600×750 (Voice Clone Detection)
|
||||||
|
- **Meta Creative A (2):** 1:1 1080×1080, 1.91:1 1200×628 — split-screen family vs AI distortion
|
||||||
|
- **Meta Creative B (2):** 1:1 1080×1080, 4:5 1080×1350 — dark terminal HUD with breach alerts
|
||||||
|
- **Meta Creative C (1):** 1:1 1080×1080 — three-panel VoicePrint/DarkWatch/SpamShield layout
|
||||||
|
- **Meta Creative D (3):** 1:1 1080×1080, 1.91:1 1200×628, 4:5 1080×1350 — family figures with digital shield overlay
|
||||||
|
- All 11 uploaded as attachments to FRE-5286; all pass spec checks (PNG, ~98–330KB, correct resolutions)
|
||||||
|
- FRE-5286 marked **done**
|
||||||
|
|
||||||
|
### Run ID: 69a228db-eaa4-4e1c-83f2-d3456a3e94ee
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Heartbeat: FRE-5285 Ad Creative Production & Landing Page Alignment
|
## Heartbeat: FRE-5285 Ad Creative Production & Landing Page Alignment
|
||||||
|
|
||||||
### Work Completed
|
### Work Completed
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
PORT=3000
|
|
||||||
NODE_ENV=development
|
|
||||||
7
nessa-api/.gitignore
vendored
7
nessa-api/.gitignore
vendored
@@ -1,7 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
.env
|
|
||||||
*.db
|
|
||||||
*.sqlite
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
.DS_Store
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
# Nessa API Server
|
|
||||||
|
|
||||||
Backend infrastructure for Nessa's community features including clubs, challenges, and social sharing.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Clubs**: Create, manage, and join communities around shared interests
|
|
||||||
- **Challenges**: Create and participate in time-bound activities within clubs
|
|
||||||
- **Social Feed**: Share updates, like posts, and comment within your community network
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
|
|
||||||
- Node.js with Express.js
|
|
||||||
- SQLite (better-sqlite3) for data persistence
|
|
||||||
- RESTful API architecture
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Node.js 18+
|
|
||||||
- npm
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd nessa-api
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running the Server
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development mode with auto-reload
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# Production mode
|
|
||||||
npm run start
|
|
||||||
```
|
|
||||||
|
|
||||||
The server will start on `http://localhost:3000` by default.
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### Health Check
|
|
||||||
- `GET /api/health` - Service health status
|
|
||||||
- `GET /api/health/ready` - Readiness check
|
|
||||||
- `GET /api/health/live` - Liveness check
|
|
||||||
|
|
||||||
### Clubs
|
|
||||||
- `GET /api/clubs` - List all clubs
|
|
||||||
- `GET /api/clubs/:id` - Get a specific club
|
|
||||||
- `POST /api/clubs` - Create a new club
|
|
||||||
- `PUT /api/clubs/:id` - Update a club
|
|
||||||
- `DELETE /api/clubs/:id` - Delete a club
|
|
||||||
- `GET /api/clubs/:id/members` - Get club members
|
|
||||||
- `POST /api/clubs/:id/members` - Join a club
|
|
||||||
|
|
||||||
### Challenges
|
|
||||||
- `GET /api/challenges` - List all challenges
|
|
||||||
- `GET /api/challenges/:id` - Get a specific challenge
|
|
||||||
- `POST /api/challenges` - Create a new challenge
|
|
||||||
- `PUT /api/challenges/:id` - Update a challenge
|
|
||||||
- `DELETE /api/challenges/:id` - Delete a challenge
|
|
||||||
- `GET /api/challenges/:id/participants` - Get challenge participants
|
|
||||||
- `POST /api/challenges/:id/participants` - Join a challenge
|
|
||||||
- `POST /api/challenges/:id/submissions` - Submit challenge progress
|
|
||||||
|
|
||||||
### Social
|
|
||||||
- `GET /api/social/feed` - Get user's social feed
|
|
||||||
- `POST /api/social/posts` - Create a new post
|
|
||||||
- `GET /api/social/posts/:id` - Get a specific post
|
|
||||||
- `DELETE /api/social/posts/:id` - Delete a post
|
|
||||||
- `POST /api/social/posts/:id/likes` - Like a post
|
|
||||||
- `DELETE /api/social/posts/:id/likes` - Unlike a post
|
|
||||||
- `POST /api/social/posts/:id/comments` - Comment on a post
|
|
||||||
- `GET /api/social/posts/:id/comments` - Get post comments
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
```bash
|
|
||||||
PORT=3000
|
|
||||||
NODE_ENV=development
|
|
||||||
```
|
|
||||||
|
|
||||||
## Database
|
|
||||||
|
|
||||||
The API uses SQLite for data persistence. The database file is created automatically at `src/data/nessa.db` when the server starts.
|
|
||||||
|
|
||||||
### Schema
|
|
||||||
|
|
||||||
- **users** - User accounts (simplified, integrates with auth service in production)
|
|
||||||
- **clubs** - Community groups
|
|
||||||
- **club_memberships** - Club member relationships
|
|
||||||
- **challenges** - Time-bound activities
|
|
||||||
- **challenge_participants** - Challenge enrollment
|
|
||||||
- **challenge_submissions** - Challenge progress tracking
|
|
||||||
- **posts** - Social media posts
|
|
||||||
- **likes** - Post likes
|
|
||||||
- **comments** - Post comments
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
nessa-api/
|
|
||||||
├── src/
|
|
||||||
│ ├── config/
|
|
||||||
│ │ └── database.js # Database setup and schema
|
|
||||||
│ ├── models/
|
|
||||||
│ │ ├── Club.js # Club data layer
|
|
||||||
│ │ ├── Challenge.js # Challenge data layer
|
|
||||||
│ │ └── Social.js # Social features data layer
|
|
||||||
│ ├── routes/
|
|
||||||
│ │ ├── health.js # Health check endpoints
|
|
||||||
│ │ ├── clubs.js # Club endpoints
|
|
||||||
│ │ ├── challenges.js # Challenge endpoints
|
|
||||||
│ │ └── social.js # Social endpoints
|
|
||||||
│ ├── utils/ # Utility functions
|
|
||||||
│ └── index.js # Application entry point
|
|
||||||
├── package.json
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
2368
nessa-api/package-lock.json
generated
2368
nessa-api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "nessa-api",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Nessa Community Features API Server",
|
|
||||||
"main": "src/index.js",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"start": "node src/index.js",
|
|
||||||
"dev": "node --watch src/index.js",
|
|
||||||
"test": "node --test tests/"
|
|
||||||
},
|
|
||||||
"keywords": ["nessa", "community", "api"],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"dotenv": "^16.3.1",
|
|
||||||
"sqlite3": "^5.1.6",
|
|
||||||
"better-sqlite3": "^9.2.2",
|
|
||||||
"uuid": "^11.1.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import Database from 'better-sqlite3';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { dirname, join } from 'path';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
|
|
||||||
const db = new Database(join(__dirname, '../data/nessa.db'));
|
|
||||||
|
|
||||||
// Enable foreign keys
|
|
||||||
db.pragma('foreign_keys = ON');
|
|
||||||
|
|
||||||
// Initialize database schema
|
|
||||||
function initializeSchema() {
|
|
||||||
// Users table (simplified - in production, use auth service)
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
username TEXT UNIQUE NOT NULL,
|
|
||||||
display_name TEXT,
|
|
||||||
avatar_url TEXT,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Clubs table
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS clubs (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
category TEXT,
|
|
||||||
creator_id TEXT NOT NULL,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (creator_id) REFERENCES users(id)
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Club memberships
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS club_memberships (
|
|
||||||
club_id TEXT NOT NULL,
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
role TEXT DEFAULT 'member',
|
|
||||||
joined_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (club_id, user_id),
|
|
||||||
FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Challenges table
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS challenges (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
type TEXT NOT NULL,
|
|
||||||
start_date DATETIME,
|
|
||||||
end_date DATETIME,
|
|
||||||
creator_id TEXT NOT NULL,
|
|
||||||
club_id TEXT,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (creator_id) REFERENCES users(id),
|
|
||||||
FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE SET NULL
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Challenge participants
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS challenge_participants (
|
|
||||||
challenge_id TEXT NOT NULL,
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
status TEXT DEFAULT 'active',
|
|
||||||
joined_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (challenge_id, user_id),
|
|
||||||
FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Challenge submissions
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS challenge_submissions (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
challenge_id TEXT NOT NULL,
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
data TEXT NOT NULL,
|
|
||||||
proof TEXT,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Posts table
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS posts (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
type TEXT DEFAULT 'text',
|
|
||||||
club_id TEXT,
|
|
||||||
challenge_id TEXT,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE SET NULL,
|
|
||||||
FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE SET NULL
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Likes table
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS likes (
|
|
||||||
post_id TEXT NOT NULL,
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (post_id, user_id),
|
|
||||||
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Comments table
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS comments (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
post_id TEXT NOT NULL,
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log('Database schema initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeSchema();
|
|
||||||
|
|
||||||
export default db;
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import cors from 'cors';
|
|
||||||
import dotenv from 'dotenv';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { dirname, join } from 'path';
|
|
||||||
|
|
||||||
import clubsRoutes from './routes/clubs.js';
|
|
||||||
import challengesRoutes from './routes/challenges.js';
|
|
||||||
import socialRoutes from './routes/social.js';
|
|
||||||
import healthRoutes from './routes/health.js';
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const PORT = process.env.PORT || 8087;
|
|
||||||
|
|
||||||
// Middleware
|
|
||||||
app.use(cors());
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(express.urlencoded({ extended: true }));
|
|
||||||
|
|
||||||
// Request logging middleware
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
const start = Date.now();
|
|
||||||
res.on('finish', () => {
|
|
||||||
const duration = Date.now() - start;
|
|
||||||
console.log(`${req.method} ${req.path} ${res.statusCode} (${duration}ms)`);
|
|
||||||
});
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Routes
|
|
||||||
app.use('/api/health', healthRoutes);
|
|
||||||
app.use('/api/clubs', clubsRoutes);
|
|
||||||
app.use('/api/challenges', challengesRoutes);
|
|
||||||
app.use('/api/social', socialRoutes);
|
|
||||||
|
|
||||||
// 404 handler
|
|
||||||
app.use((req, res) => {
|
|
||||||
res.status(404).json({ error: 'Not found' });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Error handler
|
|
||||||
app.use((err, req, res, next) => {
|
|
||||||
console.error('Error:', err);
|
|
||||||
res.status(err.status || 500).json({
|
|
||||||
error: err.message || 'Internal server error'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start server
|
|
||||||
const server = app.listen(PORT, () => {
|
|
||||||
console.log(`Nessa API server running on port ${PORT}`);
|
|
||||||
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default app;
|
|
||||||
export { server };
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
import db from '../config/database.js';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
class Challenge {
|
|
||||||
static getAll(filters = {}) {
|
|
||||||
let query = `
|
|
||||||
SELECT c.*, u.username as creator_username, cl.name as club_name
|
|
||||||
FROM challenges c
|
|
||||||
LEFT JOIN users u ON c.creator_id = u.id
|
|
||||||
LEFT JOIN clubs cl ON c.club_id = cl.id
|
|
||||||
WHERE 1=1
|
|
||||||
`;
|
|
||||||
const params = [];
|
|
||||||
|
|
||||||
if (filters.type) {
|
|
||||||
query += ' AND c.type = ?';
|
|
||||||
params.push(filters.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.clubId) {
|
|
||||||
query += ' AND c.club_id = ?';
|
|
||||||
params.push(filters.clubId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.status) {
|
|
||||||
if (filters.status === 'active') {
|
|
||||||
query += ' AND (c.end_date IS NULL OR c.end_date > ?)';
|
|
||||||
params.push(new Date().toISOString());
|
|
||||||
} else if (filters.status === 'completed') {
|
|
||||||
query += ' AND c.end_date < ?';
|
|
||||||
params.push(new Date().toISOString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query += ' ORDER BY c.created_at DESC';
|
|
||||||
|
|
||||||
const stmt = db.prepare(query);
|
|
||||||
return stmt.all(...params);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getById(id) {
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
SELECT c.*, u.username as creator_username, cl.name as club_name
|
|
||||||
FROM challenges c
|
|
||||||
LEFT JOIN users u ON c.creator_id = u.id
|
|
||||||
LEFT JOIN clubs cl ON c.club_id = cl.id
|
|
||||||
WHERE c.id = ?
|
|
||||||
`);
|
|
||||||
return stmt.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
static create({ title, description, type, startDate, endDate, creatorId, clubId }) {
|
|
||||||
const id = uuidv4();
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
INSERT INTO challenges (id, title, description, type, start_date, end_date, creator_id, club_id)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
`);
|
|
||||||
stmt.run(id, title, description || null, type, startDate || null, endDate || null, creatorId, clubId || null);
|
|
||||||
return this.getById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
static update(id, data) {
|
|
||||||
const challenge = this.getById(id);
|
|
||||||
if (!challenge) return null;
|
|
||||||
|
|
||||||
const allowedFields = ['title', 'description', 'type', 'startDate', 'endDate'];
|
|
||||||
const updates = [];
|
|
||||||
const values = [];
|
|
||||||
|
|
||||||
const fieldMap = { startDate: 'start_date', endDate: 'end_date' };
|
|
||||||
|
|
||||||
for (const field of allowedFields) {
|
|
||||||
if (data[field] !== undefined) {
|
|
||||||
const dbField = fieldMap[field] || field;
|
|
||||||
updates.push(`${dbField} = ?`);
|
|
||||||
values.push(data[field]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updates.length === 0) return challenge;
|
|
||||||
|
|
||||||
values.push(id);
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
UPDATE challenges SET ${updates.join(', ')} WHERE id = ?
|
|
||||||
`);
|
|
||||||
stmt.run(...values);
|
|
||||||
|
|
||||||
return this.getById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
static delete(id) {
|
|
||||||
const stmt = db.prepare('DELETE FROM challenges WHERE id = ?');
|
|
||||||
const result = stmt.run(id);
|
|
||||||
return result.changes > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getParticipants(challengeId) {
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
SELECT cp.*, u.username, u.display_name, u.avatar_url
|
|
||||||
FROM challenge_participants cp
|
|
||||||
JOIN users u ON cp.user_id = u.id
|
|
||||||
WHERE cp.challenge_id = ?
|
|
||||||
ORDER BY cp.joined_at ASC
|
|
||||||
`);
|
|
||||||
return stmt.all(challengeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
static addParticipant(challengeId, userId) {
|
|
||||||
const existing = db.prepare(`
|
|
||||||
SELECT * FROM challenge_participants WHERE challenge_id = ? AND user_id = ?
|
|
||||||
`).get(challengeId, userId);
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
throw new Error('Already a participant');
|
|
||||||
}
|
|
||||||
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
INSERT INTO challenge_participants (challenge_id, user_id)
|
|
||||||
VALUES (?, ?)
|
|
||||||
`);
|
|
||||||
stmt.run(challengeId, userId);
|
|
||||||
|
|
||||||
return { challengeId, userId, status: 'active', joinedAt: new Date().toISOString() };
|
|
||||||
}
|
|
||||||
|
|
||||||
static submitProgress(challengeId, { userId, data, proof }) {
|
|
||||||
const id = uuidv4();
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
INSERT INTO challenge_submissions (id, challenge_id, user_id, data, proof)
|
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
`);
|
|
||||||
stmt.run(id, challengeId, userId, JSON.stringify(data), proof || null);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
challengeId,
|
|
||||||
userId,
|
|
||||||
data,
|
|
||||||
proof,
|
|
||||||
createdAt: new Date().toISOString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static getSubmissions(challengeId) {
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
SELECT * FROM challenge_submissions
|
|
||||||
WHERE challenge_id = ?
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
`);
|
|
||||||
return stmt.all(challengeId).map(s => ({
|
|
||||||
...s,
|
|
||||||
data: JSON.parse(s.data)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Challenge;
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import db from '../config/database.js';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
class Club {
|
|
||||||
static getAll() {
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
SELECT c.*, u.username as creator_username
|
|
||||||
FROM clubs c
|
|
||||||
LEFT JOIN users u ON c.creator_id = u.id
|
|
||||||
ORDER BY c.created_at DESC
|
|
||||||
`);
|
|
||||||
return stmt.all();
|
|
||||||
}
|
|
||||||
|
|
||||||
static getById(id) {
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
SELECT c.*, u.username as creator_username
|
|
||||||
FROM clubs c
|
|
||||||
LEFT JOIN users u ON c.creator_id = u.id
|
|
||||||
WHERE c.id = ?
|
|
||||||
`);
|
|
||||||
return stmt.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
static create({ name, description, category, creatorId }) {
|
|
||||||
const id = uuidv4();
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
INSERT INTO clubs (id, name, description, category, creator_id)
|
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
`);
|
|
||||||
stmt.run(id, name, description || null, category || null, creatorId);
|
|
||||||
return this.getById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
static update(id, data) {
|
|
||||||
const club = this.getById(id);
|
|
||||||
if (!club) return null;
|
|
||||||
|
|
||||||
const allowedFields = ['name', 'description', 'category'];
|
|
||||||
const updates = [];
|
|
||||||
const values = [];
|
|
||||||
|
|
||||||
for (const field of allowedFields) {
|
|
||||||
if (data[field] !== undefined) {
|
|
||||||
updates.push(`${field} = ?`);
|
|
||||||
values.push(data[field]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updates.length === 0) return club;
|
|
||||||
|
|
||||||
values.push(id);
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
UPDATE clubs SET ${updates.join(', ')} WHERE id = ?
|
|
||||||
`);
|
|
||||||
stmt.run(...values);
|
|
||||||
|
|
||||||
return this.getById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
static delete(id) {
|
|
||||||
const stmt = db.prepare('DELETE FROM clubs WHERE id = ?');
|
|
||||||
const result = stmt.run(id);
|
|
||||||
return result.changes > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getMembers(clubId) {
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
SELECT cm.*, u.username, u.display_name, u.avatar_url
|
|
||||||
FROM club_memberships cm
|
|
||||||
JOIN users u ON cm.user_id = u.id
|
|
||||||
WHERE cm.club_id = ?
|
|
||||||
ORDER BY cm.joined_at ASC
|
|
||||||
`);
|
|
||||||
return stmt.all(clubId);
|
|
||||||
}
|
|
||||||
|
|
||||||
static addMember(clubId, userId) {
|
|
||||||
// Check if already a member
|
|
||||||
const existing = db.prepare(`
|
|
||||||
SELECT * FROM club_memberships WHERE club_id = ? AND user_id = ?
|
|
||||||
`).get(clubId, userId);
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
throw new Error('Already a member');
|
|
||||||
}
|
|
||||||
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
INSERT INTO club_memberships (club_id, user_id)
|
|
||||||
VALUES (?, ?)
|
|
||||||
`);
|
|
||||||
stmt.run(clubId, userId);
|
|
||||||
|
|
||||||
return { clubId, userId, role: 'member', joinedAt: new Date().toISOString() };
|
|
||||||
}
|
|
||||||
|
|
||||||
static removeMember(clubId, userId) {
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
DELETE FROM club_memberships WHERE club_id = ? AND user_id = ?
|
|
||||||
`);
|
|
||||||
const result = stmt.run(clubId, userId);
|
|
||||||
return result.changes > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Club;
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import db from '../config/database.js';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
class Social {
|
|
||||||
static getFeed(userId, { limit = 20, offset = 0 } = {}) {
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
SELECT
|
|
||||||
p.*,
|
|
||||||
u.username,
|
|
||||||
u.display_name,
|
|
||||||
u.avatar_url,
|
|
||||||
cl.name as club_name,
|
|
||||||
ch.title as challenge_title,
|
|
||||||
(SELECT COUNT(*) FROM likes WHERE post_id = p.id) as like_count,
|
|
||||||
(SELECT COUNT(*) FROM comments WHERE post_id = p.id) as comment_count,
|
|
||||||
(SELECT COUNT(*) FROM likes WHERE post_id = p.id AND user_id = ?) as user_liked
|
|
||||||
FROM posts p
|
|
||||||
JOIN users u ON p.user_id = u.id
|
|
||||||
LEFT JOIN clubs cl ON p.club_id = cl.id
|
|
||||||
LEFT JOIN challenges ch ON p.challenge_id = ch.id
|
|
||||||
WHERE p.user_id IN (
|
|
||||||
SELECT user_id FROM club_memberships WHERE club_id IN (
|
|
||||||
SELECT club_id FROM club_memberships WHERE user_id = ?
|
|
||||||
)
|
|
||||||
UNION
|
|
||||||
SELECT ?
|
|
||||||
)
|
|
||||||
ORDER BY p.created_at DESC
|
|
||||||
LIMIT ? OFFSET ?
|
|
||||||
`);
|
|
||||||
|
|
||||||
return stmt.all(userId, userId, userId, limit, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
static createPost({ userId, content, type, clubId, challengeId }) {
|
|
||||||
const id = uuidv4();
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
INSERT INTO posts (id, user_id, content, type, club_id, challenge_id)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
|
||||||
`);
|
|
||||||
stmt.run(id, userId, content, type || 'text', clubId || null, challengeId || null);
|
|
||||||
|
|
||||||
return this.getPost(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getPost(id) {
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
SELECT
|
|
||||||
p.*,
|
|
||||||
u.username,
|
|
||||||
u.display_name,
|
|
||||||
u.avatar_url,
|
|
||||||
cl.name as club_name,
|
|
||||||
ch.title as challenge_title
|
|
||||||
FROM posts p
|
|
||||||
JOIN users u ON p.user_id = u.id
|
|
||||||
LEFT JOIN clubs cl ON p.club_id = cl.id
|
|
||||||
LEFT JOIN challenges ch ON p.challenge_id = ch.id
|
|
||||||
WHERE p.id = ?
|
|
||||||
`);
|
|
||||||
return stmt.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
static deletePost(id) {
|
|
||||||
const stmt = db.prepare('DELETE FROM posts WHERE id = ?');
|
|
||||||
const result = stmt.run(id);
|
|
||||||
return result.changes > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static likePost(postId, userId) {
|
|
||||||
const existing = db.prepare(`
|
|
||||||
SELECT * FROM likes WHERE post_id = ? AND user_id = ?
|
|
||||||
`).get(postId, userId);
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
throw new Error('Already liked');
|
|
||||||
}
|
|
||||||
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
INSERT INTO likes (post_id, user_id)
|
|
||||||
VALUES (?, ?)
|
|
||||||
`);
|
|
||||||
stmt.run(postId, userId);
|
|
||||||
|
|
||||||
return { postId, userId, createdAt: new Date().toISOString() };
|
|
||||||
}
|
|
||||||
|
|
||||||
static unlikePost(postId, userId) {
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
DELETE FROM likes WHERE post_id = ? AND user_id = ?
|
|
||||||
`);
|
|
||||||
const result = stmt.run(postId, userId);
|
|
||||||
return result.changes > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static addComment(postId, { userId, content }) {
|
|
||||||
const id = uuidv4();
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
INSERT INTO comments (id, post_id, user_id, content)
|
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
`);
|
|
||||||
stmt.run(id, postId, userId, content);
|
|
||||||
|
|
||||||
return this.getComment(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getComment(id) {
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
SELECT
|
|
||||||
c.*,
|
|
||||||
u.username,
|
|
||||||
u.display_name,
|
|
||||||
u.avatar_url
|
|
||||||
FROM comments c
|
|
||||||
JOIN users u ON c.user_id = u.id
|
|
||||||
WHERE c.id = ?
|
|
||||||
`);
|
|
||||||
return stmt.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getComments(postId) {
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
SELECT
|
|
||||||
c.*,
|
|
||||||
u.username,
|
|
||||||
u.display_name,
|
|
||||||
u.avatar_url
|
|
||||||
FROM comments c
|
|
||||||
JOIN users u ON c.user_id = u.id
|
|
||||||
WHERE c.post_id = ?
|
|
||||||
ORDER BY c.created_at ASC
|
|
||||||
`);
|
|
||||||
return stmt.all(postId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Social;
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import Challenge from '../models/Challenge.js';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
// GET /api/challenges - List all challenges
|
|
||||||
router.get('/', (req, res) => {
|
|
||||||
try {
|
|
||||||
const challenges = Challenge.getAll(req.query);
|
|
||||||
res.json(challenges);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET /api/challenges/:id - Get a specific challenge
|
|
||||||
router.get('/:id', (req, res) => {
|
|
||||||
try {
|
|
||||||
const challenge = Challenge.getById(req.params.id);
|
|
||||||
if (!challenge) {
|
|
||||||
return res.status(404).json({ error: 'Challenge not found' });
|
|
||||||
}
|
|
||||||
res.json(challenge);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// POST /api/challenges - Create a new challenge
|
|
||||||
router.post('/', (req, res) => {
|
|
||||||
try {
|
|
||||||
const { title, description, type, startDate, endDate, creatorId, clubId } = req.body;
|
|
||||||
|
|
||||||
if (!title || !creatorId) {
|
|
||||||
return res.status(400).json({ error: 'title and creatorId are required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const challenge = Challenge.create({
|
|
||||||
title, description, type, startDate, endDate, creatorId, clubId
|
|
||||||
});
|
|
||||||
res.status(201).json(challenge);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// PUT /api/challenges/:id - Update a challenge
|
|
||||||
router.put('/:id', (req, res) => {
|
|
||||||
try {
|
|
||||||
const challenge = Challenge.update(req.params.id, req.body);
|
|
||||||
if (!challenge) {
|
|
||||||
return res.status(404).json({ error: 'Challenge not found' });
|
|
||||||
}
|
|
||||||
res.json(challenge);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// DELETE /api/challenges/:id - Delete a challenge
|
|
||||||
router.delete('/:id', (req, res) => {
|
|
||||||
try {
|
|
||||||
const deleted = Challenge.delete(req.params.id);
|
|
||||||
if (!deleted) {
|
|
||||||
return res.status(404).json({ error: 'Challenge not found' });
|
|
||||||
}
|
|
||||||
res.status(204).send();
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET /api/challenges/:id/participants - Get challenge participants
|
|
||||||
router.get('/:id/participants', (req, res) => {
|
|
||||||
try {
|
|
||||||
const participants = Challenge.getParticipants(req.params.id);
|
|
||||||
res.json(participants);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// POST /api/challenges/:id/participants - Join a challenge
|
|
||||||
router.post('/:id/participants', (req, res) => {
|
|
||||||
try {
|
|
||||||
const { userId } = req.body;
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
return res.status(400).json({ error: 'userId is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const participation = Challenge.addParticipant(req.params.id, userId);
|
|
||||||
res.status(201).json(participation);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// POST /api/challenges/:id/submissions - Submit challenge progress
|
|
||||||
router.post('/:id/submissions', (req, res) => {
|
|
||||||
try {
|
|
||||||
const { userId, data, proof } = req.body;
|
|
||||||
|
|
||||||
if (!userId || !data) {
|
|
||||||
return res.status(400).json({ error: 'userId and data are required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const submission = Challenge.submitProgress(req.params.id, { userId, data, proof });
|
|
||||||
res.status(201).json(submission);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import Club from '../models/Club.js';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
// GET /api/clubs - List all clubs
|
|
||||||
router.get('/', (req, res) => {
|
|
||||||
try {
|
|
||||||
const clubs = Club.getAll();
|
|
||||||
res.json(clubs);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET /api/clubs/:id - Get a specific club
|
|
||||||
router.get('/:id', (req, res) => {
|
|
||||||
try {
|
|
||||||
const club = Club.getById(req.params.id);
|
|
||||||
if (!club) {
|
|
||||||
return res.status(404).json({ error: 'Club not found' });
|
|
||||||
}
|
|
||||||
res.json(club);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// POST /api/clubs - Create a new club
|
|
||||||
router.post('/', (req, res) => {
|
|
||||||
try {
|
|
||||||
const { name, description, category, creatorId } = req.body;
|
|
||||||
|
|
||||||
if (!name || !creatorId) {
|
|
||||||
return res.status(400).json({ error: 'name and creatorId are required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const club = Club.create({ name, description, category, creatorId });
|
|
||||||
res.status(201).json(club);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// PUT /api/clubs/:id - Update a club
|
|
||||||
router.put('/:id', (req, res) => {
|
|
||||||
try {
|
|
||||||
const club = Club.update(req.params.id, req.body);
|
|
||||||
if (!club) {
|
|
||||||
return res.status(404).json({ error: 'Club not found' });
|
|
||||||
}
|
|
||||||
res.json(club);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// DELETE /api/clubs/:id - Delete a club
|
|
||||||
router.delete('/:id', (req, res) => {
|
|
||||||
try {
|
|
||||||
const deleted = Club.delete(req.params.id);
|
|
||||||
if (!deleted) {
|
|
||||||
return res.status(404).json({ error: 'Club not found' });
|
|
||||||
}
|
|
||||||
res.status(204).send();
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET /api/clubs/:id/members - Get club members
|
|
||||||
router.get('/:id/members', (req, res) => {
|
|
||||||
try {
|
|
||||||
const members = Club.getMembers(req.params.id);
|
|
||||||
res.json(members);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// POST /api/clubs/:id/members - Join a club
|
|
||||||
router.post('/:id/members', (req, res) => {
|
|
||||||
try {
|
|
||||||
const { userId } = req.body;
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
return res.status(400).json({ error: 'userId is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const membership = Club.addMember(req.params.id, userId);
|
|
||||||
res.status(201).json(membership);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.get('/', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
status: 'ok',
|
|
||||||
service: 'nessa-api',
|
|
||||||
version: '1.0.0',
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/ready', (req, res) => {
|
|
||||||
// Check database connection, external services, etc.
|
|
||||||
res.json({ ready: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/live', (req, res) => {
|
|
||||||
res.json({ alive: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import Social from '../models/Social.js';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
// GET /api/social/feed - Get user's social feed
|
|
||||||
router.get('/feed', (req, res) => {
|
|
||||||
try {
|
|
||||||
const { userId, limit = 20, offset = 0 } = req.query;
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
return res.status(400).json({ error: 'userId is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const feed = Social.getFeed(userId, { limit: parseInt(limit), offset: parseInt(offset) });
|
|
||||||
res.json(feed);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// POST /api/social/posts - Create a new post
|
|
||||||
router.post('/posts', (req, res) => {
|
|
||||||
try {
|
|
||||||
const { userId, content, type, clubId, challengeId } = req.body;
|
|
||||||
|
|
||||||
if (!userId || !content) {
|
|
||||||
return res.status(400).json({ error: 'userId and content are required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const post = Social.createPost({ userId, content, type, clubId, challengeId });
|
|
||||||
res.status(201).json(post);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET /api/social/posts/:id - Get a specific post
|
|
||||||
router.get('/posts/:id', (req, res) => {
|
|
||||||
try {
|
|
||||||
const post = Social.getPost(req.params.id);
|
|
||||||
if (!post) {
|
|
||||||
return res.status(404).json({ error: 'Post not found' });
|
|
||||||
}
|
|
||||||
res.json(post);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// DELETE /api/social/posts/:id - Delete a post
|
|
||||||
router.delete('/posts/:id', (req, res) => {
|
|
||||||
try {
|
|
||||||
const deleted = Social.deletePost(req.params.id);
|
|
||||||
if (!deleted) {
|
|
||||||
return res.status(404).json({ error: 'Post not found' });
|
|
||||||
}
|
|
||||||
res.status(204).send();
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// POST /api/social/posts/:id/likes - Like a post
|
|
||||||
router.post('/posts/:id/likes', (req, res) => {
|
|
||||||
try {
|
|
||||||
const { userId } = req.body;
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
return res.status(400).json({ error: 'userId is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const like = Social.likePost(req.params.id, userId);
|
|
||||||
res.status(201).json(like);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.message === 'Already liked') {
|
|
||||||
return res.status(409).json({ error: 'Already liked this post' });
|
|
||||||
}
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// DELETE /api/social/posts/:id/likes - Unlike a post
|
|
||||||
router.delete('/posts/:id/likes', (req, res) => {
|
|
||||||
try {
|
|
||||||
const { userId } = req.body;
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
return res.status(400).json({ error: 'userId is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const removed = Social.unlikePost(req.params.id, userId);
|
|
||||||
if (!removed) {
|
|
||||||
return res.status(404).json({ error: 'Like not found' });
|
|
||||||
}
|
|
||||||
res.status(204).send();
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// POST /api/social/posts/:id/comments - Comment on a post
|
|
||||||
router.post('/posts/:id/comments', (req, res) => {
|
|
||||||
try {
|
|
||||||
const { userId, content } = req.body;
|
|
||||||
|
|
||||||
if (!userId || !content) {
|
|
||||||
return res.status(400).json({ error: 'userId and content are required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const comment = Social.addComment(req.params.id, { userId, content });
|
|
||||||
res.status(201).json(comment);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET /api/social/posts/:id/comments - Get post comments
|
|
||||||
router.get('/posts/:id/comments', (req, res) => {
|
|
||||||
try {
|
|
||||||
const comments = Social.getComments(req.params.id);
|
|
||||||
res.json(comments);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { describe, test, before, after } from 'node:test';
|
|
||||||
import assert from 'node:assert';
|
|
||||||
import app from '../src/index.js';
|
|
||||||
|
|
||||||
describe('Health Endpoints', () => {
|
|
||||||
before(() => {
|
|
||||||
// Server is already listening in index.js, but we export app for testing
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GET /api/health returns service status', async () => {
|
|
||||||
// This would be tested with supertest in a real test suite
|
|
||||||
assert.ok(true, 'Health endpoint test placeholder');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Clubs Endpoints', () => {
|
|
||||||
test('POST /api/clubs creates a new club', async () => {
|
|
||||||
assert.ok(true, 'Club creation test placeholder');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GET /api/clubs returns all clubs', async () => {
|
|
||||||
assert.ok(true, 'List clubs test placeholder');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Challenges Endpoints', () => {
|
|
||||||
test('POST /api/challenges creates a new challenge', async () => {
|
|
||||||
assert.ok(true, 'Challenge creation test placeholder');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Social Endpoints', () => {
|
|
||||||
test('POST /api/social/posts creates a new post', async () => {
|
|
||||||
assert.ok(true, 'Post creation test placeholder');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user