Skip to content

Commit

Permalink
Merge branch 'main' into mob/no-open-form-warnings-recipe-chef-feast-…
Browse files Browse the repository at this point in the history
…collection
  • Loading branch information
JustinPinner committed Sep 24, 2024
2 parents 5ae2c24 + 2681597 commit 39e1b93
Show file tree
Hide file tree
Showing 17 changed files with 637 additions and 52 deletions.
5 changes: 5 additions & 0 deletions app/conf/Configuration.scala
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ class ApplicationConfiguration(
lazy val host = getString("ophan.api.host")
}

object recipesApi {
lazy val key = getString("recipes.api.key")
lazy val url = getString("recipes.api.url")
}

object analytics {
lazy val secret = getMandatoryString("analytics.secret")
}
Expand Down
33 changes: 32 additions & 1 deletion app/controllers/FaciaContentApiProxy.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import play.api.libs.concurrent.Futures._

import scala.concurrent.duration._
import logging.Logging
import org.apache.pekko.util.ByteString
import play.api.mvc.{ResponseHeader, Result}
import play.api.http.HttpEntity
import services.Capi
import switchboard.SwitchManager
import util.ContentUpgrade.rewriteBody

import scala.concurrent.ExecutionContext
import scala.concurrent.{ExecutionContext, Future}


class FaciaContentApiProxy(capi: Capi, val deps: BaseFaciaControllerComponents)(implicit ec: ExecutionContext) extends BaseFaciaController(deps) with Logging {
Expand Down Expand Up @@ -111,4 +114,32 @@ class FaciaContentApiProxy(capi: Capi, val deps: BaseFaciaControllerComponents)(
}
}
}

def recipesLookup() = AccessAPIAuthAction.async { request =>
FaciaToolMetrics.ProxyCount.increment()

val fixedQueryString = request.queryString.map(kv=>(kv._1, kv._2.head))

(config.recipesApi.url, config.recipesApi.key) match {
case (Some(baseUrl), Some(key))=>
wsClient
.url(s"$baseUrl/api/content/by-uid")
.withQueryStringParameters(fixedQueryString.toList :_*)
.withHttpHeaders("X-Api-Key" -> key)
.get()
.withTimeout(5.seconds)
.map { response =>
Cached(300) {
Result(
header = ResponseHeader(response.status, Map.empty),
body = HttpEntity.Strict(ByteString(response.body), Some("application/json"))
)
}
}
case _=>
Future.successful(
InternalServerError("""Server is misconfigured, no recipes api config available""").as("application/json")
)
}
}
}
15 changes: 2 additions & 13 deletions app/model/editions/templates/feast/FeastSouthernHemisphere.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,12 @@ object FeastSouthernHemisphere extends FeastAppEdition {

val MainFront: FrontTemplate = front(
"All Recipes",
collection("Dish of the day"),
collection("Collection 2"),
collection("Collection 3"),
collection("Collection 4"),
collection("Collection 5"),
collection("Collection 6"),
collection("Collection 7"),
collection("Collection 8"),
collection("Collection 9")
collection("Dish of the day")
)

val MeatFreeFront: FrontTemplate = front(
"Meat-Free",
collection("Dish of the day"),
collection("Collection 2"),
collection("Collection 3"),
collection("Collection 4"),
collection("Dish of the day")
)

val template: EditionTemplate = EditionTemplate(
Expand Down
4 changes: 3 additions & 1 deletion app/slices/FixedContainers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class FixedContainers(val config: ApplicationConfiguration) {
val showcase = slices(ShowcaseSingleStories)
val thrasher = slices(Fluid).copy(customCssClasses = Set("fc-container--thrasher"))
val highlights = slices(Highlights)
val scrollableSmall = slices(ScrollableSmall)
val video = slices(TTT).copy(customCssClasses = Set("fc-container--video"))

val all: Map[String, ContainerDefinition] = Map(
Expand All @@ -36,7 +37,8 @@ class FixedContainers(val config: ApplicationConfiguration) {
("fixed/video/vertical", video),
("fixed/thrasher", thrasher),
("fixed/showcase", showcase),
("scrollable/highlights", highlights)
("scrollable/highlights", highlights),
("scrollable/small", scrollableSmall)
) ++ (if (config.faciatool.showTestContainers) Map(
("all-items/not-for-production", slices(FullMedia100, FullMedia75, FullMedia50, HalfHalf, QuarterThreeQuarter, ThreeQuarterQuarter, Hl4Half, HalfQuarterQl2Ql4, TTTL4, Ql3Ql3Ql3Ql3))
) else Map.empty)
Expand Down
37 changes: 37 additions & 0 deletions app/slices/Slice.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1007,3 +1007,40 @@ case object Highlights extends Slice {
),
)
}


/*
* The Scrollable small layout is implemented via a carousel.
* The implementation on platforms limits the display to 8 cards altogether, and only 2-3 cards at a time.
* In the tool, we're satisfied with using a 8 card layout to hint at the maximum number of stories.
*
* Desktop:
* .____________.____________.____________.____________.____________.____________.____________.____________.
* | #####| #####| #####| #####| #####| #####| #####| #####|
* | #####| #####| #####| #####| #####| #####| #####| #####|
* | #####| #####| #####| #####| #####| #####| #####| #####|
* '-------------------------------------------------------------------------------------------------------'
*
* Mobile:
* .___________.___________.___________.___________.___________.___________.___________.___________.
* | | | | | | | | |
* | | | | | | | | |
* |_#########_|_#########_|_#########_|_#########_|_#########_|_#########_|_#########_|_#########_|
* |_#########_|_#########_|_#########_|_#########_|_#########_|_#########_|_#########_|_#########_|
* |_#########_|_#########_|_#########_|_#########_|_#########_|_#########_|_#########_|_#########_|
* `-----------------------------------------------------------------------------------------------'
*/
case object ScrollableSmall extends Slice {
val layout = SliceLayout(
cssClassName = "t-t-t-t-t-t-t-t",
columns = Seq(Rows(
colSpan = 1,
columns = 8,
rows = 1,
ItemClasses(
mobile = ListItem,
tablet = ListItem,
)
))
)
}
1 change: 0 additions & 1 deletion conf/logback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,4 @@
</root>

<logger name="com.google.api.client.http" level="WARN" />

</configuration>
2 changes: 1 addition & 1 deletion conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ GET /api/live/*path controllers.FaciaContentApi
GET /http/proxy/*url controllers.FaciaContentApiProxy.http(url)
GET /json/proxy/*absUrl controllers.FaciaContentApiProxy.json(absUrl)
GET /ophan/*path controllers.FaciaContentApiProxy.ophan(path)

GET /recipes/api/content/by-uid controllers.FaciaContentApiProxy.recipesLookup()
# thumbnails
GET /thumbnails/*id.svg controllers.ThumbnailController.container(id)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ interface CollectionState {
showOpenFormsWarning: boolean;
isPreviouslyOpen: boolean;
isLaunching: boolean;
isDeleteClicked?: boolean;
}

const PreviouslyCollectionContainer = styled.div``;
Expand Down Expand Up @@ -166,6 +167,7 @@ class Collection extends React.Component<CollectionProps, CollectionState> {
isPreviouslyOpen: false,
isLaunching: false,
showOpenFormsWarning: false,
isDeleteClicked: false,
};

// added to prevent setState call on unmounted component
Expand Down Expand Up @@ -277,8 +279,11 @@ class Collection extends React.Component<CollectionProps, CollectionState> {
</MoveButtonsContainer>
<HeadlineContentButton
priority="default"
onClick={() => this.removeFrontCollection()}
onClick={this.handleDeleteClick}
title="Delete the collection for this issue"
style={{
color: this.state.isDeleteClicked ? 'red' : 'white',
}}
>
Delete
</HeadlineContentButton>
Expand Down Expand Up @@ -381,6 +386,17 @@ class Collection extends React.Component<CollectionProps, CollectionState> {
private removeFrontCollection = () => {
this.props.removeFrontCollection(this.props.frontId, this.props.id);
};

private handleDeleteClick = () => {
this.setState({ isDeleteClicked: true });
const isConfirm = window.confirm(
`Are you sure you wish to delete collection? This cannot be undone.`
);
if (isConfirm) {
this.removeFrontCollection();
}
this.setState({ isDeleteClicked: false });
};
}

const createMapStateToProps = () => {
Expand Down
4 changes: 3 additions & 1 deletion fronts-client/src/components/feed/RecipeSearchContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ export const RecipeSearchContainer = ({ rightHandContainer }: Props) => {
<RecipeFeedItem key={id} id={id} />
));
case FeedType.chefs:
return chefSearchIds.map((chefId) => (
//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
return chefSearchIds.filter(chefId=>!!chefId).map((chefId) => (
<ChefFeedItem key={chefId} id={chefId} />
));
}
Expand Down
73 changes: 55 additions & 18 deletions fronts-client/src/components/inputs/InputImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
AddImageIcon,
VideoIcon,
WarningIcon,
CropIcon,
} from '../icons/Icons';
import imageDragIcon from 'images/icons/image-drag-icon.svg';
import {
Expand All @@ -49,9 +50,10 @@ const AddImageButton = styled(ButtonDefault)<{ small?: boolean }>`
small ? theme.colors.greyVeryLight : '#5e5e5e99'};
}
width: 100%;
height: 100%;
flex-grow: 1;
padding: 0;
text-shadow: 0 0 2px black;
display: inline-block;
`;

const ImageComponent = styled.div<{
Expand Down Expand Up @@ -92,6 +94,7 @@ const AddImageViaGridModalButton = styled.div`
justify-content: center;
align-items: center;
flex-grow: 1;
flex-direction: column;
`;

const AddImageViaUrlInput = styled(InputContainer)`
Expand Down Expand Up @@ -247,6 +250,7 @@ interface ComponentState {
imageSrc: string;
confirmDelete: boolean;
cancelDeleteTimeout: undefined | (() => void);
isRecropping: boolean;
}

const dragImage = new Image();
Expand Down Expand Up @@ -280,12 +284,11 @@ class InputImage extends React.Component<ComponentProps, ComponentState> {
const {
small = false,
input,
gridUrl,
gridUrl:gridBaseUrl,
useDefault,
defaultImageUrl,
message = 'Replace image',
hasVideo,
editMode,
disabled,
isSelected,
isInvalid,
Expand All @@ -294,22 +297,30 @@ class InputImage extends React.Component<ComponentProps, ComponentState> {

const imageDims = this.getCurrentImageDimensions();

if (!gridUrl) {
if (!gridBaseUrl) {
return (
<div>
<code>gridUrl</code> config value missing
</div>
);
}

const gridSearchUrl =
editMode === 'editions' ? `${gridUrl}` : this.criteriaToGridUrl();

const hasImage = !useDefault && !!input.value && !!input.value.thumb;
const imageUrl =
!useDefault && input.value && input.value.thumb
? input.value.thumb
: defaultImageUrl;

// e.g. https://media.guim.co.uk/db6bf997dee6d43f8dca1ab9cd2c7402725434b6/0_214_3960_2376/500.jpg
const maybeDefaultImagePathParts = defaultImageUrl && new URL(defaultImageUrl).pathname.split("/");
const maybeDefaultImageId = maybeDefaultImagePathParts?.[1] // pathname starts with / so index 0 is empty string
const maybeDefaultCropId = maybeDefaultImagePathParts?.[2]
const gridUrl = this.state.isRecropping && maybeDefaultImageId && maybeDefaultCropId
? `${gridBaseUrl}/images/${maybeDefaultImageId}/crop?seedCropId=${maybeDefaultCropId}&`
: `${gridBaseUrl}?`;
const gridModalUrl = `${gridUrl}${new URLSearchParams(this.criteriaToGridQueryParams()).toString()}`

const portraitImage = !!(
!useDefault &&
imageDims &&
Expand All @@ -327,7 +338,7 @@ class InputImage extends React.Component<ComponentProps, ComponentState> {
confirmDelete={this.state.confirmDelete}
>
<GridModal
url={gridSearchUrl}
url={gridModalUrl}
isOpen={this.state.modalOpen}
onClose={this.closeModal}
onMessage={this.onMessage}
Expand Down Expand Up @@ -384,13 +395,22 @@ class InputImage extends React.Component<ComponentProps, ComponentState> {
<AddImageViaGridModalButton>
<AddImageButton
type="button"
onClick={this.openModal}
onClick={this.openModal(false)}
small={small}
disabled={disabled}
>
<AddImageIcon size="l" />
{!!small ? null : <Label size="sm">{message}</Label>}
</AddImageButton>
<AddImageButton
type="button"
onClick={this.openModal(true)}
small={small}
disabled={disabled}
>
<CropIcon size="l" fill={theme.colors.white} />
{!!small ? null : <Label size="sm">Recrop image</Label>}
</AddImageButton>
</AddImageViaGridModalButton>
)}
{hasVideo && useDefault && (
Expand Down Expand Up @@ -554,8 +574,8 @@ class InputImage extends React.Component<ComponentProps, ComponentState> {
window.removeEventListener('message', this.onMessage, false);
};

private openModal = () => {
this.setState({ modalOpen: true });
private openModal = (isRecropping: boolean) => () => {
this.setState({ modalOpen: true, isRecropping });
window.addEventListener('message', this.onMessage, false);
};

Expand All @@ -566,20 +586,37 @@ class InputImage extends React.Component<ComponentProps, ComponentState> {
);
};

private criteriaToGridUrl = (): string => {
const { criteria, gridUrl } = this.props;
private criteriaToGridQueryParams = (): Record<string, string> => {
const { criteria, editMode } = this.props;

if(editMode === "editions"){
return {};
}

if (!criteria) {
return `${gridUrl}?cropType=portrait,landscape`;
return {
cropType: "portrait,landscape"
};
}

// assumes the only criteria that will be passed as props the defined
// constants for portrait(4:5), landscape (5:3) and landscape (5:4)
if (this.compareAspectRatio(portraitCardImageCriteria, criteria))
return `${gridUrl}?cropType=portrait`;
else if (this.compareAspectRatio(landscape5To4CardImageCriteria, criteria))
return `${gridUrl}?cropType=Landscape&customRatio=Landscape,5,4`;
else return `${gridUrl}?cropType=landscape`;
if (this.compareAspectRatio(portraitCardImageCriteria, criteria)) {
return {
cropType: "portrait"
};
}
else if (this.compareAspectRatio(landscape5To4CardImageCriteria, criteria)) {
return {
cropType: "Landscape",
customRatio: "Landscape,5,4"
};
}
else {
return {
cropType: "landscape"
};
}
};

private getCurrentImageDimensions = () => {
Expand Down
Loading

0 comments on commit 39e1b93

Please sign in to comment.