commit b80879f46b96d55b4e20732299e9c9fa1b51a4d6 Author: Starstrike Date: Tue Apr 29 13:08:01 2025 -0400 Initial commit: RSVP Manager with React frontend and Express backend diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc73260 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Dependencies +node_modules/ +/.pnp +.pnp.js + +# Testing +/coverage + +# Production +/build +/dist + +# Misc +.DS_Store +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Docker +.docker/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9df54b9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM node:18-alpine + +WORKDIR /app + +# Copy package files +COPY package*.json ./ +COPY frontend/package*.json ./frontend/ + +# Install dependencies +RUN npm install +RUN cd frontend && npm install + +# Copy source files +COPY . . + +# Build frontend +RUN cd frontend && npm run build + +# Expose port +EXPOSE 3000 + +# Start the application +CMD ["npm", "start"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1a8b857 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# RSVP Manager + +A modern event RSVP management system with a dark theme interface. + +## Features + +- User-friendly RSVP interface +- Event management +- Guest tracking +- Dark theme UI +- Containerized deployment + +## Tech Stack + +- Frontend: React with TypeScript +- Backend: Node.js/Express with TypeScript +- Database: MySQL +- Containerization: Docker + +## Getting Started + +### Prerequisites + +- Docker +- Docker Compose +- Node.js (for local development) + +### Installation + +1. Clone the repository +2. Run `docker-compose up` to start the application +3. Access the application at `http://localhost:3000` + +## Development + +### Local Development Setup + +1. Install dependencies: + ```bash + # Install backend dependencies + cd backend + npm install + + # Install frontend dependencies + cd ../frontend + npm install + ``` + +2. Start the development servers: + ```bash + # Start backend server + cd backend + npm run dev + + # Start frontend server + cd ../frontend + npm start + ``` + +## License + +MIT \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..81d840f --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,13 @@ +FROM node:18-alpine + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +EXPOSE 5000 + +CMD ["npm", "run", "dev"] \ No newline at end of file diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..2c1379c --- /dev/null +++ b/backend/package.json @@ -0,0 +1,26 @@ +{ + "name": "rsvp-manager-backend", + "version": "1.0.0", + "description": "Backend for RSVP Manager", + "main": "dist/index.js", + "scripts": { + "start": "node dist/index.js", + "dev": "nodemon src/index.ts", + "build": "tsc", + "test": "jest" + }, + "dependencies": { + "express": "^4.18.2", + "mysql2": "^3.6.0", + "cors": "^2.8.5", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "@types/express": "^4.17.17", + "@types/node": "^20.4.5", + "@types/cors": "^2.8.13", + "typescript": "^5.1.6", + "nodemon": "^3.0.1", + "ts-node": "^10.9.1" + } +} \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..7cdc0f9 --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,74 @@ +import express from 'express'; +import cors from 'cors'; +import mysql from 'mysql2/promise'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const app = express(); +const port = process.env.PORT || 5000; + +// Middleware +app.use(cors()); +app.use(express.json()); + +// Database connection +const pool = mysql.createPool({ + host: process.env.DB_HOST || 'mysql', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || 'password', + database: process.env.DB_NAME || 'rsvp_db', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 +}); + +// Routes +app.get('/api/events', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM events'); + res.json(rows); + } catch (error) { + console.error('Error fetching events:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +app.post('/api/events', async (req, res) => { + try { + const { title, description, date, location } = req.body; + const [result] = await pool.query( + 'INSERT INTO events (title, description, date, location) VALUES (?, ?, ?, ?)', + [title, description, date, location] + ); + res.status(201).json(result); + } catch (error) { + console.error('Error creating event:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Initialize database tables +async function initializeDatabase() { + try { + await pool.query(` + CREATE TABLE IF NOT EXISTS events ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT, + date DATETIME NOT NULL, + location VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + console.log('Database initialized successfully'); + } catch (error) { + console.error('Error initializing database:', error); + } +} + +// Start server +app.listen(port, () => { + console.log(`Server running on port ${port}`); + initializeDatabase(); +}); \ No newline at end of file diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..904601c --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9ea4f78 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "3000:3000" + volumes: + - .:/app + - /app/node_modules + environment: + - NODE_ENV=development + - DB_HOST=mysql + - DB_USER=root + - DB_PASSWORD=password + - DB_NAME=rsvp_db + depends_on: + - mysql + + mysql: + image: mysql:8.0 + ports: + - "3306:3306" + environment: + - MYSQL_ROOT_PASSWORD=password + - MYSQL_DATABASE=rsvp_db + volumes: + - mysql_data:/var/lib/mysql + +volumes: + mysql_data: \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..b7540c7 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,13 @@ +FROM node:18-alpine + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +EXPOSE 3000 + +CMD ["npm", "start"] \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..41582f3 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,39 @@ +{ + "name": "rsvp-manager-frontend", + "version": "1.0.0", + "description": "Frontend for RSVP Manager", + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.14.2", + "axios": "^1.4.0", + "@mui/material": "^5.14.2", + "@mui/icons-material": "^5.14.2", + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0" + }, + "devDependencies": { + "@types/react": "^18.2.15", + "@types/react-dom": "^18.2.7", + "typescript": "^5.1.6", + "react-scripts": "5.0.1" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..8b04066 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; +import CssBaseline from '@mui/material/CssBaseline'; +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import { Container } from '@mui/material'; +import EventList from './components/EventList'; +import EventForm from './components/EventForm'; + +const darkTheme = createTheme({ + palette: { + mode: 'dark', + primary: { + main: '#90caf9', + }, + secondary: { + main: '#f48fb1', + }, + background: { + default: '#121212', + paper: '#1e1e1e', + }, + }, +}); + +function App() { + return ( + + + + + + } /> + } /> + + + + + ); +} + +export default App; \ No newline at end of file diff --git a/frontend/src/components/EventForm.tsx b/frontend/src/components/EventForm.tsx new file mode 100644 index 0000000..f177371 --- /dev/null +++ b/frontend/src/components/EventForm.tsx @@ -0,0 +1,108 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Box, + Button, + TextField, + Typography, + Container, +} from '@mui/material'; +import axios from 'axios'; + +const EventForm: React.FC = () => { + const navigate = useNavigate(); + const [formData, setFormData] = useState({ + title: '', + description: '', + date: '', + location: '', + }); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + await axios.post('/api/events', formData); + navigate('/'); + } catch (error) { + console.error('Error creating event:', error); + } + }; + + return ( + + + + Create New Event + +
+ + + + + + + + + +
+
+ ); +}; + +export default EventForm; \ No newline at end of file diff --git a/frontend/src/components/EventList.tsx b/frontend/src/components/EventList.tsx new file mode 100644 index 0000000..303968a --- /dev/null +++ b/frontend/src/components/EventList.tsx @@ -0,0 +1,76 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Box, + Button, + Card, + CardContent, + Typography, + Grid, +} from '@mui/material'; +import axios from 'axios'; + +interface Event { + id: number; + title: string; + description: string; + date: string; + location: string; +} + +const EventList: React.FC = () => { + const [events, setEvents] = useState([]); + const navigate = useNavigate(); + + useEffect(() => { + fetchEvents(); + }, []); + + const fetchEvents = async () => { + try { + const response = await axios.get('/api/events'); + setEvents(response.data); + } catch (error) { + console.error('Error fetching events:', error); + } + }; + + return ( + + + + Events + + + + + + {events.map((event) => ( + + + + + {event.title} + + + {new Date(event.date).toLocaleDateString()} at {event.location} + + + {event.description} + + + + + ))} + + + ); +}; + +export default EventList; \ No newline at end of file diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx new file mode 100644 index 0000000..cfab8bf --- /dev/null +++ b/frontend/src/index.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const root = ReactDOM.createRoot( + document.getElementById('root') as HTMLElement +); + +root.render( + + + +); \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..be223b7 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..5aee87c --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "rsvp-manager", + "version": "1.0.0", + "description": "RSVP Manager - Combined Frontend and Backend", + "main": "src/index.ts", + "scripts": { + "start": "node dist/index.js", + "dev": "nodemon src/index.ts", + "build": "tsc && cd frontend && npm run build", + "test": "jest" + }, + "dependencies": { + "express": "^4.18.2", + "mysql2": "^3.6.0", + "cors": "^2.8.5", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "@types/express": "^4.17.17", + "@types/node": "^20.4.5", + "@types/cors": "^2.8.13", + "typescript": "^5.1.6", + "nodemon": "^3.0.1", + "ts-node": "^10.9.1" + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..6c9bc74 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,83 @@ +import express from 'express'; +import cors from 'cors'; +import mysql from 'mysql2/promise'; +import dotenv from 'dotenv'; +import path from 'path'; + +dotenv.config(); + +const app = express(); +const port = process.env.PORT || 3000; + +// Middleware +app.use(cors()); +app.use(express.json()); + +// Database connection +const pool = mysql.createPool({ + host: process.env.DB_HOST || 'mysql', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || 'password', + database: process.env.DB_NAME || 'rsvp_db', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 +}); + +// Serve static files from the frontend build directory +app.use(express.static(path.join(__dirname, '../frontend/build'))); + +// API Routes +app.get('/api/events', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM events'); + res.json(rows); + } catch (error) { + console.error('Error fetching events:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +app.post('/api/events', async (req, res) => { + try { + const { title, description, date, location } = req.body; + const [result] = await pool.query( + 'INSERT INTO events (title, description, date, location) VALUES (?, ?, ?, ?)', + [title, description, date, location] + ); + res.status(201).json(result); + } catch (error) { + console.error('Error creating event:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Serve the React app for all other routes +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, '../frontend/build/index.html')); +}); + +// Initialize database tables +async function initializeDatabase() { + try { + await pool.query(` + CREATE TABLE IF NOT EXISTS events ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT, + date DATETIME NOT NULL, + location VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + console.log('Database initialized successfully'); + } catch (error) { + console.error('Error initializing database:', error); + } +} + +// Start server +app.listen(port, () => { + console.log(`Server running on port ${port}`); + initializeDatabase(); +}); \ No newline at end of file