Commit 383ab0f1 authored by 健杭 徐's avatar 健杭 徐
Browse files

已删除 myproject/chatroom/Dockerfile, myproject/chatroom/docker-compose.yml,...

已删除 myproject/chatroom/Dockerfile, myproject/chatroom/docker-compose.yml, myproject/chatroom/go.mod, myproject/chatroom/go.sum, myproject/chatroom/main.go, myproject/my-app/src/app/ChatRoom/ChatRoom.module.css, myproject/my-app/src/app/ChatRoom/page.tsx, myproject/my-app/src/app/Register/Register.module.css, myproject/my-app/src/app/Register/page.tsx, myproject/my-app/src/app/SetName/SetName.module.css, myproject/my-app/src/app/SetName/page.tsx, myproject/my-app/src/app/public/1 (3).jpg, myproject/my-app/src/app/public/20210920000906_53764.png, myproject/my-app/src/app/public/SetbackGround.jpg, myproject/my-app/src/app/public/backGround.jpg, myproject/my-app/src/app/favicon.ico, myproject/my-app/src/app/globals.css, myproject/my-app/src/app/layout.tsx, myproject/my-app/src/app/page.module.css, myproject/my-app/src/app/page.tsx, myproject/my-app/.gitignore, myproject/my-app/Dockerfile, myproject/my-app/README.md, myproject/my-app/next.config.ts, myproject/my-app/package-lock.json, myproject/my-app/package.json, myproject/my-app/tsconfig.json, myproject/README.md, myproject/package-lock.json
parent 78c35070
# README
### 1.go返回数组时格式问题
### 2.添加了头像
### 3.使用了`react-icons/md`
## unfinished
### 1.deleted function
FROM golang:1.24.3-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/main .
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["/app/main"]
version: '3.8'
services:
backend:
build: .
ports:
- "8080:8080"
environment:
- DB_HOST=db
- DB_PORT=5432
- DB_USER=chat_room_user
- DB_PASSWORD=secure_password
- DB_NAME=chat_room_db
depends_on:
- db
db:
image: postgres:13-alpine
environment:
- POSTGRES_USER=chat_room_user
- POSTGRES_PASSWORD=secure_password
- POSTGRES_DB=chat_room_db
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
module chatroom
go 1.24.3
require (
github.com/gin-gonic/gin v1.10.1
github.com/lib/pq v1.10.9
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"os"
"strconv"
"github.com/gin-gonic/gin"
_ "github.com/lib/pq"
)
// Helper to get env var or default
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
// Helper to get env var as int or default
func getEnvAsInt(name string, defaultVal int) int {
valueStr := getEnv(name, "")
if value, err := strconv.Atoi(valueStr); err == nil {
return value
}
return defaultVal
}
type Message struct {
MessageId int `json:"message_id"`
RoomId int `json:"room_id"`
Profile int `json:"profile"`
Sender string `json:"sender"`
Content string `json:"content"`
Time string `json:"time"`
}
type RoomPreviewInfo struct {
RoomId int `json:"roomId"`
RoomName string `json:"roomName"`
LastSender sql.NullString `json:"lastSender"`
LastContent sql.NullString `json:"lastContent"`
LastTime sql.NullTime `json:"lastTime"`
}
type Response struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
type RoomAddRes struct {
RoomId int `json:"room_id"`
}
var db *sql.DB
func main() {
host := getEnv("DB_HOST", "localhost")
port := getEnvAsInt("DB_PORT", 5432)
user := getEnv("DB_USER", "chat_room_user")
password := getEnv("DB_PASSWORD", "secure_password")
dbname := getEnv("DB_NAME", "chat_room_db")
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
var err error
db, err = sql.Open("postgres", psqlInfo)
if err != nil {
log.Fatal(err)
}
defer db.Close()
err = db.Ping()
if err != nil {
log.Fatal(err)
}
createTable()
var tableExists bool
err = db.QueryRow(`
SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_name = 'rooms'
)
`).Scan(&tableExists)
if err != nil {
log.Fatal("Table check failed: ", err)
}
if !tableExists {
log.Println("Table 'rooms' not found, creating...")
createTable()
} else {
log.Println("Table 'rooms' already exists")
}
router := gin.Default()
router.Use(func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
})
router.POST("/register", Register)
router.POST("/login", Login)
router.POST("/room/add", AddNewRoom)
router.GET("/room/list", GetRoomList)
router.POST("/room/delete", DeleteRoom)
router.POST("/room/message/add", AddMessage)
router.GET("/room/message/list", GetMessageList)
router.PUT("/room/message/update", RoomMessageUpdate)
router.PUT("/room/rename", RoomRename)
router.Run(":8080")
}
func Register(c *gin.Context) {
userName := c.Query("userName")
password := c.Query("password")
var roomId int
err := db.QueryRow(
"INSERT INTO users (username, password) VALUES ($1, $2) RETURNING user_id",
userName, password,
).Scan(&roomId)
if err != nil {
c.JSON(500, Response{Code: 500, Msg: "注册失败", Data: nil})
return
}
log.Printf("New user registered: %s with ID %d", userName, roomId)
c.JSON(200, Response{Code: 0, Msg: "注册成功", Data: nil})
}
func Login(c *gin.Context) {
userName := c.Query("userName")
password := c.Query("password")
var userId int
err := db.QueryRow(
"SELECT user_id FROM users WHERE username = $1 AND password = $2",
userName, password,
).Scan(&userId)
if err != nil {
c.JSON(401, Response{Code: 401, Msg: "账号或密码错误", Data: nil})
return
}
log.Printf("User logged in: %s with ID %d", userName, userId)
c.JSON(200, Response{Code: 0, Msg: "登录成功", Data: nil})
}
func AddNewRoom(c *gin.Context) {
var room RoomPreviewInfo
if err := c.ShouldBindJSON(&room); err != nil {
c.JSON(400, Response{Code: 400, Msg: "Invalid input", Data: nil})
return
}
var roomId int
err := db.QueryRow(
"INSERT INTO rooms (room_name) VALUES ($1) RETURNING room_id",
room.RoomName,
).Scan(&roomId)
if err != nil {
c.JSON(500, Response{Code: 500, Msg: "Failed to add room", Data: nil})
return
}
room.RoomId = roomId
c.JSON(200, RoomAddRes{
RoomId: room.RoomId,
})
log.Printf("New room added: %+v", room)
}
func GetRoomList(c *gin.Context) {
var total int
roomCountQuery := "SELECT COUNT(*) FROM rooms"
err := db.QueryRow(roomCountQuery).Scan(&total)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "总数统计失败:" + err.Error(),
})
return
}
query := `
SELECT room_id, room_name, last_sender, last_content, last_time
FROM rooms
ORDER BY last_time DESC NULLS LAST
`
args := []interface{}{}
rows, err := db.Query(query, args...)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "Falie while serch:" + err.Error(),
})
return
}
defer rows.Close()
var roomList []RoomPreviewInfo
for rows.Next() {
var roomItem RoomPreviewInfo
if err := rows.Scan(
&roomItem.RoomId,
&roomItem.RoomName,
&roomItem.LastSender,
&roomItem.LastContent,
&roomItem.LastTime,
); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "Faile while parsing" + err.Error(),
})
return
}
roomList = append(roomList, roomItem)
}
c.JSON(http.StatusOK, Response{
Code: 0,
Msg: "Success",
Data: roomList,
})
}
func DeleteRoom(c *gin.Context) {
roomId := c.Query("roomId")
if roomId == "" {
c.JSON(400, Response{Code: 400, Msg: "Room ID is required", Data: nil})
return
}
_, err := db.Exec("DELETE FROM messages WHERE room_id = $1", roomId)
if err != nil {
c.JSON(500, Response{Code: 500, Msg: "Failed to delete messages in room: " + err.Error(), Data: nil})
return
}
_, err_ := db.Exec("DELETE FROM rooms WHERE room_id = $1", roomId)
if err_ != nil {
c.JSON(500, Response{Code: 500, Msg: "Failed to delete room: " + err_.Error(), Data: nil})
return
}
c.JSON(200, Response{
Code: 0,
Msg: "Room deleted successfully",
Data: nil,
})
}
func AddMessage(c *gin.Context) {
var message struct {
RoomId int `json:"roomId"`
ProfileId int `json:"profile"`
Sender string `json:"sender"`
Content string `json:"content"`
}
if err := c.ShouldBindJSON(&message); err != nil {
c.JSON(http.StatusBadRequest, Response{Code: 400, Msg: "Invalid input: " + err.Error(), Data: nil})
return
}
// 写入评论
query := `
INSERT INTO messages (room_id, profile, sender, content, "time")
VALUES ($1, $2, $3, $4, NOW())
RETURNING message_id
`
var messageId int
err := db.QueryRow(query, message.RoomId, message.ProfileId, message.Sender, message.Content).Scan(&messageId)
if err != nil {
c.JSON(http.StatusInternalServerError, Response{Code: 500, Msg: "Failed to add message: " + err.Error(), Data: nil})
return
}
// 更新room
_, err = db.Exec(
"UPDATE rooms SET last_sender = $1, last_content = $2, last_time = NOW() WHERE room_id = $3",
message.Sender, message.Content, message.RoomId,
)
if err != nil {
log.Printf("Failed to update room preview: %v", err)
c.JSON(http.StatusInternalServerError, Response{Code: 500, Msg: "Failed to updata lastest message: " + err.Error(), Data: nil})
return
}
c.JSON(http.StatusOK, Response{
Code: 0,
Msg: "Message added successfully",
Data: gin.H{"messageId": messageId},
})
log.Printf("New message with ID %d added to room %d", messageId, message.RoomId)
}
func GetMessageList(c *gin.Context) {
roomId := c.Query("roomId")
if roomId == "" {
c.JSON(400, Response{Code: 400, Msg: "Room ID is required", Data: nil})
return
}
rows, err := db.Query("SELECT profile, sender, content, time FROM messages WHERE room_id = $1 ORDER BY time ASC", roomId)
if err != nil {
c.JSON(500, Response{Code: 500, Msg: "Failed to retrieve messages", Data: nil})
return
}
defer rows.Close()
var messages []Message
for rows.Next() {
var msg Message
if err := rows.Scan(&msg.Profile, &msg.Sender, &msg.Content, &msg.Time); err != nil {
c.JSON(500, Response{Code: 500, Msg: "Failed to scan message", Data: nil})
return
}
messages = append(messages, msg)
}
c.JSON(200, Response{
Code: 0,
Msg: "Messages retrieved successfully",
Data: messages,
})
}
func RoomRename(c *gin.Context) {
roomId, err := strconv.Atoi(c.Query("roomId"))
if err != nil || roomId <= 0 {
c.JSON(http.StatusOK, Response{Code: 400, Msg: "Invalid room ID"})
return
}
var newName struct {
RoomName string `json:"roomName"`
}
if err := c.ShouldBindJSON(&newName); err != nil {
c.JSON(http.StatusBadRequest, Response{Code: 400, Msg: "Invalid input: " + err.Error(), Data: nil})
return
}
_, err = db.Exec("UPDATE rooms SET room_name = $1 WHERE room_id = $2", newName.RoomName, roomId)
if err != nil {
c.JSON(http.StatusInternalServerError, Response{Code: 500, Msg: "Failed to rename room: " + err.Error(), Data: nil})
return
}
c.JSON(http.StatusOK, Response{
Code: 0,
Msg: "Room renamed successfully",
Data: nil,
})
}
func RoomMessageUpdate(c *gin.Context) {
// 处理更新房间消息的逻辑
}
func createTable() {
query := `
CREATE TABLE IF NOT EXISTS rooms (
room_id SERIAL PRIMARY KEY,
room_name VARCHAR(100) NOT NULL UNIQUE,
last_sender VARCHAR(100),
last_content TEXT,
last_time TIMESTAMP
);
CREATE TABLE IF NOT EXISTS messages (
message_id SERIAL PRIMARY KEY,
room_id INT NOT NULL,
profile INT NOT NULL,
sender VARCHAR(100) NOT NULL,
content TEXT NOT NULL,
"time" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (room_id) REFERENCES rooms(room_id)
);
CREATE TABLE IF NOT EXISTS users (
user_id SERIAL PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL
);
`
_, err := db.Exec(query)
if err != nil {
log.Fatal("Failed to create tables: ", err)
}
log.Println("Tables created successfully")
}
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS runner
WORKDIR /app
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: 'standalone',
};
export default nextConfig;
This diff is collapsed.
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "15.3.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"typescript": "^5"
}
}
.chat-room {
display: flex;
position: fixed;
width: 100vw;
height: 100vh;
box-sizing: border-box;
overflow: hidden;
}
.chat-room-nav {
position: fixed;
padding: 10px;
width: 15%;
height: 100%;
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
z-index: 10;
}
.open {
display: flex;
position: fixed;
width: 100vw;
height: 100vh;
box-sizing: border-box;
overflow: hidden;
background-color: rgba(255, 255, 255, 0.75);
z-index: 0;
}
.open .roomName-input {
background-color: #fff;
display: flex;
flex-direction: column;
position: fixed;
top: 40%;
left: 30%;
z-index: 0;
width: 40%;
height: 25%;
border-radius: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
h3 {
align-items: center;
text-align: center;
}
input {
width: 80%;
height: 20px;
margin: 20px auto;
padding: 10px;
border-radius: 20px;
border: 1px solid #ccc;
font-size: 16px;
}
.button-container {
display: flex;
justify-content: center;
margin-top: -5px;
button {
width: 100px;
height: 40px;
background-color: #007bff;
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
margin-left: 10px;
margin-right: 10px;
&:hover {
background-color: #0056b3;
}
}
}
}
.chat-room-nav .chat-list {
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
padding: 10px;
box-sizing: border-box;
z-index: 1;
.chat-item {
display: flex;
position: relative;
align-items: center;
padding: 10px;
margin-bottom: 10px;
border-radius: 8px;
cursor: pointer;
border: 1px solid #ddd;
&:hover {
background-color: #e0e0e0;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
}
.chat-info {
height: 40px;
flex-grow: 1;
right: 0;
width: 80%;
h3 {
margin: 0;
margin-left: 5px;
font-size: 1.2em;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-message {
position: relative;
margin-bottom: 10px;
margin-left: 5px;
max-width:100px;
font-weight: bold;
color: #333;
max-height: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
}
.chat-time {
position: absolute;
color: #666;
font-size: 0.9em;
right: 5px;
bottom: 10px;
}
}
.chat-room-menu {
position: absolute;
right: 10px;
top: 10px;
width: 20px;
background-color: white;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 100;
text-align: center;
cursor: pointer;
}
/* Dropdown menu for actions */
.menu-dropdown {
position: absolute;
right: 6px;
top: 36px;
min-width: 160px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
padding: 6px 0;
display: none;
}
.menu-open {
display: block;
}
.menu-item {
width: 100%;
background: transparent;
border: none;
text-align: left;
padding: 10px 12px;
font-size: 14px;
color: #111827;
cursor: pointer;
}
.menu-item:hover {
background: #f3f4f6;
}
}
}
.message-item {
position: relative;
left: 15%;
width: 85%;
height: 100vh;
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f0f0f0;
background-size: cover;
z-index: 2;
.message-header {
z-index: 5;
width: 100%;
position: sticky;
top: 0;
background-color: #fff;
height: 70px;
display: flex;
align-items: center;
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-left: 40px;
}
h2 {
width: 10%;
color: #333;
text-align: center;
margin: 0;
padding: 0;
}
border-bottom: 1px solid #000000;
}
.message-list {
flex: 1;
overflow-y: auto;
padding: 10px;
box-sizing: border-box;
z-index: 3;
.message {
display: flex;
max-width: 80%;
padding: 10px;
margin-bottom: 15px;
margin-left: 15px;;
border-radius: 15px;
overflow-wrap: break-word;
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
flex-shrink: 0;
}
.message-content {
min-width: 0;
.message-info {
height: auto;
.message-sender {
font-weight: bold;
color: #333;
margin-right: 8px;
}
.message-time {
font-size: 0.8em;
color: #999;
}
}
.message-text {
padding: 8px 12px;
margin: 5px 0;
border-radius: 10px;
color: #555;
background-color: #f5f5f5;
display: block;
word-break: break-word;
}
}
}
}
.message-input {
position: sticky;
bottom: 0;
width: 90%;
padding: 15px;
box-sizing: border-box;
z-index: 4;
display: flex;
margin-left: 5%;
input {
flex: 1;
padding: 12px 15px;
border-radius: 25px;
font-size: 16px;
margin-right: 10px;
background-color: transparent;
}
input:hover {
border: #000000 1px solid;
}
}
}
/* From Uiverse.io by njesenberger */
.button {
-webkit-appearance: none;
appearance: none;
position: relative;
border-width: 0;
padding: 0 8px 12px;
min-width: 10em;
box-sizing: border-box;
background: transparent;
font: inherit;
cursor: pointer;
}
.button-top {
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 0;
padding: 8px 16px;
transform: translateY(0);
text-align: center;
color: #fff;
text-shadow: 0 -1px rgba(0, 0, 0, .25);
transition-property: transform;
transition-duration: .2s;
-webkit-user-select: none;
user-select: none;
}
.button:active .button-top {
transform: translateY(6px);
}
.button-top::after {
content: '';
position: absolute;
z-index: -1;
border-radius: 4px;
width: 100%;
height: 100%;
box-sizing: content-box;
background-image: radial-gradient(#cd3f64, #9d3656);
text-align: center;
color: #fff;
box-shadow: inset 0 0 0px 1px rgba(255, 255, 255, .2), 0 1px 2px 1px rgba(255, 255, 255, .2);
transition-property: border-radius, padding, width, transform;
transition-duration: .2s;
}
.button:active .button-top::after {
border-radius: 6px;
padding: 0 2px;
}
.button-bottom {
position: absolute;
z-index: -1;
bottom: 4px;
left: 4px;
border-radius: 8px / 16px 16px 8px 8px;
padding-top: 6px;
width: calc(100% - 8px);
height: calc(100% - 10px);
box-sizing: content-box;
background-color: #803;
background-image: radial-gradient(4px 8px at 4px calc(100% - 8px), rgba(255, 255, 255, .25), transparent), radial-gradient(4px 8px at calc(100% - 4px) calc(100% - 8px), rgba(255, 255, 255, .25), transparent), radial-gradient(16px at -4px 0, white, transparent), radial-gradient(16px at calc(100% + 4px) 0, white, transparent);
box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.5), inset 0 -1px 3px 3px rgba(0, 0, 0, .4);
transition-property: border-radius, padding-top;
transition-duration: .2s;
}
.button:active .button-bottom {
border-radius: 10px 10px 8px 8px / 8px;
padding-top: 0;
}
.button-base {
position: absolute;
z-index: -2;
top: 4px;
left: 0;
border-radius: 12px;
width: 100%;
height: calc(100% - 4px);
background-color: rgba(0, 0, 0, .15);
box-shadow: 0 1px 1px 0 rgba(255, 255, 255, .75), inset 0 2px 2px rgba(0, 0, 0, .25);
}
.message-item {
/* From Uiverse.io by adamgiebl */
button {
font-family: inherit;
font-size: 20px;
background: transparent;
color: white;
padding: 0.7em 1em;
padding-left: 0.9em;
display: flex;
align-items: center;
border: none;
border-radius: 16px;
overflow: hidden;
transition: all 0.2s;
cursor: pointer;
}
button span {
display: block;
margin-left: 0.3em;
transition: all 0.3s ease-in-out;
}
button svg {
display: block;
transform-origin: center center;
transition: transform 0.3s ease-in-out;
}
button:hover .svg-wrapper {
animation: fly-1 0.6s ease-in-out infinite alternate;
}
button:hover svg {
transform: translateX(1.2em) rotate(45deg) scale(1.1);
}
button:hover span {
transform: translateX(5em);
}
button:active {
transform: scale(0.95);
}
@keyframes fly-1 {
from {
transform: translateY(0.1em);
}
to {
transform: translateY(-0.1em);
}
}
}
\ No newline at end of file
'use client';
import styles from "./ChatRoom.module.css";
import React, { useEffect, useState } from "react";
import { useRouter } from 'next/navigation';
const backEnd:string = "http://localhost:8080";
const Profile = [ 'https://pic4.zhimg.com/v2-c5a0d0d57c1a85c6db56e918707f54a3_r.jpg',
'https://pic2.zhimg.com/v2-c2e79191533fdc7fced2f658eef987c9_r.jpg',
'https://pic4.zhimg.com/v2-bf5f58e7b583cd69ac228db9fdff377f_r.jpg',
'https://pic1.zhimg.com/v2-10e9368af9eb405c8844584ad3ad9dd8_r.jpg',
'https://picx.zhimg.com/50/v2-63e3500bfd25b6ae7013a6a3b6ce045b_720w.jpg',
'https://c-ssl.duitang.com/uploads/blog/202109/20/20210920000906_53764.png']
const RoomProfile = 'https://tse1-mm.cn.bing.net/th/id/OIP-C.0KyBJKAdIGi9SAQc_X62tQHaLr?cb=thvnextc2&rs=1&pid=ImgDetMain';
interface RoomEntryProps {
roomId: number;
roomName: string;
lastSender: { String: string, Valid: boolean };
lastContent: { String: string, Valid: boolean };
lastTime: { Time: string, Valid: boolean };
}
interface MessageProps {
roomId: number;
roomName: string;
messages: Array<{
profile: number;
sender: string;
content: string;
time: string;
}>;
}
function RoomEntry ({rooms, onRoomClick, onRename, onDelete} : {rooms: RoomEntryProps[], onRoomClick: (roomId: number, roomName: string) => void, onRename: (roomId: number, currentName: string) => void, onDelete: (roomId: number) => void}) {
const [openMenuFor, setOpenMenuFor] = useState<number | null>(null);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (!target.closest('[data-menu="room-actions"]')) {
setOpenMenuFor(null);
}
};
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}, []);
return (
<div className={styles["chat-room-nav"]}>
<div className={styles["sidebar-action"]}>
<button type="button" className={styles["button"]} onClick={openOpenDiv}>
<div className={styles["button-top"]}>New Chat</div>
<div className={styles["button-bottom"]}></div>
<div className={styles["button-base"]}></div>
</button>
</div>
<div className={styles["chat-list"]}>
{rooms.map((room) => (
<div className={styles["chat-item"]} key={room.roomId}>
<img src={RoomProfile} alt="Avatar" className={styles["avatar"]} />
<div className={styles["chat-info"]}>
<h3 onClick={() => onRoomClick(room.roomId, room.roomName)}>{room.roomName}</h3>
<span className={styles["chat-message"]}>
{room.lastSender.Valid ? room.lastSender.String : ''}:
{room.lastContent.Valid ? room.lastContent.String : ''}</span>
<span className={styles["chat-time"]}>{room.lastTime.Valid ? formatTimeToHoursMinutes(room.lastTime.Time) : ''}</span>
</div>
<div
className={styles["chat-room-menu"]}
data-menu="room-actions"
onClick={(e) => {
e.stopPropagation();
setOpenMenuFor(prev => (prev === room.roomId ? null : room.roomId));
}}
aria-label="Room actions"
title="Room actions"
>
···
<div className={`${styles["menu-dropdown"]} ${openMenuFor === room.roomId ? styles["menu-open"] : ''}`}>
<button
className={styles["menu-item"]}
onClick={(e) => { e.stopPropagation(); setOpenMenuFor(null); onRename(room.roomId, room.roomName); }}
>
Rename room
</button>
<button
className={styles["menu-item"]}
onClick={(e) => { e.stopPropagation(); setOpenMenuFor(null); onDelete(room.roomId); }}
>
Delete room
</button>
</div>
</div>
</div>
))}
</div>
</div>
);
// Button From Uiverse.io by njesenberger
}
function formatTimeToHoursMinutes(isoString: string) {
const date = new Date(isoString);
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
}
function InputRoomNameArea({ onAddNewRoom }: { onAddNewRoom: (roomName: string) => void}) {
const [roomNameInput, setRoomNameInput] = useState("");
const handleAddNewRoom = () => {
onAddNewRoom(roomNameInput);
setRoomNameInput("");
closeOpenDiv();
}
return (
<div className={styles["open"]}>
<div className={styles["roomName-input"]}>
<h3>Please Enter the New Room Name</h3>
<input
type="text"
className={styles["RoomNameInput"]}
placeholder="Start new chat"
value = {roomNameInput}
onChange={(e) => setRoomNameInput(e.target.value)}
onKeyUpCapture={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleAddNewRoom();
}
else if (e.key === 'Escape') {
closeOpenDiv();
}
}}
/>
<div className={styles["button-container"]}>
<button className={styles["create-button"]} onClick={handleAddNewRoom}>Submit</button>
<button className={styles["cancel-button"]} onClick={closeOpenDiv}>Cancel</button>
</div>
</div>
</div>
);
}
function openOpenDiv() {
const openDiv = document.getElementsByClassName(styles.open)[0] as HTMLDivElement | undefined;
if (openDiv) {
openDiv.style.zIndex = "1000";
}
const roomNameInput = document.getElementsByClassName(styles.RoomNameInput)[0] as HTMLInputElement | undefined;
if (roomNameInput) {
roomNameInput.style.zIndex = "1001";
}
}
function closeOpenDiv() {
const openDiv = document.getElementsByClassName(styles.open)[0] as HTMLDivElement | undefined;
if (openDiv) {
openDiv.style.zIndex = "0";
}
const roomNameInput = document.getElementsByClassName(styles.RoomNameInput)[0] as HTMLInputElement | undefined;
if (roomNameInput) {
roomNameInput.style.zIndex = "0";
roomNameInput.value = '';
}
}
function MessageItem (props: MessageProps & { onAddNewComment: (content: string) => void}) {
const [inputValue, setInputValue] = useState("");
if (props.roomId === 0) {
return <div className={styles["message-item"]}>Please select a room to chat.</div>;
}
const handlerSend = () => {
if (inputValue.trim() === '') {
alert("Message can't be empty");
return
}
props.onAddNewComment(inputValue);
setInputValue('');
}
return (
<div className={styles["message-item"]}>
<div className={styles["message-header"]}>
<img src={RoomProfile} alt="Avatar" className={styles["avatar"]} />
<h2>{props.roomName}</h2>
</div>
<div className={styles["message-list"]}>
{props.messages.map((msg, index) => (
<div key={index} className={styles["message"]}>
<img src={Profile[msg.profile]} alt={`${msg.sender}'s avatar`} className={styles["avatar"]} />
<div className={styles["message-content"]}>
<div className={styles["message-info"]}>
<span className={styles["message-sender"]}>{msg.sender}</span>
<span className={styles["message-time"]}>{formatTimeToHoursMinutes(msg.time)}</span>
</div>
<p className={styles["message-text"]}>{msg.content}</p>
</div>
</div>
))}
</div>
<div className={styles["message-input"]}>
<input
type="text"
placeholder="Type a message..."
className={styles["Inputarea"]}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyUpCapture={
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handlerSend();
}
}}/>
<button className={styles["send-button"]} onClick={handlerSend}>
<div className={styles["svg-wrapper-1"]}>
<div className={styles["svg-wrapper"]}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path fill="none" d="M0 0h24v24H0z"></path>
<path
fill="currentColor"
d="M1.946 9.315c-.522-.174-.527-.455.01-.634l19.087-6.362c.529-.176.832.12.684.638l-5.454 19.086c-.15.529-.455.547-.679.045L12 14l6-8-8 6-8.054-2.685z"
></path>
</svg>
</div>
</div>
<span>Send</span>
</button>
</div>
</div>
);
// From Uiverse.io by adamgiebl
}
function ChatRoomComponent({ userName }: { userName: string }) {
const [rooms, setRooms] = useState<RoomEntryProps[]>([]);
const [currentRoom, setCurrentRoom] = useState<MessageProps | null>(null);
const ROOM_LIST_REFRESH_INTERVAL = 1000;
const MESSAGE_REFRESH_INTERVAL = 1000;
const fetchRooms = async () => {
try {
const response = await fetch(backEnd+"/room/list");
const result = await response.json();
if (result.code === 0) {
const ferchedRooms = result.data.map((room:any) => ({
...room,
lastSender: room.lastSender || { String: '', Valid: false },
laseComment: room.lastComment || { String: '', Valid: false },
lastTime: room.lastTime || { String: '', Valid: false }
}));
setRooms(ferchedRooms);
}
} catch (error) {
console.error("Error fetching rooms:", error);
}
};
const fetchCurrentRoomMessages = async (roomId: number) => {
if (!roomId) return;
try {
const response = await fetch(backEnd+`/room/message/list?roomId=${roomId}`);
const result = await response.json();
if (result.code === 0) {
setCurrentRoom(prev => {
if (!prev || prev.roomId !== roomId) return prev;
return {
...prev,
messages: result.data || []
};
});
}
} catch (error) {
console.error("Error fetching messages:", error);
}
};
useEffect(() => {
fetchRooms();
const roomListInterval = setInterval(() => {
fetchRooms();
}, ROOM_LIST_REFRESH_INTERVAL);
return () => clearInterval(roomListInterval);
}, []);
useEffect(() => {
if (!currentRoom?.roomId) return;
const messageInterval = setInterval(() => {
fetchCurrentRoomMessages(currentRoom.roomId);
}, MESSAGE_REFRESH_INTERVAL);
return () => clearInterval(messageInterval);
}, [currentRoom?.roomId]);
const handleRoomClick = async (roomId: number, roomName: string) => {
setCurrentRoom({
roomId: roomId,
roomName: roomName,
messages: []
});
await fetchCurrentRoomMessages(roomId);
}
async function addNewRoom(roomName: string) {
if (roomName.trim() === "") {
alert("Please enter a room name.");
return;
}
try {
const response = await fetch (backEnd + "/room/add", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ roomName: roomName })
})
const result = await response.json();
if (result.room_id > 0) {
const newRoom:RoomEntryProps = {
roomId: result.RoomId,
roomName: roomName,
lastSender: { String: '', Valid: false } ,
lastContent: { String: '', Valid: false },
lastTime: { Time: '', Valid: false },
};
setRooms(prevRooms => [newRoom, ...prevRooms]);
} else {
alert("Faile to add a new room" + result.Msg);
}
} catch (error) {
console.error("Error adding new room" + error);
alert("Error adding new room.");
}
}
const addNewComment = async (content: string) => {
if (!currentRoom)
return;
let profileId = 0;
if (userName === '蔡徐坤') {
profileId = Profile.length - 1;
} else {
profileId = Math.floor(Math.random() * (Profile.length-1));
}
try {
const response = await fetch(backEnd + '/room/message/add', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
roomId: currentRoom.roomId,
profile_id: profileId,
sender: userName,
content
})
})
const result = await response.json();
debugger;
if (result.code != 0) {
alert(`Error: ${result.Msg}`);
return;
}
const newMessage: { profile: number, sender: string, content: string, time: string } = {
profile: profileId,
sender: userName,
content: content,
time: new Date().toISOString()
};
setCurrentRoom(prevRoom => {
if (!prevRoom) return null;
return {
...prevRoom,
messages: [...prevRoom.messages, newMessage]
};
});
} catch (error) {
console.error("Error in addNewComment:", error);
alert("An error occurred while sending the message.");
}
}
const handleRename = async (roomId: number, currentName: string) => {
const newName = prompt('Enter new room name', currentName);
if (!newName || newName.trim() === '' || newName === currentName) return;
try {
const response = await fetch(backEnd + '/room/rename', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ roomId, roomName: newName })
});
const result = await response.json();
if (result.code !== 0) {
alert('Rename failed: ' + (result.msg || 'unknown error'));
return;
}
setRooms(prev => prev.map(r => r.roomId === roomId ? { ...r, roomName: newName } : r));
setCurrentRoom(prev => prev && prev.roomId === roomId ? { ...prev, roomName: newName } : prev);
} catch (err) {
console.error('Rename error', err);
alert('Rename error');
}
};
const handleDelete = async (roomId: number) => {
if (!confirm('Delete this room? This cannot be undone.')) return;
try {
const response = await fetch(backEnd + '/room/delete?roomId=' + roomId, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
const result = await response.json();
if (result.code !== 0) {
alert('Delete failed: ' + (result.msg || 'unknown error'));
return;
}
setRooms(prev => prev.filter(r => r.roomId !== roomId));
setCurrentRoom(prev => (prev && prev.roomId === roomId) ? null : prev);
} catch (err) {
console.error('Delete error', err);
alert('Delete error');
}
};
return (
<div className={styles["chat-room"]}>
<RoomEntry rooms={rooms} onRoomClick={handleRoomClick} onRename={handleRename} onDelete={handleDelete}/>
<MessageItem
roomId={currentRoom?.roomId || 0}
roomName={currentRoom?.roomName || ""}
messages={currentRoom?.messages || []}
onAddNewComment={addNewComment}
/>
<InputRoomNameArea onAddNewRoom={addNewRoom}/>
</div>
);
}
export default function ChatRoom() {
const [userName, setUserName] = useState<string | null>(null);
const router = useRouter();
useEffect(() => {
const storedUserName = localStorage.getItem('userName');
if (storedUserName) {
setUserName(storedUserName);
} else {
router.push('/');
}
}, [router]);
if (!userName) {
return <div>Loading...</div>;
}
return <ChatRoomComponent userName={userName} />;
}
\ No newline at end of file
/*
{
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', sans-serif;
} */
.SetName-Body
{
z-index: 2000;
display: flex;
position: absolute;
justify-content: center;
align-items: center;
min-height: 100vh;
width: 100vw;
background-size: cover;
background-color: grey;
}
.login-box
{
position: relative;
width: 400px;
height: 450px;
background-color: transparent;
border: 2px solid rgba(255, 255 , 255, .5);
border-radius: 20px;
display: flex;
justify-content: center;
align-items: center;
backdrop-filter: blur(15px);
}
.setname-h2
{
font-size: 2em;
color: white;
text-align: center;
}
.input-box
{
position: relative;
width: 310px;
margin: 30px 0;
border-bottom: 2px solid white;
}
.input-box label
{
position: absolute;
top: 50%;
left: 5px;
transform: translateY(-50%);
font-size: 1em;
color: white;
pointer-events: none;
transition: .5s;
}
.input-box input:focus~label,
.input-box input:valid~label
{
top: -5px;
}
.input-box input
{
width: 100%;
height: 50px;
border: none;
outline: none;
background-color: transparent;
color: black;
font-size: 1em;
padding: 0 35px 0 5px;
}
.input-box .icon
{
position: absolute;
right: 8px;
color: white;
font-size: .9em;
line-height: 57px;
}
.SetName-button
{
outline: none;
border: none;
width: 100%;
height: 50px;
background-color: white;
border-radius: 40px;
cursor: pointer;
color: black;
font-size: 1em;
font-weight: bold;
}
.SetName-button:hover
{
background-color: lightgray;
}
.setname-repeat
{
font-size: .9em;
color: white;
margin: 20px 0 10px 0;
}
\ No newline at end of file
'use client';
import styles from"./Register.module.css"
import { MdLock, MdPerson } from "react-icons/md";
import { useState } from "react";
import { useRouter } from 'next/navigation';
const backEnd:string = "http://localhost:8080";
export default function Register() {
const [userName, setUserName] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordError, setPasswordError] = useState('');
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setPasswordError('');
if (password !== confirmPassword) {
setPasswordError('Passwords do not match');
return;
}
if (password.length < 6) {
setPasswordError('Password must be at least 6 characters');
return;
}
try {
const response = await fetch(backEnd + '/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ userName, password }),
});
if (!response.ok) {
throw new Error('Failed to register');
}
alert('Registration successful! Redirecting to login page.');
router.push('/SetName');
} catch (error) {
console.error(error);
setPasswordError('Registration failed. Please try again.');
}
}
return (
<div className={styles["SetName-Body"]}>
<div className={styles["login-box"]}>
<form onSubmit={handleSubmit}>
<h2 className={styles["setname-h2"]}>Register</h2>
<div className={styles["input-box"]}>
<span className={styles["icon"]}>
<MdPerson />
</span>
<input
required
value={userName}
onChange={(e) => setUserName(e.target.value)} />
<label>Name</label>
</div>
<div className={styles["input-box"]}>
<span className={styles["icon"]}>
<MdLock />
</span>
<input
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)} />
<label>Password</label>
</div>
<div className={styles["input-box"]}>
<span className={styles["icon"]}>
<MdLock />
</span>
<input
type="password"
required
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)} />
<label>Confirm Password</label>
</div>
{passwordError && (
<div style={{ color: 'red', fontSize: '14px', marginTop: '5px', textAlign: 'center' }}>
{passwordError}
</div>
)}
<div>
<button className={styles["SetName-button"]} type="submit">Register</button>
</div>
</form>
</div>
</div>
);
}
\ No newline at end of file
/*
{
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', sans-serif;
} */
.SetName-Body
{
z-index: 2000;
display: flex;
position: absolute;
justify-content: center;
align-items: center;
min-height: 100vh;
width: 100vw;
background-size: cover;
background-color: grey;
}
.login-box
{
position: relative;
width: 400px;
height: 450px;
background-color: transparent;
border: 2px solid rgba(255, 255 , 255, .5);
border-radius: 20px;
display: flex;
justify-content: center;
align-items: center;
backdrop-filter: blur(15px);
}
.setname-h2
{
font-size: 2em;
color: white;
text-align: center;
}
.input-box
{
position: relative;
width: 310px;
margin: 30px 0;
border-bottom: 2px solid white;
}
.input-box label
{
position: absolute;
top: 50%;
left: 5px;
transform: translateY(-50%);
font-size: 1em;
color: white;
pointer-events: none;
transition: .5s;
}
.input-box input:focus~label,
.input-box input:valid~label
{
top: -5px;
}
.input-box input
{
width: 100%;
height: 50px;
border: none;
outline: none;
background-color: transparent;
color: black;
font-size: 1em;
padding: 0 35px 0 5px;
}
.input-box .icon
{
position: absolute;
right: 8px;
color: white;
font-size: .9em;
line-height: 57px;
}
.SetName-button
{
outline: none;
border: none;
width: 100%;
height: 50px;
background-color: white;
border-radius: 40px;
cursor: pointer;
color: black;
font-size: 1em;
font-weight: bold;
}
.SetName-button:hover
{
background-color: lightgray;
}
.register_link
{
font-size: .9em;
text-align: center;
color: white;
margin: 20px 0 10px 0;
a
{
text-decoration: none;
color: white;
font-weight: 600;
}
a:hover
{
text-decoration: underline;
}
}
\ No newline at end of file
'use client';
import styles from"./SetName.module.css"
import { MdLock, MdPerson } from "react-icons/md";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from 'next/navigation';
const backEnd:string = "http://localhost:8080";
export default function SetName() {
const [userName, setUserName] = useState('');
const [password, setPassword] = useState('');
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const response = await fetch(`${backEnd}/login`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ userName, password })
});
const data = await response.json();
if (data.code === 0) {
localStorage.setItem('userName', userName);
router.push('/ChatRoom');
} else {
alert(data.msg);
}
} catch (error) {
console.error("Error logging in:", error);
alert("Login failed. Please check the console for details.");
}
}
return (
<div className={styles["SetName-Body"]}>
<div className={styles["login-box"]}>
<form onSubmit={handleSubmit}>
<h2 className={styles["setname-h2"]}>Login</h2>
<div className={styles["input-box"]}>
<span className={styles["icon"]}>
<MdPerson />
</span>
<input
required
value={userName}
onChange={(e) => setUserName(e.target.value)} />
<label>Name</label>
</div>
<div className={styles["input-box"]}>
<span className={styles["icon"]}>
<MdLock />
</span>
<input
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)} />
<label>Password</label>
</div>
<div>
<button className={styles["SetName-button"]} type="submit">Login in</button>
</div>
<div className={styles["register_link"]}>
<p>Don't have an account?<Link href="/Register">Register</Link></p>
</div>
</form>
</div>
</div>
);
}
:root {
/* 字体家族 */
--font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans',
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
/* 字体大小 */
--font-size-base: 13px;
--font-size-sm: 12px;
/* 字体粗细 */
--font-weight-normal: 400;
--font-weight-medium: 500;
/* 行高 */
--line-height-title: 1.5;
--line-height-body: 1.4;
/* 颜色变量 - 基础灰色 */
--gray1: hsl(0, 0%, 99%);
--gray2: hsl(0, 0%, 97.3%);
--gray3: hsl(0, 0%, 95.1%);
--gray4: hsl(0, 0%, 93%);
--gray5: hsl(0, 0%, 90.9%);
--gray6: hsl(0, 0%, 88.7%);
--gray7: hsl(0, 0%, 85.8%);
--gray8: hsl(0, 0%, 78%);
--gray9: hsl(0, 0%, 56.1%);
--gray10: hsl(0, 0%, 52.3%);
--gray11: hsl(0, 0%, 43.5%);
--gray12: hsl(0, 0%, 9%);
/* 主题颜色 */
--normal-text: var(--gray12);
--success-text: hsl(140, 100%, 27%);
}
/* 全局字体应用 */
body {
margin: 0;
font-family: var(--font-family);
font-size: var(--font-size-base);
line-height: var(--line-height-body);
font-weight: var(--font-weight-normal);
color: var(--normal-text);
}
/* 标题样式 */
h1, h2, h3, h4, h5, h6,
.title {
font-weight: var(--font-weight-medium);
line-height: var(--line-height-title);
}
/* 辅助样式类 */
.text-sm {
font-size: var(--font-size-sm);
}
.text-medium {
font-weight: var(--font-weight-medium);
}
.text-success {
color: var(--success-text);
}
/* 响应式字体大小(可选) */
@media (max-width: 600px) {
:root {
--font-size-base: 12px;
--font-size-sm: 11px;
}
}
.context-menu {
/* From Uiverse.io by andrew-demchenk0 */
.button {
--main-focus: #2d8cf0;
--font-color: #323232;
--bg-color-sub: #dedede;
--bg-color: #eee;
--main-color: #323232;
position: relative;
width: 150px;
height: 40px;
cursor: pointer;
display: flex;
align-items: center;
border: 2px solid var(--main-color);
box-shadow: 4px 4px var(--main-color);
background-color: var(--bg-color);
border-radius: 10px;
overflow: hidden;
}
.button, .button__icon, .button__text {
transition: all 0.3s;
}
.button .button__text {
transform: translateX(33px);
color: var(--font-color);
font-weight: 600;
}
.button .button__icon {
position: absolute;
transform: translateX(109px);
height: 100%;
width: 39px;
background-color: var(--bg-color-sub);
display: flex;
align-items: center;
justify-content: center;
}
.button .svg {
width: 20px;
fill: var(--main-color);
}
.button:hover {
background: var(--bg-color);
}
.button:hover .button__text {
color: transparent;
}
.button:hover .button__icon {
width: 148px;
transform: translateX(0);
}
.button:active {
transform: translate(3px, 3px);
box-shadow: 0px 0px var(--main-color);
}
}
.root {
display: flex;
position: relative;
}
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment