From 8ceab408bf353ccad22c3b4d904992e548f16e24 Mon Sep 17 00:00:00 2001 From: Charly Martin Date: Wed, 9 Oct 2024 17:33:29 +0100 Subject: [PATCH] WIP --- src/app/_components/Form/FormCombobox.tsx | 2 +- src/app/ecosystem-explorer/page.tsx | 15 +- .../actions/buildMarkdownTemplate.ts | 44 +++++ .../project-form/actions/decrypt.ts | 7 - .../project-form/actions/encrypt.ts | 7 - .../getFolderPaths.ts} | 11 +- .../actions/submitProjectToGithub.ts | 151 +++++++++--------- .../actions/updateProjectOnGitHub.ts | 56 ------- .../project-form/components/CreateForm.tsx | 7 +- .../components/EcosystemProjectForm.tsx | 17 +- .../project-form/components/UpdateForm.tsx | 11 +- .../components/UpdateFormSelect.tsx | 25 +-- .../project-form/constants/index.ts | 2 - .../hooks/useSubmitEcosystemProjectForm.ts | 139 ++++++++-------- .../project-form/schema/form.ts | 2 + .../utils/buildPlaceholderFileFromPath.ts | 11 -- .../{services/github => }/utils/fileUtils.ts | 35 +++- .../project-form/utils/formatFormData.ts | 33 ++++ .../getEcosystemMarkdownTemplate.ts} | 20 +-- .../projects/built-different.md | 3 +- 20 files changed, 308 insertions(+), 290 deletions(-) create mode 100644 src/app/ecosystem-explorer/project-form/actions/buildMarkdownTemplate.ts delete mode 100644 src/app/ecosystem-explorer/project-form/actions/decrypt.ts delete mode 100644 src/app/ecosystem-explorer/project-form/actions/encrypt.ts rename src/app/ecosystem-explorer/project-form/{services/github/utils/pathUtils.ts => actions/getFolderPaths.ts} (55%) delete mode 100644 src/app/ecosystem-explorer/project-form/actions/updateProjectOnGitHub.ts delete mode 100644 src/app/ecosystem-explorer/project-form/utils/buildPlaceholderFileFromPath.ts rename src/app/ecosystem-explorer/project-form/{services/github => }/utils/fileUtils.ts (54%) create mode 100644 src/app/ecosystem-explorer/project-form/utils/formatFormData.ts rename src/app/ecosystem-explorer/project-form/{services/github/utils/markdownUtils.ts => utils/getEcosystemMarkdownTemplate.ts} (90%) diff --git a/src/app/_components/Form/FormCombobox.tsx b/src/app/_components/Form/FormCombobox.tsx index 2a1142065..b1bb63091 100644 --- a/src/app/_components/Form/FormCombobox.tsx +++ b/src/app/_components/Form/FormCombobox.tsx @@ -69,7 +69,7 @@ export function FormCombobox({ {({ option }) => ( ) diff --git a/src/app/ecosystem-explorer/project-form/actions/buildMarkdownTemplate.ts b/src/app/ecosystem-explorer/project-form/actions/buildMarkdownTemplate.ts new file mode 100644 index 000000000..df6e3763b --- /dev/null +++ b/src/app/ecosystem-explorer/project-form/actions/buildMarkdownTemplate.ts @@ -0,0 +1,44 @@ +'use server' + +import { encrypt } from '@/utils/encryption' + +import type { FormattedFormData } from '../utils/formatFormData' +import { getEcosystemMarkdownTemplate } from '../utils/getEcosystemMarkdownTemplate' + +type Timestamps = { + createdOn: Date + updatedOn: Date + publishedOn: Date +} + +type MarkdownTemplateParams = { + formattedData: FormattedFormData + imagePath: string + timestamps: Timestamps +} + +export async function buildMarkdownTemplate({ + formattedData, + imagePath, + timestamps, +}: MarkdownTemplateParams) { + return getEcosystemMarkdownTemplate({ + encryptedEmail: encrypt(formattedData.email), + encryptedName: encrypt(formattedData.name), + projectName: formattedData.projectName, + imagePath, + category: formattedData.category, + subcategories: formattedData.subcategories, + tech: formattedData.tech, + shortDescription: formattedData.shortDescription, + longDescription: formattedData.longDescription, + yearJoined: formattedData.yearJoinedISO, + websiteUrl: formattedData.websiteUrl, + youtubeUrl: formattedData.youtubeEmbedUrl, + githubUrl: formattedData.githubUrl, + xUrl: formattedData.xUrl, + createdOn: timestamps.createdOn.toISOString(), + updatedOn: timestamps.updatedOn.toISOString(), + publishedOn: timestamps.publishedOn.toISOString(), + }) +} diff --git a/src/app/ecosystem-explorer/project-form/actions/decrypt.ts b/src/app/ecosystem-explorer/project-form/actions/decrypt.ts deleted file mode 100644 index 79d77e177..000000000 --- a/src/app/ecosystem-explorer/project-form/actions/decrypt.ts +++ /dev/null @@ -1,7 +0,0 @@ -'use server' - -import { decrypt as _decrypt } from '@/_utils/encryption' - -export async function decrypt(value: string) { - return _decrypt(value) -} diff --git a/src/app/ecosystem-explorer/project-form/actions/encrypt.ts b/src/app/ecosystem-explorer/project-form/actions/encrypt.ts deleted file mode 100644 index 27d8181a7..000000000 --- a/src/app/ecosystem-explorer/project-form/actions/encrypt.ts +++ /dev/null @@ -1,7 +0,0 @@ -'use server' - -import { encrypt as _encrypt } from '@/_utils/encryption' - -export async function encrypt(value: string) { - return _encrypt(value) -} diff --git a/src/app/ecosystem-explorer/project-form/services/github/utils/pathUtils.ts b/src/app/ecosystem-explorer/project-form/actions/getFolderPaths.ts similarity index 55% rename from src/app/ecosystem-explorer/project-form/services/github/utils/pathUtils.ts rename to src/app/ecosystem-explorer/project-form/actions/getFolderPaths.ts index ff97c847e..0d022cb8a 100644 --- a/src/app/ecosystem-explorer/project-form/services/github/utils/pathUtils.ts +++ b/src/app/ecosystem-explorer/project-form/actions/getFolderPaths.ts @@ -1,6 +1,6 @@ -import { z } from 'zod' +'use server' -import { ECOSYSTEM_PROJECTS_DIRECTORY_PATH } from '@/constants/paths' +import { z } from 'zod' import configJson from '@/data/cmsConfigSchema.json' @@ -9,12 +9,11 @@ const pathConfigSchema = z.object({ public_folder: z.string(), }) -export function getFolderPaths() { +export async function getFolderPaths() { const { media_folder, public_folder } = pathConfigSchema.parse(configJson) return { - mediaFolder: media_folder, - publicFolder: public_folder, - ecosystemFolder: ECOSYSTEM_PROJECTS_DIRECTORY_PATH, + publicAssetsFolder: media_folder, // public/assets/images + assetsFolder: public_folder, // assets/images } as const } diff --git a/src/app/ecosystem-explorer/project-form/actions/submitProjectToGithub.ts b/src/app/ecosystem-explorer/project-form/actions/submitProjectToGithub.ts index 632098f34..612aaf923 100644 --- a/src/app/ecosystem-explorer/project-form/actions/submitProjectToGithub.ts +++ b/src/app/ecosystem-explorer/project-form/actions/submitProjectToGithub.ts @@ -1,115 +1,114 @@ 'use server' -import slugify from 'slugify' +import { ECOSYSTEM_PROJECTS_DIRECTORY_PATH } from '@/constants/paths' import { getTodayISO } from '@/utils/dateUtils' -import { encrypt } from '@/utils/encryption' import { createBlob } from '../services/github/api/createBlob' import { createCommit } from '../services/github/api/createCommit' import { createPR } from '../services/github/api/createPr' import { createTreeBlobs } from '../services/github/api/createTreeBlobs' import { getLatestCommitOnMain } from '../services/github/api/getLatestCommitOnMain' -import type { AllowedImageFormats } from '../services/github/utils/fileUtils' -import { - getMarkdownTemplate, - type MarkdownTemplateParams, -} from '../services/github/utils/markdownUtils' -import { getFolderPaths } from '../services/github/utils/pathUtils' - -export type ProjectData = { - name: string - email: string - timestampISO: string - yearJoinedISO: string - projectName: MarkdownTemplateParams['projectName'] - category: MarkdownTemplateParams['category'] - subcategories: MarkdownTemplateParams['subcategories'] - tech: MarkdownTemplateParams['tech'] - shortDescription: MarkdownTemplateParams['shortDescription'] - longDescription: MarkdownTemplateParams['longDescription'] - websiteUrl: MarkdownTemplateParams['websiteUrl'] - youtubeEmbedUrl: MarkdownTemplateParams['youtubeUrl'] - githubUrl: MarkdownTemplateParams['githubUrl'] - xUrl: MarkdownTemplateParams['xUrl'] -} - -type ProjectLogo = { - base64: string - format: AllowedImageFormats -} +import type { FormattedLogo } from '../utils/fileUtils' type SubmitProjectParams = { - data: ProjectData - logo: ProjectLogo + slug: string + markdownTemplate: string + logo?: { + path: string + base64: FormattedLogo['base64'] + } } export async function submitProjectToGithub({ - data, + slug, + markdownTemplate, logo, }: SubmitProjectParams) { - const { mediaFolder, ecosystemFolder, publicFolder } = getFolderPaths() const todayISO = getTodayISO() - - const slug = slugify(data.projectName, { - lower: true, - strict: true, - }) - const branchName = `ecosystem-submission/${slug}-${todayISO}` - const markdownTemplate = getMarkdownTemplate({ - encryptedEmail: encrypt(data.email), - encryptedName: encrypt(data.name), - projectName: data.projectName, - imagePath: `${publicFolder}/${slug}.${logo.format}`, - category: data.category, - subcategories: data.subcategories, - tech: data.tech, - shortDescription: data.shortDescription, - longDescription: data.longDescription, - yearJoined: data.yearJoinedISO, - websiteUrl: data.websiteUrl, - youtubeUrl: data.youtubeEmbedUrl, - githubUrl: data.githubUrl, - xUrl: data.xUrl, - createdOn: data.timestampISO, - updatedOn: data.timestampISO, - publishedOn: data.timestampISO, - }) + const markdownBlob = await createBlob(markdownTemplate, 'utf-8') + const treeBlobs = [ + { + path: `${ECOSYSTEM_PROJECTS_DIRECTORY_PATH}/${slug}.md`, + sha: markdownBlob.sha, + }, + ] + + if (logo) { + const imageBlob = await createBlob(logo.base64, 'base64') + treeBlobs.push({ + sha: imageBlob.sha, + path: logo.path, + }) + } const latestCommitOnMain = await getLatestCommitOnMain() - const [markdownBlob, imageBlob] = await Promise.all([ - createBlob(markdownTemplate, 'utf-8'), - createBlob(logo.base64, 'base64'), - ]) - const newTree = await createTreeBlobs({ baseTreeSha: latestCommitOnMain.commit.tree.sha, - newBlobs: [ - { - path: `${mediaFolder}/${slug}.${logo.format}`, - sha: imageBlob.sha, - }, - { - path: `${ecosystemFolder}/${slug}.md`, - sha: markdownBlob.sha, - }, - ], + newBlobs: treeBlobs, }) const newCommit = await createCommit({ parentCommitSha: latestCommitOnMain.sha, treeSha: newTree.sha, - message: `Ecosystem Project Form Submission: ${data.projectName}`, + message: `Ecosystem Project Form Submission: ${slug}`, }) const newPullRequest = await createPR({ - title: `Ecosystem Project Form Submission: ${data.projectName}`, + title: `Ecosystem Project Form Submission: ${slug}`, commitSha: newCommit.sha, branchName, }) return newPullRequest } + +// const markdownTemplate = getEcosystemMarkdownTemplate({ +// encryptedEmail: encrypt(data.email), +// encryptedName: encrypt(data.name), +// projectName: projectName, +// imagePath: `${assetsFolder}/${slug}.${formattedLogo.format}`, +// category: data.category, +// subcategories: data.subcategories, +// tech: data.tech, +// shortDescription: data.shortDescription, +// longDescription: data.longDescription, +// yearJoined: data.yearJoinedISO, +// websiteUrl: data.websiteUrl, +// youtubeUrl: data.youtubeEmbedUrl, +// githubUrl: data.githubUrl, +// xUrl: data.xUrl, +// createdOn: data.timestampISO, +// updatedOn: data.timestampISO, +// publishedOn: data.timestampISO, +// }) + +// import { +// getEcosystemMarkdownTemplate, +// type MarkdownTemplateParams, +// } from '../utils/getEcosystemMarkdownTemplate' + +// type ProjectData = { +// name: string +// email: string +// timestampISO: string +// yearJoinedISO: string +// projectName: MarkdownTemplateParams['projectName'] +// category: MarkdownTemplateParams['category'] +// subcategories: MarkdownTemplateParams['subcategories'] +// tech: MarkdownTemplateParams['tech'] +// shortDescription: MarkdownTemplateParams['shortDescription'] +// longDescription: MarkdownTemplateParams['longDescription'] +// websiteUrl: MarkdownTemplateParams['websiteUrl'] +// youtubeEmbedUrl: MarkdownTemplateParams['youtubeUrl'] +// githubUrl: MarkdownTemplateParams['githubUrl'] +// xUrl: MarkdownTemplateParams['xUrl'] +// } + +// type ProjectLogo = { +// base64: string +// format: AllowedImageFormats +// } diff --git a/src/app/ecosystem-explorer/project-form/actions/updateProjectOnGitHub.ts b/src/app/ecosystem-explorer/project-form/actions/updateProjectOnGitHub.ts deleted file mode 100644 index 4425088d7..000000000 --- a/src/app/ecosystem-explorer/project-form/actions/updateProjectOnGitHub.ts +++ /dev/null @@ -1,56 +0,0 @@ -'use server' - -import type { AllowedImageFormats } from '../services/github/utils/fileUtils' -import { type MarkdownTemplateParams } from '../services/github/utils/markdownUtils' - -import { submitProjectToGithub } from './submitProjectToGithub' - -type ProjectData = { - name: string - email: string - timestampISO: string - yearJoinedISO: string - projectName: MarkdownTemplateParams['projectName'] - category: MarkdownTemplateParams['category'] - subcategories: MarkdownTemplateParams['subcategories'] - tech: MarkdownTemplateParams['tech'] - shortDescription: MarkdownTemplateParams['shortDescription'] - longDescription: MarkdownTemplateParams['longDescription'] - websiteUrl: MarkdownTemplateParams['websiteUrl'] - youtubeEmbedUrl: MarkdownTemplateParams['youtubeUrl'] - githubUrl: MarkdownTemplateParams['githubUrl'] - xUrl: MarkdownTemplateParams['xUrl'] -} - -type ProjectLogo = { - base64: string - format: AllowedImageFormats -} - -type UpdateOptions = { - projectTitleHasChanged: boolean - logoHasChanged: boolean -} - -type UpdateProjectParams = { - data: ProjectData - logo: ProjectLogo - options: UpdateOptions -} - -export async function updateProjectOnGitHub({ - data, - logo, - options, -}: UpdateProjectParams) { - // TODO - // 1. If the project title has changed, we need to resubmit everything as if it was an initial submission - // => the markdown file and logo file will have different names - - // 2. If the uploaded logo has changed, we also need to resubmit everything - // 2.a -> if only the logo changes maybe we can just re-upload the file with the markdown template? - - // If only the project text has changed, we can simply re-upload the markdown file without the logo. - - return submitProjectToGithub({ data, logo }) -} diff --git a/src/app/ecosystem-explorer/project-form/components/CreateForm.tsx b/src/app/ecosystem-explorer/project-form/components/CreateForm.tsx index 09517f277..91b390689 100644 --- a/src/app/ecosystem-explorer/project-form/components/CreateForm.tsx +++ b/src/app/ecosystem-explorer/project-form/components/CreateForm.tsx @@ -13,16 +13,13 @@ export function CreateForm() { getInitialFormData(), ) - const { createProject } = useSubmitEcosystemProjectForm() + const { create } = useSubmitEcosystemProjectForm() if (!initialFormData) { return } return ( - + ) } diff --git a/src/app/ecosystem-explorer/project-form/components/EcosystemProjectForm.tsx b/src/app/ecosystem-explorer/project-form/components/EcosystemProjectForm.tsx index 34eac300e..80c71e99f 100644 --- a/src/app/ecosystem-explorer/project-form/components/EcosystemProjectForm.tsx +++ b/src/app/ecosystem-explorer/project-form/components/EcosystemProjectForm.tsx @@ -34,21 +34,18 @@ import { FormSection } from './FormSection' type StringOrUndefined = string | undefined -type Logo = EcosystemProjectFormData['files'][0] - type EcosystemProjectFormProps = { - initial: { - formData: EcosystemProjectFormDataWithoutFiles - logo?: Logo - } + initialFormData: EcosystemProjectFormDataWithoutFiles + logo?: File onSubmit: ( - data: EcosystemProjectFormData, + formData: EcosystemProjectFormData, formState: FormState, ) => void } export function EcosystemProjectForm({ - initial, + initialFormData, + logo, onSubmit, }: EcosystemProjectFormProps) { const { data } = useSWR('categories', getCategoryData) @@ -56,8 +53,8 @@ export function EcosystemProjectForm({ const form = useForm({ resolver: zodResolver(EcosystemProjectFormSchema), defaultValues: { - ...initial.formData, - files: initial.logo ? [initial.logo] : [], + ...initialFormData, + files: logo ? [logo] : [], }, }) const isSubmitting = form.formState.isSubmitting diff --git a/src/app/ecosystem-explorer/project-form/components/UpdateForm.tsx b/src/app/ecosystem-explorer/project-form/components/UpdateForm.tsx index 58c3ad6ed..617ed135e 100644 --- a/src/app/ecosystem-explorer/project-form/components/UpdateForm.tsx +++ b/src/app/ecosystem-explorer/project-form/components/UpdateForm.tsx @@ -8,7 +8,7 @@ import { getInitialFormData } from '../actions/getInitialFormData' import { getProjectData } from '../actions/getProjectData' import { ACTIONS } from '../constants' import { useSubmitEcosystemProjectForm } from '../hooks/useSubmitEcosystemProjectForm' -import { buildPlaceholderFileFromPath } from '../utils/buildPlaceholderFileFromPath' +import { getFileFromPath } from '../utils/fileUtils' import { EcosystemProjectForm } from './EcosystemProjectForm' @@ -17,7 +17,7 @@ type UpdateFormProps = { } export function UpdateForm({ slug }: UpdateFormProps) { - const { updateProject } = useSubmitEcosystemProjectForm() + const { update } = useSubmitEcosystemProjectForm() const { data: project } = useSWR(ACTIONS.GET_PROJECTS_DATA + slug, () => getProjectData(slug), @@ -29,7 +29,7 @@ export function UpdateForm({ slug }: UpdateFormProps) { ) const { data: logo } = useSWR(project ? ACTIONS.GET_LOGO + slug : null, () => - buildPlaceholderFileFromPath(project?.image?.src), + getFileFromPath(project?.image?.src), ) if (!initialFormData || !project) { @@ -38,8 +38,9 @@ export function UpdateForm({ slug }: UpdateFormProps) { return ( ) } diff --git a/src/app/ecosystem-explorer/project-form/components/UpdateFormSelect.tsx b/src/app/ecosystem-explorer/project-form/components/UpdateFormSelect.tsx index a18191a8f..4aa414445 100644 --- a/src/app/ecosystem-explorer/project-form/components/UpdateFormSelect.tsx +++ b/src/app/ecosystem-explorer/project-form/components/UpdateFormSelect.tsx @@ -16,7 +16,7 @@ const MARKETING_DEPARTMENT_EMAIL = 'marketing@fil.org' const URL_QUERY_NAME = 'project' export function UpdateFormSelect() { - const { data: projects } = useSWR(ACTIONS.GET_PROJECTS_DATA, getProjectsData) // Fetch once on page load and never again + const { data: projects } = useSWR(ACTIONS.GET_PROJECTS_DATA, getProjectsData) const [project, setProject] = useQueryState( URL_QUERY_NAME, @@ -29,14 +29,23 @@ export function UpdateFormSelect() { const selectedProject = projects.find((p) => p.slug === project) + const options = projects.map((project) => ({ + id: project.slug, + name: project.title, + })) + const selectedOption = { + id: selectedProject?.slug || '', + name: selectedProject?.title || '', + } + return ( <> - Welcome back! Please select your project. Don't see it? Select - "Submit Project" above, or reach out to{' '} + {`Welcome back! Please select your project. Don't see it? Select + Submit Project" above, or reach out to "`} ({ - id: project.slug, - name: project.title, - }))} - value={{ - id: selectedProject?.slug || '', - name: selectedProject?.title || '', - }} + options={options} + value={selectedOption} onChange={(option) => { if (option) { setProject(option.id) diff --git a/src/app/ecosystem-explorer/project-form/constants/index.ts b/src/app/ecosystem-explorer/project-form/constants/index.ts index a81df4953..033e6d4ca 100644 --- a/src/app/ecosystem-explorer/project-form/constants/index.ts +++ b/src/app/ecosystem-explorer/project-form/constants/index.ts @@ -15,5 +15,3 @@ export const ACTIONS = { GET_PROJECTS_DATA: 'projects', GET_LOGO: 'logo', } as const - -export const EMPTY_PLACEHOLDER_FILE_NAME = '' diff --git a/src/app/ecosystem-explorer/project-form/hooks/useSubmitEcosystemProjectForm.ts b/src/app/ecosystem-explorer/project-form/hooks/useSubmitEcosystemProjectForm.ts index c17285387..8f0fa612a 100644 --- a/src/app/ecosystem-explorer/project-form/hooks/useSubmitEcosystemProjectForm.ts +++ b/src/app/ecosystem-explorer/project-form/hooks/useSubmitEcosystemProjectForm.ts @@ -2,114 +2,113 @@ import * as Sentry from '@sentry/nextjs' import type { FormState } from 'react-hook-form' - -import { createDateFromYear } from '@/utils/dateUtils' +import slugify from 'slugify' import { useUpdateSearchParams } from '@/hooks/useUpdateSearchParams' +import { buildMarkdownTemplate } from '../actions/buildMarkdownTemplate' +import { getFolderPaths } from '../actions/getFolderPaths' +import { getProjectData } from '../actions/getProjectData' import { submitProjectToGithub } from '../actions/submitProjectToGithub' -import { updateProjectOnGitHub } from '../actions/updateProjectOnGitHub' -import { EMPTY_PLACEHOLDER_FILE_NAME } from '../constants' import type { EcosystemProjectFormData } from '../schema/form' -import { - convertToBase64, - getFileFormat, -} from '../services/github/utils/fileUtils' -import { formatYoutubeEmbedUrl } from '../utils/formatYoutubeUrl' +import { formatLogo } from '../utils/fileUtils' +import { formatFormData } from '../utils/formatFormData' export function useSubmitEcosystemProjectForm() { const { updateSearchParams } = useUpdateSearchParams() - async function createProject(data: EcosystemProjectFormData) { - const logo = data.files[0] - const yearJoined = Number(data.yearJoined.name) + return { create, update } + + async function create(formData: EcosystemProjectFormData) { + const { files, ...formDataWithoutFiles } = formData + const nowTimestamp = new Date() + + const slug = slugify(formData.projectName, { lower: true, strict: true }) try { - const pullRequest = await submitProjectToGithub({ - data: { - name: data.name, - email: data.email, - projectName: data.projectName, - category: data.category.id, - subcategories: [data.topic.id], - tech: buildArrayFromTruthyKeys(data.tech), - shortDescription: data.briefSummary, - longDescription: data.networkUseCase, - yearJoinedISO: createDateFromYear(yearJoined).toISOString(), - websiteUrl: data.websiteUrl, - youtubeEmbedUrl: formatYoutubeEmbedUrl(data.youtubeUrl), - githubUrl: data.githubUrl, - xUrl: data.xUrl, - timestampISO: new Date().toISOString(), + const formattedLogo = await formatLogo(files) + const formattedData = formatFormData(formDataWithoutFiles) + + const { publicAssetsFolder, assetsFolder } = await getFolderPaths() + + const markdownTemplate = await buildMarkdownTemplate({ + formattedData, + imagePath: `${assetsFolder}/${slug}.${formattedLogo.format}`, + timestamps: { + createdOn: nowTimestamp, + updatedOn: nowTimestamp, + publishedOn: nowTimestamp, }, + }) + + const pullRequest = await submitProjectToGithub({ + slug, + markdownTemplate, logo: { - base64: await convertToBase64(logo), - format: getFileFormat(logo.name), + base64: formattedLogo.base64, + path: `${publicAssetsFolder}/${slug}.${formattedLogo.format}`, }, }) + updateSearchParams({ status: 'success', prNumber: pullRequest.number }) } catch (error) { - console.error('Error in submitProjectToGithub:', error) + console.error('useSubmitEcosystemProjectForm - create', error) Sentry.captureException(error) updateSearchParams({ status: 'error', message: - "We couldn't submit your project. Please try again or email us at info@fil.org", + "We couldn't submit your information. Please try again or email us at info@fil.org", }) } } - async function updateProject( - data: EcosystemProjectFormData, + async function update( + formData: EcosystemProjectFormData, formState: FormState, ) { - const logo = data.files[0] - const yearJoined = Number(data.yearJoined.name) + const projectNameHasChanged = formState.dirtyFields.projectName + const imageHasChanged = formState.dirtyFields.files + + if (projectNameHasChanged) { + return create(formData) + } + + const { files, ...formDataWithoutFiles } = formData + const slug = slugify(formData.projectName, { lower: true, strict: true }) try { - const pullRequest = await updateProjectOnGitHub({ - data: { - name: data.name, - email: data.email, - projectName: data.projectName, - category: data.category.id, - subcategories: [data.topic.id], - tech: buildArrayFromTruthyKeys(data.tech), - shortDescription: data.briefSummary, - longDescription: data.networkUseCase, - yearJoinedISO: createDateFromYear(yearJoined).toISOString(), - websiteUrl: data.websiteUrl, - youtubeEmbedUrl: formatYoutubeEmbedUrl(data.youtubeUrl), - githubUrl: data.githubUrl, - xUrl: data.xUrl, - timestampISO: new Date().toISOString(), - }, - logo: { - base64: await convertToBase64(logo), - format: getFileFormat(logo.name), - }, - options: { - projectTitleHasChanged: Boolean(formState.dirtyFields.projectName), - logoHasChanged: logo.name !== EMPTY_PLACEHOLDER_FILE_NAME, + const { image, createdOn, publishedOn } = await getProjectData(slug) + + const { assetsFolder, publicAssetsFolder } = await getFolderPaths() + + const formattedLogo = await formatLogo(files) + const formattedData = formatFormData(formDataWithoutFiles) + + const markdownTemplate = await buildMarkdownTemplate({ + formattedData, + imagePath: + image?.src || `${assetsFolder}/${slug}.${formattedLogo.format}`, + timestamps: { updatedOn: new Date(), createdOn, publishedOn }, + }) + + const pullRequest = await submitProjectToGithub({ + slug, + markdownTemplate, + logo: imageHasChanged && { + base64: formattedLogo.base64, + path: `${publicAssetsFolder}/${slug}.${formattedLogo.format}`, }, }) + updateSearchParams({ status: 'success', prNumber: pullRequest.number }) } catch (error) { - console.error('Error in submitProjectToGithub:', error) + console.error('useSubmitEcosystemProjectForm - update', error) Sentry.captureException(error) updateSearchParams({ status: 'error', message: - "We couldn't submit your project. Please try again or email us at info@fil.org", + "We couldn't submit your information. Please try again or email us at info@fil.org", }) } } - - return { createProject, updateProject } -} - -function buildArrayFromTruthyKeys(object: Record) { - const entries = Object.entries(object) - const truthyValueEntries = entries.filter(([, value]) => Boolean(value)) - return truthyValueEntries.map(([key]) => key) } diff --git a/src/app/ecosystem-explorer/project-form/schema/form.ts b/src/app/ecosystem-explorer/project-form/schema/form.ts index 4cd4e787f..90a8fd048 100644 --- a/src/app/ecosystem-explorer/project-form/schema/form.ts +++ b/src/app/ecosystem-explorer/project-form/schema/form.ts @@ -99,6 +99,8 @@ export type EcosystemProjectFormDataWithoutFiles = Omit< 'files' > +export type EcosystemProjectFormFiles = EcosystemProjectFormData['files'] + function validateYoutubeUrlFormat(url: string) { return url.includes(YOUTUBE_BASE_URL) } diff --git a/src/app/ecosystem-explorer/project-form/utils/buildPlaceholderFileFromPath.ts b/src/app/ecosystem-explorer/project-form/utils/buildPlaceholderFileFromPath.ts deleted file mode 100644 index 8c4f14a7a..000000000 --- a/src/app/ecosystem-explorer/project-form/utils/buildPlaceholderFileFromPath.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { EMPTY_PLACEHOLDER_FILE_NAME } from '../constants' - -export async function buildPlaceholderFileFromPath(path?: string) { - if (!path) { - return - } - - const response = await fetch(path) - const blob = await response.blob() - return new File([blob], EMPTY_PLACEHOLDER_FILE_NAME, { type: blob.type }) -} diff --git a/src/app/ecosystem-explorer/project-form/services/github/utils/fileUtils.ts b/src/app/ecosystem-explorer/project-form/utils/fileUtils.ts similarity index 54% rename from src/app/ecosystem-explorer/project-form/services/github/utils/fileUtils.ts rename to src/app/ecosystem-explorer/project-form/utils/fileUtils.ts index f5c9f4f16..c8eab0054 100644 --- a/src/app/ecosystem-explorer/project-form/services/github/utils/fileUtils.ts +++ b/src/app/ecosystem-explorer/project-form/utils/fileUtils.ts @@ -1,5 +1,34 @@ import path from 'path' +import type { EcosystemProjectFormFiles } from '../schema/form' + +export type AllowedImageFormats = (typeof ALLOWED_IMAGE_FORMATS)[number] + +export type FormattedLogo = Awaited> + +export async function formatLogo(files: EcosystemProjectFormFiles) { + const logo = files[0] + + if (!logo) { + throw new Error('No logo found') + } + + const base64 = await convertToBase64(logo) + const format = getFileFormat(logo.name) + + return { base64, format, name: logo.name } +} + +export async function getFileFromPath(path?: string) { + if (!path) { + return + } + + const response = await fetch(path) + const blob = await response.blob() + return new File([blob], path, { type: blob.type }) +} + export const ALLOWED_IMAGE_FORMATS = [ 'png', 'jpg', @@ -8,9 +37,7 @@ export const ALLOWED_IMAGE_FORMATS = [ 'webp', ] as const -export type AllowedImageFormats = (typeof ALLOWED_IMAGE_FORMATS)[number] - -export function convertToBase64(file: File): Promise { +function convertToBase64(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader() reader.readAsDataURL(file) @@ -23,7 +50,7 @@ export function convertToBase64(file: File): Promise { }) } -export function getFileFormat(fileName: string) { +function getFileFormat(fileName: string) { const fileExtension = path.extname(fileName).slice(1) if (!fileExtension) { diff --git a/src/app/ecosystem-explorer/project-form/utils/formatFormData.ts b/src/app/ecosystem-explorer/project-form/utils/formatFormData.ts new file mode 100644 index 000000000..e88403c9f --- /dev/null +++ b/src/app/ecosystem-explorer/project-form/utils/formatFormData.ts @@ -0,0 +1,33 @@ +import { createDateFromYear } from '@/utils/dateUtils' + +import type { EcosystemProjectFormDataWithoutFiles } from '../schema/form' + +import { formatYoutubeEmbedUrl } from './formatYoutubeUrl' + +export type FormattedFormData = ReturnType + +export function formatFormData(data: EcosystemProjectFormDataWithoutFiles) { + const yearJoined = Number(data.yearJoined.name) + + return { + name: data.name.trim(), + email: data.email.trim(), + projectName: data.projectName.trim(), + category: data.category.id, + subcategories: [data.topic.id], + tech: buildArrayFromTruthyKeys(data.tech), + shortDescription: data.briefSummary.trim(), + longDescription: data.networkUseCase.trim(), + yearJoinedISO: createDateFromYear(yearJoined).toISOString(), + websiteUrl: data.websiteUrl.trim(), + youtubeEmbedUrl: formatYoutubeEmbedUrl(data.youtubeUrl), + githubUrl: data.githubUrl?.trim(), + xUrl: data.xUrl?.trim(), + } +} + +function buildArrayFromTruthyKeys(object: Record) { + const entries = Object.entries(object) + const truthyValueEntries = entries.filter(([, value]) => Boolean(value)) + return truthyValueEntries.map(([key]) => key) +} diff --git a/src/app/ecosystem-explorer/project-form/services/github/utils/markdownUtils.ts b/src/app/ecosystem-explorer/project-form/utils/getEcosystemMarkdownTemplate.ts similarity index 90% rename from src/app/ecosystem-explorer/project-form/services/github/utils/markdownUtils.ts rename to src/app/ecosystem-explorer/project-form/utils/getEcosystemMarkdownTemplate.ts index dad595881..24bce8c7a 100644 --- a/src/app/ecosystem-explorer/project-form/services/github/utils/markdownUtils.ts +++ b/src/app/ecosystem-explorer/project-form/utils/getEcosystemMarkdownTemplate.ts @@ -1,10 +1,9 @@ -import type { AllowedImageFormats } from './fileUtils' - +// Could re-use the value coming from the Zod schema once merged export type MarkdownTemplateParams = { encryptedName: string encryptedEmail: string projectName: string - imagePath: `${string}.${AllowedImageFormats}` + imagePath: string category: string subcategories: Array tech: Array @@ -20,7 +19,13 @@ export type MarkdownTemplateParams = { publishedOn: string } -export function getMarkdownTemplate(data: MarkdownTemplateParams) { +type OptionalValues = { + repo: MarkdownTemplateParams['githubUrl'] + 'video-url': MarkdownTemplateParams['youtubeUrl'] + twitter: MarkdownTemplateParams['xUrl'] +} + +export function getEcosystemMarkdownTemplate(data: MarkdownTemplateParams) { return `--- ${renderValue('title', data.projectName)} ${renderValue('created-on', data.createdOn)} @@ -32,7 +37,6 @@ image: ${renderValue('src', data.imagePath)} ${renderValue('category', data.category)} ${renderArray('subcategories', data.subcategories)} -${renderArray('tags', data.subcategories)} ${renderValue('description', data.shortDescription)} ${renderValue('website', data.websiteUrl)} ${renderArray('tech', data.tech)} @@ -49,12 +53,8 @@ seo: ${data.longDescription.trim()} ` -} -type OptionalValues = { - repo: MarkdownTemplateParams['githubUrl'] - 'video-url': MarkdownTemplateParams['youtubeUrl'] - twitter: MarkdownTemplateParams['xUrl'] + // ${renderArray('tags', data.subcategories)} } function renderOptionalValues(values: OptionalValues) { diff --git a/src/content/ecosystem-explorer/projects/built-different.md b/src/content/ecosystem-explorer/projects/built-different.md index 5e98b1bb5..6ef59ae62 100644 --- a/src/content/ecosystem-explorer/projects/built-different.md +++ b/src/content/ecosystem-explorer/projects/built-different.md @@ -19,7 +19,8 @@ tech: - filecoin year-joined: 2024-03-29T21:28:48.571Z seo: - title: "Built Different: Supporting Decentralized Storage and Peer-to-Peer File + title: + "Built Different: Supporting Decentralized Storage and Peer-to-Peer File Sharing in Africa" description: Built Different creates innovative decentralized applications. ---