diff --git a/examples/chat/chat/component.go b/examples/chat/chat/component.go index ab445eb..38ff752 100644 --- a/examples/chat/chat/component.go +++ b/examples/chat/chat/component.go @@ -10,13 +10,16 @@ import ( func MessageRow(message *Message) *h.Element { return h.Div( h.Attribute("hx-swap-oob", "beforeend"), - h.Class("flex flex-col gap-2 w-full"), + h.Class("flex flex-col gap-4 w-full"), h.Id("messages"), h.Div( - h.Class("flex gap-2 items-center"), - h.Pf(message.UserName), - h.Pf(message.CreatedAt.In(time.Local).Format("01/02 03:04 PM")), - h.Pf(message.Message), + h.Class("flex flex-col gap-1"), + h.Div( + h.Class("flex gap-2 items-center"), + h.Pf(message.UserName, h.Class("font-bold")), + h.Pf(message.CreatedAt.In(time.Local).Format("01/02 03:04 PM")), + ), + h.P(h.Text(message.Message)), ), ) } @@ -26,7 +29,7 @@ func ConnectedUsers(username string) *h.Element { h.Attribute("hx-swap", "none"), h.Attribute("hx-swap-oob", "beforeend"), h.Id("connected-users"), - h.Class("flex flex-col gap-2"), + h.Class("flex flex-col"), // This would be populated dynamically with connected users ConnectedUser(username, false), ) @@ -39,7 +42,7 @@ func ConnectedUser(username string, remove bool) *h.Element { } return h.Li( h.Id(id), - h.Class("text-slate-700"), + h.Class("truncate text-slate-700"), h.Text(username), ) } diff --git a/examples/chat/pages/chat.$id.go b/examples/chat/pages/chat.$id.go index 0d2ebca..44dc48f 100644 --- a/examples/chat/pages/chat.$id.go +++ b/examples/chat/pages/chat.$id.go @@ -6,6 +6,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/maddalax/htmgo/framework/h" "github.com/maddalax/htmgo/framework/js" + "time" ) func ChatRoom(ctx *h.RequestContext) *h.Page { @@ -29,8 +30,10 @@ func ChatRoom(ctx *h.RequestContext) *h.Page { const reason = e.detail.event.reason if(['invalid room', 'no session'].includes(reason)) { window.location.href = '/'; + } else if(e.detail.event.code === 1011) { + window.location.reload() } else { - console.error('Connection closed:', e.detail.event) + console.error('Connection closed:', e.detail.event) } `), ), @@ -40,26 +43,30 @@ func ChatRoom(ctx *h.RequestContext) *h.Page { // Sidebar for connected users UserSidebar(), - // Chat Area h.Div( - h.Class("flex flex-col flex-grow gap-4 bg-white rounded p-4"), + h.Class("flex flex-col flex-grow bg-white rounded p-4"), + + // Room name at the top, fixed + CachedRoomHeader(ctx), + + // Padding to push chat content below the fixed room name + h.Div(h.Class("pt-[50px]")), h.HxAfterWsMessage( js.EvalJsOnSibling("#messages", - // language=JavaScript `element.scrollTop = element.scrollHeight;`), ), // Chat Messages h.Div( h.Id("messages"), - h.Class("flex flex-col gap-2 overflow-auto grow w-full"), + h.Class("flex flex-col gap-4 overflow-auto grow w-full"), ), // Chat Input at the bottom h.Div( h.Class("mt-auto"), - Form(ctx), + Form(), ), ), ), @@ -67,11 +74,35 @@ func ChatRoom(ctx *h.RequestContext) *h.Page { ) } +var CachedRoomHeader = h.CachedT(time.Hour, func(ctx *h.RequestContext) *h.Element { + return roomNameHeader(ctx) +}) + +func roomNameHeader(ctx *h.RequestContext) *h.Element { + roomId := chi.URLParam(ctx.Request, "id") + service := chat.NewService(ctx.ServiceLocator()) + room, err := service.GetRoom(roomId) + if err != nil { + return h.Div() + } + return h.Div( + h.Class("bg-neutral-700 text-white p-3 shadow-sm w-full fixed top-0 left-0 flex justify-center z-10"), + h.H2F(room.Name, h.Class("text-lg font-bold")), + ) +} + func UserSidebar() *h.Element { return h.Div( - h.Class("w-48 bg-slate-200 p-4 flex flex-col gap-3 rounded-l-lg"), - h.H2F("Connected Users", h.Class("text-lg font-bold")), - chat.ConnectedUsers(""), + h.Class("pt-[67px] min-w-48 w-48 bg-neutral-200 p-4 flex flex-col justify-between gap-3 rounded-l-lg"), + h.Div( + h.H3F("Connected Users", h.Class("text-lg font-bold")), + chat.ConnectedUsers(""), + ), + h.A( + h.Class("cursor-pointer"), + h.Href("/"), + h.Text("Leave Room"), + ), ) } @@ -79,8 +110,9 @@ func MessageInput() *h.Element { return h.Input("text", h.Id("message-input"), h.Required(), - h.Class("p-4 rounded-md border border-slate-200 w-full"), + h.Class("p-4 rounded-md border border-slate-200 w-full focus:outline-none focus:ring focus:ring-slate-200"), h.Name("message"), + h.MaxLength(1000), h.Placeholder("Type a message..."), h.HxAfterWsSend( js.SetValue(""), @@ -88,7 +120,7 @@ func MessageInput() *h.Element { ) } -func Form(ctx *h.RequestContext) *h.Element { +func Form() *h.Element { return h.Div( h.Class("flex gap-4 items-center"), h.Form( @@ -98,11 +130,3 @@ func Form(ctx *h.RequestContext) *h.Element { ), ) } - -func Spinner(children ...h.Ren) *h.Element { - return h.Div( - h.Children(children...), - h.Class("spinner spinner-border animate-spin w-4 h-4 border-2 border-t-transparent"), - h.Attribute("role", "status"), - ) -} diff --git a/examples/chat/pages/index.go b/examples/chat/pages/index.go index c077f08..62befcb 100644 --- a/examples/chat/pages/index.go +++ b/examples/chat/pages/index.go @@ -24,6 +24,10 @@ func ChatAppFirstScreen(ctx *h.RequestContext) *h.Page { Name: "username", Label: "Username", Required: true, + Children: []h.Ren{ + h.Attribute("autocomplete", "off"), + h.MaxLength(15), + }, }), h.Div( @@ -33,6 +37,10 @@ func ChatAppFirstScreen(ctx *h.RequestContext) *h.Page { Name: "new-chat-room", Label: "Create a New Chat Room", Placeholder: "Chat Room Name", + Children: []h.Ren{ + h.Attribute("autocomplete", "off"), + h.MaxLength(20), + }, }), h.Div( @@ -47,6 +55,10 @@ func ChatAppFirstScreen(ctx *h.RequestContext) *h.Page { Name: "join-chat-room", Label: "Join a Chat Room", Placeholder: "Chat Room Id", + Children: []h.Ren{ + h.Attribute("autocomplete", "off"), + h.MaxLength(100), + }, }), ), diff --git a/examples/chat/partials/index.go b/examples/chat/partials/index.go index ae86ea8..d38daec 100644 --- a/examples/chat/partials/index.go +++ b/examples/chat/partials/index.go @@ -19,6 +19,10 @@ func CreateOrJoinRoom(ctx *h.RequestContext) *h.Partial { return h.SwapPartial(ctx, components.FormError("Username is required")) } + if len(username) > 15 { + return h.SwapPartial(ctx, components.FormError("Username is too long")) + } + user, err := service.CreateUser(username) if err != nil { @@ -52,6 +56,11 @@ func CreateOrJoinRoom(ctx *h.RequestContext) *h.Partial { } chatRoomName := ctx.Request.FormValue("new-chat-room") + + if len(chatRoomName) > 20 { + return h.SwapPartial(ctx, components.FormError("Chat room name is too long")) + } + if chatRoomName != "" { room, _ := service.CreateRoom(chatRoomName) if room == nil { diff --git a/examples/chat/ws/handler.go b/examples/chat/ws/handler.go index 5281dea..dc0a79a 100644 --- a/examples/chat/ws/handler.go +++ b/examples/chat/ws/handler.go @@ -19,6 +19,9 @@ func Handle() http.HandlerFunc { c, err := websocket.Accept(w, r, nil) + // 2 mb + c.SetReadLimit(2 * 1024 * 1024) + if err != nil { return } diff --git a/examples/chat/ws/manager.go b/examples/chat/ws/manager.go index 06bdbd7..fddea04 100644 --- a/examples/chat/ws/manager.go +++ b/examples/chat/ws/manager.go @@ -2,7 +2,6 @@ package ws import ( "context" - "fmt" "github.com/coder/websocket" "github.com/puzpuzpuz/xsync/v3" ) @@ -142,7 +141,6 @@ func (manager *SocketManager) Broadcast(message []byte, messageType websocket.Me } func (manager *SocketManager) BroadcastText(message string) { - fmt.Printf("Broadcasting message: \n%s\n", message) manager.Broadcast([]byte(message), websocket.MessageText) }