From 7a34f5f9901d28bb8df72ea209b998856f4b8726 Mon Sep 17 00:00:00 2001 From: dvirlabs <114520947+dvirlabs@users.noreply.github.com> Date: Fri, 19 Dec 2025 00:00:15 +0200 Subject: [PATCH] Adapt to mobile --- backend/__pycache__/main.cpython-312.pyc | Bin 0 -> 21953 bytes backend/main.py | 4 +- frontend/PWA_SETUP.md | 145 ++++++++++ frontend/generate-icons.html | 132 +++++++++ frontend/index.html | 21 +- frontend/public/icon.svg | 10 + frontend/public/manifest.json | 32 +++ frontend/public/service-worker.js | 53 ++++ frontend/src/App.css | 325 ++++++++++++++++++++++- frontend/src/App.jsx | 61 ++++- frontend/src/Auth.css | 82 +++++- frontend/src/Auth.jsx | 2 +- frontend/src/main.jsx | 13 + 13 files changed, 861 insertions(+), 19 deletions(-) create mode 100644 backend/__pycache__/main.cpython-312.pyc create mode 100644 frontend/PWA_SETUP.md create mode 100644 frontend/generate-icons.html create mode 100644 frontend/public/icon.svg create mode 100644 frontend/public/manifest.json create mode 100644 frontend/public/service-worker.js diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67fafe725c8f02f902fc363ea250918948e765a7 GIT binary patch literal 21953 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%0AX(o0fe;?+Q zgP*8Q6D{%Ef9SH+&|C5nZa&qW3@gn%uOQiEoLyG3#Z|3Pv;GXPM`cD=X8dtXbwdDWu)~;EMX4-0 zHDcFVYZM>A784j;!+@{{t#9mB?{HFi1WXEvOUEN*CloRd`FafD7zq1}#Fcr5E6ex9 z{~eURgKXoUA)yz!pi09L0Xc+P#joo7LFH$a;79#G@U6b+;!>`_czq~j(a+^6ycwB_ zEc(h)z6R0Puvl2SSWtYcYNl!_-%eayzL#)uDXi*B(zQ9kY|gm2h&Wxz#kF3~vi|7e z(%mZkw{>yFkt)d(F<@y5!x*;&RhA4+F#}hYQWIE^HA(Ee-dABVDKFb zG>+krwTi#T++5W{W+ii2a0i3`1c8y~OYxH-sqhb=(iLPJPavUZ9RJ$-#!I-n8Z>to zGtJ$8>q~n*y1N?SdPix^t>{ej!=Z`J8z-MAcQ+I_cpPrf8LKlnnhfe;fAn_&5bzLv zWlb9-x~kET1|-l9C129tm6zl1(h-q?V{2vl(HvWU3)9g%w&1fyyw*V+Tl{~83i#oQ z@xkN2vSyvZ&q?;i$roH%_|p^kV-UgMm>uJtq2b{%8~pGG{tNE~UKcP+4F1Z8#0@(ksWemAGN}Gq3H%ZWFYu^Jz!#mPYzV*H z;qg?He9tdgd?P_%b|8yszk@q>IGmP}x976S6iCZUV)8z?-o zXdchMWjBNw79!=7pBRB3>QH`j!~-{xgW8nj3u(>RXcGq8F(4;$2QhUR1H%1IW9keB z0SwM#FbsiY2IGl*(1PHBFMs|5OKxI-*LBF<33BR8&gLZZ@JO6TVZjsJ`5~szVL>b} zqDK*)NYI7%Ljim>|2}*l&kspveq>1ZwcpqF-STgRyvBbCbpgF7p$4E2Jc_1&OYKTh zyM9A${Vmn_TdMXqRMl^&x?fYSUsIm{PBr|Rs{AKw-t@7#(go|52{YU!qU|>vPm0zj znvWz3yC(8MZKIhKRVY%0i%j8SQOPuXb6O$C_>y)}bkP!JrAW?l_5G|FM>Bi}^Sif<}X@>eF?O$@bGCBCq9r#&*URbpw40`P+&++|Aeue!9HX?=Nte&s=ZZedOH+`X-%g_Ya9a@G z(q)v%^4-#FrISV1E1#NZded_=W%8sk1vi+Xtn6CZ`XKG4mY*sQ4)?diDtR4%m@ibO=9z2?0w&(*AG5me>N_@doj@) zPQDv?O1bH6w_DQ~&0V=~zH^l%6W@5}8D?Ss`{kk147{f+6)-d<>RTTaA^Z;FD7X#H zu(k)MFstj)$+r?`dOjZ)FYyUsGS-d+oSzQ9{PUKEM9=|qsa?{ zPbn|$#i1aYZ@S<5BiJlRCQiTm3}YOEtZ_@4fmd~LlYkJj<8Tia_oh7yALe$8Ycoi) zZo@ARB~HEl`EhX|mKc6NIdB=*wQKsyta*kkWY_&84^I8`1g>dU;sS?j+Jyt|#9&*R zfmeA+Kt_NYbH{No1^ECq=G~aF=h4o+4}3ps7SEhdTws%DIK+&N)379n=9=bP@4%8E z`4G?#BJ5kiK6uT-i`m`x2@dR2(PDNygD8*o9e&9CqEbB1Cb&z<^U0H##j?vdb1rc{lst1$ 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); + }); + }); +}