added zustand and global dialog component

This commit is contained in:
mr-shortman 2025-03-23 13:16:59 +01:00
parent b2cd0fc560
commit 0e614f544a
7 changed files with 157 additions and 17 deletions

View File

@ -77,7 +77,8 @@
"tiptap-extension-resize-image": "^1.2.1",
"use-debounce": "^10.0.4",
"winston": "^3.17.0",
"zod": "^3.24.2"
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
"@types/eslint": "^8.56.10",

27
pnpm-lock.yaml generated
View File

@ -176,6 +176,9 @@ importers:
zod:
specifier: ^3.24.2
version: 3.24.2
zustand:
specifier: ^5.0.3
version: 5.0.3(@types/react@18.3.18)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1))
devDependencies:
'@types/eslint':
specifier: ^8.56.10
@ -4774,6 +4777,24 @@ packages:
react:
optional: true
zustand@5.0.3:
resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=18.0.0'
immer: '>=9.0.6'
react: '>=18.0.0'
use-sync-external-store: '>=1.2.0'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
use-sync-external-store:
optional: true
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@ -9663,4 +9684,10 @@ snapshots:
'@types/react': 18.3.18
react: 18.3.1
zustand@5.0.3(@types/react@18.3.18)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)):
optionalDependencies:
'@types/react': 18.3.18
react: 18.3.1
use-sync-external-store: 1.4.0(react@18.3.1)
zwitch@2.0.4: {}

View File

@ -1,3 +1,4 @@
import GlobalDialog from "@/components/global-dialog";
import { AppSidebar } from "@/components/layout/app-sidebar";
import Navbar from "@/components/layout/navbar";
import { SessionProvider } from "@/components/session-provider";
@ -13,6 +14,7 @@ async function Layout({ children }: { children: React.ReactNode }) {
return (
<HydrateClient>
<SessionProvider session={session}>
<GlobalDialog />
<SidebarProvider>
<AppSidebar user={session?.user} />
<div className="h-screen w-full bg-background">

View File

@ -2,16 +2,19 @@
import React, { useEffect, useRef, useState } from "react";
import CommentForm from "@/components/comment/comment-form";
import CommentList from "@/components/comment/comment-list";
import { Comment, CommentNode } from "@/server/db/schema";
import { CommentNode } from "@/server/db/schema";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { useComments } from "@/lib/hooks/use-comments-hook";
import { SocketProvider, useSocket } from "../socket-context";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { useSocket } from "../socket-context";
import { JOIN_ROOM_EVENT } from "@/server/socket/event-const";
import { useSession } from "../session-provider";
import { LogIn } from "lucide-react";
function CommentSection({ articleId }: { articleId: string }) {
const { comments } = useComments(articleId);
const { session } = useSession();
const [replyComment, setReplyComment] = useState<CommentNode | undefined>();
const commentRefs = useRef<Map<string, HTMLDivElement | null>>(new Map());
const { socket } = useSocket();
@ -28,12 +31,25 @@ function CommentSection({ articleId }: { articleId: string }) {
return (
<div className="space-y-4">
<div className="sticky top-0 z-50 bg-background pt-4">
<CommentForm
articleId={articleId}
parentComment={replyComment}
removeReplyComment={() => setReplyComment(undefined)}
commentRefs={commentRefs}
/>
{session ? (
<CommentForm
articleId={articleId}
parentComment={replyComment}
removeReplyComment={() => setReplyComment(undefined)}
commentRefs={commentRefs}
/>
) : (
<Alert className="flex items-center">
<LogIn className="size-4" />
<AlertTitle>
Melde dich an um einen Kommentar zu schreiben
</AlertTitle>
<Button size={"sm"} className="ml-auto">
Jetzt Anmelden
</Button>
</Alert>
)}
</div>
<div className="flex items-center justify-between gap-4">

View File

@ -9,6 +9,7 @@ import { Session } from "next-auth";
import { Badge } from "../ui/badge";
import CommentDeleteButton from "./comment-delete-button";
import { cn } from "@/lib/utils";
import { useGlobalDialogStore } from "@/lib/store/use-global-dialog";
const Comment = React.memo(
(props: {
@ -19,9 +20,19 @@ const Comment = React.memo(
level?: number;
}) => {
const { comment, setReplyComment, setRef, session, level = 0 } = props;
const isAuthor = session?.user?.id === comment?.author?.id;
const openGlobalDialog = useGlobalDialogStore((state) => state.openDialog);
const handleReplyClick = () => {
if (!session)
openGlobalDialog({
title: "Du bist nicht angemeldet",
message:
"Um auf einen Kommentar zu antworten, musst du dich anmelden.",
action: "/api/auth/signin",
actionText: "Jetzt Anmelden",
});
else setReplyComment(comment);
};
return (
<div
className={cn(
@ -62,11 +73,7 @@ const Comment = React.memo(
<div className="flex w-full items-center gap-2 pb-6">
<CommentVoteButton vote commentId={comment.id} />
<CommentVoteButton vote={false} commentId={comment.id} />
<Button
size={"sm"}
variant={"ghost"}
onClick={() => setReplyComment(comment)}
>
<Button size={"sm"} variant={"ghost"} onClick={handleReplyClick}>
Antworten
</Button>
<div className="ml-auto">

View File

@ -0,0 +1,50 @@
"use client";
import React from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useGlobalDialogStore } from "@/lib/store/use-global-dialog";
import { Button } from "./ui/button";
import Link from "next/link";
function GlobalDialog() {
const dialog = useGlobalDialogStore((state) => state.globalDialog);
const closeDialog = useGlobalDialogStore((state) => state.closeDialog);
const actionText = <span>{dialog.actionText}</span>;
const actionContent =
typeof dialog.action === "string" ? (
<Link href={dialog.action!} className="link link-primary">
{actionText}
</Link>
) : (
actionText
);
return (
<Dialog open={dialog.open} onOpenChange={closeDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>{dialog.title}</DialogTitle>
<DialogDescription>{dialog.message}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant={"outline"} onClick={closeDialog}>
Abbrechen
</Button>
<Button
className="btn btn-primary"
asChild={typeof dialog.action === "string"}
>
{actionContent}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export default GlobalDialog;

View File

@ -0,0 +1,37 @@
import { create } from "zustand";
type GlobalDialogType = {
title: string;
message: string;
action: (() => void) | string;
actionText: string;
};
interface GlobalDialogStoreState {
globalDialog: {
open: boolean;
} & Partial<GlobalDialogType>;
openDialog: (dialog: GlobalDialogType) => void;
closeDialog: () => void;
}
export const useGlobalDialogStore = create<GlobalDialogStoreState>()((set) => ({
globalDialog: {
open: false,
},
openDialog(dialog: GlobalDialogType) {
set(() => ({
globalDialog: {
...dialog,
open: true,
},
}));
},
closeDialog() {
set(() => ({
globalDialog: {
open: false,
},
}));
},
}));