From 955728d781c9c02b8942b906c0ebb6449309c94c Mon Sep 17 00:00:00 2001 From: Zintarh Date: Wed, 2 Jul 2025 18:19:41 +0100 Subject: [PATCH 1/7] feat: add discussions route --- src/app/dashboard/layout.tsx | 8 ++++---- src/components/dashboard/sidebar.tsx | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 11f959f..3dbfeff 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -8,9 +8,9 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( -
- -
{children}
-
+
+ +
{children}
+
); } diff --git a/src/components/dashboard/sidebar.tsx b/src/components/dashboard/sidebar.tsx index a0e7fdb..60e023f 100644 --- a/src/components/dashboard/sidebar.tsx +++ b/src/components/dashboard/sidebar.tsx @@ -9,6 +9,7 @@ import { LogOut, MessageSquare, User, + MessageCircle, } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; @@ -42,6 +43,11 @@ export function Sidebar() { label: "Readers Feedback", href: "/dashboard/feedback", }, + { + icon: MessageCircle, + label: "Discussions & Clubs", + href: "/dashboard/discussions", + }, { icon: Bell, label: "Notification", From 080f98948332a6dc46843079885fe6b14af076f3 Mon Sep 17 00:00:00 2001 From: Zintarh Date: Wed, 2 Jul 2025 18:20:05 +0100 Subject: [PATCH 2/7] feat: implement discussion/clubs --- next.config.js | 4 + src/app/dashboard/discussions/ClubCard.tsx | 99 +++++++++ .../discussions/components/ClubDetails.tsx | 196 ++++++++++++++++++ .../discussions/components/ClubToolBar.tsx | 52 +++++ src/app/dashboard/discussions/page.tsx | 124 +++++++++++ src/app/layout.tsx | 1 - src/lib/types.ts | 9 + 7 files changed, 484 insertions(+), 1 deletion(-) create mode 100644 src/app/dashboard/discussions/ClubCard.tsx create mode 100644 src/app/dashboard/discussions/components/ClubDetails.tsx create mode 100644 src/app/dashboard/discussions/components/ClubToolBar.tsx create mode 100644 src/app/dashboard/discussions/page.tsx create mode 100644 src/lib/types.ts diff --git a/next.config.js b/next.config.js index 8a35c52..d0a1a48 100644 --- a/next.config.js +++ b/next.config.js @@ -7,6 +7,10 @@ const nextConfig = { enabled: true, // Set turbo to true within an object }, }, + images: { + domains: ['randomuser.me'], + }, + }; // export default nextConfig; diff --git a/src/app/dashboard/discussions/ClubCard.tsx b/src/app/dashboard/discussions/ClubCard.tsx new file mode 100644 index 0000000..a62301b --- /dev/null +++ b/src/app/dashboard/discussions/ClubCard.tsx @@ -0,0 +1,99 @@ +import React from "react"; +import { Globe, PlusIcon } from "lucide-react"; +import { ClubDetailsProps } from "@/lib/types"; + +interface CardProps extends ClubDetailsProps { + onOpen: (club: ClubDetailsProps) => void; +} + +export default function ClubCard({ + id, + name, + isPublic, + memberCount, + sessionsInfo, + unreadNotifications, + authorAvatars, + onOpen, +}: CardProps) { + return ( +
+ +
+
+

+ {name} +

+ {unreadNotifications > 0 && ( + + {unreadNotifications} + + )} +
+
+
+
+
+ + {memberCount} +
+ +
+ {sessionsInfo} +
+
+ +
+
+ {authorAvatars && + authorAvatars.length > 0 && + authorAvatars.map((author) => ( + Author 1 + ))} +
+ + {authorAvatars && authorAvatars.length > 0 && ( + + {authorAvatars.length} Authors you follow are members + + )} +
+
+ +
+ ); +} diff --git a/src/app/dashboard/discussions/components/ClubDetails.tsx b/src/app/dashboard/discussions/components/ClubDetails.tsx new file mode 100644 index 0000000..f2e651d --- /dev/null +++ b/src/app/dashboard/discussions/components/ClubDetails.tsx @@ -0,0 +1,196 @@ +import { ClubDetailsProps } from "@/lib/types"; +import { Globe, PlusIcon } from "lucide-react"; +import React from "react"; + +type Props = { + club: ClubDetailsProps | null; + isOpen: boolean; + onClose: () => void; + onJoin: () => void; +}; + +export default function ClubModal({ club, isOpen, onClose, onJoin }: Props) { + if (!isOpen || !club) return null; + + return ( +
+
+
+ +
+ +
+
+ + +
+
+

About Club

+
+

+ Vibrant book club dedicated to exploring the vast and magical + realms of fantasy literature. From epic sagas and dark fantasy to + urban magic and whimsical tales, we delve into all corners of the + genre. +

+
+ +
+
+

Genre Focus

+
+
+ + Fiction + + + Drama + +
+
+
+ +
+ + +
+
+
+ ); +} + +function Badge({ text, icon }: { text: string; icon?: "users" | "calendar" }) { + let iconSvg = null; + if (icon === "users") { + iconSvg = ( + + + + ); + } else if (icon === "calendar") { + iconSvg = ( + + + + + ); + } + return ( + + {iconSvg} + {text} + + ); +} + +function AvatarGroup({ urls }: { urls: string[] }) { + return ( +
+ {urls.map((url, i) => ( + {`Author + ))} +
+ ); +} diff --git a/src/app/dashboard/discussions/components/ClubToolBar.tsx b/src/app/dashboard/discussions/components/ClubToolBar.tsx new file mode 100644 index 0000000..0d31302 --- /dev/null +++ b/src/app/dashboard/discussions/components/ClubToolBar.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import { Search, ChevronDown, PlusIcon } from "lucide-react"; + +export default function ClubsToolbar() { + return ( +
+
+
+

Clubs You've Joined (2)

+
+ +
+

Discover

+
+ + +
+ +
+
+ + +
+ + +
+
+ ); +} diff --git a/src/app/dashboard/discussions/page.tsx b/src/app/dashboard/discussions/page.tsx new file mode 100644 index 0000000..c511307 --- /dev/null +++ b/src/app/dashboard/discussions/page.tsx @@ -0,0 +1,124 @@ +"use client"; +import { Header } from "@/components/dashboard/header"; +import React, { useState } from "react"; +import ClubsToolbar from "./components/ClubToolBar"; +import ClubCard from "./ClubCard"; +import { ClubDetailsProps } from "@/lib/types"; +import ClubModal from "./components/ClubDetails"; + +export const clubCards: ClubDetailsProps[] = [ + { + id: "1", + name: "Fantasy Enthusiasts", + isPublic: true, + memberCount: 1500, + sessionsInfo: "2+ Sessions a month", + unreadNotifications: 3, + authorAvatars: [ + "https://randomuser.me/api/portraits/men/32.jpg", + "https://randomuser.me/api/portraits/women/44.jpg", + ], + }, + { + id: "2", + name: "Book Lovers", + isPublic: false, + memberCount: 1500, + sessionsInfo: "2+ Sessions in three months", + unreadNotifications: 2, + authorAvatars: [ + "https://randomuser.me/api/portraits/men/32.jpg", + "https://randomuser.me/api/portraits/women/44.jpg", + ], + }, + { + id: "3", + name: "Fantasy Enthusiasts", + isPublic: true, + memberCount: 1500, + sessionsInfo: "2+ Sessions a month", + unreadNotifications: 3, + authorAvatars: [ + "https://randomuser.me/api/portraits/men/32.jpg", + "https://randomuser.me/api/portraits/women/44.jpg", + ], + }, + { + id: "4", + name: "Book Lovers", + isPublic: false, + memberCount: 1500, + sessionsInfo: "2+ Sessions a month", + unreadNotifications: 10, + authorAvatars: [ + "https://randomuser.me/api/portraits/men/32.jpg", + "https://randomuser.me/api/portraits/women/44.jpg", + ], + }, +]; + +export default function DiscussionDashboard() { + const [selectedClub, setSelectedClub] = useState( + null + ); + const [modalOpen, setModalOpen] = useState(false); + + function openModal(club: ClubDetailsProps) { + setSelectedClub(club); + setModalOpen(true); + } + + function closeModal() { + setModalOpen(false); + setSelectedClub(null); + } + + function joinClub() { + if (selectedClub) alert(`Joined ${selectedClub.name}!`); + closeModal(); + } + + return ( +
+
+
+

Overview

+ +
+ + + {clubCards.map( + ({ + id, + name, + isPublic, + memberCount, + sessionsInfo, + authorAvatars, + unreadNotifications, + }) => ( + + ) + )} +
+ + +
+
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2747eb3..d932116 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -25,7 +25,6 @@ export default function RootLayout({ return ( - {children} diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..c271a1d --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,9 @@ +export type ClubDetailsProps = { + id: string; + name: string; + isPublic: boolean; + memberCount: number; + sessionsInfo: string; + unreadNotifications: number; + authorAvatars: string[]; +}; From 2ba89a318121745e43c3d2333aafb5959dfefd72 Mon Sep 17 00:00:00 2001 From: Zintarh Date: Thu, 3 Jul 2025 05:42:24 +0100 Subject: [PATCH 3/7] feat: create event --- .../discussions/components/ClubDetails.tsx | 237 +++++++----------- .../discussions/components/CreateEvent.tsx | 107 ++++++++ .../discussions/components/Modal.tsx | 46 ++++ src/app/dashboard/discussions/page.tsx | 11 +- 4 files changed, 248 insertions(+), 153 deletions(-) create mode 100644 src/app/dashboard/discussions/components/CreateEvent.tsx create mode 100644 src/app/dashboard/discussions/components/Modal.tsx diff --git a/src/app/dashboard/discussions/components/ClubDetails.tsx b/src/app/dashboard/discussions/components/ClubDetails.tsx index f2e651d..60f276c 100644 --- a/src/app/dashboard/discussions/components/ClubDetails.tsx +++ b/src/app/dashboard/discussions/components/ClubDetails.tsx @@ -1,181 +1,114 @@ import { ClubDetailsProps } from "@/lib/types"; import { Globe, PlusIcon } from "lucide-react"; -import React from "react"; +import Modal from "./Modal"; -type Props = { +type ClubModalProps = { club: ClubDetailsProps | null; isOpen: boolean; onClose: () => void; onJoin: () => void; }; -export default function ClubModal({ club, isOpen, onClose, onJoin }: Props) { - if (!isOpen || !club) return null; +export default function ClubModal({ + club, + isOpen, + onClose, + onJoin, +}: ClubModalProps) { + if (!club) return null; return ( -
-
-
- -
+ +
+
+
-
-
-
+
+
+ +
+ + +
+
+ ); +} + +function Badge({ text, icon }: { text: string; icon?: "globe" | "plus" }) { + const icons = { + globe: , + plus: , + }; -
- - -
-
+ return ( +
+ {icon && icons[icon]} + {text}
); } -function Badge({ text, icon }: { text: string; icon?: "users" | "calendar" }) { - let iconSvg = null; - if (icon === "users") { - iconSvg = ( - - - - ); - } else if (icon === "calendar") { - iconSvg = ( - - - - - ); - } +function SectionTitle({ title }: { title: string }) { + return ( +
+

{title}

+
+ ); +} + +function Tag({ text }: { text: string }) { return ( - - {iconSvg} - {text} + + {text} ); } diff --git a/src/app/dashboard/discussions/components/CreateEvent.tsx b/src/app/dashboard/discussions/components/CreateEvent.tsx new file mode 100644 index 0000000..b310183 --- /dev/null +++ b/src/app/dashboard/discussions/components/CreateEvent.tsx @@ -0,0 +1,107 @@ +"use client"; + +import React from "react"; +import Modal from "./Modal"; +import { Input } from "@/components/ui/input"; + +type Props = { + isOpen: boolean; + onClose: () => void; + onSubmit: () => void; +}; + +export default function CreateClubModal({ isOpen, onClose, onSubmit }: Props) { + const [name, setName] = React.useState(""); + const [description, setDescription] = React.useState(""); + const [genres, setGenres] = React.useState(["Fiction", "Drama"]); + const [clubType, setClubType] = React.useState(""); + + return ( + +
+

+ Create a new Club +

+

+ Launch your book club! Gather readers, share favorites, and spark + discussions +

+ +
+
+

Club Name

+ setName(e.target.value)} + className="mt-1 border border-gray-100 w-full py-4 border-b-0 px-2" + /> +
+ +
+ +
+ Genres +
+ {genres.map((genre) => ( + + {genre} × + + ))} +
+
+
+ +
+ +