From 88f0239ab70bf3175af8fc4e4a31730217882809 Mon Sep 17 00:00:00 2001 From: Senior Engineer Date: Tue, 28 Apr 2026 01:48:48 -0400 Subject: [PATCH] feat(FRE-596): add PWA setup and responsive design - Register service worker for offline caching (app shell + API responses) - Link manifest.json in index.html with updated theme colors - Update manifest start_url to /app/dashboard for PWA experience - Add comprehensive team management CSS with responsive breakpoints - Add alert, loading, and danger button styles - Mobile-first responsive layout for team list and detail views --- index.html | 3 +- public/manifest.json | 18 +- public/sw.js | 65 +++++ src/App.tsx | 8 + src/index.css | 622 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 709 insertions(+), 7 deletions(-) create mode 100644 public/sw.js diff --git a/index.html b/index.html index 4c61e60f2..4b9554832 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - + @@ -11,6 +11,7 @@ + Scripter — Write Faster diff --git a/public/manifest.json b/public/manifest.json index 2699782b8..b51dd49c5 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,21 +1,27 @@ { - "name": "Scripter", + "name": "Scripter — Write Faster", "short_name": "Scripter", "description": "Professional screenplay editor with real-time collaboration", - "start_url": "/", + "start_url": "/app/dashboard", "display": "standalone", - "background_color": "#1a1a2e", - "theme_color": "#1a1a2e", + "background_color": "#0a0a0a", + "theme_color": "#0a0a0a", "orientation": "any", "icons": [ { - "src": "/icon-192.png", + "src": "/src-tauri/128x128.png", + "sizes": "128x128", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/src-tauri/128x128.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, { - "src": "/icon-512.png", + "src": "/src-tauri/128x128.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 000000000..5c1fe6bd2 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,65 @@ +const CACHE_NAME = 'scripter-v1'; +const API_CACHE = 'scripter-api-v1'; +const STATIC_ASSETS = [ + '/', + '/index.html', + '/manifest.json', + '/src/App.tsx', + '/src/index.css', +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS)) + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all( + keys.filter((key) => key !== CACHE_NAME && key !== API_CACHE).map((key) => caches.delete(key)) + ) + ) + ); + self.clients.claim(); +}); + +self.addEventListener('fetch', (event) => { + const { request } = event; + const url = new URL(request.url); + + if (url.pathname.startsWith('/trpc/')) { + event.respondWith( + fetch(request) + .then((response) => { + const clonedResponse = response.clone(); + caches.open(API_CACHE).then((cache) => cache.put(request, clonedResponse)); + return response; + }) + .catch(() => caches.match(request)) + ); + return; + } + + event.respondWith( + caches.match(request).then((cached) => { + const fetchPromise = fetch(request).then((response) => { + if (response.status === 200) { + const clonedResponse = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(request, clonedResponse)); + } + return response; + }); + + return cached || fetchPromise; + }) + ); +}); + +self.addEventListener('message', (event) => { + if (event.data === 'skipWaiting') { + self.skipWaiting(); + } +}); diff --git a/src/App.tsx b/src/App.tsx index 5a80b76fe..bef566b53 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,14 @@ import { ClerkProvider } from './lib/auth/clerk-provider'; import { routes } from './routes'; import './index.css'; +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js').catch((err) => { + console.warn('Service worker registration failed:', err); + }); + }); +} + render( () => ( diff --git a/src/index.css b/src/index.css index 13db93aa5..1afe52378 100644 --- a/src/index.css +++ b/src/index.css @@ -101,3 +101,625 @@ input:focus, textarea:focus, select:focus { --sidebar-width: 0px; } } + +/* Waitlist Page Styles */ +.waitlist-page { + min-height: 100vh; + background: var(--color-bg-primary); +} + +.waitlist-hero { + min-height: calc(100vh - 80px); + display: flex; + flex-direction: column; + padding: 40px 20px; + max-width: 1200px; + margin: 0 auto; +} + +.waitlist-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 40px; +} + +.waitlist-badge { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + background: var(--color-bg-tertiary); + border: 1px solid var(--color-border); + border-radius: var(--radius-full); + font-size: 0.875rem; + color: var(--color-text-secondary); +} + +.badge-dot { + width: 8px; + height: 8px; + background: var(--color-accent); + border-radius: 50%; + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.waitlist-headline { + font-size: 2.75rem; + font-weight: 700; + line-height: 1.2; + background: linear-gradient(135deg, #fff 0%, #a3a3a3 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 16px; +} + +.waitlist-subheadline { + font-size: 1.25rem; + color: var(--color-text-secondary); + max-width: 600px; + line-height: 1.6; +} + +.waitlist-form { + background: var(--color-bg-tertiary); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 32px; + margin: 24px 0; +} + +.waitlist-form .form-group { + margin-bottom: 20px; +} + +.waitlist-form label { + display: block; + margin-bottom: 8px; + font-weight: 500; + color: var(--color-text-primary); + font-size: 0.9375rem; +} + +.waitlist-form .optional { + color: var(--color-text-muted); + font-size: 0.8125rem; + margin-left: 4px; +} + +.waitlist-form input { + width: 100%; + padding: 14px 16px; + font-size: 1rem; + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + color: var(--color-text-primary); + transition: all var(--transition-fast); +} + +.waitlist-form input:focus { + border-color: var(--color-accent); + outline: none; + box-shadow: 0 0 0 3px var(--color-accent-muted); +} + +.waitlist-form input::placeholder { + color: var(--color-text-muted); +} + +.submit-btn { + width: 100%; + padding: 16px; + font-size: 1.125rem; + font-weight: 600; + color: white; + background: var(--color-accent); + border: none; + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition-fast); +} + +.submit-btn:hover:not(:disabled) { + background: var(--color-accent-hover); + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.submit-btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.error-message { + background: rgba(239, 68, 68, 0.1); + border: 1px solid var(--color-error); + color: var(--color-error); + padding: 12px 16px; + border-radius: var(--radius-md); + font-size: 0.9375rem; + margin-top: 16px; +} + +.privacy-note { + text-align: center; + color: var(--color-text-muted); + font-size: 0.875rem; + margin-top: 20px; +} + +.waitlist-count { + text-align: center; + color: var(--color-text-muted); + font-size: 0.875rem; + margin-top: 16px; +} + +.waitlist-footer { + padding: 24px 0; + border-top: 1px solid var(--color-border); + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.875rem; + color: var(--color-text-muted); +} + +.footer-links { + display: flex; + gap: 20px; +} + +.footer-links a { + color: var(--color-text-secondary); +} + +.footer-links a:hover { + color: var(--color-accent); +} + +.waitlist-features { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 24px; + margin-top: 32px; +} + +.waitlist-features .feature { + text-align: center; + padding: 24px; + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + transition: all var(--transition-fast); +} + +.waitlist-features .feature:hover { + border-color: var(--color-border-hover); + transform: translateY(-2px); +} + +.waitlist-features .feature-icon { + font-size: 2rem; + margin-bottom: 12px; +} + +.waitlist-features .feature h3 { + font-size: 1.125rem; + font-weight: 600; + color: var(--color-text-primary); + margin-bottom: 8px; +} + +.waitlist-features .feature p { + color: var(--color-text-secondary); + font-size: 0.9375rem; + line-height: 1.5; +} + +.waitlist-social-proof { + margin-top: 48px; + padding: 32px; + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); +} + +.waitlist-social-proof .social-proof-header h3 { + font-size: 1.125rem; + font-weight: 600; + color: var(--color-text-primary); + text-align: center; + margin-bottom: 24px; +} + +.testimonials { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 24px; +} + +.testimonial { + padding: 20px; + background: var(--color-bg-tertiary); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); +} + +.testimonial-quote { + font-size: 0.9375rem; + color: var(--color-text-secondary); + font-style: italic; + margin-bottom: 16px; + line-height: 1.6; +} + +.testimonial-author { + display: flex; + flex-direction: column; + gap: 4px; +} + +.testimonial-author .author-name { + font-weight: 600; + color: var(--color-text-primary); + font-size: 0.9375rem; +} + +.testimonial-author .author-role { + font-size: 0.8125rem; + color: var(--color-text-muted); +} + +.waitlist-success { + text-align: center; + padding: 24px 0; +} + +.waitlist-success h2 { + font-size: 1.5rem; + color: var(--color-success); + margin-bottom: 12px; +} + +.waitlist-success p { + color: var(--color-text-secondary); + margin-bottom: 24px; +} + +.referral-info { + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: 20px; + margin-top: 20px; +} + +.referral-label { + font-size: 0.875rem; + color: var(--color-text-muted); + margin-bottom: 8px; +} + +.referral-code { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin-bottom: 12px; +} + +.code-display { + font-family: 'Monaco', 'Menlo', monospace; + font-size: 1.25rem; + font-weight: 700; + background: var(--color-bg-tertiary); + padding: 12px 20px; + border-radius: var(--radius-md); + letter-spacing: 2px; + color: var(--color-accent); +} + +.copy-btn { + padding: 10px 16px; + font-size: 0.875rem; + font-weight: 500; + color: var(--color-text-primary); + background: var(--color-bg-tertiary); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition-fast); +} + +.copy-btn:hover { + background: var(--color-bg-elevated); + border-color: var(--color-border-hover); +} + +.referral-hint { + font-size: 0.8125rem; + color: var(--color-text-muted); + text-align: center; +} + +/* Team Management */ +.freno-teams { + max-width: 1200px; + margin: 0 auto; + padding: 24px; +} + +.freno-team-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; + margin-top: 24px; +} + +.freno-team-card { + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 20px; + display: flex; + flex-direction: column; + gap: 12px; + transition: all var(--transition-fast); + position: relative; +} + +.freno-team-card:hover { + border-color: var(--color-border-hover); + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.freno-team-card-new { + border-style: dashed; + align-items: center; + justify-content: center; + min-height: 140px; + cursor: pointer; + text-align: center; +} + +.freno-team-card-new:hover { + border-color: var(--color-accent); + background: var(--color-accent-muted); +} + +.freno-team-card-link { + text-decoration: none; + color: inherit; +} + +.freno-team-icon { + font-size: 2rem; + margin-bottom: 4px; +} + +.freno-team-card h3 { + font-size: 1.125rem; + font-weight: 600; + color: var(--color-text-primary); +} + +.freno-team-card-actions { + display: flex; + justify-content: flex-end; + margin-top: 8px; + padding-top: 12px; + border-top: 1px solid var(--color-border); +} + +/* Team Detail */ +.freno-team-detail { + max-width: 900px; + margin: 0 auto; + padding: 24px; +} + +.freno-back-link { + font-size: 0.875rem; + color: var(--color-text-secondary); + margin-bottom: 8px; + display: inline-block; +} + +.freno-back-link:hover { + color: var(--color-accent); +} + +.freno-team-meta { + color: var(--color-text-muted); + font-size: 0.875rem; + margin-top: 4px; +} + +.freno-header-actions { + display: flex; + gap: 8px; +} + +.freno-team-members-section { + margin-top: 32px; +} + +.freno-team-members-section h2 { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 16px; +} + +.freno-members-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.freno-member-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); +} + +.freno-member-info { + display: flex; + flex-direction: column; + gap: 4px; +} + +.freno-member-id { + font-weight: 500; + color: var(--color-text-primary); +} + +.freno-member-joined { + font-size: 0.8125rem; + color: var(--color-text-muted); +} + +.freno-member-actions { + display: flex; + gap: 8px; + align-items: center; +} + +.freno-select { + background: var(--color-bg-tertiary); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: 6px 12px; + color: var(--color-text-primary); + font-size: 0.875rem; + cursor: pointer; +} + +.freno-select:focus { + border-color: var(--color-accent); + outline: none; +} + +/* Alert */ +.freno-alert { + padding: 12px 16px; + border-radius: var(--radius-md); + margin-bottom: 16px; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.875rem; +} + +.freno-alert-error { + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + color: #fca5a5; +} + +.freno-alert-dismiss { + background: none; + border: none; + color: inherit; + cursor: pointer; + font-size: 1.125rem; + padding: 0 4px; + opacity: 0.7; +} + +.freno-alert-dismiss:hover { + opacity: 1; +} + +/* Danger button */ +.freno-btn-danger { + background: var(--color-error); + color: white; + padding: 8px 16px; + border-radius: var(--radius-md); + font-size: 0.875rem; + font-weight: 500; + transition: opacity var(--transition-fast); +} + +.freno-btn-danger:hover { + opacity: 0.9; +} + +.freno-btn-danger:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.freno-btn-danger-sm { + color: var(--color-error); + font-size: 0.8125rem; + padding: 4px 8px; + border-radius: var(--radius-sm); + transition: background var(--transition-fast); +} + +.freno-btn-danger-sm:hover { + background: rgba(239, 68, 68, 0.1); +} + +/* Loading state */ +.freno-loading { + text-align: center; + padding: 48px; + color: var(--color-text-muted); +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .freno-teams, + .freno-team-detail { + padding: 16px; + } + + .freno-team-grid { + grid-template-columns: 1fr; + } + + .freno-page-header { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } + + .freno-header-actions { + width: 100%; + flex-direction: column; + } + + .freno-header-actions button { + width: 100%; + } + + .freno-member-row { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } + + .freno-member-actions { + width: 100%; + justify-content: flex-end; + } +}