mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-15 02:33:50 +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 { 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>
|
||||||
|
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