Implement platform settings (Stage 9)

- Wire settings to actual platform behavior: maintenance mode, registration toggle,
  notification emails, curator feedback emails, email verification flag
- Add logo (logoUrl, showLogo) and social network links (YouTube, VK, Telegram) settings
- Show logo + school name dynamically in student layout header
- Add footer to student layout with org requisites and social links
- Register page: read settings server-side, validate terms checkbox with legal links
- Login page: show notice when redirected from closed registration
- Settings form: add Logo and Social Networks sections

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 15:31:10 +05:00
parent ba0a630fd9
commit c64f393a7b
8 changed files with 223 additions and 24 deletions
+20 -2
View File
@@ -1,17 +1,35 @@
import { getSetting } from "@/lib/settings";
import { LoginForm } from "./login-form";
export default function LoginPage() {
export default async function LoginPage({
searchParams,
}: {
searchParams: Promise<{ notice?: string }>;
}) {
const [schoolName, { notice }] = await Promise.all([
getSetting("schoolName"),
searchParams,
]);
return (
<div className="min-h-screen flex items-center justify-center" style={{ backgroundColor: "var(--background)" }}>
<div className="w-full max-w-sm">
<div className="text-center mb-8">
<h1 className="text-2xl font-bold tracking-wide" style={{ color: "var(--foreground)" }}>
Second Brain
{schoolName}
</h1>
<p className="mt-1 text-sm uppercase tracking-widest" style={{ color: "var(--muted-foreground)", fontSize: "0.65rem" }}>
Образовательная платформа
</p>
</div>
{notice === "registration_closed" && (
<div
className="mb-4 px-4 py-3 text-sm text-center"
style={{ border: "2px solid var(--border)", color: "var(--muted-foreground)" }}
>
Регистрация временно закрыта. Обратитесь к администратору.
</div>
)}
<div className="card-aubade p-8">
<LoginForm />
</div>
+16 -3
View File
@@ -1,19 +1,32 @@
import { redirect } from "next/navigation";
import { getSettings } from "@/lib/settings";
import { RegisterForm } from "./register-form";
export default function RegisterPage() {
export default async function RegisterPage() {
const settings = await getSettings();
if (settings.registrationEnabled !== "true") {
redirect("/login?notice=registration_closed");
}
return (
<div className="min-h-screen flex items-center justify-center" style={{ backgroundColor: "var(--background)" }}>
<div className="w-full max-w-sm">
<div className="text-center mb-8">
<h1 className="text-2xl font-bold tracking-wide" style={{ color: "var(--foreground)" }}>
Second Brain
{settings.schoolName}
</h1>
<p className="mt-1 text-sm uppercase tracking-widest" style={{ color: "var(--muted-foreground)", fontSize: "0.65rem" }}>
Образовательная платформа
</p>
</div>
<div className="card-aubade p-8">
<RegisterForm />
<RegisterForm
showTermsCheckbox={settings.showTermsCheckbox === "true"}
privacyPolicyUrl={settings.privacyPolicyUrl}
termsUrl={settings.termsUrl}
offerUrl={settings.offerUrl}
/>
</div>
</div>
</div>
+51 -1
View File
@@ -4,10 +4,18 @@ import { useState } from "react";
import Link from "next/link";
import { signUp } from "@/lib/auth-client";
export function RegisterForm() {
interface Props {
showTermsCheckbox: boolean;
privacyPolicyUrl: string;
termsUrl: string;
offerUrl: string;
}
export function RegisterForm({ showTermsCheckbox, privacyPolicyUrl, termsUrl, offerUrl }: Props) {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [termsAccepted, setTermsAccepted] = useState(false);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
@@ -22,8 +30,18 @@ export function RegisterForm() {
fontFamily: "inherit",
} as React.CSSProperties;
const legalLinks = [
{ url: privacyPolicyUrl, label: "Политику конфиденциальности" },
{ url: termsUrl, label: "Согласие на обработку данных" },
{ url: offerUrl, label: "Договор-оферту" },
].filter((l) => l.url);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (showTermsCheckbox && !termsAccepted) {
setError("Необходимо принять условия для продолжения");
return;
}
setError("");
setLoading(true);
@@ -102,6 +120,38 @@ export function RegisterForm() {
placeholder="Минимум 8 символов"
/>
</div>
{showTermsCheckbox && (
<label className="flex items-start gap-2 cursor-pointer">
<input
type="checkbox"
checked={termsAccepted}
onChange={(e) => setTermsAccepted(e.target.checked)}
className="mt-0.5 flex-shrink-0"
style={{ width: "16px", height: "16px", accentColor: "var(--foreground)" }}
/>
<span className="text-xs" style={{ color: "var(--muted-foreground)" }}>
Я принимаю{" "}
{legalLinks.length > 0
? legalLinks.map((l, i) => (
<span key={l.url}>
<a
href={l.url}
target="_blank"
rel="noopener noreferrer"
className="underline"
style={{ color: "var(--foreground)" }}
>
{l.label}
</a>
{i < legalLinks.length - 1 ? ", " : ""}
</span>
))
: "условия использования платформы"}
</span>
</label>
)}
{error && (
<p className="text-xs py-2 px-3" style={{ border: "2px solid oklch(0.577 0.245 27.325)", color: "oklch(0.577 0.245 27.325)" }}>
{error}