From 4d0fb53fb56ca502d31f82129f9b23e8d2c9d9bf Mon Sep 17 00:00:00 2001 From: 21120447 Date: Sun, 27 Oct 2024 15:53:19 +0700 Subject: [PATCH] :sparkles: feat: handle search word in dictionary --- package.json | 1 + src/components/search-bar/SearchBar.tsx | 43 +++++++++++ src/components/word-result/WordResult.tsx | 73 +++++++++++++++++++ src/config/api/dictionary/apiDictionary.ts | 13 ++++ src/config/api/dictionary/axios/index.ts | 13 ++++ .../api/dictionary/axios/interceptor.ts | 30 ++++++++ src/config/router/router.tsx | 39 +++++++++- src/features/dictionary/DictionaryPage.tsx | 46 ++++++++++++ src/features/flash-card/FlashCardSetsPage.tsx | 5 ++ .../flash-card/LearnFlashCardPage.tsx | 5 ++ .../grammar-check/GrammarCheckPage.tsx | 5 ++ .../LearnThroughImagesPage.tsx | 5 ++ src/features/spell-check/SpellCheckPage.tsx | 5 ++ src/layout/Layout.tsx | 2 +- src/types/dictionary.ts | 35 +++++++++ yarn.lock | 57 +++++++++++++++ 16 files changed, 374 insertions(+), 3 deletions(-) create mode 100644 src/components/search-bar/SearchBar.tsx create mode 100644 src/components/word-result/WordResult.tsx create mode 100644 src/config/api/dictionary/apiDictionary.ts create mode 100644 src/config/api/dictionary/axios/index.ts create mode 100644 src/config/api/dictionary/axios/interceptor.ts create mode 100644 src/features/dictionary/DictionaryPage.tsx create mode 100644 src/features/flash-card/FlashCardSetsPage.tsx create mode 100644 src/features/flash-card/LearnFlashCardPage.tsx create mode 100644 src/features/grammar-check/GrammarCheckPage.tsx create mode 100644 src/features/learn-through-images/LearnThroughImagesPage.tsx create mode 100644 src/features/spell-check/SpellCheckPage.tsx create mode 100644 src/types/dictionary.ts diff --git a/package.json b/package.json index 4ae2436..d9c1257 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@chakra-ui/react": "^2.10.3", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", + "axios": "^1.7.7", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.35.0", "framer-motion": "^11.11.10", diff --git a/src/components/search-bar/SearchBar.tsx b/src/components/search-bar/SearchBar.tsx new file mode 100644 index 0000000..82eba0f --- /dev/null +++ b/src/components/search-bar/SearchBar.tsx @@ -0,0 +1,43 @@ +import { IconButton, Input, InputGroup, InputRightElement } from "@chakra-ui/react"; +import { Search } from "lucide-react"; +import React from "react"; +import { useNavigate } from "react-router-dom"; + +type Props = { + word: string; +}; + +const SearchBar = ({ word }: Props) => { + const navigate = useNavigate(); + const [searchText, setSearchText] = React.useState(word); + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + navigate("/dictionary?q=" + searchText); + }; + + return ( +
+ + setSearchText(e.target.value)} + /> + + } + type="submit" + /> + + +
+ ); +}; + +export default SearchBar; diff --git a/src/components/word-result/WordResult.tsx b/src/components/word-result/WordResult.tsx new file mode 100644 index 0000000..51c1434 --- /dev/null +++ b/src/components/word-result/WordResult.tsx @@ -0,0 +1,73 @@ +import { Volume2 } from "lucide-react"; +import { Box, Code, Heading, HStack, IconButton, Text, VStack } from "@chakra-ui/react"; +import { IWord, IWordNotFound } from "@/types/dictionary"; + +type Props = { + wordFound: IWord[] | undefined; + wordNotFound: IWordNotFound | undefined; +}; + +const WordResult = ({ wordFound, wordNotFound }: Props) => { + return ( + <> + {wordFound && ( + <> + + {wordFound[0].word} + {wordFound[0].phonetics + .filter((e) => e.audio != "") + .map((phonetic, index) => ( + + } + bg="transparent" + _hover={{ bg: "rgba(0, 0, 0, 0.04)" }} + onClick={() => new Audio(phonetic.audio).play()} + /> + + {phonetic.text} + + + ))} + + {wordFound.map((wordFound, index) => ( + + {wordFound.meanings.map((meaning, index) => ( + + + {meaning.partOfSpeech} + + {meaning.definitions.map((definition, index) => ( + + {definition.definition} + {definition.example && ( + + + Example: + + {definition.example} + + )} + + ))} + + ))} + + ))} + + )} + {wordNotFound && ( + + {wordNotFound.title} + + {wordNotFound.message} + {wordNotFound.resolution} + + + )} + + ); +}; + +export default WordResult; diff --git a/src/config/api/dictionary/apiDictionary.ts b/src/config/api/dictionary/apiDictionary.ts new file mode 100644 index 0000000..4c73283 --- /dev/null +++ b/src/config/api/dictionary/apiDictionary.ts @@ -0,0 +1,13 @@ +import { AxiosResponse } from "axios"; +import AxiosClient from "./axios"; + +export const searchWord = async (word: string) => { + const response: AxiosResponse = await AxiosClient.get(`/${word}`); + if (response.status == 404) { + return response.data; + } + if (response.status != 200) { + throw new Error("Something went wrong"); + } + return response; +}; diff --git a/src/config/api/dictionary/axios/index.ts b/src/config/api/dictionary/axios/index.ts new file mode 100644 index 0000000..eab3473 --- /dev/null +++ b/src/config/api/dictionary/axios/index.ts @@ -0,0 +1,13 @@ +import axios from "axios"; +import { setupInterceptors } from "./interceptor"; + +const AxiosClient = axios.create({ + baseURL: "https://api.dictionaryapi.dev/api/v2/entries/en", + headers: { + Accept: "application/json", + }, +}); + +setupInterceptors(AxiosClient); + +export default AxiosClient; diff --git a/src/config/api/dictionary/axios/interceptor.ts b/src/config/api/dictionary/axios/interceptor.ts new file mode 100644 index 0000000..07f9c62 --- /dev/null +++ b/src/config/api/dictionary/axios/interceptor.ts @@ -0,0 +1,30 @@ +import { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from "axios"; + +interface IRequestAxios extends InternalAxiosRequestConfig { + skipLoading?: boolean; +} + +const onRequestConfig = (config: IRequestAxios) => { + if (!config.headers["Content-Type"]) { + config.headers["Content-Type"] = "application/json"; + } + config.timeout = 30000; + return config; +}; + +const onRequestError = (error: AxiosError): Promise => { + return Promise.reject(error); +}; + +const onResponse = (res: AxiosResponse): AxiosResponse => { + return res; +}; + +const onResponseError = async (err: AxiosError): Promise => { + return Promise.reject(err?.response?.data); +}; + +export const setupInterceptors = (axiosInstance: AxiosInstance) => { + axiosInstance.interceptors.request.use(onRequestConfig, onRequestError); + axiosInstance.interceptors.response.use(onResponse, (err: AxiosError) => onResponseError(err)); +}; diff --git a/src/config/router/router.tsx b/src/config/router/router.tsx index b08b3ff..bcf9af9 100644 --- a/src/config/router/router.tsx +++ b/src/config/router/router.tsx @@ -1,11 +1,46 @@ +import DictionaryPage from "@/features/dictionary/DictionaryPage"; +import FlashCardSetsPage from "@/features/flash-card/FlashCardSetsPage"; +import LearnFlashCardPage from "@/features/flash-card/LearnFlashCardPage"; +import GrammarCheckPage from "@/features/grammar-check/GrammarCheckPage"; +import LearnThroughImagesPage from "@/features/learn-through-images/LearnThroughImagesPage"; +import SpellCheckPage from "@/features/spell-check/SpellCheckPage"; import Layout from "@/layout/Layout"; -import { createBrowserRouter } from "react-router-dom"; +import { createBrowserRouter, Navigate } from "react-router-dom"; const router = createBrowserRouter( [ { element: , - path: "*", + children: [ + { + path: "/", + element: , + }, + { + path: "/dictionary", + element: , + }, + { + path: "/learn-through-images", + element: , + }, + { + path: "/learn-flashcard", + element: , + }, + { + path: "/learn-flashcard/:ListId", + element: , + }, + { + path: "/check-spelling", + element: , + }, + { + path: "check-grammar", + element: , + }, + ], }, ], { diff --git a/src/features/dictionary/DictionaryPage.tsx b/src/features/dictionary/DictionaryPage.tsx new file mode 100644 index 0000000..0965576 --- /dev/null +++ b/src/features/dictionary/DictionaryPage.tsx @@ -0,0 +1,46 @@ +import SearchBar from "@/components/search-bar/SearchBar"; +import WordResult from "@/components/word-result/WordResult"; +import { searchWord } from "@/config/api/dictionary/apiDictionary"; +import { IWord, IWordNotFound } from "@/types/dictionary"; +import { HStack } from "@chakra-ui/react"; +import { AxiosResponse } from "axios"; +import React from "react"; +import { useSearchParams } from "react-router-dom"; + +const DictionaryPage = () => { + const [searchParams] = useSearchParams(); + const [word, setWord] = React.useState(""); + const [wordFound, setWordFound] = React.useState(); + const [wordNotFound, setWordNotFound] = React.useState(); + + React.useEffect(() => { + if (!searchParams.has("q")) return; + setWord(searchParams.get("q")!); + // handle search words + searchWord(searchParams!.get("q")!) + .then((res: AxiosResponse) => { + setWordFound(res.data as IWord[]); + setWordNotFound(undefined); + }) + .catch((err) => { + console.log(err); + setWordFound(undefined); + setWordNotFound({ + title: "No Definitions Found", + message: "Sorry pal, we couldn't find definitions for the word you were looking for.", + resolution: "You can try the search again at later time or head to the web instead", + } as IWordNotFound); + }); + }, [searchParams]); + + return ( + <> + + + + + + ); +}; + +export default DictionaryPage; diff --git a/src/features/flash-card/FlashCardSetsPage.tsx b/src/features/flash-card/FlashCardSetsPage.tsx new file mode 100644 index 0000000..d1a5753 --- /dev/null +++ b/src/features/flash-card/FlashCardSetsPage.tsx @@ -0,0 +1,5 @@ +const FlashCardSetsPage = () => { + return
FlashCardSetsPage
; +}; + +export default FlashCardSetsPage; diff --git a/src/features/flash-card/LearnFlashCardPage.tsx b/src/features/flash-card/LearnFlashCardPage.tsx new file mode 100644 index 0000000..bd16205 --- /dev/null +++ b/src/features/flash-card/LearnFlashCardPage.tsx @@ -0,0 +1,5 @@ +const LearnFlashCardPage = () => { + return
LearnFlashCardPage
; +}; + +export default LearnFlashCardPage; diff --git a/src/features/grammar-check/GrammarCheckPage.tsx b/src/features/grammar-check/GrammarCheckPage.tsx new file mode 100644 index 0000000..6e14565 --- /dev/null +++ b/src/features/grammar-check/GrammarCheckPage.tsx @@ -0,0 +1,5 @@ +const GrammarCheckPage = () => { + return
GrammarCheckPage
; +}; + +export default GrammarCheckPage; diff --git a/src/features/learn-through-images/LearnThroughImagesPage.tsx b/src/features/learn-through-images/LearnThroughImagesPage.tsx new file mode 100644 index 0000000..721936b --- /dev/null +++ b/src/features/learn-through-images/LearnThroughImagesPage.tsx @@ -0,0 +1,5 @@ +const LearnThroughImagesPage = () => { + return
LearnThroughImagesPage
; +}; + +export default LearnThroughImagesPage; diff --git a/src/features/spell-check/SpellCheckPage.tsx b/src/features/spell-check/SpellCheckPage.tsx new file mode 100644 index 0000000..7dac50d --- /dev/null +++ b/src/features/spell-check/SpellCheckPage.tsx @@ -0,0 +1,5 @@ +const SpellCheckPage = () => { + return
SpellCheckPage
; +}; + +export default SpellCheckPage; diff --git a/src/layout/Layout.tsx b/src/layout/Layout.tsx index d4cc021..991e01b 100644 --- a/src/layout/Layout.tsx +++ b/src/layout/Layout.tsx @@ -16,7 +16,7 @@ const Layout = () => { -
+
diff --git a/src/types/dictionary.ts b/src/types/dictionary.ts new file mode 100644 index 0000000..7f46628 --- /dev/null +++ b/src/types/dictionary.ts @@ -0,0 +1,35 @@ +export type IWordNotFound = { + title: string; + message: string; + resolution: string; +}; + +export type IWord = { + word: string; + phonetic: string; + phonetics: { + text: string; + audio: string; + sourceUrl: string; + license?: { + name: string; + url: string; + }; + }[]; + meanings: { + partOfSpeech: string; + definitions: { + definition: string; + synonyms: string[]; + antonyms: string[]; + example?: string; + }[]; + synonyms: string[]; + antonyms: string[]; + }[]; + license: { + name: string; + url: string; + }; + sourceUrls: string[]; +}; diff --git a/yarn.lock b/yarn.lock index 64ba247..ec384b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1221,6 +1221,11 @@ arraybuffer.prototype.slice@^1.0.3: is-array-buffer "^3.0.4" is-shared-array-buffer "^1.0.2" +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + autoprefixer@^10.4.20: version "10.4.20" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b" @@ -1240,6 +1245,15 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" +axios@^1.7.7: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-plugin-macros@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" @@ -1392,6 +1406,13 @@ color2k@^2.0.2: resolved "https://registry.yarnpkg.com/color2k/-/color2k-2.0.3.tgz#a771244f6b6285541c82aa65ff0a0c624046e533" integrity sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + commander@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" @@ -1582,6 +1603,11 @@ define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + detect-node-es@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" @@ -2074,6 +2100,11 @@ focus-lock@^1.3.5: dependencies: tslib "^2.0.3" +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -2089,6 +2120,15 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" +form-data@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" + integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + fraction.js@^4.3.7: version "4.3.7" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" @@ -2826,6 +2866,18 @@ micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.3" picomatch "^2.3.1" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mimic-fn@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" @@ -3192,6 +3244,11 @@ prop-types@^15.6.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"