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
|
||||
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 (
|
||||
id bigint PRIMARY KEY NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
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">
|
||||
Glossar
|
||||
</Link>
|
||||
<Link className={styles.sidebarLink} href="/news">
|
||||
Neuigkeiten
|
||||
</Link>
|
||||
<Link className={styles.sidebarLink} href="/contact">
|
||||
Brauchen Sie Hilfe?
|
||||
</Link>
|
||||
@@ -181,6 +184,16 @@ const extraLinks = [
|
||||
];
|
||||
|
||||
const adminSidebarLinks = [
|
||||
{
|
||||
title: "Neuigkeiten",
|
||||
link: "/teacher/news",
|
||||
id: "teacher-news-view",
|
||||
},
|
||||
{
|
||||
title: "Neuigkeiten (Verwaltung)",
|
||||
link: "/admin/news",
|
||||
id: "news",
|
||||
},
|
||||
{
|
||||
title: "Übungen erstellen",
|
||||
link: "/admin/create-exercise",
|
||||
|
||||
Reference in New Issue
Block a user