refactor: format and relocate code

This commit is contained in:
cfngc4594 2025-06-21 23:19:49 +08:00
parent 47feffd62c
commit 0695dd2f61
64 changed files with 1799 additions and 1714 deletions

View File

@ -1,67 +0,0 @@
import { PrismaClient } from "@/generated/client";
const prisma = new PrismaClient();
async function checkProblemSubmissions() {
console.log("检查所有题目的提交记录情况...");
// 获取所有题目
const problems = await prisma.problem.findMany({
orderBy: { displayId: 'asc' }
});
console.log(`总题目数: ${problems.length}`);
for (const problem of problems) {
// 统计该题目的提交记录
const submissionCount = await prisma.submission.count({
where: { problemId: problem.id }
});
// 统计该题目的完成情况
const completedCount = await prisma.submission.count({
where: {
problemId: problem.id,
status: "AC"
}
});
// 查询时包含 localizations
const problems = await prisma.problem.findMany({
include: {
localizations: {
where: { type: "TITLE" },
select: { content: true }
}
}
});
problems.forEach(problem => {
const title = problem.localizations[0]?.content || "无标题";
console.log(`题目${problem.displayId} (${title}): ...`);
});
// 统计有提交记录的题目数量
const problemsWithSubmissions = await prisma.problem.findMany({
where: {
submissions: {
some: {}
}
}
});
console.log(`\n有提交记录的题目数量: ${problemsWithSubmissions.length}`);
console.log("有提交记录的题目编号:");
problemsWithSubmissions.forEach(p => {
console.log(` ${p.displayId}`);
});
}
checkProblemSubmissions()
.catch((e) => {
console.error("检查数据时出错:", e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
})};

View File

@ -1,37 +0,0 @@
import { PrismaClient, Status } from "@/generated/client";
const prisma = new PrismaClient();
async function fillTestcaseResults() {
const submissions = await prisma.submission.findMany();
let count = 0;
for (const submission of submissions) {
const testcases = await prisma.testcase.findMany({
where: { problemId: submission.problemId },
});
for (const testcase of testcases) {
// 检查是否已存在,避免重复
const exists = await prisma.testcaseResult.findFirst({
where: {
submissionId: submission.id,
testcaseId: testcase.id,
},
});
if (!exists) {
await prisma.testcaseResult.create({
data: {
isCorrect: submission.status === Status.AC,
output: submission.status === Status.AC ? "正确答案" : "错误答案",
timeUsage: Math.floor(Math.random() * 1000) + 1,
memoryUsage: Math.floor(Math.random() * 128) + 1,
submissionId: submission.id,
testcaseId: testcase.id,
},
});
count++;
}
}
}
console.log(`已为 ${count} 个提交生成测试用例结果`);
}
fillTestcaseResults().finally(() => prisma.$disconnect());

View File

@ -1,118 +0,0 @@
import { PrismaClient, Status, Language } from "@/generated/client";
const prisma = new PrismaClient();
async function generateUserData() {
console.log("为 student@example.com 生成测试数据...");
try {
// 查找用户
const user = await prisma.user.findUnique({
where: { email: "student@example.com" }
});
if (!user) {
console.log("用户不存在,创建用户...");
const newUser = await prisma.user.create({
data: {
name: "测试学生",
email: "student@example.com",
password: "$2b$10$SD1T/dYvKTArGdTmf8ERxuBKIONxY01/wSboRNaNsHnKZzDhps/0u",
role: "GUEST",
},
});
console.log("创建用户成功:", newUser);
return;
}
console.log("找到用户:", user.name || user.email);
// 获取所有已发布的题目
const problems = await prisma.problem.findMany({
where: { isPublished: true },
select: { id: true, displayId: true, localizations: {
where: { locale: "en", type: "TITLE" },
select: { content: true }
} }
});
console.log(`找到 ${problems.length} 道已发布题目`);
// 为这个用户生成提交记录
const submissionCount = Math.min(problems.length, 8); // 最多8道题目
const selectedProblems = problems.slice(0, submissionCount);
for (const problem of selectedProblems) {
// 为每道题目生成1-3次提交
const attempts = Math.floor(Math.random() * 3) + 1;
for (let i = 0; i < attempts; i++) {
// 60%概率AC40%概率WA
const isAC = Math.random() < 0.6 || i === attempts - 1; // 最后一次提交更可能是AC
const submission = await prisma.submission.create({
data: {
language: Math.random() > 0.5 ? Language.c : Language.cpp,
content: `// ${user.name || user.email} 针对题目${problem.displayId}的第${i + 1}次提交`,
status: isAC ? Status.AC : Status.WA,
message: isAC ? "Accepted" : "Wrong Answer",
timeUsage: Math.floor(Math.random() * 1000) + 1,
memoryUsage: Math.floor(Math.random() * 128) + 1,
userId: user.id,
problemId: problem.id,
},
});
// 获取题目的测试用例
const testcases = await prisma.testcase.findMany({
where: { problemId: problem.id }
});
// 为每个提交生成测试用例结果
for (const testcase of testcases) {
await prisma.testcaseResult.create({
data: {
isCorrect: isAC,
output: isAC ? "正确答案" : "错误答案",
timeUsage: Math.floor(Math.random() * 1000) + 1,
memoryUsage: Math.floor(Math.random() * 128) + 1,
submissionId: submission.id,
testcaseId: testcase.id,
},
});
}
console.log(`题目${problem.displayId}: 第${i + 1}次提交 - ${isAC ? 'AC' : 'WA'}`);
// 如果AC了就不再继续提交这道题
if (isAC) break;
}
}
console.log("数据生成完成!");
// 验证生成的数据
const userSubmissions = await prisma.submission.findMany({
where: { userId: user.id },
include: {
problem: { select: { displayId: true, localizations: {
where: { locale: "en", type: "TITLE" },
select: { content: true }
} } }
}
});
console.log(`\n用户 ${user.name || user.email} 现在有 ${userSubmissions.length} 条提交记录:`);
userSubmissions.forEach((s, index) => {
const title = s.problem.localizations.find(l => l.content === "TITLE")?.content || "无标题";
console.log(`${index + 1}. 题目${s.problem.displayId} (${title}) - ${s.status}`);
});
} catch (error) {
console.error("生成数据时出错:", error);
} finally {
await prisma.$disconnect();
}
}
generateUserData();

View File

@ -1,144 +0,0 @@
import { PrismaClient } from "@/generated/client";
const prisma = new PrismaClient();
async function testStudentDashboard() {
console.log("测试学生仪表板数据获取...");
// 获取一个学生用户
const student = await prisma.user.findFirst({
where: { role: "GUEST" },
select: { id: true, name: true, email: true }
});
if (!student) {
console.log("未找到学生用户,创建测试学生...");
const newStudent = await prisma.user.create({
data: {
name: "测试学生",
email: "test_student@example.com",
password: "$2b$10$SD1T/dYvKTArGdTmf8ERxuBKIONxY01/wSboRNaNsHnKZzDhps/0u",
role: "GUEST",
},
});
console.log(`创建学生: ${newStudent.name} (${newStudent.email})`);
}
// 获取所有已发布的题目
const allProblems = await prisma.problem.findMany({
where: { isPublished: true },
select: {
id: true,
displayId: true,
difficulty: true,
localizations: {
where: {
type: "TITLE",
locale: "en" // 或者根据需求使用其他语言
},
select: {
content: true
}
}
}
});
console.log(`总题目数: ${allProblems.length}`);
// 获取学生的所有提交记录
const userSubmissions = await prisma.submission.findMany({
where: { userId: student?.id },
include: {
problem: {
select: {
id: true,
displayId: true,
difficulty: true,
localizations: {
where: {
type: "TITLE",
locale: "en" // 或者根据需求使用其他语言
},
select: {
content: true
}
}
}
}
}
});
console.log(`学生提交记录数: ${userSubmissions.length}`);
// 计算题目完成情况
const completedProblems = new Set();
const attemptedProblems = new Set();
const wrongSubmissions = new Map(); // problemId -> count
userSubmissions.forEach(submission => {
attemptedProblems.add(submission.problemId);
if (submission.status === "AC") {
completedProblems.add(submission.problemId);
} else {
// 统计错误提交次数
const currentCount = wrongSubmissions.get(submission.problemId) || 0;
wrongSubmissions.set(submission.problemId, currentCount + 1);
}
});
// 题目完成比例数据
const completionData = {
total: allProblems.length,
completed: completedProblems.size,
percentage: allProblems.length > 0 ? Math.round((completedProblems.size / allProblems.length) * 100) : 0,
};
// 错题比例数据
const totalSubmissions = userSubmissions.length;
const wrongSubmissionsCount = userSubmissions.filter(s => s.status !== "AC").length;
const errorData = {
total: totalSubmissions,
wrong: wrongSubmissionsCount,
percentage: totalSubmissions > 0 ? Math.round((wrongSubmissionsCount / totalSubmissions) * 100) : 0,
};
//易错题列表(按错误次数排序)
const difficultProblems = Array.from(wrongSubmissions.entries())
.map(([problemId, errorCount]) => {
const problem = allProblems.find(p => p.id === problemId);
// 从 localizations 获取标题(英文优先)
const title = problem?.localizations?.find(l => l.content === "TITLE")?.content || "未知题目";
return {
id: problem?.displayId || problemId,
title: title, // 使用从 localizations 获取的标题
difficulty: problem?.difficulty || "未知",
errorCount: errorCount as number,
};
})
.sort((a, b) => b.errorCount - a.errorCount)
.slice(0, 10);
console.log("\n=== 学生仪表板数据 ===");
console.log(`题目完成情况: ${completionData.completed}/${completionData.total} (${completionData.percentage}%)`);
console.log(`提交正确率: ${errorData.total - errorData.wrong}/${errorData.total} (${100 - errorData.percentage}%)`);
console.log(`易错题数量: ${difficultProblems.length}`);
if (difficultProblems.length > 0) {
console.log("\n易错题列表:");
difficultProblems.forEach((problem, index) => {
console.log(`${index + 1}. 题目${problem.id} (${problem.title}) - ${problem.difficulty} - 错误${problem.errorCount}`);
});
}
console.log("\n测试完成");
}
testStudentDashboard()
.catch((e) => {
console.error("测试学生仪表板时出错:", e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@ -1,79 +0,0 @@
// import { PrismaClient } from "@/generated/client";
// const prisma = new PrismaClient();
// async function testStudentDataAccess() {
// console.log("测试学生数据访问...");
// try {
// // 1. 检查是否有学生用户
// const students = await prisma.user.findMany({
// where: { role: "GUEST" },
// select: { id: true, name: true, email: true }
// });
// console.log(`找到 ${students.length} 个学生用户:`);
// students.forEach(s => console.log(` - ${s.name} (${s.email})`));
// if (students.length === 0) {
// console.log("没有学生用户,创建测试学生...");
// const testStudent = await prisma.user.create({
// data: {
// name: "测试学生",
// email: "test_student@example.com",
// password: "$2b$10$SD1T/dYvKTArGdTmf8ERxuBKIONxY01/wSboRNaNsHnKZzDhps/0u",
// role: "GUEST",
// },
// });
// console.log(`创建学生: ${testStudent.name}`);
// }
// // 2. 检查已发布的题目
// const publishedProblems = await prisma.problem.findMany({
// where: { isPublished: true },
// select: { id: true,
// displayId: true,
// localizations: {
// where: { locale: "en", type: "TITLE" },
// select: { content: true }
// }
// }
// });
// console.log(`\n已发布题目数量: ${publishedProblems.length}`);
// publishedProblems.slice(0, 5).forEach((p: any) => {
// const title = p.localizations?.find((l: any) => l.type === "TITLE")?.content || "无标题";
// console.log(` - ${p.displayId}: ${title}`);
// });
// // 3. 检查提交记录
// const allSubmissions = await prisma.submission.findMany({
// select: { id: true, userId: true, problemId: true, status: true }
// });
// console.log(`\n总提交记录数: ${allSubmissions.length}`);
// // 4. 检查特定学生的提交记录
// const firstStudent = students[0] || await prisma.user.findFirst({ where: { role: "GUEST" } });
// if (firstStudent) {
// const studentSubmissions = await prisma.submission.findMany({
// where: { userId: firstStudent.id },
// include: {
// problem: { select: { displayId: true, localizations: {
// where: { locale: "en", type: "TITLE" },
// select: { content: true }
// } } }
// }
// });
// console.log(`\n学生 ${firstStudent.name} 的提交记录数: ${studentSubmissions.length}`);
// studentSubmissions.slice(0, 3).forEach(s => {
// console.log(` - 题目${s.problem.displayId}: ${s.status}`);
// });
// }
// console.log("\n数据访问测试完成");
// } catch (error) {
// console.error("测试过程中出错:", error);
// } finally {
// await prisma.$disconnect();
// }
// }
// testStudentDataAccess();

View File

@ -1,17 +0,0 @@
import { ProblemEditView } from "@/features/admin/ui/views/problem-edit-view";
// interface PageProps {
// params: Promise<{ problemId: string }>;
// }
const Page = async (
// { params }: PageProps
) => {
// const { problemId } = await params;
return <ProblemEditView
// problemId={problemId}
/>;
};
export default Page;

View File

@ -1,11 +0,0 @@
import { AdminProtectedLayout } from "@/features/admin/ui/layouts/admin-protected-layout";
interface LayoutProps {
children: React.ReactNode;
}
const Layout = ({ children }: LayoutProps) => {
return <AdminProtectedLayout>{children}</AdminProtectedLayout>;
};
export default Layout;

View File

@ -22,7 +22,7 @@ export async function getStudentDashboardData() {
// 检查用户是否存在
const currentUser = await prisma.user.findUnique({
where: { id: userId },
select: { id: true, name: true, email: true, role: true }
select: { id: true, name: true, email: true, role: true },
});
console.log("当前用户信息:", currentUser);
@ -31,7 +31,7 @@ export async function getStudentDashboardData() {
}
// 获取所有已发布的题目(包含英文标题)
const allProblems = await prisma.problem.findMany({
const allProblems = await prisma.problem.findMany({
where: { isPublished: true },
select: {
id: true,
@ -41,19 +41,22 @@ const allProblems = await prisma.problem.findMany({
where: {
type: "TITLE",
},
}
}
});
},
},
});
console.log("已发布题目数量:", allProblems.length);
console.log("题目列表:", allProblems.map(p => ({
console.log("已发布题目数量:", allProblems.length);
console.log(
"题目列表:",
allProblems.map((p) => ({
id: p.id,
displayId: p.displayId,
title: p.localizations[0]?.content || "无标题",
difficulty: p.difficulty
})));
difficulty: p.difficulty,
}))
);
// 获取当前学生的所有提交记录(包含题目英文标题)
// 获取当前学生的所有提交记录(包含题目英文标题)
const userSubmissions = await prisma.submission.findMany({
where: { userId: userId },
include: {
@ -65,25 +68,28 @@ console.log("题目列表:", allProblems.map(p => ({
localizations: {
where: {
type: "TITLE",
locale: "en" // 或者根据需求使用其他语言
locale: "en", // 或者根据需求使用其他语言
},
select: {
content: true
}
}
}
}
}
});
content: true,
},
},
},
},
},
});
console.log("当前用户提交记录数量:", userSubmissions.length);
console.log("提交记录详情:", userSubmissions.map(s => ({
console.log("当前用户提交记录数量:", userSubmissions.length);
console.log(
"提交记录详情:",
userSubmissions.map((s) => ({
problemId: s.problemId,
problemDisplayId: s.problem.displayId,
title: s.problem.localizations[0]?.content || "无标题",
difficulty: s.problem.difficulty,
status: s.status
})));
status: s.status,
}))
);
// 计算题目完成情况
const completedProblems = new Set<string | number>();
@ -110,7 +116,10 @@ console.log("提交记录详情:", userSubmissions.map(s => ({
const completionData = {
total: allProblems.length,
completed: completedProblems.size,
percentage: allProblems.length > 0 ? Math.round((completedProblems.size / allProblems.length) * 100) : 0,
percentage:
allProblems.length > 0
? Math.round((completedProblems.size / allProblems.length) * 100)
: 0,
};
// 错题比例数据 - 基于已完成的题目计算
@ -118,7 +127,10 @@ console.log("提交记录详情:", userSubmissions.map(s => ({
// 统计在已完成的题目中,哪些题目曾经有过错误提交
userSubmissions.forEach((submission) => {
if (submission.status !== "AC" && completedProblems.has(submission.problemId)) {
if (
submission.status !== "AC" &&
completedProblems.has(submission.problemId)
) {
wrongProblems.add(submission.problemId);
}
});
@ -126,7 +138,10 @@ console.log("提交记录详情:", userSubmissions.map(s => ({
const errorData = {
total: completedProblems.size, // 已完成的题目总数
wrong: wrongProblems.size, // 在已完成的题目中有过错误的题目数
percentage: completedProblems.size > 0 ? Math.round((wrongProblems.size / completedProblems.size) * 100) : 0,
percentage:
completedProblems.size > 0
? Math.round((wrongProblems.size / completedProblems.size) * 100)
: 0,
};
// 易错题列表(按错误次数排序)
@ -135,7 +150,9 @@ console.log("提交记录详情:", userSubmissions.map(s => ({
const problem = allProblems.find((p) => p.id === problemId);
// 从 problem.localizations 中获取标题
const title = problem?.localizations?.find((loc) => loc.type === "TITLE")?.content || "未知题目";
const title =
problem?.localizations?.find((loc) => loc.type === "TITLE")
?.content || "未知题目";
return {
id: problem?.displayId || problemId,
@ -153,7 +170,10 @@ console.log("提交记录详情:", userSubmissions.map(s => ({
difficultProblems,
pieChartData: [
{ name: "已完成", value: completionData.completed },
{ name: "未完成", value: completionData.total - completionData.completed },
{
name: "未完成",
value: completionData.total - completionData.completed,
},
],
errorPieChartData: [
{ name: "正确", value: errorData.total - errorData.wrong },
@ -170,6 +190,8 @@ console.log("提交记录详情:", userSubmissions.map(s => ({
return result;
} catch (error) {
console.error("获取学生仪表板数据失败:", error);
throw new Error(`获取数据失败: ${error instanceof Error ? error.message : '未知错误'}`);
throw new Error(
`获取数据失败: ${error instanceof Error ? error.message : "未知错误"}`
);
}
}

View File

@ -1,8 +1,8 @@
"use server";
import prisma from "@/lib/prisma";
import { Locale, Status, ProblemLocalization } from "@/generated/client";
import { getLocale } from "next-intl/server";
import { Locale, Status, ProblemLocalization } from "@/generated/client";
const getLocalizedTitle = (
localizations: ProblemLocalization[],
@ -38,32 +38,37 @@ export interface DifficultProblemData {
problemDisplayId: number;
}
export async function getProblemCompletionData(): Promise<ProblemCompletionData[]> {
export async function getProblemCompletionData(): Promise<
ProblemCompletionData[]
> {
// 获取所有提交记录,按题目分组统计
const submissions = await prisma.submission.findMany({
include: {
user: true,
problem: {
include:{
localizations:true
}
}
include: {
localizations: true,
},
},
},
});
const locale = await getLocale();
// 按题目分组统计完成情况(统计独立用户数)
const problemStats = new Map<string, {
const problemStats = new Map<
string,
{
completedUsers: Set<string>;
totalUsers: Set<string>;
title: string;
displayId: number;
}>();
}
>();
submissions.forEach((submission) => {
const localizations=submission.problem.localizations;
const title=getLocalizedTitle(localizations,locale as Locale);
const localizations = submission.problem.localizations;
const title = getLocalizedTitle(localizations, locale as Locale);
const problemId = submission.problemId;
const problemTitle = title;
const problemDisplayId = submission.problem.displayId;
@ -92,7 +97,8 @@ export async function getProblemCompletionData(): Promise<ProblemCompletionData[
}
// 转换为图表数据格式按题目displayId排序
const problemDataArray = Array.from(problemStats.entries()).map(([problemId, stats]) => {
const problemDataArray = Array.from(problemStats.entries()).map(
([problemId, stats]) => {
const completed = stats.completedUsers.size;
const total = stats.totalUsers.size;
@ -106,13 +112,18 @@ export async function getProblemCompletionData(): Promise<ProblemCompletionData[
completedPercent: total > 0 ? (completed / total) * 100 : 0,
uncompletedPercent: total > 0 ? ((total - completed) / total) * 100 : 0,
};
});
}
);
// 按题目编号排序
return problemDataArray.sort((a, b) => a.problemDisplayId - b.problemDisplayId);
return problemDataArray.sort(
(a, b) => a.problemDisplayId - b.problemDisplayId
);
}
export async function getDifficultProblemsData(): Promise<DifficultProblemData[]> {
export async function getDifficultProblemsData(): Promise<
DifficultProblemData[]
> {
// 获取所有测试用例结果
const testcaseResults = await prisma.testcaseResult.findMany({
include: {
@ -120,8 +131,8 @@ export async function getDifficultProblemsData(): Promise<DifficultProblemData[]
include: {
problem: {
include: {
localizations: true
}
localizations: true,
},
},
},
},
@ -134,19 +145,22 @@ export async function getDifficultProblemsData(): Promise<DifficultProblemData[]
});
// 按问题分组统计错误率
const problemStats = new Map<string, {
const problemStats = new Map<
string,
{
totalAttempts: number;
wrongAttempts: number;
title: string;
displayId: number;
users: Set<string>;
}>();
}
>();
testcaseResults.forEach((result) => {
const problemId = result.testcase.problemId;
const problemTitle = result.testcase.problem.localizations?.find(
(loc) => loc.type === "TITLE"
)?.content || "无标题";
const problemTitle =
result.testcase.problem.localizations?.find((loc) => loc.type === "TITLE")
?.content || "无标题";
const problemDisplayId = result.testcase.problem.displayId;
const userId = result.submission.userId;
const isWrong = !result.isCorrect;
@ -181,7 +195,8 @@ export async function getDifficultProblemsData(): Promise<DifficultProblemData[]
uniqueUsers: stats.users.size,
totalAttempts: stats.totalAttempts,
}))
.filter((problem) =>
.filter(
(problem) =>
problem.errorRate > 30 && // 错误率超过30%
problem.totalAttempts >= 3 // 至少有3次尝试
)

View File

@ -0,0 +1,13 @@
import { ProblemEditView } from "@/features/admin/ui/views/problem-edit-view";
interface PageProps {
params: Promise<{ problemId: string }>;
}
const Page = async ({ params }: PageProps) => {
const { problemId } = await params;
return <ProblemEditView problemId={problemId} />;
};
export default Page;

View File

@ -1,16 +1,16 @@
import { AppSidebar } from "@/components/sidebar/app-sidebar";
import { AdminSidebar } from "@/components/sidebar/admin-sidebar";
import { TeacherSidebar } from "@/components/sidebar/teacher-sidebar";
import { DynamicBreadcrumb } from "@/components/dynamic-breadcrumb";
import { Separator } from "@/components/ui/separator";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { auth } from "@/lib/auth";
import prisma from "@/lib/prisma";
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
import { Separator } from "@/components/ui/separator";
import { AppSidebar } from "@/components/sidebar/app-sidebar";
import { AdminSidebar } from "@/components/sidebar/admin-sidebar";
import { DynamicBreadcrumb } from "@/components/dynamic-breadcrumb";
import { TeacherSidebar } from "@/components/sidebar/teacher-sidebar";
interface LayoutProps {
children: React.ReactNode;
@ -33,7 +33,7 @@ export default async function Layout({ children }: LayoutProps) {
// 获取用户的完整信息(包括角色)
const fullUser = await prisma.user.findUnique({
where: { id: user.id },
select: { id: true, name: true, email: true, image: true, role: true }
select: { id: true, name: true, email: true, image: true, role: true },
});
if (!fullUser) {

View File

@ -1,9 +1,8 @@
// changePassword.ts
"use server";
import bcrypt from "bcryptjs";
import { auth } from "@/lib/auth";
import prisma from "@/lib/prisma";
import bcrypt from "bcryptjs";
export async function changePassword(formData: FormData) {
const oldPassword = formData.get("oldPassword") as string;

View File

@ -1,4 +1,3 @@
// getUserInfo.ts
"use server";
import { auth } from "@/lib/auth";

View File

@ -1,4 +1,3 @@
// index.ts
export { getUserInfo } from "./getUserInfo";
export { updateUserInfo } from "./updateUserInfo";
export { changePassword } from "./changePassword";

View File

@ -1,4 +1,3 @@
// updateUserInfo.ts
"use server";
import { auth } from "@/lib/auth";

View File

@ -1,7 +1,8 @@
// src/app/(app)/management/change-password/page.tsx
"use client";
import { useState } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { changePassword } from "@/app/(protected)/dashboard/management/actions/changePassword";
export default function ChangePasswordPage() {
@ -51,40 +52,46 @@ export default function ChangePasswordPage() {
setShowSuccess(true);
setTimeout(() => setShowSuccess(false), 3000);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : '修改密码失败';
const errorMessage =
error instanceof Error ? error.message : "修改密码失败";
alert(errorMessage);
}
};
return (
<div className="h-full w-full p-6">
<div className="h-full w-full bg-white shadow-lg rounded-xl p-8 flex flex-col">
<div className="h-full w-full bg-card shadow-lg rounded-xl p-8 flex flex-col">
<h1 className="text-2xl font-bold mb-6"></h1>
<form onSubmit={handleSubmit} className="space-y-5 flex-1 flex flex-col">
<form
onSubmit={handleSubmit}
className="space-y-5 flex-1 flex flex-col"
>
<div>
<label className="block text-sm font-medium mb-1"></label>
<input
<Input
type="password"
value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1"></label>
<input
<Input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
{newPassword && (
<p className="mt-1 text-xs text-gray-500">
<p className="mt-1 text-xs">
<span className={`inline-block w-12 h-2 rounded ${strengthColor}`}></span>
<span
className={`inline-block w-12 h-2 rounded ${strengthColor}`}
></span>
&nbsp;
<span className="text-sm">{strengthLabel}</span>
</p>
@ -93,25 +100,27 @@ export default function ChangePasswordPage() {
<div>
<label className="block text-sm font-medium mb-1"></label>
<input
<Input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
{newPassword && confirmPassword && newPassword !== confirmPassword && (
{newPassword &&
confirmPassword &&
newPassword !== confirmPassword && (
<p className="mt-1 text-xs text-red-500"></p>
)}
</div>
<div className="mt-auto">
<button
<Button
type="submit"
className="w-full bg-black hover:bg-gray-800 text-white font-semibold py-2 px-4 rounded-lg transition-colors"
className="w-full font-semibold py-2 px-4 rounded-lg transition-colors"
>
</button>
</Button>
</div>
</form>
</div>

View File

@ -1,58 +1,50 @@
"use client"
import React, { useState } from "react"
import { Separator } from "@/components/ui/separator"
import ProfilePage from "./profile/page"
import ChangePasswordPage from "./change-password/page"
"use client";
import { cn } from "@/lib/utils";
import React, { useState } from "react";
import ProfilePage from "./profile/page";
import { Button } from "@/components/ui/button";
import ChangePasswordPage from "./change-password/page";
export default function ManagementDefaultPage() {
const [activePage, setActivePage] = useState("profile")
const [activePage, setActivePage] = useState("profile");
const renderContent = () => {
switch (activePage) {
case "profile":
return <ProfilePage />
return <ProfilePage />;
case "change-password":
return <ChangePasswordPage />
return <ChangePasswordPage />;
default:
return <ProfilePage />
}
return <ProfilePage />;
}
};
return (
<div className="flex h-full w-full flex-col">
{/* 顶部导航栏 */}
<header className="bg-background sticky top-0 z-10 flex h-16 shrink-0 items-center gap-2 border-b px-4">
<Separator orientation="vertical" className="mr-2 h-4" />
{/* 页面切换按钮 */}
<div className="ml-auto flex space-x-2">
<button
<Button
className={cn("px-3 py-1 rounded-md text-sm transition-colors")}
variant={activePage === "profile" ? "default" : "secondary"}
onClick={() => setActivePage("profile")}
className={`px-3 py-1 rounded-md text-sm transition-colors ${
activePage === "profile"
? "bg-black text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
</button>
<button
</Button>
<Button
className={cn("px-3 py-1 rounded-md text-sm transition-colors")}
variant={activePage === "change-password" ? "default" : "secondary"}
onClick={() => setActivePage("change-password")}
className={`px-3 py-1 rounded-md text-sm transition-colors ${
activePage === "change-password"
? "bg-black text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
</button>
</Button>
</div>
</header>
{/* 主体内容 */}
<main className="flex-1 overflow-auto">
{renderContent()}
</main>
<main className="flex-1 overflow-auto">{renderContent()}</main>
</div>
)
);
}

View File

@ -1,7 +1,8 @@
// src/app/(app)/management/profile/page.tsx
"use client";
import { useEffect, useState } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { getUserInfo } from "@/app/(protected)/dashboard/management/actions/getUserInfo";
import { updateUserInfo } from "@/app/(protected)/dashboard/management/actions/updateUserInfo";
@ -34,8 +35,12 @@ export default function ProfilePage() {
}, []);
const handleSave = async () => {
const nameInput = document.getElementById("name") as HTMLInputElement | null;
const emailInput = document.getElementById("email") as HTMLInputElement | null;
const nameInput = document.getElementById(
"name"
) as HTMLInputElement | null;
const emailInput = document.getElementById(
"email"
) as HTMLInputElement | null;
if (!nameInput || !emailInput) {
alert("表单元素缺失");
@ -51,16 +56,17 @@ export default function ProfilePage() {
setUser(updatedUser);
setIsEditing(false);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : '更新用户信息失败';
const errorMessage =
error instanceof Error ? error.message : "更新用户信息失败";
alert(errorMessage);
}
};
};
if (!user) return <p>...</p>;
return (
<div className="h-full w-full p-6">
<div className="h-full w-full bg-white shadow-lg rounded-xl p-8 flex flex-col">
<div className="h-full w-full bg-card shadow-lg rounded-xl p-8 flex flex-col">
<h1 className="text-2xl font-bold mb-6"></h1>
<div className="flex items-center space-x-6 mb-6">
@ -71,79 +77,91 @@ export default function ProfilePage() {
</div>
<div>
{isEditing ? (
<input
<Input
id="name"
type="text"
defaultValue={user?.name || ""}
className="mt-1 block w-full border border-gray-300 rounded-md p-2"
className="mt-1 block w-full border rounded-md p-2"
/>
) : (
<h2 className="text-xl font-semibold">{user?.name || "未提供"}</h2>
<h2 className="text-xl font-semibold">
{user?.name || "未提供"}
</h2>
)}
<p className="text-gray-500">{user?.role}</p>
<p className="text-gray-500">{user.emailVerified ? new Date(user.emailVerified).toLocaleString() : "未验证"}</p>
<p>{user?.role}</p>
<p>
{user.emailVerified
? new Date(user.emailVerified).toLocaleString()
: "未验证"}
</p>
</div>
</div>
<hr className="border-gray-200 mb-6" />
<hr className="border-border mb-6" />
<div className="space-y-4 flex-1">
<div>
<label className="block text-sm font-medium text-gray-700">ID</label>
<p className="mt-1 text-lg font-medium text-gray-900">{user.id}</p>
<label className="block text-sm font-medium">ID</label>
<p className="mt-1 text-lg font-medium">{user.id}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700"></label>
<label className="block text-sm font-medium"></label>
{isEditing ? (
<input
<Input
id="email"
type="email"
defaultValue={user.email}
className="mt-1 block w-full border border-gray-300 rounded-md p-2"
className="mt-1 block w-full border rounded-md p-2"
/>
) : (
<p className="mt-1 text-lg font-medium text-gray-900">{user.email}</p>
<p className="mt-1 text-lg font-medium">{user.email}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700"></label>
<p className="mt-1 text-lg font-medium text-gray-900">{new Date(user.createdAt).toLocaleString()}</p>
<label className="block text-sm font-medium"></label>
<p className="mt-1 text-lg font-medium">
{new Date(user.createdAt).toLocaleString()}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700"></label>
<p className="mt-1 text-lg font-medium text-gray-900">{new Date(user.updatedAt).toLocaleString()}</p>
<label className="block text-sm font-medium"></label>
<p className="mt-1 text-lg font-medium">
{new Date(user.updatedAt).toLocaleString()}
</p>
</div>
</div>
<div className="pt-4 flex justify-end space-x-2">
{isEditing ? (
<>
<button
<Button
onClick={() => setIsEditing(false)}
type="button"
className="px-4 py-2 bg-gray-300 rounded-md hover:bg-gray-400 transition-colors"
className="px-4 py-2 rounded-md transition-colors"
>
</button>
<button
</Button>
<Button
onClick={handleSave}
type="button"
className="px-4 py-2 bg-black text-white rounded-md hover:bg-gray-800 transition-colors"
variant="secondary"
className="px-4 py-2 rounded-md transition-colors"
>
</button>
</Button>
</>
) : (
<button
<Button
onClick={() => setIsEditing(true)}
type="button"
className="px-4 py-2 bg-black text-white rounded-md hover:bg-gray-800 transition-colors"
className="px-4 py-2 rounded-md transition-colors"
>
</button>
</Button>
)}
</div>
</div>

View File

@ -1,10 +1,3 @@
import { auth } from "@/lib/auth";
import prisma from "@/lib/prisma";
import { redirect } from "next/navigation";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import {
Users,
BookOpen,
@ -14,9 +7,22 @@ import {
AlertCircle,
BarChart3,
Target,
Activity
Activity,
} from "lucide-react";
import Link from "next/link";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import prisma from "@/lib/prisma";
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
interface Stats {
totalUsers?: number;
@ -45,7 +51,7 @@ export default async function DashboardPage() {
// 获取用户的完整信息
const fullUser = await prisma.user.findUnique({
where: { id: user.id },
select: { id: true, name: true, email: true, image: true, role: true }
select: { id: true, name: true, email: true, image: true, role: true },
});
if (!fullUser) {
@ -58,27 +64,35 @@ export default async function DashboardPage() {
if (fullUser.role === "ADMIN") {
// 管理员统计
const [totalUsers, totalProblems, totalSubmissions, recentUsers] = await Promise.all([
const [totalUsers, totalProblems, totalSubmissions, recentUsers] =
await Promise.all([
prisma.user.count(),
prisma.problem.count(),
prisma.submission.count(),
prisma.user.findMany({
take: 5,
orderBy: { createdAt: "desc" },
select: { id: true, name: true, email: true, role: true, createdAt: true }
})
select: {
id: true,
name: true,
email: true,
role: true,
createdAt: true,
},
}),
]);
stats = { totalUsers, totalProblems, totalSubmissions };
recentActivity = recentUsers.map(user => ({
recentActivity = recentUsers.map((user) => ({
type: "新用户注册",
title: user.name || user.email,
description: `角色: ${user.role}`,
time: user.createdAt
time: user.createdAt,
}));
} else if (fullUser.role === "TEACHER") {
// 教师统计
const [totalStudents, totalProblems, totalSubmissions, recentSubmissions] = await Promise.all([
const [totalStudents, totalProblems, totalSubmissions, recentSubmissions] =
await Promise.all([
prisma.user.count({ where: { role: "GUEST" } }),
prisma.problem.count({ where: { isPublished: true } }),
prisma.submission.count(),
@ -90,30 +104,41 @@ export default async function DashboardPage() {
problem: {
select: {
displayId: true,
localizations: { where: { type: "TITLE", locale: "zh" }, select: { content: true } }
}
}
}
})
localizations: {
where: { type: "TITLE", locale: "zh" },
select: { content: true },
},
},
},
},
}),
]);
stats = { totalStudents, totalProblems, totalSubmissions };
recentActivity = recentSubmissions.map(sub => ({
recentActivity = recentSubmissions.map((sub) => ({
type: "学生提交",
title: `${sub.user.name || sub.user.email} 提交了题目 ${sub.problem.displayId}`,
description: sub.problem.localizations[0]?.content || `题目${sub.problem.displayId}`,
title: `${sub.user.name || sub.user.email} 提交了题目 ${
sub.problem.displayId
}`,
description:
sub.problem.localizations[0]?.content || `题目${sub.problem.displayId}`,
time: sub.createdAt,
status: sub.status
status: sub.status,
}));
} else {
// 学生统计
const [totalProblems, completedProblems, totalSubmissions, recentSubmissions] = await Promise.all([
const [
totalProblems,
completedProblems,
totalSubmissions,
recentSubmissions,
] = await Promise.all([
prisma.problem.count({ where: { isPublished: true } }),
prisma.submission.count({
where: {
userId: user.id,
status: "AC"
}
status: "AC",
},
}),
prisma.submission.count({ where: { userId: user.id } }),
prisma.submission.findMany({
@ -124,20 +149,24 @@ export default async function DashboardPage() {
problem: {
select: {
displayId: true,
localizations: { where: { type: "TITLE", locale: "zh" }, select: { content: true } }
}
}
}
})
localizations: {
where: { type: "TITLE", locale: "zh" },
select: { content: true },
},
},
},
},
}),
]);
stats = { totalProblems, completedProblems, totalSubmissions };
recentActivity = recentSubmissions.map(sub => ({
recentActivity = recentSubmissions.map((sub) => ({
type: "我的提交",
title: `题目 ${sub.problem.displayId}`,
description: sub.problem.localizations[0]?.content || `题目${sub.problem.displayId}`,
description:
sub.problem.localizations[0]?.content || `题目${sub.problem.displayId}`,
time: sub.createdAt,
status: sub.status
status: sub.status,
}));
}
@ -148,52 +177,129 @@ export default async function DashboardPage() {
title: "系统管理后台",
description: "管理整个系统的用户、题目和统计数据",
stats: [
{ label: "总用户数", value: stats.totalUsers, icon: Users, color: "text-blue-600" },
{ label: "总题目数", value: stats.totalProblems, icon: BookOpen, color: "text-green-600" },
{ label: "总提交数", value: stats.totalSubmissions, icon: Activity, color: "text-purple-600" }
{
label: "总用户数",
value: stats.totalUsers,
icon: Users,
color: "text-blue-600",
},
{
label: "总题目数",
value: stats.totalProblems,
icon: BookOpen,
color: "text-green-600",
},
{
label: "总提交数",
value: stats.totalSubmissions,
icon: Activity,
color: "text-purple-600",
},
],
actions: [
{ label: "用户管理", href: "/dashboard/usermanagement/guest", icon: Users },
{ label: "题目管理", href: "/dashboard/usermanagement/problem", icon: BookOpen },
{ label: "管理员设置", href: "/dashboard/management", icon: Target }
]
{
label: "用户管理",
href: "/dashboard/usermanagement/guest",
icon: Users,
},
{
label: "题目管理",
href: "/dashboard/usermanagement/problem",
icon: BookOpen,
},
{
label: "管理员设置",
href: "/dashboard/management",
icon: Target,
},
],
};
case "TEACHER":
return {
title: "教师教学平台",
description: "查看学生学习情况,管理教学资源",
stats: [
{ label: "学生数量", value: stats.totalStudents, icon: Users, color: "text-blue-600" },
{ label: "题目数量", value: stats.totalProblems, icon: BookOpen, color: "text-green-600" },
{ label: "提交数量", value: stats.totalSubmissions, icon: Activity, color: "text-purple-600" }
{
label: "学生数量",
value: stats.totalStudents,
icon: Users,
color: "text-blue-600",
},
{
label: "题目数量",
value: stats.totalProblems,
icon: BookOpen,
color: "text-green-600",
},
{
label: "提交数量",
value: stats.totalSubmissions,
icon: Activity,
color: "text-purple-600",
},
],
actions: [
{ label: "学生管理", href: "/dashboard/usermanagement/guest", icon: Users },
{ label: "题目管理", href: "/dashboard/usermanagement/problem", icon: BookOpen },
{ label: "统计分析", href: "/dashboard/teacher/dashboard", icon: BarChart3 }
]
{
label: "学生管理",
href: "/dashboard/usermanagement/guest",
icon: Users,
},
{
label: "题目管理",
href: "/dashboard/usermanagement/problem",
icon: BookOpen,
},
{
label: "统计分析",
href: "/dashboard/teacher/dashboard",
icon: BarChart3,
},
],
};
default:
return {
title: "我的学习中心",
description: "继续您的编程学习之旅",
stats: [
{ label: "总题目数", value: stats.totalProblems, icon: BookOpen, color: "text-blue-600" },
{ label: "已完成", value: stats.completedProblems, icon: CheckCircle, color: "text-green-600" },
{ label: "提交次数", value: stats.totalSubmissions, icon: Activity, color: "text-purple-600" }
{
label: "总题目数",
value: stats.totalProblems,
icon: BookOpen,
color: "text-blue-600",
},
{
label: "已完成",
value: stats.completedProblems,
icon: CheckCircle,
color: "text-green-600",
},
{
label: "提交次数",
value: stats.totalSubmissions,
icon: Activity,
color: "text-purple-600",
},
],
actions: [
{ label: "开始做题", href: "/problemset", icon: BookOpen },
{ label: "我的进度", href: "/dashboard/student/dashboard", icon: TrendingUp },
{ label: "个人设置", href: "/dashboard/management", icon: Target }
]
{
label: "我的进度",
href: "/dashboard/student/dashboard",
icon: TrendingUp,
},
{ label: "个人设置", href: "/dashboard/management", icon: Target },
],
};
}
};
const config = getRoleConfig();
const completionRate = fullUser.role === "GUEST" ?
((stats.totalProblems || 0) > 0 ? ((stats.completedProblems || 0) / (stats.totalProblems || 1)) * 100 : 0) : 0;
const completionRate =
fullUser.role === "GUEST"
? (stats.totalProblems || 0) > 0
? ((stats.completedProblems || 0) / (stats.totalProblems || 1)) * 100
: 0
: 0;
return (
<div className="space-y-6 p-6">
@ -214,7 +320,9 @@ export default async function DashboardPage() {
{config.stats.map((stat, index) => (
<Card key={index}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{stat.label}</CardTitle>
<CardTitle className="text-sm font-medium">
{stat.label}
</CardTitle>
<stat.icon className={`h-4 w-4 ${stat.color}`} />
</CardHeader>
<CardContent>
@ -233,7 +341,8 @@ export default async function DashboardPage() {
</CardTitle>
<CardDescription>
{stats.completedProblems || 0} / {stats.totalProblems || 0}
{stats.completedProblems || 0} / {stats.totalProblems || 0}{" "}
</CardDescription>
</CardHeader>
<CardContent>
@ -287,7 +396,9 @@ export default async function DashboardPage() {
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">{activity.title}</p>
<p className="text-sm text-muted-foreground">{activity.description}</p>
<p className="text-sm text-muted-foreground">
{activity.description}
</p>
</div>
<div className="text-sm text-muted-foreground">
{new Date(activity.time).toLocaleDateString()}

View File

@ -1,7 +1,5 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import {
Table,
TableBody,
@ -10,9 +8,11 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts";
import { useEffect, useState } from "react";
import { getStudentDashboardData } from "@/app/(protected)/dashboard/(userdashboard)/_actions/student-dashboard";
import { Progress } from "@/components/ui/progress";
import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { getStudentDashboardData } from "@/app/(protected)/dashboard/actions/student-dashboard";
interface DashboardData {
completionData: {
@ -86,7 +86,13 @@ export default function StudentDashboard() {
);
}
const { completionData, errorData, difficultProblems, pieChartData, errorPieChartData } = data;
const {
completionData,
errorData,
difficultProblems,
pieChartData,
errorPieChartData,
} = data;
const COLORS = ["#4CAF50", "#FFC107"];
return (
@ -102,8 +108,12 @@ export default function StudentDashboard() {
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span>{completionData.completed}/{completionData.total}</span>
<span className="text-green-500">{completionData.percentage}%</span>
<span>
{completionData.completed}/{completionData.total}
</span>
<span className="text-green-500">
{completionData.percentage}%
</span>
</div>
<Progress value={completionData.percentage} className="h-2" />
<div className="h-[200px]">
@ -119,9 +129,17 @@ export default function StudentDashboard() {
paddingAngle={5}
dataKey="value"
>
{pieChartData.map((entry: { name: string; value: number }, index: number) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
{pieChartData.map(
(
entry: { name: string; value: number },
index: number
) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
)
)}
</Pie>
</PieChart>
</ResponsiveContainer>
@ -138,7 +156,9 @@ export default function StudentDashboard() {
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span>{errorData.wrong}/{errorData.total}</span>
<span>
{errorData.wrong}/{errorData.total}
</span>
<span className="text-yellow-500">{errorData.percentage}%</span>
</div>
<Progress value={errorData.percentage} className="h-2" />
@ -155,9 +175,17 @@ export default function StudentDashboard() {
paddingAngle={5}
dataKey="value"
>
{errorPieChartData.map((entry: { name: string; value: number }, index: number) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
{errorPieChartData.map(
(
entry: { name: string; value: number },
index: number
) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
)
)}
</Pie>
</PieChart>
</ResponsiveContainer>
@ -188,14 +216,21 @@ export default function StudentDashboard() {
</TableRow>
</TableHeader>
<TableBody>
{difficultProblems.map((problem: { id: string | number; title: string; difficulty: string; errorCount: number }) => (
{difficultProblems.map(
(problem: {
id: string | number;
title: string;
difficulty: string;
errorCount: number;
}) => (
<TableRow key={problem.id}>
<TableCell>{problem.id}</TableCell>
<TableCell>{problem.title}</TableCell>
<TableCell>{problem.difficulty}</TableCell>
<TableCell>{problem.errorCount}</TableCell>
</TableRow>
))}
)
)}
</TableBody>
</Table>
) : (

View File

@ -1,10 +1,13 @@
"use client";
import { TrendingUp } from "lucide-react";
import { Bar, BarChart, XAxis, YAxis, LabelList, CartesianGrid } from "recharts";
import { Button } from "@/components/ui/button";
import { useState, useEffect } from "react";
import {
Bar,
BarChart,
XAxis,
YAxis,
LabelList,
CartesianGrid,
} from "recharts";
import {
Card,
CardContent,
@ -27,7 +30,14 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { getDashboardStats, ProblemCompletionData, DifficultProblemData } from "@/app/(protected)/dashboard/(userdashboard)/_actions/teacher-dashboard";
import { TrendingUp } from "lucide-react";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import {
getDashboardStats,
ProblemCompletionData,
DifficultProblemData,
} from "@/app/(protected)/dashboard/actions/teacher-dashboard";
const ITEMS_PER_PAGE = 5; // 每页显示的题目数量
@ -45,7 +55,9 @@ const chartConfig = {
export default function TeacherDashboard() {
const [currentPage, setCurrentPage] = useState(1);
const [chartData, setChartData] = useState<ProblemCompletionData[]>([]);
const [difficultProblems, setDifficultProblems] = useState<DifficultProblemData[]>([]);
const [difficultProblems, setDifficultProblems] = useState<
DifficultProblemData[]
>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@ -57,8 +69,8 @@ export default function TeacherDashboard() {
setChartData(data.problemData);
setDifficultProblems(data.difficultProblems);
} catch (err) {
setError(err instanceof Error ? err.message : '获取数据失败');
console.error('Failed to fetch dashboard data:', err);
setError(err instanceof Error ? err.message : "获取数据失败");
console.error("Failed to fetch dashboard data:", err);
} finally {
setLoading(false);
}
@ -178,7 +190,9 @@ export default function TeacherDashboard() {
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
onClick={() =>
setCurrentPage((prev) => Math.max(prev - 1, 1))
}
disabled={currentPage === 1}
>
@ -189,7 +203,9 @@ export default function TeacherDashboard() {
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
onClick={() =>
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
}
disabled={currentPage === totalPages}
>
@ -222,7 +238,9 @@ export default function TeacherDashboard() {
</div>
{difficultProblems.length === 0 ? (
<div className="flex items-center justify-center h-64">
<div className="text-lg text-muted-foreground"></div>
<div className="text-lg text-muted-foreground">
</div>
</div>
) : (
<Table>
@ -236,7 +254,10 @@ export default function TeacherDashboard() {
<TableBody>
{difficultProblems.map((problem) => (
<TableRow key={problem.id}>
<TableCell>{problem.problemDisplayId || problem.id.substring(0, 8)}</TableCell>
<TableCell>
{problem.problemDisplayId ||
problem.id.substring(0, 8)}
</TableCell>
<TableCell>{problem.problemTitle}</TableCell>
<TableCell>{problem.problemCount}</TableCell>
</TableRow>

View File

@ -1,7 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import { getDashboardStats } from "@/app/(protected)/dashboard/(userdashboard)/_actions/teacher-dashboard";
import { getDashboardStats } from "@/app/(protected)/dashboard/actions/teacher-dashboard";
interface DashboardData {
problemData: Array<{
@ -37,8 +37,8 @@ export default function TestDataPage() {
const result = await getDashboardStats();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : '获取数据失败');
console.error('Failed to fetch data:', err);
setError(err instanceof Error ? err.message : "获取数据失败");
console.error("Failed to fetch data:", err);
} finally {
setLoading(false);
}
@ -77,10 +77,14 @@ export default function TestDataPage() {
<div>
<h2 className="text-xl font-semibold mb-2"></h2>
<pre className="bg-gray-100 p-4 rounded overflow-auto">
{JSON.stringify({
{JSON.stringify(
{
totalProblems: data?.totalProblems,
totalDifficultProblems: data?.totalDifficultProblems,
}, null, 2)}
},
null,
2
)}
</pre>
</div>
</div>

View File

@ -1,14 +0,0 @@
'use server'
import prisma from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import type { Problem } from '@/generated/client'
export async function createProblem(data: Omit<Problem, 'id'|'createdAt'|'updatedAt'>) {
await prisma.problem.create({ data })
revalidatePath('/usermanagement/problem')
}
export async function deleteProblem(id: string) {
await prisma.problem.delete({ where: { id } })
revalidatePath('/usermanagement/problem')
}

View File

@ -1,46 +0,0 @@
'use server'
import prisma from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import bcrypt from 'bcryptjs'
import type { User } from '@/generated/client'
import { Role } from '@/generated/client'
type UserType = 'admin' | 'teacher' | 'guest'
export async function createUser(
userType: UserType,
data: Omit<User, 'id'|'createdAt'|'updatedAt'> & { password?: string }
) {
let password = data.password
if (password) {
password = await bcrypt.hash(password, 10)
}
const role = userType.toUpperCase() as Role
await prisma.user.create({ data: { ...data, password, role } })
revalidatePath(`/usermanagement/${userType}`)
}
export async function updateUser(
userType: UserType,
id: string,
data: Partial<Omit<User, 'id'|'createdAt'|'updatedAt'>>
) {
const updateData = { ...data }
// 如果包含密码字段且不为空,则进行加密
if (data.password && data.password.trim() !== '') {
updateData.password = await bcrypt.hash(data.password, 10)
} else {
// 如果密码为空,则从更新数据中移除密码字段,保持原密码不变
delete updateData.password
}
await prisma.user.update({ where: { id }, data: updateData })
revalidatePath(`/usermanagement/${userType}`)
}
export async function deleteUser(userType: UserType, id: string) {
await prisma.user.delete({ where: { id } })
revalidatePath(`/usermanagement/${userType}`)
}

View File

@ -1,10 +0,0 @@
import ProtectedLayout from "./ProtectedLayout";
interface GenericLayoutProps {
children: React.ReactNode;
allowedRoles: string[];
}
export default function GenericLayout({ children, allowedRoles }: GenericLayoutProps) {
return <ProtectedLayout allowedRoles={allowedRoles}>{children}</ProtectedLayout>;
}

View File

@ -0,0 +1,17 @@
"use server";
import prisma from "@/lib/prisma";
import { revalidatePath } from "next/cache";
import type { Problem } from "@/generated/client";
export async function createProblem(
data: Omit<Problem, "id" | "createdAt" | "updatedAt">
) {
await prisma.problem.create({ data });
revalidatePath("/usermanagement/problem");
}
export async function deleteProblem(id: string) {
await prisma.problem.delete({ where: { id } });
revalidatePath("/usermanagement/problem");
}

View File

@ -0,0 +1,47 @@
"use server";
import bcrypt from "bcryptjs";
import prisma from "@/lib/prisma";
import { Role } from "@/generated/client";
import { revalidatePath } from "next/cache";
import type { User } from "@/generated/client";
type UserType = "admin" | "teacher" | "guest";
export async function createUser(
userType: UserType,
data: Omit<User, "id" | "createdAt" | "updatedAt"> & { password?: string }
) {
let password = data.password;
if (password) {
password = await bcrypt.hash(password, 10);
}
const role = userType.toUpperCase() as Role;
await prisma.user.create({ data: { ...data, password, role } });
revalidatePath(`/usermanagement/${userType}`);
}
export async function updateUser(
userType: UserType,
id: string,
data: Partial<Omit<User, "id" | "createdAt" | "updatedAt">>
) {
const updateData = { ...data };
// 如果包含密码字段且不为空,则进行加密
if (data.password && data.password.trim() !== "") {
updateData.password = await bcrypt.hash(data.password, 10);
} else {
// 如果密码为空,则从更新数据中移除密码字段,保持原密码不变
delete updateData.password;
}
await prisma.user.update({ where: { id }, data: updateData });
revalidatePath(`/usermanagement/${userType}`);
}
export async function deleteUser(userType: UserType, id: string) {
await prisma.user.delete({ where: { id } });
revalidatePath(`/usermanagement/${userType}`);
}

View File

@ -1,5 +1,9 @@
import GenericLayout from "../_components/GenericLayout";
import GenericLayout from "../components/GenericLayout";
export default function AdminLayout({ children }: { children: React.ReactNode }) {
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
return <GenericLayout allowedRoles={["ADMIN"]}>{children}</GenericLayout>;
}

View File

@ -1,6 +1,6 @@
import GenericPage from '@/features/user-management/components/generic-page'
import { adminConfig } from '@/features/user-management/config/admin'
import { adminConfig } from "@/features/user-management/config/admin";
import GenericPage from "@/features/user-management/components/generic-page";
export default function AdminPage() {
return <GenericPage userType="admin" config={adminConfig} />
return <GenericPage userType="admin" config={adminConfig} />;
}

View File

@ -0,0 +1,15 @@
import ProtectedLayout from "./ProtectedLayout";
interface GenericLayoutProps {
children: React.ReactNode;
allowedRoles: string[];
}
export default function GenericLayout({
children,
allowedRoles,
}: GenericLayoutProps) {
return (
<ProtectedLayout allowedRoles={allowedRoles}>{children}</ProtectedLayout>
);
}

View File

@ -1,5 +1,5 @@
import { auth } from "@/lib/auth";
import prisma from "@/lib/prisma";
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
interface ProtectedLayoutProps {
@ -7,7 +7,10 @@ interface ProtectedLayoutProps {
allowedRoles: string[];
}
export default async function ProtectedLayout({ children, allowedRoles }: ProtectedLayoutProps) {
export default async function ProtectedLayout({
children,
allowedRoles,
}: ProtectedLayoutProps) {
const session = await auth();
const userId = session?.user?.id;
@ -17,7 +20,7 @@ export default async function ProtectedLayout({ children, allowedRoles }: Protec
const user = await prisma.user.findUnique({
where: { id: userId },
select: { role: true }
select: { role: true },
});
if (!user || !allowedRoles.includes(user.role)) {

View File

@ -1,5 +1,13 @@
import GenericLayout from "../_components/GenericLayout";
import GenericLayout from "../components/GenericLayout";
export default function GuestLayout({ children }: { children: React.ReactNode }) {
return <GenericLayout allowedRoles={["ADMIN", "TEACHER"]}>{children}</GenericLayout>;
export default function GuestLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<GenericLayout allowedRoles={["ADMIN", "TEACHER"]}>
{children}
</GenericLayout>
);
}

View File

@ -1,6 +1,6 @@
import GenericPage from '@/features/user-management/components/generic-page'
import { guestConfig } from '@/features/user-management/config/guest'
import { guestConfig } from "@/features/user-management/config/guest";
import GenericPage from "@/features/user-management/components/generic-page";
export default function GuestPage() {
return <GenericPage userType="guest" config={guestConfig} />
return <GenericPage userType="guest" config={guestConfig} />;
}

View File

@ -1,5 +1,13 @@
import GenericLayout from "../_components/GenericLayout";
import GenericLayout from "../components/GenericLayout";
export default function ProblemLayout({ children }: { children: React.ReactNode }) {
return <GenericLayout allowedRoles={["ADMIN", "TEACHER"]}>{children}</GenericLayout>;
export default function ProblemLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<GenericLayout allowedRoles={["ADMIN", "TEACHER"]}>
{children}
</GenericLayout>
);
}

View File

@ -1,6 +1,6 @@
import GenericPage from '@/features/user-management/components/generic-page'
import { problemConfig } from '@/features/user-management/config/problem'
import { problemConfig } from "@/features/user-management/config/problem";
import GenericPage from "@/features/user-management/components/generic-page";
export default function ProblemPage() {
return <GenericPage userType="problem" config={problemConfig} />
return <GenericPage userType="problem" config={problemConfig} />;
}

View File

@ -1,5 +1,9 @@
import GenericLayout from "../_components/GenericLayout";
import GenericLayout from "../components/GenericLayout";
export default function TeacherLayout({ children }: { children: React.ReactNode }) {
export default function TeacherLayout({
children,
}: {
children: React.ReactNode;
}) {
return <GenericLayout allowedRoles={["ADMIN"]}>{children}</GenericLayout>;
}

View File

@ -1,6 +1,6 @@
import GenericPage from '@/features/user-management/components/generic-page'
import { teacherConfig } from '@/features/user-management/config/teacher'
import { teacherConfig } from "@/features/user-management/config/teacher";
import GenericPage from "@/features/user-management/components/generic-page";
export default function TeacherPage() {
return <GenericPage userType="teacher" config={teacherConfig} />
return <GenericPage userType="teacher" config={teacherConfig} />;
}

View File

@ -33,13 +33,13 @@
--chart-5: 213 16% 16%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
--sidebar-foreground: 213 13% 6%;
--sidebar-primary: 213 13% 16%;
--sidebar-primary-foreground: 213 13% 76%;
--sidebar-accent: 0 0% 85%;
--sidebar-accent-foreground: 0 0% 25%;
--sidebar-border: 0 0% 95%;
--sidebar-ring: 213 13% 16%;
}
.dark {
@ -67,14 +67,14 @@
--chart-3: 216 28% 22%;
--chart-4: 210 7% 28%;
--chart-5: 210 20% 82%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
--sidebar-background: 216 28% 5%;
--sidebar-foreground: 210 17% 92%;
--sidebar-primary: 210 17% 82%;
--sidebar-primary-foreground: 210 17% 22%;
--sidebar-accent: 216 28% 22%;
--sidebar-accent-foreground: 216 28% 82%;
--sidebar-border: 216 18% 12%;
--sidebar-ring: 210 17% 82%;
}
}
@ -119,14 +119,3 @@ code[data-theme*=" "] span {
color: var(--shiki-dark);
background-color: var(--shiki-dark-bg);
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -6,7 +6,6 @@ import { NextIntlClientProvider } from "next-intl";
import { ThemeProvider } from "@/components/theme-provider";
import { SettingsDialog } from "@/components/settings-dialog";
export const metadata: Metadata = {
title: "Judge4c",
description:

View File

@ -1,4 +1,3 @@
import { Button } from "@/components/ui/button"
import {
DialogContent,
DialogDescription,
@ -6,9 +5,10 @@ import {
DialogHeader,
DialogTitle,
DialogClose,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
export function ShareDialogContent({ link }: { link: string }) {
return (
@ -35,5 +35,5 @@ export function ShareDialogContent({ link }: { link: string }) {
</DialogClose>
</DialogFooter>
</DialogContent>
)
);
}

View File

@ -1,37 +1,54 @@
"use client"
"use client";
import * as React from "react"
import {
Check,
X,
Info,
AlertTriangle,
Copy,
Check as CheckIcon,
} from "lucide-react";
import Link from "next/link";
import * as React from "react";
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Check, X, Info, AlertTriangle, Copy, Check as CheckIcon } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import Link from "next/link"
} from "@/components/ui/dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
export function WrongbookDialog({ problems, children }: { problems: { id: string; name: string; status: string; url?: string }[]; children?: React.ReactNode }) {
const [copiedId, setCopiedId] = React.useState<string | null>(null)
export function WrongbookDialog({
problems,
children,
}: {
problems: { id: string; name: string; status: string; url?: string }[];
children?: React.ReactNode;
}) {
const [copiedId, setCopiedId] = React.useState<string | null>(null);
const handleCopyLink = async (item: { id: string; url?: string }) => {
const link = `${window.location.origin}/problems/${item.id}`
const link = `${window.location.origin}/problems/${item.id}`;
try {
await navigator.clipboard.writeText(link)
setCopiedId(item.id)
setTimeout(() => setCopiedId(null), 2000) // 2秒后重置状态
await navigator.clipboard.writeText(link);
setCopiedId(item.id);
setTimeout(() => setCopiedId(null), 2000); // 2秒后重置状态
} catch (err) {
console.error('Failed to copy link:', err)
}
console.error("Failed to copy link:", err);
}
};
return (
<Dialog>
<DialogTrigger asChild>
{children ? children : (
<button className="px-4 py-2 rounded bg-primary text-primary-foreground hover:bg-primary/90"></button>
{children ? (
children
) : (
<button className="px-4 py-2 rounded bg-primary text-primary-foreground hover:bg-primary/90">
</button>
)}
</DialogTrigger>
<DialogContent className="max-w-2xl p-0">
@ -44,13 +61,18 @@ export function WrongbookDialog({ problems, children }: { problems: { id: string
<thead>
<tr className="border-b bg-muted/50">
<th className="px-3 py-2 text-left font-semibold"></th>
<th className="px-3 py-2 text-left font-semibold"></th>
<th className="px-3 py-2 text-left font-semibold">
</th>
<th className="px-3 py-2 text-left font-semibold"></th>
</tr>
</thead>
<tbody>
{problems.map((item) => (
<tr key={item.id} className="border-b last:border-0 hover:bg-muted/30 transition">
<tr
key={item.id}
className="border-b last:border-0 hover:bg-muted/30 transition"
>
<td className="px-3 py-2">
<Button
variant="ghost"
@ -66,7 +88,10 @@ export function WrongbookDialog({ problems, children }: { problems: { id: string
</Button>
</td>
<td className="px-3 py-2">
<Link href={item.url || `/problems/${item.id}`} className="text-primary underline underline-offset-2 hover:text-primary/80">
<Link
href={item.url || `/problems/${item.id}`}
className="text-primary underline underline-offset-2 hover:text-primary/80"
>
{item.name}
</Link>
</td>
@ -74,28 +99,46 @@ export function WrongbookDialog({ problems, children }: { problems: { id: string
{(() => {
if (item.status === "AC") {
return (
<Badge className="bg-green-500 text-white" variant="default">
<Check className="w-3 h-3 mr-1" />{item.status}
<Badge
className="bg-green-500 text-white"
variant="default"
>
<Check className="w-3 h-3 mr-1" />
{item.status}
</Badge>
)
);
} else if (item.status === "WA") {
return (
<Badge className="bg-red-500 text-white" variant="destructive">
<X className="w-3 h-3 mr-1" />{item.status}
<Badge
className="bg-red-500 text-white"
variant="destructive"
>
<X className="w-3 h-3 mr-1" />
{item.status}
</Badge>
)
} else if (["RE", "CE", "MLE", "TLE"].includes(item.status)) {
);
} else if (
["RE", "CE", "MLE", "TLE"].includes(item.status)
) {
return (
<Badge className="bg-orange-500 text-white" variant="secondary">
<AlertTriangle className="w-3 h-3 mr-1" />{item.status}
<Badge
className="bg-orange-500 text-white"
variant="secondary"
>
<AlertTriangle className="w-3 h-3 mr-1" />
{item.status}
</Badge>
)
);
} else {
return (
<Badge className="bg-gray-200 text-gray-700" variant="secondary">
<Info className="w-3 h-3 mr-1" />{item.status}
<Badge
className="bg-gray-200 text-gray-700"
variant="secondary"
>
<Info className="w-3 h-3 mr-1" />
{item.status}
</Badge>
)
);
}
})()}
</td>
@ -107,5 +150,5 @@ export function WrongbookDialog({ problems, children }: { problems: { id: string
</div>
</DialogContent>
</Dialog>
)
);
}

View File

@ -0,0 +1,16 @@
"use client";
import { useRouter } from "next/navigation";
import { LayoutDashboardIcon } from "lucide-react";
import { DropdownMenuItem } from "./ui/dropdown-menu";
export const DashboardButton = () => {
const router = useRouter();
return (
<DropdownMenuItem onClick={() => router.push("/dashboard")}>
<LayoutDashboardIcon />
Dashboard
</DropdownMenuItem>
);
};

View File

@ -1,6 +1,5 @@
"use client"
"use client";
import { usePathname } from "next/navigation"
import {
Breadcrumb,
BreadcrumbItem,
@ -8,76 +7,77 @@ import {
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
} from "@/components/ui/breadcrumb";
import { usePathname } from "next/navigation";
interface BreadcrumbItem {
label: string
href?: string
label: string;
href?: string;
}
export function DynamicBreadcrumb() {
const pathname = usePathname()
const pathname = usePathname();
const generateBreadcrumbs = (): BreadcrumbItem[] => {
const segments = pathname.split('/').filter(Boolean)
const breadcrumbs: BreadcrumbItem[] = []
const segments = pathname.split("/").filter(Boolean);
const breadcrumbs: BreadcrumbItem[] = [];
// 添加首页
breadcrumbs.push({ label: "首页", href: "/" })
breadcrumbs.push({ label: "首页", href: "/" });
let currentPath = ""
let currentPath = "";
segments.forEach((segment, index) => {
currentPath += `/${segment}`
currentPath += `/${segment}`;
// 根据路径段生成标签
let label = segment
let label = segment;
// 路径映射
const pathMap: Record<string, string> = {
'dashboard': '仪表板',
'management': '管理面板',
'profile': '用户信息',
'change-password': '修改密码',
'problems': '题目',
'problemset': '题目集',
'admin': '管理后台',
'teacher': '教师平台',
'student': '学生平台',
'usermanagement': '用户管理',
'userdashboard': '用户仪表板',
'protected': '受保护',
'app': '应用',
'auth': '认证',
'sign-in': '登录',
'sign-up': '注册',
}
dashboard: "仪表板",
management: "管理面板",
profile: "用户信息",
"change-password": "修改密码",
problems: "题目",
problemset: "题目集",
admin: "管理后台",
teacher: "教师平台",
student: "学生平台",
usermanagement: "用户管理",
userdashboard: "用户仪表板",
protected: "受保护",
app: "应用",
auth: "认证",
"sign-in": "登录",
"sign-up": "注册",
};
// 如果是数字可能是题目ID显示为"题目详情"
if (/^\d+$/.test(segment)) {
label = "详情"
label = "详情";
} else if (pathMap[segment]) {
label = pathMap[segment]
label = pathMap[segment];
} else {
// 将 kebab-case 转换为中文
label = segment
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
// 最后一个项目不添加链接
if (index === segments.length - 1) {
breadcrumbs.push({ label })
breadcrumbs.push({ label });
} else {
breadcrumbs.push({ label, href: currentPath })
breadcrumbs.push({ label, href: currentPath });
}
})
});
return breadcrumbs
}
return breadcrumbs;
};
const breadcrumbs = generateBreadcrumbs()
const breadcrumbs = generateBreadcrumbs();
return (
<Breadcrumb>
@ -86,13 +86,9 @@ export function DynamicBreadcrumb() {
<div key={index} className="flex items-center">
<BreadcrumbItem className="hidden md:block">
{item.href ? (
<BreadcrumbLink href={item.href}>
{item.label}
</BreadcrumbLink>
<BreadcrumbLink href={item.href}>{item.label}</BreadcrumbLink>
) : (
<BreadcrumbPage>
{item.label}
</BreadcrumbPage>
<BreadcrumbPage>{item.label}</BreadcrumbPage>
)}
</BreadcrumbItem>
{index < breadcrumbs.length - 1 && (
@ -102,5 +98,5 @@ export function DynamicBreadcrumb() {
))}
</BreadcrumbList>
</Breadcrumb>
)
);
}

View File

@ -1,12 +1,5 @@
"use client"
"use client";
import { ChevronRight, type LucideIcon } from "lucide-react"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
SidebarGroup,
SidebarGroupLabel,
@ -17,21 +10,27 @@ import {
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/components/ui/sidebar"
} from "@/components/ui/sidebar";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ChevronRight, type LucideIcon } from "lucide-react";
export function NavMain({
items,
}: {
items: {
title: string
url: string
icon: LucideIcon
isActive?: boolean
title: string;
url: string;
icon: LucideIcon;
isActive?: boolean;
items?: {
title: string
url: string
}[]
}[]
title: string;
url: string;
}[];
}[];
}) {
return (
<SidebarGroup>
@ -74,5 +73,5 @@ export function NavMain({
))}
</SidebarMenu>
</SidebarGroup>
)
);
}

View File

@ -1,4 +1,4 @@
"use client"
"use client";
import {
BookX,
@ -9,19 +9,7 @@ import {
X,
Info,
AlertTriangle,
} from "lucide-react"
import React, { useState } from "react"
import { useRouter } from "next/navigation"
import {
Dialog,
} from "@/components/ui/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
} from "lucide-react";
import {
SidebarGroup,
SidebarGroupLabel,
@ -30,24 +18,33 @@ import {
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import { WrongbookDialog } from "@/components/UncompletedProject/wrongbook-dialog"
import { ShareDialogContent } from "@/components/UncompletedProject/sharedialog"
} from "@/components/ui/sidebar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import { Dialog } from "@/components/ui/dialog";
import { ShareDialogContent } from "@/components/UncompletedProject/sharedialog";
import { WrongbookDialog } from "@/components/UncompletedProject/wrongbook-dialog";
export function NavProjects({
projects,
}: {
projects: {
id: string
name: string
status: string
url?: string
}[]
id: string;
name: string;
status: string;
url?: string;
}[];
}) {
const { isMobile } = useSidebar()
const [shareOpen, setShareOpen] = useState(false)
const [shareLink, setShareLink] = useState("")
const router = useRouter()
const { isMobile } = useSidebar();
const [shareOpen, setShareOpen] = useState(false);
const [shareLink, setShareLink] = useState("");
const router = useRouter();
return (
<>
@ -73,28 +70,30 @@ export function NavProjects({
<Check className="w-3 h-3" />
{item.status}
</span>
)
);
} else if (item.status === "WA") {
return (
<span className="ml-2 min-w-[60px] text-xs text-right px-2 py-0.5 rounded-full border flex items-center gap-1 border-red-500 bg-red-500 text-white">
<X className="w-3 h-3" />
{item.status}
</span>
)
} else if (["RE", "CE", "MLE", "TLE"].includes(item.status)) {
);
} else if (
["RE", "CE", "MLE", "TLE"].includes(item.status)
) {
return (
<span className="ml-2 min-w-[60px] text-xs text-right px-2 py-0.5 rounded-full border flex items-center gap-1 border-orange-500 bg-orange-500 text-white">
<AlertTriangle className="w-3 h-3" />
{item.status}
</span>
)
);
} else {
return (
<span className="ml-2 min-w-[60px] text-xs text-right px-2 py-0.5 rounded-full border flex items-center gap-1 border-gray-400 bg-gray-100 text-gray-700">
<Info className="w-3 h-3" />
{item.status}
</span>
)
);
}
})()}
</span>
@ -114,11 +113,11 @@ export function NavProjects({
>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
e.stopPropagation();
if (item.url) {
router.push(item.url)
router.push(item.url);
} else {
router.push(`/problems/${item.id}`)
router.push(`/problems/${item.id}`);
}
}}
>
@ -127,9 +126,11 @@ export function NavProjects({
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
setShareLink(`${window.location.origin}/problems/${item.id}`)
setShareOpen(true)
e.stopPropagation();
setShareLink(
`${window.location.origin}/problems/${item.id}`
);
setShareOpen(true);
}}
>
<Share className="text-muted-foreground mr-2" />
@ -153,5 +154,5 @@ export function NavProjects({
<ShareDialogContent link={shareLink} />
</Dialog>
</>
)
);
}

View File

@ -1,23 +1,22 @@
import * as React from "react"
import { type LucideIcon } from "lucide-react"
import * as React from "react";
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
} from "@/components/ui/sidebar";
import { type LucideIcon } from "lucide-react";
export function NavSecondary({
items,
...props
}: {
items: {
title: string
url: string
icon: LucideIcon
}[]
title: string;
url: string;
icon: LucideIcon;
}[];
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
return (
<SidebarGroup {...props}>
@ -36,5 +35,5 @@ export function NavSecondary({
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)
);
}

View File

@ -1,21 +1,11 @@
"use client"
"use client";
import {
BadgeCheck,
// Bell,
ChevronsUpDown,
UserPen,
LogOut,
// Sparkles,
} from "lucide-react"
import { useRouter } from "next/navigation"
import { signOut } from "next-auth/react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import {
DropdownMenu,
DropdownMenuContent,
@ -24,30 +14,28 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
} from "@/components/ui/dropdown-menu";
import { signOut } from "next-auth/react";
import { useRouter } from "next/navigation";
import { BadgeCheck, ChevronsUpDown, UserPen, LogOut } from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
export function NavUser({
user,
}: {
user: {
name: string
email: string
avatar: string
}
name: string;
email: string;
avatar: string;
};
}) {
const { isMobile } = useSidebar()
const router = useRouter()
const { isMobile } = useSidebar();
const router = useRouter();
async function handleLogout() {
await signOut({
callbackUrl: "/sign-in",
redirect: true
redirect: true,
});
}
@ -98,13 +86,6 @@ export function NavUser({
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{/* <DropdownMenuGroup>
<DropdownMenuItem>
<Sparkles />
Update
</DropdownMenuItem>
</DropdownMenuGroup> */}
{/* <DropdownMenuSeparator /> */}
<DropdownMenuGroup>
<DropdownMenuItem onClick={handleAccount}>
<BadgeCheck />
@ -114,10 +95,6 @@ export function NavUser({
<UserPen />
Switch User
</DropdownMenuItem>
{/* <DropdownMenuItem >
<Bell />
Notifications
</DropdownMenuItem> */}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}>
@ -128,5 +105,5 @@ export function NavUser({
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
);
}

View File

@ -1,15 +1,6 @@
"use client"
import { siteConfig } from "@/config/site"
import * as React from "react"
import {
LifeBuoy,
Send,
Shield,
} from "lucide-react"
"use client";
import { NavMain } from "@/components/nav-main"
import { NavSecondary } from "@/components/nav-secondary"
import { NavUser } from "@/components/nav-user"
import * as React from "react";
import {
Sidebar,
SidebarContent,
@ -18,14 +9,19 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
import { User } from "next-auth"
} from "@/components/ui/sidebar";
import { User } from "next-auth";
import { siteConfig } from "@/config/site";
import { NavMain } from "@/components/nav-main";
import { NavUser } from "@/components/nav-user";
import { LifeBuoy, Send, Shield } from "lucide-react";
import { NavSecondary } from "@/components/nav-secondary";
const adminData = {
navMain: [
{
title: "管理面板",
url: "#",
url: "/dashboard",
icon: Shield,
isActive: true,
items: [
@ -35,19 +31,21 @@ const adminData = {
{ title: "题目管理", url: "/dashboard/usermanagement/problem" },
],
},
],
navSecondary: [
{ title: "帮助", url: "/", icon: LifeBuoy },
{ title: "反馈", url: siteConfig.url.repo.github, icon: Send },
],
}
};
interface AdminSidebarProps {
user: User;
}
export function AdminSidebar({ user, ...props }: AdminSidebarProps & React.ComponentProps<typeof Sidebar>) {
export function AdminSidebar({
user,
...props
}: AdminSidebarProps & React.ComponentProps<typeof Sidebar>) {
const userInfo = {
name: user.name ?? "管理员",
email: user.email ?? "",
@ -81,5 +79,5 @@ export function AdminSidebar({ user, ...props }: AdminSidebarProps & React.Compo
<NavUser user={userInfo} />
</SidebarFooter>
</Sidebar>
)
);
}

View File

@ -1,20 +1,6 @@
"use client";
import { siteConfig } from "@/config/site";
import * as React from "react";
import {
// BookOpen,
Command,
LifeBuoy,
Send,
// Settings2,
SquareTerminal,
} from "lucide-react";
import { NavMain } from "@/components/nav-main";
import { NavProjects } from "@/components/nav-projects";
import { NavSecondary } from "@/components/nav-secondary";
import { NavUser } from "@/components/nav-user";
import {
Sidebar,
SidebarContent,
@ -25,6 +11,12 @@ import {
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { User } from "next-auth";
import { siteConfig } from "@/config/site";
import { NavMain } from "@/components/nav-main";
import { NavUser } from "@/components/nav-user";
import { NavProjects } from "@/components/nav-projects";
import { NavSecondary } from "@/components/nav-secondary";
import { Command, LifeBuoy, Send, SquareTerminal } from "lucide-react";
const data = {
navMain: [

View File

@ -1,18 +1,13 @@
"use client"
import { siteConfig } from "@/config/site"
import * as React from "react"
"use client";
import {
Command,
LifeBuoy,
PieChart,
Send,
// Settings2,
SquareTerminal,
} from "lucide-react"
import { NavMain } from "@/components/nav-main"
import { NavSecondary } from "@/components/nav-secondary"
import { NavUser } from "@/components/nav-user"
} from "lucide-react";
import * as React from "react";
import {
Sidebar,
SidebarContent,
@ -21,8 +16,12 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
import { User } from "next-auth"
} from "@/components/ui/sidebar";
import { User } from "next-auth";
import { siteConfig } from "@/config/site";
import { NavMain } from "@/components/nav-main";
import { NavUser } from "@/components/nav-user";
import { NavSecondary } from "@/components/nav-secondary";
const data = {
navMain: [
@ -81,13 +80,16 @@ const data = {
icon: Send,
},
],
}
};
interface TeacherSidebarProps {
user: User;
}
export function TeacherSidebar({ user, ...props }: TeacherSidebarProps & React.ComponentProps<typeof Sidebar>) {
export function TeacherSidebar({
user,
...props
}: TeacherSidebarProps & React.ComponentProps<typeof Sidebar>) {
const userInfo = {
name: user.name ?? "",
email: user.email ?? "",
@ -121,5 +123,5 @@ export function TeacherSidebar({ user, ...props }: TeacherSidebarProps & React.C
<NavUser user={userInfo} />
</SidebarFooter>
</Sidebar>
)
);
}

View File

@ -13,6 +13,7 @@ import { auth, signIn, signOut } from "@/lib/auth";
import { getTranslations } from "next-intl/server";
import { Skeleton } from "@/components/ui/skeleton";
import { SettingsButton } from "@/components/settings-button";
import { DashboardButton } from "@/components/dashboard-button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
const handleLogIn = async () => {
@ -88,6 +89,7 @@ const UserAvatar = async () => {
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DashboardButton />
<SettingsButton />
<DropdownMenuItem onClick={handleLogOut}>
<LogOutIcon />

View File

@ -0,0 +1,32 @@
import prisma from "@/lib/prisma";
import { Role } from "@/generated/client";
import { auth, signIn } from "@/lib/auth";
import { redirect } from "next/navigation";
interface ProtectedLayoutProps {
roles: Role[];
children: React.ReactNode;
}
export const ProtectedLayout = async ({
roles,
children,
}: ProtectedLayoutProps) => {
const session = await auth();
const userId = session?.user?.id;
if (!userId) {
await signIn();
}
const user = await prisma.user.findUnique({
where: { id: userId },
select: { role: true },
});
if (!user || !roles.includes(user.role)) {
redirect("unauthorized");
}
return <>{children}</>;
};

View File

@ -1,28 +1,26 @@
// import EditCodePanel from "@/components/creater/edit-code-panel";
// import EditDetailPanel from "@/components/creater/edit-detail-panel";
// import EditSolutionPanel from "@/components/creater/edit-solution-panel";
// import EditTestcasePanel from "@/components/creater/edit-testcase-panel";
// import EditDescriptionPanel from "@/components/creater/edit-description-panel";
// import { ProblemEditFlexLayout } from "@/features/admin/ui/components/problem-edit-flexlayout";
import EditCodePanel from "@/components/creater/edit-code-panel";
import EditDetailPanel from "@/components/creater/edit-detail-panel";
import EditSolutionPanel from "@/components/creater/edit-solution-panel";
import EditTestcasePanel from "@/components/creater/edit-testcase-panel";
import EditDescriptionPanel from "@/components/creater/edit-description-panel";
import { ProblemEditFlexLayout } from "@/features/admin/ui/components/problem-edit-flexlayout";
// interface ProblemEditViewProps {
// problemId: string;
// }
interface ProblemEditViewProps {
problemId: string;
}
export const ProblemEditView = (
// { problemId }: ProblemEditViewProps
) => {
// const components: Record<string, React.ReactNode> = {
// description: <EditDescriptionPanel problemId={problemId} />,
// solution: <EditSolutionPanel problemId={problemId} />,
// detail: <EditDetailPanel problemId={problemId} />,
// code: <EditCodePanel problemId={problemId} />,
// testcase: <EditTestcasePanel problemId={problemId} />,
// };
export const ProblemEditView = ({ problemId }: ProblemEditViewProps) => {
const components: Record<string, React.ReactNode> = {
detail: <EditDetailPanel problemId={problemId} />,
description: <EditDescriptionPanel problemId={problemId} />,
solution: <EditSolutionPanel problemId={problemId} />,
code: <EditCodePanel problemId={problemId} />,
testcase: <EditTestcasePanel problemId={problemId} />,
};
return (
<div className="relative flex h-full w-full">
{/* <ProblemEditFlexLayout components={components} /> */}
<ProblemEditFlexLayout components={components} />
</div>
);
};

View File

@ -1,21 +1,24 @@
import { UserTable } from './user-table'
import { UserConfig } from './user-table'
import prisma from '@/lib/prisma'
import type { User, Problem } from '@/generated/client'
import { Role } from '@/generated/client'
import prisma from "@/lib/prisma";
import { UserTable } from "./user-table";
import { Role } from "@/generated/client";
import { UserConfig } from "./user-table";
import type { User, Problem } from "@/generated/client";
interface GenericPageProps {
userType: 'admin' | 'teacher' | 'guest' | 'problem'
config: UserConfig
userType: "admin" | "teacher" | "guest" | "problem";
config: UserConfig;
}
export default async function GenericPage({ userType, config }: GenericPageProps) {
if (userType === 'problem') {
const data: Problem[] = await prisma.problem.findMany({})
return <UserTable config={config} data={data} />
export default async function GenericPage({
userType,
config,
}: GenericPageProps) {
if (userType === "problem") {
const data: Problem[] = await prisma.problem.findMany({});
return <UserTable config={config} data={data} />;
} else {
const role = userType.toUpperCase() as Role
const data: User[] = await prisma.user.findMany({ where: { role } })
return <UserTable config={config} data={data} />
const role = userType.toUpperCase() as Role;
const data: User[] = await prisma.user.findMany({ where: { role } });
return <UserTable config={config} data={data} />;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,22 @@
import { z } from "zod"
import { createUserConfig, baseUserSchema, baseAddUserSchema, baseEditUserSchema } from './base-config'
import {
createUserConfig,
baseUserSchema,
baseAddUserSchema,
baseEditUserSchema,
} from "./base-config";
import { z } from "zod";
// 管理员数据校验 schema
export const adminSchema = baseUserSchema
export type Admin = z.infer<typeof adminSchema>
export const adminSchema = baseUserSchema;
export type Admin = z.infer<typeof adminSchema>;
// 添加管理员表单校验 schema
export const addAdminSchema = baseAddUserSchema
export type AddAdminFormData = z.infer<typeof addAdminSchema>
export const addAdminSchema = baseAddUserSchema;
export type AddAdminFormData = z.infer<typeof addAdminSchema>;
// 编辑管理员表单校验 schema
export const editAdminSchema = baseEditUserSchema
export type EditAdminFormData = z.infer<typeof editAdminSchema>
export const editAdminSchema = baseEditUserSchema;
export type EditAdminFormData = z.infer<typeof editAdminSchema>;
// 管理员配置
export const adminConfig = createUserConfig(
@ -20,4 +25,4 @@ export const adminConfig = createUserConfig(
"添加管理员",
"请输入管理员姓名",
"请输入管理员邮箱"
)
);

View File

@ -1,4 +1,4 @@
import { z } from "zod"
import { z } from "zod";
// 基础用户 schema
export const baseUserSchema = z.object({
@ -9,7 +9,7 @@ export const baseUserSchema = z.object({
role: z.string().optional(),
createdAt: z.string(),
updatedAt: z.string().optional(),
})
});
// 基础添加用户 schema
export const baseAddUserSchema = z.object({
@ -17,7 +17,7 @@ export const baseAddUserSchema = z.object({
email: z.string().email("请输入有效的邮箱地址"),
password: z.string().min(8, "密码长度8-32位").max(32, "密码长度8-32位"),
createdAt: z.string(),
})
});
// 基础编辑用户 schema
export const baseEditUserSchema = z.object({
@ -26,23 +26,58 @@ export const baseEditUserSchema = z.object({
email: z.string().email("请输入有效的邮箱地址"),
password: z.string().min(8, "密码长度8-32位").max(32, "密码长度8-32位"),
createdAt: z.string(),
})
});
// 基础表格列配置
export const baseColumns = [
{ key: "id", label: "ID", sortable: true },
{ key: "name", label: "姓名", sortable: true, searchable: true, placeholder: "搜索姓名" },
{ key: "email", label: "邮箱", sortable: true, searchable: true, placeholder: "搜索邮箱" },
{
key: "name",
label: "姓名",
sortable: true,
searchable: true,
placeholder: "搜索姓名",
},
{
key: "email",
label: "邮箱",
sortable: true,
searchable: true,
placeholder: "搜索邮箱",
},
{ key: "createdAt", label: "创建时间", sortable: true },
]
];
// 基础表单字段配置
export const baseFormFields = [
{ key: "name", label: "姓名", type: "text", placeholder: "请输入姓名", required: true },
{ key: "email", label: "邮箱", type: "email", placeholder: "请输入邮箱", required: true },
{ key: "password", label: "密码", type: "password", placeholder: "请输入8-32位密码", required: true },
{ key: "createdAt", label: "创建时间", type: "datetime-local", required: false },
]
{
key: "name",
label: "姓名",
type: "text",
placeholder: "请输入姓名",
required: true,
},
{
key: "email",
label: "邮箱",
type: "email",
placeholder: "请输入邮箱",
required: true,
},
{
key: "password",
label: "密码",
type: "password",
placeholder: "请输入8-32位密码",
required: true,
},
{
key: "createdAt",
label: "创建时间",
type: "datetime-local",
required: false,
},
];
// 基础操作配置
export const baseActions = {
@ -50,13 +85,13 @@ export const baseActions = {
edit: { label: "编辑", icon: "PencilIcon" },
delete: { label: "删除", icon: "TrashIcon" },
batchDelete: { label: "批量删除", icon: "TrashIcon" },
}
};
// 基础分页配置
export const basePagination = {
pageSizes: [10, 50, 100, 500],
defaultPageSize: 10,
}
};
// 创建用户配置的工厂函数
export function createUserConfig(
@ -71,16 +106,19 @@ export function createUserConfig(
title,
apiPath: "/api/user",
columns: baseColumns,
formFields: baseFormFields.map(field => ({
formFields: baseFormFields.map((field) => ({
...field,
placeholder: field.key === 'name' ? namePlaceholder :
field.key === 'email' ? emailPlaceholder :
field.placeholder
placeholder:
field.key === "name"
? namePlaceholder
: field.key === "email"
? emailPlaceholder
: field.placeholder,
})),
actions: {
...baseActions,
add: { ...baseActions.add, label: addLabel }
add: { ...baseActions.add, label: addLabel },
},
pagination: basePagination,
}
};
}

View File

@ -1,5 +1,10 @@
import {
createUserConfig,
baseUserSchema,
baseAddUserSchema,
baseEditUserSchema,
} from "./base-config";
import { z } from "zod";
import { createUserConfig, baseUserSchema, baseAddUserSchema, baseEditUserSchema } from './base-config'
export const guestSchema = baseUserSchema;
export type Guest = z.infer<typeof guestSchema>;

View File

@ -24,8 +24,20 @@ export const problemConfig = {
apiPath: "/api/problem",
columns: [
{ key: "id", label: "ID", sortable: true },
{ key: "displayId", label: "题目编号", sortable: true, searchable: true, placeholder: "搜索编号" },
{ key: "difficulty", label: "难度", sortable: true, searchable: true, placeholder: "搜索难度" },
{
key: "displayId",
label: "题目编号",
sortable: true,
searchable: true,
placeholder: "搜索编号",
},
{
key: "difficulty",
label: "难度",
sortable: true,
searchable: true,
placeholder: "搜索难度",
},
],
formFields: [
{ key: "displayId", label: "题目编号", type: "number", required: true },

View File

@ -1,5 +1,10 @@
import {
createUserConfig,
baseUserSchema,
baseAddUserSchema,
baseEditUserSchema,
} from "./base-config";
import { z } from "zod";
import { createUserConfig, baseUserSchema, baseAddUserSchema, baseEditUserSchema } from './base-config'
export const teacherSchema = baseUserSchema;
export type Teacher = z.infer<typeof teacherSchema>;

View File

@ -247,8 +247,3 @@ export const { auth, handlers, signIn, signOut } = NextAuth({
},
},
});
export const getCurrentUser=async ()=>{
const session=await auth();
return session?.user
}

View File

@ -14,85 +14,85 @@ export default {
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
"1": "hsl(var(--chart-1))",
"2": "hsl(var(--chart-2))",
"3": "hsl(var(--chart-3))",
"4": "hsl(var(--chart-4))",
"5": "hsl(var(--chart-5))",
},
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))'
}
DEFAULT: "hsl(var(--sidebar-background))",
foreground: "hsl(var(--sidebar-foreground))",
primary: "hsl(var(--sidebar-primary))",
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
accent: "hsl(var(--sidebar-accent))",
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
'accordion-down': {
"accordion-down": {
from: {
height: '0'
height: "0",
},
to: {
height: 'var(--radix-accordion-content-height)'
}
height: "var(--radix-accordion-content-height)",
},
'accordion-up': {
},
"accordion-up": {
from: {
height: 'var(--radix-accordion-content-height)'
height: "var(--radix-accordion-content-height)",
},
to: {
height: '0'
}
}
height: "0",
},
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out'
}
}
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [animate],
} satisfies Config;