[questions][feat] add useProtectedCallback hook ()

pull/475/head
Jeff Sieu 2 years ago committed by GitHub
parent ade6d1d88d
commit 538fa5ccf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -5,6 +5,7 @@ import { Fragment, useRef, useState } from 'react';
import { Menu, Transition } from '@headlessui/react'; import { Menu, Transition } from '@headlessui/react';
import { CheckIcon, HeartIcon } from '@heroicons/react/20/solid'; import { CheckIcon, HeartIcon } from '@heroicons/react/20/solid';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
export type AddToListDropdownProps = { export type AddToListDropdownProps = {
@ -85,14 +86,16 @@ export default function AddToListDropdown({
}); });
}; };
const handleMenuButtonClick = useProtectedCallback(() => {
addClickOutsideListener();
setMenuOpened(!menuOpened);
});
const CustomMenuButton = ({ children }: PropsWithChildren<unknown>) => ( const CustomMenuButton = ({ children }: PropsWithChildren<unknown>) => (
<button <button
className="focus:ring-primary-500 inline-flex w-full justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-100" className="focus:ring-primary-500 inline-flex w-full justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-100"
type="button" type="button"
onClick={() => { onClick={handleMenuButtonClick}>
addClickOutsideListener();
setMenuOpened(!menuOpened);
}}>
{children} {children}
</button> </button>
); );

@ -6,6 +6,8 @@ import {
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { TextInput } from '@tih/ui'; import { TextInput } from '@tih/ui';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
import ContributeQuestionDialog from './ContributeQuestionDialog'; import ContributeQuestionDialog from './ContributeQuestionDialog';
import type { ContributeQuestionFormProps } from './forms/ContributeQuestionForm'; import type { ContributeQuestionFormProps } from './forms/ContributeQuestionForm';
@ -23,9 +25,9 @@ export default function ContributeQuestionCard({
setShowDraftDialog(false); setShowDraftDialog(false);
}; };
const handleOpenContribute = () => { const handleOpenContribute = useProtectedCallback(() => {
setShowDraftDialog(true); setShowDraftDialog(true);
}; });
return ( return (
<div className="w-full"> <div className="w-full">

@ -4,6 +4,8 @@ import type { Vote } from '@prisma/client';
import type { ButtonSize } from '@tih/ui'; import type { ButtonSize } from '@tih/ui';
import { Button } from '@tih/ui'; import { Button } from '@tih/ui';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
export type BackendVote = { export type BackendVote = {
id: string; id: string;
vote: Vote; vote: Vote;
@ -31,6 +33,15 @@ export default function VotingButtons({
vote?.vote === 'UPVOTE' ? 'secondary' : 'tertiary'; vote?.vote === 'UPVOTE' ? 'secondary' : 'tertiary';
const downvoteButtonVariant = const downvoteButtonVariant =
vote?.vote === 'DOWNVOTE' ? 'secondary' : 'tertiary'; vote?.vote === 'DOWNVOTE' ? 'secondary' : 'tertiary';
const handleUpvoteClick = useProtectedCallback(() => {
onUpvote();
});
const handleDownvoteClick = useProtectedCallback(() => {
onDownvote();
});
return ( return (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<Button <Button
@ -42,7 +53,7 @@ export default function VotingButtons({
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
onUpvote(); handleUpvoteClick();
}} }}
/> />
<p>{upvoteCount}</p> <p>{upvoteCount}</p>
@ -55,7 +66,7 @@ export default function VotingButtons({
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
onDownvote(); handleDownvoteClick();
}} }}
/> />
</div> </div>

@ -9,6 +9,7 @@ import {
import type { QuestionsQuestionType } from '@prisma/client'; import type { QuestionsQuestionType } from '@prisma/client';
import { Button } from '@tih/ui'; import { Button } from '@tih/ui';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
import { useQuestionVote } from '~/utils/questions/useVote'; import { useQuestionVote } from '~/utils/questions/useVote';
import AddToListDropdown from '../../AddToListDropdown'; import AddToListDropdown from '../../AddToListDropdown';
@ -168,6 +169,10 @@ export default function BaseQuestionCard({
return countryCount; return countryCount;
}, [countries]); }, [countries]);
const handleCreateEncounterClick = useProtectedCallback(() => {
setShowReceivedForm(true);
});
const cardContent = ( const cardContent = (
<> <>
{showVoteButtons && ( {showVoteButtons && (
@ -244,10 +249,7 @@ export default function BaseQuestionCard({
label={createEncounterButtonText} label={createEncounterButtonText}
size="sm" size="sm"
variant="tertiary" variant="tertiary"
onClick={(event) => { onClick={handleCreateEncounterClick}
event.preventDefault();
setShowReceivedForm(true);
}}
/> />
)} )}
</div> </div>

@ -0,0 +1,40 @@
import type { PropsWithChildren } from 'react';
import { createContext, useState } from 'react';
import ProtectedDialog from './ProtectedDialog';
export type ProtectedContextData = {
showDialog: () => void;
};
export const ProtectedContext = createContext<ProtectedContextData>({
// eslint-disable-next-line @typescript-eslint/no-empty-function
showDialog: () => {},
});
export type ProtectedContextProviderProps = PropsWithChildren<
Record<string, unknown>
>;
export default function ProtectedContextProvider({
children,
}: ProtectedContextProviderProps) {
const [show, setShow] = useState(false);
return (
<ProtectedContext.Provider
value={{
showDialog: () => {
setShow(true);
},
}}>
{children}
<ProtectedDialog
show={show}
onClose={() => {
setShow(false);
}}
/>
</ProtectedContext.Provider>
);
}

@ -0,0 +1,36 @@
import { signIn } from 'next-auth/react';
import { Button, Dialog } from '@tih/ui';
export type ProtectedDialogProps = {
onClose: () => void;
show: boolean;
};
export default function ProtectedDialog({
show,
onClose,
}: ProtectedDialogProps) {
const handlePrimaryClick = () => {
signIn();
onClose();
};
return (
<Dialog
isShown={show}
primaryButton={
<Button
label="Sign in"
variant="primary"
onClick={handlePrimaryClick}
/>
}
secondaryButton={
<Button label="Cancel" variant="tertiary" onClick={onClose} />
}
title="Sign in to continue"
onClose={onClose}>
<p>This action requires you to be signed in.</p>
</Dialog>
);
}

@ -9,6 +9,7 @@ import { loggerLink } from '@trpc/client/links/loggerLink';
import { withTRPC } from '@trpc/next'; import { withTRPC } from '@trpc/next';
import AppShell from '~/components/global/AppShell'; import AppShell from '~/components/global/AppShell';
import ProtectedContextProvider from '~/components/questions/protected/ProtectedContextProvider';
import type { AppRouter } from '~/server/router'; import type { AppRouter } from '~/server/router';
@ -21,9 +22,11 @@ const MyApp: AppType<{ session: Session | null }> = ({
return ( return (
<SessionProvider session={session}> <SessionProvider session={session}>
<ToastsProvider> <ToastsProvider>
<AppShell> <ProtectedContextProvider>
<Component {...pageProps} /> <AppShell>
</AppShell> <Component {...pageProps} />
</AppShell>
</ProtectedContextProvider>
</ToastsProvider> </ToastsProvider>
</SessionProvider> </SessionProvider>
); );

@ -13,6 +13,7 @@ import SortOptionsSelect from '~/components/questions/SortOptionsSelect';
import { APP_TITLE } from '~/utils/questions/constants'; import { APP_TITLE } from '~/utils/questions/constants';
import { useFormRegister } from '~/utils/questions/useFormRegister'; import { useFormRegister } from '~/utils/questions/useFormRegister';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
import { SortOrder, SortType } from '~/types/questions.d'; import { SortOrder, SortType } from '~/types/questions.d';
@ -82,13 +83,15 @@ export default function QuestionPage() {
}, },
); );
const handleSubmitComment = (data: AnswerCommentData) => { const handleSubmitComment = useProtectedCallback(
resetComment(); (data: AnswerCommentData) => {
addComment({ resetComment();
answerId: answerId as string, addComment({
content: data.commentContent, answerId: answerId as string,
}); content: data.commentContent,
}; });
},
);
if (!answer) { if (!answer) {
return <FullScreenSpinner />; return <FullScreenSpinner />;

@ -16,6 +16,7 @@ import { APP_TITLE } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug'; import createSlug from '~/utils/questions/createSlug';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates'; import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import { useFormRegister } from '~/utils/questions/useFormRegister'; import { useFormRegister } from '~/utils/questions/useFormRegister';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
import { SortOrder, SortType } from '~/types/questions.d'; import { SortOrder, SortType } from '~/types/questions.d';
@ -53,10 +54,11 @@ export default function QuestionPage() {
const { const {
register: comRegister, register: comRegister,
handleSubmit: handleCommentSubmit, handleSubmit: handleCommentSubmitClick,
reset: resetComment, reset: resetComment,
formState: { isDirty: isCommentDirty, isValid: isCommentValid }, formState: { isDirty: isCommentDirty, isValid: isCommentValid },
} = useForm<QuestionCommentData>({ mode: 'onChange' }); } = useForm<QuestionCommentData>({ mode: 'onChange' });
const commentRegister = useFormRegister(comRegister); const commentRegister = useFormRegister(comRegister);
const { questionId } = router.query; const { questionId } = router.query;
@ -149,21 +151,25 @@ export default function QuestionPage() {
}, },
); );
const handleSubmitAnswer = (data: AnswerQuestionData) => { const handleSubmitAnswer = useProtectedCallback(
addAnswer({ (data: AnswerQuestionData) => {
content: data.answerContent, addAnswer({
questionId: questionId as string, content: data.answerContent,
}); questionId: questionId as string,
resetAnswer(); });
}; resetAnswer();
},
);
const handleSubmitComment = (data: QuestionCommentData) => { const handleSubmitComment = useProtectedCallback(
addComment({ (data: QuestionCommentData) => {
content: data.commentContent, addComment({
questionId: questionId as string, content: data.commentContent,
}); questionId: questionId as string,
resetComment(); });
}; resetComment();
},
);
if (!question) { if (!question) {
return <FullScreenSpinner />; return <FullScreenSpinner />;
@ -219,7 +225,7 @@ export default function QuestionPage() {
<div className="mt-4 px-4"> <div className="mt-4 px-4">
<form <form
className="mb-2" className="mb-2"
onSubmit={handleCommentSubmit(handleSubmitComment)}> onSubmit={handleCommentSubmitClick(handleSubmitComment)}>
<TextArea <TextArea
{...commentRegister('commentContent', { {...commentRegister('commentContent', {
minLength: 1, minLength: 1,

@ -16,6 +16,7 @@ import { Button } from '~/../../../packages/ui/dist';
import { APP_TITLE } from '~/utils/questions/constants'; import { APP_TITLE } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug'; import createSlug from '~/utils/questions/createSlug';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates'; import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
export default function ListPage() { export default function ListPage() {
@ -77,6 +78,10 @@ export default function ListPage() {
setShowCreateListDialog(false); setShowCreateListDialog(false);
}; };
const handleAddClick = useProtectedCallback(() => {
setShowCreateListDialog(true);
});
const listOptions = ( const listOptions = (
<> <>
<ul className="flex flex-1 flex-col divide-y divide-solid divide-slate-200"> <ul className="flex flex-1 flex-col divide-y divide-solid divide-slate-200">
@ -157,10 +162,10 @@ export default function ListPage() {
label="Create" label="Create"
size="md" size="md"
variant="tertiary" variant="tertiary"
onClick={(e) => { onClick={(event) => {
e.preventDefault(); event.preventDefault();
e.stopPropagation(); event.stopPropagation();
setShowCreateListDialog(true); handleAddClick();
}} }}
/> />
</div> </div>
@ -223,11 +228,13 @@ export default function ListPage() {
onCancel={handleDeleteListCancel} onCancel={handleDeleteListCancel}
onDelete={() => { onDelete={() => {
handleDeleteList(listIdToDelete); handleDeleteList(listIdToDelete);
}}></DeleteListDialog> }}
/>
<CreateListDialog <CreateListDialog
show={showCreateListDialog} show={showCreateListDialog}
onCancel={handleCreateListCancel} onCancel={handleCreateListCancel}
onSubmit={handleCreateList}></CreateListDialog> onSubmit={handleCreateList}
/>
</section> </section>
</div> </div>
</main> </main>

@ -0,0 +1,22 @@
import { useSession } from 'next-auth/react';
import { useCallback, useContext } from 'react';
import { ProtectedContext } from '~/components/questions/protected/ProtectedContextProvider';
export const useProtectedCallback = <T extends Array<unknown>, U>(
callback: (...args: T) => U,
) => {
const { showDialog } = useContext(ProtectedContext);
const { status } = useSession();
const protectedCallback = useCallback(
(...args: T) => {
if (status === 'authenticated') {
return callback(...args);
}
showDialog();
},
[callback, showDialog, status],
);
return protectedCallback;
};
Loading…
Cancel
Save