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:
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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" ? "● ОТКРЫТ" : "✓ ЗАКРЫТ"}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user