Initial code of the vibes

This commit is contained in:
Ben Shiller 2025-07-18 23:22:06 -05:00
parent e7413bcb7b
commit 72a4c4f265
Signed by: shillerben
GPG Key ID: 7B4602B1FBF82986
8 changed files with 1392 additions and 2 deletions

133
README.md
View File

@ -1,3 +1,132 @@
# vibe-praying
# Vibe Praying
Vibe coding a webapp to generate prayer partner pairs
A full-stack web application built with FastAPI that creates random prayer partnerships. Each person is paired with two other people chosen randomly, and all results are persisted in a SQLite database.
## Features
- **Random Pairing**: Each person is paired with exactly 2 other people randomly
- **Persistent Storage**: All pairing sessions are saved to SQLite database
- **Modern UI**: Clean, responsive design with smooth animations
- **Session Management**: View and reload previous pairing sessions
- **Real-time Feedback**: Loading states, success messages, and error handling
- **Mobile Responsive**: Works perfectly on desktop and mobile devices
## Installation
1. **Clone or download the project files**
2. **Install dependencies**:
```bash
pip install -r requirements.txt
```
3. **Run the application**:
```bash
python app.py
```
4. **Open your browser** and navigate to:
```
http://localhost:8000
```
## Usage
1. **Enter Names**: Type or paste names into the text area, one name per line
2. **Session Name**: Optionally provide a name for your session
3. **Create Vibes**: Click the "Create Vibes" button to generate random partnerships
4. **View Results**: See all the prayer vibes displayed in a clean format
5. **Previous Sessions**: View and reload any previous pairing sessions
## API Endpoints
- `GET /` - Main application page
- `POST /create-pairs` - Create new pairing session
- `GET /sessions` - Get all previous sessions
- `GET /session/{session_id}` - Get specific session details
## Database Schema
The application uses SQLite with two main tables:
- **pairing_sessions**: Stores session metadata and pairing results
- **pairs**: Individual pair records (currently unused but available for future features)
## Pairing Algorithm
The pairing algorithm ensures that:
- Each person is paired with exactly 2 other people
- No duplicate pairs are created
- Pairs are distributed as evenly as possible
- Results are truly random
## Technology Stack
- **Backend**: FastAPI, SQLAlchemy, SQLite
- **Frontend**: HTML5, CSS3, Vanilla JavaScript
- **Styling**: Custom CSS with modern design patterns
- **Icons**: Font Awesome
- **Fonts**: Inter (Google Fonts)
## Development
To run in development mode with auto-reload:
```bash
uvicorn app:app --reload --host 0.0.0.0 --port 8000
```
## File Structure
```
prayer-pairing/
├── app.py # Main FastAPI application
├── requirements.txt # Python dependencies
├── README.md # This file
├── templates/
│ └── index.html # Main HTML template
└── static/
├── css/
│ └── style.css # Stylesheets
└── js/
└── app.js # Frontend JavaScript
```
## Features in Detail
### Random Vibe Logic
- Takes a list of names as input
- Creates vibes where each person is matched with 2 others
- Avoids duplicate vibes and self-pairing
- Handles edge cases (odd number of people, etc.)
### Persistent Storage
- All sessions are automatically saved to SQLite database
- Sessions survive app restarts and crashes
- Previous sessions can be viewed and reloaded
- Database file: `prayer_pairs.db`
### Modern UI/UX
- Gradient background with modern card design
- Smooth animations and hover effects
- Loading states and success feedback
- Error handling with user-friendly messages
- Responsive design for all screen sizes
- Keyboard shortcuts (Ctrl+Enter to create pairs)
### Session Management
- View all previous pairing sessions
- Click any session to reload and view its pairs
- Session metadata (name, date, participants)
- Easy navigation between sessions
## Browser Compatibility
- Chrome/Chromium (recommended)
- Firefox
- Safari
- Edge
## License
This project is open source and available under the MIT License.

245
app.py Normal file
View File

@ -0,0 +1,245 @@
from fastapi import FastAPI, Request, Form, HTTPException
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from sqlalchemy import create_engine, Column, Integer, String, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.sql import func
from pydantic import BaseModel
import random
from typing import List, Dict, Any
from datetime import datetime
import os
# Create FastAPI app
app = FastAPI(title="Vibe Praying", version="1.0.0")
# Database setup
SQLALCHEMY_DATABASE_URL = "sqlite:///./prayer_pairs.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Database models
class PairingSession(Base):
__tablename__ = "pairing_sessions"
id = Column(Integer, primary_key=True, index=True)
session_name = Column(String, index=True)
created_at = Column(DateTime, default=func.now())
names = Column(String) # JSON string of names
pairs = Column(String) # JSON string of pairs
class Pair(Base):
__tablename__ = "pairs"
id = Column(Integer, primary_key=True, index=True)
session_id = Column(Integer, index=True)
person1 = Column(String)
person2 = Column(String)
created_at = Column(DateTime, default=func.now())
# Create tables
Base.metadata.create_all(bind=engine)
# Pydantic models
class NameList(BaseModel):
names: List[str]
class PairingResult(BaseModel):
session_name: str
names: List[str]
pairs: List[Dict[str, str]]
created_at: datetime
# Templates and static files
templates = Jinja2Templates(directory="templates")
app.mount("/static", StaticFiles(directory="static"), name="static")
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
def create_pairs(names: List[str]) -> List[Dict[str, str]]:
"""Create pairs where each person is paired with 2 others randomly."""
if len(names) < 3:
raise ValueError("Need at least 3 names to create pairs")
# For each person, we need to pair them with 2 others
# Total pairs needed = len(names) * 2 / 2 = len(names)
pairs_needed = len(names)
# Create a list of all possible pairs
all_possible_pairs = []
for i, person1 in enumerate(names):
for j, person2 in enumerate(names):
if i < j: # Avoid duplicates and self-pairing
all_possible_pairs.append((person1, person2))
# Shuffle the pairs
random.shuffle(all_possible_pairs)
# Track how many pairs each person has
person_pair_counts = {name: 0 for name in names}
selected_pairs = []
# Select pairs ensuring each person gets exactly 2 pairs
for person1, person2 in all_possible_pairs:
# Check if adding this pair would exceed 2 pairs for either person
if (person_pair_counts[person1] < 2 and
person_pair_counts[person2] < 2):
selected_pairs.append({"person1": person1, "person2": person2})
person_pair_counts[person1] += 1
person_pair_counts[person2] += 1
# If we have enough pairs, we're done
if len(selected_pairs) >= pairs_needed:
break
# If we don't have enough pairs, try to add more
if len(selected_pairs) < pairs_needed:
# Find people who still need pairs
people_needing_pairs = [name for name, count in person_pair_counts.items() if count < 2]
# Try to create additional pairs
for person1 in people_needing_pairs:
for person2 in people_needing_pairs:
if person1 != person2:
# Check if this pair already exists
pair_exists = any(
(p["person1"] == person1 and p["person2"] == person2) or
(p["person1"] == person2 and p["person2"] == person1)
for p in selected_pairs
)
if not pair_exists and person_pair_counts[person1] < 2 and person_pair_counts[person2] < 2:
selected_pairs.append({"person1": person1, "person2": person2})
person_pair_counts[person1] += 1
person_pair_counts[person2] += 1
if len(selected_pairs) >= pairs_needed:
break
if len(selected_pairs) >= pairs_needed:
break
# If we still don't have enough pairs, try a different approach
if len(selected_pairs) < pairs_needed:
# Reset and try a more aggressive approach
person_pair_counts = {name: 0 for name in names}
selected_pairs = []
# Try to create pairs more systematically
for i, person1 in enumerate(names):
if person_pair_counts[person1] < 2:
for j, person2 in enumerate(names):
if i != j and person_pair_counts[person2] < 2:
# Check if this pair already exists
pair_exists = any(
(p["person1"] == person1 and p["person2"] == person2) or
(p["person1"] == person2 and p["person2"] == person1)
for p in selected_pairs
)
if not pair_exists:
selected_pairs.append({"person1": person1, "person2": person2})
person_pair_counts[person1] += 1
person_pair_counts[person2] += 1
if len(selected_pairs) >= pairs_needed:
break
if len(selected_pairs) >= pairs_needed:
break
return selected_pairs
@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
"""Serve the main page."""
return templates.TemplateResponse("index.html", {"request": request})
@app.post("/create-pairs")
async def create_pairs_endpoint(names: NameList, session_name: str = "Default Session"):
"""Create pairs from a list of names."""
if len(names.names) < 3:
raise HTTPException(status_code=400, detail="Need at least 3 names to create pairs")
# Remove duplicates and empty names
clean_names = list(set([name.strip() for name in names.names if name.strip()]))
if len(clean_names) < 3:
raise HTTPException(status_code=400, detail="Need at least 3 valid names after cleaning")
# Create pairs
pairs = create_pairs(clean_names)
# Save to database
db = SessionLocal()
try:
import json
session = PairingSession(
session_name=session_name,
names=json.dumps(clean_names),
pairs=json.dumps(pairs)
)
db.add(session)
db.commit()
db.refresh(session)
return {
"session_id": session.id,
"session_name": session_name,
"names": clean_names,
"pairs": pairs,
"created_at": session.created_at
}
finally:
db.close()
@app.get("/sessions")
async def get_sessions():
"""Get all pairing sessions."""
db = SessionLocal()
try:
sessions = db.query(PairingSession).order_by(PairingSession.created_at.desc()).all()
import json
return [
{
"id": session.id,
"session_name": session.session_name,
"names": json.loads(session.names),
"pairs": json.loads(session.pairs),
"created_at": session.created_at
}
for session in sessions
]
finally:
db.close()
@app.get("/session/{session_id}")
async def get_session(session_id: int):
"""Get a specific pairing session."""
db = SessionLocal()
try:
session = db.query(PairingSession).filter(PairingSession.id == session_id).first()
if not session:
raise HTTPException(status_code=404, detail="Session not found")
import json
return {
"id": session.id,
"session_name": session.session_name,
"names": json.loads(session.names),
"pairs": json.loads(session.pairs),
"created_at": session.created_at
}
finally:
db.close()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

BIN
prayer_pairs.db Normal file

Binary file not shown.

25
requirements.txt Normal file
View File

@ -0,0 +1,25 @@
aiofiles==24.1.0
annotated-types==0.7.0
anyio==4.9.0
click==8.2.1
fastapi==0.116.1
h11==0.16.0
httptools==0.6.4
idna==3.10
Jinja2==3.1.6
MarkupSafe==3.0.2
pydantic==2.11.7
pydantic_core==2.33.2
python-dotenv==1.1.1
python-multipart==0.0.20
PyYAML==6.0.2
sniffio==1.3.1
SQLAlchemy==2.0.41
starlette==0.47.1
typing-inspection==0.4.1
typing_extensions==4.14.1
uvicorn==0.35.0
uvloop==0.21.0
watchfiles==1.1.0
websockets==15.0.1
pytest==7.4.3

387
static/css/style.css Normal file
View File

@ -0,0 +1,387 @@
/* Reset and base styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* Header */
.header {
text-align: center;
margin-bottom: 40px;
color: white;
}
.header h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.1rem;
opacity: 0.9;
font-weight: 300;
}
/* Main content */
.main-content {
display: grid;
gap: 30px;
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.main-content {
grid-template-columns: 1fr 1fr;
}
}
/* Sections */
.input-section,
.results-section,
.sessions-section {
background: white;
border-radius: 16px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
backdrop-filter: blur(10px);
}
.sessions-section {
grid-column: 1 / -1;
}
/* Section headers */
h2 {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 20px;
color: #2d3748;
display: flex;
align-items: center;
gap: 10px;
}
/* Form elements */
.input-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #4a5568;
}
input[type="text"],
textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
font-family: inherit;
transition: all 0.3s ease;
background: #f7fafc;
}
input[type="text"]:focus,
textarea:focus {
outline: none;
border-color: #667eea;
background: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
textarea {
resize: vertical;
min-height: 120px;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
font-family: inherit;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
.btn-secondary {
background: #48bb78;
color: white;
box-shadow: 0 4px 15px rgba(72, 187, 120, 0.4);
}
.btn-secondary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(72, 187, 120, 0.6);
}
.btn-outline {
background: transparent;
color: #667eea;
border: 2px solid #667eea;
}
.btn-outline:hover {
background: #667eea;
color: white;
}
/* Results section */
.session-info {
background: #f7fafc;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid #667eea;
}
.session-info h3 {
color: #2d3748;
margin-bottom: 5px;
}
.session-info p {
color: #718096;
font-size: 0.9rem;
}
/* Pairs container */
.pairs-container {
display: grid;
gap: 15px;
margin-bottom: 20px;
}
.pair-card {
background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 20px;
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.3s ease;
}
.pair-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
border-color: #667eea;
}
.pair-info {
display: flex;
align-items: center;
gap: 15px;
flex: 1;
}
.person {
background: white;
padding: 8px 16px;
border-radius: 20px;
font-weight: 500;
color: #2d3748;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.pair-arrow {
color: #667eea;
font-size: 1.2rem;
}
/* Actions */
.actions {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
/* Sessions section */
.sessions-container {
display: grid;
gap: 20px;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
.session-card {
background: white;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 20px;
transition: all 0.3s ease;
cursor: pointer;
}
.session-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
border-color: #667eea;
}
.session-card h3 {
color: #2d3748;
margin-bottom: 10px;
font-size: 1.1rem;
}
.session-card p {
color: #718096;
font-size: 0.9rem;
margin-bottom: 15px;
}
.session-names {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-bottom: 15px;
}
.name-tag {
background: #edf2f7;
color: #4a5568;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 500;
}
.session-pairs {
font-size: 0.9rem;
color: #718096;
}
/* Modals */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
backdrop-filter: blur(5px);
}
.modal-content {
background-color: white;
margin: 15% auto;
padding: 30px;
border-radius: 16px;
width: 90%;
max-width: 400px;
text-align: center;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.success-icon {
font-size: 3rem;
color: #48bb78;
margin-bottom: 20px;
}
/* Spinner */
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive design */
@media (max-width: 768px) {
.container {
padding: 15px;
}
.header h1 {
font-size: 2rem;
}
.input-section,
.results-section,
.sessions-section {
padding: 20px;
}
.actions {
flex-direction: column;
}
.btn {
width: 100%;
justify-content: center;
}
}
/* Loading states */
.loading {
opacity: 0.6;
pointer-events: none;
}
/* Error states */
.error {
border-color: #e53e3e !important;
background-color: #fed7d7 !important;
}
.error-message {
color: #e53e3e;
font-size: 0.9rem;
margin-top: 5px;
}
/* Success states */
.success {
border-color: #48bb78 !important;
background-color: #f0fff4 !important;
}

281
static/js/app.js Normal file
View File

@ -0,0 +1,281 @@
// Global variables
let currentSession = null;
// DOM elements
const createPairsBtn = document.getElementById('createPairsBtn');
const namesInput = document.getElementById('namesInput');
const sessionNameInput = document.getElementById('sessionName');
const resultsSection = document.getElementById('resultsSection');
const pairsContainer = document.getElementById('pairsContainer');
const sessionTitle = document.getElementById('sessionTitle');
const sessionDate = document.getElementById('sessionDate');
const sessionsContainer = document.getElementById('sessionsContainer');
const loadingModal = document.getElementById('loadingModal');
const successModal = document.getElementById('successModal');
// Event listeners
document.addEventListener('DOMContentLoaded', function() {
loadPreviousSessions();
createPairsBtn.addEventListener('click', createPairs);
// Add event listeners for action buttons
document.addEventListener('click', function(e) {
if (e.target.id === 'newSessionBtn') {
resetForm();
}
});
});
// API functions
async function createPairs() {
const names = namesInput.value.trim();
const sessionName = sessionNameInput.value.trim() || 'Prayer Session';
if (!names) {
showError('Please enter at least one name.');
return;
}
const nameList = names.split('\n')
.map(name => name.trim())
.filter(name => name.length > 0);
if (nameList.length < 3) {
showError('Please enter at least 3 names to create vibes.');
return;
}
showLoading();
try {
const response = await fetch('/create-pairs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
names: nameList,
session_name: sessionName
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to create vibes');
}
const result = await response.json();
currentSession = result;
displayResults(result);
hideLoading();
showSuccess();
loadPreviousSessions(); // Refresh the sessions list
} catch (error) {
hideLoading();
showError(error.message);
}
}
async function loadPreviousSessions() {
try {
const response = await fetch('/sessions');
if (!response.ok) {
throw new Error('Failed to load sessions');
}
const sessions = await response.json();
displaySessions(sessions);
} catch (error) {
console.error('Error loading sessions:', error);
}
}
async function loadSession(sessionId) {
try {
const response = await fetch(`/session/${sessionId}`);
if (!response.ok) {
throw new Error('Failed to load session');
}
const session = await response.json();
currentSession = session;
displayResults(session);
// Scroll to results section
resultsSection.scrollIntoView({ behavior: 'smooth' });
} catch (error) {
showError('Failed to load session');
}
}
// Display functions
function displayResults(session) {
sessionTitle.textContent = session.session_name;
sessionDate.textContent = formatDate(session.created_at);
// Clear previous pairs
pairsContainer.innerHTML = '';
// Display pairs
session.pairs.forEach(pair => {
const pairCard = document.createElement('div');
pairCard.className = 'pair-card';
pairCard.innerHTML = `
<div class="pair-info">
<span class="person">${pair.person1}</span>
<i class="fas fa-arrow-right pair-arrow"></i>
<span class="person">${pair.person2}</span>
</div>
`;
pairsContainer.appendChild(pairCard);
});
// Show results section
resultsSection.style.display = 'block';
// Scroll to results
resultsSection.scrollIntoView({ behavior: 'smooth' });
}
function displaySessions(sessions) {
sessionsContainer.innerHTML = '';
if (sessions.length === 0) {
sessionsContainer.innerHTML = '<p style="color: #718096; text-align: center;">No previous sessions found.</p>';
return;
}
sessions.forEach(session => {
const sessionCard = document.createElement('div');
sessionCard.className = 'session-card';
sessionCard.onclick = () => loadSession(session.id);
const namesHtml = session.names.map(name =>
`<span class="name-tag">${name}</span>`
).join('');
sessionCard.innerHTML = `
<h3>${session.session_name}</h3>
<p>Created: ${formatDate(session.created_at)}</p>
<div class="session-names">
${namesHtml}
</div>
<div class="session-pairs">
${session.pairs.length} prayer vibes created
</div>
`;
sessionsContainer.appendChild(sessionCard);
});
}
// Utility functions
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
function showLoading() {
loadingModal.style.display = 'block';
createPairsBtn.classList.add('loading');
}
function hideLoading() {
loadingModal.style.display = 'none';
createPairsBtn.classList.remove('loading');
}
function showSuccess() {
successModal.style.display = 'block';
setTimeout(() => {
hideSuccess();
}, 3000);
}
function hideSuccess() {
successModal.style.display = 'none';
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
}
function showError(message) {
// Remove any existing error messages
const existingError = document.querySelector('.error-message');
if (existingError) {
existingError.remove();
}
// Create error message
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = message;
// Add error styling to input
namesInput.classList.add('error');
// Insert error message after the textarea
namesInput.parentNode.insertBefore(errorDiv, namesInput.nextSibling);
// Remove error styling after 3 seconds
setTimeout(() => {
namesInput.classList.remove('error');
if (errorDiv.parentNode) {
errorDiv.remove();
}
}, 3000);
}
function resetForm() {
namesInput.value = '';
sessionNameInput.value = 'Prayer Session';
resultsSection.style.display = 'none';
currentSession = null;
// Remove any error states
namesInput.classList.remove('error');
const errorMessage = document.querySelector('.error-message');
if (errorMessage) {
errorMessage.remove();
}
// Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' });
}
// Close modals when clicking outside
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
}
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
// Ctrl/Cmd + Enter to create vibes
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
createPairs();
}
// Escape to close modals
if (e.key === 'Escape') {
const modals = document.querySelectorAll('.modal');
modals.forEach(modal => {
if (modal.style.display === 'block') {
modal.style.display = 'none';
}
});
}
});

89
templates/index.html Normal file
View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vibe Praying</title>
<link rel="stylesheet" href="/static/css/style.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<header class="header">
<h1><i class="fas fa-hands-praying"></i> Vibe Praying</h1>
<p>Create random prayer partnerships where each person is paired with two others</p>
</header>
<main class="main-content">
<!-- Input Section -->
<section class="input-section">
<h2>Enter Names</h2>
<div class="input-group">
<label for="sessionName">Session Name:</label>
<input type="text" id="sessionName" placeholder="Enter session name (optional)" value="Prayer Session">
</div>
<div class="input-group">
<label for="namesInput">Names (one per line):</label>
<textarea id="namesInput" placeholder="Enter names, one per line&#10;Example:&#10;John&#10;Sarah&#10;Michael&#10;Emma" rows="8"></textarea>
</div>
<button id="createPairsBtn" class="btn btn-primary">
<i class="fas fa-random"></i> Create Vibes
</button>
</section>
<!-- Results Section -->
<section class="results-section" id="resultsSection" style="display: none;">
<h2>Prayer Vibes</h2>
<div class="session-info">
<h3 id="sessionTitle"></h3>
<p id="sessionDate"></p>
</div>
<div class="pairs-container" id="pairsContainer">
<!-- Pairs will be displayed here -->
</div>
<div class="actions">
<button id="saveBtn" class="btn btn-secondary">
<i class="fas fa-save"></i> Save Session
</button>
<button id="newSessionBtn" class="btn btn-outline">
<i class="fas fa-plus"></i> New Session
</button>
</div>
</section>
<!-- Previous Sessions Section -->
<section class="sessions-section">
<h2>Previous Sessions</h2>
<div class="sessions-container" id="sessionsContainer">
<!-- Previous sessions will be loaded here -->
</div>
</section>
</main>
</div>
<!-- Loading Modal -->
<div id="loadingModal" class="modal">
<div class="modal-content">
<div class="spinner"></div>
<p>Creating prayer vibes...</p>
</div>
</div>
<!-- Success Modal -->
<div id="successModal" class="modal">
<div class="modal-content">
<i class="fas fa-check-circle success-icon"></i>
<h3>Success!</h3>
<p>Prayer vibes have been created and saved.</p>
<button class="btn btn-primary" onclick="closeModal('successModal')">OK</button>
</div>
</div>
<script src="/static/js/app.js"></script>
</body>
</html>

234
test_pairing.py Normal file
View File

@ -0,0 +1,234 @@
#!/usr/bin/env python3
"""
Unit tests for the pairing algorithm using pytest
"""
import pytest
import random
from app import create_pairs
class TestPairingAlgorithm:
"""Test class for the pairing algorithm"""
def test_minimum_names_requirement(self):
"""Test that at least 3 names are required"""
with pytest.raises(ValueError, match="Need at least 3 names"):
create_pairs([])
with pytest.raises(ValueError, match="Need at least 3 names"):
create_pairs(["Alice"])
with pytest.raises(ValueError, match="Need at least 3 names"):
create_pairs(["Alice", "Bob"])
def test_basic_pairing_4_people(self):
"""Test pairing with 4 people"""
names = ["Alice", "Bob", "Charlie", "Diana"]
pairs = create_pairs(names)
# Should have exactly 4 pairs (4 people * 2 pairs each / 2 = 4 total pairs)
assert len(pairs) == 4
# Each person should have exactly 2 pairs
person_pair_counts = self._count_pairs_per_person(pairs)
for person in names:
assert person_pair_counts[person] == 2, f"{person} has {person_pair_counts[person]} pairs, expected 2"
# No duplicate pairs
self._assert_no_duplicate_pairs(pairs)
def test_pairing_5_people(self):
"""Test pairing with 5 people"""
names = ["Alice", "Bob", "Charlie", "Diana", "Eve"]
pairs = create_pairs(names)
# Should have exactly 5 pairs
assert len(pairs) == 5
# Each person should have exactly 2 pairs
person_pair_counts = self._count_pairs_per_person(pairs)
for person in names:
assert person_pair_counts[person] == 2, f"{person} has {person_pair_counts[person]} pairs, expected 2"
# No duplicate pairs
self._assert_no_duplicate_pairs(pairs)
def test_pairing_6_people(self):
"""Test pairing with 6 people"""
names = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"]
pairs = create_pairs(names)
# Should have exactly 6 pairs
assert len(pairs) == 6
# Each person should have exactly 2 pairs
person_pair_counts = self._count_pairs_per_person(pairs)
for person in names:
assert person_pair_counts[person] == 2, f"{person} has {person_pair_counts[person]} pairs, expected 2"
# No duplicate pairs
self._assert_no_duplicate_pairs(pairs)
def test_pairing_7_people(self):
"""Test pairing with 7 people"""
names = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank", "Grace"]
pairs = create_pairs(names)
# Should have exactly 7 pairs
assert len(pairs) == 7
# Each person should have exactly 2 pairs
person_pair_counts = self._count_pairs_per_person(pairs)
for person in names:
assert person_pair_counts[person] == 2, f"{person} has {person_pair_counts[person]} pairs, expected 2"
# No duplicate pairs
self._assert_no_duplicate_pairs(pairs)
def test_no_self_pairing(self):
"""Test that no person is paired with themselves"""
names = ["Alice", "Bob", "Charlie", "Diana"]
pairs = create_pairs(names)
for pair in pairs:
assert pair["person1"] != pair["person2"], f"Self-pairing found: {pair}"
def test_pair_structure(self):
"""Test that pairs have the correct structure"""
names = ["Alice", "Bob", "Charlie"]
pairs = create_pairs(names)
for pair in pairs:
assert "person1" in pair, "Pair missing person1 key"
assert "person2" in pair, "Pair missing person2 key"
assert isinstance(pair["person1"], str), "person1 should be a string"
assert isinstance(pair["person2"], str), "person2 should be a string"
def test_randomness(self):
"""Test that the algorithm produces different results on multiple runs"""
names = ["Alice", "Bob", "Charlie", "Diana", "Eve"]
# Run multiple times and collect results
results = []
for _ in range(10):
pairs = create_pairs(names)
# Convert to set of tuples for comparison
pair_set = {(p["person1"], p["person2"]) for p in pairs}
results.append(pair_set)
# Check that we get at least 2 different results (indicating randomness)
unique_results = set()
for result in results:
unique_results.add(tuple(sorted(result)))
assert len(unique_results) >= 2, "Algorithm doesn't seem to be random"
def test_edge_case_odd_number(self):
"""Test with odd number of people"""
names = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank", "Grace"]
pairs = create_pairs(names)
# Should have exactly 7 pairs
assert len(pairs) == 7
# Each person should have exactly 2 pairs
person_pair_counts = self._count_pairs_per_person(pairs)
for person in names:
assert person_pair_counts[person] == 2, f"{person} has {person_pair_counts[person]} pairs, expected 2"
def test_edge_case_even_number(self):
"""Test with even number of people"""
names = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"]
pairs = create_pairs(names)
# Should have exactly 6 pairs
assert len(pairs) == 6
# Each person should have exactly 2 pairs
person_pair_counts = self._count_pairs_per_person(pairs)
for person in names:
assert person_pair_counts[person] == 2, f"{person} has {person_pair_counts[person]} pairs, expected 2"
def test_large_group(self):
"""Test with a larger group"""
names = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank", "Grace", "Henry", "Ivy", "Jack"]
pairs = create_pairs(names)
# Should have exactly 10 pairs
assert len(pairs) == 10
# Each person should have exactly 2 pairs
person_pair_counts = self._count_pairs_per_person(pairs)
for person in names:
assert person_pair_counts[person] == 2, f"{person} has {person_pair_counts[person]} pairs, expected 2"
# No duplicate pairs
self._assert_no_duplicate_pairs(pairs)
def test_special_characters_in_names(self):
"""Test with names containing special characters"""
names = ["José", "Mary-Jane", "O'Connor", "李小明"]
pairs = create_pairs(names)
# Should have exactly 4 pairs
assert len(pairs) == 4
# Each person should have exactly 2 pairs
person_pair_counts = self._count_pairs_per_person(pairs)
for person in names:
assert person_pair_counts[person] == 2, f"{person} has {person_pair_counts[person]} pairs, expected 2"
def _count_pairs_per_person(self, pairs):
"""Helper method to count how many pairs each person has"""
person_pair_counts = {}
for pair in pairs:
person1, person2 = pair["person1"], pair["person2"]
person_pair_counts[person1] = person_pair_counts.get(person1, 0) + 1
person_pair_counts[person2] = person_pair_counts.get(person2, 0) + 1
return person_pair_counts
def _assert_no_duplicate_pairs(self, pairs):
"""Helper method to assert no duplicate pairs exist"""
pair_strings = set()
for pair in pairs:
pair_str = f"{pair['person1']}-{pair['person2']}"
reverse_str = f"{pair['person2']}-{pair['person1']}"
assert pair_str not in pair_strings, f"Duplicate pair found: {pair}"
assert reverse_str not in pair_strings, f"Duplicate pair found (reverse): {pair}"
pair_strings.add(pair_str)
class TestPairingProperties:
"""Test class for mathematical properties of pairing"""
def test_total_pairs_formula(self):
"""Test that total pairs = number of people"""
for n in range(3, 11): # Test with 3 to 10 people
names = [f"Person{i}" for i in range(n)]
pairs = create_pairs(names)
# Total pairs should equal number of people
assert len(pairs) == n, f"Expected {n} pairs for {n} people, got {len(pairs)}"
def test_each_person_has_exactly_two_pairs(self):
"""Test that each person has exactly 2 pairs"""
for n in range(3, 11): # Test with 3 to 10 people
names = [f"Person{i}" for i in range(n)]
pairs = create_pairs(names)
person_pair_counts = {}
for pair in pairs:
person1, person2 = pair["person1"], pair["person2"]
person_pair_counts[person1] = person_pair_counts.get(person1, 0) + 1
person_pair_counts[person2] = person_pair_counts.get(person2, 0) + 1
for person in names:
assert person_pair_counts[person] == 2, f"{person} has {person_pair_counts[person]} pairs, expected 2"
if __name__ == "__main__":
# Run tests with pytest
pytest.main([__file__, "-v"])