diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index cf58465..1686908 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -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 (

- Second Brain + {schoolName}

Образовательная платформа

+ {notice === "registration_closed" && ( +
+ Регистрация временно закрыта. Обратитесь к администратору. +
+ )}
diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx index 3bdf363..c4e9088 100644 --- a/src/app/(auth)/register/page.tsx +++ b/src/app/(auth)/register/page.tsx @@ -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 (

- Second Brain + {settings.schoolName}

Образовательная платформа

- +
diff --git a/src/app/(auth)/register/register-form.tsx b/src/app/(auth)/register/register-form.tsx index 31365ed..a96cb09 100644 --- a/src/app/(auth)/register/register-form.tsx +++ b/src/app/(auth)/register/register-form.tsx @@ -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 символов" />
+ + {showTermsCheckbox && ( + + )} + {error && (

{error} diff --git a/src/app/(student)/layout.tsx b/src/app/(student)/layout.tsx index 4860840..52aa8c7 100644 --- a/src/app/(student)/layout.tsx +++ b/src/app/(student)/layout.tsx @@ -16,6 +16,17 @@ export default async function StudentLayout({ children }: { children: React.Reac if (maintenance === "true") redirect("/maintenance"); } + const [schoolName, logoUrl, showLogo, socialYoutube, socialVk, socialTelegram, orgRequisites] = + await Promise.all([ + getSetting("schoolName"), + getSetting("logoUrl"), + getSetting("showLogo"), + getSetting("socialYoutube"), + getSetting("socialVk"), + getSetting("socialTelegram"), + getSetting("orgRequisites"), + ]); + const isImpersonating = !!(session.session as { impersonatedBy?: string }).impersonatedBy; return ( @@ -25,8 +36,12 @@ export default async function StudentLayout({ children }: { children: React.Reac className="sticky top-0 z-10 flex items-center justify-between px-6 py-3" style={{ borderBottom: "2px solid var(--border)", backgroundColor: "var(--background)" }} > - - Second Brain + + {logoUrl && showLogo === "true" && ( + // eslint-disable-next-line @next/next/no-img-element + {schoolName} + )} + {schoolName}

{session.user.name} @@ -34,6 +49,35 @@ export default async function StudentLayout({ children }: { children: React.Reac
{children}
+ {(socialYoutube || socialVk || socialTelegram || orgRequisites) && ( + + )}
); } diff --git a/src/app/curator/homework/[submissionId]/actions.ts b/src/app/curator/homework/[submissionId]/actions.ts index a305e14..cb2dfe2 100644 --- a/src/app/curator/homework/[submissionId]/actions.ts +++ b/src/app/curator/homework/[submissionId]/actions.ts @@ -5,6 +5,7 @@ import { auth } from "@/lib/auth"; import { headers } from "next/headers"; import { revalidatePath } from "next/cache"; import { sendFeedbackReceivedEmail } from "@/lib/email"; +import { getSetting, asBool } from "@/lib/settings"; async function requireCurator() { const session = await auth.api.getSession({ headers: await headers() }); @@ -78,14 +79,17 @@ export async function submitFeedback( }); const { lesson } = submission.homework; - const lessonUrl = `${process.env.BETTER_AUTH_URL ?? "https://school.second-brain.ru"}/courses/${lesson.module.course.slug}/lessons/${lesson.id}`; - await sendFeedbackReceivedEmail( - submission.user.email, - submission.user.name, - lesson.title, - data.text, - lessonUrl - ); + const notifySetting = await getSetting("notifyStudentOnFeedback"); + if (asBool(notifySetting)) { + const lessonUrl = `${process.env.BETTER_AUTH_URL ?? "https://school.second-brain.ru"}/courses/${lesson.module.course.slug}/lessons/${lesson.id}`; + await sendFeedbackReceivedEmail( + submission.user.email, + submission.user.name, + lesson.title, + data.text, + lessonUrl + ); + } revalidatePath("/curator/homework"); revalidatePath(`/curator/homework/${submissionId}`); diff --git a/src/components/admin/settings-form.tsx b/src/components/admin/settings-form.tsx index 7eb115e..b69b1db 100644 --- a/src/components/admin/settings-form.tsx +++ b/src/components/admin/settings-form.tsx @@ -432,6 +432,62 @@ export function SettingsForm({ initial }: { initial: Settings }) { + {/* ── 7. Логотип ── */} +
+ + set("logoUrl", e.target.value)} + placeholder="https://..." + style={{ ...inputStyle, fontFamily: "var(--font-mono)" }} + {...focusHandlers} + /> + + set("showLogo", v ? "true" : "false")} + /> +
+ + {/* ── 8. Социальные сети ── */} +
+ + set("socialYoutube", e.target.value)} + placeholder="https://youtube.com/@..." + style={{ ...inputStyle, fontFamily: "var(--font-mono)" }} + {...focusHandlers} + /> + + + set("socialVk", e.target.value)} + placeholder="https://vk.com/..." + style={{ ...inputStyle, fontFamily: "var(--font-mono)" }} + {...focusHandlers} + /> + + + set("socialTelegram", e.target.value)} + placeholder="https://t.me/..." + style={{ ...inputStyle, fontFamily: "var(--font-mono)" }} + {...focusHandlers} + /> + +
+ {/* Bottom save button */}