Compare commits
No commits in common. "031df19b440c31a1cb870a7a8eaeb1065c2abdb3" and "90ebdd3a9a152e46ff606f11927e04385231446c" have entirely different histories.
031df19b44
...
90ebdd3a9a
@ -1,4 +1,4 @@
|
|||||||
import { use, useEffect, useState, type ChangeEvent, type FormEvent } from "react";
|
import { useEffect, useState, type ChangeEvent, type FormEvent } from "react";
|
||||||
import type { SearchFilters } from "../../types/search";
|
import type { SearchFilters } from "../../types/search";
|
||||||
import FilterButton from "../buttons/FilterButton";
|
import FilterButton from "../buttons/FilterButton";
|
||||||
import RecipeSearchFilterDropdown from "./RecipeSearchFilterDropdown";
|
import RecipeSearchFilterDropdown from "./RecipeSearchFilterDropdown";
|
||||||
@ -6,7 +6,6 @@ import { SearchRecipes } from "../../services/RecipeService";
|
|||||||
import { isApiError } from "../../types/api/error";
|
import { isApiError } from "../../types/api/error";
|
||||||
import type { Recipe } from "../../types/recipe";
|
import type { Recipe } from "../../types/recipe";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { FilterContext } from "../../context/FilterContext";
|
|
||||||
|
|
||||||
interface RecipeSearchBarProps {
|
interface RecipeSearchBarProps {
|
||||||
// filters: SearchFilters;
|
// filters: SearchFilters;
|
||||||
@ -15,17 +14,20 @@ interface RecipeSearchBarProps {
|
|||||||
searchOnLoad: boolean;
|
searchOnLoad: boolean;
|
||||||
favorites: boolean;
|
favorites: boolean;
|
||||||
setRecipes: React.Dispatch<React.SetStateAction<Recipe[]>> | null;
|
setRecipes: React.Dispatch<React.SetStateAction<Recipe[]>> | null;
|
||||||
|
|
||||||
// Loading is optional
|
|
||||||
loading?: boolean;
|
|
||||||
setLoading?: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RecipeSearchBar({ redirect, searchOnLoad, favorites, setRecipes, loading, setLoading }: RecipeSearchBarProps) {
|
export default function RecipeSearchBar({ redirect, searchOnLoad, favorites, setRecipes }: RecipeSearchBarProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { filters, setFilters } = use(FilterContext);
|
|
||||||
|
|
||||||
const [displayDropdown, setDisplayDropdown] = useState<boolean>(false);
|
const [displayDropdown, setDisplayDropdown] = useState<boolean>(false);
|
||||||
|
const [filters, setFilters] = useState<SearchFilters>({
|
||||||
|
Search: "",
|
||||||
|
MealType: 0,
|
||||||
|
Time: 0,
|
||||||
|
Difficulty: 0,
|
||||||
|
ServingSize: 0,
|
||||||
|
Favorites: favorites
|
||||||
|
});
|
||||||
|
|
||||||
// SERVER FUNCTIONS
|
// SERVER FUNCTIONS
|
||||||
const fetchSearchResults = async () => {
|
const fetchSearchResults = async () => {
|
||||||
@ -33,23 +35,14 @@ export default function RecipeSearchBar({ redirect, searchOnLoad, favorites, set
|
|||||||
await navigate("/v2/web/search");
|
await navigate("/v2/web/search");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const result = await SearchRecipes(filters);
|
||||||
// Should not allow many queries, thought we should allow redirect through loading
|
if (isApiError(result)) {
|
||||||
if (loading) return;
|
console.error(result.message);
|
||||||
if (setLoading) setLoading(true);
|
return;
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await SearchRecipes(filters);
|
|
||||||
if (isApiError(result)) {
|
|
||||||
console.error(result.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setRecipes)
|
|
||||||
setRecipes(result);
|
|
||||||
} finally {
|
|
||||||
if (setLoading) setLoading(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (setRecipes)
|
||||||
|
setRecipes(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// HANDLERS
|
// HANDLERS
|
||||||
@ -76,13 +69,6 @@ export default function RecipeSearchBar({ redirect, searchOnLoad, favorites, set
|
|||||||
void fetchSearchResults();
|
void fetchSearchResults();
|
||||||
}, [searchOnLoad]);
|
}, [searchOnLoad]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setFilters({
|
|
||||||
...filters,
|
|
||||||
Favorites: favorites
|
|
||||||
});
|
|
||||||
}, [favorites]);
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="w-full px-4 my-8" onSubmit={(e) => void searchHandler(e)}>
|
<form className="w-full px-4 my-8" onSubmit={(e) => void searchHandler(e)}>
|
||||||
|
|||||||
@ -1,6 +1,3 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { EngagementViewRecipe } from "../../services/EngagementService";
|
|
||||||
import { isApiError } from "../../types/api/error";
|
|
||||||
import type { Recipe, Tag } from "../../types/recipe"
|
import type { Recipe, Tag } from "../../types/recipe"
|
||||||
|
|
||||||
interface RecipeListItemProps {
|
interface RecipeListItemProps {
|
||||||
@ -29,26 +26,12 @@ function displayTags(tags: Tag[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function RecipeListItem({ recipe }: RecipeListItemProps) {
|
export default function RecipeListItem({ recipe }: RecipeListItemProps) {
|
||||||
const navigate = useNavigate();
|
// TODO: Click event
|
||||||
|
return <>
|
||||||
// HANDLERS
|
|
||||||
const clickHandler = async () => {
|
|
||||||
if (!recipe) return;
|
|
||||||
|
|
||||||
// Navigate first, so it feels faster
|
|
||||||
await navigate(`/v2/web/recipe/${recipe.Id}`);
|
|
||||||
|
|
||||||
const result = await EngagementViewRecipe(recipe.Id);
|
|
||||||
if (isApiError(result)) {
|
|
||||||
console.error(result.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li className="w-full border-b border-gray-300 px-2 py-4 even:bg-gray-50 hover:bg-gray-100 duration-150">
|
<li className="w-full border-b border-gray-300 px-2 py-4 even:bg-gray-50 hover:bg-gray-100 duration-150">
|
||||||
<h2 onClick={() => void clickHandler()} className="text-base md:text-lg hover:text-blue-600 duration-100 cursor-pointer">
|
<p className="text-base md:text-lg hover:text-blue-600 duration-100 cursor-pointer">
|
||||||
{recipe.Title}
|
{recipe.Title}
|
||||||
</h2>
|
</p>
|
||||||
<p className="hidden md:block text-sm text-gray-700 my-1.5">
|
<p className="hidden md:block text-sm text-gray-700 my-1.5">
|
||||||
Difficulty: <span className="font-semibold">{displayDifficulty(recipe.Difficulty)}</span>
|
Difficulty: <span className="font-semibold">{displayDifficulty(recipe.Difficulty)}</span>
|
||||||
{" "} | Duration: <span className="font-semibold">{recipe.Duration.Total} min</span>
|
{" "} | Duration: <span className="font-semibold">{recipe.Duration.Total} min</span>
|
||||||
@ -69,5 +52,5 @@ export default function RecipeListItem({ recipe }: RecipeListItemProps) {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
import { createContext } from "react";
|
|
||||||
import type { SearchFilters } from "../types/search";
|
|
||||||
|
|
||||||
interface FilterContextType {
|
|
||||||
filters: SearchFilters;
|
|
||||||
setFilters: (filters: SearchFilters) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FilterContext = createContext<FilterContextType>({
|
|
||||||
filters: {
|
|
||||||
Search: "",
|
|
||||||
MealType: 0,
|
|
||||||
Time: 0,
|
|
||||||
Difficulty: 0,
|
|
||||||
ServingSize: 0,
|
|
||||||
Favorites: false,
|
|
||||||
},
|
|
||||||
setFilters: () => { return },
|
|
||||||
});
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import { useEffect, useState, type ReactNode } from "react";
|
|
||||||
import { FilterContext } from "./FilterContext";
|
|
||||||
import type { SearchFilters } from "../types/search";
|
|
||||||
|
|
||||||
const STORE_KEY = "potion_app_search_filters";
|
|
||||||
|
|
||||||
const DEFAULT_FILTERS: SearchFilters = {
|
|
||||||
Search: "",
|
|
||||||
MealType: 0,
|
|
||||||
Time: 0,
|
|
||||||
Difficulty: 0,
|
|
||||||
ServingSize: 0,
|
|
||||||
Favorites: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function FilterProvider({ children }: { children: ReactNode }) {
|
|
||||||
const [filters, setFilters] = useState<SearchFilters>(() => {
|
|
||||||
// Window would not be found, something is wrong with the browser
|
|
||||||
if (typeof window === "undefined") return DEFAULT_FILTERS;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stored = window.localStorage.getItem(STORE_KEY);
|
|
||||||
return stored ? (JSON.parse(stored) as SearchFilters) : DEFAULT_FILTERS;
|
|
||||||
} catch {
|
|
||||||
return DEFAULT_FILTERS;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
try {
|
|
||||||
window.localStorage.setItem(STORE_KEY, JSON.stringify(filters));
|
|
||||||
} catch {
|
|
||||||
// TODO: Error here?
|
|
||||||
// ignore quota / access errors
|
|
||||||
}
|
|
||||||
}, [filters]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FilterContext value={{ filters, setFilters }}>
|
|
||||||
{children}
|
|
||||||
</FilterContext>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -5,7 +5,6 @@ import App from './App.tsx'
|
|||||||
import { AuthProvider } from './context/AuthProvider.tsx'
|
import { AuthProvider } from './context/AuthProvider.tsx'
|
||||||
import { CookiesProvider } from 'react-cookie'
|
import { CookiesProvider } from 'react-cookie'
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { FilterProvider } from './context/FilterProvider.tsx'
|
|
||||||
|
|
||||||
// Set the with 'withCredentials' by default
|
// Set the with 'withCredentials' by default
|
||||||
axios.defaults.withCredentials = true;
|
axios.defaults.withCredentials = true;
|
||||||
@ -14,9 +13,7 @@ createRoot(document.getElementById('root')!).render(
|
|||||||
<StrictMode>
|
<StrictMode>
|
||||||
<CookiesProvider>
|
<CookiesProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<FilterProvider>
|
<App />
|
||||||
<App />
|
|
||||||
</FilterProvider>
|
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</CookiesProvider>
|
</CookiesProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
|
|||||||
@ -4,31 +4,19 @@ import RecipeSearchBar from "../components/inputs/RecipeSearchBar";
|
|||||||
|
|
||||||
import type { Recipe } from "../types/recipe";
|
import type { Recipe } from "../types/recipe";
|
||||||
import RecipeSearchResult from "../components/items/RecipeSearchResult";
|
import RecipeSearchResult from "../components/items/RecipeSearchResult";
|
||||||
import Spinner from "../components/Spinner";
|
|
||||||
|
|
||||||
export default function Favorites() {
|
export default function Favorites() {
|
||||||
const [recipes, setRecipes] = useState<Recipe[]>([]);
|
const [recipes, setRecipes] = useState<Recipe[]>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Banner content="Favorites" />
|
<Banner content="Favorites" />
|
||||||
<RecipeSearchBar redirect={false} searchOnLoad={true} favorites={true} setRecipes={setRecipes} loading={loading} setLoading={setLoading} />
|
<RecipeSearchBar redirect={false} searchOnLoad={true} favorites={true} setRecipes={setRecipes}/>
|
||||||
<hr className="text-gray-300 w-full" />
|
<hr className="text-gray-300 w-full" />
|
||||||
|
|
||||||
<div className="flex flex-col w-full p-4 items-center">
|
<div className="flex flex-col w-full p-4 items-center">
|
||||||
{loading && (
|
{recipes?.map(recipe => <RecipeSearchResult key={recipe.Id} recipe={recipe} />)}
|
||||||
<div className="w-full flex items-center justify-center gap-x-2 py-4">
|
<p className="text-gray-700 text-sm py-4">{recipes ? "End of results" : "No results"}</p>
|
||||||
<Spinner content="Loading recipes..." />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!loading && (
|
|
||||||
<>
|
|
||||||
{recipes?.map(recipe => <RecipeSearchResult key={recipe.Id} recipe={recipe} />)}
|
|
||||||
<p className="text-gray-700 text-sm py-4">{recipes ? "End of results" : "No reuslts"}</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { GetRecipeOfTheWeek } from "../services/RecipeService";
|
|||||||
import { isApiError, type ApiError } from "../types/api/error";
|
import { isApiError, type ApiError } from "../types/api/error";
|
||||||
import { AuthContext } from "../context/AuthContext";
|
import { AuthContext } from "../context/AuthContext";
|
||||||
import { GetAuthenticatedUserMadeRecipes, GetAuthenticateUserViewedRecipes } from "../services/UserService";
|
import { GetAuthenticatedUserMadeRecipes, GetAuthenticateUserViewedRecipes } from "../services/UserService";
|
||||||
|
import { type SearchFilters } from "../types/search";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
// Context
|
// Context
|
||||||
@ -25,6 +26,7 @@ export default function Home() {
|
|||||||
|
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
|
// TODO: Fetch other items when needed
|
||||||
// Fetch the recipe of the week
|
// Fetch the recipe of the week
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetch() {
|
async function fetch() {
|
||||||
|
|||||||
@ -170,7 +170,7 @@ export default function Profile() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
{/* Logout Section */}
|
{/* Logout Section TODO: Click event*/}
|
||||||
<section className="w-full flex flex-col justify-center items-center py-8 border-t border-gray-300 mt-auto">
|
<section className="w-full flex flex-col justify-center items-center py-8 border-t border-gray-300 mt-auto">
|
||||||
<button onClick={logoutHandler} className="text-center border border-red-500 text-red-500 w-9/10 md:w-1/3 py-2 rounded-lg hover:cursor-pointer hover:bg-red-100 duration-300">
|
<button onClick={logoutHandler} className="text-center border border-red-500 text-red-500 w-9/10 md:w-1/3 py-2 rounded-lg hover:cursor-pointer hover:bg-red-100 duration-300">
|
||||||
Logout
|
Logout
|
||||||
|
|||||||
@ -3,30 +3,19 @@ import Banner from "../components/Banner";
|
|||||||
import RecipeSearchBar from "../components/inputs/RecipeSearchBar";
|
import RecipeSearchBar from "../components/inputs/RecipeSearchBar";
|
||||||
import { type Recipe } from "../types/recipe";
|
import { type Recipe } from "../types/recipe";
|
||||||
import RecipeSearchResult from "../components/items/RecipeSearchResult";
|
import RecipeSearchResult from "../components/items/RecipeSearchResult";
|
||||||
import Spinner from "../components/Spinner";
|
|
||||||
|
|
||||||
export default function SearchPage() {
|
export default function SearchPage() {
|
||||||
const [recipes, setRecipes] = useState<Recipe[]>([]);
|
const [recipes, setRecipes] = useState<Recipe[]>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Banner content="Recipe Search" />
|
<Banner content="Recipe Search" />
|
||||||
<RecipeSearchBar redirect={false} searchOnLoad={true} favorites={false} setRecipes={setRecipes} loading={loading} setLoading={setLoading} />
|
<RecipeSearchBar redirect={false} searchOnLoad={true} favorites={false} setRecipes={setRecipes} />
|
||||||
<hr className="text-gray-300 w-full" />
|
<hr className="text-gray-300 w-full" />
|
||||||
|
|
||||||
<div className="flex flex-col w-full p-4 items-center">
|
<div className="flex flex-col w-full p-4 items-center">
|
||||||
{loading && (
|
{recipes?.map(recipe => <RecipeSearchResult key={recipe.Id} recipe={recipe} />)}
|
||||||
<div className="w-full flex items-center justify-center gap-x-2 py-4">
|
<p className="text-gray-700 text-sm py-4">{recipes ? "End of results" : "No reuslts"}</p>
|
||||||
<Spinner content="Loading recipes..." />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!loading && (
|
|
||||||
<>
|
|
||||||
{recipes?.map(recipe => <RecipeSearchResult key={recipe.Id} recipe={recipe} />)}
|
|
||||||
<p className="text-gray-700 text-sm py-4">{recipes ? "End of results" : "No reuslts"}</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export interface RecipeDuration {
|
|||||||
Cook: number;
|
Cook: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BUG: This might need to be integers? Not sure yet.
|
||||||
export type RecipeMeal = "breakfast" | "lunch" | "dinner" | "dessert" | "snack" | "side" | "other";
|
export type RecipeMeal = "breakfast" | "lunch" | "dinner" | "dessert" | "snack" | "side" | "other";
|
||||||
|
|
||||||
export interface RecipeIngredient {
|
export interface RecipeIngredient {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user