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.
- package/README.md +2 -2
- package/bin/snice.js +4 -5
- package/bin/templates/CLAUDE.md +25 -3
- package/bin/templates/pwa/README.md +188 -0
- package/bin/templates/pwa/global.d.ts +10 -0
- package/bin/templates/pwa/index.html +16 -0
- package/bin/templates/pwa/package.json +32 -0
- package/bin/templates/pwa/public/icons/.gitkeep +6 -0
- package/bin/templates/pwa/src/daemons/notifications.ts +148 -0
- package/bin/templates/pwa/src/fetcher.ts +15 -0
- package/bin/templates/pwa/src/guards/auth.ts +12 -0
- package/bin/templates/pwa/src/main.ts +42 -0
- package/bin/templates/pwa/src/middleware/auth.ts +16 -0
- package/bin/templates/pwa/src/middleware/error.ts +36 -0
- package/bin/templates/pwa/src/middleware/retry.ts +31 -0
- package/bin/templates/pwa/src/pages/dashboard.ts +143 -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 +164 -0
- package/bin/templates/pwa/src/router.ts +20 -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/tests/helpers/test-utils.ts +84 -0
- package/bin/templates/pwa/tests/middleware/auth.test.ts +67 -0
- package/bin/templates/pwa/tests/middleware/error.test.ts +105 -0
- package/bin/templates/pwa/tests/middleware/retry.test.ts +103 -0
- package/bin/templates/pwa/tests/services/auth.test.ts +89 -0
- package/bin/templates/pwa/tests/services/jwt.test.ts +76 -0
- package/bin/templates/pwa/tests/services/storage.test.ts +69 -0
- package/bin/templates/{social → pwa}/tsconfig.json +11 -10
- package/bin/templates/pwa/vite.config.ts +94 -0
- package/bin/templates/{social/vite.config.ts → pwa/vitest.config.ts} +12 -17
- 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/timer.md +43 -0
- package/docs/ai/patterns.md +48 -1
- package/docs/components/music-player.md +314 -0
- package/docs/components/timer.md +143 -0
- package/docs/fetcher.md +447 -0
- package/docs/routing.md +11 -8
- package/package.json +2 -1
- package/bin/templates/social/README.md +0 -42
- package/bin/templates/social/global.d.ts +0 -14
- package/bin/templates/social/index.html +0 -13
- package/bin/templates/social/package.json +0 -21
- package/bin/templates/social/src/main.ts +0 -33
- package/bin/templates/social/src/pages/feed-page.ts +0 -111
- package/bin/templates/social/src/pages/messages-page.ts +0 -102
- package/bin/templates/social/src/pages/not-found-page.ts +0 -46
- package/bin/templates/social/src/pages/profile-page.ts +0 -99
- package/bin/templates/social/src/pages/settings-page.ts +0 -119
- package/bin/templates/social/src/router.ts +0 -9
- package/bin/templates/social/src/styles/global.css +0 -156
- /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
|
+
}
|