Fix student questions pages: CSS tokens, scroll, upload guard, S3 path

- Replace non-existent --surface/--surface-muted/--border-strong with actual
  design-system tokens (--color-surface, --background, --foreground, --muted)
- Remove tmp/ segment from S3 upload key in question-upload route
- Add auto-scroll to bottom on new message in QuestionThread
- Block Send while file upload is in progress (uploading guard)
- Replace <a> with Next.js <Link> in new question page back-link
- Replace hardcoded #c00 error color with var(--destructive) in both files
- Replace hardcoded #E8E8E0/#F5F5F0 hex backgrounds with CSS tokens
This commit is contained in:
2026-05-19 13:40:05 +05:00
parent c5d2caa345
commit f7d428180b
4 changed files with 27 additions and 19 deletions
+6 -5
View File
@@ -2,6 +2,7 @@
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Link from "next/link";
export default function NewQuestionPage() { export default function NewQuestionPage() {
const router = useRouter(); const router = useRouter();
@@ -38,13 +39,13 @@ export default function NewQuestionPage() {
return ( return (
<div className="max-w-xl mx-auto px-4 py-8"> <div className="max-w-xl mx-auto px-4 py-8">
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
<a <Link
href="/questions" href="/questions"
className="text-sm" className="text-sm"
style={{ color: "var(--muted-foreground)", textDecoration: "underline" }} style={{ color: "var(--muted-foreground)", textDecoration: "underline" }}
> >
Все вопросы Все вопросы
</a> </Link>
</div> </div>
<h1 className="text-xl font-bold mb-6" style={{ color: "var(--foreground)" }}> <h1 className="text-xl font-bold mb-6" style={{ color: "var(--foreground)" }}>
@@ -68,7 +69,7 @@ export default function NewQuestionPage() {
className="w-full text-sm px-3 py-2 outline-none" className="w-full text-sm px-3 py-2 outline-none"
style={{ style={{
border: "2px solid var(--border)", border: "2px solid var(--border)",
background: "var(--surface)", background: "var(--color-surface)",
color: "var(--foreground)", color: "var(--foreground)",
}} }}
/> />
@@ -90,14 +91,14 @@ export default function NewQuestionPage() {
className="w-full text-sm px-3 py-2 outline-none resize-none" className="w-full text-sm px-3 py-2 outline-none resize-none"
style={{ style={{
border: "2px solid var(--border)", border: "2px solid var(--border)",
background: "var(--surface)", background: "var(--color-surface)",
color: "var(--foreground)", color: "var(--foreground)",
}} }}
/> />
</div> </div>
{error && ( {error && (
<p className="text-sm" style={{ color: "#c00" }}> <p className="text-sm" style={{ color: "var(--destructive)" }}>
{error} {error}
</p> </p>
)} )}
+5 -5
View File
@@ -37,8 +37,8 @@ export default async function QuestionsPage() {
href="/questions/new" href="/questions/new"
className="text-sm font-bold px-4 py-2" className="text-sm font-bold px-4 py-2"
style={{ style={{
background: "var(--surface)", background: "var(--color-surface)",
border: "2px solid var(--border-strong)", border: "2px solid var(--foreground)",
color: "var(--foreground)", color: "var(--foreground)",
}} }}
> >
@@ -62,8 +62,8 @@ export default async function QuestionsPage() {
href={`/questions/${q.id}`} href={`/questions/${q.id}`}
className="block p-3 rounded-sm transition-colors" className="block p-3 rounded-sm transition-colors"
style={{ style={{
border: unread ? "2px solid var(--border-strong)" : "1px solid var(--border)", border: unread ? "2px solid var(--foreground)" : "1px solid var(--border)",
background: q.status === "CLOSED" ? "var(--surface-muted)" : "var(--surface)", background: q.status === "CLOSED" ? "var(--background)" : "var(--color-surface)",
opacity: q.status === "CLOSED" ? 0.7 : 1, opacity: q.status === "CLOSED" ? 0.7 : 1,
}} }}
> >
@@ -82,7 +82,7 @@ export default async function QuestionsPage() {
style={ style={
q.status === "OPEN" q.status === "OPEN"
? { background: "#E8F0D8", border: "1px solid var(--border)", color: "var(--foreground)" } ? { background: "#E8F0D8", border: "1px solid var(--border)", color: "var(--foreground)" }
: { background: "var(--surface-muted)", color: "var(--muted-foreground)" } : { background: "var(--background)", color: "var(--muted-foreground)" }
} }
> >
{q.status === "OPEN" ? "● ОТКРЫТ" : "✓ ЗАКРЫТ"} {q.status === "OPEN" ? "● ОТКРЫТ" : "✓ ЗАКРЫТ"}
+1 -1
View File
@@ -35,7 +35,7 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "Недопустимое расширение файла" }, { status: 415 }); return NextResponse.json({ error: "Недопустимое расширение файла" }, { status: 415 });
} }
const key = `questions/tmp/${session.user.id}/${randomUUID()}.${ext}`; const key = `questions/${session.user.id}/${randomUUID()}.${ext}`;
const buffer = Buffer.from(await file.arrayBuffer()); const buffer = Buffer.from(await file.arrayBuffer());
const url = await uploadFile(key, buffer, file.type); const url = await uploadFile(key, buffer, file.type);
+15 -8
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useRef } from "react"; import { useState, useRef, useEffect } from "react";
interface FileAttachment { interface FileAttachment {
name: string; name: string;
@@ -53,6 +53,11 @@ export function QuestionThread({
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
async function handleFileSelect(e: React.ChangeEvent<HTMLInputElement>) { async function handleFileSelect(e: React.ChangeEvent<HTMLInputElement>) {
const selected = Array.from(e.target.files ?? []); const selected = Array.from(e.target.files ?? []);
@@ -82,6 +87,7 @@ export function QuestionThread({
} }
async function handleSend() { async function handleSend() {
if (uploading) return;
if (!text.trim() && files.length === 0) return; if (!text.trim() && files.length === 0) return;
setSending(true); setSending(true);
setError(""); setError("");
@@ -120,7 +126,7 @@ export function QuestionThread({
className="max-w-[88%] px-3 py-2 rounded-sm text-sm" className="max-w-[88%] px-3 py-2 rounded-sm text-sm"
style={{ style={{
alignSelf: isMine ? "flex-end" : "flex-start", alignSelf: isMine ? "flex-end" : "flex-start",
background: isMine ? "#E8E8E0" : "#F5F5F0", background: isMine ? "var(--muted)" : "var(--background)",
border: isMine ? "none" : `2px solid #E8F0D8`, border: isMine ? "none" : `2px solid #E8F0D8`,
borderLeft: !isMine && isNew ? "3px solid #323232" : undefined, borderLeft: !isMine && isNew ? "3px solid #323232" : undefined,
}} }}
@@ -145,7 +151,7 @@ export function QuestionThread({
rel="noreferrer" rel="noreferrer"
className="flex items-center gap-1.5 text-xs px-2 py-1 rounded-sm" className="flex items-center gap-1.5 text-xs px-2 py-1 rounded-sm"
style={{ style={{
background: "#F5F5F0", background: "var(--background)",
border: "1px solid #AAAAAA", border: "1px solid #AAAAAA",
color: "var(--foreground)", color: "var(--foreground)",
width: "fit-content", width: "fit-content",
@@ -160,6 +166,7 @@ export function QuestionThread({
</div> </div>
); );
})} })}
<div ref={messagesEndRef} />
</div> </div>
{/* Queued files preview */} {/* Queued files preview */}
@@ -169,7 +176,7 @@ export function QuestionThread({
<div <div
key={i} key={i}
className="flex items-center gap-1 text-xs px-2 py-1 rounded-sm" className="flex items-center gap-1 text-xs px-2 py-1 rounded-sm"
style={{ background: "var(--surface)", border: "1px solid var(--border)" }} style={{ background: "var(--color-surface)", border: "1px solid var(--border)" }}
> >
📎 {f.name} 📎 {f.name}
<button <button
@@ -189,7 +196,7 @@ export function QuestionThread({
className="rounded-sm p-2" className="rounded-sm p-2"
style={{ style={{
border: "2px solid #AAAAAA", border: "2px solid #AAAAAA",
background: "var(--surface)", background: "var(--color-surface)",
opacity: questionStatus === "CLOSED" ? 0.5 : 1, opacity: questionStatus === "CLOSED" ? 0.5 : 1,
pointerEvents: questionStatus === "CLOSED" ? "none" : "auto", pointerEvents: questionStatus === "CLOSED" ? "none" : "auto",
}} }}
@@ -205,7 +212,7 @@ export function QuestionThread({
/> />
<div <div
className="flex items-center justify-between pt-2 mt-1" className="flex items-center justify-between pt-2 mt-1"
style={{ borderTop: "1px solid #E8E8E0" }} style={{ borderTop: "1px solid var(--muted)" }}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
@@ -221,7 +228,7 @@ export function QuestionThread({
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
disabled={uploading} disabled={uploading}
className="text-xs px-2 py-1 rounded-sm" className="text-xs px-2 py-1 rounded-sm"
style={{ background: "var(--surface)", border: "1px solid #AAAAAA" }} style={{ background: "var(--color-surface)", border: "1px solid #AAAAAA" }}
> >
{uploading ? "Загрузка..." : "📎 Прикрепить"} {uploading ? "Загрузка..." : "📎 Прикрепить"}
</button> </button>
@@ -245,7 +252,7 @@ export function QuestionThread({
</button> </button>
</div> </div>
{error && ( {error && (
<p className="text-xs mt-1" style={{ color: "#c00" }}> <p className="text-xs mt-1" style={{ color: "var(--destructive)" }}>
{error} {error}
</p> </p>
)} )}