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 #533 from OHTU-German-learning-website/glossary
News page functionality
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS news_articles (
|
||||||
|
id bigint PRIMARY KEY NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_by bigint,
|
||||||
|
updated_by bigint,
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER update_news_articles_updated_at
|
||||||
|
BEFORE UPDATE ON news_articles
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE news_articles ADD COLUMN IF NOT EXISTS is_teacher_only BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -724,6 +724,27 @@ ALTER TABLE public.multichoice_options
|
|||||||
ADD CONSTRAINT multichoice_options_multichoice_content_id_fkey
|
ADD CONSTRAINT multichoice_options_multichoice_content_id_fkey
|
||||||
FOREIGN KEY (multichoice_content_id) REFERENCES multichoice_content(id) ON DELETE CASCADE;
|
FOREIGN KEY (multichoice_content_id) REFERENCES multichoice_content(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
CREATE TABLE public.news_articles (
|
||||||
|
id bigint PRIMARY KEY NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||||
|
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone NOT NULL DEFAULT now(),
|
||||||
|
title text NOT NULL,
|
||||||
|
content text NOT NULL,
|
||||||
|
created_by bigint,
|
||||||
|
updated_by bigint,
|
||||||
|
is_teacher_only boolean NOT NULL DEFAULT false
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER update_news_articles_updated_at BEFORE UPDATE ON public.news_articles FOR EACH ROW EXECUTE FUNCTION updated_at();
|
||||||
|
|
||||||
|
ALTER TABLE public.news_articles
|
||||||
|
ADD CONSTRAINT news_articles_created_by_fkey
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
ALTER TABLE public.news_articles
|
||||||
|
ADD CONSTRAINT news_articles_updated_by_fkey
|
||||||
|
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
CREATE TABLE public.part_questions (
|
CREATE TABLE public.part_questions (
|
||||||
id bigint PRIMARY KEY NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
id bigint PRIMARY KEY NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||||
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
"use client";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Column, Row } from "@/components/ui/layout/container";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useRequest } from "@/shared/hooks/useRequest";
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import Editor from "@/components/ui/editor";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
import AdminLastModified from "@/components/ui/admin-last-modified";
|
||||||
|
|
||||||
|
const defaultFormErrors = {
|
||||||
|
title: "",
|
||||||
|
content: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CreateNewsArticle() {
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [content, setContent] = useState("");
|
||||||
|
const [isTeacherOnly, setIsTeacherOnly] = useState(false);
|
||||||
|
const [generalError, setGeneralError] = useState("");
|
||||||
|
const [formErrors, setFormErrors] = useState(defaultFormErrors);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [isLoadingArticle, setIsLoadingArticle] = useState(false);
|
||||||
|
const [lastModifiedAt, setLastModifiedAt] = useState(null);
|
||||||
|
const [lastModifiedBy, setLastModifiedBy] = useState(null);
|
||||||
|
const makeRequest = useRequest();
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const articleId = searchParams.get("id");
|
||||||
|
const isEditing = Boolean(articleId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!articleId) return;
|
||||||
|
|
||||||
|
const loadArticle = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoadingArticle(true);
|
||||||
|
const response = await fetch(
|
||||||
|
withBasePath(`/api/admin/news?id=${articleId}`)
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to load news article");
|
||||||
|
}
|
||||||
|
const article = await response.json();
|
||||||
|
if (article.error) {
|
||||||
|
throw new Error(article.error);
|
||||||
|
}
|
||||||
|
setTitle(article.title);
|
||||||
|
setContent(article.content);
|
||||||
|
setIsTeacherOnly(article.is_teacher_only || false);
|
||||||
|
setLastModifiedAt(article.updated_at || null);
|
||||||
|
setLastModifiedBy(article.updated_by || null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading news article:", error);
|
||||||
|
setGeneralError("Nachricht konnte nicht geladen werden.");
|
||||||
|
} finally {
|
||||||
|
setIsLoadingArticle(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadArticle();
|
||||||
|
}, [articleId]);
|
||||||
|
|
||||||
|
const handleTitleChange = (e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setTitle(val);
|
||||||
|
|
||||||
|
// Clear title error when user starts typing
|
||||||
|
if (formErrors.title) {
|
||||||
|
setFormErrors((prev) => ({ ...prev, title: "" }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setGeneralError("");
|
||||||
|
setFormErrors(defaultFormErrors);
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
const errors = { ...defaultFormErrors };
|
||||||
|
if (!title.trim()) {
|
||||||
|
errors.title = "Titel ist erforderlich";
|
||||||
|
}
|
||||||
|
if (!content.trim()) {
|
||||||
|
errors.content = "Inhalt ist erforderlich";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.values(errors).some((e) => e)) {
|
||||||
|
setFormErrors(errors);
|
||||||
|
setIsSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const method = isEditing ? "PUT" : "POST";
|
||||||
|
const url = "/admin/news";
|
||||||
|
|
||||||
|
const requestData = isEditing
|
||||||
|
? { id: articleId, title, content, is_teacher_only: isTeacherOnly }
|
||||||
|
: { title, content, is_teacher_only: isTeacherOnly };
|
||||||
|
|
||||||
|
const res = await makeRequest(url, requestData, { method });
|
||||||
|
|
||||||
|
if (res.status === 200 || res.status === 201) {
|
||||||
|
router.push("/admin/news");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Submission error:", e);
|
||||||
|
if (e.response?.data?.error) {
|
||||||
|
setGeneralError(e.response.data.error);
|
||||||
|
} else {
|
||||||
|
setGeneralError("Ein Fehler ist aufgetreten");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
router.push("/admin/news");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEditing && isLoadingArticle) {
|
||||||
|
return <p>Lädt Nachricht...</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column gap="md">
|
||||||
|
<h2>{isEditing ? "Nachricht bearbeiten" : "Nachricht erstellen"}</h2>
|
||||||
|
{isEditing && (
|
||||||
|
<AdminLastModified
|
||||||
|
updatedAt={lastModifiedAt}
|
||||||
|
updatedBy={lastModifiedBy}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<p>
|
||||||
|
{isEditing
|
||||||
|
? "Aktualisieren Sie die ausgewählte Nachricht."
|
||||||
|
: "Erstellen Sie eine neue Nachricht für die Startseite."}
|
||||||
|
</p>
|
||||||
|
{generalError && (
|
||||||
|
<p className="error" role="alert">
|
||||||
|
{generalError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<label>
|
||||||
|
Titel
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={title}
|
||||||
|
onChange={handleTitleChange}
|
||||||
|
placeholder="Geben Sie hier den Titel ein"
|
||||||
|
className={formErrors.title ? "error-input" : ""}
|
||||||
|
/>
|
||||||
|
{formErrors.title && <p className="error">{formErrors.title}</p>}
|
||||||
|
</label>
|
||||||
|
<Column gap="sm">
|
||||||
|
<label htmlFor="content">Inhalt</label>
|
||||||
|
<Editor
|
||||||
|
defaultContent={content}
|
||||||
|
updateEditorContent={(newContent) => {
|
||||||
|
setContent(newContent);
|
||||||
|
if (formErrors.content) {
|
||||||
|
setFormErrors((prev) => ({ ...prev, content: "" }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{formErrors.content && <p className="error">{formErrors.content}</p>}
|
||||||
|
</Column>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isTeacherOnly}
|
||||||
|
onChange={(e) => setIsTeacherOnly(e.target.checked)}
|
||||||
|
/>
|
||||||
|
Nur für Lehrer sichtbar
|
||||||
|
</label>
|
||||||
|
<Row
|
||||||
|
justify={"space-between"}
|
||||||
|
align="center"
|
||||||
|
wrap="wrap"
|
||||||
|
gap="md"
|
||||||
|
mt={"xl"}
|
||||||
|
mb={"xl"}
|
||||||
|
>
|
||||||
|
<Button variant="outline" onClick={handleCancel}>
|
||||||
|
Abbrechen
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting
|
||||||
|
? isEditing
|
||||||
|
? "Speichern..."
|
||||||
|
: "Wird gesendet..."
|
||||||
|
: isEditing
|
||||||
|
? "Speichern"
|
||||||
|
: "Absenden"}
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
.news-card {
|
||||||
|
background-color: var(--bg3);
|
||||||
|
padding: var(--u-md);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--bg3);
|
||||||
|
border-left: 4px solid var(--fg2);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: box-shadow 0.2s, border-left-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card:hover {
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
border-left-color: var(--fg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-title {
|
||||||
|
color: var(--fg1);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-date {
|
||||||
|
color: var(--fg2);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-meta {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--fg3);
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
"use client";
|
||||||
|
import { Container, Column, Row } from "@/components/ui/layout/container";
|
||||||
|
import useQuery from "@/shared/hooks/useQuery";
|
||||||
|
import { useRequest } from "@/shared/hooks/useRequest";
|
||||||
|
import { useState } from "react";
|
||||||
|
import "./news.css";
|
||||||
|
import { LinkButton } from "@/components/ui/linkbutton";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import RenderHTML from "@/components/ui/render-html/render-html";
|
||||||
|
|
||||||
|
export default function NewsList() {
|
||||||
|
const { data: articles, error, isLoading, refetch } = useQuery("/admin/news");
|
||||||
|
const makeRequest = useRequest();
|
||||||
|
const [deleting, setDeleting] = useState(null);
|
||||||
|
const [confirmDelete, setConfirmDelete] = useState(null);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <p>Wird geladen...</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <p className="error">Fehler beim Laden der Neuigkeiten</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (id) => {
|
||||||
|
if (confirmDelete === id) {
|
||||||
|
try {
|
||||||
|
setDeleting(id);
|
||||||
|
await makeRequest(`/admin/news?id=${id}`, null, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
await refetch();
|
||||||
|
setConfirmDelete(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting news article:", error);
|
||||||
|
alert("Fehler beim Löschen des Artikels");
|
||||||
|
} finally {
|
||||||
|
setDeleting(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setConfirmDelete(id);
|
||||||
|
// Auto-reset confirmation state after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (setConfirmDelete) {
|
||||||
|
setConfirmDelete((current) => (current === id ? null : current));
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortedArticles = [...(articles || [])].sort(
|
||||||
|
(a, b) => new Date(b.created_at) - new Date(a.created_at)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column gap="md" width="100%">
|
||||||
|
<Row justify="space-between" gap="xl" width="100%" align="center">
|
||||||
|
<h2 id="news-top">Neuigkeiten</h2>
|
||||||
|
</Row>
|
||||||
|
<LinkButton href="/admin/news/create">
|
||||||
|
Neue Nachricht erstellen
|
||||||
|
</LinkButton>
|
||||||
|
|
||||||
|
{sortedArticles.length === 0 ? (
|
||||||
|
<p>
|
||||||
|
Keine Neuigkeiten vorhanden. Erstellen Sie eine neue Nachricht, um zu
|
||||||
|
beginnen.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<Container display="grid" gap="md">
|
||||||
|
{sortedArticles.map((article) => (
|
||||||
|
<Container key={article.id} className="news-card">
|
||||||
|
<Column gap="sm">
|
||||||
|
<h3 className="news-title">{article.title}</h3>
|
||||||
|
<p className="news-date">
|
||||||
|
{new Date(article.created_at).toLocaleDateString("de-DE", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<RenderHTML data={article.content} disableGlossary />
|
||||||
|
<Row justify="flex-end" gap="sm" className="news-meta">
|
||||||
|
<LinkButton
|
||||||
|
href={`/admin/news/create?id=${article.id}`}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Bearbeiten
|
||||||
|
</LinkButton>
|
||||||
|
<Button
|
||||||
|
variant={
|
||||||
|
confirmDelete === article.id ? "danger" : "outline"
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
width="fit"
|
||||||
|
onClick={() => handleDelete(article.id)}
|
||||||
|
disabled={deleting === article.id}
|
||||||
|
>
|
||||||
|
{deleting === article.id
|
||||||
|
? "Löschen..."
|
||||||
|
: confirmDelete === article.id
|
||||||
|
? "Bestätigen"
|
||||||
|
: "Löschen"}
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Column>
|
||||||
|
</Container>
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { DB } from "@/backend/db";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET(request) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const id = searchParams.get("id");
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
// Fetch single article by ID
|
||||||
|
const result = await DB.pool(
|
||||||
|
`SELECT id, title, content, created_at, updated_at, created_by, updated_by, is_teacher_only
|
||||||
|
FROM news_articles
|
||||||
|
WHERE id = $1`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "News article not found" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(result.rows[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all articles
|
||||||
|
const articles = await DB.pool(
|
||||||
|
`SELECT id, title, content, created_at, updated_at, created_by, updated_by, is_teacher_only
|
||||||
|
FROM news_articles
|
||||||
|
ORDER BY created_at DESC`
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json(articles.rows);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching news articles:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to fetch news articles" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request) {
|
||||||
|
try {
|
||||||
|
const { title, content, is_teacher_only } = await request.json();
|
||||||
|
|
||||||
|
if (!title || !content) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Title and content are required" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await DB.pool(
|
||||||
|
`INSERT INTO news_articles (title, content, is_teacher_only)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING id, title, content, created_at, updated_at, is_teacher_only`,
|
||||||
|
[title, content, is_teacher_only || false]
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json(result.rows[0], { status: 201 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating news article:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to create news article" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(request) {
|
||||||
|
try {
|
||||||
|
const { id, title, content, is_teacher_only } = await request.json();
|
||||||
|
|
||||||
|
if (!id || !title || !content) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "ID, title, and content are required" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await DB.pool(
|
||||||
|
`UPDATE news_articles
|
||||||
|
SET title = $1, content = $2, is_teacher_only = $4, updated_at = now()
|
||||||
|
WHERE id = $3
|
||||||
|
RETURNING id, title, content, created_at, updated_at, is_teacher_only`,
|
||||||
|
[title, content, id, is_teacher_only || false]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "News article not found" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(result.rows[0]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating news article:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to update news article" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(request) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const id = searchParams.get("id");
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json({ error: "ID is required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await DB.pool(`DELETE FROM news_articles WHERE id = $1`, [
|
||||||
|
id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "News article not found" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ message: "News article deleted successfully" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting news article:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to delete news article" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { DB } from "@/backend/db";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const articles = await DB.pool(
|
||||||
|
`SELECT id, title, content, created_at, updated_at, is_teacher_only
|
||||||
|
FROM news_articles
|
||||||
|
WHERE is_teacher_only = false
|
||||||
|
ORDER BY created_at DESC`
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json(articles.rows);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching news articles:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to fetch news articles" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { DB } from "@/backend/db";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const articles = await DB.pool(
|
||||||
|
`SELECT id, title, content, created_at, updated_at, is_teacher_only
|
||||||
|
FROM news_articles
|
||||||
|
ORDER BY is_teacher_only DESC, created_at DESC`
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json(articles.rows);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching teacher news articles:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to fetch teacher news articles" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
.news-card {
|
||||||
|
background-color: var(--bg3);
|
||||||
|
padding: var(--u-md);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--bg3);
|
||||||
|
border-left: 4px solid var(--fg2);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: box-shadow 0.2s, border-left-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card:hover {
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
border-left-color: var(--fg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-title {
|
||||||
|
color: var(--fg1);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-date {
|
||||||
|
color: var(--fg2);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"use client";
|
||||||
|
import { Container, Column, Row } from "@/components/ui/layout/container";
|
||||||
|
import useQuery from "@/shared/hooks/useQuery";
|
||||||
|
import RenderHTML from "@/components/ui/render-html/render-html";
|
||||||
|
import "./news.css";
|
||||||
|
|
||||||
|
export default function NewsPage() {
|
||||||
|
const { data: articles, error, isLoading } = useQuery("/news");
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <p>Wird geladen...</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <p className="error">Fehler beim Laden der Neuigkeiten</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedArticles = [...(articles || [])].sort(
|
||||||
|
(a, b) => new Date(b.created_at) - new Date(a.created_at)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column gap="md" width="100%">
|
||||||
|
<Row justify="space-between" gap="xl" width="100%" align="center">
|
||||||
|
<h2>Neuigkeiten</h2>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{sortedArticles.length === 0 ? (
|
||||||
|
<p>Keine Neuigkeiten vorhanden.</p>
|
||||||
|
) : (
|
||||||
|
<Container display="grid" gap="md">
|
||||||
|
{sortedArticles.map((article) => (
|
||||||
|
<Container key={article.id} className="news-card">
|
||||||
|
<Column gap="sm">
|
||||||
|
<h3 className="news-title">{article.title}</h3>
|
||||||
|
<p className="news-date">
|
||||||
|
{new Date(article.created_at).toLocaleDateString("de-DE", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<RenderHTML data={article.content} disableGlossary />
|
||||||
|
</Column>
|
||||||
|
</Container>
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
.news-card {
|
||||||
|
background-color: var(--bg3);
|
||||||
|
padding: var(--u-md);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--bg3);
|
||||||
|
border-left: 4px solid var(--fg2);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: box-shadow 0.2s, border-left-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card:hover {
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
border-left-color: var(--fg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card--teacher {
|
||||||
|
border-left-color: var(--fg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card--teacher:hover {
|
||||||
|
border-left-color: var(--fg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-title {
|
||||||
|
color: var(--fg1);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-date {
|
||||||
|
color: var(--fg2);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
"use client";
|
||||||
|
import { Container, Column, Row } from "@/components/ui/layout/container";
|
||||||
|
import useQuery from "@/shared/hooks/useQuery";
|
||||||
|
import RenderHTML from "@/components/ui/render-html/render-html";
|
||||||
|
import "./news.css";
|
||||||
|
|
||||||
|
export default function TeacherNewsPage() {
|
||||||
|
const { data: articles, error, isLoading } = useQuery("/teacher/news");
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <p>Wird geladen...</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <p className="error">Fehler beim Laden der Neuigkeiten</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedArticles = [...(articles || [])].sort(
|
||||||
|
(a, b) => new Date(b.created_at) - new Date(a.created_at)
|
||||||
|
);
|
||||||
|
|
||||||
|
const teacherOnlyArticles = sortedArticles.filter((a) => a.is_teacher_only);
|
||||||
|
const generalArticles = sortedArticles.filter((a) => !a.is_teacher_only);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column gap="md" width="100%">
|
||||||
|
<Row justify="space-between" gap="xl" width="100%" align="center">
|
||||||
|
<h2>Neuigkeiten</h2>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{sortedArticles.length === 0 ? (
|
||||||
|
<p>Keine Neuigkeiten vorhanden.</p>
|
||||||
|
) : (
|
||||||
|
<Container display="grid" gap="md">
|
||||||
|
{teacherOnlyArticles.length > 0 && (
|
||||||
|
<>
|
||||||
|
<h3 style={{ marginTop: "0.5rem", marginBottom: "0" }}>
|
||||||
|
Für Lehrer
|
||||||
|
</h3>
|
||||||
|
{teacherOnlyArticles.map((article) => (
|
||||||
|
<Container
|
||||||
|
key={article.id}
|
||||||
|
className="news-card news-card--teacher"
|
||||||
|
>
|
||||||
|
<Column gap="sm">
|
||||||
|
<h4 className="news-title">{article.title}</h4>
|
||||||
|
<p className="news-date">
|
||||||
|
{new Date(article.created_at).toLocaleDateString(
|
||||||
|
"de-DE",
|
||||||
|
{
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<RenderHTML data={article.content} disableGlossary />
|
||||||
|
</Column>
|
||||||
|
</Container>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{generalArticles.length > 0 && (
|
||||||
|
<>
|
||||||
|
{teacherOnlyArticles.length > 0 && (
|
||||||
|
<h3 style={{ marginTop: "1.5rem", marginBottom: "0" }}>
|
||||||
|
Allgemein
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
|
{generalArticles.map((article) => (
|
||||||
|
<Container key={article.id} className="news-card">
|
||||||
|
<Column gap="sm">
|
||||||
|
<h4 className="news-title">{article.title}</h4>
|
||||||
|
<p className="news-date">
|
||||||
|
{new Date(article.created_at).toLocaleDateString(
|
||||||
|
"de-DE",
|
||||||
|
{
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<RenderHTML data={article.content} disableGlossary />
|
||||||
|
</Column>
|
||||||
|
</Container>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -115,6 +115,9 @@ function StudentSideBar() {
|
|||||||
<Link className={styles.sidebarLink} href="/glossary">
|
<Link className={styles.sidebarLink} href="/glossary">
|
||||||
Glossar
|
Glossar
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link className={styles.sidebarLink} href="/news">
|
||||||
|
Neuigkeiten
|
||||||
|
</Link>
|
||||||
<Link className={styles.sidebarLink} href="/contact">
|
<Link className={styles.sidebarLink} href="/contact">
|
||||||
Brauchen Sie Hilfe?
|
Brauchen Sie Hilfe?
|
||||||
</Link>
|
</Link>
|
||||||
@@ -181,6 +184,16 @@ const extraLinks = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const adminSidebarLinks = [
|
const adminSidebarLinks = [
|
||||||
|
{
|
||||||
|
title: "Neuigkeiten",
|
||||||
|
link: "/teacher/news",
|
||||||
|
id: "teacher-news-view",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Neuigkeiten (Verwaltung)",
|
||||||
|
link: "/admin/news",
|
||||||
|
id: "news",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Übungen erstellen",
|
title: "Übungen erstellen",
|
||||||
link: "/admin/create-exercise",
|
link: "/admin/create-exercise",
|
||||||
|
|||||||
Reference in New Issue
Block a user