From e5c5927c543c3fe8c6c0b2fb446c1f5a3430014a Mon Sep 17 00:00:00 2001 From: fegger Date: Wed, 29 Apr 2026 23:35:40 +0200 Subject: [PATCH] feat: implement dark mode toggle and enhance chat UI with timestamps --- .idea/workspace.xml | 233 ++++++++++++-------------------------------- public/index.html | 9 +- public/styles.css | 165 +++++++++++++++++++++++++------ src/APIService.ts | 5 +- src/ChatUI.ts | 29 +++++- src/main.ts | 47 +++++++-- src/types.ts | 2 +- 7 files changed, 276 insertions(+), 214 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 2a2d594..077c098 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -5,154 +5,13 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - @@ -222,7 +82,7 @@ { "selectedUrlAndAccountId": { "url": "https://github.com/floegger/messenger.git", - "accountId": "c3dae145-2cec-468f-9f70-5b09324effc3" + "accountId": "51fc9917-8632-47ef-b79f-cd5552c43996" } } @@ -243,30 +103,60 @@ - { - "keyToString": { - "JavaScript Debug.index.html.executor": "Run", - "ModuleVcsDetector.initialDetectionPerformed": "true", - "Node.js.main.ts.executor": "Run", - "RunOnceActivity.MCP Project settings loaded": "true", - "RunOnceActivity.ShowReadmeOnStart": "true", - "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", - "RunOnceActivity.git.unshallow": "true", - "RunOnceActivity.typescript.service.memoryLimit.init": "true", - "codeWithMe.voiceChat.enabledByDefault": "false", - "git-widget-placeholder": "dev", - "javascript.preferred.runtime.type.id": "node", - "last_opened_file_path": "/home/fegger/Code/WEBP", - "node.js.detected.package.eslint": "true", - "node.js.detected.package.tslint": "true", - "node.js.selected.package.eslint": "(autodetect)", - "node.js.selected.package.tslint": "(autodetect)", - "nodejs_package_manager_path": "npm", - "settings.editor.selected.configurable": "http.proxy", - "ts.external.directory.path": "/home/fegger/Code/WEBP/messenger/node_modules/typescript/lib", - "vue.rearranger.settings.migration": "true" + +}]]> + + + + + + - \ No newline at end of file + diff --git a/public/styles.css b/public/styles.css index 5671795..ed103f2 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1,3 +1,49 @@ +/* Catppuccin Latte (light) */ +:root { + --bg: #e6e9ef; /* Mantle */ + --surface: #eff1f5; /* Base */ + --sidebar-bg: #e6e9ef; /* Mantle */ + --border: #ccd0da; /* Surface0 */ + --text: #4c4f69; /* Text */ + --text-muted: #8c8fa1; /* Overlay1 */ + --input-bg: #eff1f5; /* Base */ + --input-border: #bcc0cc; /* Surface1 */ + --user-hover: #ccd0da; /* Surface0 */ + --reload-bg: #dce0e8; /* Crust */ + --reload-border: #bcc0cc; /* Surface1 */ + --reload-hover: #ccd0da; /* Surface0 */ + --msg-received-bg: #ccd0da; /* Surface0 */ + --msg-received-text:#4c4f69; /* Text */ + --error-bg: #f2cdcd; /* Flamingo tint */ + --error-text: #d20f39; /* Red */ + --accent: #1e66f5; /* Blue */ + --accent-hover: #04a5e5; /* Sky */ + --accent-text: #eff1f5; /* Base */ +} + +/* Catppuccin Mocha (dark) */ +[data-theme="dark"] { + --bg: #181825; /* Mantle */ + --surface: #1e1e2e; /* Base */ + --sidebar-bg: #181825; /* Mantle */ + --border: #313244; /* Surface0 */ + --text: #cdd6f4; /* Text */ + --text-muted: #6c7086; /* Overlay0 */ + --input-bg: #1e1e2e; /* Base */ + --input-border: #45475a; /* Surface1 */ + --user-hover: #313244; /* Surface0 */ + --reload-bg: #313244; /* Surface0 */ + --reload-border: #45475a; /* Surface1 */ + --reload-hover: #45475a; /* Surface1 */ + --msg-received-bg: #313244; /* Surface0 */ + --msg-received-text:#cdd6f4; /* Text */ + --error-bg: #3b1b27; /* custom deep red */ + --error-text: #f38ba8; /* Red */ + --accent: #89b4fa; /* Blue */ + --accent-hover: #74c7ec; /* Sapphire */ + --accent-text: #1e1e2e; /* Base */ +} + *, *::before, *::after { box-sizing: border-box; margin: 0; @@ -6,12 +52,14 @@ body { font-family: 'Segoe UI', Arial, sans-serif; - background: #f0f2f5; + background: var(--bg); + color: var(--text); min-height: 100vh; display: flex; justify-content: center; align-items: flex-start; padding: 2rem; + transition: background 0.2s, color 0.2s; } .sr-only { @@ -26,23 +74,43 @@ body { border: 0; } +#theme-toggle { + position: fixed; + top: 1rem; + right: 1rem; + background: var(--surface); + color: var(--text); + border: 1px solid var(--border); + border-radius: 20px; + padding: 0.35rem 0.75rem; + font-size: 0.85rem; + cursor: pointer; + transition: background 0.2s, color 0.2s, border-color 0.2s; + z-index: 100; +} + +#theme-toggle:hover { + background: var(--user-hover); +} + .auth-container { width: 100%; max-width: 440px; } .auth-section { - background: white; + background: var(--surface); border-radius: 8px; padding: 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 4px rgba(0,0,0,0.1); + transition: background 0.2s; } .auth-section h2 { margin-bottom: 1rem; font-size: 1.2rem; - color: #333; + color: var(--text); } form { @@ -53,22 +121,23 @@ form { input, select { padding: 0.6rem 0.8rem; - border: 1px solid #ccc; + border: 1px solid var(--input-border); border-radius: 6px; font-size: 0.95rem; - transition: border-color 0.2s; - background: white; + transition: border-color 0.2s, background 0.2s, color 0.2s; + background: var(--input-bg); + color: var(--text); } input:focus, select:focus { outline: none; - border-color: #007bff; + border-color: var(--accent); } button[type="submit"] { padding: 0.6rem; - background: #007bff; - color: white; + background: var(--accent); + color: var(--accent-text); border: none; border-radius: 6px; font-size: 0.95rem; @@ -77,7 +146,7 @@ button[type="submit"] { } button[type="submit"]:hover { - background: #0056b3; + background: var(--accent-hover); } .chat-app { @@ -85,25 +154,27 @@ button[type="submit"]:hover { width: 100%; max-width: 900px; height: 80vh; - background: white; + background: var(--surface); border-radius: 12px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.12); + transition: background 0.2s; } #user-list { width: 220px; - border-right: 1px solid #e9ecef; + border-right: 1px solid var(--border); overflow-y: auto; padding: 1rem; - background: #f8f9fa; + background: var(--sidebar-bg); + transition: background 0.2s, border-color 0.2s; } #user-list h3 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; - color: #888; + color: var(--text-muted); margin-bottom: 0.8rem; } @@ -112,23 +183,52 @@ button[type="submit"]:hover { border-radius: 6px; cursor: pointer; font-size: 0.95rem; - color: #333; + color: var(--text); transition: background 0.15s; } .user-item:hover { - background: #e9ecef; + background: var(--user-hover); } .user-item.active { - background: #007bff; - color: white; + background: var(--accent); + color: var(--accent-text); } .chat-main { flex: 1; display: flex; flex-direction: column; + min-width: 0; +} + +.chat-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.6rem 1rem; + border-bottom: 1px solid var(--border); + background: var(--surface); + font-weight: 600; + font-size: 0.95rem; + color: var(--text); + transition: background 0.2s, border-color 0.2s; +} + +.chat-header button { + padding: 0.3rem 0.8rem; + font-size: 0.8rem; + background: var(--reload-bg); + color: var(--text); + border: 1px solid var(--reload-border); + border-radius: 6px; + cursor: pointer; + transition: background 0.15s; +} + +.chat-header button:hover { + background: var(--reload-hover); } #messages { @@ -142,31 +242,40 @@ button[type="submit"]:hover { .message { max-width: 70%; + width: fit-content; padding: 8px 12px; border-radius: 12px; font-size: 0.95rem; line-height: 1.4; word-break: break-word; + display: flex; + flex-direction: column; + gap: 2px; +} + +.message-time { + font-size: 0.7rem; + opacity: 0.65; } .sent { - background: #007bff; - color: white; - margin-left: auto; + align-self: flex-end; + background: var(--accent); + color: var(--accent-text); border-bottom-right-radius: 4px; } .received { - background: #e9ecef; - color: #333; - margin-right: auto; + align-self: flex-start; + background: var(--msg-received-bg); + color: var(--msg-received-text); border-bottom-left-radius: 4px; } .message-form { display: flex; padding: 0.8rem; - border-top: 1px solid #e9ecef; + border-top: 1px solid var(--border); gap: 0.5rem; } @@ -181,10 +290,10 @@ button[type="submit"]:hover { } .error-message { - background: #fce4e4; - color: #c0392b; + background: var(--error-bg); + color: var(--error-text); padding: 0.6rem 1rem; border-radius: 6px; margin-top: 0.5rem; font-size: 0.9rem; -} \ No newline at end of file +} diff --git a/src/APIService.ts b/src/APIService.ts index 0c31bdc..514e30c 100644 --- a/src/APIService.ts +++ b/src/APIService.ts @@ -3,6 +3,7 @@ import { LoginResponse, RegisterResponse, SendMessageResponse, + Message, } from "./types.js"; export class APIService { @@ -77,8 +78,8 @@ export class APIService { async getConversation( token: string, user1Id: string, user2Id:string ): Promise { const url = new URL( `${APIService.baseUrl}/get_conversation.php` ); url.searchParams.set( "token", token ); - url.searchParams.set( "token", user1Id ); - url.searchParams.set( "conversation_id", user2Id ); + url.searchParams.set( "user1_id", user1Id ); + url.searchParams.set( "user2_id", user2Id ); const response = await fetch( url, {method: "GET"} );; if ( !response.ok ) { diff --git a/src/ChatUI.ts b/src/ChatUI.ts index 6733588..e9069fc 100644 --- a/src/ChatUI.ts +++ b/src/ChatUI.ts @@ -37,18 +37,31 @@ export class ChatUI { const bubble = document.createElement( "div" ); bubble.classList.add( "message" ); - if ( message.sender_id === currentUserId ) { + if ( String(message.sender_id) === String(currentUserId) ) { bubble.classList.add( "sent" ); } else { bubble.classList.add( "received" ); } - bubble.textContent = message.message; + const text = document.createElement( "span" ); + text.textContent = message.message; + + const time = document.createElement( "span" ); + time.classList.add( "message-time" ); + time.textContent = formatTimestamp( message.timestamp ); + + bubble.appendChild( text ); + bubble.appendChild( time ); container.appendChild( bubble ); container.scrollTop = container.scrollHeight; } + setChatHeader ( name: string ): void { + const el = document.getElementById( "chat-username" ); + if ( el ) el.textContent = name; + } + renderConversation( messages: Message[], currentUserId: string ): void { this.clearMessages(); messages.forEach( ( message ) => { @@ -111,4 +124,16 @@ export class ChatUI { item.classList.toggle( "active", item.dataset.userId === userId ); }); } +} + +function formatTimestamp ( timestamp: number | string ): string { + // Try parsing as-is (works for date strings and ms numbers) + let date = new Date( timestamp ); + if ( !isNaN( date.getTime() ) ) return date.toLocaleString(); + + // Fall back: treat as Unix seconds + date = new Date( Number( timestamp ) * 1000 ); + if ( !isNaN( date.getTime() ) ) return date.toLocaleString(); + + return ""; } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 533a4fc..a031816 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,9 +8,23 @@ const api = new APIService(); const state = new StateManager(); const ui = new ChatUI(); +let pollingInterval: number | null = null; + // initialize app when DOM is ready document.addEventListener("DOMContentLoaded", async () => { - console.log("[Messenger] App initialised. API →", "http://webp-ilv-backend.cs.technikum-wien.at/messenger"); + // Dark mode + const themeToggle = document.getElementById( "theme-toggle" ) as HTMLButtonElement; + const applyTheme = ( dark: boolean ) => { + document.documentElement.dataset.theme = dark ? "dark" : ""; + themeToggle.textContent = dark ? "Light" : "Dark"; + }; + let darkMode = localStorage.getItem( "theme" ) === "dark"; + applyTheme( darkMode ); + themeToggle.addEventListener( "click", () => { + darkMode = !darkMode; + localStorage.setItem( "theme", darkMode ? "dark" : "light" ); + applyTheme( darkMode ); + }); // restore from session if available if (state.isLoggedIn()) { ui.showView("chat-view"); @@ -82,6 +96,12 @@ document.addEventListener("DOMContentLoaded", async () => { } }); + const reloadBtn = document.getElementById( "reload-btn" ); + reloadBtn?.addEventListener( "click", () => { + const receiverId = state.getSelectedReceiverId(); + if ( receiverId ) loadConversation( receiverId ); + }); + const messageForm = document.getElementById("message-form") as HTMLFormElement; messageForm?.addEventListener("submit", async (e) => { e.preventDefault(); @@ -104,7 +124,7 @@ document.addEventListener("DOMContentLoaded", async () => { sender_id: state.getUserId(), receiver_id: receiverId, message: text, - timestamp: Date.now(), + timestamp: Math.floor( Date.now() / 1000 ), }; ui.renderMessage(message, state.getUserId()); @@ -137,18 +157,29 @@ async function loadUsers(): Promise { } } -const onUserClick = async ( user: User ) => { - ui.setActiveUser( user.id ); - ui.showView( "chat-view" ); - +async function loadConversation( userId: string ): Promise { try { const conversation = await api.getConversation( state.getToken(), state.getUserId(), - user.id + userId ); ui.renderConversation( conversation, state.getUserId() ); } catch (error) { - ui.showError("Could not load conversation. Please try again."); + ui.showError( "Could not load conversation. Please try again." ); } +} + +const onUserClick = async ( user: User ) => { + state.setSelectedReceiverId( user.id ); + ui.setActiveUser( user.id ); + ui.setChatHeader( user.name ); + ui.showView( "chat-view" ); + + if ( pollingInterval !== null ) { + clearInterval( pollingInterval ); + } + + await loadConversation( user.id ); + pollingInterval = window.setInterval( () => loadConversation( user.id ), 15000 ); }; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 4ecb73d..d520043 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,7 +20,7 @@ export interface Message { sender_id: string; receiver_id: string; message: string; - timestamp: number; + timestamp: number | string; } export interface SendMessageResponse {