|
| 1 | +import React, { useState, useEffect } from 'react'; |
| 2 | +import { useQuery, useMutation } from '@apollo/client'; |
| 3 | +import { toast } from 'react-toastify'; |
| 4 | +import { useTranslation } from 'react-i18next'; |
| 5 | +import { |
| 6 | + GET_ALL_QUESTIONS, |
| 7 | + CREATE_QUESTION, |
| 8 | + CREATE_ANSWER, |
| 9 | + DELETE_QUESTION, |
| 10 | + DELETE_ANSWER, |
| 11 | +} from '../queries/questions.query'; |
| 12 | + |
| 13 | +function CommunityQuestions() { |
| 14 | + const { loading, error, data, refetch } = useQuery(GET_ALL_QUESTIONS); |
| 15 | + const [createQuestion, { loading: isSubmitting }] = |
| 16 | + useMutation(CREATE_QUESTION); |
| 17 | + const { t } = useTranslation(); |
| 18 | + const [createAnswer] = useMutation(CREATE_ANSWER); |
| 19 | + const [deleteQuestion] = useMutation(DELETE_QUESTION); |
| 20 | + const [deleteAnswer] = useMutation(DELETE_ANSWER); |
| 21 | + const [loggedUser, setLoggedUser] = useState<string | null>(null); |
| 22 | + const [questionText, setQuestionText] = useState(''); |
| 23 | + const [answerText, setAnswerText] = useState(''); |
| 24 | + const [selectedQuestion, setSelectedQuestion] = useState<any>(null); |
| 25 | + const [questionTitleText, setQuestionTitleText] = useState<any>(null); |
| 26 | + const [questions, setQuestions] = useState<any[]>([]); |
| 27 | + |
| 28 | + useEffect(() => { |
| 29 | + const authData = localStorage.getItem('auth'); |
| 30 | + if (authData) { |
| 31 | + const parsedAuthData = JSON.parse(authData); |
| 32 | + setLoggedUser(parsedAuthData.userId); |
| 33 | + } |
| 34 | + }, []); |
| 35 | + useEffect(() => { |
| 36 | + if (data) setQuestions(data.getAllQuestions); |
| 37 | + |
| 38 | + if (data && selectedQuestion) { |
| 39 | + const updatedQuestion = data.getAllQuestions.find( |
| 40 | + (question: any) => question.id === selectedQuestion.id, |
| 41 | + ); |
| 42 | + setSelectedQuestion(updatedQuestion || null); |
| 43 | + } |
| 44 | + }, [data, selectedQuestion]); |
| 45 | + |
| 46 | + if (loading) return <p>Loading...</p>; |
| 47 | + if (error) return <p>Error loading questions.</p>; |
| 48 | + |
| 49 | + const handleCreateQuestion = async () => { |
| 50 | + if (questionText.trim()) { |
| 51 | + await createQuestion({ |
| 52 | + variables: { |
| 53 | + title: questionTitleText, |
| 54 | + content: questionText, |
| 55 | + }, |
| 56 | + update: (cache, { data: { createQuestion } }) => { |
| 57 | + const existingQuestions: any = cache.readQuery({ |
| 58 | + query: GET_ALL_QUESTIONS, |
| 59 | + }); |
| 60 | + cache.writeQuery({ |
| 61 | + query: GET_ALL_QUESTIONS, |
| 62 | + data: { |
| 63 | + getAllQuestions: [ |
| 64 | + ...existingQuestions.getAllQuestions, |
| 65 | + createQuestion, |
| 66 | + ], |
| 67 | + }, |
| 68 | + }); |
| 69 | + }, |
| 70 | + onError: (error) => { |
| 71 | + toast.error('Unable to create question.'); |
| 72 | + }, |
| 73 | + onCompleted: () => { |
| 74 | + toast.success('Question created successfully'); |
| 75 | + refetch(); |
| 76 | + setQuestionText(''); |
| 77 | + setQuestionTitleText(''); |
| 78 | + }, |
| 79 | + }); |
| 80 | + } else { |
| 81 | + toast.error('Question text cannot be empty.'); |
| 82 | + } |
| 83 | + }; |
| 84 | + |
| 85 | + const handleCreateAnswer = async (questionId: string) => { |
| 86 | + if (answerText.trim()) { |
| 87 | + await createAnswer({ |
| 88 | + variables: { |
| 89 | + questionId, |
| 90 | + content: answerText, |
| 91 | + }, |
| 92 | + update: (cache, { data: { createAnswer } }) => { |
| 93 | + const existingQuestions: any = cache.readQuery({ |
| 94 | + query: GET_ALL_QUESTIONS, |
| 95 | + }); |
| 96 | + const updatedQuestions = existingQuestions.getAllQuestions.map( |
| 97 | + (question: any) => { |
| 98 | + if (question?.id === questionId) { |
| 99 | + return { |
| 100 | + ...question, |
| 101 | + answers: [...(question?.answers || []), createAnswer], |
| 102 | + }; |
| 103 | + } |
| 104 | + |
| 105 | + return question; |
| 106 | + }, |
| 107 | + ); |
| 108 | + setQuestions(updatedQuestions); |
| 109 | + cache.writeQuery({ |
| 110 | + query: GET_ALL_QUESTIONS, |
| 111 | + data: { getAllQuestions: updatedQuestions }, |
| 112 | + }); |
| 113 | + }, |
| 114 | + onError: (error) => { |
| 115 | + toast.error('Unable to submit your answer.'); |
| 116 | + }, |
| 117 | + onCompleted: () => { |
| 118 | + toast.success('Answer submitted successfully'); |
| 119 | + refetch(); |
| 120 | + setAnswerText(''); |
| 121 | + }, |
| 122 | + }); |
| 123 | + } else { |
| 124 | + toast.error('Answer text cannot be empty.'); |
| 125 | + } |
| 126 | + }; |
| 127 | + |
| 128 | + const handleDeleteQuestion = async (questionId: string) => { |
| 129 | + await deleteQuestion({ |
| 130 | + variables: { id: questionId }, |
| 131 | + update: (cache) => { |
| 132 | + const existingQuestions: any = cache.readQuery({ |
| 133 | + query: GET_ALL_QUESTIONS, |
| 134 | + }); |
| 135 | + const updatedQuestions = existingQuestions.getAllQuestions.filter( |
| 136 | + (question: any) => question?.id !== questionId, |
| 137 | + ); |
| 138 | + cache.writeQuery({ |
| 139 | + query: GET_ALL_QUESTIONS, |
| 140 | + data: { getAllQuestions: updatedQuestions }, |
| 141 | + }); |
| 142 | + }, |
| 143 | + onError: (error) => { |
| 144 | + toast.error('Unable to delete the question.'); |
| 145 | + }, |
| 146 | + onCompleted: () => { |
| 147 | + toast.success('Question deleted successfully'); |
| 148 | + setSelectedQuestion(''); |
| 149 | + |
| 150 | + refetch(); |
| 151 | + }, |
| 152 | + }); |
| 153 | + }; |
| 154 | + |
| 155 | + const handleDeleteAnswer = async (answerId: string) => { |
| 156 | + await deleteAnswer({ |
| 157 | + variables: { id: answerId }, |
| 158 | + update: (cache) => { |
| 159 | + const existingQuestions: any = cache.readQuery({ |
| 160 | + query: GET_ALL_QUESTIONS, |
| 161 | + }); |
| 162 | + const updatedQuestions = existingQuestions.getAllQuestions.map( |
| 163 | + (question: any) => ({ |
| 164 | + ...question, |
| 165 | + answers: question?.answers.filter( |
| 166 | + (answer: any) => answer.id !== answerId, |
| 167 | + ), |
| 168 | + }), |
| 169 | + ); |
| 170 | + cache.writeQuery({ |
| 171 | + query: GET_ALL_QUESTIONS, |
| 172 | + data: { getAllQuestions: updatedQuestions }, |
| 173 | + }); |
| 174 | + }, |
| 175 | + onError: (error) => { |
| 176 | + toast.error('Unable to delete the answer.'); |
| 177 | + }, |
| 178 | + onCompleted: () => { |
| 179 | + toast.success('Answer deleted successfully'); |
| 180 | + // setSelectedQuestion(''); |
| 181 | + refetch({ query: GET_ALL_QUESTIONS }); |
| 182 | + }, |
| 183 | + }); |
| 184 | + }; |
| 185 | + |
| 186 | + return ( |
| 187 | + <div className="flex items-start p-4 bg-indigo-100 dark:bg-transparent rounded-md shadow-lg h-full "> |
| 188 | + <div className="w-1/3 pr-4 border-r border-divider-bg dark:border-border-dark "> |
| 189 | + <h2 className="text-lg font-semibold text-center text-header-text dark:text-dark-text-fill"> |
| 190 | + Questions |
| 191 | + </h2> |
| 192 | + {loggedUser && ( |
| 193 | + <div className="mt-4 -4 bg-tertiary dark:bg-transparent rounded-md flex flex-col gap-2"> |
| 194 | + <input |
| 195 | + type="text" |
| 196 | + value={questionTitleText} |
| 197 | + onChange={(e) => setQuestionTitleText(e.target.value)} |
| 198 | + placeholder="Write question title..." |
| 199 | + className="p-2 border dark:bg-dark-tertiary border-gray-300 dark:border-gray-600 dark:text-black rounded w-full" |
| 200 | + /> |
| 201 | + <textarea |
| 202 | + value={questionText} |
| 203 | + onChange={(e) => setQuestionText(e.target.value)} |
| 204 | + rows={3} |
| 205 | + className="w-full dark:bg-dark-tertiary p-2 border rounded-md dark:text-black" |
| 206 | + placeholder="Ask a question..." |
| 207 | + /> |
| 208 | + <button |
| 209 | + onClick={handleCreateQuestion} |
| 210 | + disabled={isSubmitting} |
| 211 | + type="button" |
| 212 | + className="mt-2 w-fit p-2 px-6 bg-primary text-white rounded-md disabled:opacity-50" |
| 213 | + > |
| 214 | + {isSubmitting ? 'Submitting...' : 'Submit Question'} |
| 215 | + </button> |
| 216 | + </div> |
| 217 | + )} |
| 218 | + <ul className="mt-4 space-y-4"> |
| 219 | + {questions?.map((question: any) => ( |
| 220 | + <li |
| 221 | + key={question?.id} |
| 222 | + className={`cursor-pointer p-3 rounded-md shadow-sm ${ |
| 223 | + selectedQuestion?.id === question?.id |
| 224 | + ? 'bg-primary text-dark-text-fill' |
| 225 | + : 'bg-tertiary text-light-text dark:bg-dark-tertiary dark:text-dark-text-fill' |
| 226 | + }`} |
| 227 | + onClick={() => setSelectedQuestion(question)} |
| 228 | + onKeyPress={(e) => { |
| 229 | + if (e.key === 'Enter' || e.key === ' ') { |
| 230 | + setSelectedQuestion(question); |
| 231 | + } |
| 232 | + }} |
| 233 | + role="presentation" |
| 234 | + > |
| 235 | + <p className="text-md font-semibold">{question?.title}</p> |
| 236 | + <p className="text-sm text-secondary dark:text-dark-text-fill"> |
| 237 | + Asked by: {question?.author?.email} |
| 238 | + </p> |
| 239 | + </li> |
| 240 | + ))} |
| 241 | + </ul> |
| 242 | + </div> |
| 243 | + |
| 244 | + <div className="w-2/3 pl-4"> |
| 245 | + {selectedQuestion ? ( |
| 246 | + <div> |
| 247 | + <h2 className="text-lg font-bold text-primary"> |
| 248 | + {selectedQuestion?.title} |
| 249 | + </h2> |
| 250 | + <span>{selectedQuestion?.content}</span> |
| 251 | + <p className="text-sm text-secondary dark:text-dark-text-fill"> |
| 252 | + Asked by {selectedQuestion?.author?.email} |
| 253 | + </p> |
| 254 | + {loggedUser && loggedUser === selectedQuestion?.author?.id && ( |
| 255 | + <button |
| 256 | + type="button" |
| 257 | + onClick={() => handleDeleteQuestion(selectedQuestion?.id)} |
| 258 | + className="text-red-500 text-sm" |
| 259 | + > |
| 260 | + {t('Delete')} |
| 261 | + </button> |
| 262 | + )} |
| 263 | + |
| 264 | + <div className="mt-4 dark:p-4 dark:bg-dark-tertiary"> |
| 265 | + <h3 className="font-semibold text-secondary dark:text-dark-text-fill border-b border-black py-2"> |
| 266 | + Answers thread |
| 267 | + </h3> |
| 268 | + |
| 269 | + {selectedQuestion?.answers?.length > 0 ? ( |
| 270 | + selectedQuestion?.answers.map((answer: any) => ( |
| 271 | + <div |
| 272 | + key={answer.id} |
| 273 | + className={`mt-2 p-4 rounded-md flex flex-col ${ |
| 274 | + loggedUser && loggedUser === answer?.author?.id |
| 275 | + ? 'items-end' |
| 276 | + : 'items-start' |
| 277 | + }`} |
| 278 | + > |
| 279 | + <div |
| 280 | + className={` bg-gray-100 dark:bg-primary shadow dark:text-white text-black dark:bg-dark-secondary p-2 rounded-md ${ |
| 281 | + loggedUser && loggedUser === answer?.author?.id |
| 282 | + ? 'text-right' |
| 283 | + : 'text-left' |
| 284 | + }`} |
| 285 | + > |
| 286 | + <p>{answer.content}</p> |
| 287 | + <p className="text-sm text-secondary dark:text-white"> |
| 288 | + Answered by {answer.author.email} |
| 289 | + </p> |
| 290 | + </div> |
| 291 | + |
| 292 | + {/* Delete button for the user's own answers */} |
| 293 | + {loggedUser && loggedUser === answer?.author?.id && ( |
| 294 | + <button |
| 295 | + type="button" |
| 296 | + onClick={() => handleDeleteAnswer(answer.id)} |
| 297 | + className="mt-2 text-red-500 text-sm" |
| 298 | + > |
| 299 | + {t('Delete')} |
| 300 | + </button> |
| 301 | + )} |
| 302 | + </div> |
| 303 | + )) |
| 304 | + ) : ( |
| 305 | + <p className="text-sm text-gray-500 dark:text-dark-45"> |
| 306 | + No answers yet. Be the first to answer! |
| 307 | + </p> |
| 308 | + )} |
| 309 | + |
| 310 | + {/* Answer submission form for the logged-in user */} |
| 311 | + {loggedUser && ( |
| 312 | + <div className="mt-4 flex items-start flex-col w-full"> |
| 313 | + <textarea |
| 314 | + value={answerText} |
| 315 | + onChange={(e) => setAnswerText(e.target.value)} |
| 316 | + placeholder="Write an answer..." |
| 317 | + className="p-2 border outline-none bg-transparent dark:text-white dark:bg-dark-tertiary min-w-full min-h-24 border-gray-300 dark:border-gray-600 dark:text-black rounded" |
| 318 | + /> |
| 319 | + <button |
| 320 | + type="button" |
| 321 | + onClick={() => handleCreateAnswer(selectedQuestion?.id)} |
| 322 | + className="mt-2 bg-primary text-white rounded p-2 px-6" |
| 323 | + > |
| 324 | + Submit Answer |
| 325 | + </button> |
| 326 | + </div> |
| 327 | + )} |
| 328 | + </div> |
| 329 | + </div> |
| 330 | + ) : ( |
| 331 | + <p className="text-lg text-gray-500 dark:text-dark-45"> |
| 332 | + Select a question to view details. |
| 333 | + </p> |
| 334 | + )} |
| 335 | + </div> |
| 336 | + </div> |
| 337 | + ); |
| 338 | +} |
| 339 | + |
| 340 | +export default CommunityQuestions; |
0 commit comments