+ Changelog Funktionalität

completed #95
This commit is contained in:
nocnico
2025-07-24 16:33:10 +02:00
parent 0b0f4fac2f
commit 0c80c046e1
14 changed files with 478 additions and 23 deletions

View File

@@ -0,0 +1,127 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import { Button } from "@repo/shared-components";
import MDEditor from "@uiw/react-md-editor";
import { RefreshCw } from "lucide-react";
import { updateChangelogAck } from "./ChangelogActions";
export const ChangelogModal = ({
latestChangelog,
isOpen,
onClose,
}: {
latestChangelog: { title: string; text: string; previewImage: string } | null;
isOpen: boolean;
onClose: () => void;
}) => {
if (!isOpen || !latestChangelog) return null;
const handleClose = async () => {
onClose();
await updateChangelogAck();
};
return (
<div>
<input
type="checkbox"
id="changelogModalToggle"
className="modal-toggle"
onChange={handleClose}
/>
<dialog open className="modal p-4">
<div className="modal-box max-h-11/12 w-11/12 max-w-2xl overflow-y-auto">
<form method="dialog">
<button
className="btn btn-sm btn-circle btn-ghost absolute right-3 top-3"
onClick={handleClose}
>
</button>
</form>
<h3 className="flex items-center gap-2 text-lg font-bold">
<span className="text-primary">{latestChangelog.title}</span> ist nun Verfügbar!
</h3>
<div className="flex flex-col items-center">
{latestChangelog.previewImage && (
<Image
src={latestChangelog.previewImage}
alt="Preview"
width={800}
height={400}
className="mt-4 h-auto w-full object-cover"
/>
)}
</div>
<div className="text-base-content/80 mb-2 mt-4 text-left">
<MDEditor.Markdown
source={latestChangelog.text}
style={{
backgroundColor: "transparent",
}}
/>
</div>
<div className="modal-action">
<Button className="btn btn-info btn-outline" onClick={handleClose}>
Weiter zum HUB
</Button>
</div>
</div>
<label className="modal-backdrop" htmlFor="changelogModalToggle">
Close
</label>
</dialog>
</div>
);
};
export const ChangelogBtn = ({
latestChangelog,
}: {
latestChangelog: { title: string; text: string; previewImage: string } | null;
}) => {
const [isOpen, setIsOpen] = useState(false);
if (!latestChangelog) return null;
return (
<>
<a
href="#!"
className="hover:text-primary flex items-center gap-1"
onClick={() => setIsOpen(true)}
>
<RefreshCw size={12} /> {latestChangelog.title}
</a>
<ChangelogModal
latestChangelog={latestChangelog}
isOpen={isOpen}
onClose={() => setIsOpen(false)}
/>
</>
);
};
export const OpenChangelogOnPageload = ({
latestChangelog,
}: {
latestChangelog: { title: string; text: string; previewImage: string } | null;
}) => {
const [isOpen, setIsOpen] = useState(true);
if (!latestChangelog) return null;
return (
<>
<ChangelogModal
latestChangelog={latestChangelog}
isOpen={isOpen}
onClose={() => setIsOpen(false)}
/>
</>
);
};

View File

@@ -0,0 +1,37 @@
"use server";
import { prisma } from "@repo/db";
import { getServerSession } from "../../api/auth/[...nextauth]/auth";
export async function getLatestChangelog() {
try {
const latestChangelog = await prisma.changelog.findMany({
orderBy: {
createdAt: "desc",
},
take: 1,
});
if (latestChangelog.length > 0 && latestChangelog[0]) {
return {
title: latestChangelog[0].title,
text: latestChangelog[0].text,
previewImage: latestChangelog[0].previewImage || "",
};
}
return null;
} catch (error) {
console.error("Failed to fetch latest changelog:", error);
throw new Error("Failed to fetch latest changelog");
}
}
export async function updateChangelogAck() {
const session = await getServerSession();
if (session?.user) {
await prisma.user.update({
where: { id: session.user.id },
data: { changelogAck: true },
});
}
}

View File

@@ -2,10 +2,14 @@ import Image from "next/image";
import { DiscordLogoIcon, InstagramLogoIcon, ReaderIcon } from "@radix-ui/react-icons";
import YoutubeSvg from "./youtube_wider.svg";
import FacebookSvg from "./facebook.svg";
import { ChangelogBtn } from "./Changelog";
import { getLatestChangelog } from "./ChangelogActions";
export const Footer = async () => {
const latestChangelog = await getLatestChangelog();
export const Footer = () => {
return (
<footer className="footer flex justify-between items-center p-4 bg-base-200 mt-4 rounded-lg shadow-md">
<footer className="footer bg-base-200 mt-4 flex items-center justify-between rounded-lg p-4 shadow-md">
{/* Left: Impressum & Datenschutz */}
<div className="flex gap-4 text-sm">
<a href="https://virtualairrescue.com/impressum/" className="hover:text-primary">
@@ -14,6 +18,7 @@ export const Footer = () => {
<a href="https://virtualairrescue.com/datenschutz/" className="hover:text-primary">
Datenschutzerklärung
</a>
<ChangelogBtn latestChangelog={latestChangelog} />
</div>
{/* Center: Copyright */}
@@ -28,7 +33,7 @@ export const Footer = () => {
rel="noopener noreferrer"
className="hover:text-primary"
>
<DiscordLogoIcon className="w-5 h-5" />
<DiscordLogoIcon className="h-5 w-5" />
</a>
</div>
@@ -39,7 +44,7 @@ export const Footer = () => {
rel="noopener noreferrer"
className="hover:text-primary text-white"
>
<Image src={YoutubeSvg} className="invert w-5 h5" alt="Youtube Icon" />
<Image src={YoutubeSvg} className="h5 w-5 invert" alt="Youtube Icon" />
</a>
</div>
@@ -50,7 +55,7 @@ export const Footer = () => {
rel="noopener noreferrer"
className="hover:text-primary text-white"
>
<Image src={FacebookSvg} className="invert w-5 h5" alt="Youtube Icon" />
<Image src={FacebookSvg} className="h5 w-5 invert" alt="Youtube Icon" />
</a>
</div>
@@ -61,12 +66,12 @@ export const Footer = () => {
rel="noopener noreferrer"
className="hover:text-primary"
>
<InstagramLogoIcon className="w-5 h-5" />
<InstagramLogoIcon className="h-5 w-5" />
</a>
</div>
<div className="tooltip tooltip-top" data-tip="Knowledgebase">
<a href="https://docs.virtualairrescue.com/" className="hover:text-primary">
<ReaderIcon className="w-5 h-5" />
<ReaderIcon className="h-5 w-5" />
</a>
</div>
</div>