Compare commits
1 Commits
master
...
feature/or
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30a5396d11 |
@ -20,13 +20,8 @@
|
|||||||
go
|
go
|
||||||
gopls
|
gopls
|
||||||
go-tools
|
go-tools
|
||||||
htmx-lsp2
|
|
||||||
templ
|
|
||||||
tailwindcss_4
|
tailwindcss_4
|
||||||
tailwindcss-language-server
|
tailwindcss-language-server
|
||||||
watchman
|
|
||||||
docker-language-server
|
|
||||||
dockerfile-language-server-nodejs
|
|
||||||
gcc_multi
|
gcc_multi
|
||||||
glibc_multi
|
glibc_multi
|
||||||
nodejs
|
nodejs
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@ -31,13 +32,12 @@ func (s *Server) GoogleCallbackHandlerV2(ctx *gin.Context) {
|
|||||||
domain := s.deps.EnvironmentConfig.FrontendDomain
|
domain := s.deps.EnvironmentConfig.FrontendDomain
|
||||||
|
|
||||||
if jwt, err := s.deps.AuthService.GoogleAuthSuccess(state, code); err != nil {
|
if jwt, err := s.deps.AuthService.GoogleAuthSuccess(state, code); err != nil {
|
||||||
redirectUrl := fmt.Sprintf("%s/v2/web/login?error=%s", domain, url.QueryEscape(err.Error()))
|
url := fmt.Sprintf("%s/v2/web/login?error=%s", domain, url.QueryEscape(err.Error()))
|
||||||
ctx.Redirect(http.StatusSeeOther, redirectUrl)
|
ctx.Redirect(http.StatusSeeOther, url)
|
||||||
} else {
|
} else {
|
||||||
// Pass JWT via query param - frontend will set the cookie
|
url := fmt.Sprintf("%s/v2/web/home", domain)
|
||||||
// This bypasses cross-origin cookie issues with Cloudflare/proxies
|
s.SetCookie(ctx, "jwt_token", jwt, time.Hour*24*7)
|
||||||
redirectUrl := fmt.Sprintf("%s/v2/web/auth/callback?token=%s", domain, url.QueryEscape(jwt))
|
ctx.Redirect(http.StatusSeeOther, url)
|
||||||
ctx.Redirect(http.StatusSeeOther, redirectUrl)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,8 +43,8 @@ func (s *Server) SetCookie(ctx *gin.Context, name, value string, duration time.D
|
|||||||
value,
|
value,
|
||||||
maxAge,
|
maxAge,
|
||||||
path,
|
path,
|
||||||
"gophernest.net",
|
".gophernest.net", // or your backend domain / parent
|
||||||
true,
|
true, // secure
|
||||||
httpOnly,
|
httpOnly,
|
||||||
)
|
)
|
||||||
case "dev":
|
case "dev":
|
||||||
|
|||||||
@ -210,19 +210,41 @@ func (r *RecipeRepository) DeleteRecipe(recipeId, userId int) error {
|
|||||||
// This function will only return recipes that are not deleted. Any recipes marked deleted will be ignored
|
// This function will only return recipes that are not deleted. Any recipes marked deleted will be ignored
|
||||||
// and the standard "not-found" error will be returned.
|
// and the standard "not-found" error will be returned.
|
||||||
func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error) {
|
func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error) {
|
||||||
query := `SELECT
|
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
||||||
id, title, description, instructions, serves, difficulty, duration, category, ingredients,
|
|
||||||
userid, modified, created, deleted
|
query := psql.
|
||||||
FROM recipes
|
Select(
|
||||||
WHERE id = $1 AND deleted = false;
|
"id",
|
||||||
`
|
"title",
|
||||||
|
"description",
|
||||||
|
"instructions",
|
||||||
|
"serves",
|
||||||
|
"difficulty",
|
||||||
|
"duration",
|
||||||
|
"category",
|
||||||
|
"ingredients",
|
||||||
|
"userid",
|
||||||
|
"modified",
|
||||||
|
"created",
|
||||||
|
"deleted",
|
||||||
|
).
|
||||||
|
From("recipes").
|
||||||
|
Where(sq.Eq{
|
||||||
|
"id": id,
|
||||||
|
"deleted": false,
|
||||||
|
})
|
||||||
|
|
||||||
|
_sql, args, err := query.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to construct sql query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
var durationBytes []byte
|
var durationBytes []byte
|
||||||
var instructions pq.StringArray
|
var instructions pq.StringArray
|
||||||
var ingredientBytes []byte
|
var ingredientBytes []byte
|
||||||
|
|
||||||
var recipe domain.Recipe
|
var recipe domain.Recipe
|
||||||
if err := r.db.QueryRow(query, id).Scan(
|
if err := r.db.QueryRowx(_sql, args...).Scan(
|
||||||
&recipe.Id,
|
&recipe.Id,
|
||||||
&recipe.Title,
|
&recipe.Title,
|
||||||
&recipe.Description,
|
&recipe.Description,
|
||||||
@ -504,51 +526,64 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *i
|
|||||||
// this function successfully, therefore, it must be an existing recipe. Any errors will be bubbled
|
// this function successfully, therefore, it must be an existing recipe. Any errors will be bubbled
|
||||||
// to the caller.
|
// to the caller.
|
||||||
func (r *RecipeRepository) CreateRecipeTags(recipe domain.Recipe, tags []string) error {
|
func (r *RecipeRepository) CreateRecipeTags(recipe domain.Recipe, tags []string) error {
|
||||||
tx, err := r.db.Begin()
|
tx, err := r.db.Beginx()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
// Normalize the tag names (lower case with trimmed space)
|
psql := sq.StatementBuilder.
|
||||||
normalized := make(map[string]struct{}) // Use map to disallow duplicates
|
PlaceholderFormat(sq.Dollar).
|
||||||
|
RunWith(tx)
|
||||||
|
|
||||||
|
// Normalize tags (lowercase, trimmed, no duplicates)
|
||||||
|
normalized := make(map[string]struct{})
|
||||||
for _, tag := range tags {
|
for _, tag := range tags {
|
||||||
normalized[strings.ToLower(strings.TrimSpace(tag))] = struct{}{}
|
t := strings.ToLower(strings.TrimSpace(tag))
|
||||||
|
if t != "" {
|
||||||
|
normalized[t] = struct{}{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert the tags into the DB and return their IDS into the tag ID list
|
// Insert tags and collect IDs
|
||||||
var tagIds []int
|
var tagIDs []int
|
||||||
for tag := range normalized {
|
for tag := range normalized {
|
||||||
var tagId int
|
var tagID int
|
||||||
|
|
||||||
query := `
|
_sql, args, err := psql.
|
||||||
INSERT INTO tags (name) VALUES ($1)
|
Insert("tags").
|
||||||
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
|
Columns("name").
|
||||||
RETURNING id;
|
Values(tag).
|
||||||
`
|
Suffix("ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name RETURNING id").
|
||||||
|
ToSql()
|
||||||
err := tx.QueryRow(query, tag).Scan(&tagId)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to retrieve or create tag. %s\n", err.Error())
|
return fmt.Errorf("failed to build tag insert query: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tagIds = append(tagIds, tagId)
|
if err = tx.QueryRowx(_sql, args...).Scan(&tagID); err != nil {
|
||||||
|
return fmt.Errorf("failed to retrieve or create tag: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Using a prepared statement, execute the mapping insertions one-by-one
|
tagIDs = append(tagIDs, tagID)
|
||||||
for _, id := range tagIds {
|
}
|
||||||
stmt, err := tx.Prepare("INSERT INTO RecipeTags (RecipeId, TagId) VALUES ($1, $2);")
|
|
||||||
|
// Insert recipe <-> tag mappings
|
||||||
|
for _, tagID := range tagIDs {
|
||||||
|
_sql, args, err := psql.
|
||||||
|
Insert("RecipeTags").
|
||||||
|
Columns("RecipeId", "TagId").
|
||||||
|
Values(recipe.Id, tagID).
|
||||||
|
ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to create statement for recipe tag mapping. %s\n", err.Error())
|
return fmt.Errorf("failed to build recipe tag mapping query: %w", err)
|
||||||
}
|
}
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
if _, err := stmt.Exec(recipe.Id, id); err != nil {
|
if _, err = tx.Exec(_sql, args...); err != nil {
|
||||||
return fmt.Errorf("Failed to insert tag-recipe-mapping. %s\n", err.Error())
|
return fmt.Errorf("failed to insert recipe tag mapping: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
if err = tx.Commit(); err != nil {
|
||||||
tx.Rollback()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -560,75 +595,88 @@ func (r *RecipeRepository) CreateRecipeTags(recipe domain.Recipe, tags []string)
|
|||||||
// and creates new associations for the provided tags. The recipe object must contain
|
// and creates new associations for the provided tags. The recipe object must contain
|
||||||
// a valid ID. Any errors will be bubbled to the caller.
|
// a valid ID. Any errors will be bubbled to the caller.
|
||||||
func (r *RecipeRepository) UpdateRecipeTags(recipe domain.Recipe, tags []string) error {
|
func (r *RecipeRepository) UpdateRecipeTags(recipe domain.Recipe, tags []string) error {
|
||||||
tx, err := r.db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer tx.Rollback() // Rollback if we don't commit
|
|
||||||
|
|
||||||
if recipe.Id <= 0 {
|
if recipe.Id <= 0 {
|
||||||
return fmt.Errorf("[ERROR] Recipe must have a valid ID")
|
return fmt.Errorf("[ERROR] Recipe must have a valid ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Delete all existing tag associations for this recipe
|
tx, err := r.db.Beginx()
|
||||||
deleteQuery := `DELETE FROM RecipeTags WHERE RecipeId = $1;`
|
if err != nil {
|
||||||
if _, err := tx.Exec(deleteQuery, recipe.Id); err != nil {
|
return err
|
||||||
return fmt.Errorf("[ERROR] Failed to delete existing recipe tags: %w", err)
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
psql := sq.StatementBuilder.
|
||||||
|
PlaceholderFormat(sq.Dollar).
|
||||||
|
RunWith(tx)
|
||||||
|
|
||||||
|
// Step 1: delete existing tag mappings
|
||||||
|
{
|
||||||
|
_sql, args, err := psql.
|
||||||
|
Delete("RecipeTags").
|
||||||
|
Where(sq.Eq{"RecipeId": recipe.Id}).
|
||||||
|
ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("[ERROR] failed to build delete recipe tags query: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Normalize the tag names (lower case with trimmed space)
|
if _, err = tx.Exec(_sql, args...); err != nil {
|
||||||
normalized := make(map[string]struct{}) // Use map to disallow duplicates
|
return fmt.Errorf("[ERROR] failed to delete existing recipe tags: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: normalize tags
|
||||||
|
normalized := make(map[string]struct{})
|
||||||
for _, tag := range tags {
|
for _, tag := range tags {
|
||||||
trimmed := strings.ToLower(strings.TrimSpace(tag))
|
t := strings.ToLower(strings.TrimSpace(tag))
|
||||||
if trimmed != "" {
|
if t != "" {
|
||||||
normalized[trimmed] = struct{}{}
|
normalized[t] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no tags provided, we're done (all tags removed)
|
// No tags means "remove all tags" — we’re done
|
||||||
if len(normalized) == 0 {
|
if len(normalized) == 0 {
|
||||||
if err := tx.Commit(); err != nil {
|
return tx.Commit()
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Insert the tags into the DB and return their IDs into the tag ID list
|
// Step 3: upsert tags and collect IDs
|
||||||
var tagIds []int
|
var tagIDs []int
|
||||||
for tag := range normalized {
|
for tag := range normalized {
|
||||||
var tagId int
|
var tagID int
|
||||||
query := `
|
|
||||||
INSERT INTO tags (name) VALUES ($1)
|
_sql, args, err := psql.
|
||||||
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
|
Insert("tags").
|
||||||
RETURNING id;
|
Columns("name").
|
||||||
`
|
Values(tag).
|
||||||
err := tx.QueryRow(query, tag).Scan(&tagId)
|
Suffix("ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name RETURNING id").
|
||||||
|
ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[ERROR] Failed to retrieve or create tag: %w", err)
|
return fmt.Errorf("[ERROR] failed to build tag upsert query: %w", err)
|
||||||
}
|
|
||||||
tagIds = append(tagIds, tagId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Insert the new tag associations
|
if err = tx.QueryRowx(_sql, args...).Scan(&tagID); err != nil {
|
||||||
// Use a single prepared statement for all inserts
|
return fmt.Errorf("[ERROR] failed to retrieve or create tag: %w", err)
|
||||||
stmt, err := tx.Prepare("INSERT INTO RecipeTags (RecipeId, TagId) VALUES ($1, $2);")
|
}
|
||||||
|
|
||||||
|
tagIDs = append(tagIDs, tagID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: insert new recipe ↔ tag mappings
|
||||||
|
for _, tagID := range tagIDs {
|
||||||
|
_sql, args, err := psql.
|
||||||
|
Insert("RecipeTags").
|
||||||
|
Columns("RecipeId", "TagId").
|
||||||
|
Values(recipe.Id, tagID).
|
||||||
|
ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[ERROR] Failed to create statement for recipe tag mapping: %w", err)
|
return fmt.Errorf("[ERROR] failed to build recipe tag mapping query: %w", err)
|
||||||
}
|
}
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
for _, id := range tagIds {
|
if _, err = tx.Exec(_sql, args...); err != nil {
|
||||||
if _, err := stmt.Exec(recipe.Id, id); err != nil {
|
return fmt.Errorf("[ERROR] failed to insert recipe tag mapping: %w", err)
|
||||||
return fmt.Errorf("[ERROR] Failed to insert tag-recipe mapping: %w", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit the transaction
|
return tx.Commit()
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserRecipes gets a list of a users owned recipes. This function does not ensure the user is
|
// GetUserRecipes gets a list of a users owned recipes. This function does not ensure the user is
|
||||||
@ -709,28 +757,21 @@ func (r *RecipeRepository) GetRecipeTags(recipe *domain.Recipe) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
||||||
|
query := psql.
|
||||||
|
Select("t.*").
|
||||||
|
From("tags t").
|
||||||
|
Join("recipetags rt on rt.tagid = t.id").
|
||||||
|
Where(sq.Eq{"rt.recipeid": recipe.Id})
|
||||||
|
|
||||||
|
_sql, args, err := query.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to construct sql query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
recipe.Tags = []domain.Tag{}
|
recipe.Tags = []domain.Tag{}
|
||||||
|
if err := r.db.Select(&recipe.Tags, _sql, args...); err != nil {
|
||||||
query := `
|
return fmt.Errorf("Failed to get recipe tags: %w", err)
|
||||||
SELECT t.* FROM tags t
|
|
||||||
JOIN recipetags rt ON rt.tagid = t.id
|
|
||||||
WHERE rt.recipeid = $1;
|
|
||||||
`
|
|
||||||
rows, err := r.db.Query(query, recipe.Id)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to get tags for recipe. %s\n", err.Error())
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var tag domain.Tag
|
|
||||||
|
|
||||||
err := rows.Scan(&tag.Id, &tag.Name, &tag.Created)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to scan tag onto domain model. %s\n", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
recipe.Tags = append(recipe.Tags, tag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import { use, type ReactNode } from 'react';
|
|||||||
import { AuthContext } from './context/AuthContext';
|
import { AuthContext } from './context/AuthContext';
|
||||||
import RecipePage from './pages/Recipe';
|
import RecipePage from './pages/Recipe';
|
||||||
import SearchPage from './pages/Search';
|
import SearchPage from './pages/Search';
|
||||||
import AuthCallback from './pages/AuthCallback';
|
|
||||||
|
|
||||||
function ProtectedRoute({ children }: { children: ReactNode }) {
|
function ProtectedRoute({ children }: { children: ReactNode }) {
|
||||||
const { isLoggedIn } = use(AuthContext)
|
const { isLoggedIn } = use(AuthContext)
|
||||||
@ -38,9 +37,6 @@ function App() {
|
|||||||
{/* Login page does not inherit WebLayout */}
|
{/* Login page does not inherit WebLayout */}
|
||||||
<Route path="/v2/web/login" element={<LoginPage />} />
|
<Route path="/v2/web/login" element={<LoginPage />} />
|
||||||
|
|
||||||
{/* Auth callback - handles token from OAuth redirect */}
|
|
||||||
<Route path="/v2/web/auth/callback" element={<AuthCallback />} />
|
|
||||||
|
|
||||||
<Route path="/v2/web" element={<WebLayout />}>
|
<Route path="/v2/web" element={<WebLayout />}>
|
||||||
<Route index element={<Navigate to={ROUTE_CONSTANTS.Home} replace />} />
|
<Route index element={<Navigate to={ROUTE_CONSTANTS.Home} replace />} />
|
||||||
<Route path="home" element={<Home />} />
|
<Route path="home" element={<Home />} />
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import type { SearchFilters } from "../types/search";
|
|||||||
interface FilterContextType {
|
interface FilterContextType {
|
||||||
filters: SearchFilters;
|
filters: SearchFilters;
|
||||||
setFilters: (filters: SearchFilters) => void;
|
setFilters: (filters: SearchFilters) => void;
|
||||||
resetFilters: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FilterContext = createContext<FilterContextType>({
|
export const FilterContext = createContext<FilterContextType>({
|
||||||
@ -17,5 +16,4 @@ export const FilterContext = createContext<FilterContextType>({
|
|||||||
Favorites: false,
|
Favorites: false,
|
||||||
},
|
},
|
||||||
setFilters: () => { return },
|
setFilters: () => { return },
|
||||||
resetFilters: () => { return },
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export function FilterProvider({ children }: { children: ReactNode }) {
|
|||||||
}, [filters]);
|
}, [filters]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterContext value={{ filters, setFilters, resetFilters: () => setFilters(DEFAULT_FILTERS) }}>
|
<FilterContext value={{ filters, setFilters }}>
|
||||||
{children}
|
{children}
|
||||||
</FilterContext>
|
</FilterContext>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { useSearchParams, useNavigate } from "react-router-dom";
|
|
||||||
import { useCookies } from "react-cookie";
|
|
||||||
|
|
||||||
export default function AuthCallback() {
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [, setCookie] = useCookies(["jwt_token"]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const token = searchParams.get("token");
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
// Set cookie with 7 day expiration, accessible across all subdomains
|
|
||||||
setCookie("jwt_token", token, {
|
|
||||||
path: "/",
|
|
||||||
domain: "gophernest.net", // shared across all subdomains
|
|
||||||
maxAge: 60 * 60 * 24 * 7, // 7 days in seconds
|
|
||||||
secure: true,
|
|
||||||
sameSite: "lax",
|
|
||||||
});
|
|
||||||
void navigate("/v2/web/home", { replace: true });
|
|
||||||
} else {
|
|
||||||
// No token provided, redirect to login
|
|
||||||
void navigate("/v2/web/login", { replace: true });
|
|
||||||
}
|
|
||||||
}, [searchParams, setCookie, navigate]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@ -12,13 +12,10 @@ 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 { useNavigate } from "react-router-dom";
|
|
||||||
import { FilterContext } from "../context/FilterContext";
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
// Context
|
// Context
|
||||||
const { isLoggedIn } = use(AuthContext);
|
const { isLoggedIn } = use(AuthContext);
|
||||||
const { resetFilters } = use(FilterContext);
|
|
||||||
|
|
||||||
// Page state
|
// Page state
|
||||||
const [recipeOfTheWeek, setRecipeOfTheWeek] = useState<Recipe | null>(null);
|
const [recipeOfTheWeek, setRecipeOfTheWeek] = useState<Recipe | null>(null);
|
||||||
@ -27,7 +24,6 @@ export default function Home() {
|
|||||||
const [viewedRecipes, setViewedRecipes] = useState<Recipe[]>([]);
|
const [viewedRecipes, setViewedRecipes] = useState<Recipe[]>([]);
|
||||||
|
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
// Fetch the recipe of the week
|
// Fetch the recipe of the week
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -59,12 +55,6 @@ export default function Home() {
|
|||||||
void fetch();
|
void fetch();
|
||||||
}, [isLoggedIn]);
|
}, [isLoggedIn]);
|
||||||
|
|
||||||
const viewAllRecipesHandler = () => {
|
|
||||||
// Clear filters
|
|
||||||
resetFilters();
|
|
||||||
void navigate(ROUTE_CONSTANTS.Search);
|
|
||||||
}
|
|
||||||
|
|
||||||
// BUG: Prob remove
|
// BUG: Prob remove
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error)
|
if (error)
|
||||||
@ -100,11 +90,8 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="leading-relaxed text-gray-800">
|
<p className="leading-relaxed text-gray-800">
|
||||||
Not sure what you want? {" "}
|
Not sure what you want? {" "}
|
||||||
<button onClick={viewAllRecipesHandler} className="text-blue-500 underline hover:text-blue-700 duration-300 cursor-pointer">
|
|
||||||
View all recipes here
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a href={ROUTE_CONSTANTS.Search} className="text-blue-500 underline hover:text-blue-700 duration-300">
|
<a href={ROUTE_CONSTANTS.Search} className="text-blue-500 underline hover:text-blue-700 duration-300">
|
||||||
|
View all recipes here
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user