diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..67fafe7 Binary files /dev/null and b/backend/__pycache__/main.cpython-312.pyc differ diff --git a/backend/main.py b/backend/main.py index bea5eae..c73c5e9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -20,7 +20,7 @@ allowed_origins = ["http://localhost:5173", "https://tasko.dvirlabs.com"] # Configure CORS app.add_middleware( CORSMiddleware, - allow_origins=allowed_origins, + allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -419,4 +419,4 @@ def delete_task(task_id: str, authorization: Optional[str] = Header(None), db: S if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/frontend/PWA_SETUP.md b/frontend/PWA_SETUP.md new file mode 100644 index 0000000..00a77cb --- /dev/null +++ b/frontend/PWA_SETUP.md @@ -0,0 +1,145 @@ +# PWA Installation Guide + +Your Tasko app is now a Progressive Web App (PWA)! Users can install it on their phones and use it like a native app. + +## What's Been Added + +### 1. **manifest.json** +- App name, icons, and theme colors +- Tells the browser how to install the app + +### 2. **Service Worker** +- Enables offline functionality +- Caches app resources for faster loading + +### 3. **Install Prompt** +- Beautiful bottom banner prompts users to install +- Shows on mobile devices automatically + +### 4. **App Icons** +- Multiple sizes for different devices (72px to 512px) + +## Setting Up Icons + +### Option 1: Use the Icon Generator (Easiest) +1. Open `frontend/generate-icons.html` in your browser +2. Icons will auto-generate +3. Right-click each icon and "Save image as..." +4. Save them in `frontend/public/` with exact filenames shown + +### Option 2: Create Custom Icons +1. Create PNG images in these sizes: + - 72x72, 96x96, 128x128, 144x144, 152x152, 192x192, 384x384, 512x512 +2. Name them as: `icon-{size}x{size}.png` +3. Place in `frontend/public/` folder + +### Option 3: Use an Online Tool +- [PWA Asset Generator](https://www.pwabuilder.com/) +- [RealFaviconGenerator](https://realfavicongenerator.net/) + +## Testing the PWA + +### On Mobile: +1. Build and deploy your app (or use npm run dev --host) +2. Open the app in Chrome/Safari on your phone +3. You'll see the install prompt at the bottom +4. Tap "Install" to add to home screen + +### On Desktop (Chrome): +1. Open DevTools (F12) +2. Go to Application > Manifest +3. Click "Add to home screen" to test + +### Testing Install Prompt: +```bash +# In Chrome DevTools Console: +localStorage.removeItem('installPromptDismissed') +# Then refresh the page +``` + +## Features Users Get + +✅ **Home Screen Icon** - App appears on phone like any other app +✅ **Splash Screen** - Beautiful loading screen with your branding +✅ **Standalone Mode** - Runs without browser UI (feels native) +✅ **Offline Support** - Works without internet (basic functionality) +✅ **Fast Loading** - Cached resources load instantly + +## How It Works + +### Install Flow: +1. User visits your app on mobile +2. After a few seconds, install banner appears +3. User taps "Install" +4. App is added to home screen +5. User can launch from home screen like a native app + +### Browser Support: +- ✅ Chrome/Edge (Android & Desktop) +- ✅ Safari (iOS 11.3+) +- ✅ Samsung Internet +- ✅ Firefox (Android) + +## Customization + +### Change App Name: +Edit `manifest.json`: +```json +{ + "name": "Your App Name", + "short_name": "ShortName" +} +``` + +### Change Colors: +Edit `manifest.json`: +```json +{ + "theme_color": "#667eea", + "background_color": "#667eea" +} +``` + +### Update Icons: +Replace the icon files in `public/` folder + +## Deployment + +### For Production: +1. Build your app: `npm run build` +2. Serve with HTTPS (required for PWA) +3. Icons and manifest will be included automatically + +### HTTPS Requirement: +PWAs require HTTPS (except localhost). Use: +- Vercel +- Netlify +- GitHub Pages +- Any hosting with SSL certificate + +## Troubleshooting + +### Install prompt not showing? +- Clear browser cache +- Remove: `localStorage.removeItem('installPromptDismissed')` +- Make sure you're on HTTPS (or localhost) +- Some browsers show it after 2-3 visits + +### Icons not appearing? +- Check file names match exactly: `icon-192x192.png` +- Verify they're in `public/` folder +- Clear cache and rebuild + +### Service worker not registering? +- Check browser console for errors +- Make sure `service-worker.js` is in `public/` folder +- Try hard refresh (Ctrl+Shift+R) + +## Next Steps + +1. Generate/add your app icons +2. Test on a real mobile device +3. Deploy to a hosting service with HTTPS +4. Share the link with users! + +Users will now see an install banner and can add your app to their home screen! 🎉 diff --git a/frontend/generate-icons.html b/frontend/generate-icons.html new file mode 100644 index 0000000..b50ab8e --- /dev/null +++ b/frontend/generate-icons.html @@ -0,0 +1,132 @@ + + + + Tasko Icon Generator + + + +
+

✓ Tasko Icon Generator

+ +
+

Instructions:

+
    +
  1. Click "Generate Icons" button below
  2. +
  3. Right-click each icon and "Save image as..."
  4. +
  5. Save them in the frontend/public/ folder with the exact names shown
  6. +
  7. Restart your dev server
  8. +
+
+ + + +
+
+ + + + diff --git a/frontend/index.html b/frontend/index.html index c20fbd3..bcc8cc2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,9 +2,24 @@ - - - frontend + + + + + + + + + + + + + + + + + + ✓ Tasko - Task Manager
diff --git a/frontend/public/icon.svg b/frontend/public/icon.svg new file mode 100644 index 0000000..ffc43ee --- /dev/null +++ b/frontend/public/icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json new file mode 100644 index 0000000..3abf8fd --- /dev/null +++ b/frontend/public/manifest.json @@ -0,0 +1,32 @@ +{ + "name": "Tasko - Task Manager", + "short_name": "Tasko", + "description": "Modern task management app for organizing your work and life", + "start_url": "/", + "display": "standalone", + "display_override": ["standalone", "fullscreen"], + "background_color": "#667eea", + "theme_color": "#667eea", + "orientation": "portrait-primary", + "scope": "/", + "icons": [ + { + "src": "/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any maskable" + }, + { + "src": "/icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] +} diff --git a/frontend/public/service-worker.js b/frontend/public/service-worker.js new file mode 100644 index 0000000..8ba906a --- /dev/null +++ b/frontend/public/service-worker.js @@ -0,0 +1,53 @@ +const CACHE_NAME = 'tasko-v1'; +const urlsToCache = [ + '/', + '/index.html', + '/src/main.jsx', + '/src/App.jsx', + '/src/App.css', + '/src/Auth.jsx', + '/src/Auth.css', + '/src/index.css' +]; + +// Install event - cache resources +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME) + .then((cache) => { + console.log('Opened cache'); + return cache.addAll(urlsToCache); + }) + ); +}); + +// Fetch event - serve from cache, fallback to network +self.addEventListener('fetch', (event) => { + event.respondWith( + caches.match(event.request) + .then((response) => { + // Cache hit - return response + if (response) { + return response; + } + return fetch(event.request); + } + ) + ); +}); + +// Activate event - clean up old caches +self.addEventListener('activate', (event) => { + const cacheWhitelist = [CACHE_NAME]; + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheWhitelist.indexOf(cacheName) === -1) { + return caches.delete(cacheName); + } + }) + ); + }) + ); +}); diff --git a/frontend/src/App.css b/frontend/src/App.css index 749f99c..35db3a5 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -863,33 +863,344 @@ background: #252538; } +/* Mobile Menu Toggle */ +.mobile-menu-toggle { + display: none; + position: fixed; + top: 1rem; + left: 1rem; + z-index: 1001; + width: 48px; + height: 48px; + border: none; + border-radius: 12px; + background: rgba(255, 255, 255, 0.95); + color: #667eea; + font-size: 1.5rem; + cursor: pointer; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transition: all 0.3s; +} + +.mobile-menu-toggle:hover { + background: white; + transform: scale(1.05); +} + +.mobile-menu-toggle:active { + transform: scale(0.95); +} + +/* Mobile Overlay */ +.mobile-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 999; +} + +/* Mobile Close Button */ +.mobile-close-btn { + display: none; + position: absolute; + top: 1rem; + right: 1rem; + width: 40px; + height: 40px; + border: none; + border-radius: 8px; + background: #f5f5f5; + color: #666; + font-size: 1.5rem; + cursor: pointer; + transition: all 0.3s; + z-index: 10; +} + +.mobile-close-btn:hover { + background: #e5e5e5; + transform: rotate(90deg); +} + +/* Dark mode mobile buttons */ +.dark-mode .mobile-menu-toggle { + background: rgba(42, 42, 62, 0.95); + color: #8b9bea; +} + +.dark-mode .mobile-menu-toggle:hover { + background: #2a2a3e; +} + +.dark-mode .mobile-close-btn { + background: #1a1a2e; + color: #b0b0c0; +} + +.dark-mode .mobile-close-btn:hover { + background: #252538; +} + @media (max-width: 768px) { .app { flex-direction: column; } + + /* Show mobile menu toggle */ + .mobile-menu-toggle { + display: flex; + align-items: center; + justify-content: center; + } + + /* Show mobile overlay when menu is open */ + .mobile-overlay { + display: block; + } + + /* Show mobile close button */ + .mobile-close-btn { + display: block; + } + /* Sidebar mobile styles */ .sidebar { - width: 100%; - max-height: 30vh; + position: fixed; + top: 0; + left: -100%; + width: 85%; + max-width: 320px; + height: 100vh; + z-index: 1000; + transition: left 0.3s ease; + padding-top: 4rem; } - + + .sidebar.mobile-open { + left: 0; + box-shadow: 4px 0 20px rgba(0, 0, 0, 0.3); + } + + /* Main content adjustments */ .main-content { - padding: 1.5rem; + width: 100%; + padding: 5rem 1rem 1rem; + margin-left: 0; } + .content-header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + .content-title { - font-size: 2rem; + font-size: 1.75rem; } .content-title span { - font-size: 2.5rem; + font-size: 2rem; } - + + /* Task form mobile */ .task-form { flex-direction: column; + gap: 0.75rem; + } + + .task-input { + font-size: 1rem; } .add-button { width: 100%; + padding: 0.875rem; + font-size: 1rem; + } + + /* Filter tabs mobile */ + .filter-tabs { + gap: 0.5rem; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + .filter-tabs button { + font-size: 0.875rem; + padding: 0.5rem 1rem; + white-space: nowrap; + } + + /* Tasks list mobile */ + .tasks-list { + gap: 0.75rem; + } + + .task-item { + padding: 1rem; + } + + .task-title { + font-size: 0.95rem; + } + + .delete-button { + width: 32px; + height: 32px; + font-size: 1.25rem; + } + + /* Task edit buttons mobile */ + .task-edit-buttons { + gap: 0.5rem; + } + + .task-edit-buttons button { + padding: 0.5rem 0.875rem; + font-size: 0.875rem; + } + + /* New list form mobile */ + .new-list-form { + padding: 1rem; + } + + .icon-grid { + grid-template-columns: repeat(6, 1fr); + gap: 0.5rem; + } + + .icon-grid button { + width: 40px; + height: 40px; + font-size: 1.25rem; + } + + /* Modal mobile */ + .modal-content { + width: 90%; + max-width: 360px; + padding: 1.5rem; + margin: 1rem; + } + + .modal-title { + font-size: 1.25rem; + } + + .modal-message { + font-size: 0.95rem; + } + + .modal-actions { + flex-direction: column-reverse; + gap: 0.75rem; + } + + .modal-actions button { + width: 100%; + padding: 0.875rem; + } + + /* Empty states mobile */ + .empty-state, + .empty-state-main { + font-size: 2rem; + padding: 2rem 1rem; + } + + /* User info mobile */ + .username { + font-size: 0.85rem; + } + + .logout-btn { + padding: 0.35rem 0.6rem; + font-size: 0.7rem; + } + + /* Sidebar header mobile */ + .sidebar-title { + font-size: 1.5rem; + } + + .theme-toggle { + width: 32px; + height: 32px; + font-size: 1rem; + } + + /* List items mobile */ + .list-item { + padding: 0.875rem; + } + + .list-icon { + font-size: 1.25rem; + } + + .list-name { + font-size: 0.95rem; + } + + .list-delete-btn { + width: 28px; + height: 28px; + font-size: 1.25rem; + } + + /* Add list button mobile */ + .add-list-button { + padding: 0.875rem; + font-size: 0.95rem; } } + +/* Small mobile devices */ +@media (max-width: 480px) { + .mobile-menu-toggle { + width: 44px; + height: 44px; + font-size: 1.35rem; + } + + .sidebar { + width: 90%; + } + + .content-title { + font-size: 1.5rem; + } + + .content-title span { + font-size: 1.75rem; + } + + .icon-grid { + grid-template-columns: repeat(5, 1fr); + } + + .modal-content { + width: 95%; + padding: 1.25rem; + } +} + +/* Landscape mobile */ +@media (max-width: 896px) and (orientation: landscape) { + .sidebar { + max-width: 280px; + } + + .main-content { + padding: 4rem 1.5rem 1rem; + } +} + +@media (max-width: 768px) { + /* Removed duplicate - styles are above */ +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e562e19..d96c2fd 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import Auth from './Auth' import './App.css' -const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000' +const API_URL = import.meta.env.VITE_API_URL || 'http://10.188.50.221:8000' const AVAILABLE_ICONS = [ '📝', '💼', '🏠', '🛒', '🎯', '💪', '📚', '✈️', @@ -27,6 +27,34 @@ function App() { const [editingTaskTitle, setEditingTaskTitle] = useState('') const [confirmModal, setConfirmModal] = useState({ show: false, message: '', onConfirm: null }) const [darkMode, setDarkMode] = useState(false) + const [mobileMenuOpen, setMobileMenuOpen] = useState(false) + const [deferredPrompt, setDeferredPrompt] = useState(null) + + useEffect(() => { + // Capture the install prompt event + const handleBeforeInstallPrompt = (e) => { + // Prevent Chrome 67 and earlier from automatically showing the prompt + e.preventDefault() + // Stash the event so it can be triggered later + setDeferredPrompt(e) + // Automatically show the prompt + setTimeout(() => { + e.prompt() + e.userChoice.then((choiceResult) => { + if (choiceResult.outcome === 'accepted') { + console.log('User accepted the install prompt') + } + setDeferredPrompt(null) + }) + }, 1000) + } + + window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt) + + return () => { + window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt) + } + }, []) useEffect(() => { // Check for stored token on mount @@ -276,7 +304,31 @@ function App() { return (
-
+ {/* Mobile Menu Toggle */} + + + {/* Mobile Overlay */} + {mobileMenuOpen && ( +
setMobileMenuOpen(false)} + /> + )} + +
+

✓ Tasko

@@ -298,7 +350,10 @@ function App() {
setSelectedList(list)} + onClick={() => { + setSelectedList(list) + setMobileMenuOpen(false) + }} >
{list.icon} diff --git a/frontend/src/Auth.css b/frontend/src/Auth.css index b37e6f7..b813435 100644 --- a/frontend/src/Auth.css +++ b/frontend/src/Auth.css @@ -131,12 +131,88 @@ cursor: not-allowed; } -@media (max-width: 640px) { +/* Mobile responsive styles */ +@media (max-width: 768px) { + .auth-container { + padding: 1rem; + } + .auth-box { - padding: 2rem; + padding: 2rem 1.5rem; + border-radius: 16px; } .auth-title { - font-size: 2.5rem; + font-size: 2.25rem; + } + + .auth-subtitle { + font-size: 0.95rem; + margin-bottom: 1.5rem; + } + + .auth-tabs { + margin-bottom: 1.5rem; + } + + .auth-tabs button { + padding: 0.65rem 0.875rem; + font-size: 0.95rem; + } + + .auth-form { + gap: 1.25rem; + } + + .form-group input { + padding: 0.875rem; + font-size: 1rem; + } + + .auth-submit { + padding: 0.875rem 1.5rem; + font-size: 1rem; + } + + .auth-error { + padding: 0.875rem; + font-size: 0.9rem; } } + +@media (max-width: 480px) { + .auth-box { + padding: 1.75rem 1.25rem; + } + + .auth-title { + font-size: 2rem; + } + + .auth-subtitle { + font-size: 0.9rem; + } + + .auth-tabs button { + padding: 0.6rem 0.75rem; + font-size: 0.9rem; + } + + .form-group label { + font-size: 0.85rem; + } + + .form-group input { + padding: 0.75rem; + font-size: 0.95rem; + } + + .auth-submit { + padding: 0.8rem 1.25rem; + font-size: 0.95rem; + } +} + +@media (max-width: 640px) { + /* Removed duplicate - styles are above */ +} diff --git a/frontend/src/Auth.jsx b/frontend/src/Auth.jsx index 971fec2..14360d7 100644 --- a/frontend/src/Auth.jsx +++ b/frontend/src/Auth.jsx @@ -1,7 +1,7 @@ import { useState } from 'react' import './Auth.css' -const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000' +const API_URL = import.meta.env.VITE_API_URL || 'http://10.188.50.221:8000' function Auth({ onLogin }) { const [isLogin, setIsLogin] = useState(true) diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index b9a1a6d..7e60064 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -8,3 +8,16 @@ createRoot(document.getElementById('root')).render( , ) + +// Register service worker for PWA +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/service-worker.js') + .then((registration) => { + console.log('SW registered: ', registration); + }) + .catch((error) => { + console.log('SW registration failed: ', error); + }); + }); +}