Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions client/src/components/layout/MobileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,7 @@ export function MobileHeader({ query }: MobileHeaderProps) {
}
}}
>
<SearchBar
key={query}
initialQuery={query}
inputId="mobile-search"
autoFocus
/>
<SearchBar initialQuery={query} inputId="mobile-search" autoFocus />
</div>
) : (
<>
Expand Down
33 changes: 31 additions & 2 deletions client/src/components/layout/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, type SubmitEvent } from 'react';
import { useNavigate } from 'react-router';
import { useState, useEffect, type SubmitEvent } from 'react';
import { useNavigate, useLocation } from 'react-router';

type SearchBarProps = {
initialQuery: string;
Expand All @@ -17,6 +17,34 @@ export default function SearchBar({
}: SearchBarProps) {
const [input, setInput] = useState(initialQuery);
const navigate = useNavigate();
const location = useLocation();

//oppdaterer søket når URL forandes med feks. tilbakeknapp
const [prevQuery, setPrevQuery] = useState(initialQuery);
if (initialQuery !== prevQuery) {
setPrevQuery(initialQuery);
setInput(initialQuery);
}

// søker av seg selv litt etter man slutter å skrive
useEffect(() => {
const trimmed = input.trim();
// søker ikke på nytt hvis søkteksten matcher urlen
if (trimmed === initialQuery) return;

const timer = setTimeout(() => {
// tomt felt viser alle oppskrifter
const url = trimmed
? `/recipes?q=${encodeURIComponent(trimmed)}`
: '/recipes';
// erstatter historikken så tilbakeknappen ikke går gjennom hver bokstav
navigate(url, { replace: location.pathname === '/recipes' });
// forandre her hvis vi skal forandre hvor fort autosøk skjer
}, 400);

// avbryter hvis man skriver mer før tiden er ute
return () => clearTimeout(timer);
}, [input, initialQuery, navigate, location.pathname]);

function handleSubmit(event: SubmitEvent<HTMLFormElement>) {
// Hindrer at skjemaet laster siden på nytt
Expand Down Expand Up @@ -69,6 +97,7 @@ export default function SearchBar({
onChange={(event) => setInput(event.target.value)}
placeholder="Søk etter oppskrift"
autoFocus={autoFocus}
autoComplete="off"
className="min-w-0 flex-1 bg-transparent py-2 pr-4 pl-1 text-sm placeholder:text-text/70 focus:outline-none"
/>
</form>
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/layout/StickyHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function StickyHeader({ visible, query }: StickyHeaderProps) {
<NavMenu compact />

<div className="ml-auto flex items-center gap-4">
<SearchBar key={query} initialQuery={query} inputId="sticky-search" />
<SearchBar initialQuery={query} inputId="sticky-search" />
<NewRecipeButton trigger={newRecipeButton} />
<UserLink />
</div>
Expand Down
7 changes: 1 addition & 6 deletions client/src/components/layout/TopHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,7 @@ export function TopHeader({ query, scrolled }: TopHeaderProps) {
}
}}
>
<SearchBar
key={query}
initialQuery={query}
inputId="top-search"
autoFocus
/>
<SearchBar initialQuery={query} inputId="top-search" autoFocus />
</div>
) : (
<button
Expand Down
78 changes: 43 additions & 35 deletions client/src/graphql/recipes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,54 @@ import { gql, type TypedDocumentNode } from '@apollo/client';
import type { Recipe } from '../types/recipe';

type GetRecipesData = { recipes: Recipe[] };
type GetRecipesVariables = { page?: number ; pageSize?: number };
type GetRecipesVariables = {
search?: string;
page?: number;
pageSize?: number;
};

type GetRecipeData = { recipe: Recipe | null };
type GetRecipeVariables = { id: string };

export const GET_RECIPES: TypedDocumentNode<GetRecipesData, GetRecipesVariables> = gql`
query GetRecipes($page: Int, $pageSize: Int) {
recipes(page: $page, pageSize: $pageSize) {
id
imageUrl
title
prepTimeMinutes
servings
}
export const GET_RECIPES: TypedDocumentNode<
GetRecipesData,
GetRecipesVariables
> = gql`
query GetRecipes($search: String, $page: Int, $pageSize: Int) {
recipes(search: $search, page: $page, pageSize: $pageSize) {
id
imageUrl
title
prepTimeMinutes
servings
}
`
export const GET_RECIPE: TypedDocumentNode<GetRecipeData, GetRecipeVariables> = gql`
}
`;
export const GET_RECIPE: TypedDocumentNode<GetRecipeData, GetRecipeVariables> =
gql`
query GetRecipe($id: ID!) {
recipe(id: $id) {
id
title
description
prepTimeMinutes
servings
imageUrl
tags {
id
name
}
ingredients {
id
amount
unit
name
}
steps{
id
order
instruction
}
recipe(id: $id) {
id
title
description
prepTimeMinutes
servings
imageUrl
tags {
id
name
}
ingredients {
id
amount
unit
name
}
steps {
id
order
instruction
}
}
}
`
`;
30 changes: 22 additions & 8 deletions client/src/routes/RecipeListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,42 @@ import { useState } from 'react';
const PAGE_SIZE = 12;

// Listen over alle oppskrifter eller søksresultatet
// Selve søkefeltet ligger i headeren. Resultatene kobles til senere.
// Selve søkefeltet ligger i headeren
export default function RecipeListPage() {
const [searchParams] = useSearchParams();
const query = searchParams.get('q') ?? '';

const { data, loading, error, fetchMore } = useQuery(GET_RECIPES, {
variables: { page: 1, pageSize: PAGE_SIZE },
variables: { search: query || undefined, page: 1, pageSize: PAGE_SIZE },
});

const [hasMore, setHasMore] = useState(true);

// nullstiller når man søker på noe nytt
const [prevQuery, setPrevQuery] = useState(query);
if (query !== prevQuery) {
setPrevQuery(query);
setHasMore(true);
}

if (loading && !data) {
return <p role="status">Laster oppskrifter...</p>
return <p role="status">Laster oppskrifter...</p>;
}
if(error) {
return <p role="alert">Kunne ikke hente oppskriftene. Prøv igjen senere.</p>
if (error) {
return (
<p role="alert">Kunne ikke hente oppskriftene. Prøv igjen senere.</p>
);
}

const recipes = data?.recipes ?? [];

async function handleLoadMore() {
// Regner ut hvilke side som er neste.
// Regner ut hvilke side som er neste.
const nextPage = Math.floor(recipes.length / PAGE_SIZE) + 1;

// Henter neste side
const result = await fetchMore({
variables: {page: nextPage},
variables: { page: nextPage },
// Legger de nye oppskriftene etter de gamle
updateQuery: (previous, { fetchMoreResult }) => ({
recipes: [...previous.recipes, ...fetchMoreResult.recipes],
Expand All @@ -50,7 +59,12 @@ export default function RecipeListPage() {
<>
<h1 className="font-heading text-4xl md:text-5xl">Oppskrifter</h1>

{query && <p className="mt-4">Alt med «{query}»</p>}
{query && recipes.length > 0 && <p className="mt-4">Alt med '{query}'</p>}

{/* melding hvis søket ikke finner noe */}
{query && recipes.length === 0 && (
<p className="mt-4">Fant ingen oppskrifter med '{query}'.</p>
)}
<RecipeList recipes={recipes} />

{/*Knappen vises bare hvis det kan finnes flere oppskrifter og første side er full*/}
Expand Down