Skip to content

Commit

Permalink
Merge pull request #67 from danskernesdigitalebibliotek/DDFBRA-173-br…
Browse files Browse the repository at this point in the history
…uger-skal-kunne-se-bla-titel

Add "Blue title" badge for cost free works on work page
  • Loading branch information
Adamik10 authored Dec 6, 2024
2 parents bee0ecc + 3e9e209 commit 9951c2e
Show file tree
Hide file tree
Showing 11 changed files with 211 additions and 58 deletions.
124 changes: 86 additions & 38 deletions components/pages/workPageLayout/WorkPageHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,47 @@
import { motion } from "framer-motion"
import { useRouter, useSearchParams } from "next/navigation"
import React, { useEffect, useState } from "react"

import { Badge } from "@/components/shared/badge/Badge"
import { CoverPicture } from "@/components/shared/coverPicture/CoverPicture"
import SlideSelect, { SlideSelectOption } from "@/components/shared/slideSelect/SlideSelect"
import { displayCreators } from "@/components/shared/workCard/helper"
import { WorkFullWorkPageFragment } from "@/lib/graphql/generated/fbi/graphql"
import { getCoverUrls, getLowResCoverUrl } from "@/lib/helpers/covers"
import { useGetCoverCollection } from "@/lib/rest/cover-service-api/generated/cover-service"
import { GetCoverCollectionSizesItem } from "@/lib/rest/cover-service-api/generated/model"
import { useGetV1ProductsIdentifier } from "@/lib/rest/publizon-api/generated/publizon"
import { useSelectedManifestationStore } from "@/store/selectedManifestation.store"

import WorkPageButtons from "./WorkPageButtons"
import { getManifestationByMaterialType, getWorkMaterialTypes } from "./helper"
import {
addMaterialTypeIconToSelectOption,
findInitialSliderValue,
getIsbnsFromManifestation,
getManifestationByMaterialType,
getManifestationLanguageIsoCode,
getWorkMaterialTypes,
} from "./helper"

type WorkPageHeaderProps = {
work: WorkFullWorkPageFragment
}

const WorkPageHeader = ({ work }: WorkPageHeaderProps) => {
const searchParams = useSearchParams()
const router = useRouter()
const { selectedManifestation, setSelectedManifestation } = useSelectedManifestationStore()
const [slideSelectOptions, setSlideSelectOptions] = useState<SlideSelectOption[] | null>(null)
const isbns = getIsbnsFromManifestation(selectedManifestation)
const languageIsoCode = getManifestationLanguageIsoCode(selectedManifestation)
const titleSuffix = selectedManifestation?.titles?.identifyingAddition || ""
const [initialSliderValue, setinitialSliderValue] = useState<SlideSelectOption | undefined>(
undefined
)
const workMaterialTypes = getWorkMaterialTypes(work).map(materialType => {
return { value: materialType.code, render: materialType.display }
})

const { data: dataCovers, isLoading: isLoadingCovers } = useGetCoverCollection(
{
type: "pid",
Expand All @@ -30,38 +53,59 @@ const WorkPageHeader = ({ work }: WorkPageHeaderProps) => {
},
{ query: { enabled: !!selectedManifestation?.pid } }
)
const workMaterialTypes = getWorkMaterialTypes(work).map(materialType => {
return { value: materialType.code, render: materialType.display }

const { data: dataPublizon } = useGetV1ProductsIdentifier(isbns[0]?.value || "", {
query: {
// Publizon / useGetV1ProductsIdentifier is responsible for online
// materials. It requires an ISBN to do lookups.
enabled: isbns && isbns.length > 0,
},
})
// We only want unique material types
const slideSelectOptions = workMaterialTypes.reduce<SlideSelectOption[]>((acc, materialType) => {
if (!acc.some(item => item.value === materialType.value)) {
acc.push(materialType)
}
return acc
}, [])
const titleSuffix = selectedManifestation?.titles?.identifyingAddition

const lowResCover = getLowResCoverUrl(dataCovers)
const coverSrc = getCoverUrls(
dataCovers,
[selectedManifestation?.pid || ""],
selectedManifestation?.pid ? [selectedManifestation.pid] : undefined,
["default", "original", "large", "medium-large", "medium", "small-medium", "small", "xx-small"]
)
const [initialSliderValue, setinitialSliderValue] = useState<SlideSelectOption | undefined>(
undefined
)
const findInitialSliderValue = () => {
return slideSelectOptions.find(option => {
return selectedManifestation?.materialTypes.find(materialType => {
return materialType.materialTypeGeneral.code.includes(option.value)
})
})
const onOptionSelect = (optionSelected: SlideSelectOption) => {
const params = new URLSearchParams(searchParams)
params.set("type", optionSelected.render)
router.push(`${window.location.pathname}?${params.toString()}`, { scroll: false })
}

useEffect(() => {
setinitialSliderValue(findInitialSliderValue())
// Initialize slideSelect options
const slideSelectOptions = workMaterialTypes.reduce<SlideSelectOption[]>(
(acc, materialType) => {
if (!acc.some(item => item.value === materialType.value)) {
acc.push(addMaterialTypeIconToSelectOption(materialType)) // We only want unique material types
}
return acc
},
[]
)
setSlideSelectOptions(slideSelectOptions)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedManifestation])
}, [])

useEffect(() => {
// Initialize slideSelect initial value
const searchParams = new URLSearchParams(window.location.search)
setinitialSliderValue(
findInitialSliderValue(slideSelectOptions, selectedManifestation, searchParams)
)
}, [selectedManifestation, slideSelectOptions])

useEffect(() => {
if (!!searchParams.get("type")) {
setSelectedManifestation(
getManifestationByMaterialType(work, searchParams.get("type") as string) ||
selectedManifestation
)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams])

return (
<>
Expand All @@ -82,22 +126,26 @@ const WorkPageHeader = ({ work }: WorkPageHeaderProps) => {
/>
)}
</div>
<div className="flex w-full justify-center pt-12">
<SlideSelect
options={slideSelectOptions}
initialOption={initialSliderValue}
onOptionSelect={(optionSelected: SlideSelectOption) => {
setSelectedManifestation(
getManifestationByMaterialType(work, optionSelected.value) ||
work.manifestations.bestRepresentation
)
}}
/>
</div>
{slideSelectOptions && (
<div className="flex w-full justify-center pt-12">
<SlideSelect
options={slideSelectOptions}
initialOption={initialSliderValue}
onOptionSelect={onOptionSelect}
/>
</div>
)}
</div>
<div className="col-span-4 flex flex-col justify-end">
<h1 className="mt-grid-gap-3 hyphens-auto break-words text-typo-heading-3 lg:mt-0 lg:text-typo-heading-2">
{`${selectedManifestation?.titles?.main || ""}${!!titleSuffix ? ` (${titleSuffix})` : ""}`}
<div className="col-span-4 flex flex-col items-start justify-end pt-grid-gap-3 lg:pt-0">
{!!dataPublizon?.product?.costFree && (
<Badge variant={"blue-title"} className="mb-1 lg:mb-2">
BLÅ
</Badge>
)}
<h1
lang={languageIsoCode}
className="hyphens-auto break-words text-typo-heading-3 lg:mt-0 lg:text-typo-heading-2">
{`${selectedManifestation?.titles?.full || ""}${!!titleSuffix ? ` (${titleSuffix})` : ""}`}
</h1>
<p className="mt-grid-gap-2 text-typo-caption uppercase lg:mt-7">{`af ${displayCreators(work.creators, 100)}`}</p>
</div>
Expand Down
15 changes: 12 additions & 3 deletions components/pages/workPageLayout/WorkPageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@

import { useQuery } from "@tanstack/react-query"
import { notFound } from "next/navigation"
import { useSearchParams } from "next/navigation"
import React, { useEffect } from "react"

import { GetMaterialQuery, useGetMaterialQuery } from "@/lib/graphql/generated/fbi/graphql"
import { useSelectedManifestationStore } from "@/store/selectedManifestation.store"

import WorkPageHeader from "./WorkPageHeader"
import { getBestRepresentation } from "./helper"
import { getBestRepresentation, getManifestationByMaterialType } from "./helper"

type WorkPageLayoutProps = {
workId: string
dehydratedQueryData: GetMaterialQuery | undefined
}

function WorkPageLayout({ workId, dehydratedQueryData }: WorkPageLayoutProps) {
const searchParams = useSearchParams()
const { data, isLoading } = useQuery({
queryKey: useGetMaterialQuery.getKey({ wid: workId }),
queryFn: useGetMaterialQuery.fetcher({ wid: workId }),
Expand All @@ -35,8 +37,15 @@ function WorkPageLayout({ workId, dehydratedQueryData }: WorkPageLayoutProps) {

useEffect(() => {
if (!data || !data.work) return
// Select on the initial load
if (!selectedManifestation) {
if (selectedManifestation) return

// Select work manifestation on the initial load - 1. by URL params, 2. by best representation
if (!!searchParams.get("type")) {
setSelectedManifestation(
getManifestationByMaterialType(data.work, searchParams.get("type") as string) ||
getBestRepresentation(data.work)
)
} else {
setSelectedManifestation(getBestRepresentation(data.work))
}
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
69 changes: 68 additions & 1 deletion components/pages/workPageLayout/helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { head, uniqBy } from "lodash"

import { SlideSelectOption } from "@/components/shared/slideSelect/SlideSelect"
import { materialTypeCategories } from "@/components/shared/workCard/helper"
import {
GeneralMaterialTypeCodeEnum,
IdentifierTypeEnum,
ManifestationWorkPageFragment,
WorkFullWorkPageFragment,
WorkMaterialTypesFragment,
Expand All @@ -22,7 +27,7 @@ export const getManifestationByMaterialType = (
materialType: GeneralMaterialTypeCodeEnum[0]
): ManifestationWorkPageFragment | undefined => {
return work.manifestations.all.find(manifestation =>
manifestation.materialTypes.some(type => type.materialTypeGeneral.code === materialType)
manifestation.materialTypes.some(type => type.materialTypeGeneral.display === materialType)
)
}

Expand Down Expand Up @@ -66,3 +71,65 @@ export const isAudioBook = (manifestation: ManifestationWorkPageFragment | undef
if (!manifestation) return false
return isOfMaterialType(manifestation, GeneralMaterialTypeCodeEnum.AudioBooks)
}

export const getIsbnsFromManifestation = (
manifestaion: ManifestationWorkPageFragment | undefined | null
) => {
if (!manifestaion) return []
return manifestaion.identifiers.filter(identifier => identifier.type === IdentifierTypeEnum.Isbn)
}

export const getManifestationLanguageIsoCode = (
manifestation: ManifestationWorkPageFragment | undefined | null
) => {
if (!manifestation) return undefined

const uniqueLanguagesWithIsoCode = uniqBy(manifestation.languages?.main, "isoCode")

// We only want to set the lang attribute if there is only one isoCode
const uniqIsoCode =
uniqueLanguagesWithIsoCode.length === 1 && head(uniqueLanguagesWithIsoCode)?.isoCode

if (uniqIsoCode) {
return uniqIsoCode
}
// if there is no isoCode it return undefined so that the lang attribute is not set
return undefined
}

export const findInitialSliderValue = (
sliderOptions: SlideSelectOption[] | undefined | null,
selectedManifestation: ManifestationWorkPageFragment | undefined | null,
searchParams: URLSearchParams
) => {
// If we have a material type specified in the URL, we use that
if (
!!searchParams.get("type") &&
sliderOptions?.some(option => option.render === searchParams.get("type"))
) {
return sliderOptions.find(option => option.render === searchParams.get("type"))
}
// Else select any
return sliderOptions?.find(option => {
return selectedManifestation?.materialTypes.find(materialType => {
return materialType.materialTypeGeneral.code.includes(option.value)
})
})
}

export const addMaterialTypeIconToSelectOption = (option: SlideSelectOption) => {
const code = option.value as GeneralMaterialTypeCodeEnum
if (materialTypeCategories.reading.includes(code)) {
return { ...option, icon: "book" }
}
if (materialTypeCategories.listening.includes(code)) {
return { ...option, icon: "headphones" }
}
if (materialTypeCategories.gaming.includes(code)) {
return { ...option, icon: "controller" }
}
if (materialTypeCategories.video.includes(code)) {
return { ...option, icon: "video" }
}
return option
}
4 changes: 2 additions & 2 deletions components/shared/badge/BadgeButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ const BadgeButton = ({
<button
onClick={onClick}
className={cn(
`focus-visible h-[28px] w-auto self-start whitespace-nowrap rounded-full bg-background-overlay px-4
py-2 text-typo-caption`,
`focus-visible flex h-[28px] w-auto flex-row gap-2 self-start whitespace-nowrap rounded-full
bg-background-overlay px-4 py-2 text-typo-caption`,
withAnimation ? "hover:animate-wiggle" : "",
variant === "transparent" ? "bg-transparent" : "",
isActive ? "bg-foreground text-background" : "",
Expand Down
4 changes: 2 additions & 2 deletions components/shared/dialog/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
`z-50 fixed inset-0 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out
`fixed inset-0 z-dialog bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0`,
className
)}
Expand All @@ -39,7 +39,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
`z-50 fixed left-[50%] top-[50%] m-auto grid w-[calc(100%-var(--grid-edge)*2)] max-w-[600px]
`fixed left-[50%] top-[50%] z-dialog m-auto grid w-[calc(100%-var(--grid-edge)*2)] max-w-[600px]
translate-x-[-50%] translate-y-[-50%] gap-grid-edge rounded-md bg-background p-grid-edge shadow-lg
duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95
Expand Down
3 changes: 3 additions & 0 deletions components/shared/slideSelect/SlideSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import React, { useEffect, useState } from "react"
import { cn } from "@/lib/helpers/helper.cn"

import BadgeButton from "../badge/BadgeButton"
import Icon from "../icon/Icon"

export type SlideSelectOption = {
value: string
render: string
icon?: string
}

export type SlideSelectProps = {
Expand Down Expand Up @@ -58,6 +60,7 @@ const SlideSelect = ({ options, initialOption, onOptionSelect }: SlideSelectProp
}}
variant="transparent"
classNames={cn("z-slide-select w-28", selected === index && "text-background")}>
{!!option.icon && <Icon className="m-[-7px] h-7 w-7" name={option.icon} />}
{option.render}
</BadgeButton>
)
Expand Down
6 changes: 3 additions & 3 deletions components/shared/workCard/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const displayCreators = (
}, "")
}

const workCategories = {
export const materialTypeCategories = {
reading: [
GeneralMaterialTypeCodeEnum.Articles,
GeneralMaterialTypeCodeEnum.Books,
Expand All @@ -41,10 +41,10 @@ const workCategories = {

export const isOfWorkTypeCategory = (
materialTypes: SearchWithPaginationQuery["search"]["works"][0]["materialTypes"],
category: keyof typeof workCategories
category: keyof typeof materialTypeCategories
) => {
return materialTypes.some(materialType =>
workCategories[category].includes(materialType.materialTypeGeneral.code)
materialTypeCategories[category].includes(materialType.materialTypeGeneral.code)
)
}

Expand Down
12 changes: 11 additions & 1 deletion lib/graphql/fragments/manifestation.fbi.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,17 @@ fragment ManifestationAccess on Manifestation {

fragment ManifestationTitles on Manifestation {
titles {
main
identifyingAddition
full
}
}

fragment ManifestationLanguages on Manifestation {
languages {
main {
display
isoCode
}
}
}

Expand All @@ -62,4 +71,5 @@ fragment ManifestationWorkPage on Manifestation {
...ManifestationCover
...ManifestationAccess
...ManifestationTitles
...ManifestationLanguages
}
Loading

0 comments on commit 9951c2e

Please sign in to comment.