Compare commits

...

4 Commits

Author SHA1 Message Date
TQY
37429aa7f2 translate ProfileImage component text to Chinese 2025-10-17 11:06:41 +08:00
TQY
bf6dff352e chinese 2025-10-17 10:43:04 +08:00
TQY
8c1c0abd8e update staleTime 2025-10-17 10:34:20 +08:00
root
efacf59414 init 2025-10-17 02:23:24 +00:00
14 changed files with 113 additions and 103 deletions

14
.env Normal file
View File

@@ -0,0 +1,14 @@
# 端口
PORT=3000
# MongoDB 连接地址,如果 MongoDB 在本机默认端口
DB_CONNECTION="mongodb://localhost:27017/livepoll"
# 密码加密复杂度
SALT_ROUNDS=6
# JWT 私钥(可以随便写一串随机字符)
JWT_PRIVATE="your-secret-jwt-key"
# 前端访问地址(群友打开的链接)
CLIENT_URL="http://110.42.109.143:3000"

View File

@@ -31,8 +31,8 @@ export async function signinController(req, res) {
const { token, userData } = await signinService(email, password);
res.cookie("livepoll-access-token", token, {
httpOnly: true,
secure: true,
sameSite: "None",
secure: false,
sameSite: "Lax",
expires: new Date(Date.now() + 24 * 60 * 60 * 1000 * 365), // 1 year
path: "/"
}).status(200).json({

View File

@@ -1 +1 @@
VITE_BACKEND_URL="https://livepoll-anjx.onrender.com"
VITE_BACKEND_URL="http://localhost:3000"

View File

@@ -17,15 +17,15 @@ function Header() {
<ul className="menu menu-horizontal px-1 gap-1">
{user.username ? (
<li>
<Link to={"/dashboard"}>Dashboard</Link>
<Link to={"/dashboard"}>仪表盘</Link>
</li>
) : (
<li>
<Link to={"/login"}>Login</Link>
<Link to={"/login"}>登陆</Link>
</li>
)}
<li>
<Link to={"/poll"}>Polls</Link>
<Link to={"/poll"}>投票列表</Link>
</li>
</ul>
</div>

View File

@@ -16,24 +16,24 @@ function ProfileImage({userData}) {
<div tabIndex={0} role="button" className="btn btn-ghost btn-circle avatar">
<div className="w-10 rounded-full">
<img
alt="Tailwind CSS Navbar component"
alt="用户头像"
src="https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.webp" />
</div>
</div>
<ul
tabIndex={0}
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow">
<li className='font-bold'><a >{userData?.username || "User"}</a></li>
<li className='font-bold'><a >{userData?.username || "用户"}</a></li>
<li>
<Link to={'/dashboard'} className="justify-between" >
Profile
<span className="badge">New</span>
个人资料
<span className="badge"></span>
</Link>
<Link to={"/bookmark"} className="justify-between" >
Bookmarks
收藏
</Link>
</li>
<li><a onClick={handleLogout}>Logout</a></li>
<li><a onClick={handleLogout}>退出登录</a></li>
</ul>
</div>
</div>

View File

@@ -1,8 +1,8 @@
import axios from "axios";
const axiosInstance = axios.create({
// baseURL: `http://localhost:3000/api/v1`,
baseURL: `https://livepoll-anjx.onrender.com/api/v1`,
baseURL: `http://110.42.109.143:3000/api/v1`,
//baseURL: `https://livepoll-anjx.onrender.com/api/v1`,
withCredentials: true,
headers: {
"Content-Type": "application/json",

View File

@@ -18,7 +18,7 @@ function Bookmark() {
getUserBookmarks,
{
cacheTime: 1000 * 60 * 5, // 5 minutes
staleTime: 1000 * 60 * 10, // 10 minutes
staleTime: 0, // 10 minutes
}
);
@@ -40,7 +40,7 @@ function Bookmark() {
return (
<div className="p-6 bg-base-200 h-screen">
<h1 className="md:text-4xl text-xl font-bold text-white mb-6">
Bookmarked Polls
收藏的投票
</h1>
{isError && (
<div className="h-60 w-full">
@@ -54,9 +54,9 @@ function Bookmark() {
<thead>
<tr>
<th>#</th>
<th>Title</th>
<th>Description</th>
<th>Actions</th>
<th>标题</th>
<th>描述</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@@ -74,13 +74,13 @@ function Bookmark() {
onClick={() => handleViewPollClick(bookmark._id)}
className="btn btn-sm btn-primary text-sm md:text-base mb-2 md:mb-1"
>
View Poll
查看投票
</button>
<button
onClick={() => handleRemoveBookmark(bookmark._id)}
className="btn btn-sm btn-error text-sm md:text-base ml-2"
>
Remove
移除
</button>
</td>
</tr>

View File

@@ -30,7 +30,7 @@ function CreatePollForm() {
const mutation = useMutation(createPollService, {
onSuccess: (data) => {
const message = data?.message || "Poll created successfully";
const message = data?.message || "投票创建成功";
toast.success(message);
handleClearPoll();
navigate(`/view/${data?.data?._id}`);
@@ -39,7 +39,7 @@ function CreatePollForm() {
console.log(error);
const errorMessage =
error.response?.data?.errors?.[0]?.message ||
"An unexpected error occurred";
"发生意外错误";
toast.error(errorMessage);
},
});
@@ -47,7 +47,7 @@ function CreatePollForm() {
const handlePollSubmit = (e) => {
e.preventDefault();
if (title.trim() == "" || description.trim() == "" || options.length == 0) {
toast.error("All fields are required");
toast.error("所有字段都是必填的");
return;
}
mutation.mutate({ title, description, options });
@@ -56,19 +56,19 @@ function CreatePollForm() {
return (
<div className="min-h-screen bg-base-200">
<div className="w-full p-6 text-white">
<h1 className="text-2xl font-bold mb-6 text-center">Create New Poll</h1>
<h1 className="text-2xl font-bold mb-6 text-center">创建新投票</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
{/* Poll Title */}
<div className="mb-4">
<label className="block text-lg font-medium mb-2">
Poll Title
投票标题
</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Enter poll title"
placeholder="输入投票标题"
className="input input-bordered w-full"
/>
</div>
@@ -76,12 +76,12 @@ function CreatePollForm() {
{/* Poll Description */}
<div className="mb-4">
<label className="block text-lg font-medium mb-2">
Description
描述
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Describe the purpose of the poll"
placeholder="描述投票的目的"
className="textarea textarea-bordered w-full"
rows="3"
></textarea>
@@ -91,21 +91,21 @@ function CreatePollForm() {
<div>
{/* Poll Options */}
<div className="mb-4">
<label className="block text-lg font-medium mb-2">Options</label>
<label className="block text-lg font-medium mb-2">选项</label>
{options.map((option, index) => (
<div key={index} className="flex items-center mb-2">
<input
type="text"
value={option}
placeholder={`Option ${index + 1}`}
placeholder={`选项 ${index + 1}`}
className="input input-bordered w-full cursor-not-allowed"
readOnly
/>
{options.length > 2 && (
<button
className="btn btn-error btn-circle btn-xs ml-2"
title="Remove option"
title="移除选项"
onClick={() =>
setOptions(options.filter((_, i) => i !== index))
}
@@ -122,7 +122,7 @@ function CreatePollForm() {
type="text"
value={optionInput}
onChange={(e) => setOptionInput(e.target.value)}
placeholder="Enter new option"
placeholder="输入新选项"
className="input input-bordered w-full"
/>
</div>
@@ -131,7 +131,7 @@ function CreatePollForm() {
title="Add another option"
onClick={handleAddOption}
>
<FaPlus className="mr-2" /> Add Option
<FaPlus className="mr-2" /> 添加选项
</button>
</div>
</div>
@@ -142,21 +142,21 @@ function CreatePollForm() {
className="btn btn-ghost w-full mt-4 md:w-1/2"
onClick={() => {
const sure = window.confirm(
"Are you sure you want to clear the poll?"
"确定要清除投票内容吗?"
);
if (sure) {
handleClearPoll();
}
}}
>
Clear Poll
清除投票
</button>
{/* Submit Button */}
<button
className="btn btn-success w-full mt-4 md:w-1/2"
onClick={handlePollSubmit}
>
Create Poll
创建投票
</button>
</div>
</div>

View File

@@ -60,12 +60,12 @@ function Dashboard() {
<p className="mt-2 text-center text-gray-400">
{user?.email || "Email"}
</p>
<button className="btn btn-primary mt-4 w-full">Edit Profile</button>
<button className="btn btn-primary mt-4 w-full">编辑资料</button>
<button
className="btn btn-error btn-outline mt-4 w-full"
onClick={handleLogout}
>
Logout
退出登录
</button>
</aside>
@@ -74,10 +74,10 @@ function Dashboard() {
{/* Dashboard Header */}
<div className="mb-6">
<h1 className="text-2xl md:text-4xl font-bold text-white">
Poll Dashboard
投票仪表板
</h1>
<p className="md:text-lg text-gray-300">
Manage your polls, view results, and edit as needed.
管理您的投票查看结果并根据需要进行编辑
</p>
</div>
@@ -87,7 +87,7 @@ function Dashboard() {
className="btn btn-primary"
onClick={() => navigator("/create")}
>
Add New Poll <FaPlus />
添加新投票 <FaPlus />
</button>
</div>
@@ -100,10 +100,10 @@ function Dashboard() {
<thead>
<tr>
<th>#</th>
<th>Title</th>
<th>Description</th>
<th>Published</th>
<th>Actions</th>
<th>标题</th>
<th>描述</th>
<th>已发布</th>
<th>操作</th>
</tr>
</thead>

View File

@@ -3,37 +3,33 @@ import { useNavigate } from 'react-router-dom';
import useStore from '../store/useStore';
function Home() {
const navigator = useNavigate();
return (
<div className="flex bg-base-200 min-h-screen flex-col items-center text-white p-8">
<h1 className="text-4xl mt-2 md:text-5xl font-bold mb-6 text-center flex flex-col gap-2 md:block">Welcome to <span className='bg-slate-800 px-4 rounded-md relative'>LivePoll</span></h1>
<h1 className="text-4xl mt-2 md:text-5xl font-bold mb-6 text-center flex flex-col gap-2 md:block">
欢迎来到 <span className='bg-slate-800 px-4 rounded-md relative'>LivePoll</span>
</h1>
<p className="text-lg text-center max-w-2xl mb-8">
LivePoll is your platform for real-time, interactive polling. Create polls, participate in
active discussions, and get instant feedback with live updates and visualizations. Discover
what people think on topics that matter to you and have your voice heard!
LivePoll 是您进行实时互动投票的平台创建投票参与活跃讨论并通过实时更新和可视化获得即时反馈探索人们对您关心话题的看法让您的声音被听到
</p>
<div className="flex flex-col lg:flex-row gap-6 mb-8">
<div className="bg-base-300 p-6 rounded-lg shadow-md w-full lg:w-96 text-center">
<h2 className="text-3xl font-semibold mb-4">Create Polls</h2>
<h2 className="text-3xl font-semibold mb-4">创建投票</h2>
<p className="text-gray-400">
Create custom polls on any topic and share them with others. Add options, set
permissions, and see the responses roll in real-time.
创建关于任何主题的自定义投票并与他人分享添加选项设置权限并实时查看响应结果
</p>
</div>
<div className="bg-base-300 p-6 rounded-lg shadow-md w-full lg:w-96 text-center">
<h2 className="text-3xl font-semibold mb-4">Vote & Participate</h2>
<h2 className="text-3xl font-semibold mb-4">投票与参与</h2>
<p className="text-gray-400">
Browse a variety of public polls or join private ones shared with you. Cast your vote
and see the real-time results as others participate.
浏览各种公开投票或加入与您分享的私人投票投出您的一票并在他人参与时查看实时结果
</p>
</div>
<div className="bg-base-300 p-6 rounded-lg shadow-md w-full lg:w-96 text-center">
<h2 className="text-3xl font-semibold mb-4">Bookmark & Track</h2>
<h2 className="text-3xl font-semibold mb-4">收藏与追踪</h2>
<p className="text-gray-400">
Bookmark polls to save them for later, view your past participation, and stay updated
on topics you care about.
收藏投票以便稍后查看查看您过去的参与记录并随时了解您关心的话题动态
</p>
</div>
</div>
@@ -41,7 +37,7 @@ function Home() {
className="btn btn-primary px-6 py-3 font-semibold text-lg"
onClick={() => navigator("/dashboard")}
>
Make a Poll
创建投票
</button>
</div>
)

View File

@@ -41,75 +41,75 @@ const LoginPage = () => {
return (
<div className="flex justify-center bg-base-200 h-screen p-4">
<div className="w-full max-w-md rounded-lg p-8">
<h2 className="text-2xl font-semibold text-center text-white mb-6">Login</h2>
<h2 className="text-2xl font-semibold text-center text-white mb-6">登录</h2>
<form className="space-y-4">
{/* Email Input */}
{/* 邮箱输入 */}
<div className="form-control w-full">
<label className="label">
<span className="label-text text-gray-200">Email</span>
<span className="label-text text-gray-200">邮箱</span>
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
placeholder="请输入您的邮箱"
className="input input-bordered w-full bg-gray-700 text-white focus:outline-none focus:ring focus:ring-primary"
required
/>
</div>
{/* Password Input */}
{/* 密码输入 */}
<div className="form-control w-full">
<label className="label">
<span className="label-text text-gray-200">Password</span>
<span className="label-text text-gray-200">密码</span>
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
placeholder="请输入您的密码"
className="input input-bordered w-full bg-gray-700 text-white focus:outline-none focus:ring focus:ring-primary"
required
/>
</div>
{/* Error Message */}
{/* 错误信息 */}
{mutation.isError && <InlineTextError mutation={mutation} />}
{/* Success Message */}
{/* 成功信息 */}
{mutation.isSuccess && (
<p className="text-green-500 text-sm md:text-base">
🎉 {mutation.data.message || "Login is successfull"}
🎉 {mutation.data.message || "登录成功"}
</p>
)}
{/* Forgot Password Link */}
{/* 忘记密码链接 */}
<div className="text-right">
<a href="#" className="text-sm text-primary hover:underline">Forgot password?</a>
<a href="#" className="text-sm text-primary hover:underline">忘记密码</a>
</div>
{/* Submit Button */}
{/* 提交按钮 */}
<div>
<button
onClick={handleLogin}
type="submit"
className="btn btn-primary w-full text-white"
>
{mutation.isLoading ? <SpinnerLoader/> : "Login"}
{mutation.isLoading ? <SpinnerLoader/> : "登录"}
</button>
</div>
</form>
{/* Divider */}
<div className="divider text-gray-400">OR</div>
{/* 分隔符 */}
<div className="divider text-gray-400"></div>
{/* Sign Up Link */}
{/* 注册链接 */}
<p className="text-center text-gray-300">
Dont have an account?{' '}
<Link to="/register" href="#" className="text-primary hover:underline">Sign up</Link>
还没有账户{' '}
<Link to="/register" href="#" className="text-primary hover:underline">立即注册</Link>
</p>
</div>
</div>

View File

@@ -19,7 +19,7 @@ function Polls() {
return (
<div className="container mx-auto p-4 bg-base-200">
<h1 className="text-2xl font-bold text-center mb-6">Polls</h1>
<h1 className="text-2xl font-bold text-center mb-6">投票列表</h1>
{isSuccess && (
<div className="flex flex-wrap justify-center gap-6">
{data?.data?.polls?.map((poll) => (
@@ -42,14 +42,14 @@ function Polls() {
disabled={page <= 1}
onClick={() => setPage(page - 1)}
>
Prev
上一页
</button>
<button
className="btn btn-primary btn-circle"
disabled={data?.data?.totalPages === page}
onClick={() => setPage(page + 1)}
>
Next
下一页
</button>
</div>
</div>

View File

@@ -38,20 +38,20 @@ function Register() {
<div className=" flex justify-center h-screen bg-base-200 p-4">
<div className="w-full max-w-md rounded-lg ">
<h2 className="text-2xl font-semibold text-center text-white mb-6">
Sign Up
注册
</h2>
<form className="space-y-4">
{/* Username Input */}
<div className="form-control w-full">
<label className="label">
<span className="label-text text-gray-200">Username</span>
<span className="label-text text-gray-200">用户名</span>
</label>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
type="text"
placeholder="Enter your username"
placeholder="请输入您的用户名"
className="input input-bordered w-full bg-gray-700 text-white focus:outline-none focus:ring focus:ring-primary"
required
/>
@@ -60,13 +60,13 @@ function Register() {
{/* Email Input */}
<div className="form-control w-full">
<label className="label">
<span className="label-text text-gray-200">Email</span>
<span className="label-text text-gray-200">邮箱</span>
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
placeholder="请输入您的邮箱"
className="input input-bordered w-full bg-gray-700 text-white focus:outline-none focus:ring focus:ring-primary"
required
/>
@@ -75,13 +75,13 @@ function Register() {
{/* Password Input */}
<div className="form-control w-full">
<label className="label">
<span className="label-text text-gray-200">Password</span>
<span className="label-text text-gray-200">密码</span>
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
placeholder="请输入您的密码"
className="input input-bordered w-full bg-gray-700 text-white focus:outline-none focus:ring focus:ring-primary"
required
/>
@@ -91,7 +91,7 @@ function Register() {
{mutation.isError && <InlineTextError mutation={mutation} />}
{mutation.isSuccess && (
<p className="text-green-500 text-sm md:text-base">
🎉 {mutation.data.message || "Process is successfull"}
🎉 {mutation.data.message || "注册成功"}
</p>
)}
@@ -102,19 +102,19 @@ function Register() {
type="submit"
className="btn btn-primary w-full text-white mt-4"
>
{mutation.isLoading ? <SpinnerLoader /> : "Sign Up"}
{mutation.isLoading ? <SpinnerLoader /> : "注册"}
</button>
</div>
</form>
{/* Divider */}
<div className="divider text-gray-400">OR</div>
<div className="divider text-gray-400"></div>
{/* Login Link */}
<p className="text-center text-gray-300">
Already have an account?{" "}
已有账户{" "}
<Link to="/login" className="text-primary hover:underline">
Login
登录
</Link>
</p>
</div>

View File

@@ -28,8 +28,8 @@ function VotingPage() {
const [socket, setSocket] = useState(null);
useEffect(() => {
const s = io("https://livepoll-anjx.onrender.com");
// const s = io("http://localhost:3000");
// const s = io("https://livepoll-anjx.onrender.com");
const s = io("http://110.42.109.143:3000");
setSocket(s);
s.on("connect", () => {
@@ -52,7 +52,7 @@ function VotingPage() {
refetch,
} = useQuery(["poll", pollId], () => getPollData(pollId), {
cacheTime: 10 * 60 * 1000, // 10 minutes
staleTime: 20 * 60 * 1000, // 20 minutes
staleTime: 0,
onSuccess: (data) => {
setPoll(data);
},
@@ -87,7 +87,7 @@ function VotingPage() {
const mutation = useMutation(createVoteService, {
onSuccess: (data) => {
toast.success("Vote submitted successfully");
toast.success("投票提交成功");
if (socket) {
socket.emit("vote", { pollId, success: data?.success });
}
@@ -95,7 +95,7 @@ function VotingPage() {
onError: (error) => {
console.error(error);
toast.error(
error?.response?.data?.message || "An unexpected error occurred"
error?.response?.data?.message || "发生意外错误"
);
},
});
@@ -133,7 +133,7 @@ function VotingPage() {
className="rounded-full h-7 md:h-10 w-7 md:w-10"
/>
<h2 className="text-lg md:text-xl font-semibold">
{poll?.data?.creatorData?.username || "Unknown"}
{poll?.data?.creatorData?.username || "未知"}
</h2>
</div>
@@ -145,12 +145,12 @@ function VotingPage() {
{/* Poll Title */}
<h1 className="text-xl md:text-3xl font-bold text-center">
{poll?.data?.pollData?.title || "Loading.."}
{poll?.data?.pollData?.title || "加载中.."}
</h1>
{/* Poll Description */}
<p className="text-sm font-light md:text-base mb-6 text-center">
{poll?.data?.pollData?.description || "Loading.."}
{poll?.data?.pollData?.description || "加载中.."}
</p>
{/* Voting Options */}