/**
* @module ProgressRow
*/
import { getAllStartedOrCompleted, getProgressStateByIds } from '../../contentProgress.js'
import { addContextToContent } from '../../contentAggregator.js'
import { fetchByRailContentIds, fetchShows } from '../../sanity.js'
import {
postProcessBadge,
collectionLessonTypes,
getFormattedType,
recentTypes,
showsLessonTypes,
songs,
} from '../../../contentTypeConfig.js'
import { getTimeRemainingUntilLocal } from '../../dateUtils.js'
import { PARENT_ID_TOP_LEVEL } from '../../sync/models/ContentProgress'
/**
* Fetch any content IDs with some progress, include the userPinnedItem,
* and generate a map of the cards keyed by the content IDs
*/
export async function getContentCardMap(brand, limit, userPinnedItem) {
const metadata = {
brand: brand,
contentTypes: Object.values(recentTypes.homeRow),
parentId: PARENT_ID_TOP_LEVEL,
}
let recentContentIds = await getAllStartedOrCompleted({ metadata, limit })
if (userPinnedItem?.progressType === 'content') {
recentContentIds.push(userPinnedItem.id)
}
let contents =
recentContentIds.length > 0
? await addContextToContent(
fetchByRailContentIds,
recentContentIds,
'progress-tracker',
brand,
{
addNavigateTo: true,
addProgressStatus: true,
addProgressPercentage: true,
addProgressTimestamp: true,
}
)
: []
contents = postProcessBadge(contents)
const contentCards = await Promise.all(generateContentPromises(contents))
return contentCards.reduce((contentMap, content) => {
contentMap.set(content.id, content)
return contentMap
}, new Map())
}
function generateContentPromises(contents) {
const promises = []
if (!contents) return promises
const existingShows = new Set()
let allRecentTypeSet = new Set(Object.values(recentTypes.homeRow))
allRecentTypeSet.delete('learning-path-v2') // we do this to remove from homepage, until we allow a-la-carte learning paths
contents.forEach((content) => {
const type = content.type
if (!allRecentTypeSet.has(type)) return
let childHasParent =
Array.isArray(content.parent_content_data) && content.parent_content_data.length > 0
if (!childHasParent) {
promises.push(processContentItem(content))
if (showsLessonTypes.includes(type)) {
// Shows don't have a parent to link them, but need to be handled as if they're a set of children
existingShows.add(type)
}
}
})
return promises
}
export async function processContentItem(content) {
const contentType = getFormattedType(content.type, content.brand)
const isLive = content.isLive ?? false
let ctaText = getDefaultCTATextForContent(content, contentType)
const { completedChildren, allChildren } = await getCompletedChildren(content, contentType)
content.completed_children = completedChildren
content.all_children = allChildren
if (content.type === 'guided-course') {
const nextLessonPublishedOn = content.children.find(
(child) => child.id === content.navigateTo.id
)?.published_on
let isLocked = new Date(nextLessonPublishedOn) > new Date()
if (isLocked) {
content.is_locked = true
const timeRemaining = getTimeRemainingUntilLocal(nextLessonPublishedOn, {
withTotalSeconds: true,
})
content.time_remaining_seconds = timeRemaining.totalSeconds
ctaText = 'Next lesson in ' + timeRemaining.formatted
} else if (
!content.progressStatus ||
content.progressStatus === 'not-started' ||
content.progressPercentage === 0
) {
ctaText = 'Start Course'
}
}
return {
id: content.id,
progressType: 'content',
header: contentType,
pinned: content.pinned ?? false,
content: content,
body: {
progressPercent: isLive ? undefined : content.progressPercentage,
thumbnail: content.thumbnail,
title: content.title,
isLive: isLive,
brand: content.brand,
badge: content.badge ?? null,
badge_rear: content.badge_rear ?? null,
badge_logo: content.badge_logo ?? null,
badge_template: content.badge_template ?? null,
badge_template_rear: content.badge_template_rear ?? null,
isLocked: content.is_locked ?? false,
subtitle:
collectionLessonTypes.includes(content.type) || content.lesson_count > 1
? `${content.completed_children ?? 0} of ${content.all_children ?? content.lesson_count ?? content.child_count} Lessons Complete`
: (contentType === 'lesson' || contentType === 'show') && isLive === false
? `${content.progressPercentage}% Complete`
: `${content.difficulty_string} • ${content.artist_name}`,
},
cta: {
text: ctaText,
timeRemainingToUnlockSeconds: content.time_remaining_seconds ?? null,
action: {
type: content.type,
brand: content.brand,
id: content.id,
slug: content.slug,
child: content.navigateTo,
},
},
progressTimestamp: content.progressTimestamp,
}
}
function getDefaultCTATextForContent(content, contentType) {
let ctaText = 'Continue'
if (content.progressStatus === 'completed') {
if (
contentType === songs[content.brand] ||
contentType === 'play along' ||
contentType === 'jam track'
)
ctaText = 'Replay Song'
if (contentType === 'lesson' || contentType === 'show') ctaText = 'Revisit Lesson'
if (contentType === 'song tutorial' || collectionLessonTypes.includes(content.type))
ctaText = 'Revisit Lessons'
if (contentType === 'course-collection') ctaText = 'View Lessons'
}
return ctaText
}
async function getCompletedChildren(content, contentType) {
let completedChildren = 0
let allChildren = 0
if (contentType === 'show') {
const shows = await addContextToContent(fetchShows, content.brand, content.type, {
addProgressStatus: true,
})
completedChildren = Object.values(shows).filter(
(show) => show.progressStatus === 'completed'
).length
allChildren = Object.values(shows).length
} else if (content.children && content.children.length > 0) {
const lessonIds = getLeafNodes(content)
const progressOnItems = await getProgressStateByIds(lessonIds)
completedChildren = Array.from(progressOnItems.values()).filter(
(value) => value === 'completed'
).length
allChildren = lessonIds.length
}
return { completedChildren, allChildren }
}
function getLeafNodes(content) {
const ids = []
function traverse(children) {
for (const item of children) {
if (item.children) {
traverse(item.children) // Recursively handle nested lessons
} else if (item.id) {
ids.push(item.id)
}
}
}
if (content && Array.isArray(content.children)) {
traverse(content.children)
}
return ids
}