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
This commit is contained in:
Senior Engineer
2026-04-28 01:48:48 -04:00
committed by Michael Freno
parent b6d1f4c3b6
commit 88f0239ab7
5 changed files with 709 additions and 7 deletions

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#518ac8" /> <meta name="theme-color" content="#0a0a0a" />
<meta name="description" content="Scripter — Write Faster. The modern screenwriting platform built for how you actually work." /> <meta name="description" content="Scripter — Write Faster. The modern screenwriting platform built for how you actually work." />
<meta name="keywords" content="screenwriting, screenplay, writing software, Final Draft alternative, collaboration" /> <meta name="keywords" content="screenwriting, screenplay, writing software, Final Draft alternative, collaboration" />
<meta property="og:title" content="Scripter — Write Faster" /> <meta property="og:title" content="Scripter — Write Faster" />
@@ -11,6 +11,7 @@
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<link rel="icon" type="image/png" href="/src-tauri/32x32.png" /> <link rel="icon" type="image/png" href="/src-tauri/32x32.png" />
<link rel="apple-touch-icon" href="/src-tauri/128x128.png" /> <link rel="apple-touch-icon" href="/src-tauri/128x128.png" />
<link rel="manifest" href="/manifest.json" />
<title>Scripter — Write Faster</title> <title>Scripter — Write Faster</title>
</head> </head>
<body> <body>

View File

@@ -1,21 +1,27 @@
{ {
"name": "Scripter", "name": "Scripter — Write Faster",
"short_name": "Scripter", "short_name": "Scripter",
"description": "Professional screenplay editor with real-time collaboration", "description": "Professional screenplay editor with real-time collaboration",
"start_url": "/", "start_url": "/app/dashboard",
"display": "standalone", "display": "standalone",
"background_color": "#1a1a2e", "background_color": "#0a0a0a",
"theme_color": "#1a1a2e", "theme_color": "#0a0a0a",
"orientation": "any", "orientation": "any",
"icons": [ "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", "sizes": "192x192",
"type": "image/png", "type": "image/png",
"purpose": "any maskable" "purpose": "any maskable"
}, },
{ {
"src": "/icon-512.png", "src": "/src-tauri/128x128.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png", "type": "image/png",
"purpose": "any maskable" "purpose": "any maskable"

65
public/sw.js Normal file
View File

@@ -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();
}
});

View File

@@ -4,6 +4,14 @@ import { ClerkProvider } from './lib/auth/clerk-provider';
import { routes } from './routes'; import { routes } from './routes';
import './index.css'; 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( render(
() => ( () => (
<ClerkProvider> <ClerkProvider>

View File

@@ -101,3 +101,625 @@ input:focus, textarea:focus, select:focus {
--sidebar-width: 0px; --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;
}
}