refactor: consolidate formatting utilities and add testing infrastructure
- Replace 8 duplicate formatCurrency functions with shared utility - Add comprehensive formatter utilities (currency, date, number, etc.) - Set up Vitest for unit testing with 38 passing tests - Set up Playwright for E2E testing - Consolidate database connection to single source (src/utils/db.ts) - Add auth middleware helpers to reduce code duplication - Fix display bug: remove spurious minus sign in recent activity amounts - Add path aliases for cleaner imports ($utils, $models) - Add project documentation (CODEMAP.md, REFACTORING_PLAN.md) Test coverage: 38 unit tests passing Build: successful with no breaking changes
This commit is contained in:
69
scripts/replace-formatters.md
Normal file
69
scripts/replace-formatters.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Formatter Replacement Progress
|
||||
|
||||
## Components Completed ✅
|
||||
1. DebtBreakdown.svelte - Replaced formatCurrency function
|
||||
2. EnhancedBalance.svelte - Replaced formatCurrency function (with Math.abs wrapper)
|
||||
|
||||
## Remaining Files to Update
|
||||
|
||||
### Components (3 files)
|
||||
- [ ] PaymentModal.svelte - Has formatCurrency function
|
||||
- [ ] SplitMethodSelector.svelte - Has inline .toFixed() calls
|
||||
- [ ] BarChart.svelte - Has inline .toFixed() calls
|
||||
- [ ] IngredientsPage.svelte - Has .toFixed() for recipe calculations
|
||||
|
||||
### Cospend Pages (7 files)
|
||||
- [ ] routes/cospend/+page.svelte - Has formatCurrency function
|
||||
- [ ] routes/cospend/payments/+page.svelte - Has formatCurrency function
|
||||
- [ ] routes/cospend/payments/view/[id]/+page.svelte - Has formatCurrency and .toFixed()
|
||||
- [ ] routes/cospend/payments/add/+page.svelte - Has .toFixed() and .toLocaleString()
|
||||
- [ ] routes/cospend/payments/edit/[id]/+page.svelte - Has multiple .toFixed() calls
|
||||
- [ ] routes/cospend/recurring/+page.svelte - Has formatCurrency function
|
||||
- [ ] routes/cospend/recurring/edit/[id]/+page.svelte - Has .toFixed() and .toLocaleString()
|
||||
- [ ] routes/cospend/settle/+page.svelte - Has formatCurrency function
|
||||
|
||||
## Replacement Strategy
|
||||
|
||||
### Pattern 1: Identical formatCurrency functions
|
||||
```typescript
|
||||
// OLD
|
||||
function formatCurrency(amount) {
|
||||
return new Intl.NumberFormat('de-CH', {
|
||||
style: 'currency',
|
||||
currency: 'CHF'
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
// NEW
|
||||
import { formatCurrency } from '$lib/utils/formatters';
|
||||
// Usage: formatCurrency(amount, 'CHF', 'de-CH')
|
||||
```
|
||||
|
||||
### Pattern 2: .toFixed() for currency display
|
||||
```typescript
|
||||
// OLD
|
||||
{payment.amount.toFixed(2)}
|
||||
|
||||
// NEW
|
||||
import { formatNumber } from '$lib/utils/formatters';
|
||||
{formatNumber(payment.amount, 2, 'de-CH')}
|
||||
```
|
||||
|
||||
### Pattern 3: .toLocaleString() for dates
|
||||
```typescript
|
||||
// OLD
|
||||
nextDate.toLocaleString('de-CH', { weekday: 'long', ... })
|
||||
|
||||
// NEW
|
||||
import { formatDateTime } from '$lib/utils/formatters';
|
||||
formatDateTime(nextDate, 'de-CH', { weekday: 'long', ... })
|
||||
```
|
||||
|
||||
### Pattern 4: Exchange rate display (4 decimals)
|
||||
```typescript
|
||||
// OLD
|
||||
{exchangeRate.toFixed(4)}
|
||||
|
||||
// NEW
|
||||
{formatNumber(exchangeRate, 4, 'de-CH')}
|
||||
```
|
||||
96
scripts/replace_formatters.py
Normal file
96
scripts/replace_formatters.py
Normal file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to replace inline formatCurrency functions with shared formatter utilities
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
files_to_update = [
|
||||
"/home/alex/.local/src/homepage/src/routes/cospend/+page.svelte",
|
||||
"/home/alex/.local/src/homepage/src/routes/cospend/payments/+page.svelte",
|
||||
"/home/alex/.local/src/homepage/src/routes/cospend/payments/view/[id]/+page.svelte",
|
||||
"/home/alex/.local/src/homepage/src/routes/cospend/recurring/+page.svelte",
|
||||
"/home/alex/.local/src/homepage/src/routes/cospend/settle/+page.svelte",
|
||||
]
|
||||
|
||||
def process_file(filepath):
|
||||
print(f"Processing: {filepath}")
|
||||
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
original_content = content
|
||||
|
||||
# Check if already has the import
|
||||
has_formatter_import = 'from \'$lib/utils/formatters\'' in content or 'from "$lib/utils/formatters"' in content
|
||||
|
||||
# Find the <script> tag
|
||||
script_match = re.search(r'(<script[^>]*>)', content)
|
||||
if not script_match:
|
||||
print(f" ⚠️ No <script> tag found")
|
||||
return False
|
||||
|
||||
# Add import if not present
|
||||
if not has_formatter_import:
|
||||
script_tag = script_match.group(1)
|
||||
# Find where to insert (after <script> tag)
|
||||
script_end = script_match.end()
|
||||
|
||||
# Get existing imports to find the right place
|
||||
imports_section_match = re.search(r'<script[^>]*>(.*?)(?:\n\n|\n export|\n let)', content, re.DOTALL)
|
||||
if imports_section_match:
|
||||
imports_end = imports_section_match.end() - len(imports_section_match.group(0).split('\n')[-1])
|
||||
insert_pos = imports_end
|
||||
else:
|
||||
insert_pos = script_end
|
||||
|
||||
new_import = "\n import { formatCurrency } from '$lib/utils/formatters';"
|
||||
content = content[:insert_pos] + new_import + content[insert_pos:]
|
||||
print(f" ✓ Added import")
|
||||
|
||||
# Remove the formatCurrency function definition
|
||||
# Pattern for the function with different variations
|
||||
patterns = [
|
||||
r'\n function formatCurrency\(amount\) \{\n return new Intl\.NumberFormat\(\'de-CH\',\s*\{\n\s*style:\s*\'currency\',\n\s*currency:\s*\'CHF\'\n\s*\}\)\.format\(amount\);\n \}',
|
||||
r'\n function formatCurrency\(amount,\s*currency\s*=\s*\'CHF\'\) \{\n return new Intl\.NumberFormat\(\'de-CH\',\s*\{\n\s*style:\s*\'currency\',\n\s*currency:\s*currency\n\s*\}\)\.format\(amount\);\n \}',
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
if re.search(pattern, content):
|
||||
content = re.sub(pattern, '', content)
|
||||
print(f" ✓ Removed formatCurrency function")
|
||||
break
|
||||
|
||||
# Check if content changed
|
||||
if content != original_content:
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
print(f" ✅ Updated successfully")
|
||||
return True
|
||||
else:
|
||||
print(f" ⚠️ No changes needed")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Replacing formatCurrency functions with shared utilities")
|
||||
print("=" * 60)
|
||||
|
||||
success_count = 0
|
||||
for filepath in files_to_update:
|
||||
if process_file(filepath):
|
||||
success_count += 1
|
||||
print()
|
||||
|
||||
print("=" * 60)
|
||||
print(f"Summary: {success_count}/{len(files_to_update)} files updated")
|
||||
print("=" * 60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
205
scripts/scrape-exercises.ts
Normal file
205
scripts/scrape-exercises.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { dbConnect } from '../src/utils/db';
|
||||
import { Exercise } from '../src/models/Exercise';
|
||||
|
||||
// ExerciseDB API configuration
|
||||
const RAPIDAPI_KEY = process.env.RAPIDAPI_KEY || 'your-rapidapi-key-here';
|
||||
const RAPIDAPI_HOST = 'exercisedb.p.rapidapi.com';
|
||||
const BASE_URL = 'https://exercisedb.p.rapidapi.com';
|
||||
|
||||
interface ExerciseDBExercise {
|
||||
id: string;
|
||||
name: string;
|
||||
gifUrl: string;
|
||||
bodyPart: string;
|
||||
equipment: string;
|
||||
target: string;
|
||||
secondaryMuscles: string[];
|
||||
instructions: string[];
|
||||
}
|
||||
|
||||
async function fetchFromExerciseDB(endpoint: string): Promise<any> {
|
||||
const response = await fetch(`${BASE_URL}${endpoint}`, {
|
||||
headers: {
|
||||
'X-RapidAPI-Key': RAPIDAPI_KEY,
|
||||
'X-RapidAPI-Host': RAPIDAPI_HOST
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch from ExerciseDB: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function scrapeAllExercises(): Promise<void> {
|
||||
console.log('🚀 Starting ExerciseDB scraping...');
|
||||
|
||||
try {
|
||||
await dbConnect();
|
||||
console.log('✅ Connected to database');
|
||||
|
||||
// Fetch all exercises
|
||||
console.log('📡 Fetching exercises from ExerciseDB...');
|
||||
const exercises: ExerciseDBExercise[] = await fetchFromExerciseDB('/exercises?limit=2000');
|
||||
|
||||
console.log(`📊 Found ${exercises.length} exercises`);
|
||||
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
let errors = 0;
|
||||
|
||||
for (const exercise of exercises) {
|
||||
try {
|
||||
// Check if exercise already exists
|
||||
const existingExercise = await Exercise.findOne({ exerciseId: exercise.id });
|
||||
|
||||
if (existingExercise) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine difficulty based on equipment and complexity
|
||||
let difficulty: 'beginner' | 'intermediate' | 'advanced' = 'intermediate';
|
||||
|
||||
if (exercise.equipment === 'body weight') {
|
||||
difficulty = 'beginner';
|
||||
} else if (exercise.equipment.includes('barbell') || exercise.equipment.includes('olympic')) {
|
||||
difficulty = 'advanced';
|
||||
} else if (exercise.equipment.includes('dumbbell') || exercise.equipment.includes('cable')) {
|
||||
difficulty = 'intermediate';
|
||||
}
|
||||
|
||||
// Create new exercise
|
||||
const newExercise = new Exercise({
|
||||
exerciseId: exercise.id,
|
||||
name: exercise.name,
|
||||
gifUrl: exercise.gifUrl,
|
||||
bodyPart: exercise.bodyPart.toLowerCase(),
|
||||
equipment: exercise.equipment.toLowerCase(),
|
||||
target: exercise.target.toLowerCase(),
|
||||
secondaryMuscles: exercise.secondaryMuscles.map(m => m.toLowerCase()),
|
||||
instructions: exercise.instructions,
|
||||
difficulty,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
await newExercise.save();
|
||||
imported++;
|
||||
|
||||
if (imported % 100 === 0) {
|
||||
console.log(`⏳ Imported ${imported} exercises...`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Error importing exercise ${exercise.name}:`, error);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Scraping completed!');
|
||||
console.log(`📈 Summary: ${imported} imported, ${skipped} skipped, ${errors} errors`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('💥 Scraping failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateExistingExercises(): Promise<void> {
|
||||
console.log('🔄 Updating existing exercises...');
|
||||
|
||||
try {
|
||||
await dbConnect();
|
||||
|
||||
const exercises: ExerciseDBExercise[] = await fetchFromExerciseDB('/exercises?limit=2000');
|
||||
|
||||
let updated = 0;
|
||||
|
||||
for (const exercise of exercises) {
|
||||
try {
|
||||
const existingExercise = await Exercise.findOne({ exerciseId: exercise.id });
|
||||
|
||||
if (existingExercise) {
|
||||
// Update with new data from API
|
||||
existingExercise.name = exercise.name;
|
||||
existingExercise.gifUrl = exercise.gifUrl;
|
||||
existingExercise.bodyPart = exercise.bodyPart.toLowerCase();
|
||||
existingExercise.equipment = exercise.equipment.toLowerCase();
|
||||
existingExercise.target = exercise.target.toLowerCase();
|
||||
existingExercise.secondaryMuscles = exercise.secondaryMuscles.map(m => m.toLowerCase());
|
||||
existingExercise.instructions = exercise.instructions;
|
||||
|
||||
await existingExercise.save();
|
||||
updated++;
|
||||
|
||||
if (updated % 100 === 0) {
|
||||
console.log(`⏳ Updated ${updated} exercises...`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error updating exercise ${exercise.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Updated ${updated} exercises`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('💥 Update failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function getExerciseStats(): Promise<void> {
|
||||
try {
|
||||
await dbConnect();
|
||||
|
||||
const totalExercises = await Exercise.countDocuments();
|
||||
const activeExercises = await Exercise.countDocuments({ isActive: true });
|
||||
|
||||
const bodyParts = await Exercise.distinct('bodyPart');
|
||||
const equipment = await Exercise.distinct('equipment');
|
||||
const targets = await Exercise.distinct('target');
|
||||
|
||||
console.log('📊 Exercise Database Stats:');
|
||||
console.log(` Total exercises: ${totalExercises}`);
|
||||
console.log(` Active exercises: ${activeExercises}`);
|
||||
console.log(` Body parts: ${bodyParts.length} (${bodyParts.join(', ')})`);
|
||||
console.log(` Equipment types: ${equipment.length}`);
|
||||
console.log(` Target muscles: ${targets.length}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('💥 Stats failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
const command = process.argv[2];
|
||||
|
||||
switch (command) {
|
||||
case 'scrape':
|
||||
scrapeAllExercises()
|
||||
.then(() => process.exit(0))
|
||||
.catch(() => process.exit(1));
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
updateExistingExercises()
|
||||
.then(() => process.exit(0))
|
||||
.catch(() => process.exit(1));
|
||||
break;
|
||||
|
||||
case 'stats':
|
||||
getExerciseStats()
|
||||
.then(() => process.exit(0))
|
||||
.catch(() => process.exit(1));
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Usage: tsx scripts/scrape-exercises.ts [command]');
|
||||
console.log('Commands:');
|
||||
console.log(' scrape - Import all exercises from ExerciseDB');
|
||||
console.log(' update - Update existing exercises with latest data');
|
||||
console.log(' stats - Show database statistics');
|
||||
process.exit(0);
|
||||
}
|
||||
35
scripts/update-db-imports.sh
Executable file
35
scripts/update-db-imports.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Update all files importing from the legacy $lib/db/db to use $utils/db instead
|
||||
|
||||
files=(
|
||||
"/home/alex/.local/src/homepage/src/routes/mario-kart/[id]/+page.server.ts"
|
||||
"/home/alex/.local/src/homepage/src/routes/mario-kart/+page.server.ts"
|
||||
"/home/alex/.local/src/homepage/src/routes/api/fitness/sessions/[id]/+server.ts"
|
||||
"/home/alex/.local/src/homepage/src/routes/api/fitness/sessions/+server.ts"
|
||||
"/home/alex/.local/src/homepage/src/routes/api/fitness/templates/[id]/+server.ts"
|
||||
"/home/alex/.local/src/homepage/src/routes/api/fitness/templates/+server.ts"
|
||||
"/home/alex/.local/src/homepage/src/routes/api/fitness/exercises/[id]/+server.ts"
|
||||
"/home/alex/.local/src/homepage/src/routes/api/fitness/exercises/+server.ts"
|
||||
"/home/alex/.local/src/homepage/src/routes/api/fitness/exercises/filters/+server.ts"
|
||||
"/home/alex/.local/src/homepage/src/routes/api/fitness/seed-example/+server.ts"
|
||||
"/home/alex/.local/src/homepage/src/routes/api/mario-kart/tournaments/[id]/groups/[groupId]/scores/+server.ts"
|
||||
"/home/alex/.local/src/homepage/src/routes/api/mario-kart/tournaments/[id]/groups/+server.ts"
|
||||
"/home/alex/.local/src/homepage/src/routes/api/mario-kart/tournaments/[id]/contestants/[contestantId]/dnf/+server.ts"
|
||||
"/home/alex/.local/src/homepage/src/routes/api/mario-kart/tournaments/[id]/contestants/+server.ts"
|
||||
"/home/alex/.local/src/homepage/src/routes/api/mario-kart/tournaments/[id]/+server.ts"
|
||||
"/home/alex/.local/src/homepage/src/routes/api/mario-kart/tournaments/[id]/bracket/+server.ts"
|
||||
"/home/alex/.local/src/homepage/src/routes/api/mario-kart/tournaments/[id]/bracket/matches/[matchId]/scores/+server.ts"
|
||||
"/home/alex/.local/src/homepage/src/routes/api/mario-kart/tournaments/+server.ts"
|
||||
)
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
if [ -f "$file" ]; then
|
||||
echo "Updating $file"
|
||||
sed -i "s/from '\$lib\/db\/db'/from '\$utils\/db'/g" "$file"
|
||||
else
|
||||
echo "File not found: $file"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "All files updated!"
|
||||
73
scripts/update_formatter_calls.py
Normal file
73
scripts/update_formatter_calls.py
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to update formatCurrency calls to include CHF and de-CH parameters
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
files_to_update = [
|
||||
"/home/alex/.local/src/homepage/src/routes/cospend/+page.svelte",
|
||||
"/home/alex/.local/src/homepage/src/routes/cospend/payments/+page.svelte",
|
||||
"/home/alex/.local/src/homepage/src/routes/cospend/payments/view/[id]/+page.svelte",
|
||||
"/home/alex/.local/src/homepage/src/routes/cospend/recurring/+page.svelte",
|
||||
"/home/alex/.local/src/homepage/src/routes/cospend/settle/+page.svelte",
|
||||
]
|
||||
|
||||
def process_file(filepath):
|
||||
print(f"Processing: {filepath}")
|
||||
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
original_content = content
|
||||
changes = 0
|
||||
|
||||
# Pattern 1: formatCurrency(amount) -> formatCurrency(amount, 'CHF', 'de-CH')
|
||||
# But skip if already has parameters
|
||||
def replace_single_param(match):
|
||||
amount = match.group(1)
|
||||
# Check if amount already contains currency parameter (contains comma followed by quote)
|
||||
if ", '" in amount or ', "' in amount:
|
||||
return match.group(0) # Already has parameters, skip
|
||||
return f"formatCurrency({amount}, 'CHF', 'de-CH')"
|
||||
|
||||
content, count1 = re.subn(
|
||||
r'formatCurrency\(([^)]+)\)',
|
||||
replace_single_param,
|
||||
content
|
||||
)
|
||||
changes += count1
|
||||
|
||||
if changes > 0:
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
print(f" ✅ Updated {changes} formatCurrency calls")
|
||||
return True
|
||||
else:
|
||||
print(f" ⚠️ No changes needed")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Updating formatCurrency calls with CHF and de-CH params")
|
||||
print("=" * 60)
|
||||
|
||||
success_count = 0
|
||||
for filepath in files_to_update:
|
||||
if process_file(filepath):
|
||||
success_count += 1
|
||||
print()
|
||||
|
||||
print("=" * 60)
|
||||
print(f"Summary: {success_count}/{len(files_to_update)} files updated")
|
||||
print("=" * 60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user