Dhurgh commited on
Commit
c9cf26b
Β·
1 Parent(s): 272dfc6

feat: Add video call modal, profile visiting, friend requests, QR scanner, and hover effects improvements

Browse files
Files changed (6) hide show
  1. .env.example +2 -2
  2. client.js +0 -0
  3. index.html +1584 -77
  4. package-lock.json +293 -218
  5. package.json +3 -2
  6. server.js +381 -219
.env.example CHANGED
@@ -2,8 +2,8 @@ PORT=3000
2
  HOST=0.0.0.0
3
  NODE_ENV=development
4
 
5
- # MongoDB Connection
6
- MONGODB_URI=mongodb://localhost:27017/discord-app
7
 
8
  # JWT Secret
9
  JWT_SECRET=your_super_secret_jwt_key_change_this_in_production
 
2
  HOST=0.0.0.0
3
  NODE_ENV=development
4
 
5
+ # PostgreSQL Connection (Railway will auto-provide DATABASE_URL)
6
+ DATABASE_URL=postgres://postgres:password@localhost:5432/discord-app
7
 
8
  # JWT Secret
9
  JWT_SECRET=your_super_secret_jwt_key_change_this_in_production
client.js CHANGED
The diff for this file is too large to render. See raw diff
 
index.html CHANGED
@@ -20,12 +20,29 @@
20
  --sidebar-bg: #2f3136;
21
  --text-primary: #ffffff;
22
  --text-secondary: #b9bbbe;
 
23
  --border-color: #202225;
24
  --shadow: 0 2px 10px rgba(0,0,0,0.3);
25
  --success: #43b581;
26
  --danger: #f04747;
27
  }
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  body {
30
  font-family: 'Whitney', 'Helvetica Neue', Helvetica, Arial, sans-serif;
31
  background: var(--bg-color);
@@ -132,6 +149,44 @@
132
  text-decoration: underline;
133
  }
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  /* ==================== MAIN LAYOUT ==================== */
136
  .main-container {
137
  display: grid;
@@ -179,6 +234,13 @@
179
  background: var(--primary-color);
180
  }
181
 
 
 
 
 
 
 
 
182
  /* ==================== CHANNEL SIDEBAR ==================== */
183
  .channel-sidebar {
184
  background: var(--sidebar-bg);
@@ -210,8 +272,9 @@
210
 
211
  .channels-list {
212
  flex: 1;
213
- overflow-y: auto;
214
  padding: 8px 0;
 
215
  }
216
 
217
  .channel-item {
@@ -238,11 +301,49 @@
238
  font-size: 16px;
239
  }
240
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  /* ==================== MAIN CHAT AREA ==================== */
242
  .chat-area {
 
243
  display: flex;
244
  flex-direction: column;
245
  background: var(--chat-bg);
 
 
246
  }
247
 
248
  .chat-header {
@@ -266,9 +367,19 @@
266
 
267
  .chat-header-actions {
268
  display: flex;
 
269
  gap: 12px;
270
  }
271
 
 
 
 
 
 
 
 
 
 
272
  .header-btn {
273
  background: none;
274
  border: none;
@@ -282,33 +393,68 @@
282
  color: var(--text-primary);
283
  }
284
 
 
 
 
 
 
 
 
 
285
  #messages-container {
286
  flex: 1;
 
287
  overflow-y: auto;
288
  padding: 16px 24px;
 
 
289
  display: flex;
290
  flex-direction: column;
291
  gap: 12px;
292
  scroll-behavior: smooth;
293
- scroll-snap-type: y proximity;
294
  -webkit-overflow-scrolling: touch;
295
  }
296
 
 
297
  #messages-container::-webkit-scrollbar {
298
- width: 8px;
 
299
  }
300
 
301
- #messages-container::-webkit-scrollbar-track {
302
- background: var(--chat-bg);
303
  }
304
 
305
- #messages-container::-webkit-scrollbar-thumb {
306
- background: var(--secondary-color);
307
- border-radius: 4px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  }
309
 
310
- #messages-container::-webkit-scrollbar-thumb:hover {
311
- background: #72767d;
312
  }
313
 
314
  .message {
@@ -348,14 +494,81 @@
348
  margin-bottom: 4px;
349
  }
350
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  .message-username {
352
  font-weight: 600;
353
  color: var(--primary-color);
354
  }
355
 
356
  .message-time {
357
- font-size: 12px;
358
  color: var(--text-secondary);
 
 
359
  }
360
 
361
  .message-text {
@@ -365,26 +578,96 @@
365
 
366
  .message-media {
367
  margin-top: 8px;
368
- border-radius: 4px;
369
  max-width: 400px;
 
370
  }
371
 
372
  .message-media img {
373
  max-width: 100%;
374
- border-radius: 4px;
375
- cursor: pointer;
376
  }
377
 
 
 
 
 
 
 
 
378
  .message-media video {
379
  max-width: 100%;
380
  border-radius: 4px;
381
  }
382
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  .input-area {
384
  padding: 16px 24px;
385
  border-top: 1px solid var(--border-color);
386
  display: flex;
387
  gap: 12px;
 
 
 
388
  }
389
 
390
  .input-area input {
@@ -419,7 +702,9 @@
419
  }
420
 
421
  .input-btn:hover {
422
- background: #4752c4;
 
 
423
  }
424
 
425
  .input-file {
@@ -444,8 +729,9 @@
444
 
445
  .members-list {
446
  flex: 1;
447
- overflow-y: auto;
448
  padding: 8px;
 
449
  }
450
 
451
  .member-item {
@@ -617,6 +903,22 @@
617
  background: #4752c4;
618
  }
619
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
620
  .shop-item {
621
  background: var(--bg-color);
622
  border: 1px solid var(--border-color);
@@ -676,90 +978,877 @@
676
  margin-bottom: 16px;
677
  }
678
 
679
- /* ==================== MODAL ==================== */
680
- .modal {
681
  position: fixed;
682
- top: 0;
683
- left: 0;
684
- width: 100%;
685
- height: 100%;
686
- background: rgba(0,0,0,0.6);
687
  display: flex;
688
- align-items: center;
689
- justify-content: center;
690
- z-index: 1500;
691
- }
692
-
693
- .modal.hidden {
694
- display: none;
695
  }
696
 
697
- .modal-content {
698
  background: var(--chat-bg);
699
- padding: 32px;
 
700
  border-radius: 8px;
701
- max-width: 500px;
702
- width: 90%;
703
- box-shadow: 0 10px 40px rgba(0,0,0,0.5);
704
- max-height: 90vh;
705
- overflow-y: auto;
 
706
  }
707
 
708
- /* ==================== SCROLLBAR ==================== */
709
- ::-webkit-scrollbar {
710
- width: 8px;
711
  }
712
 
713
- ::-webkit-scrollbar-track {
714
- background: transparent;
715
  }
716
 
717
- ::-webkit-scrollbar-thumb {
718
- background: var(--secondary-color);
719
- border-radius: 4px;
720
  }
721
 
722
- ::-webkit-scrollbar-thumb:hover {
723
- background: #5a5d60;
724
  }
725
 
726
- /* ==================== RESPONSIVE ==================== */
727
- @media (max-width: 1200px) {
728
- .main-container {
729
- grid-template-columns: 72px 240px 1fr;
730
- }
731
 
732
- .members-sidebar {
733
- display: none;
734
- }
 
735
 
736
- .profile-panel {
737
- width: 300px;
738
- transform: translateX(300px);
739
- }
740
 
741
- .profile-panel.show {
742
- transform: translateX(0);
743
- }
744
  }
745
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
746
  @media (max-width: 768px) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
747
  .main-container {
748
  grid-template-columns: 72px 1fr;
 
 
749
  }
750
 
751
- .channel-sidebar {
752
- display: none;
753
- position: fixed;
754
- left: 72px;
755
- top: 0;
756
- width: 240px;
757
  height: 100%;
758
- z-index: 999;
 
759
  }
760
 
761
- .channel-sidebar.show {
762
- display: flex;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
763
  }
764
  }
765
  </style>
@@ -784,8 +1873,22 @@
784
  <label>Password</label>
785
  <input type="password" id="password" placeholder="Enter your password" required>
786
  </div>
 
 
 
787
  <button type="submit" class="auth-button" id="authButton">Login</button>
788
  </form>
 
 
 
 
 
 
 
 
 
 
 
789
  <div class="auth-toggle">
790
  <span>Don't have an account? <button type="button" onclick="toggleAuthMode()">Register</button></span>
791
  </div>
@@ -805,13 +1908,43 @@
805
  <div class="guild-item" onclick="showSection('shop')" title="Shop">
806
  <i class="fas fa-store"></i>
807
  </div>
 
 
 
 
 
 
 
 
 
 
808
  </div>
809
 
810
  <!-- CHANNEL SIDEBAR -->
811
  <div class="channel-sidebar" id="channelSidebar">
812
  <div class="sidebar-header">
813
  <h3>Messages</h3>
814
- <button onclick="toggleChannelSidebar()"><i class="fas fa-times"></i></button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
815
  </div>
816
  <div class="channels-list" id="channelsList">
817
  <!-- Direct messages will be populated here by loadFriendsForDM() -->
@@ -828,28 +1961,66 @@
828
  <div class="chat-header-title" id="chatTitle">πŸ’¬ Select a friend to chat</div>
829
  </div>
830
  <div class="chat-header-actions">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
831
  <button class="header-btn" onclick="openProfile()" title="Profile">
832
  <i class="fas fa-user-circle"></i>
833
  </button>
834
- <button class="header-btn" onclick="logout()" title="Logout">
835
  <i class="fas fa-sign-out-alt"></i>
836
  </button>
837
  </div>
838
  </div>
839
 
840
  <div id="messages-container"></div>
 
 
 
 
 
 
 
 
 
 
 
 
841
 
842
  <div id="typingIndicator" style="display: none; padding: 10px; color: var(--text-secondary); font-size: 12px; font-style: italic;"></div>
843
 
844
  <div class="input-area">
845
- <button class="input-btn" onclick="document.getElementById('fileInput').click()" title="Upload file">
846
- <i class="fas fa-paperclip"></i>
 
 
 
 
 
 
 
 
 
847
  </button>
848
  <input type="text" id="messageInput" placeholder="Message..." onkeypress="handleMessageKeypress(event)">
849
  <button class="input-btn" onclick="sendMessage()" title="Send">
850
  <i class="fas fa-paper-plane"></i>
851
  </button>
852
- <input type="file" id="fileInput" class="input-file" onchange="handleFileUpload(event)" accept="image/*,video/*">
 
853
  </div>
854
  </div>
855
 
@@ -882,6 +2053,11 @@
882
  <input type="file" id="profileImageInput" class="input-file" onchange="uploadProfileImage(event)" accept="image/*" style="display: none;">
883
  <p style="font-size: 12px; color: var(--text-secondary); margin-top: 5px;">Click to upload your profile picture</p>
884
  </div>
 
 
 
 
 
885
  <div class="form-field">
886
  <label>Username</label>
887
  <input type="text" id="editUsername" placeholder="Username">
@@ -890,6 +2066,10 @@
890
  <label>Bio</label>
891
  <textarea id="editBio" placeholder="Tell us about yourself"></textarea>
892
  </div>
 
 
 
 
893
  <button class="save-btn" onclick="saveProfile()">Save Profile</button>
894
  </div>
895
 
@@ -922,7 +2102,334 @@
922
  </div>
923
  </div>
924
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
925
  <script src="/socket.io/socket.io.js"></script>
926
- <script src="client.js"></script>
 
927
  </body>
928
  </html>
 
20
  --sidebar-bg: #2f3136;
21
  --text-primary: #ffffff;
22
  --text-secondary: #b9bbbe;
23
+ --ui-text-color: #ffffff;
24
  --border-color: #202225;
25
  --shadow: 0 2px 10px rgba(0,0,0,0.3);
26
  --success: #43b581;
27
  --danger: #f04747;
28
  }
29
 
30
+ /* Light Theme */
31
+ [data-theme="light"] {
32
+ --primary-color: #5865f2;
33
+ --secondary-color: #99aab5;
34
+ --bg-color: #ffffff;
35
+ --chat-bg: #f2f3f5;
36
+ --sidebar-bg: #f9f9f9;
37
+ --text-primary: #2e3338;
38
+ --text-secondary: #747f8d;
39
+ --ui-text-color: #2e3338;
40
+ --border-color: #e3e5e8;
41
+ --shadow: 0 2px 10px rgba(0,0,0,0.1);
42
+ --success: #43b581;
43
+ --danger: #f04747;
44
+ }
45
+
46
  body {
47
  font-family: 'Whitney', 'Helvetica Neue', Helvetica, Arial, sans-serif;
48
  background: var(--bg-color);
 
149
  text-decoration: underline;
150
  }
151
 
152
+ .auth-inline-link {
153
+ margin-top: -10px;
154
+ margin-bottom: 14px;
155
+ text-align: right;
156
+ }
157
+
158
+ .auth-inline-link button {
159
+ background: none;
160
+ border: none;
161
+ color: var(--primary-color);
162
+ cursor: pointer;
163
+ font-size: 13px;
164
+ text-decoration: underline;
165
+ padding: 0;
166
+ }
167
+
168
+ .reset-password-panel {
169
+ margin-top: 14px;
170
+ padding-top: 12px;
171
+ border-top: 1px solid var(--border-color);
172
+ display: none;
173
+ }
174
+
175
+ .reset-password-panel.show {
176
+ display: block;
177
+ }
178
+
179
+ .auth-mini-button {
180
+ width: 100%;
181
+ margin-top: 8px;
182
+ padding: 10px;
183
+ border: 1px solid var(--primary-color);
184
+ border-radius: 6px;
185
+ background: transparent;
186
+ color: var(--primary-color);
187
+ cursor: pointer;
188
+ }
189
+
190
  /* ==================== MAIN LAYOUT ==================== */
191
  .main-container {
192
  display: grid;
 
234
  background: var(--primary-color);
235
  }
236
 
237
+ .guild-divider {
238
+ width: 32px;
239
+ height: 2px;
240
+ background: var(--border-color);
241
+ margin: 8px 0;
242
+ }
243
+
244
  /* ==================== CHANNEL SIDEBAR ==================== */
245
  .channel-sidebar {
246
  background: var(--sidebar-bg);
 
272
 
273
  .channels-list {
274
  flex: 1;
275
+ overflow-y: scroll;
276
  padding: 8px 0;
277
+ scrollbar-gutter: stable;
278
  }
279
 
280
  .channel-item {
 
301
  font-size: 16px;
302
  }
303
 
304
+ .channel-avatar {
305
+ width: 32px;
306
+ height: 32px;
307
+ border-radius: 50%;
308
+ background: linear-gradient(135deg, var(--primary-color), #4752c4);
309
+ display: flex;
310
+ align-items: center;
311
+ justify-content: center;
312
+ color: white;
313
+ font-weight: 600;
314
+ font-size: 14px;
315
+ flex-shrink: 0;
316
+ }
317
+
318
+ .member-avatar {
319
+ width: 32px;
320
+ height: 32px;
321
+ border-radius: 50%;
322
+ background: linear-gradient(135deg, var(--primary-color), #4752c4);
323
+ display: flex;
324
+ align-items: center;
325
+ justify-content: center;
326
+ color: white;
327
+ font-weight: 600;
328
+ font-size: 14px;
329
+ flex-shrink: 0;
330
+ }
331
+
332
+ .channel-section-title {
333
+ padding: 10px;
334
+ color: var(--text-secondary);
335
+ font-size: 12px;
336
+ font-weight: bold;
337
+ }
338
+
339
  /* ==================== MAIN CHAT AREA ==================== */
340
  .chat-area {
341
+ position: relative;
342
  display: flex;
343
  flex-direction: column;
344
  background: var(--chat-bg);
345
+ min-height: 0;
346
+ overflow: hidden;
347
  }
348
 
349
  .chat-header {
 
367
 
368
  .chat-header-actions {
369
  display: flex;
370
+ align-items: center;
371
  gap: 12px;
372
  }
373
 
374
+ .build-badge {
375
+ padding: 4px 8px;
376
+ border: 1px solid var(--border-color);
377
+ border-radius: 999px;
378
+ font-size: 11px;
379
+ color: var(--text-secondary);
380
+ background: var(--bg-color);
381
+ }
382
+
383
  .header-btn {
384
  background: none;
385
  border: none;
 
393
  color: var(--text-primary);
394
  }
395
 
396
+ .header-btn.danger {
397
+ color: var(--danger);
398
+ }
399
+
400
+ .header-btn.success {
401
+ color: var(--success);
402
+ }
403
+
404
  #messages-container {
405
  flex: 1;
406
+ min-height: 0;
407
  overflow-y: auto;
408
  padding: 16px 24px;
409
+ padding-right: 44px;
410
+ padding-bottom: 24px;
411
  display: flex;
412
  flex-direction: column;
413
  gap: 12px;
414
  scroll-behavior: smooth;
 
415
  -webkit-overflow-scrolling: touch;
416
  }
417
 
418
+ /* Hide native scrollbar (custom scrollbar is used) */
419
  #messages-container::-webkit-scrollbar {
420
+ width: 0;
421
+ height: 0;
422
  }
423
 
424
+ #messages-container {
425
+ scrollbar-width: none;
426
  }
427
 
428
+ .chat-scroll-rail {
429
+ position: absolute;
430
+ top: 72px;
431
+ right: 8px;
432
+ bottom: 84px;
433
+ width: 14px;
434
+ border-radius: 999px;
435
+ background: #d8d8d8;
436
+ border: 1px solid #c5c5c5;
437
+ z-index: 12;
438
+ cursor: pointer;
439
+ }
440
+
441
+ .chat-scroll-thumb {
442
+ width: 100%;
443
+ min-height: 56px;
444
+ border-radius: 999px;
445
+ background: linear-gradient(180deg, #10d9a6 0%, #00b88f 50%, #009a78 100%);
446
+ border: 1px solid rgba(0, 0, 0, 0.08);
447
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
448
+ transform: translateY(0);
449
+ transition: background 0.15s ease;
450
+ }
451
+
452
+ .chat-scroll-thumb:hover {
453
+ background: linear-gradient(180deg, #20ecb8 0%, #00c79a 50%, #00ab86 100%);
454
  }
455
 
456
+ .chat-scroll-rail.hidden {
457
+ display: none;
458
  }
459
 
460
  .message {
 
494
  margin-bottom: 4px;
495
  }
496
 
497
+ .message-actions {
498
+ position: absolute;
499
+ top: 4px;
500
+ right: 4px;
501
+ display: none;
502
+ gap: 4px;
503
+ background: var(--chat-bg);
504
+ padding: 4px;
505
+ border-radius: 4px;
506
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3);
507
+ }
508
+
509
+ .message:hover .message-actions {
510
+ display: flex;
511
+ }
512
+
513
+ .message-action-btn {
514
+ background: var(--bg-color);
515
+ border: 1px solid var(--border-color);
516
+ color: var(--text-secondary);
517
+ padding: 4px 8px;
518
+ border-radius: 4px;
519
+ cursor: pointer;
520
+ font-size: 11px;
521
+ transition: all 0.2s;
522
+ }
523
+
524
+ .message-action-btn:hover {
525
+ background: var(--primary-color);
526
+ color: white;
527
+ border-color: var(--primary-color);
528
+ }
529
+
530
+ .message-reactions {
531
+ display: flex;
532
+ flex-wrap: wrap;
533
+ gap: 4px;
534
+ margin-top: 4px;
535
+ }
536
+
537
+ .reaction-item {
538
+ background: var(--bg-color);
539
+ border: 1px solid var(--border-color);
540
+ padding: 2px 8px;
541
+ border-radius: 12px;
542
+ font-size: 14px;
543
+ cursor: pointer;
544
+ transition: all 0.2s;
545
+ display: flex;
546
+ align-items: center;
547
+ gap: 4px;
548
+ }
549
+
550
+ .reaction-item:hover {
551
+ background: var(--primary-color);
552
+ border-color: var(--primary-color);
553
+ transform: scale(1.05);
554
+ }
555
+
556
+ .reaction-count {
557
+ font-size: 11px;
558
+ color: var(--text-secondary);
559
+ font-weight: 600;
560
+ }
561
+
562
  .message-username {
563
  font-weight: 600;
564
  color: var(--primary-color);
565
  }
566
 
567
  .message-time {
568
+ font-size: 11px;
569
  color: var(--text-secondary);
570
+ display: block;
571
+ margin-top: 2px;
572
  }
573
 
574
  .message-text {
 
578
 
579
  .message-media {
580
  margin-top: 8px;
581
+ border-radius: 8px;
582
  max-width: 400px;
583
+ overflow: hidden;
584
  }
585
 
586
  .message-media img {
587
  max-width: 100%;
588
+ border-radius: 8px;
589
+ display: block;
590
  }
591
 
592
+ .message-media video {
593
+ max-width: 100%;
594
+ border-radius: 8px;
595
+ display: block;
596
+ cursor: pointer;
597
+ }
598
+
599
  .message-media video {
600
  max-width: 100%;
601
  border-radius: 4px;
602
  }
603
 
604
+ .draft-preview {
605
+ margin: 8px 24px 0;
606
+ padding: 10px;
607
+ border: 1px solid var(--border-color);
608
+ border-radius: 8px;
609
+ background: var(--bg-color);
610
+ display: none;
611
+ align-items: center;
612
+ justify-content: space-between;
613
+ gap: 12px;
614
+ }
615
+
616
+ .draft-preview.show {
617
+ display: flex;
618
+ }
619
+
620
+ .draft-preview-left {
621
+ display: flex;
622
+ align-items: center;
623
+ gap: 10px;
624
+ min-width: 0;
625
+ flex: 1;
626
+ }
627
+
628
+ .draft-preview-thumb {
629
+ width: 56px;
630
+ height: 56px;
631
+ border-radius: 6px;
632
+ object-fit: cover;
633
+ border: 1px solid var(--border-color);
634
+ background: var(--chat-bg);
635
+ display: none;
636
+ }
637
+
638
+ .draft-preview-emoji {
639
+ font-size: 36px;
640
+ line-height: 1;
641
+ display: none;
642
+ }
643
+
644
+ .draft-preview-meta {
645
+ font-size: 12px;
646
+ color: var(--text-secondary);
647
+ white-space: nowrap;
648
+ overflow: hidden;
649
+ text-overflow: ellipsis;
650
+ }
651
+
652
+ .draft-clear-btn {
653
+ border: 1px solid var(--danger);
654
+ color: var(--danger);
655
+ background: transparent;
656
+ border-radius: 6px;
657
+ padding: 6px 10px;
658
+ cursor: pointer;
659
+ }
660
+
661
+ /* Scroll buttons removed - using WhatsApp scrollbar instead */
662
+
663
  .input-area {
664
  padding: 16px 24px;
665
  border-top: 1px solid var(--border-color);
666
  display: flex;
667
  gap: 12px;
668
+ background: var(--chat-bg);
669
+ z-index: 10;
670
+ flex-shrink: 0;
671
  }
672
 
673
  .input-area input {
 
702
  }
703
 
704
  .input-btn:hover {
705
+ background: var(--primary-color);
706
+ opacity: 0.85;
707
+ transform: scale(1.05);
708
  }
709
 
710
  .input-file {
 
729
 
730
  .members-list {
731
  flex: 1;
732
+ overflow-y: scroll;
733
  padding: 8px;
734
+ scrollbar-gutter: stable;
735
  }
736
 
737
  .member-item {
 
903
  background: #4752c4;
904
  }
905
 
906
+ .secondary-btn {
907
+ width: 100%;
908
+ padding: 10px;
909
+ background: var(--secondary-color);
910
+ color: white;
911
+ border: none;
912
+ border-radius: 4px;
913
+ font-weight: 600;
914
+ cursor: pointer;
915
+ transition: all 0.2s ease;
916
+ }
917
+
918
+ .secondary-btn:hover {
919
+ opacity: 0.9;
920
+ }
921
+
922
  .shop-item {
923
  background: var(--bg-color);
924
  border: 1px solid var(--border-color);
 
978
  margin-bottom: 16px;
979
  }
980
 
981
+ /* ==================== TOAST NOTIFICATIONS ==================== */
982
+ .toast-container {
983
  position: fixed;
984
+ top: 20px;
985
+ right: 20px;
986
+ z-index: 3000;
 
 
987
  display: flex;
988
+ flex-direction: column;
989
+ gap: 10px;
 
 
 
 
 
990
  }
991
 
992
+ .toast {
993
  background: var(--chat-bg);
994
+ border-left: 4px solid var(--success);
995
+ padding: 16px 20px;
996
  border-radius: 8px;
997
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
998
+ min-width: 300px;
999
+ display: flex;
1000
+ align-items: center;
1001
+ gap: 12px;
1002
+ animation: slideInRight 0.3s ease, fadeOut 0.3s ease 2.7s;
1003
  }
1004
 
1005
+ .toast.error {
1006
+ border-left-color: var(--danger);
 
1007
  }
1008
 
1009
+ .toast.warning {
1010
+ border-left-color: #faa61a;
1011
  }
1012
 
1013
+ .toast-icon {
1014
+ font-size: 20px;
 
1015
  }
1016
 
1017
+ .toast-content {
1018
+ flex: 1;
1019
  }
1020
 
1021
+ .toast-title {
1022
+ font-weight: 600;
1023
+ margin-bottom: 4px;
1024
+ }
 
1025
 
1026
+ .toast-message {
1027
+ font-size: 13px;
1028
+ color: var(--text-secondary);
1029
+ }
1030
 
1031
+ @keyframes slideInRight {
1032
+ from { transform: translateX(100%); opacity: 0; }
1033
+ to { transform: translateX(0); opacity: 1; }
1034
+ }
1035
 
1036
+ @keyframes fadeOut {
1037
+ to { opacity: 0; transform: translateX(100%); }
 
1038
  }
1039
 
1040
+ /* ==================== STICKER & GIF PICKER - INTEGRATED ==================== */
1041
+ .picker-panel {
1042
+ position: absolute;
1043
+ bottom: 60px;
1044
+ left: 50px;
1045
+ width: 350px;
1046
+ height: 400px;
1047
+ background: var(--chat-bg);
1048
+ border-radius: 8px 8px 0 0;
1049
+ box-shadow: 0 -4px 24px rgba(0,0,0,0.3);
1050
+ display: none;
1051
+ flex-direction: column;
1052
+ z-index: 100;
1053
+ border: 1px solid var(--border-color);
1054
+ border-bottom: none;
1055
+ }
1056
+
1057
+ .picker-panel.show {
1058
+ display: flex;
1059
+ }
1060
+
1061
+ .picker-tabs {
1062
+ display: flex;
1063
+ border-bottom: 1px solid var(--border-color);
1064
+ }
1065
+
1066
+ .picker-tab {
1067
+ flex: 1;
1068
+ padding: 12px;
1069
+ background: none;
1070
+ border: none;
1071
+ color: var(--text-secondary);
1072
+ cursor: pointer;
1073
+ transition: all 0.2s;
1074
+ font-size: 14px;
1075
+ }
1076
+
1077
+ .picker-tab.active {
1078
+ color: var(--primary-color);
1079
+ border-bottom: 2px solid var(--primary-color);
1080
+ }
1081
+
1082
+ .picker-content {
1083
+ flex: 1;
1084
+ overflow-y: auto;
1085
+ padding: 12px;
1086
+ display: grid;
1087
+ grid-template-columns: repeat(4, 1fr);
1088
+ gap: 8px;
1089
+ }
1090
+
1091
+ .sticker-item, .gif-item {
1092
+ font-size: 64px;
1093
+ text-align: center;
1094
+ cursor: pointer;
1095
+ padding: 8px;
1096
+ border-radius: 4px;
1097
+ transition: all 0.2s;
1098
+ }
1099
+
1100
+ .sticker-item:hover, .gif-item:hover {
1101
+ background: var(--bg-color);
1102
+ transform: scale(1.1);
1103
+ }
1104
+
1105
+ .gif-item {
1106
+ width: 100%;
1107
+ height: 80px;
1108
+ object-fit: cover;
1109
+ }
1110
+
1111
+ /* ==================== VOICE RECORDER ==================== */
1112
+ .voice-recorder {
1113
+ position: absolute;
1114
+ bottom: 70px;
1115
+ right: 20px;
1116
+ background: var(--chat-bg);
1117
+ border-radius: 8px;
1118
+ padding: 20px;
1119
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4);
1120
+ display: none;
1121
+ flex-direction: column;
1122
+ align-items: center;
1123
+ gap: 12px;
1124
+ z-index: 100;
1125
+ min-width: 320px;
1126
+ }
1127
+
1128
+ .voice-preview-container {
1129
+ width: 100%;
1130
+ margin-top: 8px;
1131
+ padding: 12px;
1132
+ background: rgba(0, 168, 132, 0.1);
1133
+ border: 2px solid #00a884;
1134
+ border-radius: 8px;
1135
+ }
1136
+
1137
+ .voice-preview-label {
1138
+ font-size: 13px;
1139
+ color: #00a884;
1140
+ margin-bottom: 8px;
1141
+ font-weight: 700;
1142
+ text-align: center;
1143
+ }
1144
+
1145
+ .voice-recorder.show {
1146
+ display: flex;
1147
+ }
1148
+
1149
+ .record-button {
1150
+ width: 60px;
1151
+ height: 60px;
1152
+ border-radius: 50%;
1153
+ background: var(--danger);
1154
+ border: none;
1155
+ color: white;
1156
+ font-size: 24px;
1157
+ cursor: pointer;
1158
+ transition: all 0.2s;
1159
+ }
1160
+
1161
+ .record-button:hover {
1162
+ transform: scale(1.1);
1163
+ }
1164
+
1165
+ .record-button.recording {
1166
+ animation: pulse 1s infinite;
1167
+ }
1168
+
1169
+ @keyframes pulse {
1170
+ 0%, 100% { transform: scale(1); }
1171
+ 50% { transform: scale(1.1); }
1172
+ }
1173
+
1174
+ .record-timer {
1175
+ font-size: 18px;
1176
+ font-weight: 600;
1177
+ color: var(--danger);
1178
+ }
1179
+
1180
+ /* ====================SETTINGS PANEL ==================== */
1181
+ .settings-panel {
1182
+ position: fixed;
1183
+ top: 0;
1184
+ right: -400px;
1185
+ width: 400px;
1186
+ height: 100%;
1187
+ background: var(--chat-bg);
1188
+ box-shadow: -4px 0 20px rgba(0,0,0,0.3);
1189
+ transition: right 0.3s ease;
1190
+ z-index: 2500;
1191
+ display: flex;
1192
+ flex-direction: column;
1193
+ }
1194
+
1195
+ .settings-panel.show {
1196
+ right: 0;
1197
+ }
1198
+
1199
+ .settings-header {
1200
+ padding: 20px;
1201
+ border-bottom: 1px solid var(--border-color);
1202
+ display: flex;
1203
+ justify-content: space-between;
1204
+ align-items: center;
1205
+ }
1206
+
1207
+ .settings-header h2 {
1208
+ font-size: 20px;
1209
+ color: var(--ui-text-color);
1210
+ }
1211
+
1212
+ .settings-nav {
1213
+ display: flex;
1214
+ flex-direction: column;
1215
+ border-right: 1px solid var(--border-color);
1216
+ min-width: 140px;
1217
+ }
1218
+
1219
+ .settings-nav-item {
1220
+ padding: 12px 16px;
1221
+ cursor: pointer;
1222
+ transition: all 0.2s;
1223
+ font-size: 14px;
1224
+ border-left: 3px solid transparent;
1225
+ color: var(--ui-text-color);
1226
+ }
1227
+
1228
+ .settings-nav-item:hover {
1229
+ background: var(--bg-color);
1230
+ }
1231
+
1232
+ .settings-nav-item.active {
1233
+ background: var(--bg-color);
1234
+ border-left-color: var(--primary-color);
1235
+ color: var(--primary-color);
1236
+ }
1237
+
1238
+ .settings-content {
1239
+ flex: 1;
1240
+ overflow-y: auto;
1241
+ padding: 20px;
1242
+ }
1243
+
1244
+ .settings-body {
1245
+ display: flex;
1246
+ flex: 1;
1247
+ overflow: hidden;
1248
+ }
1249
+
1250
+ .settings-section {
1251
+ display: none;
1252
+ }
1253
+
1254
+ .settings-section.active {
1255
+ display: block;
1256
+ }
1257
+
1258
+ .settings-section h3 {
1259
+ color: var(--ui-text-color);
1260
+ margin-bottom: 16px;
1261
+ }
1262
+
1263
+ .settings-item {
1264
+ margin-bottom: 20px;
1265
+ padding-bottom: 20px;
1266
+ border-bottom: 1px solid var(--border-color);
1267
+ }
1268
+
1269
+ .settings-item:last-child {
1270
+ border-bottom: none;
1271
+ }
1272
+
1273
+ .settings-label {
1274
+ font-weight: 600;
1275
+ margin-bottom: 8px;
1276
+ font-size: 14px;
1277
+ color: var(--ui-text-color);
1278
+ }
1279
+
1280
+ .settings-description {
1281
+ font-size: 12px;
1282
+ color: var(--text-secondary);
1283
+ margin-bottom: 8px;
1284
+ }
1285
+
1286
+ .toggle-switch {
1287
+ position: relative;
1288
+ display: inline-block;
1289
+ width: 44px;
1290
+ height: 24px;
1291
+ }
1292
+
1293
+ .toggle-switch input {
1294
+ opacity: 0;
1295
+ width: 0;
1296
+ height: 0;
1297
+ }
1298
+
1299
+ .toggle-slider {
1300
+ position: absolute;
1301
+ cursor: pointer;
1302
+ top: 0;
1303
+ left: 0;
1304
+ right: 0;
1305
+ bottom: 0;
1306
+ background-color: var(--secondary-color);
1307
+ transition: 0.3s;
1308
+ border-radius: 24px;
1309
+ }
1310
+
1311
+ .toggle-slider:before {
1312
+ position: absolute;
1313
+ content: "";
1314
+ height: 18px;
1315
+ width: 18px;
1316
+ left: 3px;
1317
+ bottom: 3px;
1318
+ background-color: white;
1319
+ transition: 0.3s;
1320
+ border-radius: 50%;
1321
+ }
1322
+
1323
+ .toggle-switch input:checked + .toggle-slider {
1324
+ background-color: var(--success);
1325
+ }
1326
+
1327
+ .toggle-switch input:checked + .toggle-slider:before {
1328
+ transform: translateX(20px);
1329
+ }
1330
+
1331
+ /* ==================== MODAL ==================== */
1332
+ .modal {
1333
+ position: fixed;
1334
+ top: 0;
1335
+ left: 0;
1336
+ width: 100%;
1337
+ height: 100%;
1338
+ background: rgba(0,0,0,0.6);
1339
+ display: flex;
1340
+ align-items: center;
1341
+ justify-content: center;
1342
+ z-index: 1500;
1343
+ }
1344
+
1345
+ .modal.hidden {
1346
+ display: none;
1347
+ }
1348
+
1349
+ .modal-content {
1350
+ background: var(--chat-bg);
1351
+ padding: 32px;
1352
+ border-radius: 8px;
1353
+ max-width: 500px;
1354
+ width: 90%;
1355
+ box-shadow: 0 10px 40px rgba(0,0,0,0.5);
1356
+ max-height: 90vh;
1357
+ overflow-y: auto;
1358
+ }
1359
+
1360
+ /* ==================== SCROLLBAR ==================== */
1361
+ ::-webkit-scrollbar {
1362
+ width: 8px;
1363
+ }
1364
+
1365
+ ::-webkit-scrollbar-track {
1366
+ background: transparent;
1367
+ }
1368
+
1369
+ ::-webkit-scrollbar-thumb {
1370
+ background: var(--secondary-color);
1371
+ border-radius: 4px;
1372
+ }
1373
+
1374
+ ::-webkit-scrollbar-thumb:hover {
1375
+ background: #5a5d60;
1376
+ }
1377
+
1378
+ /* ==================== RESPONSIVE ==================== */
1379
+ @media (max-width: 1200px) {
1380
+ .main-container {
1381
+ grid-template-columns: 72px 240px 1fr;
1382
+ }
1383
+
1384
+ .members-sidebar {
1385
+ display: none;
1386
+ }
1387
+
1388
+ .profile-panel {
1389
+ width: 300px;
1390
+ transform: translateX(300px);
1391
+ }
1392
+
1393
+ .profile-panel.show {
1394
+ transform: translateX(0);
1395
+ }
1396
+ }
1397
+
1398
+ @media (max-width: 768px) {
1399
+ .main-container {
1400
+ grid-template-columns: 72px 1fr;
1401
+ }
1402
+
1403
+ .channel-sidebar {
1404
+ display: none;
1405
+ position: fixed;
1406
+ left: 72px;
1407
+ top: 0;
1408
+ width: 240px;
1409
+ height: 100%;
1410
+ z-index: 999;
1411
+ }
1412
+
1413
+ .channel-sidebar.show {
1414
+ display: flex;
1415
+ }
1416
+
1417
+ .picker-panel {
1418
+ width: 90%;
1419
+ left: 5%;
1420
+ bottom: 80px;
1421
+ }
1422
+
1423
+ .voice-recorder {
1424
+ width: 90%;
1425
+ left: 5%;
1426
+ right: auto;
1427
+ bottom: 80px;
1428
+ }
1429
+
1430
+ .settings-panel {
1431
+ width: 100%;
1432
+ right: -100%;
1433
+ }
1434
+
1435
+ .settings-panel.show {
1436
+ right: 0;
1437
+ }
1438
+
1439
+ .settings-body {
1440
+ flex-direction: column;
1441
+ }
1442
+
1443
+ .settings-nav {
1444
+ flex-direction: row;
1445
+ overflow-x: auto;
1446
+ border-right: none;
1447
+ border-bottom: 1px solid var(--border-color);
1448
+ min-width: auto;
1449
+ }
1450
+
1451
+ .settings-nav-item {
1452
+ white-space: nowrap;
1453
+ border-left: none;
1454
+ border-bottom: 3px solid transparent;
1455
+ }
1456
+
1457
+ .settings-nav-item.active {
1458
+ border-left: none;
1459
+ border-bottom-color: var(--primary-color);
1460
+ }
1461
+
1462
+ .toast-container {
1463
+ top: 10px;
1464
+ right: 10px;
1465
+ left: 10px;
1466
+ }
1467
+
1468
+ .toast {
1469
+ min-width: auto;
1470
+ }
1471
+ }
1472
+
1473
+ /* ==================== CUSTOM DIALOG MODAL ==================== */
1474
+ .custom-dialog {
1475
+ position: fixed;
1476
+ top: 0;
1477
+ left: 0;
1478
+ width: 100%;
1479
+ height: 100%;
1480
+ background: rgba(0, 0, 0, 0.7);
1481
+ backdrop-filter: blur(8px);
1482
+ display: flex;
1483
+ align-items: center;
1484
+ justify-content: center;
1485
+ z-index: 9999;
1486
+ animation: fadeIn 0.2s ease;
1487
+ }
1488
+
1489
+ .custom-dialog-content {
1490
+ background: var(--chat-bg);
1491
+ border-radius: 16px;
1492
+ padding: 32px;
1493
+ min-width: 400px;
1494
+ max-width: 90%;
1495
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.8);
1496
+ animation: slideUp 0.3s ease;
1497
+ border: 2px solid var(--primary-color);
1498
+ }
1499
+
1500
+ .custom-dialog-header {
1501
+ display: flex;
1502
+ align-items: center;
1503
+ gap: 12px;
1504
+ margin-bottom: 20px;
1505
+ }
1506
+
1507
+ .custom-dialog-icon {
1508
+ font-size: 32px;
1509
+ }
1510
+
1511
+ .custom-dialog-title {
1512
+ font-size: 22px;
1513
+ font-weight: 700;
1514
+ color: var(--ui-text-color);
1515
+ }
1516
+
1517
+ .custom-dialog-message {
1518
+ font-size: 15px;
1519
+ color: var(--text-primary);
1520
+ margin-bottom: 24px;
1521
+ line-height: 1.6;
1522
+ }
1523
+
1524
+ .custom-dialog-input {
1525
+ width: 100%;
1526
+ padding: 12px;
1527
+ background: var(--bg-color);
1528
+ border: 2px solid var(--border-color);
1529
+ border-radius: 8px;
1530
+ color: var(--text-primary);
1531
+ font-size: 14px;
1532
+ margin-bottom: 20px;
1533
+ transition: all 0.2s;
1534
+ }
1535
+
1536
+ .custom-dialog-input:focus {
1537
+ outline: none;
1538
+ border-color: var(--primary-color);
1539
+ box-shadow: 0 0 0 3px rgba(88, 101, 242, 0.1);
1540
+ }
1541
+
1542
+ .custom-dialog-buttons {
1543
+ display: flex;
1544
+ gap: 12px;
1545
+ justify-content: flex-end;
1546
+ }
1547
+
1548
+ .custom-dialog-btn {
1549
+ padding: 12px 24px;
1550
+ border: none;
1551
+ border-radius: 8px;
1552
+ font-size: 14px;
1553
+ font-weight: 600;
1554
+ cursor: pointer;
1555
+ transition: all 0.2s;
1556
+ min-width: 100px;
1557
+ }
1558
+
1559
+ .custom-dialog-btn-primary {
1560
+ background: var(--primary-color);
1561
+ color: white;
1562
+ }
1563
+
1564
+ .custom-dialog-btn-primary:hover {
1565
+ background: #4752c4;
1566
+ transform: translateY(-2px);
1567
+ box-shadow: 0 4px 12px rgba(88, 101, 242, 0.4);
1568
+ }
1569
+
1570
+ .custom-dialog-btn-danger {
1571
+ background: var(--danger);
1572
+ color: white;
1573
+ }
1574
+
1575
+ .custom-dialog-btn-danger:hover {
1576
+ background: #d93636;
1577
+ transform: translateY(-2px);
1578
+ box-shadow: 0 4px 12px rgba(240, 71, 71, 0.4);
1579
+ }
1580
+
1581
+ .custom-dialog-btn-secondary {
1582
+ background: var(--secondary-color);
1583
+ color: var(--text-primary);
1584
+ }
1585
+
1586
+ .custom-dialog-btn-secondary:hover {
1587
+ opacity: 0.8;
1588
+ transform: translateY(-2px);
1589
+ }
1590
+
1591
+ /* ==================== FRIEND SEARCH BAR ==================== */
1592
+ .friend-search-container {
1593
+ padding: 12px 16px;
1594
+ border-bottom: 1px solid var(--border-color);
1595
+ }
1596
+
1597
+ .friend-search-input {
1598
+ width: 100%;
1599
+ padding: 10px 12px 10px 38px;
1600
+ background: var(--bg-color);
1601
+ border: 1px solid var(--border-color);
1602
+ border-radius: 20px;
1603
+ color: var(--text-primary);
1604
+ font-size: 13px;
1605
+ transition: all 0.2s;
1606
+ }
1607
+
1608
+ .friend-search-input:focus {
1609
+ outline: none;
1610
+ border-color: var(--primary-color);
1611
+ box-shadow: 0 0 0 2px rgba(88, 101, 242, 0.15);
1612
+ }
1613
+
1614
+ .friend-search-wrapper {
1615
+ position: relative;
1616
+ }
1617
+
1618
+ .friend-search-icon {
1619
+ position: absolute;
1620
+ left: 12px;
1621
+ top: 50%;
1622
+ transform: translateY(-50%);
1623
+ color: var(--text-secondary);
1624
+ font-size: 14px;
1625
+ }
1626
+
1627
+ /* ==================== QR CODE MODAL ==================== */
1628
+ .qr-modal {
1629
+ position: fixed;
1630
+ top: 0;
1631
+ left: 0;
1632
+ width: 100%;
1633
+ height: 100%;
1634
+ background: rgba(0, 0, 0, 0.8);
1635
+ backdrop-filter: blur(10px);
1636
+ display: none;
1637
+ align-items: center;
1638
+ justify-content: center;
1639
+ z-index: 9998;
1640
+ }
1641
+
1642
+ .qr-modal.show {
1643
+ display: flex;
1644
+ }
1645
+
1646
+ .qr-modal-content {
1647
+ background: var(--chat-bg);
1648
+ border-radius: 20px;
1649
+ padding: 40px;
1650
+ text-align: center;
1651
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.9);
1652
+ border: 2px solid var(--primary-color);
1653
+ max-width: 450px;
1654
+ }
1655
+
1656
+ .qr-modal-title {
1657
+ font-size: 24px;
1658
+ font-weight: 700;
1659
+ color: var(--ui-text-color);
1660
+ margin-bottom: 16px;
1661
+ }
1662
+
1663
+ .qr-modal-subtitle {
1664
+ font-size: 14px;
1665
+ color: var(--text-secondary);
1666
+ margin-bottom: 24px;
1667
+ }
1668
+
1669
+ .qr-code-container {
1670
+ background: white;
1671
+ padding: 20px;
1672
+ border-radius: 12px;
1673
+ display: inline-block;
1674
+ margin-bottom: 20px;
1675
+ }
1676
+
1677
+ #qrCodeCanvas {
1678
+ display: block;
1679
+ }
1680
+
1681
+ .qr-modal-close {
1682
+ margin-top: 12px;
1683
+ padding: 12px 32px;
1684
+ background: var(--primary-color);
1685
+ color: white;
1686
+ border: none;
1687
+ border-radius: 8px;
1688
+ cursor: pointer;
1689
+ font-weight: 600;
1690
+ transition: all 0.2s;
1691
+ }
1692
+
1693
+ .qr-modal-close:hover {
1694
+ background: #4752c4;
1695
+ transform: translateY(-2px);
1696
+ }
1697
+
1698
+ /* ==================== MEDIA PREVIEW PANEL ==================== */
1699
+ .media-preview-panel {
1700
+ position: fixed;
1701
+ bottom: -100%;
1702
+ left: 0;
1703
+ right: 0;
1704
+ height: 60vh;
1705
+ background: var(--chat-bg);
1706
+ border-top: 3px solid var(--primary-color);
1707
+ z-index: 1500;
1708
+ transition: bottom 0.4s cubic-bezier(0.4, 0, 0.2, 1);
1709
+ box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.5);
1710
+ display: flex;
1711
+ flex-direction: column;
1712
+ }
1713
+
1714
+ .media-preview-panel.show {
1715
+ bottom: 0;
1716
+ }
1717
+
1718
+ .media-preview-header {
1719
+ padding: 20px 24px;
1720
+ border-bottom: 1px solid var(--border-color);
1721
+ display: flex;
1722
+ justify-content: space-between;
1723
+ align-items: center;
1724
+ background: var(--bg-color);
1725
+ }
1726
+
1727
+ .media-preview-title {
1728
+ font-size: 18px;
1729
+ font-weight: 600;
1730
+ color: var(--ui-text-color);
1731
+ }
1732
+
1733
+ .media-preview-close {
1734
+ background: none;
1735
+ border: none;
1736
+ color: var(--text-secondary);
1737
+ cursor: pointer;
1738
+ font-size: 24px;
1739
+ padding: 8px;
1740
+ transition: all 0.2s;
1741
+ }
1742
+
1743
+ .media-preview-close:hover {
1744
+ color: var(--danger);
1745
+ transform: rotate(90deg);
1746
+ }
1747
+
1748
+ .media-preview-content {
1749
+ flex: 1;
1750
+ overflow-y: auto;
1751
+ padding: 24px;
1752
+ display: flex;
1753
+ flex-wrap: wrap;
1754
+ gap: 16px;
1755
+ align-content: flex-start;
1756
+ }
1757
+
1758
+ .media-preview-item {
1759
+ position: relative;
1760
+ width: 200px;
1761
+ height: 200px;
1762
+ border-radius: 12px;
1763
+ overflow: hidden;
1764
+ border: 2px solid var(--border-color);
1765
+ transition: all 0.2s;
1766
+ }
1767
+
1768
+ .media-preview-item:hover {
1769
+ transform: scale(1.05);
1770
+ border-color: var(--primary-color);
1771
+ box-shadow: 0 8px 24px rgba(88, 101, 242, 0.3);
1772
+ }
1773
+
1774
+ .media-preview-item img,
1775
+ .media-preview-item video {
1776
+ width: 100%;
1777
+ height: 100%;
1778
+ object-fit: cover;
1779
+ }
1780
+
1781
+ /* ==================== ENHANCED HOVER EFFECTS ==================== */
1782
+ .danger-hover:hover {
1783
+ color: var(--danger) !important;
1784
+ transform: scale(1.1);
1785
+ filter: drop-shadow(0 0 8px rgba(240, 71, 71, 0.6));
1786
+ }
1787
+
1788
+ .success-hover:hover {
1789
+ color: var(--success) !important;
1790
+ transform: scale(1.1);
1791
+ filter: drop-shadow(0 0 8px rgba(67, 181, 129, 0.6));
1792
+ }
1793
+
1794
+ .primary-hover:hover {
1795
+ color: var(--primary-color) !important;
1796
+ transform: scale(1.1);
1797
+ filter: drop-shadow(0 0 8px rgba(88, 101, 242, 0.6));
1798
+ }
1799
+
1800
+ /* ==================== MOBILE RESPONSIVENESS IMPROVEMENTS ==================== */
1801
  @media (max-width: 768px) {
1802
+ .custom-dialog-content {
1803
+ min-width: 90%;
1804
+ padding: 24px;
1805
+ }
1806
+
1807
+ .media-preview-panel {
1808
+ height: 70vh;
1809
+ }
1810
+
1811
+ .media-preview-item {
1812
+ width: calc(50% - 8px);
1813
+ height: 150px;
1814
+ }
1815
+
1816
+ .qr-modal-content {
1817
+ margin: 20px;
1818
+ padding: 24px;
1819
+ }
1820
+
1821
  .main-container {
1822
  grid-template-columns: 72px 1fr;
1823
+ height: 100vh;
1824
+ height: 100dvh; /* Dynamic viewport height for mobile */
1825
  }
1826
 
1827
+ .chat-area {
 
 
 
 
 
1828
  height: 100%;
1829
+ max-height: 100vh;
1830
+ max-height: 100dvh;
1831
  }
1832
 
1833
+ #messages-container {
1834
+ max-height: calc(100vh - 200px);
1835
+ max-height: calc(100dvh - 200px);
1836
+ -webkit-overflow-scrolling: touch;
1837
+ }
1838
+ }
1839
+
1840
+ @media (max-width: 480px) {
1841
+ .media-preview-item {
1842
+ width: 100%;
1843
+ height: 200px;
1844
+ }
1845
+
1846
+ .custom-dialog-buttons {
1847
+ flex-direction: column;
1848
+ }
1849
+
1850
+ .custom-dialog-btn {
1851
+ width: 100%;
1852
  }
1853
  }
1854
  </style>
 
1873
  <label>Password</label>
1874
  <input type="password" id="password" placeholder="Enter your password" required>
1875
  </div>
1876
+ <div class="auth-inline-link" id="forgotPasswordWrap">
1877
+ <button type="button" onclick="toggleForgotPasswordPanel()">Forgot password?</button>
1878
+ </div>
1879
  <button type="submit" class="auth-button" id="authButton">Login</button>
1880
  </form>
1881
+ <div class="reset-password-panel" id="resetPasswordPanel">
1882
+ <div class="form-group" style="margin-bottom: 12px;">
1883
+ <label>Reset Email</label>
1884
+ <input type="email" id="resetEmail" placeholder="Enter your account email">
1885
+ </div>
1886
+ <div class="form-group" style="margin-bottom: 12px;">
1887
+ <label>New Password</label>
1888
+ <input type="password" id="resetNewPassword" placeholder="Enter new password (min 6 chars)">
1889
+ </div>
1890
+ <button type="button" class="auth-mini-button" onclick="resetForgottenPassword()">Reset Password</button>
1891
+ </div>
1892
  <div class="auth-toggle">
1893
  <span>Don't have an account? <button type="button" onclick="toggleAuthMode()">Register</button></span>
1894
  </div>
 
1908
  <div class="guild-item" onclick="showSection('shop')" title="Shop">
1909
  <i class="fas fa-store"></i>
1910
  </div>
1911
+ <div class="guild-divider"></div>
1912
+ <div class="guild-item" onclick="showArchivedMessages()" title="Archived Messages">
1913
+ <i class="fas fa-archive"></i>
1914
+ </div>
1915
+ <div class="guild-item" onclick="openCommunitiesModal()" title="Communities">
1916
+ <i class="fas fa-globe"></i>
1917
+ </div>
1918
+ <div class="guild-item" onclick="openGroupsModal()" title="Groups">
1919
+ <i class="fas fa-user-group"></i>
1920
+ </div>
1921
  </div>
1922
 
1923
  <!-- CHANNEL SIDEBAR -->
1924
  <div class="channel-sidebar" id="channelSidebar">
1925
  <div class="sidebar-header">
1926
  <h3>Messages</h3>
1927
+ <div style="display: flex; gap: 8px;">
1928
+ <button onclick="showQRCodeModal()" title="Show My QR Code" style="padding: 4px 8px; background: var(--success); border: none; border-radius: 4px; color: white; cursor: pointer; font-size: 12px;">
1929
+ <i class="fas fa-qrcode"></i>
1930
+ </button>
1931
+ <button onclick="scanQRCodeForFriend()" title="Scan QR Code" style="padding: 4px 8px; background: #8e44ad; border: none; border-radius: 4px; color: white; cursor: pointer; font-size: 12px;">
1932
+ <i class="fas fa-camera"></i>
1933
+ </button>
1934
+ <button onclick="createNewGroup()" title="Create Group" style="padding: 4px 8px; background: var(--primary-color); border: none; border-radius: 4px; color: white; cursor: pointer; font-size: 12px;">
1935
+ <i class="fas fa-plus"></i> Group
1936
+ </button>
1937
+ <button onclick="showArchivedChats()" title="Archived Chats" style="padding: 4px 8px; background: var(--secondary-color); border: none; border-radius: 4px; color: var(--text-primary); cursor: pointer; font-size: 12px;">
1938
+ <i class="fas fa-archive"></i>
1939
+ </button>
1940
+ <button onclick="toggleChannelSidebar()"><i class="fas fa-times"></i></button>
1941
+ </div>
1942
+ </div>
1943
+ <div class="friend-search-container">
1944
+ <div class="friend-search-wrapper">
1945
+ <i class="fas fa-search friend-search-icon"></i>
1946
+ <input type="text" id="friendSearchInput" class="friend-search-input" placeholder="Search friends..." oninput="filterFriends(this.value)">
1947
+ </div>
1948
  </div>
1949
  <div class="channels-list" id="channelsList">
1950
  <!-- Direct messages will be populated here by loadFriendsForDM() -->
 
1961
  <div class="chat-header-title" id="chatTitle">πŸ’¬ Select a friend to chat</div>
1962
  </div>
1963
  <div class="chat-header-actions">
1964
+ <span class="build-badge" title="Frontend build version">v2.2.0</span>
1965
+ <button class="header-btn" onclick="toggleArchiveChat()" title="Archive chat">
1966
+ <i class="fas fa-archive"></i>
1967
+ </button>
1968
+ <button class="header-btn" onclick="startVideoCall()" title="Video call">
1969
+ <i class="fas fa-video"></i>
1970
+ </button>
1971
+ <button class="header-btn success" onclick="addCurrentChatToList()" title="Add chat to list">
1972
+ <i class="fas fa-bookmark"></i>
1973
+ </button>
1974
+ <button class="header-btn" onclick="closeCurrentChat()" title="Close Chat" id="closeChatBtn" style="display: none;">
1975
+ <i class="fas fa-times-circle"></i>
1976
+ </button>
1977
+ <button class="header-btn" onclick="openSettings()" title="Settings">
1978
+ <i class="fas fa-cog"></i>
1979
+ </button>
1980
  <button class="header-btn" onclick="openProfile()" title="Profile">
1981
  <i class="fas fa-user-circle"></i>
1982
  </button>
1983
+ <button class="header-btn danger-hover" onclick="logout()" title="Logout">
1984
  <i class="fas fa-sign-out-alt"></i>
1985
  </button>
1986
  </div>
1987
  </div>
1988
 
1989
  <div id="messages-container"></div>
1990
+ <div id="chatScrollRail" class="chat-scroll-rail hidden">
1991
+ <div id="chatScrollThumb" class="chat-scroll-thumb"></div>
1992
+ </div>
1993
+
1994
+ <div id="mediaDraftPreview" class="draft-preview">
1995
+ <div class="draft-preview-left">
1996
+ <img id="draftPreviewThumb" class="draft-preview-thumb" alt="Selected media preview">
1997
+ <div id="draftPreviewEmoji" class="draft-preview-emoji"></div>
1998
+ <div id="draftPreviewMeta" class="draft-preview-meta">Media selected. Press Send when ready.</div>
1999
+ </div>
2000
+ <button class="draft-clear-btn" onclick="clearPendingMediaDraft()">Clear</button>
2001
+ </div>
2002
 
2003
  <div id="typingIndicator" style="display: none; padding: 10px; color: var(--text-secondary); font-size: 12px; font-style: italic;"></div>
2004
 
2005
  <div class="input-area">
2006
+ <button class="input-btn" onclick="document.getElementById('fileInput').click()" title="Upload Image">
2007
+ <i class="fas fa-image"></i>
2008
+ </button>
2009
+ <button class="input-btn" onclick="document.getElementById('videoInput').click()" title="Upload Video">
2010
+ <i class="fas fa-video"></i>
2011
+ </button>
2012
+ <button class="input-btn" onclick="togglePicker()" title="Stickers & GIFs">
2013
+ <i class="fas fa-smile"></i>
2014
+ </button>
2015
+ <button class="input-btn" onclick="toggleVoiceRecorder()" title="Voice message">
2016
+ <i class="fas fa-microphone"></i>
2017
  </button>
2018
  <input type="text" id="messageInput" placeholder="Message..." onkeypress="handleMessageKeypress(event)">
2019
  <button class="input-btn" onclick="sendMessage()" title="Send">
2020
  <i class="fas fa-paper-plane"></i>
2021
  </button>
2022
+ <input type="file" id="fileInput" class="input-file" onchange="handleFileUpload(event)" accept="image/*" multiple>
2023
+ <input type="file" id="videoInput" class="input-file" onchange="handleFileUpload(event)" accept="video/*" multiple>
2024
  </div>
2025
  </div>
2026
 
 
2053
  <input type="file" id="profileImageInput" class="input-file" onchange="uploadProfileImage(event)" accept="image/*" style="display: none;">
2054
  <p style="font-size: 12px; color: var(--text-secondary); margin-top: 5px;">Click to upload your profile picture</p>
2055
  </div>
2056
+ <div class="form-field">
2057
+ <label>Share Profile</label>
2058
+ <button class="save-btn" onclick="shareProfileLink()" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">πŸ“‹ Copy Profile Link</button>
2059
+ <p style="font-size: 12px; color: var(--text-secondary); margin-top: 5px;">Share this link with friends to connect</p>
2060
+ </div>
2061
  <div class="form-field">
2062
  <label>Username</label>
2063
  <input type="text" id="editUsername" placeholder="Username">
 
2066
  <label>Bio</label>
2067
  <textarea id="editBio" placeholder="Tell us about yourself"></textarea>
2068
  </div>
2069
+ <div class="form-field">
2070
+ <label>Phone Number</label>
2071
+ <input type="tel" id="editPhoneNumber" placeholder="Add your phone number">
2072
+ </div>
2073
  <button class="save-btn" onclick="saveProfile()">Save Profile</button>
2074
  </div>
2075
 
 
2102
  </div>
2103
  </div>
2104
 
2105
+ <!-- TOAST NOTIFICATIONS -->
2106
+ <div class="toast-container" id="toastContainer"></div>
2107
+
2108
+ <!-- STICKER/GIF PICKER -->
2109
+ <div class="picker-panel" id="pickerPanel">
2110
+ <div class="picker-tabs">
2111
+ <button class="picker-tab active" onclick="switchPickerTab('stickers')">πŸ˜€ Stickers</button>
2112
+ <button class="picker-tab" onclick="switchPickerTab('gifs')">🎬 GIFs</button>
2113
+ </div>
2114
+ <div class="picker-content" id="pickerContent">
2115
+ <!-- Will be populated by JS -->
2116
+ </div>
2117
+ </div>
2118
+
2119
+ <!-- VOICE RECORDER -->
2120
+ <div class="voice-recorder" id="voiceRecorder">
2121
+ <div class="record-timer" id="recordTimer">00:00</div>
2122
+ <button class="record-button" id="recordButton" onclick="toggleRecording()">
2123
+ <i class="fas fa-microphone"></i>
2124
+ </button>
2125
+ <div class="voice-preview-container" id="voicePreviewContainer" style="display: none;">
2126
+ <div class="voice-preview-label">🎧 Preview your recording:</div>
2127
+ <audio id="voicePreview" controls style="width: 100%; margin-bottom: 8px;"></audio>
2128
+ </div>
2129
+ <div style="display: flex; gap: 10px; width: 100%;">
2130
+ <button class="save-btn" onclick="sendVoiceMessage()" style="padding: 8px 16px; flex: 1;" disabled>Send Voice Message</button>
2131
+ <button class="save-btn" onclick="cancelRecording()" style="padding: 8px 16px; background: var(--danger); flex: 1;">Cancel</button>
2132
+ </div>
2133
+ </div>
2134
+
2135
+ <!-- SETTINGS PANEL -->
2136
+ <div class="settings-panel" id="settingsPanel">
2137
+ <div class="settings-header">
2138
+ <h2>βš™οΈ Settings</h2>
2139
+ <button class="header-btn" onclick="closeSettings()"><i class="fas fa-times"></i></button>
2140
+ </div>
2141
+ <div class="settings-body">
2142
+ <div class="settings-nav">
2143
+ <div class="settings-nav-item active" onclick="switchSettingsTab('account')">πŸ‘€ Account</div>
2144
+ <div class="settings-nav-item" onclick="switchSettingsTab('privacy')">πŸ”’ Privacy</div>
2145
+ <div class="settings-nav-item" onclick="switchSettingsTab('chat')">πŸ’¬ Chat</div>
2146
+ <div class="settings-nav-item" onclick="switchSettingsTab('notifications')">πŸ”” Notifications</div>
2147
+ <div class="settings-nav-item" onclick="switchSettingsTab('appearance')">🎨 Appearance</div>
2148
+ <div class="settings-nav-item" onclick="switchSettingsTab('stats')">πŸ“Š Statistics</div>
2149
+ </div>
2150
+ <div class="settings-content">
2151
+ <!-- Account Settings -->
2152
+ <div class="settings-section active" id="accountSettings">
2153
+ <h3>Account Settings</h3>
2154
+ <div class="settings-item">
2155
+ <div class="settings-label">Email</div>
2156
+ <div class="settings-description">Your current email address</div>
2157
+ <input type="email" id="settingsEmail" class="form-group input" readonly style="background: var(--bg-color); color: var(--text-secondary);">
2158
+ </div>
2159
+ <div class="settings-item">
2160
+ <div class="settings-label">Change Username</div>
2161
+ <div class="settings-description">Update your display name</div>
2162
+ <input type="text" id="settingsUsername" placeholder="New username" style="width: 100%; padding: 10px; background: var(--bg-color); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary); margin-bottom: 8px;">
2163
+ <button class="save-btn" onclick="updateUsername()">Update Username</button>
2164
+ </div>
2165
+ <div class="settings-item">
2166
+ <div class="settings-label">Delete Account</div>
2167
+ <div class="settings-description">Permanently delete your account and all data</div>
2168
+ <button class="save-btn danger-hover" onclick="showToast('Feature coming soon!', 'warning')" style="background: var(--danger);">Delete Account</button>
2169
+ </div>
2170
+ <div class="settings-item">
2171
+ <div class="settings-label">Switch Account</div>
2172
+ <div class="settings-description">Quickly switch between previously signed-in accounts</div>
2173
+ <select id="accountSwitcher" style="width: 100%; padding: 10px; background: var(--bg-color); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary); margin-bottom: 8px;"></select>
2174
+ <button class="secondary-btn" onclick="switchAccount()">Switch Account</button>
2175
+ </div>
2176
+ </div>
2177
+
2178
+ <!-- Privacy Settings -->
2179
+ <div class="settings-section" id="privacySettings">
2180
+ <h3>Privacy & Safety</h3>
2181
+ <div class="settings-item">
2182
+ <div class="settings-label">Who can send you friend requests</div>
2183
+ <label class="toggle-switch">
2184
+ <input type="checkbox" checked id="allowFriendRequests">
2185
+ <span class="toggle-slider"></span>
2186
+ </label>
2187
+ </div>
2188
+ <div class="settings-item">
2189
+ <div class="settings-label">Show online status</div>
2190
+ <label class="toggle-switch">
2191
+ <input type="checkbox" checked id="showOnlineStatus">
2192
+ <span class="toggle-slider"></span>
2193
+ </label>
2194
+ </div>
2195
+ <div class="settings-item">
2196
+ <div class="settings-label">Read receipts</div>
2197
+ <div class="settings-description">Let others know when you've read their messages</div>
2198
+ <label class="toggle-switch">
2199
+ <input type="checkbox" checked id="readReceipts">
2200
+ <span class="toggle-slider"></span>
2201
+ </label>
2202
+ </div>
2203
+ </div>
2204
+
2205
+ <!-- Chat Settings -->
2206
+ <div class="settings-section" id="chatSettings">
2207
+ <h3>Chat Settings</h3>
2208
+ <div class="settings-item">
2209
+ <div class="settings-label">Enter to send</div>
2210
+ <div class="settings-description">Press Enter to send messages (Shift+Enter for new line)</div>
2211
+ <label class="toggle-switch">
2212
+ <input type="checkbox" checked id="enterToSend">
2213
+ <span class="toggle-slider"></span>
2214
+ </label>
2215
+ </div>
2216
+ <div class="settings-item">
2217
+ <div class="settings-label">Show typing indicator</div>
2218
+ <label class="toggle-switch">
2219
+ <input type="checkbox" checked id="showTypingIndicator">
2220
+ <span class="toggle-slider"></span>
2221
+ </label>
2222
+ </div>
2223
+ <div class="settings-item">
2224
+ <div class="settings-label">Auto-play GIFs</div>
2225
+ <label class="toggle-switch">
2226
+ <input type="checkbox" checked id="autoplayGifs">
2227
+ <span class="toggle-slider"></span>
2228
+ </label>
2229
+ </div>
2230
+ <div class="settings-item">
2231
+ <div class="settings-label">🎀 Microphone</div>
2232
+ <div class="settings-description">Select audio input device for voice messages</div>
2233
+ <select id="microphoneSelector" onchange="saveAudioSettings()" style="width: 100%; padding: 10px; background: var(--bg-color); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary); margin-top: 8px;">
2234
+ <option value="default">Default Microphone</option>
2235
+ </select>
2236
+ <button class="secondary-btn" onclick="refreshAudioDevices()" style="margin-top: 8px; padding: 6px 12px; font-size: 13px;">πŸ”„ Refresh Devices</button>
2237
+ </div>
2238
+ <div class="settings-item">
2239
+ <div class="settings-label">πŸ”Š Speaker / Earphones</div>
2240
+ <div class="settings-description">Select audio output device for playback</div>
2241
+ <select id="speakerSelector" onchange="saveAudioSettings()" style="width: 100%; padding: 10px; background: var(--bg-color); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary); margin-top: 8px;">
2242
+ <option value="default">Default Speaker</option>
2243
+ </select>
2244
+ </div>
2245
+ </div>
2246
+
2247
+ <!-- Notification Settings -->
2248
+ <div class="settings-section" id="notificationSettings">
2249
+ <h3>Notifications</h3>
2250
+ <div class="settings-item">
2251
+ <div class="settings-label">Desktop notifications</div>
2252
+ <label class="toggle-switch">
2253
+ <input type="checkbox" id="desktopNotifications">
2254
+ <span class="toggle-slider"></span>
2255
+ </label>
2256
+ </div>
2257
+ <div class="settings-item">
2258
+ <div class="settings-label">Message sound</div>
2259
+ <label class="toggle-switch">
2260
+ <input type="checkbox" checked id="messageSound">
2261
+ <span class="toggle-slider"></span>
2262
+ </label>
2263
+ </div>
2264
+ <div class="settings-item">
2265
+ <div class="settings-label">Notification settings</div>
2266
+ <div class="settings-description">All messages, Only mentions, Nothing</div>
2267
+ <select id="notificationMode" onchange="saveNotificationSettings()" style="width: 100%; padding: 10px; background: var(--bg-color); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary);">
2268
+ <option value="all">All messages</option>
2269
+ <option value="mentions">Only mentions</option>
2270
+ <option value="none">Nothing</option>
2271
+ </select>
2272
+ </div>
2273
+ <div class="settings-item">
2274
+ <div class="settings-label">Notify on sent messages (this device)</div>
2275
+ <label class="toggle-switch">
2276
+ <input type="checkbox" id="notifyOnSent" onchange="saveNotificationSettings()">
2277
+ <span class="toggle-slider"></span>
2278
+ </label>
2279
+ </div>
2280
+ <div class="settings-item">
2281
+ <div class="settings-label">Notify on received messages (this device)</div>
2282
+ <label class="toggle-switch">
2283
+ <input type="checkbox" id="notifyOnReceived" checked onchange="saveNotificationSettings()">
2284
+ <span class="toggle-slider"></span>
2285
+ </label>
2286
+ </div>
2287
+ </div>
2288
+
2289
+ <!-- Appearance Settings -->
2290
+ <div class="settings-section" id="appearanceSettings">
2291
+ <h3>Appearance</h3>
2292
+ <div class="settings-item">
2293
+ <div class="settings-label">Theme</div>
2294
+ <div class="settings-description">Choose your preferred theme</div>
2295
+ <select id="themeSelector" onchange="changeTheme(this.value)" style="width: 100%; padding: 10px; background: var(--bg-color); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary);">
2296
+ <option value="dark">Dark</option>
2297
+ <option value="light">Light</option>
2298
+ <option value="custom">Custom Colors</option>
2299
+ </select>
2300
+ </div>
2301
+ <div class="settings-item" id="customColorSection" style="display: none;">
2302
+ <div class="settings-label">🎨 Customize Colors</div>
2303
+ <div class="settings-description">Change background, text, and icon colors</div>
2304
+ <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; margin-top: 10px;">
2305
+ <div>
2306
+ <label style="display: block; margin-bottom: 4px; font-size: 13px; color: var(--text-secondary);">Background</label>
2307
+ <input type="color" id="bgColorPicker" value="#1e1f22" oninput="applySimpleCustomColors()" style="width: 100%; height: 40px; border: 1px solid var(--border-color); border-radius: 4px; cursor: pointer;">
2308
+ </div>
2309
+ <div>
2310
+ <label style="display: block; margin-bottom: 4px; font-size: 13px; color: var(--text-secondary);">Text Color</label>
2311
+ <input type="color" id="textColorPicker" value="#ffffff" oninput="applySimpleCustomColors()" style="width: 100%; height: 40px; border: 1px solid var(--border-color); border-radius: 4px; cursor: pointer;">
2312
+ </div>
2313
+ <div>
2314
+ <label style="display: block; margin-bottom: 4px; font-size: 13px; color: var(--text-secondary);">Icons/Accents</label>
2315
+ <input type="color" id="accentColorPicker" value="#5865F2" oninput="applySimpleCustomColors()" style="width: 100%; height: 40px; border: 1px solid var(--border-color); border-radius: 4px; cursor: pointer;">
2316
+ </div>
2317
+ </div>
2318
+ <button class="secondary-btn" onclick="resetSimpleCustomColors()" style="margin-top: 10px; width: 100%; padding: 8px;">Reset to Default</button>
2319
+ </div>
2320
+ <div class="settings-item">
2321
+ <div class="settings-label">Compact mode</div>
2322
+ <div class="settings-description">Show more messages on screen</div>
2323
+ <label class="toggle-switch">
2324
+ <input type="checkbox" id="compactMode">
2325
+ <span class="toggle-slider"></span>
2326
+ </label>
2327
+ </div>
2328
+ <div class="settings-item">
2329
+ <div class="settings-label">Message font size</div>
2330
+ <select id="fontSizeSelector" onchange="saveAppearanceSettings()" style="width: 100%; padding: 10px; background: var(--bg-color); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary);">
2331
+ <option value="small">Small</option>
2332
+ <option value="medium" selected>Medium</option>
2333
+ <option value="large">Large</option>
2334
+ </select>
2335
+ </div>
2336
+ <div class="settings-item">
2337
+ <div class="settings-label">Disappearing messages</div>
2338
+ <div class="settings-description">Auto-remove newly received/sent messages from your screen</div>
2339
+ <select id="disappearTime" onchange="saveChatSettings()" style="width: 100%; padding: 10px; background: var(--bg-color); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary);">
2340
+ <option value="0">Off</option>
2341
+ <option value="30">30 seconds</option>
2342
+ <option value="60">1 minute</option>
2343
+ <option value="300">5 minutes</option>
2344
+ <option value="3600">1 hour</option>
2345
+ </select>
2346
+ </div>
2347
+ </div>
2348
+
2349
+ <!-- Statistics Settings -->
2350
+ <div class="settings-section" id="statsSettings">
2351
+ <h3>πŸ“Š Your Statistics</h3>
2352
+ <div class="settings-item" style="padding: 20px; background: var(--bg-color); border-radius: 8px;">
2353
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
2354
+ <div style="background: var(--chat-bg); padding: 16px; border-radius: 6px; text-align: center; border: 1px solid var(--border-color);">
2355
+ <div style="font-size: 32px; font-weight: bold; color: var(--primary-color);" id="statMessagesCount">0</div>
2356
+ <div style="font-size: 13px; color: var(--text-secondary); margin-top: 8px;">Messages Sent</div>
2357
+ </div>
2358
+ <div style="background: var(--chat-bg); padding: 16px; border-radius: 6px; text-align: center; border: 1px solid var(--border-color);">
2359
+ <div style="font-size: 32px; font-weight: bold; color: var(--primary-color);" id="statTotalChars">0</div>
2360
+ <div style="font-size: 13px; color: var(--text-secondary); margin-top: 8px;">Characters Typed</div>
2361
+ </div>
2362
+ <div style="background: var(--chat-bg); padding: 16px; border-radius: 6px; text-align: center; border: 1px solid var(--border-color);">
2363
+ <div style="font-size: 32px; font-weight: bold; color: var(--primary-color);" id="statReactionsGiven">0</div>
2364
+ <div style="font-size: 13px; color: var(--text-secondary); margin-top: 8px;">Reactions Given</div>
2365
+ </div>
2366
+ <div style="background: var(--chat-bg); padding: 16px; border-radius: 6px; text-align: center; border: 1px solid var(--border-color);">
2367
+ <div style="font-size: 32px; font-weight: bold; color: var(--primary-color);" id="statQuizCorrect">0</div>
2368
+ <div style="font-size: 13px; color: var(--text-secondary); margin-top: 8px;">Quiz Correct</div>
2369
+ </div>
2370
+ </div>
2371
+ </div>
2372
+ <div class="settings-item">
2373
+ <button class="save-btn" onclick="resetUserStats()" style="width: 100%;">Reset Statistics</button>
2374
+ </div>
2375
+ <div class="settings-item">
2376
+ <button class="primary-btn" onclick="startQuiz()" style="width: 100%; padding: 12px; background: var(--primary-color); color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold;">🎯 Take a Quiz Question</button>
2377
+ </div>
2378
+ </div>
2379
+ </div>
2380
+ </div>
2381
+ </div>
2382
+
2383
+ <!-- COMMUNITIES MODAL -->
2384
+ <div class="modal-content">
2385
+ <h2><i class="fas fa-globe"></i> Communities</h2>
2386
+ <p style="margin-top: 10px; color: var(--text-secondary);">Join public communities to discover people with shared interests.</p>
2387
+ <div id="communitiesList" style="margin-top: 16px;"></div>
2388
+ <button class="save-btn" onclick="closeCommunitiesModal()" style="margin-top: 20px;">Close</button>
2389
+ </div>
2390
+ </div>
2391
+
2392
+ <!-- GROUPS MODAL -->
2393
+ <div class="modal hidden" id="groupsModal">
2394
+ <div class="modal-content">
2395
+ <h2><i class="fas fa-user-group"></i> Groups</h2>
2396
+ <p style="margin-top: 10px; color: var(--text-secondary);">Create and manage your own small group chats.</p>
2397
+ <div id="groupsList" style="margin-top: 16px;"></div>
2398
+ <button class="save-btn" onclick="closeGroupsModal()" style="margin-top: 20px;">Close</button>
2399
+ </div>
2400
+ </div>
2401
+
2402
+ <!-- QR CODE MODAL -->
2403
+ <div class="qr-modal" id="qrModal">
2404
+ <div class="qr-modal-content">
2405
+ <h2 class="qr-modal-title">πŸ“± Friend Request QR Code</h2>
2406
+ <p class="qr-modal-subtitle">Let your friends scan this code to send you a friend request!</p>
2407
+ <div class="qr-code-container">
2408
+ <canvas id="qrCodeCanvas" width="200" height="200"></canvas>
2409
+ </div>
2410
+ <p style="font-size: 13px; color: var(--text-secondary); margin-bottom: 16px;">Your ID: <strong id="qrUserId" style="color: var(--primary-color);"></strong></p>
2411
+ <button class="qr-modal-close" onclick="closeQRCodeModal()">Close</button>
2412
+ </div>
2413
+ </div>
2414
+
2415
+ <!-- MEDIA PREVIEW PANEL -->
2416
+ <div class="media-preview-panel" id="mediaPreviewPanel">
2417
+ <div class="media-preview-header">
2418
+ <h3 class="media-preview-title">πŸ“Έ Copied Media</h3>
2419
+ <button class="media-preview-close" onclick="closeMediaPreview()">
2420
+ <i class="fas fa-times"></i>
2421
+ </button>
2422
+ </div>
2423
+ <div class="media-preview-content" id="mediaPreviewContent">
2424
+ <!-- Copied media will be displayed here -->
2425
+ </div>
2426
+ </div>
2427
+
2428
+ <!-- CUSTOM DIALOG MODAL (replaces alert/confirm/prompt) -->
2429
+ <div id="customDialog" style="display: none;"></div>
2430
+
2431
  <script src="/socket.io/socket.io.js"></script>
2432
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
2433
+ <script src="client.js?v=2.2.0"></script>
2434
  </body>
2435
  </html>
package-lock.json CHANGED
@@ -14,23 +14,14 @@
14
  "dotenv": "^16.3.1",
15
  "express": "^4.18.2",
16
  "jsonwebtoken": "^9.0.2",
17
- "mongoose": "^7.5.0",
18
  "multer": "^1.4.5-lts.1",
19
  "nodemon": "^3.0.1",
 
 
20
  "socket.io": "^4.5.4",
21
  "uuid": "^9.0.0"
22
  }
23
  },
24
- "node_modules/@mongodb-js/saslprep": {
25
- "version": "1.4.6",
26
- "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz",
27
- "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==",
28
- "license": "MIT",
29
- "optional": true,
30
- "dependencies": {
31
- "sparse-bitfield": "^3.0.3"
32
- }
33
- },
34
  "node_modules/@socket.io/component-emitter": {
35
  "version": "3.1.2",
36
  "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
@@ -46,6 +37,21 @@
46
  "@types/node": "*"
47
  }
48
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  "node_modules/@types/node": {
50
  "version": "25.3.3",
51
  "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz",
@@ -55,22 +61,12 @@
55
  "undici-types": "~7.18.0"
56
  }
57
  },
58
- "node_modules/@types/webidl-conversions": {
59
- "version": "7.0.3",
60
- "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
61
- "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
62
  "license": "MIT"
63
  },
64
- "node_modules/@types/whatwg-url": {
65
- "version": "8.2.2",
66
- "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz",
67
- "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
68
- "license": "MIT",
69
- "dependencies": {
70
- "@types/node": "*",
71
- "@types/webidl-conversions": "*"
72
- }
73
- },
74
  "node_modules/accepts": {
75
  "version": "1.3.8",
76
  "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -208,15 +204,6 @@
208
  "node": ">=8"
209
  }
210
  },
211
- "node_modules/bson": {
212
- "version": "5.5.1",
213
- "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz",
214
- "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==",
215
- "license": "Apache-2.0",
216
- "engines": {
217
- "node": ">=14.20.1"
218
- }
219
- },
220
  "node_modules/buffer-equal-constant-time": {
221
  "version": "1.0.1",
222
  "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -424,6 +411,12 @@
424
  "url": "https://dotenvx.com"
425
  }
426
  },
 
 
 
 
 
 
427
  "node_modules/dunder-proto": {
428
  "version": "1.0.1",
429
  "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -815,21 +808,21 @@
815
  "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
816
  "license": "ISC"
817
  },
 
 
 
 
 
 
 
 
 
818
  "node_modules/inherits": {
819
  "version": "2.0.4",
820
  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
821
  "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
822
  "license": "ISC"
823
  },
824
- "node_modules/ip-address": {
825
- "version": "10.1.0",
826
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
827
- "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
828
- "license": "MIT",
829
- "engines": {
830
- "node": ">= 12"
831
- }
832
- },
833
  "node_modules/ipaddr.js": {
834
  "version": "1.9.1",
835
  "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -930,14 +923,11 @@
930
  "safe-buffer": "^5.0.1"
931
  }
932
  },
933
- "node_modules/kareem": {
934
- "version": "2.5.1",
935
- "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz",
936
- "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==",
937
- "license": "Apache-2.0",
938
- "engines": {
939
- "node": ">=12.0.0"
940
- }
941
  },
942
  "node_modules/lodash.includes": {
943
  "version": "4.3.0",
@@ -999,13 +989,6 @@
999
  "node": ">= 0.6"
1000
  }
1001
  },
1002
- "node_modules/memory-pager": {
1003
- "version": "1.5.0",
1004
- "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
1005
- "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
1006
- "license": "MIT",
1007
- "optional": true
1008
- },
1009
  "node_modules/merge-descriptors": {
1010
  "version": "1.0.3",
1011
  "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
@@ -1093,98 +1076,25 @@
1093
  "mkdirp": "bin/cmd.js"
1094
  }
1095
  },
1096
- "node_modules/mongodb": {
1097
- "version": "5.9.2",
1098
- "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz",
1099
- "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==",
1100
- "license": "Apache-2.0",
1101
- "dependencies": {
1102
- "bson": "^5.5.0",
1103
- "mongodb-connection-string-url": "^2.6.0",
1104
- "socks": "^2.7.1"
1105
- },
1106
- "engines": {
1107
- "node": ">=14.20.1"
1108
- },
1109
- "optionalDependencies": {
1110
- "@mongodb-js/saslprep": "^1.1.0"
1111
- },
1112
- "peerDependencies": {
1113
- "@aws-sdk/credential-providers": "^3.188.0",
1114
- "@mongodb-js/zstd": "^1.0.0",
1115
- "kerberos": "^1.0.0 || ^2.0.0",
1116
- "mongodb-client-encryption": ">=2.3.0 <3",
1117
- "snappy": "^7.2.2"
1118
- },
1119
- "peerDependenciesMeta": {
1120
- "@aws-sdk/credential-providers": {
1121
- "optional": true
1122
- },
1123
- "@mongodb-js/zstd": {
1124
- "optional": true
1125
- },
1126
- "kerberos": {
1127
- "optional": true
1128
- },
1129
- "mongodb-client-encryption": {
1130
- "optional": true
1131
- },
1132
- "snappy": {
1133
- "optional": true
1134
- }
1135
- }
1136
- },
1137
- "node_modules/mongodb-connection-string-url": {
1138
- "version": "2.6.0",
1139
- "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz",
1140
- "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==",
1141
- "license": "Apache-2.0",
1142
- "dependencies": {
1143
- "@types/whatwg-url": "^8.2.1",
1144
- "whatwg-url": "^11.0.0"
1145
- }
1146
- },
1147
- "node_modules/mongoose": {
1148
- "version": "7.8.9",
1149
- "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.8.9.tgz",
1150
- "integrity": "sha512-V3GBAJbmOAdzEP8murOvlg7q1szlbe4jTBRyW+JBHRduJBe7F9dk5eyqJDTuYrdBcOOWfLbr6AgXrDK7F0/o5A==",
1151
- "license": "MIT",
1152
- "dependencies": {
1153
- "bson": "^5.5.0",
1154
- "kareem": "2.5.1",
1155
- "mongodb": "5.9.2",
1156
- "mpath": "0.9.0",
1157
- "mquery": "5.0.0",
1158
- "ms": "2.1.3",
1159
- "sift": "16.0.1"
1160
- },
1161
- "engines": {
1162
- "node": ">=14.20.1"
1163
- },
1164
- "funding": {
1165
- "type": "opencollective",
1166
- "url": "https://opencollective.com/mongoose"
1167
- }
1168
- },
1169
- "node_modules/mpath": {
1170
- "version": "0.9.0",
1171
- "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
1172
- "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
1173
  "license": "MIT",
1174
  "engines": {
1175
- "node": ">=4.0.0"
1176
  }
1177
  },
1178
- "node_modules/mquery": {
1179
- "version": "5.0.0",
1180
- "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz",
1181
- "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
1182
  "license": "MIT",
1183
  "dependencies": {
1184
- "debug": "4.x"
1185
  },
1186
  "engines": {
1187
- "node": ">=14.0.0"
1188
  }
1189
  },
1190
  "node_modules/ms": {
@@ -1306,6 +1216,96 @@
1306
  "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
1307
  "license": "MIT"
1308
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1309
  "node_modules/picomatch": {
1310
  "version": "2.3.1",
1311
  "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@@ -1318,6 +1318,45 @@
1318
  "url": "https://github.com/sponsors/jonschlinkert"
1319
  }
1320
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1321
  "node_modules/process-nextick-args": {
1322
  "version": "2.0.1",
1323
  "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -1343,15 +1382,6 @@
1343
  "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
1344
  "license": "MIT"
1345
  },
1346
- "node_modules/punycode": {
1347
- "version": "2.3.1",
1348
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
1349
- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
1350
- "license": "MIT",
1351
- "engines": {
1352
- "node": ">=6"
1353
- }
1354
- },
1355
  "node_modules/qs": {
1356
  "version": "6.14.2",
1357
  "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
@@ -1424,6 +1454,12 @@
1424
  "node": ">=8.10.0"
1425
  }
1426
  },
 
 
 
 
 
 
1427
  "node_modules/safe-buffer": {
1428
  "version": "5.2.1",
1429
  "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -1501,6 +1537,86 @@
1501
  "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
1502
  "license": "MIT"
1503
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1504
  "node_modules/serve-static": {
1505
  "version": "1.16.3",
1506
  "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
@@ -1594,12 +1710,6 @@
1594
  "url": "https://github.com/sponsors/ljharb"
1595
  }
1596
  },
1597
- "node_modules/sift": {
1598
- "version": "16.0.1",
1599
- "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz",
1600
- "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==",
1601
- "license": "MIT"
1602
- },
1603
  "node_modules/simple-update-notifier": {
1604
  "version": "2.0.0",
1605
  "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
@@ -1612,16 +1722,6 @@
1612
  "node": ">=10"
1613
  }
1614
  },
1615
- "node_modules/smart-buffer": {
1616
- "version": "4.2.0",
1617
- "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
1618
- "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
1619
- "license": "MIT",
1620
- "engines": {
1621
- "node": ">= 6.0.0",
1622
- "npm": ">= 3.0.0"
1623
- }
1624
- },
1625
  "node_modules/socket.io": {
1626
  "version": "4.8.3",
1627
  "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz",
@@ -1663,28 +1763,13 @@
1663
  "node": ">=10.0.0"
1664
  }
1665
  },
1666
- "node_modules/socks": {
1667
- "version": "2.8.7",
1668
- "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
1669
- "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
1670
- "license": "MIT",
1671
- "dependencies": {
1672
- "ip-address": "^10.0.1",
1673
- "smart-buffer": "^4.2.0"
1674
- },
1675
  "engines": {
1676
- "node": ">= 10.0.0",
1677
- "npm": ">= 3.0.0"
1678
- }
1679
- },
1680
- "node_modules/sparse-bitfield": {
1681
- "version": "3.0.3",
1682
- "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
1683
- "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
1684
- "license": "MIT",
1685
- "optional": true,
1686
- "dependencies": {
1687
- "memory-pager": "^1.0.2"
1688
  }
1689
  },
1690
  "node_modules/statuses": {
@@ -1752,6 +1837,12 @@
1752
  "node": ">=0.6"
1753
  }
1754
  },
 
 
 
 
 
 
1755
  "node_modules/touch": {
1756
  "version": "3.1.1",
1757
  "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
@@ -1761,18 +1852,6 @@
1761
  "nodetouch": "bin/nodetouch.js"
1762
  }
1763
  },
1764
- "node_modules/tr46": {
1765
- "version": "3.0.0",
1766
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
1767
- "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
1768
- "license": "MIT",
1769
- "dependencies": {
1770
- "punycode": "^2.1.1"
1771
- },
1772
- "engines": {
1773
- "node": ">=12"
1774
- }
1775
- },
1776
  "node_modules/type-is": {
1777
  "version": "1.6.18",
1778
  "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -1841,6 +1920,15 @@
1841
  "uuid": "dist/bin/uuid"
1842
  }
1843
  },
 
 
 
 
 
 
 
 
 
1844
  "node_modules/vary": {
1845
  "version": "1.1.2",
1846
  "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -1850,26 +1938,13 @@
1850
  "node": ">= 0.8"
1851
  }
1852
  },
1853
- "node_modules/webidl-conversions": {
1854
- "version": "7.0.0",
1855
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
1856
- "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
1857
- "license": "BSD-2-Clause",
1858
- "engines": {
1859
- "node": ">=12"
1860
- }
1861
- },
1862
- "node_modules/whatwg-url": {
1863
- "version": "11.0.0",
1864
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
1865
- "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
1866
  "license": "MIT",
1867
  "dependencies": {
1868
- "tr46": "^3.0.0",
1869
- "webidl-conversions": "^7.0.0"
1870
- },
1871
- "engines": {
1872
- "node": ">=12"
1873
  }
1874
  },
1875
  "node_modules/ws": {
 
14
  "dotenv": "^16.3.1",
15
  "express": "^4.18.2",
16
  "jsonwebtoken": "^9.0.2",
 
17
  "multer": "^1.4.5-lts.1",
18
  "nodemon": "^3.0.1",
19
+ "pg": "^8.11.3",
20
+ "sequelize": "^6.35.2",
21
  "socket.io": "^4.5.4",
22
  "uuid": "^9.0.0"
23
  }
24
  },
 
 
 
 
 
 
 
 
 
 
25
  "node_modules/@socket.io/component-emitter": {
26
  "version": "3.1.2",
27
  "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
 
37
  "@types/node": "*"
38
  }
39
  },
40
+ "node_modules/@types/debug": {
41
+ "version": "4.1.12",
42
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
43
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
44
+ "license": "MIT",
45
+ "dependencies": {
46
+ "@types/ms": "*"
47
+ }
48
+ },
49
+ "node_modules/@types/ms": {
50
+ "version": "2.1.0",
51
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
52
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
53
+ "license": "MIT"
54
+ },
55
  "node_modules/@types/node": {
56
  "version": "25.3.3",
57
  "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz",
 
61
  "undici-types": "~7.18.0"
62
  }
63
  },
64
+ "node_modules/@types/validator": {
65
+ "version": "13.15.10",
66
+ "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
67
+ "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
68
  "license": "MIT"
69
  },
 
 
 
 
 
 
 
 
 
 
70
  "node_modules/accepts": {
71
  "version": "1.3.8",
72
  "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
 
204
  "node": ">=8"
205
  }
206
  },
 
 
 
 
 
 
 
 
 
207
  "node_modules/buffer-equal-constant-time": {
208
  "version": "1.0.1",
209
  "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
 
411
  "url": "https://dotenvx.com"
412
  }
413
  },
414
+ "node_modules/dottie": {
415
+ "version": "2.0.7",
416
+ "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.7.tgz",
417
+ "integrity": "sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==",
418
+ "license": "MIT"
419
+ },
420
  "node_modules/dunder-proto": {
421
  "version": "1.0.1",
422
  "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
 
808
  "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
809
  "license": "ISC"
810
  },
811
+ "node_modules/inflection": {
812
+ "version": "1.13.4",
813
+ "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz",
814
+ "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==",
815
+ "engines": [
816
+ "node >= 0.4.0"
817
+ ],
818
+ "license": "MIT"
819
+ },
820
  "node_modules/inherits": {
821
  "version": "2.0.4",
822
  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
823
  "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
824
  "license": "ISC"
825
  },
 
 
 
 
 
 
 
 
 
826
  "node_modules/ipaddr.js": {
827
  "version": "1.9.1",
828
  "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
 
923
  "safe-buffer": "^5.0.1"
924
  }
925
  },
926
+ "node_modules/lodash": {
927
+ "version": "4.17.23",
928
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
929
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
930
+ "license": "MIT"
 
 
 
931
  },
932
  "node_modules/lodash.includes": {
933
  "version": "4.3.0",
 
989
  "node": ">= 0.6"
990
  }
991
  },
 
 
 
 
 
 
 
992
  "node_modules/merge-descriptors": {
993
  "version": "1.0.3",
994
  "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
 
1076
  "mkdirp": "bin/cmd.js"
1077
  }
1078
  },
1079
+ "node_modules/moment": {
1080
+ "version": "2.30.1",
1081
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
1082
+ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1083
  "license": "MIT",
1084
  "engines": {
1085
+ "node": "*"
1086
  }
1087
  },
1088
+ "node_modules/moment-timezone": {
1089
+ "version": "0.5.48",
1090
+ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
1091
+ "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
1092
  "license": "MIT",
1093
  "dependencies": {
1094
+ "moment": "^2.29.4"
1095
  },
1096
  "engines": {
1097
+ "node": "*"
1098
  }
1099
  },
1100
  "node_modules/ms": {
 
1216
  "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
1217
  "license": "MIT"
1218
  },
1219
+ "node_modules/pg": {
1220
+ "version": "8.20.0",
1221
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
1222
+ "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
1223
+ "license": "MIT",
1224
+ "peer": true,
1225
+ "dependencies": {
1226
+ "pg-connection-string": "^2.12.0",
1227
+ "pg-pool": "^3.13.0",
1228
+ "pg-protocol": "^1.13.0",
1229
+ "pg-types": "2.2.0",
1230
+ "pgpass": "1.0.5"
1231
+ },
1232
+ "engines": {
1233
+ "node": ">= 16.0.0"
1234
+ },
1235
+ "optionalDependencies": {
1236
+ "pg-cloudflare": "^1.3.0"
1237
+ },
1238
+ "peerDependencies": {
1239
+ "pg-native": ">=3.0.1"
1240
+ },
1241
+ "peerDependenciesMeta": {
1242
+ "pg-native": {
1243
+ "optional": true
1244
+ }
1245
+ }
1246
+ },
1247
+ "node_modules/pg-cloudflare": {
1248
+ "version": "1.3.0",
1249
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
1250
+ "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
1251
+ "license": "MIT",
1252
+ "optional": true
1253
+ },
1254
+ "node_modules/pg-connection-string": {
1255
+ "version": "2.12.0",
1256
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
1257
+ "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
1258
+ "license": "MIT"
1259
+ },
1260
+ "node_modules/pg-int8": {
1261
+ "version": "1.0.1",
1262
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
1263
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
1264
+ "license": "ISC",
1265
+ "engines": {
1266
+ "node": ">=4.0.0"
1267
+ }
1268
+ },
1269
+ "node_modules/pg-pool": {
1270
+ "version": "3.13.0",
1271
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
1272
+ "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
1273
+ "license": "MIT",
1274
+ "peerDependencies": {
1275
+ "pg": ">=8.0"
1276
+ }
1277
+ },
1278
+ "node_modules/pg-protocol": {
1279
+ "version": "1.13.0",
1280
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
1281
+ "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
1282
+ "license": "MIT"
1283
+ },
1284
+ "node_modules/pg-types": {
1285
+ "version": "2.2.0",
1286
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
1287
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
1288
+ "license": "MIT",
1289
+ "dependencies": {
1290
+ "pg-int8": "1.0.1",
1291
+ "postgres-array": "~2.0.0",
1292
+ "postgres-bytea": "~1.0.0",
1293
+ "postgres-date": "~1.0.4",
1294
+ "postgres-interval": "^1.1.0"
1295
+ },
1296
+ "engines": {
1297
+ "node": ">=4"
1298
+ }
1299
+ },
1300
+ "node_modules/pgpass": {
1301
+ "version": "1.0.5",
1302
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
1303
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
1304
+ "license": "MIT",
1305
+ "dependencies": {
1306
+ "split2": "^4.1.0"
1307
+ }
1308
+ },
1309
  "node_modules/picomatch": {
1310
  "version": "2.3.1",
1311
  "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
 
1318
  "url": "https://github.com/sponsors/jonschlinkert"
1319
  }
1320
  },
1321
+ "node_modules/postgres-array": {
1322
+ "version": "2.0.0",
1323
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
1324
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
1325
+ "license": "MIT",
1326
+ "engines": {
1327
+ "node": ">=4"
1328
+ }
1329
+ },
1330
+ "node_modules/postgres-bytea": {
1331
+ "version": "1.0.1",
1332
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
1333
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
1334
+ "license": "MIT",
1335
+ "engines": {
1336
+ "node": ">=0.10.0"
1337
+ }
1338
+ },
1339
+ "node_modules/postgres-date": {
1340
+ "version": "1.0.7",
1341
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
1342
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
1343
+ "license": "MIT",
1344
+ "engines": {
1345
+ "node": ">=0.10.0"
1346
+ }
1347
+ },
1348
+ "node_modules/postgres-interval": {
1349
+ "version": "1.2.0",
1350
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
1351
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
1352
+ "license": "MIT",
1353
+ "dependencies": {
1354
+ "xtend": "^4.0.0"
1355
+ },
1356
+ "engines": {
1357
+ "node": ">=0.10.0"
1358
+ }
1359
+ },
1360
  "node_modules/process-nextick-args": {
1361
  "version": "2.0.1",
1362
  "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
 
1382
  "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
1383
  "license": "MIT"
1384
  },
 
 
 
 
 
 
 
 
 
1385
  "node_modules/qs": {
1386
  "version": "6.14.2",
1387
  "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
 
1454
  "node": ">=8.10.0"
1455
  }
1456
  },
1457
+ "node_modules/retry-as-promised": {
1458
+ "version": "7.1.1",
1459
+ "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz",
1460
+ "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==",
1461
+ "license": "MIT"
1462
+ },
1463
  "node_modules/safe-buffer": {
1464
  "version": "5.2.1",
1465
  "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
 
1537
  "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
1538
  "license": "MIT"
1539
  },
1540
+ "node_modules/sequelize": {
1541
+ "version": "6.37.7",
1542
+ "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz",
1543
+ "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==",
1544
+ "funding": [
1545
+ {
1546
+ "type": "opencollective",
1547
+ "url": "https://opencollective.com/sequelize"
1548
+ }
1549
+ ],
1550
+ "license": "MIT",
1551
+ "dependencies": {
1552
+ "@types/debug": "^4.1.8",
1553
+ "@types/validator": "^13.7.17",
1554
+ "debug": "^4.3.4",
1555
+ "dottie": "^2.0.6",
1556
+ "inflection": "^1.13.4",
1557
+ "lodash": "^4.17.21",
1558
+ "moment": "^2.29.4",
1559
+ "moment-timezone": "^0.5.43",
1560
+ "pg-connection-string": "^2.6.1",
1561
+ "retry-as-promised": "^7.0.4",
1562
+ "semver": "^7.5.4",
1563
+ "sequelize-pool": "^7.1.0",
1564
+ "toposort-class": "^1.0.1",
1565
+ "uuid": "^8.3.2",
1566
+ "validator": "^13.9.0",
1567
+ "wkx": "^0.5.0"
1568
+ },
1569
+ "engines": {
1570
+ "node": ">=10.0.0"
1571
+ },
1572
+ "peerDependenciesMeta": {
1573
+ "ibm_db": {
1574
+ "optional": true
1575
+ },
1576
+ "mariadb": {
1577
+ "optional": true
1578
+ },
1579
+ "mysql2": {
1580
+ "optional": true
1581
+ },
1582
+ "oracledb": {
1583
+ "optional": true
1584
+ },
1585
+ "pg": {
1586
+ "optional": true
1587
+ },
1588
+ "pg-hstore": {
1589
+ "optional": true
1590
+ },
1591
+ "snowflake-sdk": {
1592
+ "optional": true
1593
+ },
1594
+ "sqlite3": {
1595
+ "optional": true
1596
+ },
1597
+ "tedious": {
1598
+ "optional": true
1599
+ }
1600
+ }
1601
+ },
1602
+ "node_modules/sequelize-pool": {
1603
+ "version": "7.1.0",
1604
+ "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz",
1605
+ "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==",
1606
+ "license": "MIT",
1607
+ "engines": {
1608
+ "node": ">= 10.0.0"
1609
+ }
1610
+ },
1611
+ "node_modules/sequelize/node_modules/uuid": {
1612
+ "version": "8.3.2",
1613
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
1614
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
1615
+ "license": "MIT",
1616
+ "bin": {
1617
+ "uuid": "dist/bin/uuid"
1618
+ }
1619
+ },
1620
  "node_modules/serve-static": {
1621
  "version": "1.16.3",
1622
  "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
 
1710
  "url": "https://github.com/sponsors/ljharb"
1711
  }
1712
  },
 
 
 
 
 
 
1713
  "node_modules/simple-update-notifier": {
1714
  "version": "2.0.0",
1715
  "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
 
1722
  "node": ">=10"
1723
  }
1724
  },
 
 
 
 
 
 
 
 
 
 
1725
  "node_modules/socket.io": {
1726
  "version": "4.8.3",
1727
  "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz",
 
1763
  "node": ">=10.0.0"
1764
  }
1765
  },
1766
+ "node_modules/split2": {
1767
+ "version": "4.2.0",
1768
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
1769
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
1770
+ "license": "ISC",
 
 
 
 
1771
  "engines": {
1772
+ "node": ">= 10.x"
 
 
 
 
 
 
 
 
 
 
 
1773
  }
1774
  },
1775
  "node_modules/statuses": {
 
1837
  "node": ">=0.6"
1838
  }
1839
  },
1840
+ "node_modules/toposort-class": {
1841
+ "version": "1.0.1",
1842
+ "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz",
1843
+ "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==",
1844
+ "license": "MIT"
1845
+ },
1846
  "node_modules/touch": {
1847
  "version": "3.1.1",
1848
  "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
 
1852
  "nodetouch": "bin/nodetouch.js"
1853
  }
1854
  },
 
 
 
 
 
 
 
 
 
 
 
 
1855
  "node_modules/type-is": {
1856
  "version": "1.6.18",
1857
  "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
 
1920
  "uuid": "dist/bin/uuid"
1921
  }
1922
  },
1923
+ "node_modules/validator": {
1924
+ "version": "13.15.26",
1925
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz",
1926
+ "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==",
1927
+ "license": "MIT",
1928
+ "engines": {
1929
+ "node": ">= 0.10"
1930
+ }
1931
+ },
1932
  "node_modules/vary": {
1933
  "version": "1.1.2",
1934
  "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
 
1938
  "node": ">= 0.8"
1939
  }
1940
  },
1941
+ "node_modules/wkx": {
1942
+ "version": "0.5.0",
1943
+ "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz",
1944
+ "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==",
 
 
 
 
 
 
 
 
 
1945
  "license": "MIT",
1946
  "dependencies": {
1947
+ "@types/node": "*"
 
 
 
 
1948
  }
1949
  },
1950
  "node_modules/ws": {
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "discord-like-app",
3
- "version": "2.0.0",
4
  "main": "server.js",
5
  "scripts": {
6
  "dev": "nodemon server.js",
@@ -11,7 +11,8 @@
11
  "dependencies": {
12
  "express": "^4.18.2",
13
  "socket.io": "^4.5.4",
14
- "mongoose": "^7.5.0",
 
15
  "jsonwebtoken": "^9.0.2",
16
  "bcryptjs": "^2.4.3",
17
  "multer": "^1.4.5-lts.1",
 
1
  {
2
  "name": "discord-like-app",
3
+ "version": "2.2.0",
4
  "main": "server.js",
5
  "scripts": {
6
  "dev": "nodemon server.js",
 
11
  "dependencies": {
12
  "express": "^4.18.2",
13
  "socket.io": "^4.5.4",
14
+ "sequelize": "^6.35.2",
15
+ "pg": "^8.11.3",
16
  "jsonwebtoken": "^9.0.2",
17
  "bcryptjs": "^2.4.3",
18
  "multer": "^1.4.5-lts.1",
server.js CHANGED
@@ -4,7 +4,7 @@ const os = require('os');
4
  const path = require('path');
5
  const http = require('http');
6
  const https = require('https');
7
- const mongoose = require('mongoose');
8
  const jwt = require('jsonwebtoken');
9
  const bcrypt = require('bcryptjs');
10
  const multer = require('multer');
@@ -17,106 +17,199 @@ const app = express();
17
  // Middleware
18
  app.use(bodyParser.json({ limit: '50mb' }));
19
  app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
 
 
 
 
 
 
 
 
 
 
 
20
  app.use(express.static(__dirname));
21
 
22
- // MongoDB Connection
23
- mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/discord-app', {
24
- useNewUrlParser: true,
25
- useUnifiedTopology: true
26
- }).then(() => console.log('MongoDB connected')).catch(err => console.error('MongoDB connection error:', err));
27
-
28
- // ======================== SCHEMAS ========================
29
-
30
- // User Schema
31
- const userSchema = new mongoose.Schema({
32
- username: { type: String, required: true, unique: true },
33
- email: { type: String, required: true, unique: true },
34
- password: { type: String, required: true },
35
- avatar: { type: String, default: '' },
36
- bio: { type: String, default: 'No bio yet' },
37
- status: { type: String, enum: ['online', 'offline', 'away'], default: 'offline' },
38
- friends: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
39
- blockedUsers: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
40
- coins: { type: Number, default: 1000 },
41
- createdAt: { type: Date, default: Date.now }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  });
43
 
44
- // Channel Schema
45
- const channelSchema = new mongoose.Schema({
46
- channelId: String,
47
- name: String,
48
- type: { type: String, enum: ['text', 'voice'], default: 'text' },
49
- description: String,
50
- isPrivate: { type: Boolean, default: false },
51
- members: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
52
- createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
53
- createdAt: { type: Date, default: Date.now }
 
 
 
 
 
 
 
54
  });
55
 
56
- // Message Schema
57
- const messageSchema = new mongoose.Schema({
58
- messageId: String,
59
- channelId: String,
60
- sender: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
61
- senderUsername: String,
62
- content: String,
63
- mediaType: { type: String, enum: ['text', 'image', 'video', 'emoji'], default: 'text' },
64
- mediaUrl: String,
65
- timestamp: { type: Date, default: Date.now },
66
- reactions: [{ emoji: String, users: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }] }]
 
 
 
 
67
  });
68
 
69
- // Shop Item Schema
70
- const shopItemSchema = new mongoose.Schema({
71
- itemId: String,
72
- name: String,
73
- description: String,
74
- price: Number,
75
- category: String,
76
- image: String,
77
- createdAt: { type: Date, default: Date.now }
 
 
 
78
  });
79
 
80
- // User Inventory Schema
81
- const inventorySchema = new mongoose.Schema({
82
- userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
83
- itemId: String,
84
- quantity: { type: Number, default: 1 },
85
- purchasedAt: { type: Date, default: Date.now }
 
 
 
 
 
86
  });
87
 
88
- // Friend Request Schema
89
- const friendRequestSchema = new mongoose.Schema({
90
- from: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
91
- to: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
92
- status: { type: String, enum: ['pending', 'accepted', 'rejected'], default: 'pending' },
93
- createdAt: { type: Date, default: Date.now }
 
 
 
 
94
  });
95
 
96
- // Direct Message Schema
97
- const directMessageSchema = new mongoose.Schema({
98
- messageId: String,
99
- from: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
100
- to: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
101
- fromUsername: String,
102
- toUsername: String,
103
- content: String,
104
- mediaType: { type: String, enum: ['text', 'image', 'video', 'emoji'], default: 'text' },
105
- mediaUrl: String,
106
- timestamp: { type: Date, default: Date.now },
107
- readBy: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }]
 
 
 
108
  });
109
 
110
- // Models
111
- const User = mongoose.model('User', userSchema);
112
- const Channel = mongoose.model('Channel', channelSchema);
113
- const Message = mongoose.model('Message', messageSchema);
114
- const ShopItem = mongoose.model('ShopItem', shopItemSchema);
115
- const Inventory = mongoose.model('Inventory', inventorySchema);
116
- const FriendRequest = mongoose.model('FriendRequest', friendRequestSchema);
117
- const DirectMessage = mongoose.model('DirectMessage', directMessageSchema);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
- // Auth Middleware
120
  const authenticateToken = (req, res, next) => {
121
  const authHeader = req.headers['authorization'];
122
  const token = authHeader && authHeader.split(' ')[1];
@@ -132,7 +225,6 @@ const authenticateToken = (req, res, next) => {
132
 
133
  // ======================== AUTH ROUTES ========================
134
 
135
- // Register
136
  app.post('/api/register', async (req, res) => {
137
  try {
138
  const { username, email, password } = req.body;
@@ -141,30 +233,25 @@ app.post('/api/register', async (req, res) => {
141
  return res.status(400).json({ error: 'All fields required' });
142
  }
143
 
144
- if (await User.findOne({ username })) {
145
- return res.status(400).json({ error: 'Username already taken' });
146
- }
147
-
148
- if (await User.findOne({ email })) {
149
- return res.status(400).json({ error: 'Email already registered' });
150
  }
151
 
152
  const hashedPassword = await bcrypt.hash(password, 10);
153
- const user = new User({ username, email, password: hashedPassword });
154
- await user.save();
155
 
156
  const token = jwt.sign(
157
- { userId: user._id, username: user.username },
158
  process.env.JWT_SECRET || 'secret_key'
159
  );
160
 
161
- res.status(201).json({ token, user: { id: user._id, username, email } });
162
  } catch (error) {
163
  res.status(500).json({ error: error.message });
164
  }
165
  });
166
 
167
- // Login
168
  app.post('/api/login', async (req, res) => {
169
  try {
170
  const { email, password } = req.body;
@@ -173,7 +260,7 @@ app.post('/api/login', async (req, res) => {
173
  return res.status(400).json({ error: 'Email and password required' });
174
  }
175
 
176
- const user = await User.findOne({ email });
177
  if (!user) {
178
  return res.status(401).json({ error: 'Invalid credentials' });
179
  }
@@ -184,40 +271,67 @@ app.post('/api/login', async (req, res) => {
184
  }
185
 
186
  const token = jwt.sign(
187
- { userId: user._id, username: user.username },
188
  process.env.JWT_SECRET || 'secret_key'
189
  );
190
 
191
- res.json({
192
- token,
193
- user: {
194
- id: user._id,
195
- username: user.username,
196
  email: user.email,
197
  avatar: user.avatar,
198
  bio: user.bio,
 
199
  coins: user.coins
200
- }
201
  });
202
  } catch (error) {
203
  res.status(500).json({ error: error.message });
204
  }
205
  });
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  // ======================== PROFILE ROUTES ========================
208
 
209
- // Get Profile
210
  app.get('/api/profile/:userId', authenticateToken, async (req, res) => {
211
  try {
212
- const user = await User.findById(req.params.userId).populate('friends');
213
  if (!user) return res.status(404).json({ error: 'User not found' });
214
 
215
  res.json({
216
- id: user._id,
217
  username: user.username,
218
  email: user.email,
219
  avatar: user.avatar,
220
  bio: user.bio,
 
221
  status: user.status,
222
  coins: user.coins,
223
  friends: user.friends,
@@ -228,16 +342,16 @@ app.get('/api/profile/:userId', authenticateToken, async (req, res) => {
228
  }
229
  });
230
 
231
- // Update Profile
232
  app.put('/api/profile', authenticateToken, async (req, res) => {
233
  try {
234
- const { username, bio, avatar } = req.body;
235
- const user = await User.findById(req.user.userId);
236
 
237
  if (!user) return res.status(404).json({ error: 'User not found' });
238
 
239
  if (username && username !== user.username) {
240
- if (await User.findOne({ username })) {
 
241
  return res.status(400).json({ error: 'Username already taken' });
242
  }
243
  user.username = username;
@@ -245,16 +359,29 @@ app.put('/api/profile', authenticateToken, async (req, res) => {
245
 
246
  if (bio) user.bio = bio;
247
  if (avatar) user.avatar = avatar;
 
248
 
249
  await user.save();
250
 
251
- res.json({
 
 
 
 
 
 
 
 
 
 
 
252
  message: 'Profile updated',
253
  user: {
254
- id: user._id,
255
  username: user.username,
256
  avatar: user.avatar,
257
- bio: user.bio
 
258
  }
259
  });
260
  } catch (error) {
@@ -262,20 +389,29 @@ app.put('/api/profile', authenticateToken, async (req, res) => {
262
  }
263
  });
264
 
265
- // Upload Profile Image
266
  app.post('/api/profile/upload', authenticateToken, async (req, res) => {
267
  try {
268
  const { avatar } = req.body;
269
- const user = await User.findById(req.user.userId);
270
 
271
  if (!user) return res.status(404).json({ error: 'User not found' });
272
 
273
  if (avatar) {
274
  user.avatar = avatar;
275
  await user.save();
 
 
 
 
 
 
 
 
 
 
276
  }
277
 
278
- res.json({
279
  message: 'Profile image updated',
280
  avatar: user.avatar
281
  });
@@ -286,42 +422,43 @@ app.post('/api/profile/upload', authenticateToken, async (req, res) => {
286
 
287
  // ======================== FRIENDS ROUTES ========================
288
 
289
- // Send Friend Request
290
  app.post('/api/friends/request', authenticateToken, async (req, res) => {
291
  try {
292
  const { toUserId } = req.body;
293
- const request = new FriendRequest({ from: req.user.userId, to: toUserId });
294
- await request.save();
295
  res.status(201).json({ message: 'Friend request sent' });
296
  } catch (error) {
297
  res.status(500).json({ error: error.message });
298
  }
299
  });
300
 
301
- // Get Friend Requests
302
  app.get('/api/friends/requests', authenticateToken, async (req, res) => {
303
  try {
304
- const requests = await FriendRequest.find({ to: req.user.userId, status: 'pending' })
305
- .populate('from', 'username avatar');
 
 
306
  res.json(requests);
307
  } catch (error) {
308
  res.status(500).json({ error: error.message });
309
  }
310
  });
311
 
312
- // Accept Friend Request
313
  app.post('/api/friends/accept', authenticateToken, async (req, res) => {
314
  try {
315
  const { requestId } = req.body;
316
- const request = await FriendRequest.findById(requestId);
317
 
318
  if (!request) return res.status(404).json({ error: 'Request not found' });
319
 
320
  request.status = 'accepted';
321
  await request.save();
322
 
323
- await User.findByIdAndUpdate(request.from, { $addToSet: { friends: request.to } });
324
- await User.findByIdAndUpdate(request.to, { $addToSet: { friends: request.from } });
 
 
 
325
 
326
  res.json({ message: 'Friend request accepted' });
327
  } catch (error) {
@@ -329,11 +466,16 @@ app.post('/api/friends/accept', authenticateToken, async (req, res) => {
329
  }
330
  });
331
 
332
- // Get Friends List
333
  app.get('/api/friends', authenticateToken, async (req, res) => {
334
  try {
335
- const user = await User.findById(req.user.userId).populate('friends', 'username avatar status bio');
336
- res.json(user.friends);
 
 
 
 
 
 
337
  } catch (error) {
338
  res.status(500).json({ error: error.message });
339
  }
@@ -341,41 +483,59 @@ app.get('/api/friends', authenticateToken, async (req, res) => {
341
 
342
  // ======================== DIRECT MESSAGES ROUTES ========================
343
 
344
- // Get DMs with a specific friend
345
  app.get('/api/dms/:friendId', authenticateToken, async (req, res) => {
346
  try {
347
  const { friendId } = req.params;
348
- const messages = await DirectMessage.find({
349
- $or: [
350
- { from: req.user.userId, to: friendId },
351
- { from: friendId, to: req.user.userId }
352
- ]
353
- }).sort({ timestamp: 1 });
354
-
355
- res.json(messages);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  } catch (error) {
357
  res.status(500).json({ error: error.message });
358
  }
359
  });
360
 
361
- // Send DM (POST also works in addition to Socket.IO)
362
  app.post('/api/dms', authenticateToken, async (req, res) => {
363
  try {
364
  const { toUserId, content, mediaType, mediaUrl } = req.body;
365
- const fromUser = await User.findById(req.user.userId);
366
- const toUser = await User.findById(toUserId);
367
 
368
  if (!toUser) return res.status(404).json({ error: 'User not found' });
369
 
370
- // Check if users are friends
371
- if (!fromUser.friends.includes(toUserId)) {
372
  return res.status(403).json({ error: 'Can only DM friends' });
373
  }
374
 
375
- const dm = new DirectMessage({
376
  messageId: uuidv4(),
377
- from: req.user.userId,
378
- to: toUserId,
379
  fromUsername: fromUser.username,
380
  toUsername: toUser.username,
381
  content,
@@ -383,7 +543,6 @@ app.post('/api/dms', authenticateToken, async (req, res) => {
383
  mediaUrl: mediaUrl || null
384
  });
385
 
386
- await dm.save();
387
  res.status(201).json(dm);
388
  } catch (error) {
389
  res.status(500).json({ error: error.message });
@@ -392,22 +551,20 @@ app.post('/api/dms', authenticateToken, async (req, res) => {
392
 
393
  // ======================== SHOP ROUTES ========================
394
 
395
- // Get Shop Items
396
  app.get('/api/shop', authenticateToken, async (req, res) => {
397
  try {
398
- const items = await ShopItem.find();
399
  res.json(items);
400
  } catch (error) {
401
  res.status(500).json({ error: error.message });
402
  }
403
  });
404
 
405
- // Buy Shop Item
406
  app.post('/api/shop/buy', authenticateToken, async (req, res) => {
407
  try {
408
  const { itemId } = req.body;
409
- const item = await ShopItem.findById(itemId);
410
- const user = await User.findById(req.user.userId);
411
 
412
  if (!item) return res.status(404).json({ error: 'Item not found' });
413
  if (user.coins < item.price) return res.status(400).json({ error: 'Not enough coins' });
@@ -415,8 +572,7 @@ app.post('/api/shop/buy', authenticateToken, async (req, res) => {
415
  user.coins -= item.price;
416
  await user.save();
417
 
418
- const inventory = new Inventory({ userId: user._id, itemId: item._id });
419
- await inventory.save();
420
 
421
  res.json({ message: 'Item purchased', coins: user.coins });
422
  } catch (error) {
@@ -424,43 +580,13 @@ app.post('/api/shop/buy', authenticateToken, async (req, res) => {
424
  }
425
  });
426
 
427
- // Get User Inventory
428
  app.get('/api/inventory', authenticateToken, async (req, res) => {
429
  try {
430
- const inventory = await Inventory.find({ userId: req.user.userId }).populate('itemId');
431
- res.json(inventory);
432
- } catch (error) {
433
- res.status(500).json({ error: error.message });
434
- }
435
- });
436
-
437
- // ======================== CHANNELS ROUTES ========================
438
-
439
- // Create Channel
440
- app.post('/api/channels', authenticateToken, async (req, res) => {
441
- try {
442
- const { name, type, description, isPrivate } = req.body;
443
- const channel = new Channel({
444
- channelId: uuidv4(),
445
- name,
446
- type,
447
- description,
448
- isPrivate,
449
- createdBy: req.user.userId,
450
- members: [req.user.userId]
451
  });
452
- await channel.save();
453
- res.status(201).json(channel);
454
- } catch (error) {
455
- res.status(500).json({ error: error.message });
456
- }
457
- });
458
-
459
- // Get All Channels
460
- app.get('/api/channels', authenticateToken, async (req, res) => {
461
- try {
462
- const channels = await Channel.find({ $or: [{ isPrivate: false }, { members: req.user.userId }] });
463
- res.json(channels);
464
  } catch (error) {
465
  res.status(500).json({ error: error.message });
466
  }
@@ -495,23 +621,22 @@ function createServer(appInstance) {
495
 
496
  const { server, protocol } = createServer(app);
497
  const io = require('socket.io')(server, {
 
498
  cors: {
499
  origin: process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',') : '*',
500
  methods: ['GET', 'POST']
501
  }
502
  });
503
 
504
- // Store active connections
505
  const onlineUsers = new Map();
506
 
507
  io.on('connection', (socket) => {
508
  console.log('User connected:', socket.id);
509
 
510
- // User joins with authentication token
511
  socket.on('join', async (data) => {
512
  try {
513
  const decoded = jwt.verify(data.token, process.env.JWT_SECRET || 'secret_key');
514
- const user = await User.findById(decoded.userId);
515
 
516
  if (user) {
517
  socket.userId = decoded.userId;
@@ -528,7 +653,6 @@ io.on('connection', (socket) => {
528
  user.status = 'online';
529
  await user.save();
530
 
531
- // Broadcast updated users list
532
  io.emit('users update', Array.from(onlineUsers.values()));
533
  }
534
  } catch (error) {
@@ -536,71 +660,65 @@ io.on('connection', (socket) => {
536
  }
537
  });
538
 
539
- // Join a DM room with a friend
540
  socket.on('join dm', (friendId) => {
541
  const roomId = [socket.userId, friendId].sort().join('_');
542
  socket.join(roomId);
543
  socket.currentDmRoom = roomId;
544
  });
545
 
546
- // Leave a DM room
547
  socket.on('leave dm', (friendId) => {
548
  const roomId = [socket.userId, friendId].sort().join('_');
549
  socket.leave(roomId);
550
  socket.currentDmRoom = null;
551
  });
552
 
553
- // Send DM
554
  socket.on('send dm', async (data) => {
555
  try {
556
  const { toUserId, content, mediaType, mediaUrl } = data;
557
- const fromUser = await User.findById(socket.userId);
558
- const toUser = await User.findById(toUserId);
559
 
560
  if (!toUser) {
561
  socket.emit('error', 'User not found');
562
  return;
563
  }
564
 
565
- // Check they are friends
566
- if (!fromUser.friends.includes(toUserId)) {
 
567
  socket.emit('error', 'Can only DM friends');
568
  return;
569
  }
570
 
571
- const dm = new DirectMessage({
572
  messageId: uuidv4(),
573
- from: socket.userId,
574
- to: toUserId,
575
- fromUsername: socket.username,
576
  toUsername: toUser.username,
577
  content,
578
  mediaType: mediaType || 'text',
579
  mediaUrl: mediaUrl || null
580
  });
581
 
582
- await dm.save();
583
-
584
- // Send to DM room
585
  const roomId = [socket.userId, toUserId].sort().join('_');
586
  io.to(roomId).emit('dm message', {
587
  messageId: dm.messageId,
588
  from: socket.userId,
589
- fromUsername: socket.username,
590
- fromAvatar: socket.userAvatar,
591
  to: toUserId,
592
  toUsername: toUser.username,
593
  content: dm.content,
594
  mediaType: dm.mediaType,
595
  mediaUrl: dm.mediaUrl,
596
- timestamp: dm.timestamp
597
  });
598
  } catch (error) {
599
  socket.emit('error', error.message);
600
  }
601
  });
602
 
603
- // Typing indicator for DMs
604
  socket.on('dm typing', (friendId) => {
605
  const roomId = [socket.userId, friendId].sort().join('_');
606
  socket.broadcast.to(roomId).emit('dm user typing', {
@@ -617,12 +735,11 @@ io.on('connection', (socket) => {
617
  });
618
  });
619
 
620
- // Disconnect
621
  socket.on('disconnect', async () => {
622
  try {
623
  const userData = onlineUsers.get(socket.id);
624
  if (userData) {
625
- const user = await User.findById(socket.userId);
626
  if (user) {
627
  user.status = 'offline';
628
  await user.save();
@@ -636,7 +753,7 @@ io.on('connection', (socket) => {
636
  });
637
  });
638
 
639
- // ======================== STARTUP ========================
640
 
641
  function getLocalNetworkAddress() {
642
  const interfaces = os.networkInterfaces();
@@ -653,9 +770,49 @@ function getLocalNetworkAddress() {
653
  const PORT = process.env.PORT || 3000;
654
  const HOST = process.env.HOST || '0.0.0.0';
655
 
656
- server.listen(PORT, HOST, () => {
657
- const networkAddress = getLocalNetworkAddress();
658
- console.log(`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
659
  ╔═══════════════════════════════════════╗
660
  β•‘ πŸš€ Discord-Like Server Running β•‘
661
  β•‘ β•‘
@@ -670,7 +827,12 @@ server.listen(PORT, HOST, () => {
670
  β•‘ βœ“ Media Sharing β•‘
671
  β•‘ βœ“ Shop System β•‘
672
  β•‘ βœ“ Real-time Messaging β•‘
 
673
  β•‘ β•‘
674
  β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
675
- `);
676
- });
 
 
 
 
 
4
  const path = require('path');
5
  const http = require('http');
6
  const https = require('https');
7
+ const { Sequelize, DataTypes, Op } = require('sequelize');
8
  const jwt = require('jsonwebtoken');
9
  const bcrypt = require('bcryptjs');
10
  const multer = require('multer');
 
17
  // Middleware
18
  app.use(bodyParser.json({ limit: '50mb' }));
19
  app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
20
+
21
+ app.use((req, res, next) => {
22
+ if (req.path.endsWith('.html') || req.path.endsWith('.js') || req.path.endsWith('.css')) {
23
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
24
+ res.setHeader('Pragma', 'no-cache');
25
+ res.setHeader('Expires', '0');
26
+ res.setHeader('Surrogate-Control', 'no-store');
27
+ }
28
+ next();
29
+ });
30
+
31
  app.use(express.static(__dirname));
32
 
33
+ // ======================== DATABASE CONNECTION ========================
34
+
35
+ const sequelize = new Sequelize(
36
+ process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/discord-app',
37
+ {
38
+ dialect: 'postgres',
39
+ logging: false,
40
+ protocol: process.env.DATABASE_URL ? 'postgres' : undefined,
41
+ dialectOptions: process.env.DATABASE_URL ? { ssl: { require: true, rejectUnauthorized: false } } : undefined
42
+ }
43
+ );
44
+
45
+ // ======================== MODELS ========================
46
+
47
+ const User = sequelize.define('User', {
48
+ id: {
49
+ type: DataTypes.UUID,
50
+ defaultValue: DataTypes.UUIDV4,
51
+ primaryKey: true
52
+ },
53
+ username: {
54
+ type: DataTypes.STRING,
55
+ allowNull: false,
56
+ unique: true
57
+ },
58
+ email: {
59
+ type: DataTypes.STRING,
60
+ allowNull: false,
61
+ unique: true
62
+ },
63
+ password: {
64
+ type: DataTypes.STRING,
65
+ allowNull: false
66
+ },
67
+ avatar: {
68
+ type: DataTypes.TEXT,
69
+ defaultValue: ''
70
+ },
71
+ bio: {
72
+ type: DataTypes.STRING,
73
+ defaultValue: 'No bio yet'
74
+ },
75
+ phoneNumber: {
76
+ type: DataTypes.STRING,
77
+ defaultValue: ''
78
+ },
79
+ status: {
80
+ type: DataTypes.ENUM('online', 'offline', 'away'),
81
+ defaultValue: 'offline'
82
+ },
83
+ coins: {
84
+ type: DataTypes.INTEGER,
85
+ defaultValue: 1000
86
+ }
87
  });
88
 
89
+ const Channel = sequelize.define('Channel', {
90
+ id: {
91
+ type: DataTypes.UUID,
92
+ defaultValue: DataTypes.UUIDV4,
93
+ primaryKey: true
94
+ },
95
+ channelId: DataTypes.STRING,
96
+ name: DataTypes.STRING,
97
+ type: {
98
+ type: DataTypes.ENUM('text', 'voice'),
99
+ defaultValue: 'text'
100
+ },
101
+ description: DataTypes.STRING,
102
+ isPrivate: {
103
+ type: DataTypes.BOOLEAN,
104
+ defaultValue: false
105
+ }
106
  });
107
 
108
+ const Message = sequelize.define('Message', {
109
+ id: {
110
+ type: DataTypes.UUID,
111
+ defaultValue: DataTypes.UUIDV4,
112
+ primaryKey: true
113
+ },
114
+ messageId: DataTypes.STRING,
115
+ channelId: DataTypes.STRING,
116
+ senderUsername: DataTypes.STRING,
117
+ content: DataTypes.TEXT,
118
+ mediaType: {
119
+ type: DataTypes.ENUM('text', 'image', 'video', 'emoji', 'sticker', 'gif', 'voice'),
120
+ defaultValue: 'text'
121
+ },
122
+ mediaUrl: DataTypes.TEXT
123
  });
124
 
125
+ const ShopItem = sequelize.define('ShopItem', {
126
+ id: {
127
+ type: DataTypes.UUID,
128
+ defaultValue: DataTypes.UUIDV4,
129
+ primaryKey: true
130
+ },
131
+ itemId: DataTypes.STRING,
132
+ name: DataTypes.STRING,
133
+ description: DataTypes.STRING,
134
+ price: DataTypes.INTEGER,
135
+ category: DataTypes.STRING,
136
+ image: DataTypes.TEXT
137
  });
138
 
139
+ const Inventory = sequelize.define('Inventory', {
140
+ id: {
141
+ type: DataTypes.UUID,
142
+ defaultValue: DataTypes.UUIDV4,
143
+ primaryKey: true
144
+ },
145
+ itemId: DataTypes.STRING,
146
+ quantity: {
147
+ type: DataTypes.INTEGER,
148
+ defaultValue: 1
149
+ }
150
  });
151
 
152
+ const FriendRequest = sequelize.define('FriendRequest', {
153
+ id: {
154
+ type: DataTypes.UUID,
155
+ defaultValue: DataTypes.UUIDV4,
156
+ primaryKey: true
157
+ },
158
+ status: {
159
+ type: DataTypes.ENUM('pending', 'accepted', 'rejected'),
160
+ defaultValue: 'pending'
161
+ }
162
  });
163
 
164
+ const DirectMessage = sequelize.define('DirectMessage', {
165
+ id: {
166
+ type: DataTypes.UUID,
167
+ defaultValue: DataTypes.UUIDV4,
168
+ primaryKey: true
169
+ },
170
+ messageId: DataTypes.STRING,
171
+ fromUsername: DataTypes.STRING,
172
+ toUsername: DataTypes.STRING,
173
+ content: DataTypes.TEXT,
174
+ mediaType: {
175
+ type: DataTypes.ENUM('text', 'image', 'video', 'emoji', 'sticker', 'gif', 'voice'),
176
+ defaultValue: 'text'
177
+ },
178
+ mediaUrl: DataTypes.TEXT
179
  });
180
 
181
+ // ======================== ASSOCIATIONS ========================
182
+
183
+ User.belongsToMany(User, {
184
+ as: 'friends',
185
+ through: 'UserFriends',
186
+ foreignKey: 'UserId',
187
+ otherKey: 'FriendId'
188
+ });
189
+
190
+ User.hasMany(Channel, { foreignKey: 'createdById', as: 'channels' });
191
+ Channel.belongsTo(User, { foreignKey: 'createdById', as: 'creator' });
192
+
193
+ Channel.belongsToMany(User, { through: 'ChannelMembers', foreignKey: 'ChannelId', otherKey: 'UserId' });
194
+ User.belongsToMany(Channel, { through: 'ChannelMembers', foreignKey: 'UserId', otherKey: 'ChannelId' });
195
+
196
+ User.hasMany(Message, { foreignKey: 'senderId' });
197
+ Message.belongsTo(User, { foreignKey: 'senderId' });
198
+
199
+ User.hasMany(Inventory, { foreignKey: 'userId' });
200
+ Inventory.belongsTo(User, { foreignKey: 'userId' });
201
+
202
+ ShopItem.hasMany(Inventory, { foreignKey: 'shopItemId' });
203
+ Inventory.belongsTo(ShopItem, { foreignKey: 'shopItemId' });
204
+
205
+ FriendRequest.belongsTo(User, { as: 'from', foreignKey: 'fromId' });
206
+ FriendRequest.belongsTo(User, { as: 'to', foreignKey: 'toId' });
207
+
208
+ DirectMessage.belongsTo(User, { as: 'sender', foreignKey: 'fromId' });
209
+ DirectMessage.belongsTo(User, { as: 'recipient', foreignKey: 'toId' });
210
+
211
+ // ======================== AUTH MIDDLEWARE ========================
212
 
 
213
  const authenticateToken = (req, res, next) => {
214
  const authHeader = req.headers['authorization'];
215
  const token = authHeader && authHeader.split(' ')[1];
 
225
 
226
  // ======================== AUTH ROUTES ========================
227
 
 
228
  app.post('/api/register', async (req, res) => {
229
  try {
230
  const { username, email, password } = req.body;
 
233
  return res.status(400).json({ error: 'All fields required' });
234
  }
235
 
236
+ const existingUser = await User.findOne({ where: { [Op.or]: [{ username }, { email }] } });
237
+ if (existingUser) {
238
+ return res.status(400).json({ error: existingUser.username === username ? 'Username already taken' : 'Email already registered' });
 
 
 
239
  }
240
 
241
  const hashedPassword = await bcrypt.hash(password, 10);
242
+ const user = await User.create({ username, email, password: hashedPassword });
 
243
 
244
  const token = jwt.sign(
245
+ { userId: user.id, username: user.username },
246
  process.env.JWT_SECRET || 'secret_key'
247
  );
248
 
249
+ res.status(201).json({ token, user: { id: user.id, username, email } });
250
  } catch (error) {
251
  res.status(500).json({ error: error.message });
252
  }
253
  });
254
 
 
255
  app.post('/api/login', async (req, res) => {
256
  try {
257
  const { email, password } = req.body;
 
260
  return res.status(400).json({ error: 'Email and password required' });
261
  }
262
 
263
+ const user = await User.findOne({ where: { email } });
264
  if (!user) {
265
  return res.status(401).json({ error: 'Invalid credentials' });
266
  }
 
271
  }
272
 
273
  const token = jwt.sign(
274
+ { userId: user.id, username: user.username },
275
  process.env.JWT_SECRET || 'secret_key'
276
  );
277
 
278
+ res.json({
279
+ token,
280
+ user: {
281
+ id: user.id,
282
+ username: user.username,
283
  email: user.email,
284
  avatar: user.avatar,
285
  bio: user.bio,
286
+ phoneNumber: user.phoneNumber,
287
  coins: user.coins
288
+ }
289
  });
290
  } catch (error) {
291
  res.status(500).json({ error: error.message });
292
  }
293
  });
294
 
295
+ app.post('/api/reset-password', async (req, res) => {
296
+ try {
297
+ const { email, newPassword } = req.body;
298
+
299
+ if (!email || !newPassword) {
300
+ return res.status(400).json({ error: 'Email and new password are required' });
301
+ }
302
+
303
+ if (String(newPassword).length < 6) {
304
+ return res.status(400).json({ error: 'New password must be at least 6 characters' });
305
+ }
306
+
307
+ const user = await User.findOne({ where: { email } });
308
+ if (!user) {
309
+ return res.status(404).json({ error: 'No account found with this email' });
310
+ }
311
+
312
+ user.password = await bcrypt.hash(newPassword, 10);
313
+ await user.save();
314
+
315
+ res.json({ success: true, message: 'Password reset successfully' });
316
+ } catch (error) {
317
+ res.status(500).json({ error: error.message });
318
+ }
319
+ });
320
+
321
  // ======================== PROFILE ROUTES ========================
322
 
 
323
  app.get('/api/profile/:userId', authenticateToken, async (req, res) => {
324
  try {
325
+ const user = await User.findByPk(req.params.userId, { include: ['friends'] });
326
  if (!user) return res.status(404).json({ error: 'User not found' });
327
 
328
  res.json({
329
+ id: user.id,
330
  username: user.username,
331
  email: user.email,
332
  avatar: user.avatar,
333
  bio: user.bio,
334
+ phoneNumber: user.phoneNumber,
335
  status: user.status,
336
  coins: user.coins,
337
  friends: user.friends,
 
342
  }
343
  });
344
 
 
345
  app.put('/api/profile', authenticateToken, async (req, res) => {
346
  try {
347
+ const { username, bio, avatar, phoneNumber } = req.body;
348
+ const user = await User.findByPk(req.user.userId);
349
 
350
  if (!user) return res.status(404).json({ error: 'User not found' });
351
 
352
  if (username && username !== user.username) {
353
+ const existing = await User.findOne({ where: { username } });
354
+ if (existing) {
355
  return res.status(400).json({ error: 'Username already taken' });
356
  }
357
  user.username = username;
 
359
 
360
  if (bio) user.bio = bio;
361
  if (avatar) user.avatar = avatar;
362
+ if (typeof phoneNumber === 'string') user.phoneNumber = phoneNumber;
363
 
364
  await user.save();
365
 
366
+ for (const [socketId, onlineUser] of onlineUsers.entries()) {
367
+ if (onlineUser.userId === user.id) {
368
+ onlineUsers.set(socketId, {
369
+ ...onlineUser,
370
+ username: user.username,
371
+ avatar: user.avatar
372
+ });
373
+ }
374
+ }
375
+ io.emit('users update', Array.from(onlineUsers.values()));
376
+
377
+ res.json({
378
  message: 'Profile updated',
379
  user: {
380
+ id: user.id,
381
  username: user.username,
382
  avatar: user.avatar,
383
+ bio: user.bio,
384
+ phoneNumber: user.phoneNumber
385
  }
386
  });
387
  } catch (error) {
 
389
  }
390
  });
391
 
 
392
  app.post('/api/profile/upload', authenticateToken, async (req, res) => {
393
  try {
394
  const { avatar } = req.body;
395
+ const user = await User.findByPk(req.user.userId);
396
 
397
  if (!user) return res.status(404).json({ error: 'User not found' });
398
 
399
  if (avatar) {
400
  user.avatar = avatar;
401
  await user.save();
402
+
403
+ for (const [socketId, onlineUser] of onlineUsers.entries()) {
404
+ if (onlineUser.userId === user.id) {
405
+ onlineUsers.set(socketId, {
406
+ ...onlineUser,
407
+ avatar: user.avatar
408
+ });
409
+ }
410
+ }
411
+ io.emit('users update', Array.from(onlineUsers.values()));
412
  }
413
 
414
+ res.json({
415
  message: 'Profile image updated',
416
  avatar: user.avatar
417
  });
 
422
 
423
  // ======================== FRIENDS ROUTES ========================
424
 
 
425
  app.post('/api/friends/request', authenticateToken, async (req, res) => {
426
  try {
427
  const { toUserId } = req.body;
428
+ await FriendRequest.create({ fromId: req.user.userId, toId: toUserId });
 
429
  res.status(201).json({ message: 'Friend request sent' });
430
  } catch (error) {
431
  res.status(500).json({ error: error.message });
432
  }
433
  });
434
 
 
435
  app.get('/api/friends/requests', authenticateToken, async (req, res) => {
436
  try {
437
+ const requests = await FriendRequest.findAll({
438
+ where: { toId: req.user.userId, status: 'pending' },
439
+ include: [{ model: User, as: 'from', attributes: ['id', 'username', 'avatar'] }]
440
+ });
441
  res.json(requests);
442
  } catch (error) {
443
  res.status(500).json({ error: error.message });
444
  }
445
  });
446
 
 
447
  app.post('/api/friends/accept', authenticateToken, async (req, res) => {
448
  try {
449
  const { requestId } = req.body;
450
+ const request = await FriendRequest.findByPk(requestId);
451
 
452
  if (!request) return res.status(404).json({ error: 'Request not found' });
453
 
454
  request.status = 'accepted';
455
  await request.save();
456
 
457
+ const fromUser = await User.findByPk(request.fromId);
458
+ const toUser = await User.findByPk(request.toId);
459
+
460
+ await fromUser.addFriend(toUser);
461
+ await toUser.addFriend(fromUser);
462
 
463
  res.json({ message: 'Friend request accepted' });
464
  } catch (error) {
 
466
  }
467
  });
468
 
 
469
  app.get('/api/friends', authenticateToken, async (req, res) => {
470
  try {
471
+ const user = await User.findByPk(req.user.userId, {
472
+ include: {
473
+ association: 'friends',
474
+ attributes: ['id', 'username', 'avatar', 'status', 'bio'],
475
+ through: { attributes: [] }
476
+ }
477
+ });
478
+ res.json(user.friends || []);
479
  } catch (error) {
480
  res.status(500).json({ error: error.message });
481
  }
 
483
 
484
  // ======================== DIRECT MESSAGES ROUTES ========================
485
 
 
486
  app.get('/api/dms/:friendId', authenticateToken, async (req, res) => {
487
  try {
488
  const { friendId } = req.params;
489
+ const messages = await DirectMessage.findAll({
490
+ where: {
491
+ [Op.or]: [
492
+ { fromId: req.user.userId, toId: friendId },
493
+ { fromId: friendId, toId: req.user.userId }
494
+ ]
495
+ },
496
+ order: [['createdAt', 'ASC']]
497
+ });
498
+
499
+ const userIds = [...new Set(messages.map(msg => msg.fromId))];
500
+ const users = await User.findAll({
501
+ where: { id: userIds },
502
+ attributes: ['id', 'avatar', 'username']
503
+ });
504
+ const userMap = new Map(users.map(user => [String(user.id), user]));
505
+
506
+ const enriched = messages.map(msg => {
507
+ const sender = userMap.get(String(msg.fromId));
508
+ return {
509
+ ...msg.toJSON(),
510
+ fromAvatar: sender?.avatar || null,
511
+ fromUsername: sender?.username || msg.fromUsername,
512
+ timestamp: msg.createdAt
513
+ };
514
+ });
515
+
516
+ res.json(enriched);
517
  } catch (error) {
518
  res.status(500).json({ error: error.message });
519
  }
520
  });
521
 
 
522
  app.post('/api/dms', authenticateToken, async (req, res) => {
523
  try {
524
  const { toUserId, content, mediaType, mediaUrl } = req.body;
525
+ const fromUser = await User.findByPk(req.user.userId);
526
+ const toUser = await User.findByPk(toUserId);
527
 
528
  if (!toUser) return res.status(404).json({ error: 'User not found' });
529
 
530
+ const friends = await fromUser.getFriends({ where: { id: toUserId } });
531
+ if (friends.length === 0) {
532
  return res.status(403).json({ error: 'Can only DM friends' });
533
  }
534
 
535
+ const dm = await DirectMessage.create({
536
  messageId: uuidv4(),
537
+ fromId: req.user.userId,
538
+ toId: toUserId,
539
  fromUsername: fromUser.username,
540
  toUsername: toUser.username,
541
  content,
 
543
  mediaUrl: mediaUrl || null
544
  });
545
 
 
546
  res.status(201).json(dm);
547
  } catch (error) {
548
  res.status(500).json({ error: error.message });
 
551
 
552
  // ======================== SHOP ROUTES ========================
553
 
 
554
  app.get('/api/shop', authenticateToken, async (req, res) => {
555
  try {
556
+ const items = await ShopItem.findAll();
557
  res.json(items);
558
  } catch (error) {
559
  res.status(500).json({ error: error.message });
560
  }
561
  });
562
 
 
563
  app.post('/api/shop/buy', authenticateToken, async (req, res) => {
564
  try {
565
  const { itemId } = req.body;
566
+ const item = await ShopItem.findByPk(itemId);
567
+ const user = await User.findByPk(req.user.userId);
568
 
569
  if (!item) return res.status(404).json({ error: 'Item not found' });
570
  if (user.coins < item.price) return res.status(400).json({ error: 'Not enough coins' });
 
572
  user.coins -= item.price;
573
  await user.save();
574
 
575
+ await Inventory.create({ userId: user.id, shopItemId: item.id });
 
576
 
577
  res.json({ message: 'Item purchased', coins: user.coins });
578
  } catch (error) {
 
580
  }
581
  });
582
 
 
583
  app.get('/api/inventory', authenticateToken, async (req, res) => {
584
  try {
585
+ const inventory = await Inventory.findAll({
586
+ where: { userId: req.user.userId },
587
+ include: [{ model: ShopItem }]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
588
  });
589
+ res.json(inventory);
 
 
 
 
 
 
 
 
 
 
 
590
  } catch (error) {
591
  res.status(500).json({ error: error.message });
592
  }
 
621
 
622
  const { server, protocol } = createServer(app);
623
  const io = require('socket.io')(server, {
624
+ maxHttpBufferSize: 40 * 1024 * 1024,
625
  cors: {
626
  origin: process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',') : '*',
627
  methods: ['GET', 'POST']
628
  }
629
  });
630
 
 
631
  const onlineUsers = new Map();
632
 
633
  io.on('connection', (socket) => {
634
  console.log('User connected:', socket.id);
635
 
 
636
  socket.on('join', async (data) => {
637
  try {
638
  const decoded = jwt.verify(data.token, process.env.JWT_SECRET || 'secret_key');
639
+ const user = await User.findByPk(decoded.userId);
640
 
641
  if (user) {
642
  socket.userId = decoded.userId;
 
653
  user.status = 'online';
654
  await user.save();
655
 
 
656
  io.emit('users update', Array.from(onlineUsers.values()));
657
  }
658
  } catch (error) {
 
660
  }
661
  });
662
 
 
663
  socket.on('join dm', (friendId) => {
664
  const roomId = [socket.userId, friendId].sort().join('_');
665
  socket.join(roomId);
666
  socket.currentDmRoom = roomId;
667
  });
668
 
 
669
  socket.on('leave dm', (friendId) => {
670
  const roomId = [socket.userId, friendId].sort().join('_');
671
  socket.leave(roomId);
672
  socket.currentDmRoom = null;
673
  });
674
 
 
675
  socket.on('send dm', async (data) => {
676
  try {
677
  const { toUserId, content, mediaType, mediaUrl } = data;
678
+ const fromUser = await User.findByPk(socket.userId);
679
+ const toUser = await User.findByPk(toUserId);
680
 
681
  if (!toUser) {
682
  socket.emit('error', 'User not found');
683
  return;
684
  }
685
 
686
+ const isSelfChat = String(toUserId) === String(socket.userId);
687
+ const friends = isSelfChat ? [] : await fromUser.getFriends({ where: { id: toUserId } });
688
+ if (!isSelfChat && friends.length === 0) {
689
  socket.emit('error', 'Can only DM friends');
690
  return;
691
  }
692
 
693
+ const dm = await DirectMessage.create({
694
  messageId: uuidv4(),
695
+ fromId: socket.userId,
696
+ toId: toUserId,
697
+ fromUsername: fromUser.username,
698
  toUsername: toUser.username,
699
  content,
700
  mediaType: mediaType || 'text',
701
  mediaUrl: mediaUrl || null
702
  });
703
 
 
 
 
704
  const roomId = [socket.userId, toUserId].sort().join('_');
705
  io.to(roomId).emit('dm message', {
706
  messageId: dm.messageId,
707
  from: socket.userId,
708
+ fromUsername: fromUser.username,
709
+ fromAvatar: fromUser.avatar,
710
  to: toUserId,
711
  toUsername: toUser.username,
712
  content: dm.content,
713
  mediaType: dm.mediaType,
714
  mediaUrl: dm.mediaUrl,
715
+ timestamp: dm.createdAt
716
  });
717
  } catch (error) {
718
  socket.emit('error', error.message);
719
  }
720
  });
721
 
 
722
  socket.on('dm typing', (friendId) => {
723
  const roomId = [socket.userId, friendId].sort().join('_');
724
  socket.broadcast.to(roomId).emit('dm user typing', {
 
735
  });
736
  });
737
 
 
738
  socket.on('disconnect', async () => {
739
  try {
740
  const userData = onlineUsers.get(socket.id);
741
  if (userData) {
742
+ const user = await User.findByPk(socket.userId);
743
  if (user) {
744
  user.status = 'offline';
745
  await user.save();
 
753
  });
754
  });
755
 
756
+ // ======================== INITIALIZATION ========================
757
 
758
  function getLocalNetworkAddress() {
759
  const interfaces = os.networkInterfaces();
 
770
  const PORT = process.env.PORT || 3000;
771
  const HOST = process.env.HOST || '0.0.0.0';
772
 
773
+ // Migrate ENUM types for media support
774
+ async function migrateMediaTypeEnum() {
775
+ try {
776
+ // Check if we need to update the ENUM type
777
+ const [results] = await sequelize.query(`
778
+ SELECT EXISTS (
779
+ SELECT 1 FROM pg_type t
780
+ JOIN pg_enum e ON t.oid = e.enumtypid
781
+ WHERE t.typname = 'enum_DirectMessages_mediaType'
782
+ AND e.enumlabel = 'voice'
783
+ ) as has_voice;
784
+ `);
785
+
786
+ if (!results[0].has_voice) {
787
+ console.log('πŸ“¦ Migrating mediaType ENUM to include sticker, gif, voice...');
788
+
789
+ // PostgreSQL requires special handling for ENUM changes
790
+ await sequelize.query(`
791
+ ALTER TYPE "enum_DirectMessages_mediaType" ADD VALUE IF NOT EXISTS 'sticker';
792
+ ALTER TYPE "enum_DirectMessages_mediaType" ADD VALUE IF NOT EXISTS 'gif';
793
+ ALTER TYPE "enum_DirectMessages_mediaType" ADD VALUE IF NOT EXISTS 'voice';
794
+ `);
795
+
796
+ await sequelize.query(`
797
+ ALTER TYPE "enum_Messages_mediaType" ADD VALUE IF NOT EXISTS 'sticker';
798
+ ALTER TYPE "enum_Messages_mediaType" ADD VALUE IF NOT EXISTS 'gif';
799
+ ALTER TYPE "enum_Messages_mediaType" ADD VALUE IF NOT EXISTS 'voice';
800
+ `);
801
+
802
+ console.log('βœ… ENUM migration complete!');
803
+ } else {
804
+ console.log('βœ… mediaType ENUM already up to date');
805
+ }
806
+ } catch (error) {
807
+ console.log('ℹ️ ENUM migration skipped (likely first run or different DB)');
808
+ }
809
+ }
810
+
811
+ sequelize.sync({ alter: true }).then(async () => {
812
+ await migrateMediaTypeEnum();
813
+ server.listen(PORT, HOST, () => {
814
+ const networkAddress = getLocalNetworkAddress();
815
+ console.log(`
816
  ╔═══════════════════════════════════════╗
817
  β•‘ πŸš€ Discord-Like Server Running β•‘
818
  β•‘ β•‘
 
827
  β•‘ βœ“ Media Sharing β•‘
828
  β•‘ βœ“ Shop System β•‘
829
  β•‘ βœ“ Real-time Messaging β•‘
830
+ β•‘ βœ“ PostgreSQL Database β•‘
831
  β•‘ β•‘
832
  β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
833
+ `);
834
+ });
835
+ }).catch(err => {
836
+ console.error('Database sync error:', err);
837
+ process.exit(1);
838
+ });