added zustand and global dialog component
This commit is contained in:
parent
b2cd0fc560
commit
0e614f544a
@ -77,7 +77,8 @@
|
|||||||
"tiptap-extension-resize-image": "^1.2.1",
|
"tiptap-extension-resize-image": "^1.2.1",
|
||||||
"use-debounce": "^10.0.4",
|
"use-debounce": "^10.0.4",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2",
|
||||||
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/eslint": "^8.56.10",
|
"@types/eslint": "^8.56.10",
|
||||||
|
|||||||
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
@ -176,6 +176,9 @@ importers:
|
|||||||
zod:
|
zod:
|
||||||
specifier: ^3.24.2
|
specifier: ^3.24.2
|
||||||
version: 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:
|
devDependencies:
|
||||||
'@types/eslint':
|
'@types/eslint':
|
||||||
specifier: ^8.56.10
|
specifier: ^8.56.10
|
||||||
@ -4774,6 +4777,24 @@ packages:
|
|||||||
react:
|
react:
|
||||||
optional: true
|
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:
|
zwitch@2.0.4:
|
||||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||||
|
|
||||||
@ -9663,4 +9684,10 @@ snapshots:
|
|||||||
'@types/react': 18.3.18
|
'@types/react': 18.3.18
|
||||||
react: 18.3.1
|
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: {}
|
zwitch@2.0.4: {}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import GlobalDialog from "@/components/global-dialog";
|
||||||
import { AppSidebar } from "@/components/layout/app-sidebar";
|
import { AppSidebar } from "@/components/layout/app-sidebar";
|
||||||
import Navbar from "@/components/layout/navbar";
|
import Navbar from "@/components/layout/navbar";
|
||||||
import { SessionProvider } from "@/components/session-provider";
|
import { SessionProvider } from "@/components/session-provider";
|
||||||
@ -13,6 +14,7 @@ async function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<HydrateClient>
|
<HydrateClient>
|
||||||
<SessionProvider session={session}>
|
<SessionProvider session={session}>
|
||||||
|
<GlobalDialog />
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<AppSidebar user={session?.user} />
|
<AppSidebar user={session?.user} />
|
||||||
<div className="h-screen w-full bg-background">
|
<div className="h-screen w-full bg-background">
|
||||||
|
|||||||
@ -2,16 +2,19 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import CommentForm from "@/components/comment/comment-form";
|
import CommentForm from "@/components/comment/comment-form";
|
||||||
import CommentList from "@/components/comment/comment-list";
|
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 { Badge } from "../ui/badge";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { useComments } from "@/lib/hooks/use-comments-hook";
|
import { useComments } from "@/lib/hooks/use-comments-hook";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { SocketProvider, useSocket } from "../socket-context";
|
import { useSocket } from "../socket-context";
|
||||||
import { JOIN_ROOM_EVENT } from "@/server/socket/event-const";
|
import { JOIN_ROOM_EVENT } from "@/server/socket/event-const";
|
||||||
|
import { useSession } from "../session-provider";
|
||||||
|
import { LogIn } from "lucide-react";
|
||||||
|
|
||||||
function CommentSection({ articleId }: { articleId: string }) {
|
function CommentSection({ articleId }: { articleId: string }) {
|
||||||
const { comments } = useComments(articleId);
|
const { comments } = useComments(articleId);
|
||||||
|
const { session } = useSession();
|
||||||
const [replyComment, setReplyComment] = useState<CommentNode | undefined>();
|
const [replyComment, setReplyComment] = useState<CommentNode | undefined>();
|
||||||
const commentRefs = useRef<Map<string, HTMLDivElement | null>>(new Map());
|
const commentRefs = useRef<Map<string, HTMLDivElement | null>>(new Map());
|
||||||
const { socket } = useSocket();
|
const { socket } = useSocket();
|
||||||
@ -28,12 +31,25 @@ function CommentSection({ articleId }: { articleId: string }) {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="sticky top-0 z-50 bg-background pt-4">
|
<div className="sticky top-0 z-50 bg-background pt-4">
|
||||||
|
{session ? (
|
||||||
<CommentForm
|
<CommentForm
|
||||||
articleId={articleId}
|
articleId={articleId}
|
||||||
parentComment={replyComment}
|
parentComment={replyComment}
|
||||||
removeReplyComment={() => setReplyComment(undefined)}
|
removeReplyComment={() => setReplyComment(undefined)}
|
||||||
commentRefs={commentRefs}
|
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>
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { Session } from "next-auth";
|
|||||||
import { Badge } from "../ui/badge";
|
import { Badge } from "../ui/badge";
|
||||||
import CommentDeleteButton from "./comment-delete-button";
|
import CommentDeleteButton from "./comment-delete-button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useGlobalDialogStore } from "@/lib/store/use-global-dialog";
|
||||||
|
|
||||||
const Comment = React.memo(
|
const Comment = React.memo(
|
||||||
(props: {
|
(props: {
|
||||||
@ -19,9 +20,19 @@ const Comment = React.memo(
|
|||||||
level?: number;
|
level?: number;
|
||||||
}) => {
|
}) => {
|
||||||
const { comment, setReplyComment, setRef, session, level = 0 } = props;
|
const { comment, setReplyComment, setRef, session, level = 0 } = props;
|
||||||
|
|
||||||
const isAuthor = session?.user?.id === comment?.author?.id;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -62,11 +73,7 @@ const Comment = React.memo(
|
|||||||
<div className="flex w-full items-center gap-2 pb-6">
|
<div className="flex w-full items-center gap-2 pb-6">
|
||||||
<CommentVoteButton vote commentId={comment.id} />
|
<CommentVoteButton vote commentId={comment.id} />
|
||||||
<CommentVoteButton vote={false} commentId={comment.id} />
|
<CommentVoteButton vote={false} commentId={comment.id} />
|
||||||
<Button
|
<Button size={"sm"} variant={"ghost"} onClick={handleReplyClick}>
|
||||||
size={"sm"}
|
|
||||||
variant={"ghost"}
|
|
||||||
onClick={() => setReplyComment(comment)}
|
|
||||||
>
|
|
||||||
Antworten
|
Antworten
|
||||||
</Button>
|
</Button>
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
|
|||||||
50
src/components/global-dialog.tsx
Normal file
50
src/components/global-dialog.tsx
Normal 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;
|
||||||
37
src/lib/store/use-global-dialog.ts
Normal file
37
src/lib/store/use-global-dialog.ts
Normal 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,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
}));
|
||||||
Loading…
x
Reference in New Issue
Block a user