🧙‍♂️ Transform LifeRPG into The Wizard's Grimoire - Production-Ready Application

 Major Features Added:
- Complete magical theming and rebranding from LifeRPG to The Wizard's Grimoire
- Production-grade React frontend with Tailwind CSS v4 and magical aesthetics
- Comprehensive analytics dashboard with Recharts integration (ScryingPortal)
- Push notifications system with PWA service worker support
- Drag & drop functionality using @dnd-kit for habit reordering
- Social features with friends system and leaderboards
- Performance optimization tools and monitoring
- Mobile app enhancement with PWA installation support

🏗️ Technical Infrastructure:
- Advanced service worker with offline support and background sync
- Zustand state management for scalable application state
- Production-ready UI component system with enhanced Button, Card, Input
- Progressive Web App (PWA) with manifest and app installation
- FastAPI backend with comprehensive API endpoints
- Docker containerization and CI/CD pipeline setup

📱 Progressive Web App Features:
- Offline functionality with intelligent caching
- Push notification support for habit reminders
- App installation on mobile and desktop platforms
- Background sync for offline data management
- Performance monitoring and optimization tools

🎨 User Experience:
- Magical wizard/grimoire theming throughout application
- Responsive design optimized for all device sizes
- Drag & drop habit management with smooth animations
- Interactive analytics with multiple chart types
- Social connectivity with friends and competitive features
- Comprehensive notification and performance settings

🔧 Developer Experience:
- Modern development stack with Vite and React
- Comprehensive testing setup and CI/CD pipelines
- Code quality tools with pre-commit hooks
- Docker development environment
- Detailed documentation and implementation guides

This represents a complete transformation from prototype to production-ready application with enterprise-grade features and magical user experience.
This commit is contained in:
TLimoges33
2025-08-30 17:32:42 +00:00
committed by GitHub
parent 00ad1bd8d4
commit 7fe4ae5365
270 changed files with 46366 additions and 7824 deletions
+135
View File
@@ -0,0 +1,135 @@
{
"name": "The Wizard's Grimoire",
"short_name": "Grimoire",
"description": "Track your magical habits and build powerful routines with The Wizard's Grimoire",
"start_url": "/",
"display": "standalone",
"theme_color": "#7c3aed",
"background_color": "#0f172a",
"orientation": "portrait-primary",
"scope": "/",
"categories": [
"productivity",
"lifestyle",
"health"
],
"lang": "en",
"dir": "ltr",
"icons": [
{
"src": "/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
],
"shortcuts": [
{
"name": "Quick Add Habit",
"short_name": "Add Habit",
"description": "Quickly add a new magical habit",
"url": "/habits/new",
"icons": [
{
"src": "/icon-96x96.png",
"sizes": "96x96"
}
]
},
{
"name": "Today's Progress",
"short_name": "Today",
"description": "View today's habit progress",
"url": "/today",
"icons": [
{
"src": "/icon-96x96.png",
"sizes": "96x96"
}
]
},
{
"name": "Analytics",
"short_name": "Stats",
"description": "View your magical progress analytics",
"url": "/analytics",
"icons": [
{
"src": "/icon-96x96.png",
"sizes": "96x96"
}
]
}
],
"screenshots": [
{
"src": "/screenshot-wide.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide",
"label": "The Wizard's Grimoire desktop interface"
},
{
"src": "/screenshot-narrow.png",
"sizes": "375x812",
"type": "image/png",
"form_factor": "narrow",
"label": "The Wizard's Grimoire mobile interface"
}
],
"prefer_related_applications": false,
"related_applications": [
{
"platform": "webapp",
"url": "https://wizards-grimoire.app"
}
],
"protocol_handlers": [
{
"protocol": "web+grimoire",
"url": "/share?habit=%s"
}
]
}
+264
View File
@@ -0,0 +1,264 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Offline - The Wizard's Grimoire</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1e1b4b, #312e81, #1e1b4b);
color: #e2e8f0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
text-align: center;
max-width: 500px;
padding: 2rem;
background: rgba(30, 27, 75, 0.8);
border-radius: 20px;
border: 1px solid rgba(124, 58, 237, 0.3);
backdrop-filter: blur(10px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.icon {
font-size: 4rem;
margin-bottom: 1rem;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%,
100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
h1 {
color: #c084fc;
margin-bottom: 1rem;
font-size: 2rem;
font-weight: 600;
}
p {
color: #cbd5e1;
margin-bottom: 2rem;
line-height: 1.6;
}
.button {
display: inline-block;
background: linear-gradient(135deg, #7c3aed, #a855f7);
color: white;
padding: 12px 24px;
border-radius: 12px;
text-decoration: none;
font-weight: 500;
transition: all 0.3s ease;
border: none;
cursor: pointer;
margin: 0 8px;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(124, 58, 237, 0.4);
}
.button.secondary {
background: transparent;
border: 2px solid #7c3aed;
color: #c084fc;
}
.status {
margin-top: 2rem;
padding: 1rem;
background: rgba(249, 115, 22, 0.1);
border: 1px solid rgba(249, 115, 22, 0.3);
border-radius: 12px;
color: #fed7aa;
}
.features {
margin-top: 2rem;
text-align: left;
}
.features h3 {
color: #c084fc;
margin-bottom: 1rem;
font-size: 1.2rem;
}
.features ul {
list-style: none;
padding: 0;
}
.features li {
padding: 8px 0;
border-bottom: 1px solid rgba(124, 58, 237, 0.2);
color: #cbd5e1;
}
.features li:last-child {
border-bottom: none;
}
.features li::before {
content: "✨ ";
color: #a855f7;
margin-right: 8px;
}
.network-status {
position: fixed;
top: 20px;
right: 20px;
padding: 8px 16px;
background: #ef4444;
color: white;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 500;
}
.network-status.online {
background: #10b981;
}
@media (max-width: 600px) {
.container {
padding: 1.5rem;
margin: 10px;
}
h1 {
font-size: 1.5rem;
}
.icon {
font-size: 3rem;
}
}
</style>
</head>
<body>
<div id="networkStatus" class="network-status">📡 Offline</div>
<div class="container">
<div class="icon">🧙‍♂️</div>
<h1>You're Offline</h1>
<p>Your magical connection has been temporarily disrupted, but your grimoire is still accessible!</p>
<div style="margin-bottom: 2rem;">
<button class="button" onclick="tryReconnect()">🔄 Try Reconnecting</button>
<a href="/" class="button secondary">📖 Open Grimoire</a>
</div>
<div class="status">
<strong>🛡️ Offline Mode Active</strong><br>
Your data is safely stored locally and will sync when connection is restored.
</div>
<div class="features">
<h3>✨ What You Can Still Do</h3>
<ul>
<li>View your existing habits and progress</li>
<li>Mark habits as complete (will sync later)</li>
<li>Browse cached analytics and insights</li>
<li>Access previously loaded content</li>
<li>Create new habits (will sync when online)</li>
</ul>
</div>
</div>
<script>
// Check network status
function updateNetworkStatus() {
const statusEl = document.getElementById('networkStatus');
if (navigator.onLine) {
statusEl.textContent = '🌐 Back Online!';
statusEl.className = 'network-status online';
setTimeout(() => {
window.location.href = '/';
}, 2000);
} else {
statusEl.textContent = '📡 Offline';
statusEl.className = 'network-status';
}
}
// Try to reconnect
function tryReconnect() {
const button = event.target;
button.textContent = '🔄 Checking...';
button.disabled = true;
// Simple connectivity test
fetch('/', {
method: 'HEAD',
cache: 'no-cache'
})
.then(() => {
button.textContent = '✅ Connected!';
setTimeout(() => {
window.location.href = '/';
}, 1000);
})
.catch(() => {
button.textContent = '❌ Still Offline';
setTimeout(() => {
button.textContent = '🔄 Try Reconnecting';
button.disabled = false;
}, 2000);
});
}
// Listen for network changes
window.addEventListener('online', updateNetworkStatus);
window.addEventListener('offline', updateNetworkStatus);
// Initial status check
updateNetworkStatus();
// Auto-retry connection every 30 seconds
setInterval(() => {
if (!navigator.onLine) {
fetch('/', {
method: 'HEAD',
cache: 'no-cache'
})
.then(() => {
updateNetworkStatus();
})
.catch(() => {
// Still offline
});
}
}, 30000);
</script>
</body>
</html>
+407
View File
@@ -0,0 +1,407 @@
const CACHE_NAME = 'wizards-grimoire-v1.0.0';
const OFFLINE_URL = '/offline.html';
const API_CACHE_NAME = 'api-cache-v1';
// Resources to cache immediately
const STATIC_CACHE_URLS = [
'/',
'/static/js/bundle.js',
'/static/css/main.css',
'/manifest.json',
'/icon-192x192.png',
'/icon-512x512.png',
OFFLINE_URL
];
// API endpoints to cache
const API_CACHE_PATTERNS = [
/\/api\/v1\/habits$/,
/\/api\/v1\/user\/profile$/,
/\/api\/v1\/analytics/
];
// Install event - cache static resources
self.addEventListener('install', (event) => {
console.log('Service Worker: Installing...');
event.waitUntil(
(async () => {
try {
const cache = await caches.open(CACHE_NAME);
console.log('Service Worker: Caching static resources');
await cache.addAll(STATIC_CACHE_URLS);
// Force activation of the new service worker
await self.skipWaiting();
} catch (error) {
console.error('Service Worker: Failed to cache static resources', error);
}
})()
);
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('Service Worker: Activating...');
event.waitUntil(
(async () => {
try {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames
.filter(cacheName =>
cacheName !== CACHE_NAME &&
cacheName !== API_CACHE_NAME
)
.map(cacheName => {
console.log('Service Worker: Deleting old cache', cacheName);
return caches.delete(cacheName);
})
);
// Take control of all clients
await self.clients.claim();
} catch (error) {
console.error('Service Worker: Failed to activate', error);
}
})()
);
});
// Fetch event - serve cached content when offline
self.addEventListener('fetch', (event) => {
// Skip non-GET requests for modification requests
const url = new URL(event.request.url);
// Skip chrome-extension requests
if (event.request.url.startsWith('chrome-extension://')) return;
event.respondWith(
(async () => {
try {
// Handle API requests
if (url.pathname.startsWith('/api/')) {
return await handleApiRequest(event.request);
}
// Handle navigation requests
if (event.request.mode === 'navigate') {
return await handleNavigationRequest(event.request);
}
// Handle static resource requests
return await handleStaticRequest(event.request);
} catch (error) {
console.error('Service Worker: Fetch error', error);
return await handleFallback(event.request);
}
})()
);
});
// Handle API requests with cache-first strategy for GET requests
async function handleApiRequest(request) {
const url = new URL(request.url);
const shouldCache = API_CACHE_PATTERNS.some(pattern => pattern.test(url.pathname));
if (shouldCache && request.method === 'GET') {
try {
// Try cache first for API requests
const cachedResponse = await caches.match(request);
if (cachedResponse) {
// Return cached response and update in background
updateApiCache(request);
return cachedResponse;
}
// Fetch from network and cache
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(API_CACHE_NAME);
cache.put(request, response.clone());
}
return response;
} catch (error) {
// Return cached version if network fails
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
throw error;
}
}
// For non-cached API requests, try network first
try {
const response = await fetch(request);
// If it's a POST/PUT/DELETE request that modifies data, store it for sync
if (['POST', 'PUT', 'DELETE'].includes(request.method)) {
await storeOfflineAction(request);
}
return response;
} catch (error) {
// Store the action for later sync
if (['POST', 'PUT', 'DELETE'].includes(request.method)) {
await storeOfflineAction(request);
return new Response(JSON.stringify({ success: true, offline: true }), {
headers: { 'Content-Type': 'application/json' }
});
}
throw error;
}
}
// Handle navigation requests
async function handleNavigationRequest(request) {
try {
const response = await fetch(request);
return response;
} catch (error) {
// Return cached version or offline page
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
const offlineResponse = await caches.match(OFFLINE_URL);
return offlineResponse || new Response('Offline', { status: 200 });
}
}
// Handle static resource requests
async function handleStaticRequest(request) {
// Try cache first for static resources
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// If not in cache, fetch from network
try {
const response = await fetch(request);
// Cache successful responses
if (response.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
}
return response;
} catch (error) {
throw error;
}
}
// Fallback handler
async function handleFallback(request) {
if (request.mode === 'navigate') {
const offlineResponse = await caches.match(OFFLINE_URL);
return offlineResponse || new Response('Offline', { status: 200 });
}
return new Response('Resource not available offline', { status: 503 });
}
// Update API cache in background
async function updateApiCache(request) {
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(API_CACHE_NAME);
await cache.put(request, response);
}
} catch (error) {
console.log('Background update failed:', error);
}
}
// Store offline actions for later sync
async function storeOfflineAction(request) {
try {
const action = {
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
body: await request.text(),
timestamp: Date.now()
};
const existingActions = await getStoredActions();
existingActions.push(action);
// Store in IndexedDB or localStorage fallback
const storage = await getOfflineStorage();
await storage.setItem('offline-actions', JSON.stringify(existingActions));
} catch (error) {
console.error('Failed to store offline action:', error);
}
}
// Get stored offline actions
async function getStoredActions() {
try {
const storage = await getOfflineStorage();
const actions = await storage.getItem('offline-actions');
return actions ? JSON.parse(actions) : [];
} catch (error) {
console.error('Failed to get stored actions:', error);
return [];
}
}
// Simple storage abstraction
async function getOfflineStorage() {
// Try to use IndexedDB, fallback to cache storage
return {
async getItem(key) {
const cache = await caches.open('offline-storage');
const response = await cache.match(`/${key}`);
return response ? await response.text() : null;
},
async setItem(key, value) {
const cache = await caches.open('offline-storage');
await cache.put(`/${key}`, new Response(value));
}
};
}
// Background sync event
self.addEventListener('sync', (event) => {
if (event.tag === 'background-sync') {
event.waitUntil(syncOfflineActions());
}
});
// Sync offline actions when back online
async function syncOfflineActions() {
try {
const actions = await getStoredActions();
const successfulSyncs = [];
for (const action of actions) {
try {
const request = new Request(action.url, {
method: action.method,
headers: action.headers,
body: action.body || undefined
});
const response = await fetch(request);
if (response.ok) {
successfulSyncs.push(action);
}
} catch (error) {
console.error('Failed to sync action:', error);
}
}
// Remove successfully synced actions
if (successfulSyncs.length > 0) {
const remainingActions = actions.filter(
action => !successfulSyncs.includes(action)
);
const storage = await getOfflineStorage();
await storage.setItem('offline-actions', JSON.stringify(remainingActions));
}
} catch (error) {
console.error('Background sync failed:', error);
}
}
// Push notification event
self.addEventListener('push', (event) => {
if (!event.data) return;
try {
const data = event.data.json();
const options = {
body: data.body || 'Time to practice your magical habits!',
icon: '/icon-192x192.png',
badge: '/icon-72x72.png',
image: data.image,
vibrate: [200, 100, 200],
data: {
url: data.url || '/',
action: data.action || 'open'
},
actions: [
{
action: 'complete',
title: '✓ Mark Complete',
icon: '/icon-72x72.png'
},
{
action: 'view',
title: '👁 View Details',
icon: '/icon-72x72.png'
}
],
requireInteraction: true,
tag: data.tag || 'habit-reminder'
};
event.waitUntil(
self.registration.showNotification(
data.title || '🧙‍♂️ Grimoire Reminder',
options
)
);
} catch (error) {
console.error('Push notification error:', error);
}
});
// Notification click event
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const action = event.action;
const data = event.notification.data;
if (action === 'complete') {
// Handle habit completion
event.waitUntil(handleHabitCompletion(data));
} else {
// Open the app
event.waitUntil(
clients.openWindow(data.url || '/')
);
}
});
// Handle habit completion from notification
async function handleHabitCompletion(data) {
try {
if (data.habitId) {
// Store completion for sync
await storeOfflineAction(new Request(`/api/v1/habits/${data.habitId}/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completedAt: new Date().toISOString() })
}));
// Show success notification
await self.registration.showNotification('✅ Habit Completed!', {
body: 'Great job! Your progress has been recorded.',
icon: '/icon-192x192.png',
tag: 'completion-success'
});
}
} catch (error) {
console.error('Failed to complete habit:', error);
}
}
console.log('Service Worker: Loaded');