Commit ce10b840 authored by Aneeb Imamdin's avatar Aneeb Imamdin

First commit

parent 693fe1ed
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -3,11 +3,17 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^1.9.5",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"antd": "^5.6.3",
"install": "^0.13.0",
"npm": "^9.7.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.1.1",
"react-router-dom": "^6.14.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
......
import logo from './logo.svg';
import './App.css';
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Main from "./pages/Main";
import Recipes, { loadRecipes } from "./pages/Recipes";
import ErrorHandler from "./components/ErrorHandler";
import Recipe from "./pages/Recipe";
import { Home } from "./pages/Home";
const router = createBrowserRouter([
{
path: "",
element: <Main />,
errorElement: <ErrorHandler />,
children: [
{
index: true,
element: <Home />,
},
{
path: "recipes",
element: <Recipes />,
loader: loadRecipes,
},
{
path: "recipe/:id",
element: <Recipe />,
},
],
},
]);
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
return <RouterProvider router={router} />;
}
export default App;
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f2f2f2;
padding: 10px;
margin: 0px;
}
.logo img {
height: 40px;
width: auto;
}
.navLinks {
list-style: none;
display: flex;
margin: 0;
padding: 0;
}
.navLinks li {
margin-right: 15px;
}
.navLinks a {
text-decoration: none;
color: #333;
}
.icons img {
height: 20px;
width: auto;
margin-left: 10px;
}
.thumbnail {
width: 10%;
}
import { Link } from "react-router-dom";
const ErrorHandler = () => {
return (
<div style={{ textAlign: "center" }}>
<h3>There is some issue, please try again later</h3>
<Link to="/">Go to Home Page</Link>
</div>
);
};
export default ErrorHandler;
import { Avatar, Card, List, Skeleton, Space, Switch, Button } from "antd";
import { useSelector, useDispatch } from "react-redux";
import { LayoutActions } from "../store/layout";
import { RecipeActions } from "../store/recipe";
import { useNavigate, useLocation } from "react-router-dom";
import { BreadCrumbActions } from "../store/breadcrumb";
import { Link } from "react-router-dom";
const Layout = ({ recipes }) => {
const layout = useSelector((state) => state.layout.layout);
const dispatch = useDispatch();
const navigate = useNavigate();
const handleLayoutChange = (checked) => {
dispatch(LayoutActions.updateLayout(checked ? "list" : "grid"));
};
const location = useLocation();
const searchParams = new URLSearchParams(location.search);
// Access query parameters using the useParams hook
const query = searchParams.get("query");
const handleClick = (item) => {
dispatch(RecipeActions.updateRecipe(item));
const bread = [
{
title: <Link to="/">Home</Link>,
},
{
title: <Link to={`/recipes?query=${query}`}>{query}</Link>,
},
{
title: item.name,
},
];
dispatch(BreadCrumbActions.updateBreadCrumb(bread));
navigate(`/recipe/${item.id}`);
};
if (layout === "grid") {
return (
<>
<Space direction="vertical">
<Switch
checkedChildren="List"
unCheckedChildren="Grid"
defaultChecked
onChange={handleLayoutChange}
/>
</Space>
<List
grid={{ gutter: 16, column: 4 }}
dataSource={recipes}
renderItem={(item) => (
<List.Item>
<Card
title={
<Button onClick={() => handleClick(item)}>{item.name}</Button>
}
>
{item.description}
</Card>
</List.Item>
)}
/>
</>
);
}
return (
<>
<Space direction="vertical">
<Switch
checkedChildren="List"
unCheckedChildren="Grid"
defaultChecked
onChange={handleLayoutChange}
/>
</Space>
<List
className="demo-loadmore-list"
loading={false}
itemLayout="horizontal"
dataSource={recipes}
renderItem={(item) => (
<List.Item>
<Skeleton avatar title={false} loading={false} active>
<List.Item.Meta
avatar={<Avatar src={item.thumbnail_url} />}
title={
<Button onClick={() => handleClick(item)}>{item.name}</Button>
}
description={item.description}
/>
</Skeleton>
</List.Item>
)}
/>
</>
);
};
export default Layout;
import { Link, useNavigate } from "react-router-dom";
import styles from "./../assets/css/navigation.module.css";
import { AutoComplete, Input } from "antd";
import { useState, useEffect } from "react";
import { useDispatch } from "react-redux";
import { SearchActions } from "../store/search";
const Navigation = () => {
const [query, setQuery] = useState("");
const navigate = useNavigate();
const dispatch = useDispatch();
const [options, setOptions] = useState([
{ value: "", label: "Type to search" },
]);
const handleSearch = (value) => {
setQuery(value);
};
const onSelect = (value) => {
dispatch(SearchActions.setQuery(value));
navigate(`/recipes?query=${value}`);
};
useEffect(() => {
const timer = setTimeout(async () => {
if (query) {
setOptions([{ value: "", label: "Loading..." }]);
const response = await fetch(
`https://tasty.p.rapidapi.com/recipes/auto-complete?prefix=${query}`,
{
headers: {
"X-RapidAPI-Key":
"a144142bc4mshd5814560d376563p1bc61ajsna42f23118976",
"X-RapidAPI-Host": "tasty.p.rapidapi.com",
},
}
);
const data = await response.json();
const parsed = data.results.map((option) => {
return { value: option.display, label: option.search_value };
});
if (parsed.length < 1) {
setOptions([{ value: "", label: "No results" }]);
} else {
setOptions(parsed);
}
}
}, 200);
return function cleanup() {
clearTimeout(timer);
};
}, [query]);
return (
<nav className={styles.navbar}>
<div className={styles.logo}>
<Link to="/">Recipe Finder</Link>
</div>
<ul className={styles.navLinks}>
<li>
<AutoComplete
style={{
width: 200,
}}
onSearch={handleSearch}
options={options}
onSelect={onSelect}
>
<Input.Search
size="large"
placeholder="Search Recipe"
enterButton
/>
</AutoComplete>
</li>
</ul>
</nav>
);
};
export default Navigation;
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
import ReactDOM from "react-dom/client";
import App from "./App";
import { Provider } from "react-redux";
import store from "./store/index";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</React.StrictMode>
</Provider>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
import { useDispatch } from "react-redux";
import { BreadCrumbActions } from "../store/breadcrumb";
import { useEffect } from "react";
export const Home = () => {
const dispatch = useDispatch();
useEffect(() => {
const breadCrumb = [];
dispatch(BreadCrumbActions.updateBreadCrumb(breadCrumb));
}, []);
return <p style={{ textAlign: "center" }}>Home Page</p>;
};
import { Outlet } from "react-router-dom";
import Navigation from "../components/Navigation";
import { Breadcrumb } from "antd";
import { useSelector } from "react-redux";
const Main = () => {
const items = useSelector((state) => state.breadCrumb.items);
return (
<>
<Navigation />
<Breadcrumb items={items} />
<Outlet />
</>
);
};
export default Main;
import classes from "./../assets/css/recipe.module.css";
import { useSelector } from "react-redux/es/hooks/useSelector";
import { useEffect } from "react";
import { BreadCrumbActions } from "../store/breadcrumb";
import { useDispatch } from "react-redux";
import { Link } from "react-router-dom";
const Recipe = () => {
const recipe = useSelector((state) => state.recipe);
const { query } = useSelector((state) => state.search);
const dispatch = useDispatch();
useEffect(() => {
const breadCrumb = [
{
title: <Link to="/">Home</Link>,
},
{
title: <Link to={`/recipes?query=${query}`}>{query}</Link>,
},
{
title: recipe.name,
},
];
dispatch(BreadCrumbActions.updateBreadCrumb(breadCrumb));
}, []);
return (
<div>
<div>
<img
className={classes.thumbnail}
src={recipe.thumbnail_url}
alt={recipe.thumbnail_alt_text}
></img>
</div>
<div className={classes.title}>{recipe.name}</div>
<p>{recipe.description}</p>
<div>
<h4>Instructions :</h4>
{recipe.instructions.map((item) => (
<li key={item.id}>{item.display_text}</li>
))}
</div>
</div>
);
};
export default Recipe;
import {
defer,
useLoaderData,
Await,
json,
useLocation,
} from "react-router-dom";
import { Suspense } from "react";
import Layout from "../components/Layout";
import { Skeleton } from "antd";
import { useEffect } from "react";
import { Link } from "react-router-dom";
import { BreadCrumbActions } from "../store/breadcrumb";
import { useDispatch, useSelector } from "react-redux";
const Recipes = () => {
const { recipes } = useLoaderData();
const dispatch = useDispatch();
let { query } = useSelector((state) => state.search);
const location = useLocation();
if (!query) {
const searchParams = new URLSearchParams(location.search);
query = searchParams.get("query");
}
useEffect(() => {
const breadCrumb = [
{
title: <Link to="/">Home</Link>,
},
{
title: <Link to={`/recipes?query=${query}`}>{query}</Link>,
},
];
dispatch(BreadCrumbActions.updateBreadCrumb(breadCrumb));
}, []);
return (
<Suspense
fallback={
<>
<Skeleton />
<Skeleton />
<Skeleton />
<Skeleton />
<Skeleton />
</>
}
>
<Await resolve={recipes}>
{(loadedRecipes) => {
return <Layout recipes={loadedRecipes.results} />;
}}
</Await>
</Suspense>
);
};
const FetchRecipes = async (data) => {
const url = new URL(data.request.url);
const queryParams = new URLSearchParams(url.search);
// Access individual query parameters
const query = queryParams.get("query");
const response = await fetch(
`https://tasty.p.rapidapi.com/recipes/list?from=0&size=20&q=${query}`,
{
headers: {
"X-RapidAPI-Key": "a144142bc4mshd5814560d376563p1bc61ajsna42f23118976",
"X-RapidAPI-Host": "tasty.p.rapidapi.com",
},
}
);
try {
if (response.ok) {
const recipes = await response.json();
return recipes;
} else {
throw json({ message: "Cannot get data from API" }, { status: 500 });
}
} catch (error) {
console.log(error);
}
};
export const loadRecipes = async (data) => {
return defer({ recipes: FetchRecipes(data) });
};
export default Recipes;
import { createSlice } from "@reduxjs/toolkit";
const initialBreadCrumbState = {
items: [],
};
const breadCrumbSlice = createSlice({
name: "breadcrumb",
initialState: initialBreadCrumbState,
reducers: {
updateBreadCrumb(state, { payload }) {
state.items = payload;
},
},
});
export const BreadCrumbActions = breadCrumbSlice.actions;
export default breadCrumbSlice;
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import layoutSlice from "./layout";
import recipeSlice from "./recipe";
import breadCrumbSlice from "./breadcrumb";
import searchSlice from "./search";
const rootReducer = combineReducers({
layout: layoutSlice,
recipe: recipeSlice,
breadCrumb: breadCrumbSlice.reducer,
search: searchSlice.reducer,
});
const store = configureStore({
reducer: rootReducer,
});
export default store;
import { createSlice } from "@reduxjs/toolkit";
const initialLayoutState = {
layout: "list",
};
const layoutSlice = createSlice({
name: "layout",
initialState: initialLayoutState,
reducers: {
updateLayout(state, { payload }) {
state.layout = payload;
},
},
});
export const LayoutActions = layoutSlice.actions;
export default layoutSlice.reducer;
import { createSlice } from "@reduxjs/toolkit";
const initialRecipeState = {
name: "",
thumbnail_url: "",
instructions: [],
description: "",
};
const recipeSlice = createSlice({
name: "layout",
initialState: initialRecipeState,
reducers: {
updateRecipe(state, { payload }) {
state.name = payload.name;
state.thumbnail_url = payload.thumbnail_url;
state.instructions = payload.instructions;
state.description = payload.description;
},
},
});
export const RecipeActions = recipeSlice.actions;
export default recipeSlice.reducer;
import { createSlice } from "@reduxjs/toolkit";
const initialSearchState = {
query: "",
};
const searchSlice = createSlice({
name: "breadcrumb",
initialState: initialSearchState,
reducers: {
setQuery(state, { payload }) {
state.query = payload;
},
},
});
export const SearchActions = searchSlice.actions;
export default searchSlice;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment