From 89d614fa007613a917d7493587e863dfb0045d38 Mon Sep 17 00:00:00 2001 From: dmitriylaukhin Date: Tue, 19 May 2026 13:31:31 +0500 Subject: [PATCH] Add student questions list, new question form, and thread pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tasks 8–10: student-facing questions UI — list page with unread badges, new question form with client-side submission, and thread page with QuestionThread component for real-time reply + file attachment flow. Co-Authored-By: Claude Sonnet 4.6 --- src/app/(student)/questions/[id]/page.tsx | 86 +++++++ src/app/(student)/questions/new/page.tsx | 121 ++++++++++ src/app/(student)/questions/page.tsx | 113 +++++++++ src/components/questions/QuestionThread.tsx | 255 ++++++++++++++++++++ 4 files changed, 575 insertions(+) create mode 100644 src/app/(student)/questions/[id]/page.tsx create mode 100644 src/app/(student)/questions/new/page.tsx create mode 100644 src/app/(student)/questions/page.tsx create mode 100644 src/components/questions/QuestionThread.tsx diff --git a/src/app/(student)/questions/[id]/page.tsx b/src/app/(student)/questions/[id]/page.tsx new file mode 100644 index 0000000..ba5cb55 --- /dev/null +++ b/src/app/(student)/questions/[id]/page.tsx @@ -0,0 +1,86 @@ +import { headers } from "next/headers"; +import { auth } from "@/lib/auth"; +import { redirect, notFound } from "next/navigation"; +import { prisma } from "@/lib/prisma"; +import Link from "next/link"; +import { QuestionThread } from "@/components/questions/QuestionThread"; + +export default async function QuestionThreadPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const session = await auth.api.getSession({ headers: await headers() }); + if (!session) redirect("/login"); + + const { id } = await params; + + const question = await prisma.studentQuestion.findUnique({ + where: { id }, + include: { + user: { select: { id: true, name: true } }, + messages: { + include: { author: { select: { id: true, name: true, role: true } } }, + orderBy: { createdAt: "asc" }, + }, + }, + }); + + if (!question || question.userId !== session.user.id) notFound(); + + // Mark staff messages as read + await prisma.studentQuestionMessage.updateMany({ + where: { + questionId: id, + isRead: false, + NOT: { authorId: session.user.id }, + }, + data: { isRead: true }, + }); + + return ( +
+
+
+

+ {question.title} +

+

+ Создан{" "} + {new Date(question.createdAt).toLocaleDateString("ru", { + day: "numeric", + month: "long", + })}{" "} + ·{" "} + + {question.status === "OPEN" ? "● Открыт" : "✓ Закрыт"} + +

+
+ + ← Все вопросы + +
+ + ({ + ...m, + files: m.files as Array<{ name: string; url: string; size: number }> | null, + createdAt: m.createdAt.toISOString(), + }))} + /> +
+ ); +} diff --git a/src/app/(student)/questions/new/page.tsx b/src/app/(student)/questions/new/page.tsx new file mode 100644 index 0000000..3e9c45f --- /dev/null +++ b/src/app/(student)/questions/new/page.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +export default function NewQuestionPage() { + const router = useRouter(); + const [title, setTitle] = useState(""); + const [text, setText] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!title.trim() || !text.trim()) return; + setLoading(true); + setError(""); + + try { + const res = await fetch("/api/questions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ title, text }), + }); + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error ?? "Ошибка при создании вопроса"); + } + const q = await res.json(); + router.push(`/questions/${q.id}`); + } catch (err) { + setError(err instanceof Error ? err.message : "Ошибка"); + } finally { + setLoading(false); + } + } + + return ( +
+
+ + ← Все вопросы + +
+ +

+ Новый вопрос +

+ +
+
+ + setTitle(e.target.value)} + placeholder="Кратко опишите суть вопроса" + required + className="w-full text-sm px-3 py-2 outline-none" + style={{ + border: "2px solid var(--border)", + background: "var(--surface)", + color: "var(--foreground)", + }} + /> +
+ +
+ +