diff --git a/.github/workflows/deploy-assets.yml b/.github/workflows/deploy-assets.yml index 73e5b83eb0..1d6d5b97c5 100644 --- a/.github/workflows/deploy-assets.yml +++ b/.github/workflows/deploy-assets.yml @@ -58,7 +58,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - run: find assets -mindepth 1 ! -name ".gitkeep" -exec rm -rf {} + name: Delete global assets content - - run: aws s3 cp ./src s3://sabbath-school-resources-assets.adventech.io/ --acl "public-read" --region us-east-1 --no-progress --recursive --exclude "*" --include "**/assets/cover.png" --include "**/assets/cover-landscape.png" --include "**/assets/cover-square.png" --include "**/assets/splash.png" --include "**/assets/fonts/*.ttf" + - run: aws s3 cp ./src s3://sabbath-school-resources-assets.adventech.io/ --acl "public-read" --region us-east-1 --no-progress --recursive --exclude "*" --include "**/assets/cover.png" --include "**/assets/cover-landscape.png" --include "**/assets/cover-square.png" --include "**/assets/splash.png" --include "**/assets/logo.png" --include "**/assets/fonts/*.ttf" name: Upload resource assets env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} diff --git a/.github/workflows/deploy-resources.yml b/.github/workflows/deploy-resources.yml index 6415bc48c9..1370c19fdf 100644 --- a/.github/workflows/deploy-resources.yml +++ b/.github/workflows/deploy-resources.yml @@ -62,6 +62,10 @@ jobs: name: Deploy resources - run: node ops/deploy/deploy-languages.js name: Deploy languages + - run: node ops/deploy/deploy-authors.js + name: Deploy authors + - run: node ops/deploy/deploy-categories.js + name: Deploy categories - run: aws s3 cp dist/ s3://sabbath-school`[[ "${{ inputs.branch }}" = "stage" ]] && echo '-stage'`.adventech.io --acl "public-read" --region us-east-1 --no-progress --recursive env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} diff --git a/ops/deploy/deploy-authors.js b/ops/deploy/deploy-authors.js index 22a0935aad..66fb657633 100644 --- a/ops/deploy/deploy-authors.js +++ b/ops/deploy/deploy-authors.js @@ -5,7 +5,7 @@ import yaml from "js-yaml" import fs from "fs-extra" import { fdir } from "fdir" import { getResourceInfo } from "./deploy-resources.js" -import { isMainModule, parseResourcePath } from "../helpers/helpers.js" +import { isMainModule, parseResourcePath, sortResourcesByPattern } from "../helpers/helpers.js" import { API_DIST, SOURCE_DIR, @@ -15,11 +15,18 @@ import { AUTHORS_FEED_FILENAME, AUTHORS_INFO_FILENAME, FEED_VIEWS, - FEED_SCOPES, + FEED_SCOPES, FEED_DIRECTION, } from "../helpers/constants.js" +import crypto from "crypto" +import { getLanguageInfo } from "./deploy-languages.js" +import picomatch from "picomatch" -let getAuthorInfo = async function (author) { +let getAuthorInfo = async function (author, full) { const authorInfo = yaml.load(fs.readFileSync(author, "utf8")) + if (!full) { + delete authorInfo.details + delete authorInfo.links + } return authorInfo } @@ -44,7 +51,7 @@ let getAllTaggedResources = async function () { for (let resource of resources) { try { let resourceInfo = await getResourceInfo(`${SOURCE_DIR}/${resource}`) - if (resourceInfo.author) { + if (resourceInfo.authors) { allTaggedResources["resources"].push(resourceInfo) } } catch (e) { @@ -65,59 +72,73 @@ let processAuthors = async function () { .sync(); const allTaggedResources = await getAllTaggedResources() - const allCategories = {} + const allAuthors = {} for (let author of authors) { + try { - const authorInfo = await getAuthorInfo(`${SOURCE_DIR}/${author}`) + const authorInfo = await getAuthorInfo(`${SOURCE_DIR}/${author}`, true) const authorInfoDetail = { ...authorInfo } const authorPathInfo = parseResourcePath(`${SOURCE_DIR}/${author}`) + const languageInfo = await getLanguageInfo(authorPathInfo.language) const authorFeed = await getAuthorFeed(`${SOURCE_DIR}/${authorPathInfo.language}/${AUTHORS_DIRNAME}/${authorPathInfo.title}/${AUTHORS_FEED_FILENAME}`) - authorInfoDetail.feed = [] + authorInfoDetail.feed = {} + authorInfoDetail.feed.title = authorInfo.title + authorInfoDetail.feed.groups = [] - const authorResources = allTaggedResources.resources.filter(r => r.author === authorPathInfo.title) + const authorResources = allTaggedResources.resources.filter(r => r.authors.find((a) => a.id === authorInfo.id)) - authorFeed.map(g => { - authorInfoDetail.feed.push({ + authorFeed.groups.map((g, index) => { + authorInfoDetail.feed.groups.push({ ...g, title: g.group, resources: [], author: g.author || null, resourceIds: g.resources || [], scope: g.scope || null, - view: g.view || FEED_VIEWS.TILE, + view: g.view || FEED_VIEWS.FOLIO, + direction: g.direction || FEED_DIRECTION.HORIZONTAL, + type: "author", + seeAll: languageInfo.feedSeeAll ?? "See All", + id: crypto.createHash("sha256").update( + `${authorPathInfo.language}-authors-${g.group}-${index}` + ).digest("hex"), }) }) for (let authorResource of authorResources) { // name - let groupByName = authorInfoDetail.feed.find(g => g.resourceIds.indexOf(authorResource.id) >= 0) + let groupByName = authorInfoDetail.feed.groups.find(g => g.resourceIds.some(i => picomatch(i)(authorResource.id) )) + if (groupByName) { groupByName.resources.push(authorResource) + groupByName.resources = sortResourcesByPattern(groupByName.resources, groupByName.resourceIds) continue } - let groupByAuthor = authorInfoDetail.feed.find(g => g.author === authorResource.author) + let groupByAuthor = authorInfoDetail.feed.groups.find(g => g.author === authorResource.author) if (groupByAuthor) { groupByAuthor.resources.push(authorResource) + groupByAuthor.resources = sortResourcesByPattern(groupByAuthor.resources, groupByAuthor.resourceIds) continue } - let groupByKind = authorInfoDetail.feed.find(g => g.kind === authorResource.kind) + let groupByKind = authorInfoDetail.feed.groups.find(g => g.kind === authorResource.kind) if (groupByKind) { groupByKind.resources.push(authorResource) + groupByKind.resources = sortResourcesByPattern(groupByKind.resources, groupByKind.resourceIds) continue } - let groupByScope = authorInfoDetail.feed.find(g => g.scope === FEED_SCOPES.RESOURCE) + let groupByScope = authorInfoDetail.feed.groups.find(g => g.scope === FEED_SCOPES.RESOURCE) if (groupByScope) { groupByScope.resources.push(authorResource) + groupByScope.resources = sortResourcesByPattern(groupByScope.resources, groupByScope.resourceIds) } } - - authorInfoDetail.feed = authorInfoDetail.feed.filter(g => { + authorInfoDetail.feed.groups = authorInfoDetail.feed.groups.filter(g => { // filter feed groups that do not have any resources or docs return g.resources.length }).map(g => { @@ -133,10 +154,27 @@ let processAuthors = async function () { return g }) - if (!allCategories[authorPathInfo.language]) { - allCategories[authorPathInfo.language] = [authorInfo] + if (!allAuthors[authorPathInfo.language]) { + allAuthors[authorPathInfo.language] = [authorInfo] } else { - allCategories[authorPathInfo.language].push(authorInfo) + allAuthors[authorPathInfo.language].push(authorInfo) + } + + if (authorInfoDetail.feed.groups.length === 1) { + authorInfoDetail.feed.groups[0].direction = FEED_DIRECTION.VERTICAL + delete authorInfoDetail.feed.groups[0].seeAll + } + + for(let authorFeedGroup of authorInfoDetail.feed.groups) { + let authorFeedGroupFinal = JSON.parse(JSON.stringify(authorFeedGroup)) + authorFeedGroupFinal.direction = FEED_DIRECTION.VERTICAL + delete authorFeedGroupFinal.seeAll + + fs.outputFileSync(`${API_DIST}/${authorPathInfo.language}/${authorPathInfo.type}/${authorPathInfo.title}/feeds/${authorFeedGroup.id}/index.json`, JSON.stringify(authorFeedGroupFinal)) + + if (authorInfoDetail.feed.groups.length > 1 && authorFeedGroup["resources"].length > 10) { + authorFeedGroup["resources"] = authorFeedGroup["resources"].slice(0, 10) + } } fs.outputFileSync(`${API_DIST}/${authorPathInfo.language}/${authorPathInfo.type}/${authorPathInfo.title}/index.json`, JSON.stringify(authorInfoDetail)) @@ -145,8 +183,8 @@ let processAuthors = async function () { } } - for (let language of Object.keys(allCategories)) { - fs.outputFileSync(`${API_DIST}/${language}/${AUTHORS_DIRNAME}/index.json`, JSON.stringify(allCategories[language])) + for (let language of Object.keys(allAuthors)) { + fs.outputFileSync(`${API_DIST}/${language}/${AUTHORS_DIRNAME}/index.json`, JSON.stringify(allAuthors[language])) } } diff --git a/ops/deploy/deploy-categories.js b/ops/deploy/deploy-categories.js index 0d2d0c7f62..c139635f35 100644 --- a/ops/deploy/deploy-categories.js +++ b/ops/deploy/deploy-categories.js @@ -6,7 +6,7 @@ import yaml from "js-yaml" import { fdir } from "fdir" import { getResourceInfo } from "./deploy-resources.js" import { getDocumentInfoYml } from "./deploy-documents.js" -import { isMainModule, parseResourcePath } from "../helpers/helpers.js" +import { isMainModule, parseResourcePath, sortResourcesByPattern } from "../helpers/helpers.js" import { API_DIST, SOURCE_DIR, @@ -18,6 +18,9 @@ import { FEED_VIEWS, FEED_SCOPES, FEED_DIRECTION, } from "../helpers/constants.js" +import crypto from "crypto" +import { getLanguageInfo } from "./deploy-languages.js" +import picomatch from "picomatch" let getCategoryInfo = async function (category) { const categoryInfo = yaml.load(fs.readFileSync(category, "utf8")) @@ -39,17 +42,17 @@ let getAllTaggedResources = async function () { .crawl(SOURCE_DIR) .sync(); - const documents = new fdir() - .withBasePath() - .withRelativePaths() - .withMaxDepth(5) - .glob(`**/+(${RESOURCE_TYPE.DEVO}|${RESOURCE_TYPE.PM})/**/content/**/info.yml`) - .crawl(SOURCE_DIR) - .sync(); + // const documents = new fdir() + // .withBasePath() + // .withRelativePaths() + // .withMaxDepth(5) + // .glob(`**/+(${RESOURCE_TYPE.DEVO}|${RESOURCE_TYPE.PM})/**/content/**/info.yml`) + // .crawl(SOURCE_DIR) + // .sync(); let allTaggedResources = { resources: [], - documents: [], + // documents: [], } for (let resource of resources) { @@ -63,19 +66,19 @@ let getAllTaggedResources = async function () { } } - for (let document of documents) { - try { - let documentInfo = await getDocumentInfoYml(`${SOURCE_DIR}/${document}`) - if (documentInfo.categories) { - const documentPathInfo = parseResourcePath(`${SOURCE_DIR}/${document}`) - const parentResource = await getResourceInfo(`${SOURCE_DIR}/${documentPathInfo.language}/${documentPathInfo.type}/${documentPathInfo.title}/${RESOURCE_INFO_FILENAME}`) - documentInfo.parentResource = parentResource - allTaggedResources.documents.push(documentInfo) - } - } catch (e) { - console.error(`Error processing tagged document: ${e}`); - } - } + // for (let document of documents) { + // try { + // let documentInfo = await getDocumentInfoYml(`${SOURCE_DIR}/${document}`) + // if (documentInfo.categories) { + // const documentPathInfo = parseResourcePath(`${SOURCE_DIR}/${document}`) + // const parentResource = await getResourceInfo(`${SOURCE_DIR}/${documentPathInfo.language}/${documentPathInfo.type}/${documentPathInfo.title}/${RESOURCE_INFO_FILENAME}`) + // documentInfo.parentResource = parentResource + // allTaggedResources.documents.push(documentInfo) + // } + // } catch (e) { + // console.error(`Error processing tagged document: ${e}`); + // } + // } return allTaggedResources } @@ -97,68 +100,80 @@ let processCategories = async function () { const categoryInfo = await getCategoryInfo(`${SOURCE_DIR}/${category}`) const categoryInfoDetail = { ...categoryInfo } const categoryPathInfo = parseResourcePath(`${SOURCE_DIR}/${category}`) + const languageInfo = await getLanguageInfo(categoryPathInfo.language) const categoryFeed = await getCategoryFeed(`${SOURCE_DIR}/${categoryPathInfo.language}/${CATEGORIES_DIRNAME}/${categoryPathInfo.title}/${CATEGORY_FEED_FILENAME}`) - categoryInfoDetail.feed = [] + categoryInfoDetail.feed = {} + categoryInfoDetail.feed.title = categoryInfo.title + categoryInfoDetail.feed.groups = [] const categoryResources = allTaggedResources.resources.filter(r => r.categories.indexOf(categoryPathInfo.title) >= 0) - const categoryDocuments = allTaggedResources.documents.filter(d => d.categories.indexOf(categoryPathInfo.title) >= 0) + // const categoryDocuments = allTaggedResources.documents.filter(d => d.categories.indexOf(categoryPathInfo.title) >= 0) - categoryFeed.map(g => { - categoryInfoDetail.feed.push({ + categoryFeed.map((g, index) => { + categoryInfoDetail.feed.groups.push({ ...g, title: g.group, resources: [], - documents: [], + // documents: [], author: g.author || null, resourceIds: g.resources || [], - documentIds: [], + // documentIds: [], scope: g.scope || null, - view: g.view || FEED_VIEWS.TILE, + view: g.view || FEED_VIEWS.FOLIO, direction: g.direction || FEED_DIRECTION.HORIZONTAL, + type: "category", + seeAll: languageInfo.feedSeeAll ?? "See All", + id: crypto.createHash("sha256").update( + `${categoryPathInfo.language}-authors-${g.group}-${index}` + ).digest("hex"), }) }) for (let categoryResource of categoryResources) { - let groupByName = categoryInfoDetail.feed.find(g => g.resourceIds.indexOf(categoryResource.id) >= 0) + let groupByName = categoryInfoDetail.feed.groups.find(g => g.resourceIds.some(i => picomatch(i)(authorResource.id) )) if (groupByName) { groupByName.resources.push(categoryResource) + groupByName.resources = sortResourcesByPattern(groupByName.resources, groupByName.resourceIds) continue } - let groupByAuthor = categoryInfoDetail.feed.find(g => g.author === categoryResource.author) + let groupByAuthor = categoryInfoDetail.feed.groups.find(g => g.author === categoryResource.author) if (groupByAuthor) { groupByAuthor.resources.push(categoryResource) + groupByAuthor.resources = sortResourcesByPattern(groupByAuthor.resources, groupByAuthor.resourceIds) continue } - let groupByKind = categoryInfoDetail.feed.find(g => g.kind === categoryResource.kind) + let groupByKind = categoryInfoDetail.feed.groups.find(g => g.kind === categoryResource.kind) if (groupByKind) { groupByKind.resources.push(categoryResource) + groupByKind.resources = sortResourcesByPattern(groupByKind.resources, groupByKind.resourceIds) continue } - let groupByScope = categoryInfoDetail.feed.find(g => g.scope === FEED_SCOPES.RESOURCE) + let groupByScope = categoryInfoDetail.feed.groups.find(g => g.scope === FEED_SCOPES.RESOURCE) if (groupByScope) { groupByScope.resources.push(categoryResource) + groupByScope.resources = sortResourcesByPattern(groupByScope.resources, groupByScope.resourceIds) } } - for (let categoryDocument of categoryDocuments) { - let groupByScope = categoryInfoDetail.feed.find(g => g.scope === FEED_SCOPES.DOCUMENT) - if (groupByScope) { - groupByScope.documents.push(categoryDocument) - } - } + // for (let categoryDocument of categoryDocuments) { + // let groupByScope = categoryInfoDetail.feed.find(g => g.scope === FEED_SCOPES.DOCUMENT) + // if (groupByScope) { + // groupByScope.documents.push(categoryDocument) + // } + // } - categoryInfoDetail.feed = categoryInfoDetail.feed.filter(g => { + categoryInfoDetail.feed.groups = categoryInfoDetail.feed.groups.filter(g => { // filter feed groups that do not have any resources or docs - return g.resources.length || g.documents.length + return g.resources.length // || g.documents.length }).map(g => { // remove unnecessary params delete g.kind delete g.resourceIds - delete g.documentIds + // delete g.documentIds delete g.author delete g.group if (!g.scope && g.resources.length) { @@ -166,10 +181,10 @@ let processCategories = async function () { } // If scope is document, force "square" view - if ((!g.scope && g.document.length) || (g.scope === FEED_SCOPES.DOCUMENT)-0) { - g.scope = FEED_SCOPES.DOCUMENT - g.view = FEED_VIEWS.SQUARE - } + // if ((!g.scope && g.document.length) || (g.scope === FEED_SCOPES.DOCUMENT)-0) { + // g.scope = FEED_SCOPES.DOCUMENT + // g.view = FEED_VIEWS.SQUARE + // } return g }) @@ -179,6 +194,23 @@ let processCategories = async function () { allCategories[categoryPathInfo.language].push(categoryInfo) } + if (categoryInfoDetail.feed.groups.length === 1) { + categoryInfoDetail.feed.groups[0].direction = FEED_DIRECTION.VERTICAL + delete categoryInfoDetail.feed.groups[0].seeAll + } + + for(let categoryFeedGroup of categoryInfoDetail.feed.groups) { + let categoryFeedGroupFinal = JSON.parse(JSON.stringify(categoryFeedGroup)) + categoryFeedGroupFinal.direction = FEED_DIRECTION.VERTICAL + delete categoryFeedGroupFinal.seeAll + + fs.outputFileSync(`${API_DIST}/${categoryPathInfo.language}/${categoryPathInfo.type}/${categoryPathInfo.title}/feeds/${categoryFeedGroup.id}/index.json`, JSON.stringify(categoryFeedGroupFinal)) + + if (categoryInfoDetail.feed.groups.length > 1 && categoryFeedGroup["resources"].length > 10) { + categoryFeedGroup["resources"] = categoryFeedGroup["resources"].slice(0, 10) + } + } + fs.outputFileSync(`${API_DIST}/${categoryPathInfo.language}/${categoryPathInfo.type}/${categoryPathInfo.title}/index.json`, JSON.stringify(categoryInfoDetail)) } catch (e) { console.error(`Error processing categories: ${e}`); diff --git a/ops/deploy/deploy-resources.js b/ops/deploy/deploy-resources.js index c06d4c25e9..df48347341 100644 --- a/ops/deploy/deploy-resources.js +++ b/ops/deploy/deploy-resources.js @@ -10,7 +10,7 @@ import { fdir } from "fdir" import { database } from "../helpers/firebase.js" import { getDocumentInfoYml } from "./deploy-documents.js" import { getLanguageInfo } from "./deploy-languages.js" -import { arg, isMainModule, parseResourcePath } from "../helpers/helpers.js" +import { arg, isMainModule, parseResourcePath, sortResourcesByPattern } from "../helpers/helpers.js" import { SOURCE_DIR, API_DIST, @@ -32,8 +32,10 @@ import { RESOURCE_AUDIO_FILENAME, RESOURCE_VIDEO_FILENAME, RESOURCE_PDF_FILENAME, - RESOURCE_COVER_PLACEHOLDER, RESOURCE_PROGRESS_TRACKING + RESOURCE_COVER_PLACEHOLDER, RESOURCE_PROGRESS_TRACKING, AUTHORS_DIRNAME } from "../helpers/constants.js" +import { getAuthorInfo } from "./deploy-authors.js" +import { getCategoryInfo } from "./deploy-categories.js" let getResourceInfo = async function (resource, depth = 0) { const resourceInfo = yaml.load(fs.readFileSync(resource, "utf8")); @@ -109,8 +111,7 @@ let getResourceInfo = async function (resource, depth = 0) { scope: FEED_SCOPES.RESOURCE, view: resourceInfo.featuredResourcesView ?? FEED_VIEWS.FOLIO, title: languageInfo.featuredResources.title, - resources: featuredResources.filter(r => r), - seeAll: languageInfo.feedSeeAll ?? "See All", + resources: featuredResources.filter(r => r) }] } @@ -221,6 +222,29 @@ let getResourceInfo = async function (resource, depth = 0) { }) } + if (resourceInfo.author) { + try { + if (Array.isArray(resourceInfo.author)) { + resourceInfo.authors = [] + + for (let author of resourceInfo.author) { + let authorInfo = await getAuthorInfo(`${SOURCE_DIR}/${resourcePathInfo.language}/${AUTHORS_DIRNAME}/${author}/info.yml`) + resourceInfo.authors.push(authorInfo) + } + + if (!resourceInfo.authors.length) { + delete resourceInfo.authors + } + } else { + let authorInfo = await getAuthorInfo(`${SOURCE_DIR}/${resourcePathInfo.language}/${AUTHORS_DIRNAME}/${resourceInfo.author}/info.yml`) + resourceInfo.authors = [authorInfo] + } + } catch (e) { + console.log(`Error processing ${resourceInfo.title} ${resourceInfo.author} ${e}`) + } + delete resourceInfo.author + } + return resourceInfo } @@ -228,19 +252,6 @@ let getResourceFeed = async function (resource) { return yaml.load(fs.readFileSync(resource, "utf8")) } -let sortResourcesByPattern = function (resources, resourceIds) { - return resources.sort((a, b) => { - const indexA = resourceIds.findIndex((pattern) => - picomatch(pattern)(a.id) - ); - const indexB = resourceIds.findIndex((pattern) => - picomatch(pattern)(b.id) - ); - return indexA - indexB; - }); -} - - let processResources = async function (languageGlob, resourceType, resourceGlob) { const languages = new fdir() .withBasePath() @@ -300,7 +311,11 @@ let processResources = async function (languageGlob, resourceType, resourceGlob) author: g.author || null, scope: g.scope || null, resources: [], + authors: [], + categories: [], resourceIds: g.resources || [], + authorIds: g.authors || [], + categoryIds: g.categories || [], view: g.view || FEED_VIEWS.FOLIO, recent: g.recent || null, type: resourcePathInfo.type, @@ -338,7 +353,67 @@ let processResources = async function (languageGlob, resourceType, resourceGlob) fs.outputFileSync(`${API_DIST}/${resourcePathInfo.language}/${resourcePathInfo.type}/${resourcePathInfo.title}/index.json`, JSON.stringify(resourceInfo)) } catch (e) { - console.error(`Error processing resources: ${e}`); + console.log(`Error processing resources: ${e}`); + } + } + + if ( + resourceFeedConfigs[language] && + resourceFeedConfigs[language][resourceType].groups.find((g) => g.scope === FEED_SCOPES.AUTHOR) + ) { + let authors = new fdir() + .withBasePath() + .withRelativePaths() + .withMaxDepth(3) + .glob(`${language}/authors/*/${RESOURCE_INFO_FILENAME}`) + .crawl(SOURCE_DIR) + .sync().reverse() + + for (let author of authors) { + console.log(`Processing author ${author}`) + try { + const authorInfo = await getAuthorInfo(`${SOURCE_DIR}/${author}`, false) + + let resourceFeed = resourceFeedConfigs[language][resourceType] + let groupByName = resourceFeed.groups.find(g => g.authorIds.some(i => picomatch(i)(authorInfo.id) )) + + if (groupByName) { + groupByName.authors.push(authorInfo) + groupByName.authors = sortResourcesByPattern(groupByName.authors, groupByName.authorIds) + } + } catch (e) { + console.log(`Error processing categories: ${e}`); + } + } + } + + if ( + resourceFeedConfigs[language] && + resourceFeedConfigs[language][resourceType].groups.find((g) => g.scope === FEED_SCOPES.CATEGORY) + ) { + let categories = new fdir() + .withBasePath() + .withRelativePaths() + .withMaxDepth(3) + .glob(`${language}/categories/*/${RESOURCE_INFO_FILENAME}`) + .crawl(SOURCE_DIR) + .sync().reverse() + + for (let category of categories) { + console.log(`Processing category ${category}`) + try { + const categoryInfo = await getCategoryInfo(`${SOURCE_DIR}/${category}`) + + let resourceFeed = resourceFeedConfigs[language][resourceType] + let groupByName = resourceFeed.groups.find(g => g.categoryIds.some(i => picomatch(i)(categoryInfo.id) )) + + if (groupByName) { + groupByName.categories.push(categoryInfo) + groupByName.categories = sortResourcesByPattern(groupByName.categories, groupByName.categoryIds) + } + } catch (e) { + console.log(`Error processing categories: ${e}`); + } } } @@ -365,10 +440,14 @@ let processResources = async function (languageGlob, resourceType, resourceGlob) for (let resourceFeedForType of Object.keys(resourceFeedConfigs[resourceFeedLanguage])) { let resourceFeed = resourceFeedConfigs[resourceFeedLanguage][resourceFeedForType] - resourceFeed.groups = resourceFeed.groups.filter(g => g.resources.length).map(g => { + let originalResourceFeed = JSON.parse(JSON.stringify(resourceFeedConfigs[resourceFeedLanguage][resourceFeedForType])) + + resourceFeed.groups = resourceFeed.groups.filter(g => g.resources.length || g.authors.length || g.categories.length).map(g => { delete g.kind delete g.resourceIds + delete g.authorIds delete g.documentIds + delete g.categoryIds delete g.author delete g.group if (!g.scope && g.resources.length) { @@ -382,16 +461,49 @@ let processResources = async function (languageGlob, resourceType, resourceGlob) if (g.reverse) { g.resources = g.resources.reverse() + g.authors = g.authors.reverse() + g.categories = g.categories.reverse() delete g.reverse } + + if (g.scope === FEED_SCOPES.RESOURCE) { + delete g.authors + delete g.categories + } + + if (g.scope === FEED_SCOPES.AUTHOR) { + delete g.resources + delete g.categories + } + + if (g.scope === FEED_SCOPES.CATEGORY) { + delete g.resources + delete g.authors + } + return g }) for (let feedGroup of resourceFeed.groups) { let feedGroupAll = { ...feedGroup } feedGroupAll.direction = FEED_DIRECTION.VERTICAL + delete feedGroupAll.backgroundColor + let key = "resources" + let sortingKey = "resourceIds" + + if (feedGroup.scope === FEED_SCOPES.RESOURCE) { + key = "resources" + sortingKey = "resourceIds" + } else if (feedGroup.scope === FEED_SCOPES.AUTHOR) { + key = "authors" + sortingKey = "authorIds" + } else if (feedGroup.scope === FEED_SCOPES.CATEGORY) { + key = "categories" + sortingKey = "categoryIds" + } + // Get existing feed for non-global deployments if (resourceGlob !== "*") { let existingFeedResponse @@ -400,13 +512,19 @@ let processResources = async function (languageGlob, resourceType, resourceGlob) existingFeedResponse = await fetch(`${API_URL()}${API_PREFIX}${language}/${resourceFeedForType}/feeds/${feedGroup.id}/index.json`) existingFeed = await existingFeedResponse.json() } catch (e) { - existingFeed = { resources: [] } + if (feedGroup.scope === FEED_SCOPES.RESOURCE) { + existingFeed = { resources: [] } + } else if (feedGroup.scope === FEED_SCOPES.AUTHOR) { + existingFeed = { authors: [] } + } else if (feedGroup.scope === FEED_SCOPES.CATEGORY) { + existingFeed = { categories: []} + } } - for (let feedGroupResource of feedGroup.resources) { + for (let feedGroupResource of feedGroup[key]) { let found = false - existingFeed.resources = existingFeed.resources.map((existingResource) => { + existingFeed[key] = existingFeed[key].map((existingResource) => { if (existingResource.id === feedGroupResource.id) { found = true existingResource = feedGroupResource @@ -415,18 +533,26 @@ let processResources = async function (languageGlob, resourceType, resourceGlob) }) if (!found) { - existingFeed.resources.unshift(feedGroupResource) + existingFeed[key].unshift(feedGroupResource) + let originalResourceFeedGroup = originalResourceFeed.groups.find((g) => g.id === feedGroup.id) + + if (originalResourceFeedGroup) { + existingFeed[key] = sortResourcesByPattern(existingFeed[key], originalResourceFeedGroup[sortingKey]) + } } } - feedGroupAll.resources = existingFeed.resources - feedGroup.resources = existingFeed.resources + feedGroupAll[key] = existingFeed[key] + feedGroup[key] = existingFeed[key] } - fs.outputFileSync(`${API_DIST}/${language}/${resourceFeedForType}/feeds/${feedGroup.id}/index.json`, JSON.stringify(feedGroupAll)) + let feedGroupAllFinal = JSON.parse(JSON.stringify(feedGroupAll)) + delete feedGroupAllFinal.seeAll - if (resourceFeed.groups.length > 1 && feedGroup.resources.length > 10) { - feedGroup.resources = feedGroup.resources.slice(0, 10) + fs.outputFileSync(`${API_DIST}/${language}/${resourceFeedForType}/feeds/${feedGroup.id}/index.json`, JSON.stringify(feedGroupAllFinal)) + + if (resourceFeed.groups.length > 1 && feedGroup[key].length > 10) { + feedGroup[key] = feedGroup[key].slice(0, 10) } } @@ -447,6 +573,12 @@ let processResources = async function (languageGlob, resourceType, resourceGlob) return existingMainFeedGroup }) + for (let resourceFeedGroup of resourceFeed.groups) { + if (!existingMainFeed.groups.find((g) => g.id === resourceFeedGroup.id)) { + existingMainFeed.groups.push(resourceFeedGroup) + } + } + resourceFeed.groups = existingMainFeed.groups } catch (e) { existingMainFeed = { groups: [] } diff --git a/ops/helpers/helpers.js b/ops/helpers/helpers.js index 3164d12e30..fb73eacb3e 100644 --- a/ops/helpers/helpers.js +++ b/ops/helpers/helpers.js @@ -17,6 +17,7 @@ import { DOCUMENT_INFO_FILENAME, SECTION_INFO_FILENAME, LANGUAGE_INFO_FILENAME, RESOURCE_FEED_FILENAME } from "./constants.js" +import picomatch from "picomatch" const options = { alias: { @@ -110,6 +111,10 @@ let getPositiveCoverImagesGlob = function () { return `+(${Object.values(RESOURCE_COVERS).join("|")})` } +let getPositiveCoverAndLogoImagesGlob = function () { + return `+(logo.png|${Object.values(RESOURCE_COVERS).join("|")})` +} + let getFontsGlob = function () { return `${RESOURCE_FONTS_DIRNAME}/*.ttf` } @@ -362,6 +367,18 @@ if (!args.defaulted.lang } } +let sortResourcesByPattern = function (resources, resourceIds) { + return resources.sort((a, b) => { + const indexA = resourceIds.findIndex((pattern) => + picomatch(pattern)(a.id) + ); + const indexB = resourceIds.findIndex((pattern) => + picomatch(pattern)(b.id) + ); + return indexA - indexB; + }); +} + export { parseResourcePath, isMainModule, @@ -370,6 +387,7 @@ export { getResourceTypesGlob, getPositiveCoverImagesGlob, getNegativeCoverImagesGlob, + getPositiveCoverAndLogoImagesGlob, getFontsGlob, escapeAssetPathForSed, getImageRatio, @@ -381,4 +399,5 @@ export { getCurrentQuarter, getNextQuarter, deepMerge, + sortResourcesByPattern, } \ No newline at end of file diff --git a/ops/sync/transfer-resource-assets.js b/ops/sync/transfer-resource-assets.js index 3055a1957c..ef26cc4c78 100644 --- a/ops/sync/transfer-resource-assets.js +++ b/ops/sync/transfer-resource-assets.js @@ -6,7 +6,7 @@ import yaml from "js-yaml" import ttfMeta from "ttfmeta" import { fdir } from "fdir" import { getCategoryInfo } from "../deploy/deploy-categories.js" -import { parseResourcePath, getResourceTypesGlob, getPositiveCoverImagesGlob, getFontsGlob, determineFontWeight } from "../helpers/helpers.js" +import { parseResourcePath, getResourceTypesGlob, getPositiveCoverImagesGlob, getPositiveCoverAndLogoImagesGlob, getFontsGlob, determineFontWeight } from "../helpers/helpers.js" import { ASSETS_URL, SOURCE_DIR, @@ -45,8 +45,8 @@ let transferCategoriesAssets = async function () { const categoriesImageAssets = new fdir() .withBasePath() .withRelativePaths() - .withMaxDepth(4) - .glob(`**/${CATEGORIES_DIRNAME}/**/assets/${getPositiveCoverImagesGlob()}`) + .withMaxDepth(6) + .glob(`**/${CATEGORIES_DIRNAME}/**/assets/${getPositiveCoverAndLogoImagesGlob()}`) .crawl(SOURCE_DIR) .sync(); @@ -58,7 +58,6 @@ let transferCategoriesAssets = async function () { return category }, {}) - for (let category of Object.keys(categories)) { const categoryPath = parseResourcePath(category) const categoryInfoFile = `${SOURCE_DIR}/${categoryPath.language}/${CATEGORIES_DIRNAME}/${categoryPath.title}/${CATEGORY_INFO_FILENAME}` @@ -71,14 +70,20 @@ let transferCategoriesAssets = async function () { for (let categoryImageAsset of categories[category]) { const imageAssetPath = parseResourcePath(categoryImageAsset) + if (imageAssetPath.document === "logo.png") { + categoryInfo.logo = `${ASSETS_URL}/${categoryInfo.language}/${AUTHORS_DIRNAME}/${categoryInfo.title}/${AUTHORS_ASSETS_DIRNAME}/${imageAssetPath.document}` + if (mode === "remote") fs.removeSync(`${SOURCE_DIR}/${categoryInfo}`) + } + Object.keys(RESOURCE_COVERS).map(k => { - if (imageAssetPath.section === RESOURCE_COVERS[k]) { - categoryInfo.covers[getCoverKey(RESOURCE_COVERS[k])] = `${ASSETS_URL}/${categoryPath.language}/${CATEGORIES_DIRNAME}/${categoryPath.title}/${CATEGORY_ASSETS_DIRNAME}/${imageAssetPath.section}` + if (imageAssetPath.document === RESOURCE_COVERS[k]) { + categoryInfo.covers[getCoverKey(RESOURCE_COVERS[k])] = `${ASSETS_URL}/${categoryPath.language}/${CATEGORIES_DIRNAME}/${categoryPath.title}/${CATEGORY_ASSETS_DIRNAME}/${imageAssetPath.document}` if (mode === "remote") fs.removeSync(`${SOURCE_DIR}/${categoryImageAsset}`) } }) } - if (mode === "remote") fs.outputFileSync(categoryInfoFile, yaml.dump(categoryInfo)) + // if (mode === "remote") + fs.outputFileSync(categoryInfoFile, yaml.dump(categoryInfo)) } } @@ -86,8 +91,8 @@ let transferAuthorsAssets = async function () { const authorsImageAssets = new fdir() .withBasePath() .withRelativePaths() - .withMaxDepth(4) - .glob(`**/${AUTHORS_DIRNAME}/**/assets/${getPositiveCoverImagesGlob()}`) + .withMaxDepth(6) + .glob(`**/${AUTHORS_DIRNAME}/**/assets/${getPositiveCoverAndLogoImagesGlob()}`) .crawl(SOURCE_DIR) .sync(); @@ -99,7 +104,6 @@ let transferAuthorsAssets = async function () { return author }, {}) - for (let author of Object.keys(authors)) { const authorPath = parseResourcePath(author) const authorInfoFile = `${SOURCE_DIR}/${authorPath.language}/${AUTHORS_DIRNAME}/${authorPath.title}/${AUTHORS_INFO_FILENAME}` @@ -112,9 +116,14 @@ let transferAuthorsAssets = async function () { for (let authorImageAsset of authors[author]) { const imageAssetPath = parseResourcePath(authorImageAsset) + if (imageAssetPath.document === "logo.png") { + authorInfo.logo = `${ASSETS_URL}/${authorPath.language}/${AUTHORS_DIRNAME}/${authorPath.title}/${AUTHORS_ASSETS_DIRNAME}/${imageAssetPath.document}` + if (mode === "remote") fs.removeSync(`${SOURCE_DIR}/${authorImageAsset}`) + } + Object.keys(RESOURCE_COVERS).map(k => { - if (imageAssetPath.section === RESOURCE_COVERS[k]) { - authorInfo.covers[getCoverKey(RESOURCE_COVERS[k])] = `${ASSETS_URL}/${authorPath.language}/${AUTHORS_DIRNAME}/${authorPath.title}/${AUTHORS_ASSETS_DIRNAME}/${imageAssetPath.section}` + if (imageAssetPath.document === RESOURCE_COVERS[k]) { + authorInfo.covers[getCoverKey(RESOURCE_COVERS[k])] = `${ASSETS_URL}/${authorPath.language}/${AUTHORS_DIRNAME}/${authorPath.title}/${AUTHORS_ASSETS_DIRNAME}/${imageAssetPath.document}` if (mode === "remote") fs.removeSync(`${SOURCE_DIR}/${authorImageAsset}`) } }) diff --git a/src/en/authors/README.md b/src/en/authors/README.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/en/authors/sspm/feed.yml b/src/en/authors/sspm/feed.yml index 5cd8724fb0..f1ab9ad1a8 100644 --- a/src/en/authors/sspm/feed.yml +++ b/src/en/authors/sspm/feed.yml @@ -1,3 +1,22 @@ --- - - group: Other resources +title: Personal Ministries +groups: + - group: Global TMI + view: tile + resources: + - en-pm-making-disciples-101 + - en-pm-global-tmi-church* + - en-pm-global-tmi* + - group: GROW Series + view: folio + resources: + - en-pm-discipleship-handbook + - en-pm-spread-the-word + - en-pm-fundamentals-of-faith + - group: Keys Series + view: folio + resources: + - en-pm-keys-* + - group: Other Resources + view: square scope: resource \ No newline at end of file diff --git a/src/en/authors/sspm/info.yml b/src/en/authors/sspm/info.yml index 2075f48bae..c8cbe9cbd3 100644 --- a/src/en/authors/sspm/info.yml +++ b/src/en/authors/sspm/info.yml @@ -1,11 +1,30 @@ id: sspm title: Sabbath School and Personal Ministries +links: + - title: Official website + url: https://sabbathschoolpersonalministires.org + - title: Global TMI + url: https://globaltmi.org + - title: GROW + url: https://grow.adventist.org +details: + - title: About + content: >- + Personal Ministries is a facet of the church whose origin can be traced to + the beginning of Seventh-day Adventist® history in the 1860s. It endeavors + to inspire, motivate, equip, train, and mobilize all members for dynamic + Christian service with the conviction that “The church of Christ is + organized for service” (_Ministry of Healing_, p. 148) and “Every son and + daughter of God is called to be a missionary; we are called to the service + of God and our fellow men” (_The Ministry of Healing_, p. 395). +primaryColor: '#2F557F' +primaryColorDark: '#183656' covers: landscape: >- - https://sabbath-school-stage.adventech.io/api/v2/en/authors/sspm/assets/cover-landscape.png + https://sabbath-school-resources-assets.adventech.io/en/authors/sspm/assets/cover-landscape.png square: >- - https://sabbath-school-stage.adventech.io/api/v2/en/authors/sspm/assets/cover-square.png + https://sabbath-school-resources-assets.adventech.io/en/authors/sspm/assets/cover-square.png portrait: >- - https://sabbath-school-stage.adventech.io/api/v2/en/authors/sspm/assets/cover.png - splash: >- - https://sabbath-school-stage.adventech.io/api/v2/en/authors/sspm/assets/splash.png + https://sabbath-school-resources-assets.adventech.io/en/authors/sspm/assets/cover.png +logo: >- + https://sabbath-school-resources-assets.adventech.io/en/authors/sspm/assets/logo.png?update diff --git a/src/en/categories/README.md b/src/en/categories/README.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/en/categories/love/feed.yml b/src/en/categories/love/feed.yml deleted file mode 100644 index 7b6ec1e1dd..0000000000 --- a/src/en/categories/love/feed.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- - - group: Devotionals - scope: resource - kind: devotional - - group: Other resources - scope: resource - - group: Found within titles - scope: document \ No newline at end of file diff --git a/src/en/categories/love/info.yml b/src/en/categories/love/info.yml deleted file mode 100644 index b67e2a55c6..0000000000 --- a/src/en/categories/love/info.yml +++ /dev/null @@ -1,11 +0,0 @@ -id: love -title: Love -covers: - landscape: >- - https://sabbath-school-stage.adventech.io/api/v2/en/categories/love/assets/cover-landscape.png - square: >- - https://sabbath-school-stage.adventech.io/api/v2/en/categories/love/assets/cover-square.png - portrait: >- - https://sabbath-school-stage.adventech.io/api/v2/en/categories/love/assets/cover.png - splash: >- - https://sabbath-school-stage.adventech.io/api/v2/en/categories/love/assets/splash.png diff --git a/src/en/categories/prayer/feed.yml b/src/en/categories/prayer/feed.yml new file mode 100644 index 0000000000..5cd8724fb0 --- /dev/null +++ b/src/en/categories/prayer/feed.yml @@ -0,0 +1,3 @@ +--- + - group: Other resources + scope: resource \ No newline at end of file diff --git a/src/en/categories/prayer/info.yml b/src/en/categories/prayer/info.yml new file mode 100644 index 0000000000..963a3e1523 --- /dev/null +++ b/src/en/categories/prayer/info.yml @@ -0,0 +1,13 @@ +id: prayer +title: Prayer +primaryColor: '#C29FFF' +primaryColorDark: '#814FD8' +covers: + landscape: >- + https://sabbath-school-resources-assets.adventech.io/en/categories/prayer/assets/cover-landscape.png + square: >- + https://sabbath-school-resources-assets.adventech.io/en/categories/prayer/assets/cover-square.png + portrait: >- + https://sabbath-school-resources-assets.adventech.io/en/categories/prayer/assets/cover.png + splash: >- + https://sabbath-school-resources-assets.adventech.io/en/categories/prayer/assets/splash.png diff --git a/src/en/devo/test/info.yml b/src/en/devo/test/info.yml index 6e2f0ada6f..630582ab2c 100644 --- a/src/en/devo/test/info.yml +++ b/src/en/devo/test/info.yml @@ -6,7 +6,7 @@ primaryColor: '#333333' primaryColorDark: '#000000' kind: devotional categories: - - love + - prayer featuredResources: - en/pm/discipleship-handbook - en/ss/2024-04 diff --git a/src/en/explore/family-to-family-church-guide/info.yml b/src/en/explore/family-to-family-church-guide/info.yml index d6d67ebd77..81da06f47d 100644 --- a/src/en/explore/family-to-family-church-guide/info.yml +++ b/src/en/explore/family-to-family-church-guide/info.yml @@ -1,7 +1,7 @@ title: Family to Family kind: book -subtitle: Church Guide -description: A Winsome Plan to Help Families Witness in Their Communities +subtitle: Church Guide. A Winsome Plan to Help Families Witness in Their Communities +description: Families are a special gift from God. Despite the challenges at every step of the journey, there is nothing like a family to create a sense of belonging—a place where one is cherished despite one’s share of imperfections. primaryColor: '#818DA7' primaryColorDark: '#50638C' author: afm diff --git a/src/en/explore/family-to-family-family-guide/info.yml b/src/en/explore/family-to-family-family-guide/info.yml index 62871294d4..479cbe3a1f 100644 --- a/src/en/explore/family-to-family-family-guide/info.yml +++ b/src/en/explore/family-to-family-family-guide/info.yml @@ -1,7 +1,7 @@ title: Family to Family kind: book -subtitle: Family Guide -description: A Winsome Plan to Help Families Witness in Their Communities +subtitle: Family Guide. A Winsome Plan to Help Families Witness in Their Communities +description: "Everyone can see that families in our world are under attack. We are inundated with stories and statistics regarding high divorce rates, domestic violence, rebellious children, pornography, and babies born to unwed parents. New research continues to emphasize an old problem: Homes are falling to pieces. Communities are populated with overwhelmed single parents, angry teens, neglected children, etc. And no culture is untouched by these results of broken families." primaryColor: '#575387' primaryColorDark: '#37336E' author: afm diff --git a/src/en/explore/family-worship-kit/info.yml b/src/en/explore/family-worship-kit/info.yml index daa52dfb63..38f038df46 100644 --- a/src/en/explore/family-worship-kit/info.yml +++ b/src/en/explore/family-worship-kit/info.yml @@ -1,9 +1,8 @@ title: Lessons from Jesus kind: book subtitle: Family Worship Kit -description: >- - This fun, kid-friendly, interactive worship kit was prepared by the Center for - Youth Evangelism. +description: + This fun, kid-friendly, interactive worship kit was prepared by the Center for Youth Evangelism. Parents are sometimes challenged in providing meaningful worship experiences for their children. This is an especially difficult time to disciple children when families are quarantined and churches are closed. “Lessons from Jesus” will teach principles from some of Jesus’ parables. This curriculum can help parents disciple their children even when they are not able to attend a church worship service. primaryColor: '#77421D' primaryColorDark: '#1A1A19' covers: diff --git a/src/en/explore/feed.yml b/src/en/explore/feed.yml index 63f5cd5d8a..0f47a0f9cc 100644 --- a/src/en/explore/feed.yml +++ b/src/en/explore/feed.yml @@ -17,4 +17,14 @@ - en-explore-steps-to-discipleship-leader-guide - group: Other Resources view: folio - scope: resource \ No newline at end of file + scope: resource + - group: Authors + view: square + scope: author + authors: + - sspm + - group: Categories + view: tile + scope: category + categories: + - prayer \ No newline at end of file diff --git a/src/en/pm/discipleship-handbook/info.yml b/src/en/pm/discipleship-handbook/info.yml index 4b7c367274..af2b95479d 100644 --- a/src/en/pm/discipleship-handbook/info.yml +++ b/src/en/pm/discipleship-handbook/info.yml @@ -7,7 +7,8 @@ description: The Discipleship kind: book primaryColor: '#7A0119' primaryColorDark: '#1A1A19' -author: sspm +author: + - sspm credits: - name: Copyright value: >-