Skip to content

Commit

Permalink
Merge pull request #1576 from guardian/db/add-chef
Browse files Browse the repository at this point in the history
Add Chef card type to the Feast Platform
  • Loading branch information
jonathonherbert authored May 10, 2024
2 parents 3a090ae + fcf6223 commit dfc20da
Show file tree
Hide file tree
Showing 14 changed files with 265 additions and 2 deletions.
1 change: 1 addition & 0 deletions app/model/editions/EditionsCard.scala
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ sealed abstract class CardType extends EnumEntry with Uncapitalised
object CardType extends PlayEnum[CardType] {
case object Article extends CardType
case object Recipe extends CardType
case object Chef extends CardType
override def values = findValues
}

Expand Down
18 changes: 18 additions & 0 deletions fronts-client/src/bundles/chefsBundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import createAsyncResourceBundle from '../lib/createAsyncResourceBundle';
import { Chef } from '../types/Chef';
import chefOttolenghi from './fixtures/chef-ottolenghi.json';
import chefStein from './fixtures/chef-stein.json';
import chefCloake from './fixtures/chef-cloake.json';

export const { actions, reducer, selectors } = createAsyncResourceBundle<Chef>(
'chefs',
{
indexById: true,
initialData: {
// Add stub data in the absence of proper search data.
[chefOttolenghi.id]: chefOttolenghi,
[chefStein.id]: chefStein,
[chefCloake.id]: chefCloake,
},
}
);
12 changes: 12 additions & 0 deletions fronts-client/src/bundles/fixtures/chef-cloake.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"id": "profile/felicity-cloake",
"type": "contributor",
"webTitle": "Felicity Cloake",
"webUrl": "https://www.theguardian.com/profile/felicity-cloake",
"apiUrl": "https://content.guardianapis.com/profile/felicity-cloake",
"bio": "<p>Award-winning food writer Felicity Cloake has written five cookbooks, two food travelogues and is the author of the Guardian's <a href=\"https://www.theguardian.com/food/series/how-to-cook-the-perfect----\">How to Cook the Perfect ...</a> recipe series</p>",
"bylineImageUrl": "https://uploads.guim.co.uk/2018/01/29/Felicity-Cloake.jpg",
"bylineLargeImageUrl": "https://uploads.guim.co.uk/2018/01/29/Felicity_Cloake,_L.png",
"firstName": "Felicity",
"lastName": "Cloake"
}
15 changes: 15 additions & 0 deletions fronts-client/src/bundles/fixtures/chef-ottolenghi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"id": "profile/yotamottolenghi",
"type": "contributor",
"sectionId": "lifeandstyle",
"sectionName": "Life and style",
"webTitle": "Yotam Ottolenghi",
"webUrl": "https://www.theguardian.com/profile/yotamottolenghi",
"apiUrl": "https://content.guardianapis.com/profile/yotamottolenghi",
"bio": "<p>Yotam Ottolenghi&nbsp;is chef-patron of the <a href=\"https://ottolenghi.co.uk/\">Ottolenghi</a> delis and the Nopi and Rovi restaurants. He has published 10 bestselling cookbooks</p>",
"bylineImageUrl": "https://uploads.guim.co.uk/2023/10/10/yotam_ottolenghi.jpg",
"bylineLargeImageUrl": "https://uploads.guim.co.uk/2023/10/10/yotam_ottolenghi.png",
"firstName": "Yotam",
"lastName": "Ottolenghi",
"twitterHandle": "ottolenghi"
}
9 changes: 9 additions & 0 deletions fronts-client/src/bundles/fixtures/chef-stein.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"id": "profile/rick-stein",
"type": "contributor",
"webTitle": "Rick Stein",
"webUrl": "https://www.theguardian.com/profile/rick-stein",
"apiUrl": "https://content.guardianapis.com/profile/rick-stein",
"firstName": "stein",
"lastName": "rick"
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { DefaultDropIndicator } from 'components/DropZone';
import DragIntentContainer from 'components/DragIntentContainer';
import { CardTypes, CardTypesMap } from 'constants/cardTypes';
import { RecipeCard } from 'components/card/recipe/RecipeCard';
import { ChefCard } from 'components/card/chef/ChefCard';

export const createCardId = (id: string) => `collection-item-${id}`;

Expand Down Expand Up @@ -111,7 +112,7 @@ class Card extends React.Component<CardContainerProps> {
};

public toggleShowArticleSublinks = (e?: React.MouseEvent) => {
const togPos = this.state.showCardSublinks ? false : true;
const togPos = !this.state.showCardSublinks;
this.setState({ showCardSublinks: togPos });
if (e) {
e.stopPropagation();
Expand Down Expand Up @@ -223,6 +224,24 @@ class Card extends React.Component<CardContainerProps> {
{getSublinks}
</>
);
case CardTypesMap.CHEF:
return (
<>
<ChefCard
frontId={frontId}
collectionId={collectionId}
id={uuid}
isUneditable={isUneditable}
{...getNodeProps()}
onDelete={this.onDelete}
onAddToClipboard={this.handleAddToClipboard}
onClick={isUneditable ? undefined : () => onSelect(uuid)}
size={size}
textSize={textSize}
showMeta={showMeta}
/>
</>
);
default:
return (
<p>
Expand Down
85 changes: 85 additions & 0 deletions fronts-client/src/components/card/chef/ChefCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from 'react';
import { Card, CardSizes } from '../../../types/Collection';
import { useSelector } from 'react-redux';
import { selectCard } from '../../../selectors/shared';
import { State } from '../../../types/State';
import { selectors as chefsSelectors } from 'bundles/chefsBundle';
import CardContainer from '../CardContainer';
import CardBody from '../CardBody';
import CardMetaHeading from '../CardMetaHeading';
import CardMetaContainer from '../CardMetaContainer';
import CardMetaContent from '../CardMetaContent';
import upperFirst from 'lodash/upperFirst';
import CardContent from '../CardContent';
import CardSettingsDisplay from '../CardSettingsDisplay';
import CardHeadingContainer from '../CardHeadingContainer';
import CardHeading from '../CardHeading';
import ImageAndGraphWrapper from '../../image/ImageAndGraphWrapper';
import { ThumbnailSmall } from '../../image/Thumbnail';

interface Props {
onDragStart?: (d: React.DragEvent<HTMLElement>) => void;
onDrop?: (d: React.DragEvent<HTMLElement>) => void;
onDelete?: (uuid: string) => void;
onAddToClipboard?: (uuid: string) => void;
onClick?: () => void;
id: string;
collectionId?: string;
frontId: string;
draggable?: boolean;
size?: CardSizes;
textSize?: CardSizes;
fade?: boolean;
children?: React.ReactNode;
isUneditable?: boolean;
showMeta?: boolean;
}

export const ChefCard = ({
id,
fade,
size = 'default',
textSize = 'default',
onDelete,
onAddToClipboard,
children,
isUneditable,
collectionId,
frontId,
showMeta = true,
...rest
}: Props) => {
const card = useSelector<State, Card>((state) => selectCard(state, id));
const chef = useSelector((state) =>
chefsSelectors.selectById(state, card.id)
);
return (
<CardContainer {...rest}>
<CardBody data-testid="snap" size={size} fade={fade}>
{showMeta && (
<CardMetaContainer size={size}>
<CardMetaHeading>Chef</CardMetaHeading>
<CardMetaContent>{upperFirst(chef?.type)}</CardMetaContent>
</CardMetaContainer>
)}
<CardContent textSize={textSize}>
<CardSettingsDisplay
isBreaking={card.meta?.isBreaking}
showByline={card.meta?.showByline}
showQuotedHeadline={card.meta?.showQuotedHeadline}
showLargeHeadline={card.meta?.showLargeHeadline}
isBoosted={card.meta?.isBoosted}
/>
<CardHeadingContainer size={size}>
<CardHeading data-testid="headline" html>
{chef?.webTitle ?? 'No Chef found'}
</CardHeading>
</CardHeadingContainer>
</CardContent>
<ImageAndGraphWrapper size={size}>
<ThumbnailSmall url={chef?.bylineLargeImageUrl} />
</ImageAndGraphWrapper>
</CardBody>
</CardContainer>
);
};
53 changes: 53 additions & 0 deletions fronts-client/src/components/feed/ChefFeedItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Chef } from '../../types/Chef';
import React from 'react';
import {
dragOffsetX,
dragOffsetY,
} from '../FrontsEdit/CollectionComponents/ArticleDrag';
import { FeedItem } from './FeedItem';
import { ContentInfo } from './ContentInfo';
import { selectFeatureValue } from '../../selectors/featureSwitchesSelectors';
import { State } from '../../types/State';
import { connect } from 'react-redux';
import noop from 'lodash/noop';

interface ComponentProps {
chef: Chef;
shouldObscureFeed: boolean;
}

export const ChefFeedItemComponent = ({
chef,
shouldObscureFeed,
}: ComponentProps) => {
const handleDragStart = (
event: React.DragEvent<HTMLDivElement>,
dragNode: HTMLDivElement
) => {
event.dataTransfer.setData('chef', JSON.stringify(chef));
if (dragNode) {
event.dataTransfer.setDragImage(dragNode, dragOffsetX, dragOffsetY);
}
};

return (
<FeedItem
id={chef.id}
title={`${chef.firstName} ${chef.lastName}`}
hasVideo={false}
isLive={true}
liveUrl={`https://theguardian.com/${chef.apiUrl}`}
thumbnail={chef.bylineLargeImageUrl}
onAddToClipboard={noop}
handleDragStart={handleDragStart}
shouldObscureFeed={shouldObscureFeed}
metaContent={<ContentInfo>Chef</ContentInfo>}
/>
);
};

const mapStateToProps = (state: State) => ({
shouldObscureFeed: selectFeatureValue(state, 'obscure-feed'),
});

export const ChefFeedItem = connect(mapStateToProps)(ChefFeedItemComponent);
13 changes: 13 additions & 0 deletions fronts-client/src/components/feed/RecipeSearchContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import { styled } from 'constants/theme';
import React from 'react';
import { connect } from 'react-redux';
import { selectors as recipeSelectors } from 'bundles/recipesBundle';
import { selectors as chefSelectors } from 'bundles/chefsBundle';
import { State } from 'types/State';
import { Recipe } from 'types/Recipe';
import { Chef } from 'types/Chef';
import { SearchResultsHeadingContainer } from './SearchResultsHeadingContainer';
import { SearchTitle } from './SearchTitle';
import { RecipeFeedItem } from './RecipeFeedItem';
import { ChefFeedItem } from './ChefFeedItem';

const InputContainer = styled.div`
margin-bottom: 10px;
Expand All @@ -28,11 +31,13 @@ const FixedContentContainer = styled.div`
interface Props {
rightHandContainer?: React.ReactElement<any>;
recipes: Record<string, Recipe>;
chefs: Record<string, Chef>;
}

const FeastSearchContainerComponent = ({
rightHandContainer,
recipes,
chefs,
}: Props) => (
<React.Fragment>
<InputContainer>
Expand All @@ -51,12 +56,20 @@ const FeastSearchContainerComponent = ({
{Object.values(recipes).map((recipe) => (
<RecipeFeedItem key={recipe.id} recipe={recipe} />
))}
{Object.values(chefs).map((chef) => (
<ChefFeedItem
key={chef.id}
chef={chef}
/> /*TODO: as of now, added rendering of chefs just after the recipes. Need
to change when "search-chefs" action gets finalised*/
))}
</FixedContentContainer>
</React.Fragment>
);

const mapStateToProps = (state: State) => ({
recipes: recipeSelectors.selectAll(state),
chefs: chefSelectors.selectAll(state),
});

export const RecipeSearchContainer = connect(mapStateToProps)(
Expand Down
1 change: 1 addition & 0 deletions fronts-client/src/constants/cardTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const CardTypesMap = {
SNAP_LINK: 'snap-link',
ARTICLE: 'article',
RECIPE: 'recipe',
CHEF: 'chef',
} as const;

export type CardTypes = (typeof CardTypesMap)[keyof typeof CardTypesMap];
2 changes: 2 additions & 0 deletions fronts-client/src/reducers/rootReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { reducer as focusReducer } from 'bundles/focusBundle';
import { reducer as featureSwitches } from 'reducers/featureSwitchesReducer';
import { reducer as notificationsReducer } from 'bundles/notificationsBundle';
import { reducer as recipesReducer } from 'bundles/recipesBundle';
import { reducer as chefsReducer } from 'bundles/chefsBundle';

const rootReducer = (state: any = { feed: {} }, action: any) => ({
fronts: fronts(state.fronts, action),
Expand Down Expand Up @@ -55,6 +56,7 @@ const rootReducer = (state: any = { feed: {} }, action: any) => ({
pageViewData: pageViewData(state.pageViewData, action),
notifications: notificationsReducer(state.notifications, action),
recipes: recipesReducer(state.recipes, action),
chefs: chefsReducer(state.chefs, action),
});

export default rootReducer;
15 changes: 15 additions & 0 deletions fronts-client/src/types/Chef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export interface Chef {
id: string;
type: string;
sectionId?: string;
sectionName?: string;
webTitle: string;
webUrl: string;
apiUrl: string;
bio?: string;
bylineImageUrl?: string;
bylineLargeImageUrl?: string;
firstName: string;
lastName: string;
twitterHandle?: string;
}
10 changes: 10 additions & 0 deletions fronts-client/src/util/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
} from 'util/url';
import { Recipe } from '../types/Recipe';
import type { CardTypes } from '../constants/cardTypes';
import { Chef } from '../types/Chef';

interface CreateCardOptions {
cardType?: CardTypes;
Expand Down Expand Up @@ -170,6 +171,10 @@ const getCardEntitiesFromDrop = async (
return getRecipeEntityFromFeedDrop(drop.data);
}

if (drop.type === 'CHEF') {
return getChefEntityFromFeedDrop(drop.data);
}

const droppedDataURL = drop.data.trim();
const resourceIdOrUrl = isGoogleRedirectUrl(droppedDataURL)
? getRelevantURLFromGoogleRedirectURL(droppedDataURL)
Expand Down Expand Up @@ -273,6 +278,11 @@ const getCardEntitiesFromDrop = async (
return [];
};

const getChefEntityFromFeedDrop = (chef: Chef): [Card] => {
const card = createCard(chef.id, false, { cardType: 'chef' });
return [card];
};

const getRecipeEntityFromFeedDrop = (recipe: Recipe): [Card] => {
const card = createCard(recipe.id, false, { cardType: 'recipe' });

Expand Down
Loading

0 comments on commit dfc20da

Please sign in to comment.