;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (in-package #:live-chat-ui) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defvar *default-prefix* "/lisp-chat") (defun set-subpath-prefix (prefix) (setf *default-prefix* prefix)) (defun subpath-prefix (path) (format nil "~a~a" *default-prefix* path)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Render chat messages (defun render-chat-messages () "Render the list of chat messages as HTML." (let ((messages (fetch-messages))) (if messages (cl-who:with-html-output-to-string (*standard-output* nil :indent t) (loop for msg in messages do (cl-who:htm (:div :class "box" :style "overflow: auto;" (cl-who:str msg))))) ""))) ;; Render chat UI (defun render-chat-ui () "Render the main chat page with HTMX integration." (cl-who:with-html-output-to-string (*standard-output* nil :prologue t :indent t) (:html :lang "en" :class "has-navbar-fixed-bottom" (:head (:meta :charset "utf-8") (:meta :name "viewport" :content "width=device-width, initial-scale=1") (:title "Live Chat") (:link :rel "stylesheet" :href "https://cdnjs.cloudflare.com/ajax/libs/bulma/1.0.2/css/bulma.min.css" :integrity "sha512-RpeJZX3aH5oZN3U3JhE7Sd+HG8XQsqmP3clIbu4G28p668yNsRNj3zMASKe1ATjl/W80wuEtCx2dFA8xaebG5w==" :crossorigin "anonymous" :referrerpolicy "no-referrer") (:script :src "https://cdnjs.cloudflare.com/ajax/libs/htmx/2.0.3/htmx.min.js" :integrity "sha512-dQu3OKLMpRu85mW24LA1CUZG67BgLPR8Px3mcxmpdyijgl1UpCM1RtJoQP6h8UkufSnaHVRTUx98EQT9fcKohw==" :crossorigin "anonymous" :referrerpolicy "no-referrer") ;; (:script :src "https://cdnjs.cloudflare.com/ajax/libs/htmx/2.0.3/ext/ws.min.js" ;; :integrity "sha512-1OIiXEswZd/etj60BUwFmyoi0OhrWdoYlzayJpSBivoMV0pQPIQr+vtAn3W3htsbWtLRU8DrBl0epdK4DQbj/w==" ;; :crossorigin "anonymous" ;; :referrerpolicy "no-referrer") ) (:body (:section :class "section" (:div :class "container" ;; :hx-ext "ws" ;; :ws-connect (subpath-prefix "/ws-chat-messages") (:div :class "container" :id "chat-messages" ;; :hx-get "/chat-messages" (cl-who:str (render-chat-messages))))) (:nav :role "navigation" :class "navbar is-fixed-bottom is-spaced has-shadow" (:div :class "navbar-menu is-active" :id "navbarBasicExample" (:div :class "navbar-start") (:div :class "container" (:div :class "field has-addons" (:div :class "control is-expanded" (:input :class "input is-expanded" :id "chat-input" :autocomplete "off" :placeholder "Enter your message..." :type "text" :name "message")) (:div :class "control" (:button :class "button is-link is-light" :type "submit" "Send")))) (:div :class "navbar-end"))) (:script (cl-who:str (js-snippet (subpath-prefix "/ws-chat-messages")))))))) (defun js-snippet (path) (format nil "function receivedMessage(msg) { document.querySelector('#chat-messages').insertAdjacentHTML('afterbegin', msg.data); } const ws = new WebSocket('wss://' + window.location.host + '~a'); ws.addEventListener('message', receivedMessage); const inputField = document.getElementById('chat-input'); inputField.addEventListener('keyup', (evt) => { if (evt.key === 'Enter') { ws.send(evt.target.value); evt.target.value = ''; } }); // https://bulma.io/documentation/components/navbar/#navbar-menu document.addEventListener('DOMContentLoaded', () => { const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0); // Add a click event on each of them $navbarBurgers.forEach( el => { el.addEventListener('click', () => { const target = el.dataset.target; const $target = document.getElementById(target); el.classList.toggle('is-active'); $target.classList.toggle('is-active'); }); }); });" path))