diff --git a/fronts-client/src/bundles/recipesBundle.ts b/fronts-client/src/bundles/recipesBundle.ts index 034815c4bd..37998dde4e 100644 --- a/fronts-client/src/bundles/recipesBundle.ts +++ b/fronts-client/src/bundles/recipesBundle.ts @@ -43,7 +43,7 @@ export const fetchRecipesById = const recipes = await liveRecipes.recipesById(idList); dispatch( actions.fetchSuccess(recipes, { - order: recipes.map((_) => _.id), + ignoreOrder: true, }), ); } catch (e) { diff --git a/fronts-client/src/components/feed/FeedItem.tsx b/fronts-client/src/components/feed/FeedItem.tsx index d1a8c1733f..6c18e605c2 100644 --- a/fronts-client/src/components/feed/FeedItem.tsx +++ b/fronts-client/src/components/feed/FeedItem.tsx @@ -53,7 +53,7 @@ const Byline = styled.h2` font-weight: bold; `; -const Title = styled.h2` +export const Title = styled.h2` margin: 2px 2px 0 0; vertical-align: top; font-family: TS3TextSans; @@ -111,6 +111,7 @@ interface FeedItemProps { id: string; type: CardTypes; title: string; + bodyContent?: JSX.Element; liveUrl?: string; metaContent?: JSX.Element; scheduledPublicationDate?: string; @@ -125,6 +126,7 @@ interface FeedItemProps { ) => void; shouldObscureFeed?: boolean; byline?: string; + showPinboard?: boolean; } export class FeedItem extends React.Component { @@ -138,6 +140,7 @@ export class FeedItem extends React.Component { id, type, title, + bodyContent, liveUrl, isLive, metaContent, @@ -149,6 +152,7 @@ export class FeedItem extends React.Component { hasVideo, handleDragStart, byline, + showPinboard, } = this.props; const { preview, live, ophan } = getPaths(id); @@ -199,10 +203,16 @@ export class FeedItem extends React.Component { - {title} - {byline ? ( - {byline} - ) : undefined} + {bodyContent ? ( + bodyContent + ) : ( + <> + {title} + {byline ? ( + {byline} + ) : undefined} + + )} { toolTipPosition={'top'} toolTipAlign={'right'} urlPath={liveUrl} + showPinboard={showPinboard} renderButtons={(props) => ( <> diff --git a/fronts-client/src/components/feed/RecipeFeedItem.tsx b/fronts-client/src/components/feed/RecipeFeedItem.tsx index ab6045b93e..17c68c3619 100644 --- a/fronts-client/src/components/feed/RecipeFeedItem.tsx +++ b/fronts-client/src/components/feed/RecipeFeedItem.tsx @@ -9,12 +9,15 @@ import { useDispatch } from 'react-redux'; import { insertCardWithCreate } from 'actions/Cards'; import { selectors as recipeSelectors } from 'bundles/recipesBundle'; import { handleDragStartForCard } from 'util/dragAndDrop'; +import { Title as FeedItemTitle } from './FeedItem'; +import format from 'date-fns/format'; interface ComponentProps { id: string; + showTimes: boolean; } -export const RecipeFeedItem = ({ id }: ComponentProps) => { +export const RecipeFeedItem = ({ id, showTimes }: ComponentProps) => { const shouldObscureFeed = useSelector((state) => selectFeatureValue(state, 'obscure-feed'), ); @@ -33,6 +36,15 @@ export const RecipeFeedItem = ({ id }: ComponentProps) => { ); }, [recipe]); + const renderTimestamp = (iso: string) => { + try { + const date = new Date(iso); + return format(date, 'HH:mm on do MMM YYYY'); + } catch (err) { + console.warn(err); + return iso; + } + }; return ( { handleDragStart={handleDragStartForCard(CardTypesMap.RECIPE, recipe)} onAddToClipboard={onAddToClipboard} shouldObscureFeed={shouldObscureFeed} + showPinboard={false} + bodyContent={ + <> + {recipe.title} + {recipe?.lastModifiedDate && showTimes ? ( + + Modified {renderTimestamp(recipe.lastModifiedDate)} + + ) : undefined} + {recipe?.publishedDate && showTimes ? ( + + Published {renderTimestamp(recipe.publishedDate)} + + ) : undefined} + + } metaContent={ <> Recipe diff --git a/fronts-client/src/components/feed/RecipeSearchContainer.tsx b/fronts-client/src/components/feed/RecipeSearchContainer.tsx index 4dd892b7d5..bacb1c1aaa 100644 --- a/fronts-client/src/components/feed/RecipeSearchContainer.tsx +++ b/fronts-client/src/components/feed/RecipeSearchContainer.tsx @@ -1,7 +1,7 @@ import ClipboardHeader from 'components/ClipboardHeader'; import TextInput from 'components/inputs/TextInput'; import { styled } from 'constants/theme'; -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { fetchRecipes, @@ -18,9 +18,11 @@ import Pagination from './Pagination'; import ScrollContainer from '../ScrollContainer'; import { ChefSearchParams, + DateParamField, RecipeSearchParams, } from '../../services/recipeQuery'; import debounce from 'lodash/debounce'; +import ButtonDefault from '../inputs/ButtonDefault'; const InputContainer = styled.div` margin-bottom: 10px; @@ -43,6 +45,9 @@ const PaginationContainer = styled.div` const TopOptions = styled.div` display: flex; flex-direction: row; + justify-content: space-between; + margin-right: 1em; + margin-bottom: 1em; `; const FeedsContainerWrapper = styled.div` @@ -61,6 +66,12 @@ enum FeedType { export const RecipeSearchContainer = ({ rightHandContainer }: Props) => { const [selectedOption, setSelectedOption] = useState(FeedType.recipes); const [searchText, setSearchText] = useState(''); + + const [showAdvancedRecipes, setShowAdvancedRecipes] = useState(false); + const [dateField, setDateField] = useState(undefined); + const [orderingForce, setOrderingForce] = useState('default'); + const [forceDates, setForceDates] = useState(false); + const dispatch: Dispatch = useDispatch(); const searchForChefs = useCallback( (params: ChefSearchParams) => { @@ -84,13 +95,11 @@ export const RecipeSearchContainer = ({ rightHandContainer }: Props) => { const [page, setPage] = useState(1); - /*const debouncedRunSearch = debounce(() => runSearch(page), 750); TODO need to check if needed for chef-search? if yes then how to improve implementing it*/ - useEffect(() => { const dbf = debounce(() => runSearch(page), 750); dbf(); return () => dbf.cancel(); - }, [selectedOption, searchText, page]); + }, [selectedOption, searchText, page, dateField, orderingForce]); const chefsPagination: IPagination | null = useSelector((state: State) => chefSelectors.selectPagination(state), @@ -98,6 +107,26 @@ export const RecipeSearchContainer = ({ rightHandContainer }: Props) => { const hasPages = (chefsPagination?.totalPages ?? 0) > 1; + const getUpdateConfig = () => { + switch (orderingForce) { + case 'gentle': + return { + decay: 0.95, + dropoffScaleDays: 90, + offsetDays: 7, + }; + case 'forceful': + return { + decay: 0.7, + dropoffScaleDays: 180, + offsetDays: 14, + }; + case 'default': + default: + return undefined; + } + }; + const runSearch = useCallback( (page: number = 1) => { switch (selectedOption) { @@ -109,17 +138,25 @@ export const RecipeSearchContainer = ({ rightHandContainer }: Props) => { case FeedType.recipes: searchForRecipes({ queryText: searchText, + uprateByDate: dateField, + uprateConfig: getUpdateConfig(), }); break; } }, - [selectedOption, searchText, page], + [selectedOption, searchText, page, dateField, orderingForce], ); const renderTheFeed = () => { switch (selectedOption) { case FeedType.recipes: - return recipeSearchIds.map((id) => ); + return recipeSearchIds.map((id) => ( + + )); case FeedType.chefs: //Fixing https://the-guardian.sentry.io/issues/5820707430/?project=35467&referrer=issue-stream&statsPeriod=90d&stream_index=0&utc=true //It seems that some null values got into the `chefSearchIds` list @@ -144,11 +181,99 @@ export const RecipeSearchContainer = ({ rightHandContainer }: Props) => { setPage(1); setSearchText(event.target.value); }} + onClick={() => setShowAdvancedRecipes(true)} value={searchText} /> + + {showAdvancedRecipes && selectedOption === FeedType.recipes ? ( + <> + +
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+ setForceDates(evt.target.checked)} + /> +
+
+ + setShowAdvancedRecipes(false)}> + Close + + + + ) : undefined} + JSX.Element; + showPinboard?: boolean; //Note- defaults to `true` } export const HoverActionsButtonWrapper = ({ @@ -53,6 +54,7 @@ export const HoverActionsButtonWrapper = ({ size, urlPath, renderButtons, + showPinboard, }: WrapperProps) => { const [toolTipText, setToolTipText] = useState(undefined); @@ -79,7 +81,7 @@ export const HoverActionsButtonWrapper = ({ hideToolTip, size, })} - {urlPath && ( + {urlPath && (showPinboard || showPinboard === undefined) && ( // the below tag is empty and meaningless to the fronts tool itself, but serves as a handle for // Pinboard to attach itself via, identified/distinguished by the urlPath data attribute // @ts-ignore diff --git a/fronts-client/src/services/recipeQuery.ts b/fronts-client/src/services/recipeQuery.ts index 029351a44c..cb5f31acbb 100644 --- a/fronts-client/src/services/recipeQuery.ts +++ b/fronts-client/src/services/recipeQuery.ts @@ -18,6 +18,12 @@ export interface RecipeSearchFilters { filterType: 'During' | 'Post'; } +export type DateParamField = + | undefined + | 'publishedDate' + | 'firstPublishedDate' + | 'lastModifiedDate'; + export interface RecipeSearchParams { queryText: string; searchType?: 'Embedded' | 'Match' | 'Lucene'; @@ -25,6 +31,16 @@ export interface RecipeSearchParams { kfactor?: number; limit?: number; filters?: RecipeSearchFilters; + uprateByDate?: DateParamField; + uprateConfig?: { + originDate?: string; //should be ISO format date, defaults to today + //take this and add it to `offsetDays`. Then, weights will be modified so that + //at originDate +/- this many days results will be downweighted by `decay` + dropoffScaleDays?: number; + offsetDays?: number; + decay?: number; + }; + format?: 'Full' | 'Titles'; } export interface ChefSearchHit { @@ -165,7 +181,10 @@ const recipeQuery = (baseUrl: string) => { recipes: async ( params: RecipeSearchParams, ): Promise => { - const queryDoc = JSON.stringify(params); + const queryDoc = JSON.stringify({ + ...params, + noStats: true, //we are not reading stats, so no point slowing the query down by retrieving them. + }); const response = await fetch(`${baseUrl}/search`, { method: 'POST', body: queryDoc, diff --git a/fronts-client/src/types/Recipe.ts b/fronts-client/src/types/Recipe.ts index 19c53c5432..6d6945e967 100644 --- a/fronts-client/src/types/Recipe.ts +++ b/fronts-client/src/types/Recipe.ts @@ -21,6 +21,9 @@ export interface Recipe { difficultyLevel: string; featuredImage?: RecipeImage; // the latter is an old image format that appears in our test fixtures previewImage?: RecipeImage; + firstPublishedDate?: string; + lastModifiedDate?: string; + publishedDate?: string; } export interface RecipeIndexData {