mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-14 01:33:01 +08:00
[questions][feat] add useProtectedCallback hook (#472)
This commit is contained in:
@ -5,6 +5,7 @@ import { Fragment, useRef, useState } from 'react';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import { CheckIcon, HeartIcon } from '@heroicons/react/20/solid';
|
||||
|
||||
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
export type AddToListDropdownProps = {
|
||||
@ -85,14 +86,16 @@ export default function AddToListDropdown({
|
||||
});
|
||||
};
|
||||
|
||||
const handleMenuButtonClick = useProtectedCallback(() => {
|
||||
addClickOutsideListener();
|
||||
setMenuOpened(!menuOpened);
|
||||
});
|
||||
|
||||
const CustomMenuButton = ({ children }: PropsWithChildren<unknown>) => (
|
||||
<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"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
addClickOutsideListener();
|
||||
setMenuOpened(!menuOpened);
|
||||
}}>
|
||||
onClick={handleMenuButtonClick}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
@ -6,6 +6,8 @@ import {
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { TextInput } from '@tih/ui';
|
||||
|
||||
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
|
||||
|
||||
import ContributeQuestionDialog from './ContributeQuestionDialog';
|
||||
import type { ContributeQuestionFormProps } from './forms/ContributeQuestionForm';
|
||||
|
||||
@ -23,9 +25,9 @@ export default function ContributeQuestionCard({
|
||||
setShowDraftDialog(false);
|
||||
};
|
||||
|
||||
const handleOpenContribute = () => {
|
||||
const handleOpenContribute = useProtectedCallback(() => {
|
||||
setShowDraftDialog(true);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
|
@ -4,6 +4,8 @@ import type { Vote } from '@prisma/client';
|
||||
import type { ButtonSize } from '@tih/ui';
|
||||
import { Button } from '@tih/ui';
|
||||
|
||||
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
|
||||
|
||||
export type BackendVote = {
|
||||
id: string;
|
||||
vote: Vote;
|
||||
@ -31,6 +33,15 @@ export default function VotingButtons({
|
||||
vote?.vote === 'UPVOTE' ? 'secondary' : 'tertiary';
|
||||
const downvoteButtonVariant =
|
||||
vote?.vote === 'DOWNVOTE' ? 'secondary' : 'tertiary';
|
||||
|
||||
const handleUpvoteClick = useProtectedCallback(() => {
|
||||
onUpvote();
|
||||
});
|
||||
|
||||
const handleDownvoteClick = useProtectedCallback(() => {
|
||||
onDownvote();
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<Button
|
||||
@ -42,7 +53,7 @@ export default function VotingButtons({
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onUpvote();
|
||||
handleUpvoteClick();
|
||||
}}
|
||||
/>
|
||||
<p>{upvoteCount}</p>
|
||||
@ -55,7 +66,7 @@ export default function VotingButtons({
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onDownvote();
|
||||
handleDownvoteClick();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
import type { QuestionsQuestionType } from '@prisma/client';
|
||||
import { Button } from '@tih/ui';
|
||||
|
||||
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
|
||||
import { useQuestionVote } from '~/utils/questions/useVote';
|
||||
|
||||
import AddToListDropdown from '../../AddToListDropdown';
|
||||
@ -168,6 +169,10 @@ export default function BaseQuestionCard({
|
||||
return countryCount;
|
||||
}, [countries]);
|
||||
|
||||
const handleCreateEncounterClick = useProtectedCallback(() => {
|
||||
setShowReceivedForm(true);
|
||||
});
|
||||
|
||||
const cardContent = (
|
||||
<>
|
||||
{showVoteButtons && (
|
||||
@ -244,10 +249,7 @@ export default function BaseQuestionCard({
|
||||
label={createEncounterButtonText}
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
setShowReceivedForm(true);
|
||||
}}
|
||||
onClick={handleCreateEncounterClick}
|
||||
/>
|
||||
)}
|
||||
</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 AppShell from '~/components/global/AppShell';
|
||||
import ProtectedContextProvider from '~/components/questions/protected/ProtectedContextProvider';
|
||||
|
||||
import type { AppRouter } from '~/server/router';
|
||||
|
||||
@ -21,9 +22,11 @@ const MyApp: AppType<{ session: Session | null }> = ({
|
||||
return (
|
||||
<SessionProvider session={session}>
|
||||
<ToastsProvider>
|
||||
<AppShell>
|
||||
<Component {...pageProps} />
|
||||
</AppShell>
|
||||
<ProtectedContextProvider>
|
||||
<AppShell>
|
||||
<Component {...pageProps} />
|
||||
</AppShell>
|
||||
</ProtectedContextProvider>
|
||||
</ToastsProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
|
@ -13,6 +13,7 @@ import SortOptionsSelect from '~/components/questions/SortOptionsSelect';
|
||||
|
||||
import { APP_TITLE } from '~/utils/questions/constants';
|
||||
import { useFormRegister } from '~/utils/questions/useFormRegister';
|
||||
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
import { SortOrder, SortType } from '~/types/questions.d';
|
||||
@ -82,13 +83,15 @@ export default function QuestionPage() {
|
||||
},
|
||||
);
|
||||
|
||||
const handleSubmitComment = (data: AnswerCommentData) => {
|
||||
resetComment();
|
||||
addComment({
|
||||
answerId: answerId as string,
|
||||
content: data.commentContent,
|
||||
});
|
||||
};
|
||||
const handleSubmitComment = useProtectedCallback(
|
||||
(data: AnswerCommentData) => {
|
||||
resetComment();
|
||||
addComment({
|
||||
answerId: answerId as string,
|
||||
content: data.commentContent,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (!answer) {
|
||||
return <FullScreenSpinner />;
|
||||
|
@ -16,6 +16,7 @@ import { APP_TITLE } from '~/utils/questions/constants';
|
||||
import createSlug from '~/utils/questions/createSlug';
|
||||
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
|
||||
import { useFormRegister } from '~/utils/questions/useFormRegister';
|
||||
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
import { SortOrder, SortType } from '~/types/questions.d';
|
||||
@ -53,10 +54,11 @@ export default function QuestionPage() {
|
||||
|
||||
const {
|
||||
register: comRegister,
|
||||
handleSubmit: handleCommentSubmit,
|
||||
handleSubmit: handleCommentSubmitClick,
|
||||
reset: resetComment,
|
||||
formState: { isDirty: isCommentDirty, isValid: isCommentValid },
|
||||
} = useForm<QuestionCommentData>({ mode: 'onChange' });
|
||||
|
||||
const commentRegister = useFormRegister(comRegister);
|
||||
|
||||
const { questionId } = router.query;
|
||||
@ -149,21 +151,25 @@ export default function QuestionPage() {
|
||||
},
|
||||
);
|
||||
|
||||
const handleSubmitAnswer = (data: AnswerQuestionData) => {
|
||||
addAnswer({
|
||||
content: data.answerContent,
|
||||
questionId: questionId as string,
|
||||
});
|
||||
resetAnswer();
|
||||
};
|
||||
const handleSubmitAnswer = useProtectedCallback(
|
||||
(data: AnswerQuestionData) => {
|
||||
addAnswer({
|
||||
content: data.answerContent,
|
||||
questionId: questionId as string,
|
||||
});
|
||||
resetAnswer();
|
||||
},
|
||||
);
|
||||
|
||||
const handleSubmitComment = (data: QuestionCommentData) => {
|
||||
addComment({
|
||||
content: data.commentContent,
|
||||
questionId: questionId as string,
|
||||
});
|
||||
resetComment();
|
||||
};
|
||||
const handleSubmitComment = useProtectedCallback(
|
||||
(data: QuestionCommentData) => {
|
||||
addComment({
|
||||
content: data.commentContent,
|
||||
questionId: questionId as string,
|
||||
});
|
||||
resetComment();
|
||||
},
|
||||
);
|
||||
|
||||
if (!question) {
|
||||
return <FullScreenSpinner />;
|
||||
@ -219,7 +225,7 @@ export default function QuestionPage() {
|
||||
<div className="mt-4 px-4">
|
||||
<form
|
||||
className="mb-2"
|
||||
onSubmit={handleCommentSubmit(handleSubmitComment)}>
|
||||
onSubmit={handleCommentSubmitClick(handleSubmitComment)}>
|
||||
<TextArea
|
||||
{...commentRegister('commentContent', {
|
||||
minLength: 1,
|
||||
|
@ -16,6 +16,7 @@ import { Button } from '~/../../../packages/ui/dist';
|
||||
import { APP_TITLE } from '~/utils/questions/constants';
|
||||
import createSlug from '~/utils/questions/createSlug';
|
||||
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
|
||||
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
export default function ListPage() {
|
||||
@ -77,6 +78,10 @@ export default function ListPage() {
|
||||
setShowCreateListDialog(false);
|
||||
};
|
||||
|
||||
const handleAddClick = useProtectedCallback(() => {
|
||||
setShowCreateListDialog(true);
|
||||
});
|
||||
|
||||
const listOptions = (
|
||||
<>
|
||||
<ul className="flex flex-1 flex-col divide-y divide-solid divide-slate-200">
|
||||
@ -157,10 +162,10 @@ export default function ListPage() {
|
||||
label="Create"
|
||||
size="md"
|
||||
variant="tertiary"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setShowCreateListDialog(true);
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleAddClick();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -223,11 +228,13 @@ export default function ListPage() {
|
||||
onCancel={handleDeleteListCancel}
|
||||
onDelete={() => {
|
||||
handleDeleteList(listIdToDelete);
|
||||
}}></DeleteListDialog>
|
||||
}}
|
||||
/>
|
||||
<CreateListDialog
|
||||
show={showCreateListDialog}
|
||||
onCancel={handleCreateListCancel}
|
||||
onSubmit={handleCreateList}></CreateListDialog>
|
||||
onSubmit={handleCreateList}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
22
apps/portal/src/utils/questions/useProtectedCallback.ts
Normal file
22
apps/portal/src/utils/questions/useProtectedCallback.ts
Normal file
@ -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;
|
||||
};
|
Reference in New Issue
Block a user