Skip to content

Commit

Permalink
✨ feat: handle search word in dictionary
Browse files Browse the repository at this point in the history
  • Loading branch information
nxhawk committed Oct 27, 2024
1 parent ce3da0c commit 4d0fb53
Show file tree
Hide file tree
Showing 16 changed files with 374 additions and 3 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
43 changes: 43 additions & 0 deletions src/components/search-bar/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<form onSubmit={handleSearch}>
<InputGroup size="md" width="400px">
<Input
pr="4.5rem"
placeholder="Search dictionary"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
<InputRightElement width="4.5rem">
<IconButton
w="3rem"
size="sm"
bgColor="transparent"
_hover={{ bg: "rgba(0, 0,0, 0.04)" }}
aria-label="Search dictionary"
icon={<Search className="w-5 h-5" />}
type="submit"
/>
</InputRightElement>
</InputGroup>
</form>
);
};

export default SearchBar;
73 changes: 73 additions & 0 deletions src/components/word-result/WordResult.tsx
Original file line number Diff line number Diff line change
@@ -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 && (
<>
<VStack alignItems="flex-start" gap="0px">
<Heading marginY="5px">{wordFound[0].word}</Heading>
{wordFound[0].phonetics
.filter((e) => e.audio != "")
.map((phonetic, index) => (
<HStack key={`sound${index}`} alignItems="center" gap="16px">
<IconButton
aria-label="audio button"
icon={<Volume2 className="text-cyan-800" />}
bg="transparent"
_hover={{ bg: "rgba(0, 0, 0, 0.04)" }}
onClick={() => new Audio(phonetic.audio).play()}
/>
<Text>
<Code fontSize="1em">{phonetic.text}</Code>
</Text>
</HStack>
))}
</VStack>
{wordFound.map((wordFound, index) => (
<VStack key={index} gap={"32px"} alignItems="flex-start" marginTop={"20px"}>
{wordFound.meanings.map((meaning, index) => (
<VStack key={`meaning${index}`} gap={"16px"} alignItems="flex-start">
<Heading size="lg" color={"blue.900"}>
{meaning.partOfSpeech}
</Heading>
{meaning.definitions.map((definition, index) => (
<Box key={`definition${index}`} gap={"16px"}>
<Text>{definition.definition}</Text>
{definition.example && (
<HStack alignItems={"flex-start"}>
<Text fontWeight="bold" color="blue.900">
Example:
</Text>
<Text>{definition.example}</Text>
</HStack>
)}
</Box>
))}
</VStack>
))}
</VStack>
))}
</>
)}
{wordNotFound && (
<VStack alignItems="flex-start" gap={"10px"} marginY={"40px"}>
<Heading variant="h1">{wordNotFound.title}</Heading>
<VStack alignItems="flex-start" gap={"4px"}>
<Text variant="subtitle1">{wordNotFound.message}</Text>
<Text variant="subtitle1">{wordNotFound.resolution}</Text>
</VStack>
</VStack>
)}
</>
);
};

export default WordResult;
13 changes: 13 additions & 0 deletions src/config/api/dictionary/apiDictionary.ts
Original file line number Diff line number Diff line change
@@ -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;
};
13 changes: 13 additions & 0 deletions src/config/api/dictionary/axios/index.ts
Original file line number Diff line number Diff line change
@@ -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;
30 changes: 30 additions & 0 deletions src/config/api/dictionary/axios/interceptor.ts
Original file line number Diff line number Diff line change
@@ -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<AxiosError> => {
return Promise.reject(error);
};

const onResponse = (res: AxiosResponse): AxiosResponse => {
return res;
};

const onResponseError = async (err: AxiosError): Promise<AxiosError | undefined> => {
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));
};
39 changes: 37 additions & 2 deletions src/config/router/router.tsx
Original file line number Diff line number Diff line change
@@ -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: <Layout />,
path: "*",
children: [
{
path: "/",
element: <Navigate to="/dictionary" />,
},
{
path: "/dictionary",
element: <DictionaryPage />,
},
{
path: "/learn-through-images",
element: <LearnThroughImagesPage />,
},
{
path: "/learn-flashcard",
element: <FlashCardSetsPage />,
},
{
path: "/learn-flashcard/:ListId",
element: <LearnFlashCardPage />,
},
{
path: "/check-spelling",
element: <SpellCheckPage />,
},
{
path: "check-grammar",
element: <GrammarCheckPage />,
},
],
},
],
{
Expand Down
46 changes: 46 additions & 0 deletions src/features/dictionary/DictionaryPage.tsx
Original file line number Diff line number Diff line change
@@ -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<string>("");
const [wordFound, setWordFound] = React.useState<IWord[]>();
const [wordNotFound, setWordNotFound] = React.useState<IWordNotFound>();

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 (
<>
<HStack justifyContent="center" marginY="15px">
<SearchBar word={word} />
</HStack>
<WordResult wordFound={wordFound} wordNotFound={wordNotFound} />
</>
);
};

export default DictionaryPage;
5 changes: 5 additions & 0 deletions src/features/flash-card/FlashCardSetsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const FlashCardSetsPage = () => {
return <div>FlashCardSetsPage</div>;
};

export default FlashCardSetsPage;
5 changes: 5 additions & 0 deletions src/features/flash-card/LearnFlashCardPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const LearnFlashCardPage = () => {
return <div>LearnFlashCardPage</div>;
};

export default LearnFlashCardPage;
5 changes: 5 additions & 0 deletions src/features/grammar-check/GrammarCheckPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const GrammarCheckPage = () => {
return <div>GrammarCheckPage</div>;
};

export default GrammarCheckPage;
5 changes: 5 additions & 0 deletions src/features/learn-through-images/LearnThroughImagesPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const LearnThroughImagesPage = () => {
return <div>LearnThroughImagesPage</div>;
};

export default LearnThroughImagesPage;
5 changes: 5 additions & 0 deletions src/features/spell-check/SpellCheckPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const SpellCheckPage = () => {
return <div>SpellCheckPage</div>;
};

export default SpellCheckPage;
2 changes: 1 addition & 1 deletion src/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const Layout = () => {
<AppBar toggleSidebar={toggleSidebar} />
<HStack position="relative" align="flex-start">
<SideBar isOpen={isSidebarOpen} />
<main style={{ flex: 1, transition: "margin-left 0.3s", marginTop: "64px" }} id="content">
<main style={{ flex: 1, transition: "margin-left 0.3s", marginTop: "64px", padding: "10px" }} id="content">
<Outlet />
</main>
</HStack>
Expand Down
35 changes: 35 additions & 0 deletions src/types/dictionary.ts
Original file line number Diff line number Diff line change
@@ -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[];
};
Loading

0 comments on commit 4d0fb53

Please sign in to comment.