mirror of
https://github.com/OHTU-German-learning-website/OHTU-German-learning-website.git
synced 2026-10-12 19:13:51 +03:00
Merge pull request #518 from erzabytyci/user-tiers
feat: add feedback form, remove: talkback form
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { z } from "zod";
|
||||
import { withAuth } from "@/backend/middleware/withAuth";
|
||||
import sendEmail from "@/backend/mailService";
|
||||
|
||||
const contactSchema = z.object({
|
||||
message: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Nachricht ist erforderlich.")
|
||||
.max(2000, "Nachricht ist zu lang."),
|
||||
});
|
||||
|
||||
export const POST = withAuth(
|
||||
async (req) => {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const result = contactSchema.safeParse(body);
|
||||
|
||||
if (!result.success) {
|
||||
return Response.json(
|
||||
{ message: result.error.issues[0]?.message || "Ungültige Eingabe." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { message } = result.data;
|
||||
const contactEmail = process.env.CONTACT_EMAIL;
|
||||
|
||||
if (!contactEmail) {
|
||||
return Response.json(
|
||||
{ message: "Kontakt-E-Mail ist nicht konfiguriert." },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
await sendEmail({
|
||||
to: contactEmail,
|
||||
subject: "Kontaktieren Sie uns",
|
||||
text: message,
|
||||
});
|
||||
|
||||
return Response.json({ message: "Nachricht erfolgreich gesendet." });
|
||||
} catch (error) {
|
||||
return Response.json(
|
||||
{
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Nachricht konnte nicht gesendet werden.",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
requireAuth: true,
|
||||
requireAdmin: false,
|
||||
}
|
||||
);
|
||||
@@ -1,61 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { withAuth } from "@/backend/middleware/withAuth";
|
||||
import sendEmail from "@/backend/mailService";
|
||||
|
||||
const talkbackSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.trim()
|
||||
.email("Bitte geben Sie eine gültige E-Mail-Adresse des Empfängers ein."),
|
||||
subject: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Betreff ist erforderlich.")
|
||||
.max(200, "Betreff ist zu lang."),
|
||||
message: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Nachricht ist erforderlich.")
|
||||
.max(2000, "Nachricht ist zu lang."),
|
||||
});
|
||||
|
||||
export const POST = withAuth(
|
||||
async (req) => {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const result = talkbackSchema.safeParse(body);
|
||||
|
||||
if (!result.success) {
|
||||
return Response.json(
|
||||
{
|
||||
message: result.error.issues[0]?.message || "Ungültige Eingabe.",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { email, subject, message } = result.data;
|
||||
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject,
|
||||
text: message,
|
||||
});
|
||||
|
||||
return Response.json({
|
||||
message: "Nachricht erfolgreich gesendet.",
|
||||
});
|
||||
} catch {
|
||||
return Response.json(
|
||||
{
|
||||
message: "Nachricht konnte nicht gesendet werden.",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
requireAuth: true,
|
||||
requireAdmin: true,
|
||||
}
|
||||
);
|
||||
@@ -12,7 +12,7 @@
|
||||
border-radius: var(--radius-2xl);
|
||||
padding: var(--u-xl);
|
||||
box-shadow: 0 10px 24px rgb(0 0 0 / 10%);
|
||||
width: 570px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.auth-box {
|
||||
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import "./contact.css";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { withBasePath } from "@/shared/utils/basePath";
|
||||
import { useUser } from "@/context/user.context";
|
||||
|
||||
export default function ContactPage() {
|
||||
const {
|
||||
auth: { isLoggedIn, isAuthResolved },
|
||||
} = useUser();
|
||||
const [message, setMessage] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
|
||||
// Show nothing while session is being resolved, or if not logged in.
|
||||
// Middleware already redirects unauthenticated requests to /auth/login.
|
||||
if (!isAuthResolved || !isLoggedIn) return null;
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
setError("");
|
||||
|
||||
const trimmedMessage = message.trim();
|
||||
|
||||
if (!trimmedMessage) {
|
||||
setError("Nachricht ist erforderlich.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSending(true);
|
||||
|
||||
const response = await fetch(withBasePath("/api/contact"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ message: trimmedMessage }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
data.message || "Nachricht konnte nicht gesendet werden."
|
||||
);
|
||||
}
|
||||
|
||||
setMessage("");
|
||||
alert("Nachricht erfolgreich gesendet.");
|
||||
} catch (requestError) {
|
||||
setError(requestError.message || "Etwas ist schief gelaufen.");
|
||||
} finally {
|
||||
setIsSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="talkback-container">
|
||||
<div className="talkback-box">
|
||||
<h1 className="auth-title">Kontaktieren Sie unser Team</h1>
|
||||
|
||||
{error && <p className="error">{error}</p>}
|
||||
|
||||
<form className="talkback-form" onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="contact-message">
|
||||
Nachricht
|
||||
</label>
|
||||
<textarea
|
||||
className="form-input talkback-message-input"
|
||||
id="contact-message"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
required
|
||||
rows={8}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="form-button"
|
||||
type="submit"
|
||||
size="md"
|
||||
width="full"
|
||||
disabled={isSending}
|
||||
>
|
||||
{isSending ? "Senden..." : "Nachricht senden"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import "../auth/auth.css";
|
||||
|
||||
export default function AuthLayout({ children }) {
|
||||
return (
|
||||
<div className="auth-container">
|
||||
<div className="auth-box">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import "./talkback.css";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { withBasePath } from "@/shared/utils/basePath";
|
||||
import { useIsAdmin } from "@/context/user.context";
|
||||
|
||||
export default function TalkbackPage() {
|
||||
const isAdmin = useIsAdmin();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdmin === false) {
|
||||
router.replace(withBasePath("/"));
|
||||
}
|
||||
}, [isAdmin, router]);
|
||||
|
||||
if (isAdmin !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const defaultSubject = "Rückmeldung von GRADESA 2.0";
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [subject, setSubject] = useState(defaultSubject);
|
||||
const [message, setMessage] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
setError("");
|
||||
|
||||
const trimmedEmail = email.trim();
|
||||
const trimmedSubject = subject.trim() || defaultSubject;
|
||||
const trimmedMessage = message.trim();
|
||||
|
||||
if (!trimmedEmail) {
|
||||
setError("Die E-Mail-Adresse des Empfängers ist erforderlich.");
|
||||
return;
|
||||
}
|
||||
|
||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
if (!emailPattern.test(trimmedEmail)) {
|
||||
setError(
|
||||
"Bitte geben Sie eine gültige E-Mail-Adresse des Empfängers ein."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!trimmedMessage) {
|
||||
setError("Nachricht ist erforderlich.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSending(true);
|
||||
|
||||
const response = await fetch(withBasePath("/api/talkback"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: trimmedEmail,
|
||||
subject: trimmedSubject,
|
||||
message: trimmedMessage,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
data.message || "Nachricht konnte nicht gesendet werden."
|
||||
);
|
||||
}
|
||||
|
||||
setMessage("");
|
||||
alert("Feedback erfolgreich gesendet.");
|
||||
} catch (requestError) {
|
||||
setError(requestError.message || "Etwas ist schief gelaufen.");
|
||||
} finally {
|
||||
setIsSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="talkback-container">
|
||||
<div className="talkback-box">
|
||||
<h1 className="auth-title">Brauchen Sie Hilfe?</h1>
|
||||
|
||||
{error && <p className="error">{error}</p>}
|
||||
|
||||
<form className="talkback-form" onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="talkback-email">
|
||||
E-Mail-Adresse des Empfängers
|
||||
</label>
|
||||
<input
|
||||
className="form-input"
|
||||
id="talkback-email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="talkback-subject">
|
||||
Betreff
|
||||
</label>
|
||||
<input
|
||||
className="form-input"
|
||||
id="talkback-subject"
|
||||
type="text"
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="talkback-message">
|
||||
Nachricht
|
||||
</label>
|
||||
<textarea
|
||||
className="form-input talkback-message-input"
|
||||
id="talkback-message"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
required
|
||||
rows={8}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="form-button"
|
||||
type="submit"
|
||||
size="md"
|
||||
width="full"
|
||||
disabled={isSending}
|
||||
>
|
||||
{isSending ? "Senden..." : "Nachricht senden"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -115,8 +115,8 @@ function StudentSideBar() {
|
||||
<Link className={styles.sidebarLink} href="/glossary">
|
||||
Glossar
|
||||
</Link>
|
||||
<Link className={styles.sidebarLink} href="/talkback">
|
||||
Brauchen Sie Hilfe?
|
||||
<Link className={styles.sidebarLink} href="/contact">
|
||||
Kontaktieren Sie unser Team
|
||||
</Link>
|
||||
<SidebarGroup
|
||||
title="Links zu anderen Webseiten"
|
||||
|
||||
@@ -8,7 +8,7 @@ import { AUTH_COOKIE_NAME } from "@/shared/const";
|
||||
* to the login page when attempting to access these paths).
|
||||
* @type {string[]}
|
||||
*/
|
||||
const authRequired = ["/grammar/exercises", "/edit_info"];
|
||||
const authRequired = ["/grammar/exercises", "/edit_info", "/contact"];
|
||||
|
||||
// Restricted paths logged-in users cannot access
|
||||
/**
|
||||
@@ -34,13 +34,19 @@ export async function middleware(request) {
|
||||
/\/$/,
|
||||
""
|
||||
);
|
||||
const pathnameWithoutBasePath = pathname.startsWith(basePath)
|
||||
? pathname.slice(basePath.length) || "/"
|
||||
: pathname;
|
||||
const sessionCookie = request.cookies.get(AUTH_COOKIE_NAME);
|
||||
const hasCookie = !!sessionCookie && sessionCookie.value !== "";
|
||||
|
||||
// Redirect logged-out users trying to access protected paths
|
||||
if (!hasCookie && authRequired.some((path) => pathname.startsWith(path))) {
|
||||
if (
|
||||
!hasCookie &&
|
||||
authRequired.some((path) => pathnameWithoutBasePath.startsWith(path))
|
||||
) {
|
||||
const loginUrl = new URL(`${basePath}/auth/login`, request.url);
|
||||
loginUrl.searchParams.set("redirect", pathname); // Add the original path as a query parameter
|
||||
loginUrl.searchParams.set("redirect", pathnameWithoutBasePath); // Add the original path as a query parameter
|
||||
return NextResponse.redirect(loginUrl);
|
||||
}
|
||||
// Otherwise, allow the request to proceed and prevent caching of the response
|
||||
|
||||
Reference in New Issue
Block a user