Release v2.0.1 #99

Merged
PxlLoewe merged 26 commits from staging into release 2025-07-25 00:25:07 +00:00
17 changed files with 215 additions and 177 deletions
Showing only changes of commit a5c4a1dc7c - Show all commits

View File

@@ -7,14 +7,22 @@ import AdminPanel from "_components/navbar/AdminPanel";
import { getServerSession } from "api/auth/[...nextauth]/auth";
import { WarningAlert } from "_components/navbar/PageAlert";
import { Radar } from "lucide-react";
import { ChangelogWrapper } from "_components/navbar/ChangelogWrapper";
import { prisma } from "@repo/db";
export default async function Navbar() {
const session = await getServerSession();
const latestChangelog = await prisma.changelog.findFirst({
orderBy: {
createdAt: "desc",
},
});
return (
<div className="navbar bg-base-100 flex justify-between gap-5 shadow-sm">
<div className="flex items-center gap-2">
<p className="text-xl font-semibold normal-case">VAR Leitstelle V2</p>
<p className="text-xl font-semibold normal-case">VAR Leitstelle</p>
<ChangelogWrapper latestChangelog={latestChangelog} />
{session?.user.permissions.includes("ADMIN_KICK") && <AdminPanel />}
</div>
<WarningAlert />

View File

@@ -5,12 +5,20 @@ import Link from "next/link";
import { Settings } from "./_components/Settings";
import { WarningAlert } from "_components/navbar/PageAlert";
import { Radar } from "lucide-react";
import { prisma } from "@repo/db";
import { ChangelogWrapper } from "_components/navbar/ChangelogWrapper";
export default function Navbar() {
export default async function Navbar() {
const latestChangelog = await prisma.changelog.findFirst({
orderBy: {
createdAt: "desc",
},
});
return (
<div className="navbar bg-base-100 flex justify-between gap-5 shadow-sm">
<div className="flex items-center gap-2">
<p className="text-xl font-semibold normal-case">VAR Operations Center</p>
<ChangelogWrapper latestChangelog={latestChangelog} />
</div>
<WarningAlert />
<div className="flex items-center gap-5">

View File

@@ -113,7 +113,6 @@ export const SmartPopup = (
);
const handleConflict = useCallback(() => {
console.log("handleConflict in smartMarker", id, options);
const newAnchor = calculateAnchor(id, "popup", options);
setAnchor(newAnchor);
}, [id, options]);

View File

@@ -0,0 +1,34 @@
"use client";
import { Changelog } from "@repo/db";
import { ChangelogModalBtn } from "@repo/shared-components";
import { useMutation } from "@tanstack/react-query";
import { editUserAPI } from "_querys/user";
import { useSession } from "next-auth/react";
import toast from "react-hot-toast";
export const ChangelogWrapper = ({ latestChangelog }: { latestChangelog: Changelog | null }) => {
const { data: session } = useSession();
const editUserMutation = useMutation({
mutationFn: editUserAPI,
});
const autoOpen = !session?.user.changelogAck && !!latestChangelog;
if (!latestChangelog) return null;
if (!session) return null;
return (
<ChangelogModalBtn
hideIcon
className="text-sm text-gray-500"
latestChangelog={latestChangelog}
autoOpen={autoOpen}
onClose={async () => {
await editUserMutation.mutateAsync({ id: session?.user.id, user: { changelogAck: true } });
if (!session?.user.changelogAck) {
toast.success("Changelog als gelesen markiert");
}
}}
/>
);
};

View File

@@ -5,7 +5,7 @@
"private": true,
"packageManager": "pnpm@10.13.1",
"scripts": {
"dev": "next dev --turbopack -p 3001",
"dev": "next dev -p 3001",
"build": "next build",
"start": "next start",
"lint": "next lint --max-warnings 0",

View File

@@ -1,124 +0,0 @@
"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";
import { Changelog } from "@repo/db";
export const ChangelogModal = ({
latestChangelog,
isOpen,
onClose,
}: {
latestChangelog: Changelog | 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: Changelog | 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: Changelog | null;
}) => {
const [isOpen, setIsOpen] = useState(true);
if (!latestChangelog) return null;
return (
<>
<ChangelogModal
latestChangelog={latestChangelog}
isOpen={isOpen}
onClose={() => setIsOpen(false)}
/>
</>
);
};

View File

@@ -1,33 +0,0 @@
"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 latestChangelog[0];
}
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

@@ -0,0 +1,26 @@
"use client";
import { updateUser } from "(app)/settings/actions";
import { Changelog } from "@repo/db";
import { ChangelogModalBtn } from "@repo/shared-components";
import { useSession } from "next-auth/react";
import toast from "react-hot-toast";
export const ChangelogWrapper = ({ latestChangelog }: { latestChangelog: Changelog | null }) => {
const { data: session } = useSession();
const autoOpen = !session?.user.changelogAck && !!latestChangelog;
if (!latestChangelog) return null;
return (
<ChangelogModalBtn
latestChangelog={latestChangelog}
autoOpen={autoOpen}
onClose={async () => {
await updateUser({ changelogAck: true });
if (!session?.user.changelogAck) {
toast.success("Changelog als gelesen markiert");
}
}}
/>
);
};

View File

@@ -2,11 +2,22 @@ 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";
import { ChangelogModalBtn } from "@repo/shared-components";
import { getServerSession } from "api/auth/[...nextauth]/auth";
import { updateUser } from "(app)/settings/actions";
import toast from "react-hot-toast";
import { ChangelogWrapper } from "(app)/_components/ChangelogWrapper";
import { prisma } from "@repo/db";
export const Footer = async () => {
const latestChangelog = await getLatestChangelog();
const session = await getServerSession();
const latestChangelog = await prisma.changelog.findFirst({
orderBy: {
createdAt: "desc",
},
});
const autoOpen = !session?.user.changelogAck && !!latestChangelog;
return (
<footer className="footer bg-base-200 mt-4 flex items-center justify-between rounded-lg p-4 shadow-md">
@@ -18,7 +29,7 @@ export const Footer = async () => {
<a href="https://virtualairrescue.com/datenschutz/" className="hover:text-primary">
Datenschutzerklärung
</a>
<ChangelogBtn latestChangelog={latestChangelog} />
<ChangelogWrapper latestChangelog={latestChangelog} />
</div>
{/* Center: Copyright */}

View File

@@ -9,7 +9,6 @@ import { useEffect, useState } from "react";
import { deleteChangelog, upsertChangelog } from "../action";
import { Button } from "../../../../_components/ui/Button";
import { redirect } from "next/navigation";
import Image from "next/image";
import dynamic from "next/dynamic";
import toast from "react-hot-toast";
@@ -73,7 +72,13 @@ export const ChangelogForm = ({ changelog }: { changelog?: Changelog }) => {
<h2 className="card-title">
<FileText className="h-5 w-5" /> Allgemeines
</h2>
<Input form={form} label="Titel" name="title" className="input-sm" />
<Input
form={form}
label="Titel"
name="title"
placeholder="Titel (vX.X.X)"
className="input-sm"
/>
<Input
form={form}
label="Bild-URL"
@@ -91,7 +96,7 @@ export const ChangelogForm = ({ changelog }: { changelog?: Changelog }) => {
{(() => {
if (showImage && isValidImageUrl(previewImage) && !imageError) {
return (
<Image
<img
src={previewImage}
alt="Preview"
width={200}

View File

@@ -1,7 +1,6 @@
"use server";
import { prisma, Prisma, Changelog } from "@repo/db";
export const upsertChangelog = async (
changelog: Prisma.ChangelogCreateInput,
id?: Changelog["id"],

View File

@@ -5,8 +5,6 @@ import { getServerSession } from "../api/auth/[...nextauth]/auth";
import { EmailVerification } from "_components/EmailVerification";
import { FirstPath } from "./_components/FirstPath";
import { Penalty } from "_components/Penalty";
import { getLatestChangelog } from "(app)/_components/ChangelogActions";
import { OpenChangelogOnPageload } from "(app)/_components/Changelog";
import { Footer } from "(app)/_components/Footer";
@@ -24,8 +22,6 @@ export default async function RootLayout({
if (!session) redirect(`/login`);
const latestChangelog = !session.user.changelogAck ? await getLatestChangelog() : null;
return (
<div
className="hero min-h-screen"
@@ -54,9 +50,7 @@ export default async function RootLayout({
</div>
)}
{!session.user.pathSelected && <FirstPath />}
{session.user.pathSelected && latestChangelog && (
<OpenChangelogOnPageload latestChangelog={latestChangelog} />
)}
{children}
</div>
</div>

View File

@@ -4,7 +4,7 @@
"private": true,
"packageManager": "pnpm@10.13.1",
"scripts": {
"dev": "next dev --turbopack -p 3000",
"dev": "next dev -p 3000",
"build": "next build",
"start": "next start",
"lint": "next lint"

View File

@@ -0,0 +1,102 @@
"use client";
import { useState } from "react";
import { Button, cn } from "@repo/shared-components";
import MDEditor from "@uiw/react-md-editor";
import { RefreshCw } from "lucide-react";
import { Changelog } from "@repo/db";
export const ChangelogModal = ({
latestChangelog,
isOpen,
onClose,
}: {
latestChangelog: Changelog;
isOpen: boolean;
onClose: () => void;
}) => {
return (
<dialog open={isOpen} 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={onClose}
>
</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 && (
<img
src={latestChangelog.previewImage}
alt="Preview"
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={onClose}>
Weiter zum HUB
</Button>
</div>
</div>
<label className="modal-backdrop" htmlFor="changelogModalToggle">
Close
</label>
</dialog>
);
};
export const ChangelogModalBtn = ({
latestChangelog,
autoOpen,
onClose,
className = "",
hideIcon = false,
}: {
latestChangelog: Changelog | null | undefined;
autoOpen: boolean;
onClose?: () => void;
className?: string;
hideIcon?: boolean;
}) => {
const [isOpen, setIsOpen] = useState(autoOpen);
if (!latestChangelog) return null;
return (
<>
<a
href="#!"
className={cn("hover:text-primary flex items-center gap-1", className)}
onClick={() => setIsOpen(true)}
>
{!hideIcon && <RefreshCw size={12} />} {latestChangelog.title}
</a>
<ChangelogModal
latestChangelog={latestChangelog}
isOpen={isOpen}
onClose={() => {
setIsOpen(false);
if (onClose) {
onClose();
}
}}
/>
</>
);
};

View File

@@ -2,3 +2,4 @@ export * from "./Badge";
export * from "./PenaltyDropdown";
export * from "./Maintenance";
export * from "./Button";
export * from "./Changelog";

View File

@@ -10,8 +10,10 @@
"@repo/db": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "^22.15.29",
"@uiw/react-md-editor": "^4.0.8",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.525.0",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {

6
pnpm-lock.yaml generated
View File

@@ -618,12 +618,18 @@ importers:
'@types/node':
specifier: ^22.15.29
version: 22.15.29
'@uiw/react-md-editor':
specifier: ^4.0.8
version: 4.0.8(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
clsx:
specifier: ^2.1.1
version: 2.1.1
date-fns:
specifier: ^4.1.0
version: 4.1.0
lucide-react:
specifier: ^0.525.0
version: 0.525.0(react@19.1.0)
tailwind-merge:
specifier: ^3.3.1
version: 3.3.1