snice 3.6.0 → 3.8.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.
Files changed (80) hide show
  1. package/README.md +2 -2
  2. package/bin/snice.js +4 -5
  3. package/bin/templates/CLAUDE.md +25 -3
  4. package/bin/templates/pwa/README.md +188 -0
  5. package/bin/templates/pwa/global.d.ts +10 -0
  6. package/bin/templates/pwa/index.html +16 -0
  7. package/bin/templates/pwa/package.json +32 -0
  8. package/bin/templates/pwa/public/icons/.gitkeep +6 -0
  9. package/bin/templates/pwa/src/daemons/notifications.ts +148 -0
  10. package/bin/templates/pwa/src/fetcher.ts +15 -0
  11. package/bin/templates/pwa/src/guards/auth.ts +12 -0
  12. package/bin/templates/pwa/src/main.ts +42 -0
  13. package/bin/templates/pwa/src/middleware/auth.ts +16 -0
  14. package/bin/templates/pwa/src/middleware/error.ts +36 -0
  15. package/bin/templates/pwa/src/middleware/retry.ts +31 -0
  16. package/bin/templates/pwa/src/pages/dashboard.ts +143 -0
  17. package/bin/templates/pwa/src/pages/login.ts +161 -0
  18. package/bin/templates/pwa/src/pages/notifications.ts +156 -0
  19. package/bin/templates/pwa/src/pages/profile.ts +164 -0
  20. package/bin/templates/pwa/src/router.ts +20 -0
  21. package/bin/templates/pwa/src/services/auth.ts +48 -0
  22. package/bin/templates/pwa/src/services/jwt.ts +35 -0
  23. package/bin/templates/pwa/src/services/storage.ts +24 -0
  24. package/bin/templates/pwa/src/styles/global.css +55 -0
  25. package/bin/templates/pwa/src/types/auth.ts +21 -0
  26. package/bin/templates/pwa/src/types/notifications.ts +9 -0
  27. package/bin/templates/pwa/tests/helpers/test-utils.ts +84 -0
  28. package/bin/templates/pwa/tests/middleware/auth.test.ts +67 -0
  29. package/bin/templates/pwa/tests/middleware/error.test.ts +105 -0
  30. package/bin/templates/pwa/tests/middleware/retry.test.ts +103 -0
  31. package/bin/templates/pwa/tests/services/auth.test.ts +89 -0
  32. package/bin/templates/pwa/tests/services/jwt.test.ts +76 -0
  33. package/bin/templates/pwa/tests/services/storage.test.ts +69 -0
  34. package/bin/templates/{social → pwa}/tsconfig.json +11 -10
  35. package/bin/templates/pwa/vite.config.ts +94 -0
  36. package/bin/templates/{social/vite.config.ts → pwa/vitest.config.ts} +12 -17
  37. package/dist/components/music-player/snice-music-player.d.ts +72 -0
  38. package/dist/components/music-player/snice-music-player.js +730 -0
  39. package/dist/components/music-player/snice-music-player.js.map +1 -0
  40. package/dist/components/music-player/snice-music-player.types.d.ts +43 -0
  41. package/dist/components/timer/snice-timer.d.ts +27 -0
  42. package/dist/components/timer/snice-timer.js +197 -0
  43. package/dist/components/timer/snice-timer.js.map +1 -0
  44. package/dist/components/timer/snice-timer.types.d.ts +10 -0
  45. package/dist/fetcher.d.ts +65 -0
  46. package/dist/index.cjs +92 -3
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.esm.js +92 -4
  50. package/dist/index.esm.js.map +1 -1
  51. package/dist/index.iife.js +92 -3
  52. package/dist/index.iife.js.map +1 -1
  53. package/dist/symbols.cjs +1 -1
  54. package/dist/symbols.esm.js +1 -1
  55. package/dist/transitions.cjs +1 -1
  56. package/dist/transitions.esm.js +1 -1
  57. package/dist/types/context.d.ts +7 -1
  58. package/dist/types/router-options.d.ts +6 -0
  59. package/docs/ai/api.md +33 -1
  60. package/docs/ai/components/music-player.md +134 -0
  61. package/docs/ai/components/timer.md +43 -0
  62. package/docs/ai/patterns.md +48 -1
  63. package/docs/components/music-player.md +314 -0
  64. package/docs/components/timer.md +143 -0
  65. package/docs/fetcher.md +447 -0
  66. package/docs/routing.md +11 -8
  67. package/package.json +2 -1
  68. package/bin/templates/social/README.md +0 -42
  69. package/bin/templates/social/global.d.ts +0 -14
  70. package/bin/templates/social/index.html +0 -13
  71. package/bin/templates/social/package.json +0 -21
  72. package/bin/templates/social/src/main.ts +0 -33
  73. package/bin/templates/social/src/pages/feed-page.ts +0 -111
  74. package/bin/templates/social/src/pages/messages-page.ts +0 -102
  75. package/bin/templates/social/src/pages/not-found-page.ts +0 -46
  76. package/bin/templates/social/src/pages/profile-page.ts +0 -99
  77. package/bin/templates/social/src/pages/settings-page.ts +0 -119
  78. package/bin/templates/social/src/router.ts +0 -9
  79. package/bin/templates/social/src/styles/global.css +0 -156
  80. /package/bin/templates/{social → pwa}/public/vite.svg +0 -0
@@ -0,0 +1,143 @@
1
+ import { page } from '../router';
2
+ import { render, styles, html, css, context } from 'snice';
3
+ import type { Placard, Context } from 'snice';
4
+ import type { Principal } from '../types/auth';
5
+ import { authGuard } from '../guards/auth';
6
+
7
+ const placard: Placard = {
8
+ name: 'dashboard',
9
+ title: 'Dashboard',
10
+ icon: '📊',
11
+ show: true,
12
+ order: 1
13
+ };
14
+
15
+ @page({ tag: 'dashboard-page', routes: ['/', '/dashboard'], guards: [authGuard], placard })
16
+ export class DashboardPage extends HTMLElement {
17
+ userName = '';
18
+
19
+ @context()
20
+ handleContext(ctx: Context) {
21
+ const principal = ctx.application.principal as Principal | undefined;
22
+ this.userName = principal?.user?.name || 'User';
23
+ }
24
+
25
+ @render()
26
+ renderContent() {
27
+ return html`
28
+ <div class="container">
29
+ <h1>Welcome, ${this.userName}!</h1>
30
+ <p class="subtitle">This is your dashboard</p>
31
+
32
+ <div class="grid">
33
+ <snice-card>
34
+ <h3>⚡ Features</h3>
35
+ <ul>
36
+ <li>JWT Authentication</li>
37
+ <li>Protected Routes</li>
38
+ <li>Middleware Pattern</li>
39
+ <li>Service Worker</li>
40
+ <li>Live Notifications</li>
41
+ </ul>
42
+ </snice-card>
43
+
44
+ <snice-card>
45
+ <h3>🛠️ Architecture</h3>
46
+ <ul>
47
+ <li><strong>Utils:</strong> Pure functions</li>
48
+ <li><strong>Services:</strong> Business logic</li>
49
+ <li><strong>Middleware:</strong> Fetch interceptors</li>
50
+ <li><strong>Daemons:</strong> Lifecycle classes</li>
51
+ <li><strong>Guards:</strong> Route protection</li>
52
+ </ul>
53
+ </snice-card>
54
+
55
+ <snice-card>
56
+ <h3>📦 What's Included</h3>
57
+ <ul>
58
+ <li>TypeScript configuration</li>
59
+ <li>Vite + SWC setup</li>
60
+ <li>PWA manifest</li>
61
+ <li>Mock authentication</li>
62
+ <li>WebSocket daemon example</li>
63
+ </ul>
64
+ </snice-card>
65
+
66
+ <snice-card>
67
+ <h3>🚀 Get Started</h3>
68
+ <p>Check out the other pages:</p>
69
+ <div class="links">
70
+ <a href="#/profile">
71
+ <snice-button variant="secondary">View Profile</snice-button>
72
+ </a>
73
+ <a href="#/notifications">
74
+ <snice-button variant="secondary">Notifications</snice-button>
75
+ </a>
76
+ </div>
77
+ </snice-card>
78
+ </div>
79
+ </div>
80
+ `;
81
+ }
82
+
83
+ @styles()
84
+ componentStyles() {
85
+ return css`
86
+ .container {
87
+ padding: 2rem;
88
+ max-width: 1200px;
89
+ margin: 0 auto;
90
+ }
91
+
92
+ h1 {
93
+ color: var(--primary-color);
94
+ margin: 0 0 0.5rem 0;
95
+ }
96
+
97
+ .subtitle {
98
+ color: var(--text-light);
99
+ margin: 0 0 2rem 0;
100
+ }
101
+
102
+ .grid {
103
+ display: grid;
104
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
105
+ gap: 1.5rem;
106
+ }
107
+
108
+ snice-card {
109
+ padding: 1.5rem;
110
+ }
111
+
112
+ h3 {
113
+ margin: 0 0 1rem 0;
114
+ color: var(--primary-color);
115
+ }
116
+
117
+ ul {
118
+ margin: 0;
119
+ padding-left: 1.5rem;
120
+ }
121
+
122
+ li {
123
+ margin: 0.5rem 0;
124
+ color: var(--text-color);
125
+ }
126
+
127
+ .links {
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 0.75rem;
131
+ margin-top: 1rem;
132
+ }
133
+
134
+ .links a {
135
+ text-decoration: none;
136
+ }
137
+
138
+ snice-button {
139
+ width: 100%;
140
+ }
141
+ `;
142
+ }
143
+ }
@@ -0,0 +1,161 @@
1
+ import { page } from '../router';
2
+ import { render, styles, html, css, context } from 'snice';
3
+ import type { Placard, Context } from 'snice';
4
+ import type { Principal } from '../types/auth';
5
+ import { login } from '../services/auth';
6
+
7
+ const placard: Placard = {
8
+ name: 'login',
9
+ title: 'Login',
10
+ show: false
11
+ };
12
+
13
+ @page({ tag: 'login-page', routes: ['/login'], placard })
14
+ export class LoginPage extends HTMLElement {
15
+ email = 'demo@example.com';
16
+ password = 'demo';
17
+ error = '';
18
+ loading = false;
19
+
20
+ private ctx?: Context;
21
+
22
+ @context()
23
+ handleContext(ctx: Context) {
24
+ this.ctx = ctx;
25
+ }
26
+
27
+ async handleSubmit(e: Event) {
28
+ e.preventDefault();
29
+ this.error = '';
30
+ this.loading = true;
31
+
32
+ try {
33
+ const result = await login({ email: this.email, password: this.password });
34
+
35
+ // Update context with new auth state
36
+ if (this.ctx && this.ctx.application.principal) {
37
+ const principal = this.ctx.application.principal as Principal;
38
+ principal.user = result.user;
39
+ principal.isAuthenticated = true;
40
+ }
41
+
42
+ window.location.href = '#/dashboard';
43
+ } catch (err) {
44
+ this.error = err instanceof Error ? err.message : 'Login failed';
45
+ } finally {
46
+ this.loading = false;
47
+ }
48
+ }
49
+
50
+ @render()
51
+ renderContent() {
52
+ return html`
53
+ <div class="container">
54
+ <snice-card class="login-card">
55
+ <h1>{{projectName}}</h1>
56
+ <p class="subtitle">Sign in to your account</p>
57
+
58
+ <form @submit=${this.handleSubmit}>
59
+ <snice-input
60
+ label="Email"
61
+ type="email"
62
+ .value=${this.email}
63
+ @input=${(e: Event) => this.email = (e.target as HTMLInputElement).value}
64
+ required
65
+ ></snice-input>
66
+
67
+ <snice-input
68
+ label="Password"
69
+ type="password"
70
+ .value=${this.password}
71
+ @input=${(e: Event) => this.password = (e.target as HTMLInputElement).value}
72
+ required
73
+ ></snice-input>
74
+
75
+ <if ${this.error}>
76
+ <snice-alert variant="error" class="error">
77
+ ${this.error}
78
+ </snice-alert>
79
+ </if>
80
+
81
+ <snice-button
82
+ type="submit"
83
+ variant="primary"
84
+ size="large"
85
+ ?loading=${this.loading}
86
+ full-width
87
+ >
88
+ Sign In
89
+ </snice-button>
90
+ </form>
91
+
92
+ <div class="hint">
93
+ <p>Demo credentials:</p>
94
+ <p><strong>Email:</strong> demo@example.com</p>
95
+ <p><strong>Password:</strong> demo</p>
96
+ </div>
97
+ </snice-card>
98
+ </div>
99
+ `;
100
+ }
101
+
102
+ @styles()
103
+ componentStyles() {
104
+ return css`
105
+ :host {
106
+ display: block;
107
+ min-height: 100vh;
108
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
109
+ }
110
+
111
+ .container {
112
+ display: flex;
113
+ align-items: center;
114
+ justify-content: center;
115
+ min-height: 100vh;
116
+ padding: 2rem;
117
+ }
118
+
119
+ .login-card {
120
+ width: 100%;
121
+ max-width: 400px;
122
+ padding: 2rem;
123
+ }
124
+
125
+ h1 {
126
+ text-align: center;
127
+ color: var(--primary-color);
128
+ margin: 0 0 0.5rem 0;
129
+ }
130
+
131
+ .subtitle {
132
+ text-align: center;
133
+ color: var(--text-light);
134
+ margin: 0 0 2rem 0;
135
+ }
136
+
137
+ form {
138
+ display: flex;
139
+ flex-direction: column;
140
+ gap: 1rem;
141
+ }
142
+
143
+ .error {
144
+ margin: 0;
145
+ }
146
+
147
+ .hint {
148
+ margin-top: 1.5rem;
149
+ padding-top: 1.5rem;
150
+ border-top: 1px solid var(--border-color);
151
+ text-align: center;
152
+ font-size: 0.875rem;
153
+ color: var(--text-light);
154
+ }
155
+
156
+ .hint p {
157
+ margin: 0.25rem 0;
158
+ }
159
+ `;
160
+ }
161
+ }
@@ -0,0 +1,156 @@
1
+ import { page } from '../router';
2
+ import { render, styles, html, css, ready, dispose } from 'snice';
3
+ import type { Placard } from 'snice';
4
+ import { authGuard } from '../guards/auth';
5
+ import { getNotificationsDaemon } from '../daemons/notifications';
6
+ import type { Notification } from '../types/notifications';
7
+
8
+ const placard: Placard = {
9
+ name: 'notifications',
10
+ title: 'Notifications',
11
+ icon: '🔔',
12
+ show: true,
13
+ order: 3
14
+ };
15
+
16
+ @page({ tag: 'notifications-page', routes: ['/notifications'], guards: [authGuard], placard })
17
+ export class NotificationsPage extends HTMLElement {
18
+ notifications: Notification[] = [];
19
+ private unsubscribe: (() => void) | null = null;
20
+
21
+ @ready()
22
+ initialize() {
23
+ const daemon = getNotificationsDaemon();
24
+ this.unsubscribe = daemon.subscribe((notification) => {
25
+ this.notifications = [notification, ...this.notifications];
26
+ });
27
+ }
28
+
29
+ @dispose()
30
+ cleanup() {
31
+ if (this.unsubscribe) {
32
+ this.unsubscribe();
33
+ this.unsubscribe = null;
34
+ }
35
+ }
36
+
37
+ getVariant(type: string): string {
38
+ const variants: Record<string, string> = {
39
+ info: 'info',
40
+ success: 'success',
41
+ warning: 'warning',
42
+ error: 'danger'
43
+ };
44
+ return variants[type] || 'info';
45
+ }
46
+
47
+ clearAll() {
48
+ this.notifications = [];
49
+ }
50
+
51
+ removeNotification(id: string) {
52
+ this.notifications = this.notifications.filter(n => n.id !== id);
53
+ }
54
+
55
+ @render()
56
+ renderContent() {
57
+ return html`
58
+ <div class="container">
59
+ <div class="header">
60
+ <h1>Notifications</h1>
61
+ <if ${this.notifications.length > 0}>
62
+ <snice-button
63
+ variant="secondary"
64
+ size="small"
65
+ @click=${this.clearAll}
66
+ >
67
+ Clear All
68
+ </snice-button>
69
+ </if>
70
+ </div>
71
+
72
+ <if ${this.notifications.length === 0}>
73
+ <snice-empty-state
74
+ icon="🔔"
75
+ title="No notifications"
76
+ description="You'll see live notifications here as they arrive"
77
+ ></snice-empty-state>
78
+ </if>
79
+
80
+ <if ${this.notifications.length > 0}>
81
+ <div class="notifications">
82
+ ${this.notifications.map(notification => html`
83
+ <snice-alert
84
+ key=${notification.id}
85
+ variant="${this.getVariant(notification.type)}"
86
+ dismissible
87
+ @dismiss=${() => this.removeNotification(notification.id)}
88
+ >
89
+ <strong>${notification.title}</strong>
90
+ <p>${notification.message}</p>
91
+ <small>${new Date(notification.timestamp).toLocaleTimeString()}</small>
92
+ </snice-alert>
93
+ `)}
94
+ </div>
95
+ </if>
96
+ </div>
97
+ `;
98
+ }
99
+
100
+ @styles()
101
+ componentStyles() {
102
+ return css`
103
+ .container {
104
+ padding: 2rem;
105
+ max-width: 800px;
106
+ margin: 0 auto;
107
+ }
108
+
109
+ .header {
110
+ display: flex;
111
+ justify-content: space-between;
112
+ align-items: center;
113
+ margin-bottom: 2rem;
114
+ }
115
+
116
+ h1 {
117
+ margin: 0;
118
+ color: var(--primary-color);
119
+ }
120
+
121
+ .notifications {
122
+ display: flex;
123
+ flex-direction: column;
124
+ gap: 1rem;
125
+ }
126
+
127
+ snice-alert {
128
+ animation: slideIn 0.3s ease-out;
129
+ }
130
+
131
+ @keyframes slideIn {
132
+ from {
133
+ opacity: 0;
134
+ transform: translateX(-20px);
135
+ }
136
+ to {
137
+ opacity: 1;
138
+ transform: translateX(0);
139
+ }
140
+ }
141
+
142
+ snice-alert strong {
143
+ display: block;
144
+ margin-bottom: 0.5rem;
145
+ }
146
+
147
+ snice-alert p {
148
+ margin: 0 0 0.5rem 0;
149
+ }
150
+
151
+ snice-alert small {
152
+ opacity: 0.7;
153
+ }
154
+ `;
155
+ }
156
+ }
@@ -0,0 +1,164 @@
1
+ import { page } from '../router';
2
+ import { render, styles, html, css, context } from 'snice';
3
+ import type { Placard, Context } from 'snice';
4
+ import type { Principal, User } from '../types/auth';
5
+ import { authGuard } from '../guards/auth';
6
+ import { logout } from '../services/auth';
7
+
8
+ const placard: Placard = {
9
+ name: 'profile',
10
+ title: 'Profile',
11
+ icon: '👤',
12
+ show: true,
13
+ order: 2
14
+ };
15
+
16
+ @page({ tag: 'profile-page', routes: ['/profile'], guards: [authGuard], placard })
17
+ export class ProfilePage extends HTMLElement {
18
+ user: User | null = null;
19
+ private ctx?: Context;
20
+
21
+ @context()
22
+ handleContext(ctx: Context) {
23
+ this.ctx = ctx;
24
+ const principal = ctx.application.principal as Principal | undefined;
25
+ this.user = principal?.user || null;
26
+ }
27
+
28
+ async handleLogout() {
29
+ await logout();
30
+
31
+ // Update context to reflect logged out state
32
+ if (this.ctx && this.ctx.application.principal) {
33
+ const principal = this.ctx.application.principal as Principal;
34
+ principal.user = null;
35
+ principal.isAuthenticated = false;
36
+ }
37
+
38
+ window.location.href = '#/login';
39
+ }
40
+
41
+ @render()
42
+ renderContent() {
43
+ if (!this.user) {
44
+ return html`<div class="container"><snice-spinner></snice-spinner></div>`;
45
+ }
46
+
47
+ return html`
48
+ <div class="container">
49
+ <snice-card class="profile-card">
50
+ <div class="header">
51
+ <snice-avatar
52
+ src="${this.user.avatar}"
53
+ name="${this.user.name}"
54
+ size="large"
55
+ ></snice-avatar>
56
+ <h1>${this.user.name}</h1>
57
+ <p class="email">${this.user.email}</p>
58
+ </div>
59
+
60
+ <div class="section">
61
+ <h3>Account Information</h3>
62
+ <div class="info-grid">
63
+ <div class="info-item">
64
+ <span class="label">User ID:</span>
65
+ <span class="value">${this.user.id}</span>
66
+ </div>
67
+ <div class="info-item">
68
+ <span class="label">Email:</span>
69
+ <span class="value">${this.user.email}</span>
70
+ </div>
71
+ <div class="info-item">
72
+ <span class="label">Name:</span>
73
+ <span class="value">${this.user.name}</span>
74
+ </div>
75
+ </div>
76
+ </div>
77
+
78
+ <div class="section">
79
+ <h3>Session</h3>
80
+ <p>You are currently logged in with JWT authentication.</p>
81
+ <snice-button
82
+ variant="danger"
83
+ @click=${this.handleLogout}
84
+ >
85
+ Sign Out
86
+ </snice-button>
87
+ </div>
88
+ </snice-card>
89
+ </div>
90
+ `;
91
+ }
92
+
93
+ @styles()
94
+ componentStyles() {
95
+ return css`
96
+ .container {
97
+ padding: 2rem;
98
+ max-width: 800px;
99
+ margin: 0 auto;
100
+ }
101
+
102
+ .profile-card {
103
+ padding: 2rem;
104
+ }
105
+
106
+ .header {
107
+ text-align: center;
108
+ padding-bottom: 2rem;
109
+ border-bottom: 1px solid var(--border-color);
110
+ }
111
+
112
+ snice-avatar {
113
+ margin-bottom: 1rem;
114
+ }
115
+
116
+ h1 {
117
+ margin: 0 0 0.5rem 0;
118
+ color: var(--primary-color);
119
+ }
120
+
121
+ .email {
122
+ margin: 0;
123
+ color: var(--text-light);
124
+ }
125
+
126
+ .section {
127
+ padding-top: 2rem;
128
+ }
129
+
130
+ h3 {
131
+ margin: 0 0 1rem 0;
132
+ color: var(--primary-color);
133
+ }
134
+
135
+ .info-grid {
136
+ display: flex;
137
+ flex-direction: column;
138
+ gap: 1rem;
139
+ }
140
+
141
+ .info-item {
142
+ display: flex;
143
+ justify-content: space-between;
144
+ padding: 0.75rem;
145
+ background: var(--bg-secondary);
146
+ border-radius: var(--radius-sm);
147
+ }
148
+
149
+ .label {
150
+ font-weight: 600;
151
+ color: var(--text-color);
152
+ }
153
+
154
+ .value {
155
+ color: var(--text-light);
156
+ }
157
+
158
+ .section p {
159
+ margin: 0 0 1rem 0;
160
+ color: var(--text-light);
161
+ }
162
+ `;
163
+ }
164
+ }
@@ -0,0 +1,20 @@
1
+ import { Router } from 'snice';
2
+ import type { Principal } from './types/auth';
3
+ import { getUser } from './services/storage';
4
+ import { isAuthenticated } from './services/auth';
5
+ import { fetcher } from './fetcher';
6
+
7
+ const { page, initialize, navigate } = Router({
8
+ target: '#app',
9
+ type: 'hash',
10
+ layout: 'snice-layout',
11
+ fetcher,
12
+ context: {
13
+ principal: {
14
+ user: getUser(),
15
+ isAuthenticated: isAuthenticated()
16
+ } as Principal
17
+ }
18
+ });
19
+
20
+ export { page, initialize, navigate };
@@ -0,0 +1,48 @@
1
+ import type { LoginCredentials, LoginResponse, User } from '../types/auth';
2
+ import { setToken, setUser, clearToken, getToken } from './storage';
3
+ import { isTokenExpired } from './jwt';
4
+
5
+ // Mock API - replace with real API calls
6
+ const MOCK_USER: User = {
7
+ id: '1',
8
+ email: 'demo@example.com',
9
+ name: 'Demo User',
10
+ avatar: 'https://i.pravatar.cc/150?img=1'
11
+ };
12
+
13
+ const MOCK_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZW1haWwiOiJkZW1vQGV4YW1wbGUuY29tIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjk5OTk5OTk5OTl9.mock';
14
+
15
+ export async function login(credentials: LoginCredentials): Promise<LoginResponse> {
16
+ // Simulate API call
17
+ await new Promise(resolve => setTimeout(resolve, 500));
18
+
19
+ // Mock validation
20
+ if (credentials.email === 'demo@example.com' && credentials.password === 'demo') {
21
+ setToken(MOCK_TOKEN);
22
+ setUser(MOCK_USER);
23
+
24
+ return {
25
+ token: MOCK_TOKEN,
26
+ user: MOCK_USER
27
+ };
28
+ }
29
+
30
+ throw new Error('Invalid credentials');
31
+ }
32
+
33
+ export async function logout(): Promise<void> {
34
+ clearToken();
35
+ }
36
+
37
+ export function isAuthenticated(): boolean {
38
+ const token = getToken();
39
+ if (!token) return false;
40
+ return !isTokenExpired(token);
41
+ }
42
+
43
+ export async function refreshToken(): Promise<string> {
44
+ // Mock refresh - in real app, call refresh endpoint
45
+ await new Promise(resolve => setTimeout(resolve, 300));
46
+ setToken(MOCK_TOKEN);
47
+ return MOCK_TOKEN;
48
+ }