Commit 953586b9 authored by 健杭 徐's avatar 健杭 徐
Browse files

src

parents
/*
{
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;
font-size: 0.9em;
}
.input-box label
{
position: absolute;
top: 50%;
left: 5px;
transform: translateY(-50%);
font-size: 0.9em;
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: 25px;
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;
bottom: 8px;
}
.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 "../login.module.css";
import { MdLock, MdPerson } from "react-icons/md";
import { useState, useEffect } from "react";
import Link from "next/link";
import { useRouter } from 'next/navigation';
export default function Register() {
const [userName, setUserName] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const router = useRouter();
useEffect(() => {
document.title = 'Register | Chat Room App';
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError('');
if (password !== confirmPassword) {
setError('Passwords do not match');
setIsLoading(false);
return;
}
if (password.length < 6) {
setError('Password must be at least 6 characters');
setIsLoading(false);
return;
}
try {
const response = await fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ userName, password }),
});
const data = await response.json();
if (data.code === 0) {
alert('Registration successful! Please login.');
router.push('/');
} else {
setError(data.msg || 'Registration failed');
}
} catch (error) {
console.error('Error registering:', error);
setError('Network error. Please try again.');
} finally {
setIsLoading(false);
}
};
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)}
disabled={isLoading}
/>
<label>Username</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)}
disabled={isLoading}
/>
<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)}
disabled={isLoading}
/>
<label>Confirm Password</label>
</div>
{error && (
<div style={{
color: 'red',
fontSize: '14px',
marginTop: '10px',
textAlign: 'center'
}}>
{error}
</div>
)}
<div>
<button
className={styles["SetName-button"]}
type="submit"
disabled={isLoading}
>
{isLoading ? 'Registering...' : 'Register'}
</button>
</div>
<div className={styles["register_link"]}>
<p>Already have an account? <Link href="/">Login</Link></p>
</div>
</form>
</div>
</div>
);
}
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '../../lib/prisma';
import bcrypt from 'bcryptjs';
export async function POST(request: NextRequest) {
try {
const { userName, password } = await request.json();
if (!userName || !password) {
return NextResponse.json(
{ code: 1, msg: 'Username and password are required' },
{ status: 400 }
);
}
const user = await prisma.user.findUnique({
where: {
userName: userName,
},
});
if (!user) {
return NextResponse.json(
{ code: 1, msg: 'Invalid username or password' },
{ status: 401 }
);
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return NextResponse.json(
{ code: 1, msg: 'Invalid username or password' },
{ status: 401 }
);
}
return NextResponse.json({
code: 0,
msg: 'Login successful',
data: {
userName: user.userName,
token: `token_${Date.now()}`
}
});
} catch (error) {
console.error('Login error:', error);
return NextResponse.json(
{ code: 1, msg: 'Internal server error' },
{ status: 500 }
);
}
}
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '../../../lib/prisma';
export async function POST(request: NextRequest) {
try {
const { roomId, userName, content } = await request.json();
if (!roomId || !userName || !content) {
return NextResponse.json(
{ code: 1, msg: 'Room ID, username, and content are required' },
{ status: 400 }
);
}
const user = await prisma.user.findUnique({
where: {
userName: userName
}
});
if (!user) {
return NextResponse.json(
{ code: 1, msg: 'User not found' },
{ status: 404 }
);
}
const message = await prisma.message.create({
data: {
content: content.trim(),
senderId: user.id,
roomId: parseInt(roomId)
},
include: {
sender: {
select: {
userName: true
}
}
}
});
await prisma.room.update({
where: {
id: parseInt(roomId)
},
data: {
updatedAt: new Date()
}
});
return NextResponse.json({
code: 0,
msg: 'Message sent successfully',
data: {
id: message.id,
content: message.content,
sender: message.sender.userName,
time: message.createdAt.toISOString()
}
});
} catch (error) {
console.error('Send message error:', error);
return NextResponse.json(
{ code: 1, msg: 'Internal server error' },
{ status: 500 }
);
}
}
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '../../../lib/prisma';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const roomId = searchParams.get('roomId');
if (!roomId) {
return NextResponse.json(
{ code: 1, msg: 'Room ID is required' },
{ status: 400 }
);
}
const messages = await prisma.message.findMany({
where: {
roomId: parseInt(roomId)
},
include: {
sender: {
select: {
userName: true
}
}
},
orderBy: {
createdAt: 'asc'
}
});
const formattedMessages = messages.map((message, index) => ({
profile: index % 6,
sender: message.sender.userName,
content: message.content,
time: message.createdAt.toString()
}));
return NextResponse.json({
code: 0,
data: formattedMessages
});
} catch (error) {
console.error('Get messages error:', error);
return NextResponse.json(
{ code: 1, msg: 'Internal server error' },
{ status: 500 }
);
}
}
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '../../lib/prisma';
import bcrypt from 'bcryptjs';
export async function POST(request: NextRequest) {
try {
const { userName, password } = await request.json();
if (!userName || !password) {
return NextResponse.json(
{ code: 1, msg: 'Username and password are required' },
{ status: 400 }
);
}
if (userName.length < 3) {
return NextResponse.json(
{ code: 1, msg: 'Username must be at least 3 characters' },
{ status: 400 }
);
}
if (password.length < 6) {
return NextResponse.json(
{ code: 1, msg: 'Password must be at least 6 characters' },
{ status: 400 }
);
}
const existingUser = await prisma.user.findUnique({
where: {
userName: userName,
},
});
if (existingUser) {
return NextResponse.json(
{ code: 1, msg: 'Username already exists' },
{ status: 409 }
);
}
const hashedPassword = await bcrypt.hash(password, 12);
const newUser = await prisma.user.create({
data: {
userName: userName,
password: hashedPassword,
},
});
return NextResponse.json({
code: 0,
msg: 'Registration successful',
data: {
userName: newUser.userName,
registeredAt: new Date().toISOString()
}
});
} catch (error) {
console.error('Registration error:', error);
return NextResponse.json(
{ code: 1, msg: 'Internal server error' },
{ status: 500 }
);
}
}
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '../../../lib/prisma';
export async function POST(request: NextRequest) {
try {
const { name } = await request.json();
if (!name || name.trim() === '') {
return NextResponse.json(
{ code: 1, msg: 'Room name is required' },
{ status: 400 }
);
}
const room = await prisma.room.create({
data: {
name: name.trim(),
},
});
return NextResponse.json({
code: 0,
msg: 'Room created successfully',
data: {
roomId: room.id,
roomName: room.name
}
});
} catch (error) {
console.error('Create room error:', error);
return NextResponse.json(
{ code: 1, msg: 'Internal server error' },
{ status: 500 }
);
}
}
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '../../../lib/prisma';
export async function DELETE(request: NextRequest) {
try {
const { roomId } = await request.json();
if (!roomId) {
return NextResponse.json(
{ code: 1, msg: 'Room ID is required' },
{ status: 400 }
);
}
const room = await prisma.room.findUnique({
where: { id: roomId }
});
if (!room) {
return NextResponse.json(
{ code: 1, msg: 'Room not found' },
{ status: 404 }
);
}
await prisma.room.delete({
where: { id: roomId }
});
return NextResponse.json({
code: 0,
msg: 'Room deleted successfully'
});
} catch (error) {
console.error('Delete room error:', error);
return NextResponse.json(
{ code: 1, msg: 'Internal server error' },
{ status: 500 }
);
}
}
import { NextResponse } from 'next/server';
import { prisma } from '../../../lib/prisma';
export async function GET() {
try {
const rooms = await prisma.room.findMany({
include: {
messages: {
orderBy: {
createdAt: 'desc'
},
take: 1,
include: {
sender: {
select: {
userName: true
}
}
}
}
},
orderBy: {
updatedAt: 'desc'
}
});
const formattedRooms = rooms.map(room => ({
roomId: room.id,
roomName: room.name,
lastSender: room.messages[0]?.sender ? {
String: room.messages[0].sender.userName,
Valid: true
} : { String: '', Valid: false },
lastContent: room.messages[0]?.content ? {
String: room.messages[0].content,
Valid: true
} : { String: '', Valid: false },
lastTime: room.messages[0]?.createdAt ? {
Time: room.messages[0].createdAt.toString(),
Valid: true
} : { Time: '', Valid: false }
}));
return NextResponse.json({
code: 0,
data: formattedRooms
});
} catch (error) {
console.error('Get rooms error:', error);
return NextResponse.json(
{ code: 1, msg: 'Internal server error' },
{ status: 500 }
);
}
}
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '../../../lib/prisma';
export async function PATCH(request: NextRequest) {
try {
const { roomId, newName } = await request.json();
if (!roomId || !newName) {
return NextResponse.json(
{ code: 1, msg: 'Room ID and new name are required' },
{ status: 400 }
);
}
const room = await prisma.room.findUnique({
where: { id: roomId }
});
if (!room) {
return NextResponse.json(
{ code: 1, msg: 'Room not found' },
{ status: 404 }
);
}
const updatedRoom = await prisma.room.update({
where: { id: roomId },
data: { name: newName }
});
return NextResponse.json({
code: 0,
msg: 'Room renamed successfully',
data: {
roomId: updatedRoom.id,
roomName: updatedRoom.name
}
});
} catch (error) {
console.error('Rename room error:', error);
return NextResponse.json(
{ code: 1, msg: 'Internal server error' },
{ status: 500 }
);
}
}
.chat-room {
display: flex;
position: fixed;
width: 100vw;
height: 100vh;
box-sizing: border-box;
overflow: hidden;
}
.chat-room-nav {
position: fixed;
padding: 10px;
width: 15%;
height: 100%;
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
z-index: 10;
}
.open {
display: flex;
position: fixed;
width: 100vw;
height: 100vh;
box-sizing: border-box;
overflow: hidden;
background-color: rgba(255, 255, 255, 0.75);
z-index: 0;
}
.open .roomName-input {
background-color: #fff;
display: flex;
flex-direction: column;
position: fixed;
top: 40%;
left: 30%;
z-index: 0;
width: 40%;
height: 25%;
border-radius: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
h3 {
align-items: center;
text-align: center;
}
input {
width: 80%;
height: 20px;
margin: 20px auto;
padding: 10px;
border-radius: 20px;
border: 1px solid #ccc;
font-size: 16px;
}
.button-container {
display: flex;
justify-content: center;
margin-top: -5px;
button {
width: 100px;
height: 40px;
background-color: #007bff;
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
margin-left: 10px;
margin-right: 10px;
&:hover {
background-color: #0056b3;
}
}
}
}
.chat-room-nav .chat-list {
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
padding: 10px;
box-sizing: border-box;
z-index: 1;
.chat-item {
display: flex;
position: relative;
align-items: center;
padding: 10px;
margin-bottom: 10px;
border-radius: 8px;
cursor: pointer;
border: 1px solid #ddd;
&:hover {
background-color: #e0e0e0;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
}
.chat-info {
height: 40px;
flex-grow: 1;
right: 0;
width: 80%;
h3 {
margin-top: -3px;
margin-left: 2px;
font-size: 1.2em;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-message {
position: relative;
margin-bottom: 3px;
margin-left: 2px;
max-width:100px;
font-weight: bold;
color: #333;
max-height: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
font-size: 0.9em;
}
.chat-time {
position: absolute;
color: #666;
font-size: 0.9em;
right: 5px;
bottom: 2px;
}
}
.chat-room-menu {
position: absolute;
right: 10px;
top: 10px;
width: 20px;
height: 20px;
background-color: white;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 100;
text-align: center;
cursor: pointer;
}
/* Dropdown menu for actions */
.menu-dropdown {
position: absolute;
right: 6px;
top: 36px;
min-width: 160px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
padding: 6px 0;
display: none;
}
.menu-open {
display: block;
}
.menu-item {
width: 100%;
background: transparent;
border: none;
text-align: left;
padding: 10px 12px;
font-size: 14px;
color: #111827;
cursor: pointer;
}
.menu-item:hover {
background: #f3f4f6;
}
}
}
.message-item {
position: relative;
left: 15%;
width: 85%;
height: 100vh;
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f0f0f0;
background-size: cover;
z-index: 2;
.message-header {
z-index: 5;
width: 100%;
position: sticky;
top: 0;
background-color: #fff;
height: 70px;
display: flex;
align-items: center;
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-left: 40px;
}
h2 {
width: 10%;
color: #333;
text-align: center;
margin: 0;
padding: 0;
}
border-bottom: 1px solid #000000;
}
.message-list {
flex: 1;
overflow-y: auto;
padding: 10px;
box-sizing: border-box;
z-index: 3;
.message {
display: flex;
max-width: 80%;
padding: 10px;
margin-bottom: 15px;
margin-left: 15px;;
border-radius: 15px;
overflow-wrap: break-word;
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
flex-shrink: 0;
}
.message-content {
min-width: 0;
.message-info {
height: auto;
.message-sender {
font-weight: bold;
color: #333;
margin-right: 8px;
}
.message-time {
font-size: 0.8em;
color: #999;
}
}
.message-text {
padding: 8px 12px;
margin: 5px 0;
border-radius: 10px;
color: #555;
background-color: #f5f5f5;
display: block;
word-break: break-word;
}
}
}
}
.message-input {
position: sticky;
bottom: 0;
width: 90%;
padding: 15px;
box-sizing: border-box;
z-index: 4;
display: flex;
margin-left: 5%;
input {
flex: 1;
padding: 12px 15px;
border-radius: 25px;
font-size: 16px;
margin-right: 10px;
background-color: transparent;
border: #000000 1px solid;
}
}
}
/* From Uiverse.io by njesenberger */
.button {
-webkit-appearance: none;
appearance: none;
position: relative;
border-width: 0;
padding: 0 8px 12px;
min-width: 10em;
box-sizing: border-box;
background: transparent;
font: inherit;
cursor: pointer;
}
.button-top {
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 0;
padding: 8px 16px;
transform: translateY(0);
text-align: center;
color: #fff;
text-shadow: 0 -1px rgba(0, 0, 0, .25);
transition-property: transform;
transition-duration: .2s;
-webkit-user-select: none;
user-select: none;
}
.button:active .button-top {
transform: translateY(6px);
}
.button-top::after {
content: '';
position: absolute;
z-index: -1;
border-radius: 4px;
width: 100%;
height: 100%;
box-sizing: content-box;
background-image: radial-gradient(#cd3f64, #9d3656);
text-align: center;
color: #fff;
box-shadow: inset 0 0 0px 1px rgba(255, 255, 255, .2), 0 1px 2px 1px rgba(255, 255, 255, .2);
transition-property: border-radius, padding, width, transform;
transition-duration: .2s;
}
.button:active .button-top::after {
border-radius: 6px;
padding: 0 2px;
}
.button-bottom {
position: absolute;
z-index: -1;
bottom: 4px;
left: 4px;
border-radius: 8px / 16px 16px 8px 8px;
padding-top: 6px;
width: calc(100% - 8px);
height: calc(100% - 10px);
box-sizing: content-box;
background-color: #803;
background-image: radial-gradient(4px 8px at 4px calc(100% - 8px), rgba(255, 255, 255, .25), transparent), radial-gradient(4px 8px at calc(100% - 4px) calc(100% - 8px), rgba(255, 255, 255, .25), transparent), radial-gradient(16px at -4px 0, white, transparent), radial-gradient(16px at calc(100% + 4px) 0, white, transparent);
box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.5), inset 0 -1px 3px 3px rgba(0, 0, 0, .4);
transition-property: border-radius, padding-top;
transition-duration: .2s;
}
.button:active .button-bottom {
border-radius: 10px 10px 8px 8px / 8px;
padding-top: 0;
}
.button-base {
position: absolute;
z-index: -2;
top: 4px;
left: 0;
border-radius: 12px;
width: 100%;
height: calc(100% - 4px);
background-color: rgba(0, 0, 0, .15);
box-shadow: 0 1px 1px 0 rgba(255, 255, 255, .75), inset 0 2px 2px rgba(0, 0, 0, .25);
}
.message-item {
/* From Uiverse.io by adamgiebl */
button {
font-family: inherit;
font-size: 20px;
background: transparent;
color: #000000;
padding: 0.7em 1em;
padding-left: 0.9em;
display: flex;
align-items: center;
border: none;
border-radius: 16px;
overflow: hidden;
transition: all 0.2s;
cursor: pointer;
border: #000000 1px solid;
}
button span {
display: block;
margin-left: 0.3em;
transition: all 0.3s ease-in-out;
}
button svg {
display: block;
transform-origin: center center;
transition: transform 0.3s ease-in-out;
}
button:hover .svg-wrapper {
animation: fly-1 0.6s ease-in-out infinite alternate;
}
button:hover svg {
transform: translateX(1.2em) rotate(45deg) scale(1.1);
}
button:hover span {
transform: translateX(5em);
}
button:active {
transform: scale(0.95);
}
@keyframes fly-1 {
from {
transform: translateY(0.1em);
}
to {
transform: translateY(-0.1em);
}
}
}
\ No newline at end of file
'use client';
import styles from "./chat.module.css";
import React, { useEffect, useState } from "react";
import { useRouter } from 'next/navigation';
const Profile = [ 'https://pic4.zhimg.com/v2-c5a0d0d57c1a85c6db56e918707f54a3_r.jpg',
'https://pic2.zhimg.com/v2-c2e79191533fdc7fced2f658eef987c9_r.jpg',
'https://pic4.zhimg.com/v2-bf5f58e7b583cd69ac228db9fdff377f_r.jpg',
'https://pic1.zhimg.com/v2-10e9368af9eb405c8844584ad3ad9dd8_r.jpg',
'https://picx.zhimg.com/50/v2-63e3500bfd25b6ae7013a6a3b6ce045b_720w.jpg',
'https://c-ssl.duitang.com/uploads/blog/202109/20/20210920000906_53764.png']
const RoomProfile = 'https://tse1-mm.cn.bing.net/th/id/OIP-C.0KyBJKAdIGi9SAQc_X62tQHaLr?cb=thvnextc2&rs=1&pid=ImgDetMain';
interface RoomEntryProps {
roomId: number;
roomName: string;
lastSender: { String: string, Valid: boolean };
lastContent: { String: string, Valid: boolean };
lastTime: { Time: string, Valid: boolean };
}
interface MessageProps {
roomId: number;
roomName: string;
messages: Array<{
profile: number;
sender: string;
content: string;
time: string;
}>;
}
function RoomEntry ({rooms, onRoomClick, onRename, onDelete} : {rooms: RoomEntryProps[], onRoomClick: (roomId: number, roomName: string) => void, onRename: (roomId: number, currentName: string) => void, onDelete: (roomId: number) => void}) {
const [openMenuFor, setOpenMenuFor] = useState<number | null>(null);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (!target.closest('[data-menu="room-actions"]')) {
setOpenMenuFor(null);
}
};
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}, []);
return (
<div className={styles["chat-room-nav"]}>
<div className={styles["sidebar-action"]}>
<button type="button" className={styles["button"]} onClick={openOpenDiv}>
<div className={styles["button-top"]}>New Chat</div>
<div className={styles["button-bottom"]}></div>
<div className={styles["button-base"]}></div>
</button>
</div>
<div className={styles["chat-list"]}>
{rooms.map((room) => (
<div className={styles["chat-item"]} key={room.roomId}>
<img src={RoomProfile} alt="Avatar" className={styles["avatar"]} />
<div className={styles["chat-info"]}>
<h3 onClick={() => onRoomClick(room.roomId, room.roomName)}>{room.roomName}</h3>
<span className={styles["chat-message"]}>
{room.lastSender.Valid ? room.lastSender.String : ''}:
{room.lastContent.Valid ? room.lastContent.String : ''}</span>
<span className={styles["chat-time"]}>{room.lastTime.Valid ? formatTimeToHoursMinutes(room.lastTime.Time) : ''}</span>
</div>
<div
className={styles["chat-room-menu"]}
data-menu="room-actions"
onClick={(e) => {
e.stopPropagation();
setOpenMenuFor(prev => (prev === room.roomId ? null : room.roomId));
}}
aria-label="Room actions"
title="Room actions"
>
···
<div className={`${styles["menu-dropdown"]} ${openMenuFor === room.roomId ? styles["menu-open"] : ''}`}>
<button
className={styles["menu-item"]}
onClick={(e) => { e.stopPropagation(); setOpenMenuFor(null); onRename(room.roomId, room.roomName); }}
>
Rename room
</button>
<button
className={styles["menu-item"]}
onClick={(e) => { e.stopPropagation(); setOpenMenuFor(null); onDelete(room.roomId); }}
>
Delete room
</button>
</div>
</div>
</div>
))}
</div>
</div>
);
// Button From Uiverse.io by njesenberger
}
function formatTimeToHoursMinutes(isoString: string) {
const date = new Date(isoString);
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
}
function InputRoomNameArea({ onAddNewRoom }: { onAddNewRoom: (roomName: string) => void}) {
const [roomNameInput, setRoomNameInput] = useState("");
const handleAddNewRoom = () => {
onAddNewRoom(roomNameInput);
setRoomNameInput("");
closeOpenDiv();
}
return (
<div className={styles["open"]}>
<div className={styles["roomName-input"]}>
<h3>Please Enter the New Room Name</h3>
<input
type="text"
className={styles["RoomNameInput"]}
placeholder="Start new chat"
value = {roomNameInput}
onChange={(e) => setRoomNameInput(e.target.value)}
onKeyUpCapture={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleAddNewRoom();
}
else if (e.key === 'Escape') {
closeOpenDiv();
}
}}
/>
<div className={styles["button-container"]}>
<button className={styles["create-button"]} onClick={handleAddNewRoom}>Submit</button>
<button className={styles["cancel-button"]} onClick={closeOpenDiv}>Cancel</button>
</div>
</div>
</div>
);
}
function openOpenDiv() {
const openDiv = document.getElementsByClassName(styles.open)[0] as HTMLDivElement | undefined;
if (openDiv) {
openDiv.style.zIndex = "1000";
}
const roomNameInput = document.getElementsByClassName(styles.RoomNameInput)[0] as HTMLInputElement | undefined;
if (roomNameInput) {
roomNameInput.style.zIndex = "1001";
}
}
function closeOpenDiv() {
const openDiv = document.getElementsByClassName(styles.open)[0] as HTMLDivElement | undefined;
if (openDiv) {
openDiv.style.zIndex = "0";
}
const roomNameInput = document.getElementsByClassName(styles.RoomNameInput)[0] as HTMLInputElement | undefined;
if (roomNameInput) {
roomNameInput.style.zIndex = "0";
roomNameInput.value = '';
}
}
function MessageItem (props: MessageProps & { onAddNewComment: (content: string) => void}) {
const [inputValue, setInputValue] = useState("");
if (props.roomId === 0) {
return <div className={styles["message-item"]}>Please select a room to chat.</div>;
}
const handlerSend = () => {
if (inputValue.trim() === '') {
alert("Message can't be empty");
return
}
props.onAddNewComment(inputValue);
setInputValue('');
}
return (
<div className={styles["message-item"]}>
<div className={styles["message-header"]}>
<img src={RoomProfile} alt="Avatar" className={styles["avatar"]} />
<h2>{props.roomName}</h2>
</div>
<div className={styles["message-list"]}>
{props.messages.map((msg, index) => (
<div key={index} className={styles["message"]}>
<img src={Profile[msg.profile]} alt={`${msg.sender}'s avatar`} className={styles["avatar"]} />
<div className={styles["message-content"]}>
<div className={styles["message-info"]}>
<span className={styles["message-sender"]}>{msg.sender}</span>
<span className={styles["message-time"]}>{formatTimeToHoursMinutes(msg.time)}</span>
</div>
<p className={styles["message-text"]}>{msg.content}</p>
</div>
</div>
))}
</div>
<div className={styles["message-input"]}>
<input
type="text"
placeholder="Type a message..."
className={styles["Inputarea"]}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyUpCapture={
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handlerSend();
}
}}/>
<button className={styles["send-button"]} onClick={handlerSend}>
<div className={styles["svg-wrapper-1"]}>
<div className={styles["svg-wrapper"]}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path fill="none" d="M0 0h24v24H0z"></path>
<path
fill="currentColor"
d="M1.946 9.315c-.522-.174-.527-.455.01-.634l19.087-6.362c.529-.176.832.12.684.638l-5.454 19.086c-.15.529-.455.547-.679.045L12 14l6-8-8 6-8.054-2.685z"
></path>
</svg>
</div>
</div>
<span>Send</span>
</button>
</div>
</div>
);
// From Uiverse.io by adamgiebl
}
function ChatRoomComponent({ userName }: { userName: string }) {
const [rooms, setRooms] = useState<RoomEntryProps[]>([]);
const [currentRoom, setCurrentRoom] = useState<MessageProps | null>(null);
const ROOM_LIST_REFRESH_INTERVAL = 1000;
const MESSAGE_REFRESH_INTERVAL = 1000;
useEffect(() => {
document.title = `Chat Room | ${userName}`;
}, [userName]);
const fetchRooms = async () => {
try {
const response = await fetch("/api/room/list", {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.code === 0) {
const fetchedRooms = result.data.map((room: RoomEntryProps) => ({
...room,
lastSender: room.lastSender || { String: '', Valid: false },
lastContent: room.lastContent || { String: '', Valid: false },
lastTime: room.lastTime || { Time: '', Valid: false }
}));
setRooms(fetchedRooms);
} else {
console.error("Failed to fetch rooms:", result.msg);
}
} catch (error) {
console.error("Error fetching rooms:", error);
}
};
const fetchCurrentRoomMessages = async (roomId: number) => {
if (!roomId) return;
try {
const response = await fetch(`/api/message/get?roomId=${roomId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.code === 0) {
setCurrentRoom(prev => {
if (!prev || prev.roomId !== roomId) return prev;
return {
...prev,
messages: result.data || []
};
});
} else {
console.error("Failed to fetch 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: []
});
await fetchCurrentRoomMessages(roomId);
}
async function addNewRoom(roomName: string) {
if (roomName.trim() === "") {
alert("Please enter a room name.");
return;
}
try {
const response = await fetch("/api/room/add", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: roomName })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.code === 0) {
const newRoom: RoomEntryProps = {
roomId: result.data.roomId,
roomName: roomName,
lastSender: { String: '', Valid: false },
lastContent: { String: '', Valid: false },
lastTime: { Time: '', Valid: false },
};
setRooms(prevRooms => [newRoom, ...prevRooms]);
} else {
alert("Failed to add a new room: " + result.msg);
}
} catch (error) {
console.error("Error adding new room:", error);
alert("Error adding new room.");
}
}
const addNewComment = async (content: string) => {
if (!currentRoom)
return;
let profileId = 0;
if (userName === '蔡徐坤') {
profileId = Profile.length - 1;
} else {
profileId = Math.floor(Math.random() * (Profile.length - 1));
}
try {
const response = await fetch('/api/message/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
roomId: currentRoom.roomId,
userName: userName,
content: content
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.code === 0) {
const newMessage: { profile: number, sender: string, content: string, time: string } = {
profile: profileId,
sender: userName,
content: content,
time: new Date().toISOString()
};
setCurrentRoom(prevRoom => {
if (!prevRoom) return null;
return {
...prevRoom,
messages: [...prevRoom.messages, newMessage]
};
});
} else {
alert(`Error: ${result.msg}`);
}
} catch (error) {
console.error("Error in addNewComment:", error);
alert("An error occurred while sending the message.");
}
}
const handleRename = async (roomId: number, currentName: string) => {
const newName = prompt('Enter new room name', currentName);
if (!newName || newName.trim() === '' || newName === currentName) return;
try {
const response = await fetch('/api/room/rename', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ roomId, newName: newName })
});
if (!response.ok) {
const errorText = await response.text();
console.error(`HTTP error! status: ${response.status}, body: ${errorText}`);
throw new Error(`HTTP error! status: ${response.status}`);
}
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('/api/room/delete', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ roomId })
});
if (!response.ok) {
const errorText = await response.text();
console.error(`HTTP error! status: ${response.status}, body: ${errorText}`);
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.code !== 0) {
alert('Delete failed: ' + (result.msg || 'unknown error'));
return;
}
setRooms(prev => prev.filter(r => r.roomId !== roomId));
setCurrentRoom(prev => (prev && prev.roomId === roomId) ? null : prev);
} catch (err) {
console.error('Delete error', err);
alert('Delete error');
}
};
return (
<div className={styles["chat-room"]}>
<RoomEntry rooms={rooms} onRoomClick={handleRoomClick} onRename={handleRename} onDelete={handleDelete}/>
<MessageItem
roomId={currentRoom?.roomId || 0}
roomName={currentRoom?.roomName || ""}
messages={currentRoom?.messages || []}
onAddNewComment={addNewComment}
/>
<InputRoomNameArea onAddNewRoom={addNewRoom}/>
</div>
);
}
export default function ChatRoom() {
const [userName, setUserName] = useState<string | null>(null);
const router = useRouter();
useEffect(() => {
const storedUserName = localStorage.getItem('userName');
if (storedUserName) {
setUserName(storedUserName);
} else {
router.push('/');
}
}, [router]);
if (!userName) {
return <div>Loading...</div>;
}
return <ChatRoomComponent userName={userName} />;
}
\ No newline at end of file
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: {
default: "Chat Room",
template: "%s | Chat Room"
},
description: "A real-time chat application built with Next.js and Prisma",
keywords: ["chat", "messaging", "real-time", "next.js", "prisma"],
authors: [{ name: "Your Name" }],
icons: {
icon: [
{ url: '/favicon.svg', type: 'image/svg+xml' },
{ url: '/favicon.ico', sizes: '32x32', type: 'image/x-icon' }
],
shortcut: '/favicon.svg',
apple: '/favicon.svg',
},
manifest: '/manifest.json',
openGraph: {
title: "Chat Room App",
description: "A real-time chat application",
type: "website",
locale: "en_US",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
import { PrismaClient } from '../../../generated/prisma';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: ['query'],
});
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
/*
{
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;
font-size: 0.9em;
}
.input-box label
{
position: absolute;
top: 50%;
left: 5px;
transform: translateY(-50%);
font-size: 0.9em;
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: 30px;
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;
bottom: 8px;
}
.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"./login.module.css"
import { MdLock, MdPerson } from "react-icons/md";
import { useState, useEffect } from "react";
import Link from "next/link";
import { useRouter } from 'next/navigation';
export default function SetName() {
const [userName, setUserName] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const router = useRouter();
useEffect(() => {
document.title = 'Login | Chat Room App';
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError('');
try {
const response = await fetch('/api/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);
if (data.data?.token) {
localStorage.setItem('token', data.data.token);
}
router.push('/chat');
} else {
setError(data.msg || 'Login failed');
}
} catch (error) {
console.error('Error logging in:', error);
setError('Network error. Please try again.');
} finally {
setIsLoading(false);
}
}
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>
{error && (
<div style={{
color: 'red',
fontSize: '14px',
marginTop: '10px',
textAlign: 'center'
}}>
{error}
</div>
)}
<div>
<button
className={styles["SetName-button"]}
type="submit"
disabled={isLoading}
>
{isLoading ? 'Logging in...' : 'Login'}
</button>
</div>
<div className={styles["register_link"]}>
<p>Don&apos;t have an account? <Link href="/Register">Register</Link></p>
</div>
</form>
</div>
</div>
);
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment