diff --git a/app/providers.tsx b/app/providers.tsx index 5232990..dfceab7 100644 --- a/app/providers.tsx +++ b/app/providers.tsx @@ -3,8 +3,8 @@ import { theme } from "@/chakra/theme"; import Layout from "@/components/layout/Layout"; import { ColorModeProvider } from "@/components/ui/color-mode"; -import { toaster } from "@/hooks/useCustomToast"; -import { ChakraProvider, Toaster } from "@chakra-ui/react"; +import { Toaster } from "@/components/ui/toaster"; +import { ChakraProvider } from "@chakra-ui/react"; import { Provider as JotaiProvider } from "jotai"; import { useEffect, useState } from "react"; import EmotionRegistry from "./emotion-registry"; @@ -29,46 +29,7 @@ export function Providers({ children }: { children: React.ReactNode }) { {children} - {mounted && ( - // @ts-ignore - - {(toast: any) => { - const type = toast.type || "info"; - const colors: any = { - success: "green", - error: "red", - warning: "orange", - info: "blue", - }; - const colorScheme = colors[type]; - - return ( -
-
- {toast.title} -
- {toast.description && ( -
- {toast.description} -
- )} -
- ); - }} -
- )} + {mounted && } diff --git a/components/community/community-header/CommunityHeader.tsx b/components/community/community-header/CommunityHeader.tsx index 0764027..49edeff 100644 --- a/components/community/community-header/CommunityHeader.tsx +++ b/components/community/community-header/CommunityHeader.tsx @@ -1,6 +1,6 @@ import { Community } from "@/types/community"; import { Box, Flex } from "@chakra-ui/react"; -import React from "react"; +import React, { useState } from "react"; import useCommunityState from "@/hooks/community/useCommunityState"; import useCommunityMembershipActions from "@/hooks/community/useCommunityMembershipActions"; import CommunityIcon from "./CommunityIcon"; @@ -8,6 +8,7 @@ import CommunityName from "./CommunityName"; import JoinOrLeaveButton from "./JoinOrLeaveButton"; import CommunitySettings from "./CommunitySettings"; import CommunityMembersButton from "./CommunityMembersButton"; +import ConfirmationDialog from "@/components/modal/ConfirmationDialog"; /** * @param {communityData} - data required to be displayed @@ -36,6 +37,21 @@ const CommunityHeader: React.FC = ({ communityData }) => { const isJoined = !!communityStateValue.mySnippets.find( (item) => item.communityId === communityData.id ); + const [leaveConfirmationOpen, setLeaveConfirmationOpen] = useState(false); + + const handleJoinOrLeave = () => { + if (isJoined) { + setLeaveConfirmationOpen(true); + } else { + onJoinOrLeaveCommunity(communityData, isJoined); + } + }; + + const onConfirmLeave = async () => { + await onJoinOrLeaveCommunity(communityData, isJoined); + setLeaveConfirmationOpen(false); + }; + return ( @@ -66,12 +82,21 @@ const CommunityHeader: React.FC = ({ communityData }) => { onJoinOrLeaveCommunity(communityData, isJoined)} + onClick={handleJoinOrLeave} /> + setLeaveConfirmationOpen(false)} + onConfirm={onConfirmLeave} + title="Unsubscribe from Community" + body={`Are you sure you want to unsubscribe from r/${communityData.id}?`} + confirmButtonText="Unsubscribe" + isLoading={loading} + /> ); }; diff --git a/components/modal/ConfirmationDialog.tsx b/components/modal/ConfirmationDialog.tsx new file mode 100644 index 0000000..498b219 --- /dev/null +++ b/components/modal/ConfirmationDialog.tsx @@ -0,0 +1,89 @@ +import { + Button, + DialogActionTrigger, + DialogBackdrop, + DialogBody, + DialogCloseTrigger, + DialogContent, + DialogFooter, + DialogHeader, + DialogPositioner, + DialogRoot, + DialogTitle, + Portal, +} from "@chakra-ui/react"; +import React from "react"; + +interface ConfirmationDialogProps { + open: boolean; + onClose: () => void; + onConfirm: () => void; + title: string; + body: string; + confirmButtonText?: string; + cancelButtonText?: string; + isLoading?: boolean; +} + +const ConfirmationDialog: React.FC = ({ + open, + onClose, + onConfirm, + title, + body, + confirmButtonText = "Confirm", + cancelButtonText = "Cancel", + isLoading = false, +}) => { + return ( + !details.open && onClose()} + placement="center" + > + + + + + + {title} + + {body} + + + + + + + { + e.stopPropagation(); + onClose(); + }} + /> + + + + + ); +}; + +export default ConfirmationDialog; diff --git a/components/modal/auth/Login.tsx b/components/modal/auth/Login.tsx index 5282486..0c93424 100644 --- a/components/modal/auth/Login.tsx +++ b/components/modal/auth/Login.tsx @@ -6,6 +6,7 @@ import { authModalStateAtom } from "../../../atoms/authModalAtom"; import { auth } from "../../../firebase/clientApp"; import { FIREBASE_ERRORS } from "../../../firebase/errors"; import InputField from "./InputField"; +import { PasswordInput } from "@/components/ui/password-input"; type LoginProps = {}; @@ -78,11 +79,27 @@ const Login: React.FC = () => { onChange={onChange} /> - { onChange={onChange} /> - - {/* If there is error than the error is shown */} diff --git a/components/modal/community-members/CommunityMembersModal.tsx b/components/modal/community-members/CommunityMembersModal.tsx index 5fbe8c9..80e0a15 100644 --- a/components/modal/community-members/CommunityMembersModal.tsx +++ b/components/modal/community-members/CommunityMembersModal.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { Box, DialogBackdrop, @@ -25,6 +25,7 @@ import { useAtomValue } from "jotai"; import { communityStateAtom } from "@/atoms/communitiesAtom"; import useCommunityPermissions from "@/hooks/community/useCommunityPermissions"; import useRemoveCommunityMember from "@/hooks/community/useRemoveCommunityMember"; +import ConfirmationDialog from "@/components/modal/ConfirmationDialog"; type CommunityMembersModalProps = { isOpen: boolean; @@ -43,6 +44,7 @@ const CommunityMembersModal: React.FC = ({ communityStateValue.currentCommunity ); const { removeMember, loading: removeLoading } = useRemoveCommunityMember(); + const [memberToRemove, setMemberToRemove] = useState(null); useEffect(() => { if (!isOpen) return; @@ -56,6 +58,12 @@ const CommunityMembersModal: React.FC = ({ } }; + const confirmRemoveMember = async () => { + if (!memberToRemove) return; + await handleRemoveMember(memberToRemove); + setMemberToRemove(null); + }; + const renderContent = () => { if (loading) { return ( @@ -98,12 +106,10 @@ const CommunityMembersModal: React.FC = ({ {isAdmin && ( handleRemoveMember(member.uid)} - disabled={removeLoading} - aria-label="Remove member" + colorPalette="red" size="sm" + aria-label="Remove member" + onClick={() => setMemberToRemove(member.uid)} > @@ -117,24 +123,28 @@ const CommunityMembersModal: React.FC = ({ return ( { - if (!open) onClose(); - }} + onOpenChange={(details: { open: boolean }) => !details.open && onClose()} + placement="center" > - - - - - - {`${members.length} Subscribers`} - - - - {renderContent()} - - - - + + + + + Community Members + + + {renderContent()} + + + setMemberToRemove(null)} + onConfirm={confirmRemoveMember} + title="Remove Member" + body="Are you sure you want to remove this member from the community?" + confirmButtonText="Remove" + isLoading={removeLoading} + /> ); }; diff --git a/components/modal/community-settings/AdminManager.tsx b/components/modal/community-settings/AdminManager.tsx index 0906166..543b8bd 100644 --- a/components/modal/community-settings/AdminManager.tsx +++ b/components/modal/community-settings/AdminManager.tsx @@ -17,6 +17,7 @@ import { import React, { useEffect, useState } from "react"; import { useAuthState } from "react-firebase-hooks/auth"; import { AdminUser } from "@/types/adminUser"; +import ConfirmationDialog from "@/components/modal/ConfirmationDialog"; type AdminManagerProps = { communityData: Community; @@ -33,6 +34,8 @@ const AdminManager: React.FC = ({ communityData }) => { const [showResults, setShowResults] = useState(false); const showToast = useCustomToast(); const [user] = useAuthState(auth); + const [adminToRemove, setAdminToRemove] = useState(null); + const [removingAdmin, setRemovingAdmin] = useState(false); useEffect(() => { loadAdmins(communityData.creatorId, communityData.adminIds).catch( @@ -101,10 +104,12 @@ const AdminManager: React.FC = ({ communityData }) => { } }; - const onRemoveAdmin = async (uid: string) => { + const confirmRemoveAdmin = async () => { + if (!adminToRemove) return; + setRemovingAdmin(true); try { // Remove admin (updates Firestore + local + global state) - await handleRemoveAdmin(communityData.id, uid, setAdmins); + await handleRemoveAdmin(communityData.id, adminToRemove, setAdmins); showToast({ title: "Admin removed", @@ -118,6 +123,9 @@ const AdminManager: React.FC = ({ communityData }) => { description: "Could not remove admin", status: "error", }); + } finally { + setRemovingAdmin(false); + setAdminToRemove(null); } }; @@ -239,7 +247,7 @@ const AdminManager: React.FC = ({ communityData }) => { size="sm" variant="outline" colorPalette="red" - onClick={() => onRemoveAdmin(admin.uid)} + onClick={() => setAdminToRemove(admin.uid)} > Remove @@ -253,6 +261,15 @@ const AdminManager: React.FC = ({ communityData }) => { ))} )} + setAdminToRemove(null)} + onConfirm={confirmRemoveAdmin} + title="Remove Admin" + body="Are you sure you want to remove this user from admins?" + confirmButtonText="Remove" + isLoading={removingAdmin} + /> ); }; diff --git a/components/modal/community-settings/CommunitySettings.tsx b/components/modal/community-settings/CommunitySettings.tsx index aebc1a7..f6ee9d3 100644 --- a/components/modal/community-settings/CommunitySettings.tsx +++ b/components/modal/community-settings/CommunitySettings.tsx @@ -15,8 +15,7 @@ import { DialogRoot, DialogTitle, Portal, - Separator, - Stack, + Tabs, } from "@chakra-ui/react"; import { useAtom } from "jotai"; import React, { useRef, useState } from "react"; @@ -114,37 +113,48 @@ const CommunitySettingsModal: React.FC = ({ flexDirection="column" padding="10px 0px" > - - - - - - - - - + + + Profile + Privacy + Admins + Danger Zone + + + + + + + + + + + + + + diff --git a/components/modal/community-settings/DangerZone.tsx b/components/modal/community-settings/DangerZone.tsx index deb3ddf..68d7e34 100644 --- a/components/modal/community-settings/DangerZone.tsx +++ b/components/modal/community-settings/DangerZone.tsx @@ -1,5 +1,6 @@ import { Button, Stack, Text } from "@chakra-ui/react"; -import React from "react"; +import React, { useState } from "react"; +import ConfirmationDialog from "@/components/modal/ConfirmationDialog"; type DangerZoneProps = { deleteCommunity: () => Promise; @@ -13,6 +14,8 @@ const DangerZone: React.FC = ({ deleteCommunity, loading, }) => { + const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false); + return ( @@ -23,13 +26,25 @@ const DangerZone: React.FC = ({ + setDeleteConfirmationOpen(false)} + onConfirm={() => { + deleteCommunity(); + setDeleteConfirmationOpen(false); + }} + title="Delete Community" + body="Are you sure you want to delete this community? This action cannot be undone and will delete all posts and comments." + confirmButtonText="Delete Community" + isLoading={loading} + /> ); }; diff --git a/components/modal/community-settings/PrivacySettings.tsx b/components/modal/community-settings/PrivacySettings.tsx index f5497d8..dfc79ae 100644 --- a/components/modal/community-settings/PrivacySettings.tsx +++ b/components/modal/community-settings/PrivacySettings.tsx @@ -52,7 +52,7 @@ const PrivacySettings: React.FC = ({ p={3} border="1px solid" borderColor={isSelected ? "red.500" : "gray.200"} - borderRadius="md" + borderRadius="xl" cursor="pointer" onClick={() => handlePrivacyTypeChange({ value: type.value })} bg={ diff --git a/components/modal/create-community/CommunityTypeOption.tsx b/components/modal/create-community/CommunityTypeOption.tsx index a173a3e..d3003be 100644 --- a/components/modal/create-community/CommunityTypeOption.tsx +++ b/components/modal/create-community/CommunityTypeOption.tsx @@ -1,11 +1,5 @@ import React, { FC } from "react"; -import { Icon, Text, Flex } from "@chakra-ui/react"; -import { - CheckboxControl, - CheckboxIndicator, - CheckboxLabel, - CheckboxRoot, -} from "@chakra-ui/react"; +import { CheckboxCard, Icon, Flex, VStack } from "@chakra-ui/react"; import type { IconType } from "react-icons"; type CommunityTypeOptionProps = { @@ -26,40 +20,34 @@ const CommunityTypeOption: FC = ({ onChange, }) => { return ( - onChange(name)} colorPalette="red" - display="flex" - alignItems="center" - gap={2} + borderRadius="xl" cursor="pointer" - py={1} > - - - - - - - - {label} - - - {description} - - - - + + + + + + + {label} + + {description} + + + + + + + ); }; diff --git a/components/modal/create-community/CommunityTypeOptions.tsx b/components/modal/create-community/CommunityTypeOptions.tsx index 6657021..120cf20 100644 --- a/components/modal/create-community/CommunityTypeOptions.tsx +++ b/components/modal/create-community/CommunityTypeOptions.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { VStack } from "@chakra-ui/react"; import CommunityTypeOption from "./CommunityTypeOption"; interface CommunityTypeOptionsProps { @@ -18,7 +19,8 @@ const CommunityTypeOptions: React.FC = ({ onCommunityTypeChange, }) => { return ( -
+ // add top margin to increase gap between the title and options + {options.map((option) => ( = ({ onChange={onCommunityTypeChange} /> ))} -
+ ); }; diff --git a/components/posts/comments/CommentItem.tsx b/components/posts/comments/CommentItem.tsx index 0210656..37dbb0b 100644 --- a/components/posts/comments/CommentItem.tsx +++ b/components/posts/comments/CommentItem.tsx @@ -13,6 +13,7 @@ import React, { useState } from "react"; import { CgProfile } from "react-icons/cg"; import { LuPencil, LuReply, LuTrash } from "react-icons/lu"; import CommentInput from "./CommentInput"; +import ConfirmationDialog from "@/components/modal/ConfirmationDialog"; /** * Required props for CommentItem component @@ -30,7 +31,8 @@ type CommentItemProps = { onCreateComment: ( user: User, text: string, - parentId: string + parentId: string, + depth: number ) => Promise; user?: User; }; @@ -62,11 +64,17 @@ const CommentItem: React.FC = ({ const [isReplying, setIsReplying] = useState(false); const [replyText, setReplyText] = useState(""); const [replyLoading, setReplyLoading] = useState(false); + const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false); const handleReply = async () => { if (!user) return; setReplyLoading(true); - await onCreateComment(user, replyText, comment.id); + await onCreateComment( + user, + replyText, + comment.id, + (comment.depth || 0) + 1 + ); setReplyLoading(false); setReplyText(""); setIsReplying(false); @@ -103,21 +111,23 @@ const CommentItem: React.FC = ({ borderColor={{ base: "gray.100", _dark: "gray.600" }} > - + {(comment.depth || 0) < 2 && ( + + )} {userId === comment.creatorId && (