Adapt to mobile
This commit is contained in:
parent
8094192d7a
commit
7a34f5f990
BIN
backend/__pycache__/main.cpython-312.pyc
Normal file
BIN
backend/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
@ -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)
|
||||
|
||||
145
frontend/PWA_SETUP.md
Normal file
145
frontend/PWA_SETUP.md
Normal file
@ -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! 🎉
|
||||
132
frontend/generate-icons.html
Normal file
132
frontend/generate-icons.html
Normal file
@ -0,0 +1,132 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Tasko Icon Generator</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 { color: #667eea; }
|
||||
.instructions {
|
||||
background: #f0f0f0;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
canvas {
|
||||
border: 2px solid #ddd;
|
||||
margin: 10px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
button {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
button:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
.icons-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.icon-item {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>✓ Tasko Icon Generator</h1>
|
||||
|
||||
<div class="instructions">
|
||||
<h3>Instructions:</h3>
|
||||
<ol>
|
||||
<li>Click "Generate Icons" button below</li>
|
||||
<li>Right-click each icon and "Save image as..."</li>
|
||||
<li>Save them in the <code>frontend/public/</code> folder with the exact names shown</li>
|
||||
<li>Restart your dev server</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<button onclick="generateIcons()">Generate Icons</button>
|
||||
|
||||
<div class="icons-grid" id="iconsGrid"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
|
||||
|
||||
function generateIcons() {
|
||||
const grid = document.getElementById('iconsGrid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
sizes.forEach(size => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Create gradient background
|
||||
const gradient = ctx.createLinearGradient(0, 0, size, size);
|
||||
gradient.addColorStop(0, '#667eea');
|
||||
gradient.addColorStop(1, '#764ba2');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
// Add rounded corners
|
||||
ctx.globalCompositeOperation = 'destination-in';
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(0, 0, size, size, size * 0.2);
|
||||
ctx.fill();
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
|
||||
// Draw checkmark
|
||||
ctx.strokeStyle = 'white';
|
||||
ctx.lineWidth = size * 0.1;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(size * 0.25, size * 0.5);
|
||||
ctx.lineTo(size * 0.4, size * 0.65);
|
||||
ctx.lineTo(size * 0.75, size * 0.35);
|
||||
ctx.stroke();
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'icon-item';
|
||||
item.innerHTML = `
|
||||
<canvas width="${size}" height="${size}" style="width: ${Math.min(size, 150)}px; height: ${Math.min(size, 150)}px;"></canvas>
|
||||
<p><strong>${size}x${size}</strong></p>
|
||||
<p><small>icon-${size}x${size}.png</small></p>
|
||||
`;
|
||||
|
||||
const itemCanvas = item.querySelector('canvas');
|
||||
itemCanvas.getContext('2d').drawImage(canvas, 0, 0);
|
||||
|
||||
grid.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-generate on load
|
||||
window.onload = generateIcons;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -2,9 +2,24 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes" />
|
||||
<meta name="theme-color" content="#667eea" />
|
||||
<meta name="description" content="Modern task management app" />
|
||||
|
||||
<!-- PWA Configuration -->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="apple-touch-icon" href="/icon.svg" />
|
||||
|
||||
<!-- iOS Specific -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Tasko" />
|
||||
|
||||
<!-- Android -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
|
||||
<title>✓ Tasko - Task Manager</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
10
frontend/public/icon.svg
Normal file
10
frontend/public/icon.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="192" height="192" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="192" height="192" rx="38" fill="url(#grad)"/>
|
||||
<path d="M 48 96 L 77 125 L 144 58" stroke="white" stroke-width="20" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 514 B |
32
frontend/public/manifest.json
Normal file
32
frontend/public/manifest.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
53
frontend/public/service-worker.js
Normal file
53
frontend/public/service-worker.js
Normal file
@ -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);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
@ -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 */
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<div className={`app ${darkMode ? 'dark-mode' : ''}`}>
|
||||
<div className="sidebar">
|
||||
{/* Mobile Menu Toggle */}
|
||||
<button
|
||||
className="mobile-menu-toggle"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
{mobileMenuOpen ? '✕' : '☰'}
|
||||
</button>
|
||||
|
||||
{/* Mobile Overlay */}
|
||||
{mobileMenuOpen && (
|
||||
<div
|
||||
className="mobile-overlay"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={`sidebar ${mobileMenuOpen ? 'mobile-open' : ''}`}>
|
||||
<button
|
||||
className="mobile-close-btn"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
aria-label="Close menu"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<div className="sidebar-header">
|
||||
<div className="header-top">
|
||||
<h2 className="sidebar-title">✓ Tasko</h2>
|
||||
@ -298,7 +350,10 @@ function App() {
|
||||
<div
|
||||
key={list.id}
|
||||
className={`list-item ${selectedList?.id === list.id ? 'active' : ''}`}
|
||||
onClick={() => setSelectedList(list)}
|
||||
onClick={() => {
|
||||
setSelectedList(list)
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className="list-info">
|
||||
<span className="list-icon" style={{ color: list.color }}>{list.icon}</span>
|
||||
|
||||
@ -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 */
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -8,3 +8,16 @@ createRoot(document.getElementById('root')).render(
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user