Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
健杭 徐
intern_project_frontend_backend
Compare Revisions
646b90d2641224d0cb3e5fcdb0f0436c9d2efcf6...78c35070b2d60985ee1e2c90e2bf13f7477fe090
Commits (5)
finish
· 50dc23f5
健杭 徐
authored
Jun 27, 2025
50dc23f5
finish
· c48b7d0f
健杭 徐
authored
Jun 27, 2025
c48b7d0f
finish all
· 2be90194
健杭 徐
authored
Aug 27, 2025
2be90194
111
· 4ab1ada5
健杭 徐
authored
Aug 27, 2025
4ab1ada5
docker
· 78c35070
健杭 徐
authored
Aug 28, 2025
78c35070
Show whitespace changes
Inline
Side-by-side
comment-system/handlers/handlers.go
View file @
78c35070
...
...
@@ -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"
,
...
...
comment-system/main.go
View file @
78c35070
...
...
@@ -81,7 +81,6 @@ func main() {
log
.
Fatal
(
router
.
Run
(
":8080"
))
}
// 新增表创建函数
func
createTable
()
{
query
:=
`
CREATE TABLE IF NOT EXISTS comments (
...
...
comment-system/static/index.html
View file @
78c35070
...
...
@@ -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>
\ No newline at end of file
comment-system/static/src/App.tsx
View file @
78c35070
import
{
format
}
from
"
date-f
ns
"
;
import
{
COMMENT_ADD
}
from
"
./assets/co
ns
t
"
;
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
)
{
onSuccess
();
}
catch
(
error
)
{
console
.
error
(
'
添加评论失败:
'
,
error
);
}
;
}
}
export
default
AddComment
\ No newline at end of file
export
default
AddComment
;
\ No newline at end of file
comment-system/static/src/CommentSection.tsx
0 → 100644
View file @
78c35070
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
}
>
«
Last
</
button
>
<
span
>
{
totalComments
>
0
?
`Page
${
currentPage
}
/
${
totalPages
}
`
:
'
No Comment Yet
'
}
</
span
>
<
button
onClick
=
{
()
=>
setCurrentPage
(
prev
=>
prev
+
1
)
}
disabled
=
{
currentPage
>=
totalPages
}
>
Next
»
</
button
>
</
div
>
</
div
>
);
}
\ No newline at end of file
comment-system/static/src/assets/const.tsx
0 → 100644
View file @
78c35070
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
comment-system/static/src/index.css
View file @
78c35070
...
...
@@ -540,3 +540,42 @@ 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
comment-system/static/src/load.js
View file @
78c35070
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
);
...
...
@@ -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">« 上一页</button>
<span id="page-info"></span>
<button id="next-button">下一页 »</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
comment-system/static/src/main.tsx
View file @
78c35070
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
(
function
App
()
{
const
[
refreshTrigger
,
setRefreshTrigger
]
=
useState
(
0
);
const
handleCommentAdded
=
()
=>
{
setRefreshTrigger
(
prev
=>
prev
+
1
);
};
return
(
<
StrictMode
>
<
div
>
<
h1
style
=
{
{
textAlign
:
'
center
'
}
}
>
Welcome To Xanadu`s Comment
</
h1
>
</
div
>
<
Input_Area
/>
<
div
className
=
'comment'
>
<
div
>
<
h3
style
=
{
{
textAlign
:
'
center
'
}
}
>
All Comment
</
h3
>
</
div
>
<
ul
id
=
'commentlist'
></
ul
>
<
h1
style
=
{
{
textAlign
:
'
center
'
}
}
>
Welcome To Xanadu`s Comment
</
h1
>
</
div
>
<
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%
'
}
}
/>
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
myproject/chatroom/Dockerfile
0 → 100644
View file @
78c35070
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"]
myproject/chatroom/docker-compose.yml
0 → 100644
View file @
78c35070
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
:
myproject/chatroom/main.go
View file @
78c35070
...
...
@@ -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
(
"room
T
d"
)
))
if
err
!=
nil
||
roomId
<
=
0
{
c
.
JSON
(
http
.
StatusOK
,
Response
{
Code
:
400
,
Msg
:
"
Invalid ID"
})
roomId
:=
c
.
Query
(
"room
I
d"
)
if
roomId
=
=
""
{
c
.
JSON
(
400
,
Response
{
Code
:
400
,
Msg
:
"
Room ID is required"
,
Data
:
nil
})
return
}
result
,
err
:=
db
.
Exec
(
"DELETE FROM
room
s WHERE room
I
d = $1"
,
roomId
)
_
,
err
:=
db
.
Exec
(
"DELETE FROM
message
s WHERE room
_i
d = $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
:
4
00
,
Msg
:
"
Room not found"
})
_
,
err_
:=
db
.
Exec
(
"DELETE FROM rooms WHERE room_id = $1"
,
roomId
)
if
err_
!=
nil
{
c
.
JSON
(
500
,
Response
{
Code
:
5
00
,
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
{
...
...
myproject/my-app/Dockerfile
0 → 100644
View file @
78c35070
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"]
myproject/my-app/next.config.ts
View file @
78c35070
import
type
{
NextConfig
}
from
"
next
"
;
const
nextConfig
:
NextConfig
=
{
/* config options here */
output
:
'
standalone
'
,
};
export
default
nextConfig
;
myproject/my-app/src/app/
pages/
ChatRoom/ChatRoom.css
→
myproject/my-app/src/app/ChatRoom/ChatRoom.
module.
css
View file @
78c35070
...
...
@@ -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
;
...
...
myproject/my-app/src/app/
pages/
ChatRoom/
ChatRoom
.tsx
→
myproject/my-app/src/app/ChatRoom/
page
.tsx
View file @
78c35070
'
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
;
const
openDiv
=
document
.
getElementsByClassName
(
styles
.
open
)[
0
]
as
HTMLDivElement
|
undefined
;
if
(
openDiv
)
{
openDiv
.
style
.
zIndex
=
"
1000
"
;
const
roomNameInput
=
document
.
getElementsByClassName
(
"
RoomNameInput
"
)[
0
]
as
HTMLInputElement
;
}
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
;
const
openDiv
=
document
.
getElementsByClassName
(
styles
.
open
)[
0
]
as
HTMLDivElement
|
undefined
;
if
(
openDiv
)
{
openDiv
.
style
.
zIndex
=
"
0
"
;
const
roomNameInput
=
document
.
getElementsByClassName
(
"
RoomNameInput
"
)[
0
]
as
HTMLInputElement
;
}
const
roomNameInput
=
document
.
getElementsByClassName
(
styles
.
RoomNameInput
)[
0
]
as
HTMLInputElement
|
undefined
;
if
(
roomNameInput
)
{
roomNameInput
.
style
.
zIndex
=
"
0
"
;
(
document
.
getElementsByClassName
(
"
RoomNameInput
"
)[
0
]
as
HTMLInputElement
).
value
=
''
;
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,30 +185,30 @@ 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"
>
<
div
className
=
{
styles
[
"
message-input
"
]
}
>
<
input
type
=
"text"
placeholder
=
"Type a message..."
className
=
"Inputarea"
className
=
{
styles
[
"
Inputarea
"
]
}
value
=
{
inputValue
}
onChange
=
{
(
e
)
=>
setInputValue
(
e
.
target
.
value
)
}
onKeyUpCapture
=
{
...
...
@@ -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,11 +242,13 @@ function MessageItem (props: MessageProps & { onAddNewComment: (content: string)
// From Uiverse.io by adamgiebl
}
export
function
ChatRoom
({
userName
}:
{
userName
:
string
})
{
function
ChatRoom
Component
({
userName
}:
{
userName
:
string
})
{
const
[
rooms
,
setRooms
]
=
useState
<
RoomEntryProps
[]
>
([]);
const
[
currentRoom
,
setCurrentRoom
]
=
useState
<
MessageProps
|
null
>
(
null
);
useEffect
(()
=>
{
const
ROOM_LIST_REFRESH_INTERVAL
=
1000
;
const
MESSAGE_REFRESH_INTERVAL
=
1000
;
const
fetchRooms
=
async
()
=>
{
try
{
const
response
=
await
fetch
(
backEnd
+
"
/room/list
"
);
...
...
@@ -215,39 +265,57 @@ export function ChatRoom({ userName }: { userName: string }) {
}
catch
(
error
)
{
console
.
error
(
"
Error fetching rooms:
"
,
error
);
}
}
fetchRooms
();
},
[]);
};
const
handleRoomClick
=
async
(
roomId
:
number
,
roomName
:
string
)
=>
{
setCurrentRoom
({
roomId
:
roomId
,
roomName
:
roomName
,
messages
:
[]
});
const
fetchCurrentRoomMessages
=
async
(
roomId
:
number
)
=>
{
if
(
!
roomId
)
return
;
try
{
const
response
=
await
fetch
(
backEnd
+
`/room/message/list?roomId=
${
roomId
}
`
)
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
,
setCurrentRoom
(
prev
=>
{
if
(
!
prev
||
prev
.
roomId
!==
roomId
)
return
prev
;
return
{
...
prev
,
messages
:
result
.
data
||
[]
};
});
}
else
{
alert
(
`Error fetching messages:
${
result
.
msg
}
`
);
}
}
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
:
[]
});
}
}
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
||
""
}
...
...
@@ -347,3 +457,23 @@ export function ChatRoom({ userName }: { userName: string }) {
</
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
myproject/my-app/src/app/
pages/SetName/SetNam
e.css
→
myproject/my-app/src/app/
Register/Register.modul
e.css
View file @
78c35070
*
/
*
{
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
myproject/my-app/src/app/Register/page.tsx
0 → 100644
View file @
78c35070
'
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
myproject/my-app/src/app/SetName/SetName.module.css
0 → 100644
View file @
78c35070
/*
{
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
myproject/my-app/src/app/SetName/page.tsx
0 → 100644
View file @
78c35070
'
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
>
);
}
Prev
1
2
Next