Skip to content

Commit

Permalink
feat(app): add ability to edit question on questions page and single …
Browse files Browse the repository at this point in the history
…question page for admin (#502)

* feat(app): add ability to edit questions on questions list to admin

* feat(app): add ability to edit question on question page to admin

* fix(app): refetch questions after edit one of them in user panel

* fix(app): fix text align in "CtaHedaerActiveLink"

* Remove unused import

* chore: add prettier config file to worspace config

* fix: update questions list after edit any of them in user dashboard

* fix: add loading indicator to questions list in dashboards

* fix: disable ssr for QuestionsManagement on questions pages

* fix: small improvements in "EditAnswer" and "AnswerForm"

* fix: remove unused import

* fix: refresh ssr generated page after edit question

* fix: remove unused import

* fix: fix vercel deployment

* fix: rename function name "customOnClose" => "additionalActionOnClose"

* feat: change loading indicator component

* fix: improve "Loading" component styles

* fix: improve loader a11y

* fix: improve styles of "Loading" component

* fix: improve light theme of loading component

* Update apps/app/src/components/Loading.tsx

* implement "additionalActionOnClose" function in dashboard modals

* improve loading component and implement this changes
  • Loading branch information
grzegorzpokorski authored Jan 24, 2023
1 parent 41778f1 commit 7c98b84
Show file tree
Hide file tree
Showing 17 changed files with 221 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Loading } from "../../../../../components/Loading";

export default function LoadingQuestions() {
return <Loading label="ładowanie pytań" />;
return <Loading label="ładowanie pytań" type="article" withTechnology={false} withFilters />;
}
13 changes: 10 additions & 3 deletions apps/app/src/components/AddQuestionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const AddQuestionModal = (props: ComponentProps<typeof BaseModal>) => {
onSuccess: () => {
setSelectData({});
setContent("");
modalData ? closeModal() : openModal("AddQuestionConfirmationModal");
modalData ? handleCloseModal() : openModal("AddQuestionConfirmationModal");
},
onError: () => setIsError(true),
};
Expand All @@ -78,8 +78,15 @@ export const AddQuestionModal = (props: ComponentProps<typeof BaseModal>) => {
}
};

const handleCloseModal = () => {
closeModal();
if (modalData?.additionalActionOnClose) {
modalData.additionalActionOnClose();
}
};

return (
<BaseModal {...props}>
<BaseModal {...props} onClose={handleCloseModal}>
<BaseModal.Title modalId={props.modalId}>
{modalData ? "Edytuj" : "Nowe"} pytanie
</BaseModal.Title>
Expand Down Expand Up @@ -135,7 +142,7 @@ export const AddQuestionModal = (props: ComponentProps<typeof BaseModal>) => {
<Button
type="button"
variant="branding"
onClick={closeModal}
onClick={handleCloseModal}
className="dark:focus:shadow-white"
>
Anuluj
Expand Down
3 changes: 2 additions & 1 deletion apps/app/src/components/AdminPanel/AdminPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Level } from "../../lib/level";
import { QuestionStatus } from "../../lib/question";
import { Technology } from "../../lib/technologies";
import { FilterableQuestionsList } from "../FilterableQuestionsList/FilterableQuestionsList";
import { Loading } from "../Loading";
import { AdminPanelQuestionsList } from "./AdminPanelQuestionsList";

type AdminPanelProps = Readonly<{
Expand Down Expand Up @@ -35,7 +36,7 @@ export const AdminPanel = ({ page, technology, levels, status }: AdminPanelProps
data={{ status, technology, levels }}
>
{isSuccess && data.data.data.length > 0 ? (
<Suspense>
<Suspense fallback={<Loading label="ładowanie pytań" type="article" admin />}>
<AdminPanelQuestionsList questions={data.data.data} refetchQuestions={refetchQuestions} />
</Suspense>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@ export const AdminPanelQuestionLeftSection = ({
const { openModal, openedModal } = useUIContext();
const { id, _statusId: status } = question;

useEffect(() => {
if (!openedModal) {
refetchQuestions();
}
}, [openedModal, refetchQuestions]);

const handleAcceptQuestionClick = () => {
patchQuestionMutation.mutate(
{ id, status: "accepted" },
Expand All @@ -48,7 +42,7 @@ export const AdminPanelQuestionLeftSection = ({
};

const handleEditQuestionClick = () => {
openModal("AddQuestionModal", question);
openModal("AddQuestionModal", { ...question, additionalActionOnClose: refetchQuestions });
};

return (
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/components/CtaHeader/CtaHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type CtaHeaderActiveLinkProps = Readonly<{
const CtaHeaderActiveLink = (props: CtaHeaderActiveLinkProps) => (
<ActiveLink
activeClassName="font-bold border-white"
className="flex h-12 grow items-center justify-center border-b-2 border-transparent px-7 transition-opacity hover:opacity-80"
className="flex h-12 grow items-center justify-center border-b-2 border-transparent px-7 text-center transition-opacity hover:opacity-80"
{...props}
/>
);
Expand Down
84 changes: 81 additions & 3 deletions apps/app/src/components/Loading.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,88 @@
import { twMerge } from "tailwind-merge";

export const Loading = ({ label, className }: { label: string; className?: string }) => {
type LoaderType = "spinner" | "article";

export const Loading = ({
label,
type = "spinner",
withTechnology = false,
withFilters = false,
admin = false,
className,
}: {
label: string;
withTechnology?: boolean;
type?: LoaderType;
withFilters?: boolean;
className?: string;
admin?: boolean;
}) => {
if (type === "article") {
return (
<div role="status" aria-label={label} className="flex w-full flex-col gap-10">
{withFilters && (
<div className="flex animate-pulse flex-wrap items-baseline justify-between gap-3">
<span className="h-8 w-28 bg-white shadow-md dark:bg-neutral-700 md:gap-3"></span>
<div className="flex flex-wrap items-baseline gap-1.5 md:gap-3">
<span className="h-8 w-28 bg-white shadow-md dark:bg-neutral-700 md:gap-3"></span>
<span className="h-8 w-52 bg-white shadow-md dark:bg-neutral-700 md:gap-3"></span>
</div>
</div>
)}
<ul className="flex w-full list-none flex-col gap-10">
{[...Array(10).keys()].map((i) => (
<li key={i}>
<article
className={twMerge(
`flex animate-pulse bg-white p-3 text-sm text-neutral-500 shadow-md dark:bg-white-dark dark:text-neutral-200 md:p-5`,
)}
style={{ animationDelay: `${i * 0.25}s` }}
>
<div className="mr-3 flex flex-col justify-start gap-1.5">
{admin ? (
<span className="block h-8 w-20 rounded-md bg-neutral-100 dark:bg-neutral-700"></span>
) : (
<span className="block h-7 w-8 rounded-md bg-neutral-100 dark:bg-neutral-700"></span>
)}
</div>
<div className="question-content flex max-w-full grow flex-col gap-1.5 px-2">
<span className="block h-3.5 w-full bg-neutral-100 dark:bg-neutral-700"></span>
<span className="block h-3.5 w-full bg-neutral-100 dark:bg-neutral-700"></span>
<span className="block h-3.5 w-full bg-neutral-100 dark:bg-neutral-700"></span>
<span className="block h-3.5 w-full bg-neutral-100 dark:bg-neutral-700"></span>
<span className="block h-3.5 w-full bg-neutral-100 dark:bg-neutral-700"></span>
<span className="block h-3.5 w-[80%] bg-neutral-100 dark:bg-neutral-700"></span>
</div>
<div className="flex min-w-max flex-col items-center md:ml-4 md:items-end">
<div className="flex flex-col items-center">
{withTechnology && (
<div className="mb-3 flex flex-col items-center self-center">
<span className="mb-1 h-3.5 w-10 bg-neutral-100 dark:bg-neutral-700"></span>
<div className="h-8 w-8 rounded-full bg-neutral-100 dark:bg-neutral-700"></div>
</div>
)}
<span className="h-8 min-w-[64px] rounded-full bg-neutral-100 dark:bg-neutral-700 md:min-w-[80px]"></span>
</div>
<div className="mt-3">
<div className="block h-3 w-12 bg-neutral-100 dark:bg-neutral-700 md:w-20"></div>
</div>
</div>
</article>
</li>
))}
</ul>
</div>
);
}

return (
<div className={twMerge(`mt-8 flex items-center justify-center`, className)} aria-label={label}>
<div
className={twMerge(`mt-8 flex items-center justify-center`, className)}
aria-label={label}
role="status"
>
<div className="h-12 w-12 animate-loader border-4 border-primary dark:border-neutral-200">
<div className="w-full animate-loader-inner bg-primary dark:bg-neutral-200" />
<div className="w-full animate-loader-inner bg-primary dark:bg-neutral-100" />
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ export const AnswerForm = ({
const [isError, setIsError] = useState(false);

const disabled =
content.trim().length === 0 || !sources.every((source) => URL_REGEX.test(source));
content.trim().length === 0 ||
!sources.every((source) => URL_REGEX.test(source)) ||
initContent === content;

const handleFormSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/components/QuestionAnswers/EditAnswer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const EditAnswer = ({
return (
<div className="mt-4">
<AnswerForm
title="Edytuj odpowiedź"
title="Zapisz zmiany"
initContent={content}
initSources={sources}
error={isError}
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/components/QuestionItem/QuestionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Technology } from "../../lib/technologies";
import { formatDate } from "../../utils/intl";
import { Box } from "../Box";
import { MarkdownContent } from "../MarkdownContent";
import { Level, QuestionLevel } from "./QuestionLevel";
import { Level } from "./QuestionLevel";

type QuestionItemProps = Readonly<{
id: number;
Expand Down
37 changes: 27 additions & 10 deletions apps/app/src/components/QuestionsList/QuestionsList.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
"use client";

import dynamic from "next/dynamic";
import { useGetQuestionsVotes } from "../../hooks/useQuestionVoting";
import { Question, QuestionFilter } from "../../types";
import { AdminQuestion, QuestionFilter } from "../../types";
import { QuestionItem } from "../QuestionItem/QuestionItem";
import { QuestionLevel } from "../QuestionItem/QuestionLevel";
import { QuestionVoting } from "./QuestionVoting";

type QuestionsListProps = Readonly<{
questions: Question[];
questions: AdminQuestion[];
questionFilter: QuestionFilter;
}>;

const QuestionsManagement = dynamic(
() =>
import(/* webpackChunkName: "QuestionsManagement" */ "./QuestionsManagment").then(
(mod) => mod.QuestionsManagement,
),
{
ssr: false,
},
);

export const QuestionsList = ({ questions, questionFilter }: QuestionsListProps) => {
const { questionsVotes, refetch } = useGetQuestionsVotes(questionFilter);

Expand All @@ -20,8 +31,11 @@ export const QuestionsList = ({ questions, questionFilter }: QuestionsListProps)

return (
<ul className="space-y-10">
{questions.map(({ id, mdxContent, _levelId, _categoryId, acceptedAt }) => {
const questionVote = questionsVotes?.find((questionVote) => questionVote.id === id);
{questions.map((question) => {
const { id, mdxContent, _levelId, _categoryId, acceptedAt } = question;
const questionVote = questionsVotes?.find(
(questionVote) => questionVote.id === question.id,
);
const [votes, voted] = questionVote
? [questionVote.votesCount, questionVote.currentUserVotedOn]
: [0, false];
Expand All @@ -35,12 +49,15 @@ export const QuestionsList = ({ questions, questionFilter }: QuestionsListProps)
technology={_categoryId}
acceptedAt={acceptedAt}
leftSection={
<QuestionVoting
questionId={id}
votes={votes}
voted={voted}
onQuestionVote={onQuestionVote}
/>
<div className="flex flex-col gap-1.5">
<QuestionVoting
questionId={id}
votes={votes}
voted={voted}
onQuestionVote={onQuestionVote}
/>
<QuestionsManagement question={question} />
</div>
}
rightSection={<QuestionLevel level={_levelId} />}
/>
Expand Down
36 changes: 36 additions & 0 deletions apps/app/src/components/QuestionsList/QuestionsManagment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useRouter } from "next/navigation";
import { useCallback } from "react";
import { Button } from "../Button/Button";
import { PrivateElement } from "../PrivateElement";
import PencilIcon from "../../../public/icons/pencil.svg";
import { AdminQuestion } from "../../types";
import { useUIContext } from "../../providers/UIProvider";

export const QuestionsManagement = ({ question }: { question: AdminQuestion }) => {
const { openModal } = useUIContext();

const router = useRouter();
const onQuestionUpdate = useCallback(() => {
router.refresh();
}, [router]);

return (
<PrivateElement role="admin">
<div className="mr-3 flex flex-col justify-start gap-1.5">
<Button
variant="branding"
className="m-px flex w-24 min-w-0 items-center justify-center gap-2 p-0"
onClick={() =>
openModal("AddQuestionModal", {
...question,
additionalActionOnClose: onQuestionUpdate,
})
}
>
<PencilIcon className="fill-violet-700 dark:fill-neutral-200" />
Edytuj
</Button>
</div>
</PrivateElement>
);
};
39 changes: 26 additions & 13 deletions apps/app/src/components/SingleQuestion.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
"use client";

import { Question } from "../types";
import dynamic from "next/dynamic";
import { AdminQuestion } from "../types";
import { useGetQuestionVotesById } from "../hooks/useQuestionVoting";
import { QuestionItem } from "./QuestionItem/QuestionItem";
import { QuestionVoting } from "./QuestionsList/QuestionVoting";
import { QuestionTechnology } from "./QuestionItem/QuestionTechnology";
import { QuestionLevel } from "./QuestionItem/QuestionLevel";

const QuestionsManagement = dynamic(
() =>
import(
/* webpackChunkName: "QuestionsManagement" */ "../components/QuestionsList/QuestionsManagment"
).then((mod) => mod.QuestionsManagement),
{
ssr: false,
},
);

type SingleQuestionProps = Readonly<{
question: Question;
question: AdminQuestion;
}>;

export const SingleQuestion = ({
question: { id, mdxContent, _levelId, _categoryId, acceptedAt },
}: SingleQuestionProps) => {
export const SingleQuestion = ({ question }: SingleQuestionProps) => {
const { id, mdxContent, _levelId, _categoryId, acceptedAt } = question;
const { votes, voted, refetch } = useGetQuestionVotesById(id);

return (
Expand All @@ -24,14 +34,17 @@ export const SingleQuestion = ({
technology={_categoryId}
acceptedAt={acceptedAt}
leftSection={
<QuestionVoting
questionId={id}
votes={votes}
voted={voted}
onQuestionVote={() => {
void refetch();
}}
/>
<div className="flex flex-col gap-1.5">
<QuestionVoting
questionId={id}
votes={votes}
voted={voted}
onQuestionVote={() => {
void refetch();
}}
/>
<QuestionsManagement question={question} />
</div>
}
rightSection={
<div className="flex flex-col items-center">
Expand Down
Loading

0 comments on commit 7c98b84

Please sign in to comment.