create-elit 3.2.7 → 3.2.8
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/dist/templates/README.md +23 -0
- package/dist/templates/elit.config.ts +59 -0
- package/dist/templates/package.json +14 -0
- package/dist/templates/public/favicon.svg +22 -0
- package/dist/templates/public/index.html +14 -0
- package/dist/templates/src/client.ts +15 -0
- package/dist/templates/src/components/Footer.ts +20 -0
- package/dist/templates/src/components/Header.ts +70 -0
- package/dist/templates/src/components/index.ts +2 -0
- package/dist/templates/src/main.ts +22 -0
- package/dist/templates/src/pages/ChatListPage.ts +144 -0
- package/dist/templates/src/pages/ChatPage.ts +186 -0
- package/dist/templates/src/pages/ForgotPasswordPage.ts +110 -0
- package/dist/templates/src/pages/HomePage.ts +166 -0
- package/dist/templates/src/pages/LoginPage.ts +182 -0
- package/dist/templates/src/pages/PrivateChatPage.ts +268 -0
- package/dist/templates/src/pages/ProfilePage.ts +342 -0
- package/dist/templates/src/pages/RegisterPage.ts +230 -0
- package/dist/templates/src/router.ts +30 -0
- package/dist/templates/src/server.ts +595 -0
- package/dist/templates/src/styles.ts +1181 -0
- package/dist/templates/tsconfig.json +23 -0
- package/package.json +2 -3
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# ELIT_PROJECT_NAME
|
|
2
|
+
|
|
3
|
+
A new Elit project created with create-elit.
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Visit http://localhost:3003 to view your app.
|
|
13
|
+
|
|
14
|
+
## Available Scripts
|
|
15
|
+
|
|
16
|
+
- `npm run dev` - Start development server with HMR
|
|
17
|
+
- `npm run build` - Build for production
|
|
18
|
+
- `npm run preview` - Preview production build
|
|
19
|
+
|
|
20
|
+
## Learn More
|
|
21
|
+
|
|
22
|
+
- [Elit Documentation](https://d-osc.github.io/elit)
|
|
23
|
+
- [GitHub Repository](https://github.com/d-osc/elit)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { server } from './src/server';
|
|
2
|
+
import { client } from './src/client';
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
dev: {
|
|
6
|
+
port: 3003,
|
|
7
|
+
host: 'localhost',
|
|
8
|
+
open: true,
|
|
9
|
+
logging: true,
|
|
10
|
+
clients: [{
|
|
11
|
+
root: '.',
|
|
12
|
+
basePath: '',
|
|
13
|
+
ssr: () => client,
|
|
14
|
+
api: server
|
|
15
|
+
}]
|
|
16
|
+
},
|
|
17
|
+
build: [{
|
|
18
|
+
entry: './src/main.ts',
|
|
19
|
+
outDir: './dist',
|
|
20
|
+
outFile: 'main.js',
|
|
21
|
+
format: 'esm',
|
|
22
|
+
minify: true,
|
|
23
|
+
sourcemap: true,
|
|
24
|
+
target: 'es2020',
|
|
25
|
+
copy: [
|
|
26
|
+
{
|
|
27
|
+
from: './public/index.html', to: './index.html',
|
|
28
|
+
transform: (content: string, config: { basePath: string; projectName: string; }) => {
|
|
29
|
+
// Replace script src
|
|
30
|
+
let html = content.replace('src="../src/main.ts"', 'src="main.js"');
|
|
31
|
+
|
|
32
|
+
// Replace project name placeholder
|
|
33
|
+
html = html.replace(/ELIT_PROJECT_NAME/g, config.projectName);
|
|
34
|
+
|
|
35
|
+
// Inject base tag if basePath is configured
|
|
36
|
+
if (config.basePath) {
|
|
37
|
+
const baseTag = `<base href="${config.basePath}/">`;
|
|
38
|
+
html = html.replace(
|
|
39
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1.0">',
|
|
40
|
+
`<meta name="viewport" content="width=device-width, initial-scale=1.0">\n ${baseTag}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return html;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{ from: './public/favicon.svg', to: './favicon.svg' }
|
|
48
|
+
]
|
|
49
|
+
}],
|
|
50
|
+
preview: {
|
|
51
|
+
port: 3000,
|
|
52
|
+
host: 'localhost',
|
|
53
|
+
open: false,
|
|
54
|
+
logging: true,
|
|
55
|
+
root: './dist',
|
|
56
|
+
basePath: '',
|
|
57
|
+
index: './index.html'
|
|
58
|
+
}
|
|
59
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ELIT_PROJECT_NAME",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "elit dev",
|
|
7
|
+
"build": "elit build",
|
|
8
|
+
"preview": "elit preview"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@types/node": "^25.0.9",
|
|
12
|
+
"elit": "^ELIT_VERSION"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" stop-color="#6366f1"/>
|
|
5
|
+
<stop offset="100%" stop-color="#8b5cf6"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
|
|
9
|
+
<!-- Clean background -->
|
|
10
|
+
<rect width="100" height="100" rx="20" fill="url(#grad)"/>
|
|
11
|
+
|
|
12
|
+
<!-- Simple E shape - 3 horizontal bars -->
|
|
13
|
+
<rect x="28" y="25" width="44" height="8" rx="4" fill="white"/>
|
|
14
|
+
<rect x="28" y="46" width="32" height="8" rx="4" fill="white"/>
|
|
15
|
+
<rect x="28" y="67" width="44" height="8" rx="4" fill="white"/>
|
|
16
|
+
|
|
17
|
+
<!-- Vertical connector -->
|
|
18
|
+
<rect x="28" y="25" width="8" height="50" rx="4" fill="white"/>
|
|
19
|
+
|
|
20
|
+
<!-- Single accent dot -->
|
|
21
|
+
<circle cx="72" cy="50" r="6" fill="white" opacity="0.5"/>
|
|
22
|
+
</svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
<title>ELIT_PROJECT_NAME</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
8
|
+
<meta name="description" content="Built with Elit - Full-stack TypeScript framework">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
<script type="module" src="../src/main.ts"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { div, html, head, body, title, link, script, meta } from 'elit/el';
|
|
2
|
+
|
|
3
|
+
export const client = html(
|
|
4
|
+
head(
|
|
5
|
+
title('ELIT_PROJECT_NAME - Elit App'),
|
|
6
|
+
link({ rel: 'icon', type: 'image/svg+xml', href: 'public/favicon.svg' }),
|
|
7
|
+
meta({ charset: 'UTF-8' }),
|
|
8
|
+
meta({ name: 'viewport', content: 'width=device-width, initial-scale=1.0' }),
|
|
9
|
+
meta({ name: 'description', content: 'Elit - Full-stack TypeScript framework with dev server, HMR, routing, SSR, and REST API.' })
|
|
10
|
+
),
|
|
11
|
+
body(
|
|
12
|
+
div({ id: 'app' }),
|
|
13
|
+
script({ type: 'module', src: '/src/main.js' })
|
|
14
|
+
)
|
|
15
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { footer, div, p, a } from 'elit/el';
|
|
2
|
+
|
|
3
|
+
export function Footer() {
|
|
4
|
+
return footer({ className: 'footer' },
|
|
5
|
+
div({ className: 'footer-content' },
|
|
6
|
+
div({ className: 'footer-section' },
|
|
7
|
+
p({ className: 'footer-title' }, 'My Elit App'),
|
|
8
|
+
p({ className: 'footer-text' }, 'Built with Elit Framework')
|
|
9
|
+
),
|
|
10
|
+
div({ className: 'footer-section' },
|
|
11
|
+
a({ href: 'https://github.com', target: '_blank', className: 'footer-link' }, 'GitHub'),
|
|
12
|
+
a({ href: '#', className: 'footer-link' }, 'Documentation'),
|
|
13
|
+
a({ href: '#', className: 'footer-link' }, 'Support')
|
|
14
|
+
),
|
|
15
|
+
div({ className: 'footer-section' },
|
|
16
|
+
p({ className: 'footer-copyright' }, '© 2026 My Elit App. All rights reserved.')
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { header, nav, div, a, h1, button, span } from 'elit/el';
|
|
2
|
+
import { createState, reactive } from 'elit/state';
|
|
3
|
+
import type { Router } from 'elit';
|
|
4
|
+
|
|
5
|
+
export function Header(router: Router) {
|
|
6
|
+
// Check if user is logged in (has token in localStorage)
|
|
7
|
+
const isLoggedIn = createState(!!localStorage.getItem('token'));
|
|
8
|
+
const user = createState(() => {
|
|
9
|
+
const userStr = localStorage.getItem('user');
|
|
10
|
+
return userStr ? JSON.parse(userStr) : null;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Listen for storage changes to update header when login/logout happens
|
|
14
|
+
const handleStorageChange = (e: StorageEvent) => {
|
|
15
|
+
if (e.key === 'token' || e.key === 'user') {
|
|
16
|
+
isLoggedIn.value = !!localStorage.getItem('token');
|
|
17
|
+
const userStr = localStorage.getItem('user');
|
|
18
|
+
user.value = userStr ? JSON.parse(userStr) : null;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Also listen for custom storage events (same-tab updates)
|
|
23
|
+
const handleCustomStorageChange = () => {
|
|
24
|
+
isLoggedIn.value = !!localStorage.getItem('token');
|
|
25
|
+
const userStr = localStorage.getItem('user');
|
|
26
|
+
user.value = userStr ? JSON.parse(userStr) : null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
window.addEventListener('storage', handleStorageChange);
|
|
30
|
+
window.addEventListener('elit:storage', handleCustomStorageChange);
|
|
31
|
+
|
|
32
|
+
const handleLogout = () => {
|
|
33
|
+
localStorage.removeItem('token');
|
|
34
|
+
localStorage.removeItem('user');
|
|
35
|
+
isLoggedIn.value = false;
|
|
36
|
+
router.push('/');
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return header({ className: 'header' },
|
|
40
|
+
nav({ className: 'nav' },
|
|
41
|
+
div({ className: 'nav-brand' },
|
|
42
|
+
a({ href: '#/', className: 'brand-link' },
|
|
43
|
+
h1({ className: 'brand-title' }, 'ELIT_PROJECT_NAME')
|
|
44
|
+
)
|
|
45
|
+
),
|
|
46
|
+
|
|
47
|
+
reactive(isLoggedIn, (loggedIn) => {
|
|
48
|
+
if (loggedIn) {
|
|
49
|
+
return div({ className: 'nav-menu' },
|
|
50
|
+
a({ href: '#/chat/list', className: 'nav-link' }, 'Messages'),
|
|
51
|
+
a({ href: '#/profile', className: 'nav-link' }, 'Profile'),
|
|
52
|
+
reactive(user, (u) => u ? span({ className: 'nav-user' }, `Welcome, ${u.name}`) : null),
|
|
53
|
+
button({
|
|
54
|
+
className: 'btn btn-secondary btn-sm',
|
|
55
|
+
onclick: handleLogout
|
|
56
|
+
}, 'Logout')
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return div({ className: 'nav-menu' },
|
|
61
|
+
a({ href: '#/login', className: 'nav-link' }, 'Login'),
|
|
62
|
+
button({
|
|
63
|
+
className: 'btn btn-primary btn-sm',
|
|
64
|
+
onclick: () => router.push('/register')
|
|
65
|
+
}, 'Sign Up')
|
|
66
|
+
);
|
|
67
|
+
})
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { div, main } from 'elit/el';
|
|
2
|
+
import { reactive } from 'elit/state';
|
|
3
|
+
import { dom } from 'elit/dom';
|
|
4
|
+
import { injectStyles } from './styles';
|
|
5
|
+
import { router, RouterView } from './router';
|
|
6
|
+
import { Header } from './components/Header';
|
|
7
|
+
import { Footer } from './components/Footer';
|
|
8
|
+
|
|
9
|
+
injectStyles()
|
|
10
|
+
// Create reactive state (shared between SSR and client)
|
|
11
|
+
// Main App
|
|
12
|
+
const App = () =>
|
|
13
|
+
div(
|
|
14
|
+
Header(router),
|
|
15
|
+
main(
|
|
16
|
+
reactive(router.currentRoute, () => RouterView())
|
|
17
|
+
),
|
|
18
|
+
Footer()
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// Render
|
|
22
|
+
dom.render('#app', App());
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { div, h2, h3, p, button, span, input } from 'elit/el';
|
|
2
|
+
import { createState, reactive } from 'elit/state';
|
|
3
|
+
import type { Router } from 'elit';
|
|
4
|
+
|
|
5
|
+
interface User {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
email: string;
|
|
9
|
+
bio: string;
|
|
10
|
+
avatar: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ChatListPage(router: Router) {
|
|
14
|
+
// Check if user is logged in
|
|
15
|
+
const token = localStorage.getItem('token');
|
|
16
|
+
const user = localStorage.getItem('user');
|
|
17
|
+
|
|
18
|
+
if (!token || !user) {
|
|
19
|
+
router.push('/login');
|
|
20
|
+
return div({ className: 'chat-page' }, p('Redirecting to login...'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const userData = JSON.parse(user);
|
|
24
|
+
|
|
25
|
+
const users = createState<User[]>([]);
|
|
26
|
+
const searchQuery = createState('');
|
|
27
|
+
const isLoading = createState(false);
|
|
28
|
+
const error = createState('');
|
|
29
|
+
|
|
30
|
+
// Load users
|
|
31
|
+
const loadUsers = async () => {
|
|
32
|
+
isLoading.value = true;
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch('/api/users', {
|
|
35
|
+
method: 'GET',
|
|
36
|
+
headers: {
|
|
37
|
+
'Authorization': `Bearer ${token}`
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (response.ok) {
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
console.log('Current user ID:', userData.id);
|
|
44
|
+
console.log('All users:', data.users);
|
|
45
|
+
|
|
46
|
+
// Filter out current user
|
|
47
|
+
const allUsers = data.users || [];
|
|
48
|
+
users.value = allUsers.filter((u: User) => u.id !== userData.id);
|
|
49
|
+
|
|
50
|
+
console.log('Filtered users:', users.value);
|
|
51
|
+
} else {
|
|
52
|
+
error.value = 'Failed to load users';
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error('Error loading users:', err);
|
|
56
|
+
error.value = 'Network error. Please try again.';
|
|
57
|
+
} finally {
|
|
58
|
+
isLoading.value = false;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Load users on mount
|
|
63
|
+
loadUsers();
|
|
64
|
+
|
|
65
|
+
// Open chat with specific user
|
|
66
|
+
const openChat = (userId: string) => {
|
|
67
|
+
router.push(`/chat/dm/${userId}`);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return div({ className: 'chat-list-page' },
|
|
71
|
+
div({ className: 'chat-container' },
|
|
72
|
+
// Header
|
|
73
|
+
div({ className: 'chat-header' },
|
|
74
|
+
div({ className: 'chat-header-info' },
|
|
75
|
+
h2({ className: 'chat-title' }, 'Messages'),
|
|
76
|
+
p({ className: 'chat-subtitle' }, 'Select a user to start chatting')
|
|
77
|
+
),
|
|
78
|
+
button({
|
|
79
|
+
className: 'btn btn-secondary btn-sm',
|
|
80
|
+
onclick: () => router.push('/profile')
|
|
81
|
+
}, 'Back to Profile')
|
|
82
|
+
),
|
|
83
|
+
|
|
84
|
+
// Search
|
|
85
|
+
div({ className: 'chat-search' },
|
|
86
|
+
input({
|
|
87
|
+
type: 'text',
|
|
88
|
+
className: 'chat-input',
|
|
89
|
+
placeholder: 'Search users...',
|
|
90
|
+
oninput: (e: Event) => {
|
|
91
|
+
searchQuery.value = (e.target as HTMLInputElement).value;
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
),
|
|
95
|
+
|
|
96
|
+
// Error Display
|
|
97
|
+
reactive(error, (err) => err ? div({ className: 'auth-error' }, err) : null),
|
|
98
|
+
|
|
99
|
+
// Users List
|
|
100
|
+
div({ className: 'chat-users-list' },
|
|
101
|
+
reactive(isLoading, (loading) => {
|
|
102
|
+
if (loading) {
|
|
103
|
+
return div({ className: 'chat-loading' }, p('Loading users...'));
|
|
104
|
+
}
|
|
105
|
+
return reactive(users, (userList) => {
|
|
106
|
+
const filteredUsers = searchQuery.value
|
|
107
|
+
? userList.filter(u =>
|
|
108
|
+
u.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
|
109
|
+
u.email.toLowerCase().includes(searchQuery.value.toLowerCase())
|
|
110
|
+
)
|
|
111
|
+
: userList;
|
|
112
|
+
|
|
113
|
+
if (filteredUsers.length === 0) {
|
|
114
|
+
return div({ className: 'chat-empty' },
|
|
115
|
+
p({ className: 'chat-empty-text' }, searchQuery.value ? 'No users found' : 'No other users yet')
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return div({ className: 'chat-users-grid' },
|
|
120
|
+
...filteredUsers.map(u =>
|
|
121
|
+
div({
|
|
122
|
+
className: 'chat-user-card',
|
|
123
|
+
onclick: () => openChat(u.id)
|
|
124
|
+
},
|
|
125
|
+
div({ className: 'chat-user-avatar' },
|
|
126
|
+
span({ className: 'chat-avatar-text' }, u.name.charAt(0).toUpperCase())
|
|
127
|
+
),
|
|
128
|
+
div({ className: 'chat-user-info' },
|
|
129
|
+
h3({ className: 'chat-user-name' }, u.name),
|
|
130
|
+
p({ className: 'chat-user-email' }, u.email),
|
|
131
|
+
u.bio ? p({ className: 'chat-user-bio' }, u.bio) : null
|
|
132
|
+
),
|
|
133
|
+
button({
|
|
134
|
+
className: 'btn btn-primary btn-sm chat-chat-button'
|
|
135
|
+
}, 'Chat')
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
})
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { div, h2, p, input, button, span, form } from 'elit/el';
|
|
2
|
+
import { createState, reactive, createSharedState } from 'elit/state';
|
|
3
|
+
import type { Router } from 'elit';
|
|
4
|
+
|
|
5
|
+
interface ChatMessage {
|
|
6
|
+
id: string;
|
|
7
|
+
roomId: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
userName: string;
|
|
10
|
+
text: string;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ChatPage(router: Router) {
|
|
15
|
+
// Check if user is logged in
|
|
16
|
+
const token = localStorage.getItem('token');
|
|
17
|
+
const user = localStorage.getItem('user');
|
|
18
|
+
|
|
19
|
+
if (!token || !user) {
|
|
20
|
+
router.push('/login');
|
|
21
|
+
return div({ className: 'chat-page' }, p('Redirecting to login...'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const userData = JSON.parse(user);
|
|
25
|
+
|
|
26
|
+
// Create shared state for chat messages with WebSocket sync
|
|
27
|
+
const chatMessages = createSharedState<ChatMessage[]>('chat:general', [], `ws://${location.host}`);
|
|
28
|
+
const newMessage = createState('');
|
|
29
|
+
const error = createState('');
|
|
30
|
+
const isLoading = createState(false);
|
|
31
|
+
|
|
32
|
+
// Load initial messages from API
|
|
33
|
+
const loadMessages = async () => {
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch('/api/chat/messages?roomId=general', {
|
|
36
|
+
method: 'GET',
|
|
37
|
+
headers: {
|
|
38
|
+
'Authorization': `Bearer ${token}`
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (response.ok) {
|
|
43
|
+
const data = await response.json();
|
|
44
|
+
chatMessages.value = data.messages || [];
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error('Failed to load messages:', err);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Load messages on mount
|
|
52
|
+
loadMessages();
|
|
53
|
+
|
|
54
|
+
const handleSendMessage = async (e: Event) => {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
|
|
57
|
+
const messageText = newMessage.value.trim();
|
|
58
|
+
if (!messageText) {
|
|
59
|
+
error.value = 'Please enter a message';
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
newMessage.value = '';
|
|
64
|
+
error.value = '';
|
|
65
|
+
isLoading.value = true;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch('/api/chat/send', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
'Authorization': `Bearer ${token}`
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
roomId: 'general',
|
|
76
|
+
message: messageText
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (response.ok) {
|
|
81
|
+
const data = await response.json();
|
|
82
|
+
|
|
83
|
+
// Add the sent message to shared state (syncs with other clients)
|
|
84
|
+
if (data.message) {
|
|
85
|
+
chatMessages.value = [...chatMessages.value, data.message];
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
const errorData = await response.json();
|
|
89
|
+
error.value = errorData.error || 'Failed to send message';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
isLoading.value = false;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
error.value = 'Network error. Please try again.';
|
|
95
|
+
isLoading.value = false;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Cleanup shared state on unmount
|
|
100
|
+
const cleanup = () => {
|
|
101
|
+
chatMessages.disconnect();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Poll for new messages every 2 seconds (fallback if WebSocket is not available)
|
|
105
|
+
const pollInterval = setInterval(() => {
|
|
106
|
+
if (!chatMessages.value || chatMessages.value.length === 0) {
|
|
107
|
+
loadMessages();
|
|
108
|
+
}
|
|
109
|
+
}, 2000);
|
|
110
|
+
|
|
111
|
+
return div({ className: 'chat-page' },
|
|
112
|
+
div({ className: 'chat-container' },
|
|
113
|
+
// Chat Header
|
|
114
|
+
div({ className: 'chat-header' },
|
|
115
|
+
div({ className: 'chat-header-info' },
|
|
116
|
+
h2({ className: 'chat-title' }, 'Chat Room'),
|
|
117
|
+
p({ className: 'chat-subtitle' }, `Logged in as ${userData.name}`)
|
|
118
|
+
),
|
|
119
|
+
button({
|
|
120
|
+
className: 'btn btn-secondary btn-sm',
|
|
121
|
+
onclick: () => {
|
|
122
|
+
clearInterval(pollInterval);
|
|
123
|
+
cleanup();
|
|
124
|
+
router.push('/profile');
|
|
125
|
+
}
|
|
126
|
+
}, 'Back to Profile')
|
|
127
|
+
),
|
|
128
|
+
|
|
129
|
+
// Messages Area
|
|
130
|
+
div({ className: 'chat-messages' },
|
|
131
|
+
reactive(chatMessages.state, (msgs) => {
|
|
132
|
+
if (!msgs || msgs.length === 0) {
|
|
133
|
+
return div({ className: 'chat-empty' },
|
|
134
|
+
p({ className: 'chat-empty-text' }, 'No messages yet. Start the conversation!')
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return div({ className: 'chat-messages-list' },
|
|
139
|
+
...msgs.map(msg =>
|
|
140
|
+
div({
|
|
141
|
+
className: `chat-message ${msg.userId === userData.id ? 'chat-message-user' : 'chat-message-other'}`
|
|
142
|
+
},
|
|
143
|
+
div({ className: 'chat-message-content' },
|
|
144
|
+
span({ className: 'chat-message-sender' },
|
|
145
|
+
msg.userId === userData.id ? 'You' : msg.userName
|
|
146
|
+
),
|
|
147
|
+
p({ className: 'chat-message-text' }, msg.text)
|
|
148
|
+
),
|
|
149
|
+
span({ className: 'chat-message-time' },
|
|
150
|
+
new Date(msg.timestamp).toLocaleTimeString()
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
);
|
|
155
|
+
}),
|
|
156
|
+
isLoading.value ? div({ className: 'chat-typing' },
|
|
157
|
+
span({ className: 'typing-dot' }, '.'),
|
|
158
|
+
span({ className: 'typing-dot' }, '.'),
|
|
159
|
+
span({ className: 'typing-dot' }, '.')
|
|
160
|
+
) : null
|
|
161
|
+
),
|
|
162
|
+
|
|
163
|
+
// Error Display
|
|
164
|
+
reactive(error, (err) => err ? div({ className: 'auth-error' }, err) : null),
|
|
165
|
+
|
|
166
|
+
// Input Area
|
|
167
|
+
form({ className: 'chat-input-area', onsubmit: handleSendMessage },
|
|
168
|
+
input({
|
|
169
|
+
type: 'text',
|
|
170
|
+
className: 'chat-input',
|
|
171
|
+
placeholder: 'Type your message...',
|
|
172
|
+
value: newMessage.value,
|
|
173
|
+
oninput: (e: Event) => {
|
|
174
|
+
newMessage.value = (e.target as HTMLInputElement).value;
|
|
175
|
+
error.value = '';
|
|
176
|
+
}
|
|
177
|
+
}),
|
|
178
|
+
button({
|
|
179
|
+
type: 'submit',
|
|
180
|
+
className: 'btn btn-primary',
|
|
181
|
+
disabled: isLoading.value || !newMessage.value.trim()
|
|
182
|
+
}, isLoading.value ? 'Sending...' : 'Send')
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
);
|
|
186
|
+
}
|