Commit ce10b840 authored by Aneeb Imamdin's avatar Aneeb Imamdin

First commit

parent 693fe1ed
This diff is collapsed.
......@@ -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