Commits (5)
......@@ -29,74 +29,46 @@ type Response struct {
// 获取评论
func GetCommentsHandler(c *gin.Context) {
// 参数验证
page, err := strconv.Atoi(c.DefaultQuery("page", "1"))
if err != nil || page < 1 {
pageStr := c.DefaultQuery("page", "1")
sizeStr := c.DefaultQuery("size", "10")
page, _ := strconv.Atoi(pageStr)
size, _ := strconv.Atoi(sizeStr)
if page < 1 {
page = 1
}
size, err := strconv.Atoi(c.DefaultQuery("size", "10"))
if err != nil || size < -1 {
size = 10
}
comments := make([]Comment, 0)
var total int64
// 查询总数
var total int
countQuery := "SELECT COUNT(*) FROM comments"
err = db.QueryRow(countQuery).Scan(&total)
// 统计总数
err := db.QueryRow("SELECT COUNT(*) FROM comments").Scan(&total)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "查询总数失败: " + err.Error(),
})
c.JSON(http.StatusOK, Response{Code: 500, Msg: err.Error()})
return
}
// 构建查询
query := "SELECT id, name, content, TO_CHAR(created_at, 'YYYY-MM-DD HH24:MI') AS created_at FROM comments"
args := []interface{}{}
// 始终排序
query += " ORDER BY created_at DESC"
// 分页处理
var rows *sql.Rows
if size != -1 {
offset := (page - 1) * size
query += " LIMIT $1 OFFSET $2"
args = append(args, size, offset)
rows, err = db.Query("SELECT id, name, content, created_at FROM comments ORDER BY created_at ASC LIMIT $1 OFFSET $2", size, offset)
} else {
rows, err = db.Query("SELECT id, name, content, created_at FROM comments ORDER BY created_at ASC")
}
// 执行查询
rows, err := db.Query(query, args...)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "查询评论失败: " + err.Error(),
})
c.JSON(http.StatusOK, Response{Code: 500, Msg: err.Error()})
return
}
defer rows.Close()
// 处理结果
var comments []Comment
for rows.Next() {
var comment Comment
if err := rows.Scan(
&comment.ID,
&comment.Name,
&comment.Content,
&comment.CreatedAt,
); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "解析评论失败: " + err.Error(),
})
if err := rows.Scan(&comment.ID, &comment.Name, &comment.Content, &comment.CreatedAt); err != nil {
c.JSON(http.StatusOK, Response{Code: 500, Msg: err.Error()})
return
}
comments = append(comments, comment)
}
// 返回标准响应结构
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "success",
......
......@@ -81,7 +81,6 @@ func main() {
log.Fatal(router.Run(":8080"))
}
// 新增表创建函数
func createTable() {
query := `
CREATE TABLE IF NOT EXISTS comments (
......
......@@ -9,6 +9,5 @@
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script src="/src/load.js"></script>
</body>
</html>
</html>
\ No newline at end of file
import { format } from "date-fns";
import { COMMENT_ADD } from "./assets/const";
const currentTime = new Date();
const formattedTime = format(currentTime, 'yyyy-MM-dd HH:mm');
async function AddComment({name, profilePhoto}:{name: string, profilePhoto:string}) {
async function AddComment({ name, onSuccess }: { name: string; onSuccess: () => void; }) {
const textInput = document.getElementById('textInput') as HTMLInputElement;
const content = textInput.value;
if (content === '')
{
alert('Text input cannot be empty')
return
if (content === '') {
alert('Text input cannot be empty');
return;
}
if (name === '')
{
name = '小黑子'
if (name === '') {
name = '小黑子';
}
try {
const response = await fetch('http://localhost:8080/comment/add', {
const response = await fetch(COMMENT_ADD, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, content, created_at: formattedTime})
body: JSON.stringify({ name, content })
});
const result = await response.json();
if (result.code !== 0)
{
if (result.code !== 0) {
alert(`操作失败: ${result.msg}`);
throw new Error(result.msg);
}
const newComment = result.data;
const NewCommentPart = document.createElement('div')
const UserPhoto = document.createElement('img')
UserPhoto.className = 'comment-avatar'
UserPhoto.src = profilePhoto
UserPhoto.alt = name
UserPhoto.style.width = '30px'
UserPhoto.style.height = '30px'
UserPhoto.style.borderRadius = '50%'
NewCommentPart.appendChild(UserPhoto)
const UserCommentContent = document.createElement('div')
UserCommentContent.className = 'comment-content'
const UserName = document.createElement('span')
UserName.textContent = name
UserName.className = 'comment-meta'
UserCommentContent.appendChild(UserName)
const UserDate = document.createElement('span')
UserDate.textContent = formattedTime
UserDate.className = 'comment-date'
UserCommentContent.appendChild(UserDate)
const UserComment = document.createElement('p')
UserComment.textContent = textInput.value
UserComment.style.marginLeft = '10px'
textInput.value = '';
(document.getElementById('nameInput') as HTMLInputElement).value = '';
UserCommentContent.appendChild(UserComment)
const DeleteButtonArea = document.createElement('div')
DeleteButtonArea.className = 'comment-delete'
const DeleteButton = document.createElement('button')
DeleteButton.textContent = 'Delete'
DeleteButton.id = 'deleteButton'
DeleteButton.onclick = async () => {
const commentItem = DeleteButton.closest('.comment-item')
if (commentItem) {
commentItem.remove()
try {
await fetch(`http://localhost:8080/comment/delete?id=${newComment.id}`, {
method: 'POST'
});
commentItem.remove();
} catch {
alert('删除失败:');
}
}
}
DeleteButtonArea.appendChild(DeleteButton)
UserCommentContent.appendChild(DeleteButtonArea)
NewCommentPart.appendChild(UserCommentContent)
const NewComment = document.createElement('li')
NewComment.className = 'comment-item'
NewComment.style.listStyle = 'none'
NewComment.appendChild(NewCommentPart)
const CommentList = document.getElementById('commentlist') as HTMLUListElement
CommentList.appendChild(NewComment)
}
catch (error) {
textInput.value = '';
(document.getElementById('nameInput') as HTMLInputElement).value = '';
onSuccess();
} catch (error) {
console.error('添加评论失败:', error);
};
}
}
export default AddComment
\ No newline at end of file
export default AddComment;
\ No newline at end of file
import { useState, useEffect, useRef } from 'react';
import { COMMENT_GET, COMMENT_DEL } from './assets/const'
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef<() => void>(undefined);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
if (savedCallback.current) {
savedCallback.current();
}
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
interface Comment {
id: number;
name: string;
content: string;
created_at: string;
}
const pageSize = 5;
const POLLING_INTERVAL = 1000;
export function CommentSection({ refreshTrigger }: { refreshTrigger: number }) {
const [comments, setComments] = useState<Comment[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalComments, setTotalComments] = useState(0);
const loadComments = async (page: number) => {
try {
const response = await fetch(COMMENT_GET + `?page=${page}&size=${pageSize}`);
const result = await response.json();
if (result.code !== 0) throw new Error(result.msg);
setComments(result.data.comments);
setTotalComments(result.data.total);
} catch (error) {
console.error('加载评论错误:', error);
}
};
const deleteComment = async (commentId: number) => {
if (!confirm('确定要删除这条评论吗?'))
return;
try {
const response = await fetch(COMMENT_DEL + `?id=${commentId}`, {
method: 'POST'
});
const result = await response.json();
if (result.code !== 0) throw new Error(result.msg);
alert('评论已删除');
loadComments(currentPage);
} catch (error) {
console.error('删除评论错误:', error);
alert('删除评论失败: ' + (error as Error).message);
}
};
useEffect(() => {
loadComments(currentPage);
}, [currentPage, refreshTrigger]);
useInterval(() => {
loadComments(currentPage);
}, POLLING_INTERVAL);
const totalPages = Math.ceil(totalComments / pageSize);
return (
<div className="comment">
<div>
<h3 style={{ textAlign: 'center' }}>All Comment</h3>
</div>
<ul id="commentlist">
{comments.length > 0 ? (
comments.map(comment => (
<li key={comment.id} className="comment-item">
<div>
<img src='https://pic4.zhimg.com/v2-bf5f58e7b583cd69ac228db9fdff377f_r.jpg'
className='comment-avatar' alt={comment.name} />
<div className='comment-content'>
<div>
<span className='comment-meta'>{comment.name}</span>
<span className='comment-date'>{new Date(comment.created_at).toLocaleString()}</span>
</div>
<p>{comment.content}</p>
<div className='comment-delete'>
<button id='deleteButton' onClick={() => deleteComment(comment.id)}>Delete</button>
</div>
</div>
</div>
</li>
))
) : (
<div className="no-comments">No Comment Yet</div>
)}
</ul>
<div id="pagination-controls" className="pagination-controls">
<button onClick={() => setCurrentPage(prev => prev - 1)} disabled={currentPage <= 1}>
&laquo; Last
</button>
<span>
{totalComments > 0 ? `Page ${currentPage} / ${totalPages}` : 'No Comment Yet'}
</span>
<button onClick={() => setCurrentPage(prev => prev + 1)} disabled={currentPage >= totalPages}>
Next &raquo;
</button>
</div>
</div>
);
}
\ No newline at end of file
const IP: string = 'http://10.197.150.54:8080'
export const COMMENT_ADD: string = IP + '/comment/add'
export const COMMENT_GET: string = IP + '/comment/get'
export const COMMENT_DEL: string = IP + '/comment/delete'
\ No newline at end of file
......@@ -539,4 +539,43 @@ h3::after {
::-webkit-scrollbar-thumb:hover {
background: var(--primary-color);
}
.pagination-controls {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
gap: 15px;
}
.pagination-controls button {
padding: 8px 16px;
border: 1px solid #ddd;
background-color: #f8f8f8;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s;
}
.pagination-controls button:hover:not(:disabled) {
background-color: #e2e2e2;
}
.pagination-controls button:disabled {
cursor: not-allowed;
color: #ccc;
background-color: #fdfdfd;
}
#page-info {
font-size: 14px;
color: #555;
}
.no-comments {
text-align: center;
color: #888;
padding: 20px;
font-style: italic;
}
\ No newline at end of file
async function loadComments() {
let currentPage = 1;
const pageSize = 5;
let totalComments = 0;
async function loadComments(page) {
currentPage = page;
try {
const response = await fetch('http://localhost:8080/comment/get');
const response = await fetch(`http://localhost:8080/comment/get?page=${page}&size=${pageSize}`);
const result = await response.json();
// 检查业务状态码
if (result.code !== 0) {
throw new Error(result.msg || '未知错误');
}
// 正确的数据结构访问
totalComments = result.data.total;
displayComments(result.data.comments);
updatePaginationControls();
} catch (error) {
console.error('加载评论错误:', error);
alert('加载评论失败: ' + error.message);
......@@ -19,7 +25,7 @@ async function loadComments() {
function displayComments(comments) {
const commentsContainer = document.getElementById('commentlist');
commentsContainer.innerHTML = '';
if (!comments || comments.length === 0) {
commentsContainer.innerHTML = '<div class="no-comments">No Comments Yet</div>';
return;
......@@ -29,6 +35,8 @@ function displayComments(comments) {
const commentElement = document.createElement('li');
commentElement.className = 'comment-item';
const displayDate = new Date(comment.created_at).toLocaleString();
commentElement.innerHTML = `
<div>
<img src='https://pic4.zhimg.com/v2-bf5f58e7b583cd69ac228db9fdff377f_r.jpg'
......@@ -36,7 +44,7 @@ function displayComments(comments) {
<div class='comment-content'>
<div>
<span class='comment-meta'>${comment.name}</span>
<span class='comment-date'>${comment.created_at}</span>
<span class='comment-date'>${displayDate}</span>
</div>
<p>${comment.content}</p>
<div class='comment-delete'>
......@@ -63,7 +71,7 @@ function deleteComment(commentId) {
throw new Error(result.msg || '删除失败');
}
alert('评论已删除');
loadComments(); // 重新加载评论列表
loadComments(currentPage);
})
.catch(error => {
console.error('删除评论错误:', error);
......@@ -71,9 +79,50 @@ function deleteComment(commentId) {
});
}
function setupPagination() {
const paginationContainer = document.getElementById('pagination-controls');
if (!paginationContainer) return;
paginationContainer.innerHTML = `
<button id="prev-button">&laquo; 上一页</button>
<span id="page-info"></span>
<button id="next-button">下一页 &raquo;</button>
`;
document.getElementById('prev-button').addEventListener('click', () => {
if (currentPage > 1) {
loadComments(currentPage - 1);
}
});
document.getElementById('next-button').addEventListener('click', () => {
const totalPages = Math.ceil(totalComments / pageSize);
if (currentPage < totalPages) {
loadComments(currentPage + 1);
}
});
}
function updatePaginationControls() {
const prevButton = document.getElementById('prev-button');
const nextButton = document.getElementById('next-button');
const pageInfo = document.getElementById('page-info');
if (!prevButton || !nextButton || !pageInfo) return;
const totalPages = Math.ceil(totalComments / pageSize);
if (totalPages <= 0) {
pageInfo.textContent = '暂无评论';
} else {
pageInfo.textContent = `第 ${currentPage} / ${totalPages} 页 (共 ${totalComments} 条)`;
}
prevButton.disabled = currentPage <= 1;
nextButton.disabled = currentPage >= totalPages;
}
// 页面加载时调用
document.addEventListener('DOMContentLoaded', () => {
loadComments();
setupPagination();
loadComments(1);
});
\ No newline at end of file
import { StrictMode } from 'react'
import { StrictMode, useState } from 'react'
import { createRoot } from 'react-dom/client'
import AddComment from './App.tsx'
import DateTimeDisplay from './date.tsx'
import { CommentSection } from './CommentSection.tsx'
import './index.css'
const profilePhoto:string = 'https://pic4.zhimg.com/v2-bf5f58e7b583cd69ac228db9fdff377f_r.jpg'
const profilePhoto: string = 'https://pic4.zhimg.com/v2-bf5f58e7b583cd69ac228db9fdff377f_r.jpg'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<div>
<h1 style={{textAlign: 'center'}}>Welcome To Xanadu`s Comment</h1>
</div>
<Input_Area />
<div className='comment'>
function App() {
const [refreshTrigger, setRefreshTrigger] = useState(0);
const handleCommentAdded = () => {
setRefreshTrigger(prev => prev + 1);
};
return (
<StrictMode>
<div>
<h3 style={{textAlign: 'center'}}>
All Comment
</h3>
<h1 style={{ textAlign: 'center' }}>Welcome To Xanadu`s Comment</h1>
</div>
<ul id='commentlist'></ul>
</div>
</StrictMode>
)
<Input_Area onCommentAdded={handleCommentAdded} />
<CommentSection refreshTrigger={refreshTrigger} />
</StrictMode>
);
}
function Input_Area() {
function Input_Area({ onCommentAdded }: { onCommentAdded: () => void }) {
return (
<div className='inputarea'>
<img src={profilePhoto} alt={'小黑子'}
style={{width: '40px', height: '40px', borderRadius: '50%'}} />
<img src={profilePhoto} alt={'小黑子'}
style={{ width: '40px', height: '40px', borderRadius: '50%' }} />
<DateTimeDisplay />
<input type="text" placeholder="Please enter your name" id="nameInput" />
<input type="text" placeholder="Please follow the Community Guidelines" id="textInput" onKeyUpCapture={
(e) => {
const name:string = (document.getElementById('nameInput') as HTMLInputElement).value;
if (e.key === 'Enter') {
AddComment({name, profilePhoto});
const name: string = (document.getElementById('nameInput') as HTMLInputElement).value;
AddComment({ name, onSuccess: onCommentAdded });
}
}
}/>
} />
<button
id="submitButton"
onClick={() => AddComment({
name: (document.getElementById('nameInput') as HTMLInputElement).value,
profilePhoto: profilePhoto
})}
onClick={() => {
const name: string = (document.getElementById('nameInput') as HTMLInputElement).value;
AddComment({ name, onSuccess: onCommentAdded });
}}
>
Submit
</button>
......@@ -54,4 +56,4 @@ function Input_Area() {
)
}
// Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
\ No newline at end of file
createRoot(document.getElementById('root')!).render(<App />);
\ No newline at end of file
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:
......@@ -5,19 +5,29 @@ import (
"fmt"
"log"
"net/http"
"os"
"strconv"
"github.com/gin-gonic/gin"
_ "github.com/lib/pq"
)
const (
host = "localhost"
port = 5432
user = "chat_room_user"
password = "secure_password"
dbname = "chat_room_db"
)
// 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"`
......@@ -49,6 +59,12 @@ type RoomAddRes struct {
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)
......@@ -98,15 +114,56 @@ func main() {
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 {
......@@ -186,27 +243,27 @@ func GetRoomList(c *gin.Context) {
}
func DeleteRoom(c *gin.Context) {
roomId, err := strconv.Atoi((c.Query("roomTd")))
if err != nil || roomId <= 0 {
c.JSON(http.StatusOK, Response{Code: 400, Msg: "Invalid ID"})
roomId := c.Query("roomId")
if roomId == "" {
c.JSON(400, Response{Code: 400, Msg: "Room ID is required", Data: nil})
return
}
result, err := db.Exec("DELETE FROM rooms WHERE roomId = $1", roomId)
_, err := db.Exec("DELETE FROM messages WHERE room_id = $1", roomId)
if err != nil {
c.JSON(http.StatusOK, Response{Code: 500, Msg: err.Error()})
c.JSON(500, Response{Code: 500, Msg: "Failed to delete messages in room: " + err.Error(), Data: nil})
return
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
c.JSON(http.StatusOK, Response{Code: 400, Msg: "Room not found"})
_, 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(http.StatusOK, Response{
c.JSON(200, Response{
Code: 0,
Msg: "Room deleted",
Msg: "Room deleted successfully",
Data: nil,
})
}
......@@ -287,6 +344,34 @@ func GetMessageList(c *gin.Context) {
})
}
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) {
// 处理更新房间消息的逻辑
}
......@@ -310,6 +395,12 @@ func createTable() {
"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 {
......
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"]
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
output: 'standalone',
};
export default nextConfig;
......@@ -100,6 +100,7 @@
margin-bottom: 10px;
border-radius: 8px;
cursor: pointer;
border: 1px solid #ddd;
&:hover {
background-color: #e0e0e0;
......@@ -150,6 +151,53 @@
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;
}
}
}
......@@ -161,7 +209,7 @@
display: flex;
flex-direction: column;
box-sizing: border-box;
background-image: url('../../public/backGround.jpg');
background-color: #f0f0f0;
background-size: cover;
z-index: 2;
......
'use client';
import "./ChatRoom.css";
import styles from "./ChatRoom.module.css";
import React, { useEffect, useState } from "react";
import { useRouter } from 'next/navigation';
const backEnd:string = "http://localhost:8080";
......@@ -33,27 +34,66 @@ interface MessageProps {
}>;
}
function RoomEntry ({rooms, onRoomClick} : {rooms: RoomEntryProps[], onRoomClick: (roomId: number, roomName: string) => void}) {
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="chat-room-nav">
<div className="sidebar-action">
<button type="button" className="button" onClick={openOpenDiv}>
<div className="button-top">New Chat</div>
<div className="button-bottom"></div>
<div className="button-base"></div>
<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="chat-list">
<div className={styles["chat-list"]}>
{rooms.map((room) => (
<div className="chat-item" key={room.roomId} onClick={() => onRoomClick(room.roomId, room.roomName)}>
<img src={RoomProfile} alt="Avatar" className="avatar" />
<div className="chat-info">
<h3>{room.roomName}</h3>
<span className="chat-message">
<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="chat-time">{room.lastTime.Valid ? formatTimeToHoursMinutes(room.lastTime.Time) : ''}</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>
))}
......@@ -65,8 +105,8 @@ function RoomEntry ({rooms, onRoomClick} : {rooms: RoomEntryProps[], onRoomClick
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'); // 确保两位数
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
}
......@@ -79,12 +119,12 @@ function InputRoomNameArea({ onAddNewRoom }: { onAddNewRoom: (roomName: string)
closeOpenDiv();
}
return (
<div className="open">
<div className="roomName-input">
<div className={styles["open"]}>
<div className={styles["roomName-input"]}>
<h3>Please Enter the New Room Name</h3>
<input
type="text"
className="RoomNameInput"
className={styles["RoomNameInput"]}
placeholder="Start new chat"
value = {roomNameInput}
onChange={(e) => setRoomNameInput(e.target.value)}
......@@ -97,9 +137,9 @@ function InputRoomNameArea({ onAddNewRoom }: { onAddNewRoom: (roomName: string)
}
}}
/>
<div className="button-container">
<button className="create-button" onClick={handleAddNewRoom}>Submit</button>
<button className="cancel-button" onClick={closeOpenDiv}>Cancel</button>
<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>
......@@ -107,25 +147,33 @@ function InputRoomNameArea({ onAddNewRoom }: { onAddNewRoom: (roomName: string)
}
function openOpenDiv() {
const openDiv = document.getElementsByClassName("open")[0] as HTMLDivElement;
openDiv.style.zIndex = "1000";
const roomNameInput = document.getElementsByClassName("RoomNameInput")[0] as HTMLInputElement;
roomNameInput.style.zIndex = "1001";
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("open")[0] as HTMLDivElement;
openDiv.style.zIndex = "0";
const roomNameInput = document.getElementsByClassName("RoomNameInput")[0] as HTMLInputElement;
roomNameInput.style.zIndex = "0";
(document.getElementsByClassName("RoomNameInput")[0] as HTMLInputElement).value = '';
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="message-item">Please select a room to chat.</div>;
return <div className={styles["message-item"]}>Please select a room to chat.</div>;
}
const handlerSend = () => {
......@@ -137,31 +185,31 @@ function MessageItem (props: MessageProps & { onAddNewComment: (content: string)
setInputValue('');
}
return (
<div className="message-item">
<div className="message-header">
<img src={RoomProfile} alt="Avatar" className="avatar" />
<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="message-list">
<div className={styles["message-list"]}>
{props.messages.map((msg, index) => (
<div key={index} className="message">
<img src={Profile[msg.profile]} alt={`${msg.sender}'s avatar`} className="avatar" />
<div className="message-content">
<div className="message-info">
<span className="message-sender">{msg.sender}</span>
<span className="message-time">{formatTimeToHoursMinutes(msg.time)}</span>
<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="message-text">{msg.content}</p>
<p className={styles["message-text"]}>{msg.content}</p>
</div>
</div>
))}
</div>
<div className="message-input">
<input
type="text"
placeholder="Type a message..."
className="Inputarea"
value={inputValue}
<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>) => {
......@@ -169,9 +217,9 @@ function MessageItem (props: MessageProps & { onAddNewComment: (content: string)
handlerSend();
}
}}/>
<button className="send-button" onClick={handlerSend}>
<div className="svg-wrapper-1">
<div className="svg-wrapper">
<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"
......@@ -194,31 +242,72 @@ function MessageItem (props: MessageProps & { onAddNewComment: (content: string)
// From Uiverse.io by adamgiebl
}
export function ChatRoom({ userName }: { userName: string }) {
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;
useEffect(() => {
const fetchRooms = async () => {
try {
const response = await fetch(backEnd+"/room/list");
const result = await response.json();
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) {
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);
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,
......@@ -226,28 +315,7 @@ export function ChatRoom({ userName }: { userName: string }) {
messages: []
});
try {
const response = await fetch(backEnd+`/room/message/list?roomId=${roomId}`)
const result = await response.json();
debugger;
if (result.code === 0) {
setCurrentRoom({
roomId: roomId,
roomName: roomName,
messages: result.data || []
});
} else {
alert(`Error fetching messages: ${result.msg}`);
setCurrentRoom({
roomId: roomId,
roomName: roomName,
messages: []
});
}
} catch (error){
console.error("Error fetching messages:", error);
alert("Failed to fetch messages. See console for details.");
}
await fetchCurrentRoomMessages(roomId);
}
async function addNewRoom(roomName: string) {
......@@ -334,9 +402,51 @@ export function ChatRoom({ userName }: { userName: string }) {
}
}
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="chat-room">
<RoomEntry rooms={rooms} onRoomClick={handleRoomClick}/>
<div className={styles["chat-room"]}>
<RoomEntry rooms={rooms} onRoomClick={handleRoomClick} onRename={handleRename} onDelete={handleDelete}/>
<MessageItem
roomId={currentRoom?.roomId || 0}
roomName={currentRoom?.roomName || ""}
......@@ -346,4 +456,24 @@ export function ChatRoom({ userName }: { userName: string }) {
<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
{
......@@ -16,7 +16,7 @@
min-height: 100vh;
width: 100vw;
background-size: cover;
background: url('../../public/SetbackGround.jpg') no-repeat center;
background-color: grey;
}
.login-box
......@@ -33,7 +33,7 @@
backdrop-filter: blur(15px);
}
h2
.setname-h2
{
font-size: 2em;
color: white;
......@@ -73,7 +73,7 @@ h2
border: none;
outline: none;
background-color: transparent;
color: white;
color: black;
font-size: 1em;
padding: 0 35px 0 5px;
}
......@@ -105,3 +105,10 @@ h2
{
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>
);
}