Add embedded web chat client with C2S HTTP API
- New DB schema: users, channel_members, messages tables (migration 003) - Full C2S HTTP API: register, channels, messages, DMs, polling - Preact SPA embedded via embed.FS, served at GET / - IRC-style UI: tab bar, channel messages, user list, DM tabs, /commands - Dark theme, responsive, esbuild-bundled (~19KB) - Polling-based message delivery (1.5s interval) - Commands: /join, /part, /msg, /nick
This commit is contained in:
1 parent
03cbc3cd1a
commit
ac933d07d2
16 files changed
+2271
No files matched your search
Vendored
+1
@@ -0,0 +1 @@
|
||||
(()=>{var te,b,Ce,Ge,O,ge,Se,xe,Te,ae,ie,se,Qe,q={},Ee=[],Xe=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,ne=Array.isArray;function U(e,t){for(var n in t)e[n]=t[n];return e}function le(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function m(e,t,n){var _,i,o,s={};for(o in t)o=="key"?_=t[o]:o=="ref"?i=t[o]:s[o]=t[o];if(arguments.length>2&&(s.children=arguments.length>3?te.call(arguments,2):n),typeof e=="function"&&e.defaultProps!=null)for(o in e.defaultProps)s[o]===void 0&&(s[o]=e.defaultProps[o]);return Y(e,s,_,i,null)}function Y(e,t,n,_,i){var o={type:e,props:t,key:n,ref:_,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:i??++Ce,__i:-1,__u:0};return i==null&&b.vnode!=null&&b.vnode(o),o}function _e(e){return e.children}function Z(e,t){this.props=e,this.context=t}function j(e,t){if(t==null)return e.__?j(e.__,e.__i+1):null;for(var n;t<e.__k.length;t++)if((n=e.__k[t])!=null&&n.__e!=null)return n.__e;return typeof e.type=="function"?j(e):null}function Pe(e){var t,n;if((e=e.__)!=null&&e.__c!=null){for(e.__e=e.__c.base=null,t=0;t<e.__k.length;t++)if((n=e.__k[t])!=null&&n.__e!=null){e.__e=e.__c.base=n.__e;break}return Pe(e)}}function be(e){(!e.__d&&(e.__d=!0)&&O.push(e)&&!ee.__r++||ge!=b.debounceRendering)&&((ge=b.debounceRendering)||Se)(ee)}function ee(){for(var e,t,n,_,i,o,s,l=1;O.length;)O.length>l&&O.sort(xe),e=O.shift(),l=O.length,e.__d&&(n=void 0,_=void 0,i=(_=(t=e).__v).__e,o=[],s=[],t.__P&&((n=U({},_)).__v=_.__v+1,b.vnode&&b.vnode(n),ue(t.__P,n,_,t.__n,t.__P.namespaceURI,32&_.__u?[i]:null,o,i??j(_),!!(32&_.__u),s),n.__v=_.__v,n.__.__k[n.__i]=n,He(o,n,s),_.__e=_.__=null,n.__e!=i&&Pe(n)));ee.__r=0}function Ie(e,t,n,_,i,o,s,l,d,c,h){var r,u,f,k,P,w,g,y=_&&_.__k||Ee,D=t.length;for(d=Ye(n,t,y,d,D),r=0;r<D;r++)(f=n.__k[r])!=null&&(u=f.__i==-1?q:y[f.__i]||q,f.__i=r,w=ue(e,f,u,i,o,s,l,d,c,h),k=f.__e,f.ref&&u.ref!=f.ref&&(u.ref&&fe(u.ref,null,f),h.push(f.ref,f.__c||k,f)),P==null&&k!=null&&(P=k),(g=!!(4&f.__u))||u.__k===f.__k?d=Me(f,d,e,g):typeof f.type=="function"&&w!==void 0?d=w:k&&(d=k.nextSibling),f.__u&=-7);return n.__e=P,d}function Ye(e,t,n,_,i){var o,s,l,d,c,h=n.length,r=h,u=0;for(e.__k=new Array(i),o=0;o<i;o++)(s=t[o])!=null&&typeof s!="boolean"&&typeof s!="function"?(typeof s=="string"||typeof s=="number"||typeof s=="bigint"||s.constructor==String?s=e.__k[o]=Y(null,s,null,null,null):ne(s)?s=e.__k[o]=Y(_e,{children:s},null,null,null):s.constructor===void 0&&s.__b>0?s=e.__k[o]=Y(s.type,s.props,s.key,s.ref?s.ref:null,s.__v):e.__k[o]=s,d=o+u,s.__=e,s.__b=e.__b+1,l=null,(c=s.__i=Ze(s,n,d,r))!=-1&&(r--,(l=n[c])&&(l.__u|=2)),l==null||l.__v==null?(c==-1&&(i>h?u--:i<h&&u++),typeof s.type!="function"&&(s.__u|=4)):c!=d&&(c==d-1?u--:c==d+1?u++:(c>d?u--:u++,s.__u|=4))):e.__k[o]=null;if(r)for(o=0;o<h;o++)(l=n[o])!=null&&(2&l.__u)==0&&(l.__e==_&&(_=j(l)),Ne(l,l));return _}function Me(e,t,n,_){var i,o;if(typeof e.type=="function"){for(i=e.__k,o=0;i&&o<i.length;o++)i[o]&&(i[o].__=e,t=Me(i[o],t,n,_));return t}e.__e!=t&&(_&&(t&&e.type&&!t.parentNode&&(t=j(e)),n.insertBefore(e.__e,t||null)),t=e.__e);do t=t&&t.nextSibling;while(t!=null&&t.nodeType==8);return t}function Ze(e,t,n,_){var i,o,s,l=e.key,d=e.type,c=t[n],h=c!=null&&(2&c.__u)==0;if(c===null&&l==null||h&&l==c.key&&d==c.type)return n;if(_>(h?1:0)){for(i=n-1,o=n+1;i>=0||o<t.length;)if((c=t[s=i>=0?i--:o++])!=null&&(2&c.__u)==0&&l==c.key&&d==c.type)return s}return-1}function ke(e,t,n){t[0]=="-"?e.setProperty(t,n??""):e[t]=n==null?"":typeof n!="number"||Xe.test(t)?n:n+"px"}function X(e,t,n,_,i){var o,s;e:if(t=="style")if(typeof n=="string")e.style.cssText=n;else{if(typeof _=="string"&&(e.style.cssText=_=""),_)for(t in _)n&&t in n||ke(e.style,t,"");if(n)for(t in n)_&&n[t]==_[t]||ke(e.style,t,n[t])}else if(t[0]=="o"&&t[1]=="n")o=t!=(t=t.replace(Te,"$1")),s=t.toLowerCase(),t=s in e||t=="onFocusOut"||t=="onFocusIn"?s.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+o]=n,n?_?n.u=_.u:(n.u=ae,e.addEventListener(t,o?se:ie,o)):e.removeEventListener(t,o?se:ie,o);else{if(i=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=n??"";break e}catch{}typeof n=="function"||(n==null||n===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&n==1?"":n))}}function we(e){return function(t){if(this.l){var n=this.l[t.type+e];if(t.t==null)t.t=ae++;else if(t.t<n.u)return;return n(b.event?b.event(t):t)}}}function ue(e,t,n,_,i,o,s,l,d,c){var h,r,u,f,k,P,w,g,y,D,N,$,E,Q,L,F,R,M=t.type;if(t.constructor!==void 0)return null;128&n.__u&&(d=!!(32&n.__u),o=[l=t.__e=n.__e]),(h=b.__b)&&h(t);e:if(typeof M=="function")try{if(g=t.props,y="prototype"in M&&M.prototype.render,D=(h=M.contextType)&&_[h.__c],N=h?D?D.props.value:h.__:_,n.__c?w=(r=t.__c=n.__c).__=r.__E:(y?t.__c=r=new M(g,N):(t.__c=r=new Z(g,N),r.constructor=M,r.render=tt),D&&D.sub(r),r.state||(r.state={}),r.__n=_,u=r.__Line truncated
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Chat</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
+274
@@ -0,0 +1,274 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--bg: #1a1a2e;
|
||||
--bg-secondary: #16213e;
|
||||
--bg-input: #0f3460;
|
||||
--text: #e0e0e0;
|
||||
--text-muted: #888;
|
||||
--accent: #e94560;
|
||||
--accent2: #0f3460;
|
||||
--border: #2a2a4a;
|
||||
--nick: #53a8b6;
|
||||
--timestamp: #666;
|
||||
--tab-active: #e94560;
|
||||
--tab-bg: #16213e;
|
||||
--tab-hover: #1a1a3e;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 14px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Login screen */
|
||||
.login-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.login-screen h1 {
|
||||
color: var(--accent);
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.login-screen input {
|
||||
padding: 10px 16px;
|
||||
font-size: 16px;
|
||||
font-family: inherit;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: 4px;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.login-screen button {
|
||||
padding: 10px 24px;
|
||||
font-size: 16px;
|
||||
font-family: inherit;
|
||||
background: var(--accent);
|
||||
border: none;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-screen .error {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.login-screen .motd {
|
||||
color: var(--text-muted);
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Main layout */
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Tab bar */
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
overflow-x: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
white-space: nowrap;
|
||||
color: var(--text-muted);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background: var(--tab-hover);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--text);
|
||||
border-bottom-color: var(--tab-active);
|
||||
}
|
||||
|
||||
.tab .close-btn {
|
||||
margin-left: 8px;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tab .close-btn:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Content area */
|
||||
.content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
.messages-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 2px 0;
|
||||
line-height: 1.4;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message .timestamp {
|
||||
color: var(--timestamp);
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.message .nick {
|
||||
color: var(--nick);
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.message .nick::before { content: '<'; }
|
||||
.message .nick::after { content: '>'; }
|
||||
|
||||
.message.system {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.message.system .nick {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.message.system .nick::before,
|
||||
.message.system .nick::after { content: ''; }
|
||||
|
||||
/* Input */
|
||||
.input-bar {
|
||||
display: flex;
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--bg-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-bar input {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
background: var(--bg-input);
|
||||
border: none;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-bar button {
|
||||
padding: 10px 16px;
|
||||
font-family: inherit;
|
||||
background: var(--accent);
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* User list */
|
||||
.user-list {
|
||||
width: 160px;
|
||||
background: var(--bg-secondary);
|
||||
border-left: 1px solid var(--border);
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-list h3 {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.user-list .user {
|
||||
padding: 3px 4px;
|
||||
color: var(--nick);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-list .user:hover {
|
||||
background: var(--tab-hover);
|
||||
}
|
||||
|
||||
/* Server tab */
|
||||
.server-messages {
|
||||
color: var(--text-muted);
|
||||
padding: 12px;
|
||||
white-space: pre-wrap;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Channel join dialog */
|
||||
.join-dialog {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.join-dialog input {
|
||||
padding: 6px 10px;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: 3px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.join-dialog button {
|
||||
padding: 6px 14px;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
background: var(--accent2);
|
||||
border: none;
|
||||
color: var(--text);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 600px) {
|
||||
.user-list { display: none; }
|
||||
.tab { padding: 6px 10px; font-size: 13px; }
|
||||
}
|
||||
Reference in new issue
Block a user