snice 3.5.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/snice.js +2 -1
- package/bin/templates/CLAUDE.md +24 -2
- package/bin/templates/pwa/README.md +173 -0
- package/bin/templates/pwa/global.d.ts +10 -0
- package/bin/templates/pwa/index.html +17 -0
- package/bin/templates/pwa/package.json +25 -0
- package/bin/templates/pwa/public/icons/.gitkeep +6 -0
- package/bin/templates/pwa/public/manifest.json +24 -0
- package/bin/templates/pwa/public/vite.svg +1 -0
- package/bin/templates/pwa/src/daemons/notifications.ts +148 -0
- package/bin/templates/pwa/src/guards/auth.ts +10 -0
- package/bin/templates/pwa/src/main.ts +42 -0
- package/bin/templates/pwa/src/middleware/auth.ts +15 -0
- package/bin/templates/pwa/src/middleware/error.ts +25 -0
- package/bin/templates/pwa/src/middleware/retry.ts +27 -0
- package/bin/templates/pwa/src/pages/dashboard.ts +142 -0
- package/bin/templates/pwa/src/pages/login.ts +161 -0
- package/bin/templates/pwa/src/pages/notifications.ts +156 -0
- package/bin/templates/pwa/src/pages/profile.ts +163 -0
- package/bin/templates/pwa/src/router.ts +16 -0
- package/bin/templates/pwa/src/services/auth.ts +48 -0
- package/bin/templates/pwa/src/services/jwt.ts +35 -0
- package/bin/templates/pwa/src/services/storage.ts +24 -0
- package/bin/templates/pwa/src/styles/global.css +55 -0
- package/bin/templates/pwa/src/types/auth.ts +21 -0
- package/bin/templates/pwa/src/types/notifications.ts +9 -0
- package/bin/templates/pwa/src/utils/fetch.ts +39 -0
- package/bin/templates/pwa/tsconfig.json +23 -0
- package/bin/templates/pwa/vite.config.ts +94 -0
- package/dist/components/audio-recorder/snice-audio-recorder.d.ts +14 -4
- package/dist/components/audio-recorder/snice-audio-recorder.js +248 -71
- package/dist/components/audio-recorder/snice-audio-recorder.js.map +1 -1
- package/dist/components/audio-recorder/snice-audio-recorder.types.d.ts +2 -0
- package/dist/components/music-player/snice-music-player.d.ts +72 -0
- package/dist/components/music-player/snice-music-player.js +730 -0
- package/dist/components/music-player/snice-music-player.js.map +1 -0
- package/dist/components/music-player/snice-music-player.types.d.ts +43 -0
- package/dist/components/timer/snice-timer.d.ts +27 -0
- package/dist/components/timer/snice-timer.js +197 -0
- package/dist/components/timer/snice-timer.js.map +1 -0
- package/dist/components/timer/snice-timer.types.d.ts +10 -0
- package/dist/fetcher.d.ts +65 -0
- package/dist/index.cjs +92 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +92 -4
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +92 -3
- package/dist/index.iife.js.map +1 -1
- package/dist/symbols.cjs +1 -1
- package/dist/symbols.esm.js +1 -1
- package/dist/transitions.cjs +1 -1
- package/dist/transitions.esm.js +1 -1
- package/dist/types/context.d.ts +7 -1
- package/dist/types/router-options.d.ts +6 -0
- package/docs/ai/api.md +33 -1
- package/docs/ai/components/music-player.md +134 -0
- package/docs/ai/components/terminal.md +147 -0
- package/docs/ai/components/timer.md +43 -0
- package/docs/ai/patterns.md +47 -0
- package/docs/components/music-player.md +314 -0
- package/docs/components/terminal.md +451 -0
- package/docs/components/timer.md +143 -0
- package/docs/fetcher.md +447 -0
- package/docs/routing.md +2 -0
- package/package.json +2 -1
package/bin/snice.js
CHANGED
|
@@ -47,6 +47,7 @@ Options:
|
|
|
47
47
|
Templates:
|
|
48
48
|
base - Minimal starter with counter example (default)
|
|
49
49
|
social - Social media sample app showcasing components
|
|
50
|
+
pwa - Progressive Web App with auth, middleware, and live notifications
|
|
50
51
|
|
|
51
52
|
Examples:
|
|
52
53
|
snice create-app my-app
|
|
@@ -90,7 +91,7 @@ function createApp(projectPath, template = 'base') {
|
|
|
90
91
|
// Check if template exists
|
|
91
92
|
if (!existsSync(templateDir)) {
|
|
92
93
|
console.error(`❌ Template "${template}" not found!`);
|
|
93
|
-
console.error(`Available templates: base, social`);
|
|
94
|
+
console.error(`Available templates: base, social, pwa`);
|
|
94
95
|
process.exit(1);
|
|
95
96
|
}
|
|
96
97
|
|
package/bin/templates/CLAUDE.md
CHANGED
|
@@ -17,13 +17,23 @@ src/
|
|
|
17
17
|
pages/ # @page decorated route components
|
|
18
18
|
components/ # @element decorated UI components
|
|
19
19
|
controllers/ # @controller decorated behavior modules
|
|
20
|
+
utils/ # Pure functions, helpers (formatDate, debounce)
|
|
21
|
+
services/ # Stateless static functions for business logic/external APIs
|
|
22
|
+
daemons/ # Stateful classes with lifecycle (start/stop/dispose)
|
|
23
|
+
middleware/ # Composable middleware (fetch, logging, etc)
|
|
24
|
+
guards/ # Route guards
|
|
25
|
+
types/ # TypeScript types
|
|
20
26
|
styles/ # Global CSS
|
|
21
27
|
```
|
|
22
28
|
|
|
23
29
|
**Separation of concerns:**
|
|
24
30
|
- **Pages** - Orchestrate elements, handle URLs
|
|
25
|
-
- **
|
|
26
|
-
- **Controllers** -
|
|
31
|
+
- **Components** - Pure presentation, no business logic
|
|
32
|
+
- **Controllers** - Attach to elements, add behavior
|
|
33
|
+
- **Utils** - Pure helper functions
|
|
34
|
+
- **Services** - Stateless business logic, API calls
|
|
35
|
+
- **Daemons** - Lifecycle-managed (WebSocket, P2P, intervals)
|
|
36
|
+
- **Middleware** - Composable functions (auth, logging, retry)
|
|
27
37
|
|
|
28
38
|
## Decorators
|
|
29
39
|
|
|
@@ -129,6 +139,18 @@ html`
|
|
|
129
139
|
- **Element ↔ Controller:** Request/Response (`@request`, `@respond`)
|
|
130
140
|
- **Global State:** Context (`@context()`)
|
|
131
141
|
|
|
142
|
+
## Common Mistakes
|
|
143
|
+
|
|
144
|
+
**Controllers are NOT global services.** They attach to elements via `controller="name"`. Use `utils/` for shared logic like API calls, auth, toasts.
|
|
145
|
+
|
|
146
|
+
**@request/@respond is NOT a service bus.** It's for element-to-element or element-to-controller communication only. Use utility functions for app-wide features.
|
|
147
|
+
|
|
148
|
+
**Guards receive Context<T>, not T.** Check `ctx.application.property`, not `ctx.property`. Guards: `(ctx: Context<AppContext>) => boolean`
|
|
149
|
+
|
|
150
|
+
**Context must be mutated then updated.** After changing `ctx.application`, call `ctx.update()` to notify subscribers. Pages need `@context()` to get context reference.
|
|
151
|
+
|
|
152
|
+
**@property is for parent-provided attributes.** Internal component state should be regular properties, not decorated. Only use `@property()` when the value comes from a parent element via attributes.
|
|
153
|
+
|
|
132
154
|
## Build Commands
|
|
133
155
|
|
|
134
156
|
```bash
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
A Progressive Web App (PWA) built with [Snice](https://github.com/sniceio/snice).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ⚡ **JWT Authentication** - Token-based auth with automatic refresh
|
|
8
|
+
- 🔒 **Protected Routes** - Route guards for authenticated pages
|
|
9
|
+
- 🎯 **Middleware Pattern** - Composable fetch middleware (auth, error, retry)
|
|
10
|
+
- 📱 **PWA Ready** - Service worker, offline support, installable
|
|
11
|
+
- 🔔 **Live Notifications** - WebSocket daemon for real-time updates
|
|
12
|
+
- 🎨 **Snice Components** - Pre-built UI components
|
|
13
|
+
- 📦 **Type-Safe** - Full TypeScript support
|
|
14
|
+
- 🚀 **Fast Build** - Vite + SWC for blazing fast dev and builds
|
|
15
|
+
|
|
16
|
+
## Getting Started
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Install dependencies
|
|
20
|
+
npm install
|
|
21
|
+
|
|
22
|
+
# Start dev server
|
|
23
|
+
npm run dev
|
|
24
|
+
|
|
25
|
+
# Build for production
|
|
26
|
+
npm run build
|
|
27
|
+
|
|
28
|
+
# Preview production build
|
|
29
|
+
npm run preview
|
|
30
|
+
|
|
31
|
+
# Type check
|
|
32
|
+
npm run type-check
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Demo Credentials
|
|
36
|
+
|
|
37
|
+
- **Email:** demo@example.com
|
|
38
|
+
- **Password:** demo
|
|
39
|
+
|
|
40
|
+
## Project Structure
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
src/
|
|
44
|
+
utils/ # Pure helper functions
|
|
45
|
+
services/ # Business logic (auth, storage, jwt)
|
|
46
|
+
middleware/ # Fetch middleware (auth, error, retry)
|
|
47
|
+
daemons/ # Lifecycle-managed classes (notifications WebSocket)
|
|
48
|
+
guards/ # Route guards (auth)
|
|
49
|
+
types/ # TypeScript types
|
|
50
|
+
pages/ # Routable pages (@page decorator)
|
|
51
|
+
styles/ # Global styles
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Architecture Patterns
|
|
55
|
+
|
|
56
|
+
### Middleware-Based Fetch
|
|
57
|
+
|
|
58
|
+
Composable middleware for all API calls:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { createFetch } from './utils/fetch';
|
|
62
|
+
import { authMiddleware } from './middleware/auth';
|
|
63
|
+
import { errorMiddleware } from './middleware/error';
|
|
64
|
+
import { retryMiddleware } from './middleware/retry';
|
|
65
|
+
|
|
66
|
+
export const api = createFetch({
|
|
67
|
+
baseURL: import.meta.env.VITE_API_URL,
|
|
68
|
+
middleware: [authMiddleware, errorMiddleware, retryMiddleware()],
|
|
69
|
+
headers: { 'Content-Type': 'application/json' }
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Usage
|
|
73
|
+
const data = await api('/users').then(r => r.json());
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Daemons for Lifecycle Management
|
|
77
|
+
|
|
78
|
+
Use daemons for resources that need start/stop/dispose:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const daemon = getNotificationsDaemon();
|
|
82
|
+
daemon.start(); // In main.ts
|
|
83
|
+
|
|
84
|
+
// In component
|
|
85
|
+
const unsubscribe = daemon.subscribe((notification) => {
|
|
86
|
+
console.log(notification);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Cleanup
|
|
90
|
+
unsubscribe();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Route Guards
|
|
94
|
+
|
|
95
|
+
Protect routes with guards:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { authGuard } from './guards/auth';
|
|
99
|
+
|
|
100
|
+
@page({
|
|
101
|
+
tag: 'dashboard-page',
|
|
102
|
+
routes: ['/dashboard'],
|
|
103
|
+
guards: [authGuard]
|
|
104
|
+
})
|
|
105
|
+
export class DashboardPage extends HTMLElement {
|
|
106
|
+
// ...
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Context for Global State
|
|
111
|
+
|
|
112
|
+
Access shared state via context:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
@context()
|
|
116
|
+
handleContext(ctx: Context<AppContext>) {
|
|
117
|
+
this.user = ctx.application.user;
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Customization
|
|
122
|
+
|
|
123
|
+
### Replace Mock API
|
|
124
|
+
|
|
125
|
+
Update `src/services/auth.ts` to call your real API:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
export async function login(credentials: LoginCredentials) {
|
|
129
|
+
const response = await fetch('https://your-api.com/auth/login', {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: { 'Content-Type': 'application/json' },
|
|
132
|
+
body: JSON.stringify(credentials)
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const data = await response.json();
|
|
136
|
+
setToken(data.token);
|
|
137
|
+
setUser(data.user);
|
|
138
|
+
return data;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Enable Real WebSocket
|
|
143
|
+
|
|
144
|
+
Update `src/daemons/notifications.ts`:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// Replace startMockNotifications() with:
|
|
148
|
+
this.connect();
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Configure Environment
|
|
152
|
+
|
|
153
|
+
Create `.env` file:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
VITE_API_URL=https://your-api.com
|
|
157
|
+
VITE_WS_URL=wss://your-ws.com
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Update Icons
|
|
161
|
+
|
|
162
|
+
Replace placeholder icons in `public/icons/`:
|
|
163
|
+
- `icon-192.png` (192x192)
|
|
164
|
+
- `icon-512.png` (512x512)
|
|
165
|
+
|
|
166
|
+
## Learn More
|
|
167
|
+
|
|
168
|
+
- [Snice Documentation](https://sniceio.github.io/snice)
|
|
169
|
+
- [Vite PWA Plugin](https://vite-pwa-org.netlify.app/)
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="description" content="{{projectName}} - PWA built with Snice" />
|
|
7
|
+
<meta name="theme-color" content="#6366f1" />
|
|
8
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
9
|
+
<link rel="manifest" href="/manifest.json" />
|
|
10
|
+
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
|
|
11
|
+
<title>{{projectName}}</title>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div id="app"></div>
|
|
15
|
+
<script type="module" src="/src/main.ts"></script>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"type-check": "tsc --noEmit",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"snice": "^3.0.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/node": "^20.0.0",
|
|
17
|
+
"@vite-pwa/assets-generator": "^0.2.4",
|
|
18
|
+
"terser": "^5.24.0",
|
|
19
|
+
"typescript": "^5.3.3",
|
|
20
|
+
"unplugin-swc": "^1.5.7",
|
|
21
|
+
"vite": "^5.0.10",
|
|
22
|
+
"vite-plugin-pwa": "^0.17.4",
|
|
23
|
+
"workbox-window": "^7.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"short_name": "{{projectName}}",
|
|
4
|
+
"description": "PWA built with Snice",
|
|
5
|
+
"start_url": "/",
|
|
6
|
+
"display": "standalone",
|
|
7
|
+
"background_color": "#ffffff",
|
|
8
|
+
"theme_color": "#6366f1",
|
|
9
|
+
"orientation": "portrait-primary",
|
|
10
|
+
"icons": [
|
|
11
|
+
{
|
|
12
|
+
"src": "/icons/icon-192.png",
|
|
13
|
+
"sizes": "192x192",
|
|
14
|
+
"type": "image/png",
|
|
15
|
+
"purpose": "any maskable"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"src": "/icons/icon-512.png",
|
|
19
|
+
"sizes": "512x512",
|
|
20
|
+
"type": "image/png",
|
|
21
|
+
"purpose": "any maskable"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { Notification } from '../types/notifications';
|
|
2
|
+
|
|
3
|
+
type NotificationHandler = (notification: Notification) => void;
|
|
4
|
+
|
|
5
|
+
export class NotificationsDaemon {
|
|
6
|
+
private ws: WebSocket | null = null;
|
|
7
|
+
private handlers: Set<NotificationHandler> = new Set();
|
|
8
|
+
private reconnectInterval: number = 5000;
|
|
9
|
+
private reconnectTimer: number | null = null;
|
|
10
|
+
private mockInterval: number | null = null;
|
|
11
|
+
private url: string;
|
|
12
|
+
private isStarted: boolean = false;
|
|
13
|
+
|
|
14
|
+
constructor(url: string) {
|
|
15
|
+
this.url = url;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
start(): void {
|
|
19
|
+
if (this.isStarted) return;
|
|
20
|
+
this.isStarted = true;
|
|
21
|
+
|
|
22
|
+
// For demo purposes, use mock notifications instead of real WebSocket
|
|
23
|
+
// In production, uncomment the connect() line and remove startMockNotifications()
|
|
24
|
+
// this.connect();
|
|
25
|
+
this.startMockNotifications();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
stop(): void {
|
|
29
|
+
if (!this.isStarted) return;
|
|
30
|
+
this.isStarted = false;
|
|
31
|
+
|
|
32
|
+
this.stopMockNotifications();
|
|
33
|
+
|
|
34
|
+
if (this.reconnectTimer !== null) {
|
|
35
|
+
clearTimeout(this.reconnectTimer);
|
|
36
|
+
this.reconnectTimer = null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (this.ws) {
|
|
40
|
+
this.ws.close();
|
|
41
|
+
this.ws = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
dispose(): void {
|
|
46
|
+
this.stop();
|
|
47
|
+
this.handlers.clear();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
subscribe(handler: NotificationHandler): () => void {
|
|
51
|
+
this.handlers.add(handler);
|
|
52
|
+
return () => this.handlers.delete(handler);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private connect(): void {
|
|
56
|
+
try {
|
|
57
|
+
this.ws = new WebSocket(this.url);
|
|
58
|
+
|
|
59
|
+
this.ws.onopen = () => {
|
|
60
|
+
console.log('WebSocket connected');
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
this.ws.onmessage = (event) => {
|
|
64
|
+
try {
|
|
65
|
+
const notification: Notification = JSON.parse(event.data);
|
|
66
|
+
this.notify(notification);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error('Failed to parse notification:', err);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
this.ws.onerror = (error) => {
|
|
73
|
+
console.error('WebSocket error:', error);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
this.ws.onclose = () => {
|
|
77
|
+
console.log('WebSocket disconnected');
|
|
78
|
+
if (this.isStarted) {
|
|
79
|
+
this.scheduleReconnect();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error('Failed to create WebSocket:', err);
|
|
84
|
+
if (this.isStarted) {
|
|
85
|
+
this.scheduleReconnect();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private scheduleReconnect(): void {
|
|
91
|
+
if (this.reconnectTimer !== null) return;
|
|
92
|
+
|
|
93
|
+
this.reconnectTimer = window.setTimeout(() => {
|
|
94
|
+
this.reconnectTimer = null;
|
|
95
|
+
this.connect();
|
|
96
|
+
}, this.reconnectInterval);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private notify(notification: Notification): void {
|
|
100
|
+
this.handlers.forEach(handler => {
|
|
101
|
+
try {
|
|
102
|
+
handler(notification);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error('Notification handler error:', err);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Mock notifications for demo
|
|
110
|
+
private startMockNotifications(): void {
|
|
111
|
+
const messages = [
|
|
112
|
+
{ title: 'Welcome!', message: 'Thanks for checking out the PWA template', type: 'info' as const },
|
|
113
|
+
{ title: 'New Feature', message: 'Check out the notifications page', type: 'success' as const },
|
|
114
|
+
{ title: 'System Update', message: 'A new version is available', type: 'info' as const },
|
|
115
|
+
{ title: 'Reminder', message: 'Don\'t forget to star the repo!', type: 'warning' as const }
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
let index = 0;
|
|
119
|
+
this.mockInterval = window.setInterval(() => {
|
|
120
|
+
const notification: Notification = {
|
|
121
|
+
id: `mock-${Date.now()}`,
|
|
122
|
+
...messages[index % messages.length],
|
|
123
|
+
timestamp: new Date().toISOString()
|
|
124
|
+
};
|
|
125
|
+
this.notify(notification);
|
|
126
|
+
index++;
|
|
127
|
+
}, 10000); // Send notification every 10 seconds
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private stopMockNotifications(): void {
|
|
131
|
+
if (this.mockInterval !== null) {
|
|
132
|
+
clearInterval(this.mockInterval);
|
|
133
|
+
this.mockInterval = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Singleton instance
|
|
139
|
+
let instance: NotificationsDaemon | null = null;
|
|
140
|
+
|
|
141
|
+
export function getNotificationsDaemon(): NotificationsDaemon {
|
|
142
|
+
if (!instance) {
|
|
143
|
+
// In production, use actual WebSocket URL from env
|
|
144
|
+
const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8080';
|
|
145
|
+
instance = new NotificationsDaemon(wsUrl);
|
|
146
|
+
}
|
|
147
|
+
return instance;
|
|
148
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Context } from 'snice';
|
|
2
|
+
import type { AppContext } from '../types/auth';
|
|
3
|
+
|
|
4
|
+
export function authGuard(ctx: Context<AppContext>): boolean {
|
|
5
|
+
if (!ctx.application.isAuthenticated) {
|
|
6
|
+
window.location.href = '#/login';
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { initialize } from './router';
|
|
2
|
+
import './styles/global.css';
|
|
3
|
+
|
|
4
|
+
// Import snice layout and components
|
|
5
|
+
import 'snice/components/layout/snice-layout';
|
|
6
|
+
import 'snice/components/button/snice-button';
|
|
7
|
+
import 'snice/components/card/snice-card';
|
|
8
|
+
import 'snice/components/input/snice-input';
|
|
9
|
+
import 'snice/components/alert/snice-alert';
|
|
10
|
+
import 'snice/components/avatar/snice-avatar';
|
|
11
|
+
import 'snice/components/empty-state/snice-empty-state';
|
|
12
|
+
import 'snice/components/spinner/snice-spinner';
|
|
13
|
+
|
|
14
|
+
// Import pages
|
|
15
|
+
import './pages/login';
|
|
16
|
+
import './pages/dashboard';
|
|
17
|
+
import './pages/profile';
|
|
18
|
+
import './pages/notifications';
|
|
19
|
+
|
|
20
|
+
// Import and start daemons
|
|
21
|
+
import { getNotificationsDaemon } from './daemons/notifications';
|
|
22
|
+
|
|
23
|
+
// Start notifications daemon
|
|
24
|
+
const notificationsDaemon = getNotificationsDaemon();
|
|
25
|
+
notificationsDaemon.start();
|
|
26
|
+
|
|
27
|
+
// Initialize router
|
|
28
|
+
initialize();
|
|
29
|
+
|
|
30
|
+
// Register service worker (PWA)
|
|
31
|
+
if ('serviceWorker' in navigator) {
|
|
32
|
+
window.addEventListener('load', () => {
|
|
33
|
+
navigator.serviceWorker.register('/sw.js').then(
|
|
34
|
+
(registration) => {
|
|
35
|
+
console.log('Service Worker registered:', registration);
|
|
36
|
+
},
|
|
37
|
+
(error) => {
|
|
38
|
+
console.error('Service Worker registration failed:', error);
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Middleware } from '../utils/fetch';
|
|
2
|
+
import { getToken } from '../services/storage';
|
|
3
|
+
|
|
4
|
+
export async function authMiddleware(url: string, options: RequestInit, next: () => Promise<Response>): Promise<Response> {
|
|
5
|
+
const token = getToken();
|
|
6
|
+
|
|
7
|
+
if (token) {
|
|
8
|
+
options.headers = {
|
|
9
|
+
...options.headers,
|
|
10
|
+
'Authorization': `Bearer ${token}`,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return next();
|
|
15
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Middleware } from '../utils/fetch';
|
|
2
|
+
import { clearToken } from '../services/storage';
|
|
3
|
+
|
|
4
|
+
export async function errorMiddleware(url: string, options: RequestInit, next: () => Promise<Response>): Promise<Response> {
|
|
5
|
+
const response = await next();
|
|
6
|
+
|
|
7
|
+
// Handle 401 unauthorized - token expired or invalid
|
|
8
|
+
if (response.status === 401) {
|
|
9
|
+
clearToken();
|
|
10
|
+
window.location.href = '#/login';
|
|
11
|
+
throw new Error('Unauthorized - redirecting to login');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Handle other errors
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
const contentType = response.headers.get('content-type');
|
|
17
|
+
if (contentType && contentType.includes('application/json')) {
|
|
18
|
+
const error = await response.json();
|
|
19
|
+
throw new Error(error.message || `Request failed with status ${response.status}`);
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Request failed with status ${response.status}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return response;
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Middleware } from '../utils/fetch';
|
|
2
|
+
|
|
3
|
+
export function retryMiddleware(retries = 3, delay = 1000): Middleware {
|
|
4
|
+
return async (url, options, next) => {
|
|
5
|
+
let lastError: Error;
|
|
6
|
+
|
|
7
|
+
for (let i = 0; i < retries; i++) {
|
|
8
|
+
try {
|
|
9
|
+
return await next();
|
|
10
|
+
} catch (err) {
|
|
11
|
+
lastError = err as Error;
|
|
12
|
+
|
|
13
|
+
// Don't retry on certain errors
|
|
14
|
+
if (err instanceof Error && err.message.includes('Unauthorized')) {
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Wait before retrying (exponential backoff)
|
|
19
|
+
if (i < retries - 1) {
|
|
20
|
+
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
throw lastError!;
|
|
26
|
+
};
|
|
27
|
+
}
|