From 48d5e396d7ec25be81a718ae24142891fecf40b0 Mon Sep 17 00:00:00 2001 From: emil28092005 Date: Wed, 7 Jan 2026 23:59:28 +0300 Subject: [PATCH 1/2] create initial frontend. --- cmd/server/main.go | 26 +++++ internal/api/websocket.go | 46 ++++++++ internal/db/sqlite.go | 2 +- internal/websocket/room.go | 8 +- project_context.txt | 224 +++++++++++++++++++++++++++++++++++++ shagram.db | Bin 0 -> 20480 bytes static/index.html | 44 ++++++++ static/js/chat.js | 43 +++++++ 8 files changed, 388 insertions(+), 5 deletions(-) create mode 100644 cmd/server/main.go create mode 100644 internal/api/websocket.go create mode 100644 shagram.db create mode 100644 static/index.html create mode 100644 static/js/chat.js diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..a034499 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "log" + "shagram/internal/api" + "shagram/internal/db" + "shagram/internal/websocket" + + "github.com/gin-gonic/gin" +) + +func main() { + database, err := db.NewDB("shagram.db") + if err != nil { + log.Fatal(err) + } + defer database.Close() + hub := websocket.NewHub() + router := gin.Default() + + router.GET("/ws/:room", api.WebSocketHandler(hub)) + router.GET("/api/rooms", func(c *gin.Context) { + c.JSON(200, gin.H{"rooms": "TODO"}) + }) + router.Run(":8080") +} diff --git a/internal/api/websocket.go b/internal/api/websocket.go new file mode 100644 index 0000000..eefba1c --- /dev/null +++ b/internal/api/websocket.go @@ -0,0 +1,46 @@ +package api + +import ( + "net/http" + "shagram/internal/websocket" + + "github.com/gin-gonic/gin" + gws "github.com/gorilla/websocket" +) + +var upgrader = gws.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, +} + +func WebSocketHandler(hub *websocket.Hub) gin.HandlerFunc { + return func(c *gin.Context) { + roomID := c.Param("room") + room := hub.GetOrCreateRoom(roomID) + + conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + return + } + + client := &websocket.Client{Conn: conn, Room: room} + room.Register(client) + + go func() { + defer func() { + room.Unregister(client) + hub.CleanupRoom(roomID) + }() + + for { + var msg map[string]string + err := client.Conn.ReadJSON(&msg) + if err != nil { + break + } + message := []byte(msg["text"]) + room.Broadcast(message) + } + }() + } + +} diff --git a/internal/db/sqlite.go b/internal/db/sqlite.go index 9d0795d..727c294 100644 --- a/internal/db/sqlite.go +++ b/internal/db/sqlite.go @@ -17,7 +17,7 @@ func NewDB(path string) (*DB, error) { return nil, err } - schema, err := os.ReadFile("../../migrations/schema.sql") + schema, err := os.ReadFile("migrations/schema.sql") if err != nil { return nil, err } diff --git a/internal/websocket/room.go b/internal/websocket/room.go index f98d340..a69e59e 100644 --- a/internal/websocket/room.go +++ b/internal/websocket/room.go @@ -3,8 +3,8 @@ package websocket import "github.com/gorilla/websocket" type Client struct { - conn *websocket.Conn - room *Room + Conn *websocket.Conn + Room *Room } type Room struct { @@ -25,12 +25,12 @@ func (r *Room) Register(client *Client) { func (r *Room) Unregister(client *Client) { delete(r.clients, client) - client.conn.Close() + client.Conn.Close() } func (r *Room) Broadcast(message []byte) { for client := range r.clients { - err := client.conn.WriteMessage(websocket.TextMessage, message) + err := client.Conn.WriteMessage(websocket.TextMessage, message) if err != nil { r.Unregister(client) } diff --git a/project_context.txt b/project_context.txt index e8461d9..50d4c7f 100644 --- a/project_context.txt +++ b/project_context.txt @@ -1,3 +1,31 @@ +--- cmd/server/main.go --- +package main + +import ( + "log" + "shagram/internal/api" + "shagram/internal/db" + "shagram/internal/websocket" + + "github.com/gin-gonic/gin" +) + +func main() { + database, err := db.NewDB("shagram.db") + if err != nil { + log.Fatal(err) + } + defer database.Close() + hub := websocket.NewHub() + router := gin.Default() + + router.GET("/ws/:room", api.WebSocketHandler(hub)) + router.GET("/api/rooms", func(c *gin.Context) { + c.JSON(200, gin.H{"rooms": "TODO"}) + }) + router.Run(":8080") +} + --- go.mod --- module shagram @@ -125,3 +153,199 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +--- internal/api/websocket.go --- +package api + +import ( + "net/http" + "shagram/internal/websocket" + + "github.com/gin-gonic/gin" + gws "github.com/gorilla/websocket" +) + +var upgrader = gws.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, +} + +func WebSocketHandler(hub *websocket.Hub) gin.HandlerFunc { + return func(c *gin.Context) { + roomID := c.Param("room") + room := hub.GetOrCreateRoom(roomID) + + conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + return + } + + client := &websocket.Client{Conn: conn, Room: room} + room.Register(client) + + go func() { + defer func() { + room.Unregister(client) + hub.CleanupRoom(roomID) + }() + + for { + var msg map[string]string + err := client.Conn.ReadJSON(&msg) + if err != nil { + break + } + message := []byte(msg["text"]) + room.Broadcast(message) + } + }() + } + +} + +--- internal/db/sqlite.go --- +package db + +import ( + "database/sql" + "os" + + _ "github.com/mattn/go-sqlite3" +) + +type DB struct { + *sql.DB +} + +func NewDB(path string) (*DB, error) { + db, err := sql.Open("sqlite3", path) + if err != nil { + return nil, err + } + + schema, err := os.ReadFile("migrations/schema.sql") + if err != nil { + return nil, err + } + + _, err = db.Exec(string(schema)) + if err != nil { + return nil, err + } + + return &DB{db}, nil +} + +--- internal/models/message.go --- +package models + +import "time" + +type Message struct { + ID int `json:"id"` + RoomID string `json:"room_id"` + User string `json:"user"` + Text string `json:"text"` + CreatedAt time.Time `json:"created_at"` +} + +type Room struct { + ID string `json:"id"` + Name string `json:"name"` +} + +--- internal/websocket/hub.go --- +package websocket + +type Hub struct { + rooms map[string]*Room +} + +func NewHub() *Hub { + return &Hub{ + rooms: make(map[string]*Room), + } +} + +func (h *Hub) GetOrCreateRoom(roomID string) *Room { + if _, ok := h.rooms[roomID]; !ok { + h.rooms[roomID] = NewRoom(roomID) + } + return h.rooms[roomID] +} + +func (h *Hub) CleanupRoom(roomID string) { + room := h.rooms[roomID] + if len(room.clients) == 0 { + delete(h.rooms, roomID) + } +} + +--- internal/websocket/room.go --- +package websocket + +import "github.com/gorilla/websocket" + +type Client struct { + Conn *websocket.Conn + Room *Room +} + +type Room struct { + id string + clients map[*Client]bool +} + +func NewRoom(id string) *Room { + return &Room{ + id: id, + clients: make(map[*Client]bool), + } +} + +func (r *Room) Register(client *Client) { + r.clients[client] = true +} + +func (r *Room) Unregister(client *Client) { + delete(r.clients, client) + client.Conn.Close() +} + +func (r *Room) Broadcast(message []byte) { + for client := range r.clients { + err := client.Conn.WriteMessage(websocket.TextMessage, message) + if err != nil { + r.Unregister(client) + } + } +} + +--- migrations/schema.sql --- +CREATE TABLE IF NOT EXISTS rooms ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + room_id TEXT NOT NULL, + user TEXT NOT NULL, + text TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (room_id) REFERENCES rooms (id) +); + +INSERT OR IGNORE INTO rooms (id, name) VALUES ('general', 'General'); +--- shagram.db --- +SQLite format 3@ .  n] P++Ytablesqlite_sequencesqlite_sequenceCREATE TABLE sqlite_sequence(name,seq)otablemessagesmessagesCREATE TABLE messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + room_id TEXT NOT NULL, + user TEXT NOT NULL, + text TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (room_id) REFERENCES rooms (id) +)]tableroomsroomsCREATE TABLE rooms ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL +))=indexsqlite_autoindex_rooms_1rooms generalGeneral + + general   diff --git a/shagram.db b/shagram.db new file mode 100644 index 0000000000000000000000000000000000000000..acd77158154b5b8f435eae2c0e25f3227b596275 GIT binary patch literal 20480 zcmeI&O>5gQ7zc1EPTS3+@dgDmLP5uvmciJ0P%l-2nd4-R1IwWpuhB{%v8nAquj_96 zZu=-Z4tCsQ#fGMI=Rx>CSdlF2#roM7C7FKmm!%S4vwWSD;vIX*IA<3^Fvbi@2b7v< zQK8+OP|hEe|5Y^Dn;$2~)*sgHykJ&p{p?&(0RaIBKmY;|fB*y_009U<00RG0VA4Lc z&(8R~OunpCal4{@VO*%&jY_W{pC0r>=|)mS?gw9rM{Rp)vR3ER)4A^$NA~F{|DNeF z*QzLz8&x!UduNoUR`g6kv0Ml*h~!X)ViI~IH=K))axUCiH1-1OIFdnhu3P3=wvMTJ zBtJ(Y7*m@0{`S(QQ2A5!rMfGhs=v-vQmREvTj5JuU*wIXxRe8T=0~DG3qzVSt}3UI zJDO~#7>q;d4Fi31z4}%h5z2w2)_pk@Rb3%^%Y|b)SNzDfZT{`G_Rv@KM{7r-?$I#l zbv-cH&RZexF2Qu1)}j60=4_cR)Ls2ll1-WE^H>iXztvf_w|`c>Y{!(i^^4JgfB*y_ z009U<00Izz00bZa0SG`~9|T@;quaevsmhbpaGRO5E^hs1bRZxA0SG_<0uX=z1Rwwb z2tWV=5ZEUH)9600zXqs&|EE9wBOm|)2tWV=5P$##AOHafKmY;|*kb|g|M&ReVrdY7 W00bZa0SG_<0uX=z1Rwx`THr5eN2zrH literal 0 HcmV?d00001 diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..97921a1 --- /dev/null +++ b/static/index.html @@ -0,0 +1,44 @@ + + + + Shagram Chat + + + +

📱 Shagram Chat

+ +
+ + +
+ +
+ +
+ + +
+ + + + diff --git a/static/js/chat.js b/static/js/chat.js new file mode 100644 index 0000000..ca42810 --- /dev/null +++ b/static/js/chat.js @@ -0,0 +1,43 @@ +let ws = null; +const messagesDiv = document.getElementById('messages'); +const messageInput = document.getElementById('messageInput'); +const roomInput = document.getElementById('roomInput'); + +function connectRoom() { + const room = roomInput.value || 'general'; + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + ws = new WebSocket(`${protocol}//localhost:8080/ws/${room}`); + + ws.onopen = function() { + console.log('Connected to room: ' + room); + messagesDiv.innerHTML = '
✅ Connected to ' + room + '
'; + }; + + ws.onmessage = function(event) { + const msgDiv = document.createElement('div'); + msgDiv.className = 'message'; + msgDiv.textContent = event.data; + messagesDiv.appendChild(msgDiv); + messagesDiv.scrollTop = messagesDiv.scrollHeight; + }; + + ws.onerror = function(error) { + console.error('WebSocket error:', error); + messagesDiv.innerHTML += '
❌ Error: ' + error + '
'; + }; + + ws.onclose = function() { + messagesDiv.innerHTML += '
⛔ Disconnected
'; + }; +} + +function sendMessage() { + const text = messageInput.value.trim(); + if (!text || !ws || ws.readyState !== WebSocket.OPEN) { + alert('Not connected or empty message'); + return; + } + + ws.send(JSON.stringify({text: text})); + messageInput.value = ''; +} \ No newline at end of file From 04646192bc827ebd6c749857c4e829b009d7e38a Mon Sep 17 00:00:00 2001 From: emil28092005 Date: Thu, 8 Jan 2026 00:10:58 +0300 Subject: [PATCH 2/2] chat worksgit add .! --- cmd/server/main.go | 4 ++ project_context.txt | 94 +++++++++++++++++++++++++++++++++++++++++++++ static/index.html | 2 +- 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index a034499..7ad9a18 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -22,5 +22,9 @@ func main() { router.GET("/api/rooms", func(c *gin.Context) { c.JSON(200, gin.H{"rooms": "TODO"}) }) + router.Static("/static", "./static") + router.GET("/", func(c *gin.Context) { + c.File("./static/index.html") + }) router.Run(":8080") } diff --git a/project_context.txt b/project_context.txt index 50d4c7f..443baeb 100644 --- a/project_context.txt +++ b/project_context.txt @@ -23,6 +23,10 @@ func main() { router.GET("/api/rooms", func(c *gin.Context) { c.JSON(200, gin.H{"rooms": "TODO"}) }) + router.Static("/static", "./static") + router.GET("/", func(c *gin.Context) { + c.File("./static/index.html") + }) router.Run(":8080") } @@ -349,3 +353,93 @@ SQLite format 3@  ))=indexsqlite_autoindex_rooms_1rooms generalGeneral   general   +--- static/index.html --- + + + + Shagram Chat + + + +

📱 Shagram Chat

+ +
+ + +
+ +
+ +
+ + +
+ + + + + +--- static/js/chat.js --- +let ws = null; +const messagesDiv = document.getElementById('messages'); +const messageInput = document.getElementById('messageInput'); +const roomInput = document.getElementById('roomInput'); + +function connectRoom() { + const room = roomInput.value || 'general'; + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + ws = new WebSocket(`${protocol}//localhost:8080/ws/${room}`); + + ws.onopen = function() { + console.log('Connected to room: ' + room); + messagesDiv.innerHTML = '
✅ Connected to ' + room + '
'; + }; + + ws.onmessage = function(event) { + const msgDiv = document.createElement('div'); + msgDiv.className = 'message'; + msgDiv.textContent = event.data; + messagesDiv.appendChild(msgDiv); + messagesDiv.scrollTop = messagesDiv.scrollHeight; + }; + + ws.onerror = function(error) { + console.error('WebSocket error:', error); + messagesDiv.innerHTML += '
❌ Error: ' + error + '
'; + }; + + ws.onclose = function() { + messagesDiv.innerHTML += '
⛔ Disconnected
'; + }; +} + +function sendMessage() { + const text = messageInput.value.trim(); + if (!text || !ws || ws.readyState !== WebSocket.OPEN) { + alert('Not connected or empty message'); + return; + } + + ws.send(JSON.stringify({text: text})); + messageInput.value = ''; +} diff --git a/static/index.html b/static/index.html index 97921a1..5e58725 100644 --- a/static/index.html +++ b/static/index.html @@ -39,6 +39,6 @@ - +