diff --git a/.env.example b/.env.example index 9011070..2775ed7 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,4 @@ -SYNC_LIFECYCLE_DOCS= -GITHUB_TOKEN= UI_BRANCH=main CORE_BRANCH=main -LC_DOCS_PUBLISH= NEXT_PUBLIC_DEV_ENV=local DEV_ENV=local diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e7375b4..b1f625e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -39,8 +39,6 @@ jobs: - name: Deploy app run: bun run deploy env: - GITHUB_TOKEN: ${{ secrets.LC_DOCS_PUBLISH }} - SYNC_LIFECYCLE_DOCS: ${{ secrets.LC_DOCS_PUBLISH }} UI_BRANCH: main CORE_BRANCH: main diff --git a/.gitignore b/.gitignore index 502cd62..62b1a12 100644 --- a/.gitignore +++ b/.gitignore @@ -131,8 +131,6 @@ dist .yarn/install-state.gz .pnp.* -src/pages/docs/schema/yaml - src/lib/data src/pages/tags diff --git a/.husky/pre-commit b/.husky/pre-commit index 9537097..6d964ef 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -4,9 +4,7 @@ echo "Running build:prep..." bun run build:prep echo "Staging changes from build:prep..." # add all page changes after build:prep -git add src/pages/articles/*.mdx src/pages/docs/*.mdx -# add all static changes after build:prep -git add src/lib/static +git add src/pages/docs/*.mdx echo "Running lint-staged..." lint-staged echo "Pre-commit hooks completed successfully. ✨" diff --git a/Dockerfile b/Dockerfile index d011690..cd3031c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,8 +19,6 @@ WORKDIR /app # Copy dependency files COPY package*.json bun.lock* ./ RUN bun install -ARG SYNC_LIFECYCLE_DOCS -ENV SYNC_LIFECYCLE_DOCS=${SYNC_LIFECYCLE_DOCS} COPY . . diff --git a/README.md b/README.md index d97be86..c3230a9 100644 --- a/README.md +++ b/README.md @@ -92,23 +92,6 @@ Install the dependencies bun install ``` -Create a PAT with with the permissions below (`"code"` is `"Read and Write content"` ). -Add it to your `.env` file by copying `.env.example` and adding your PAT to the `SYNC_LIFECYCLE_DOCS` and `GITHUB_TOKEN`. - -### PAT: Personal Access Token, create a fine-grained access token with the following permissions - -| rule | access level | -| --- | --- | -| members | `read` | -| metadata | `read` | -| actions | `read/write` | -| contents | `read/write` | -| pull requests | `read/write` | -| workflows | `read/write` | - -\*The following permissiones allow you to update content located in scripts - - Run the development server ```bash diff --git a/bun.lock b/bun.lock index ae82a48..41829a4 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "lifecycle-docs", @@ -16,6 +17,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "codehike": "^1.0.7", + "framer-motion": "^12.26.2", "lucide-react": "^0.511.0", "next": "^15.3.2", "nextra": "^^^3.2.4", @@ -1280,6 +1282,8 @@ "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], + "framer-motion": ["framer-motion@12.26.2", "", { "dependencies": { "motion-dom": "^12.26.2", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -1736,6 +1740,10 @@ "mlly": ["mlly@1.7.4", "", { "dependencies": { "acorn": "^8.14.0", "pathe": "^2.0.1", "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw=="], + "motion-dom": ["motion-dom@12.26.2", "", { "dependencies": { "motion-utils": "^12.24.10" } }, "sha512-KLMT1BroY8oKNeliA3JMNJ+nbCIsTKg6hJpDb4jtRAJ7nCKnnpg/LTq/NGqG90Limitz3kdAnAVXecdFVGlWTw=="], + + "motion-utils": ["motion-utils@12.24.10", "", {}, "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], diff --git a/package.json b/package.json index 1f55ba2..ed5c3d1 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,8 @@ "build:styles": "tailwindcss -i ./src/styles/globals.css -o public/styles.css", "build:meta": "bun run ./scripts/generateMeta.ts", "build:tags": "bun run ./scripts/generateTagPages.ts", - "build:sectiondata": "bun run ./scripts/generateSectionData.ts", - "build:blogroll": "bun run ./scripts/generateBlogroll.ts --debug", - "build:prep": "bun run clean && bun run build:remote:schema && bun run build:tags && bun run build:meta && bun run build:sectiondata && bun run sync:doc:dates && bun run build:blogroll && bun run sync:content", - "sync:doc:dates": "bun run ./scripts/syncDocDates.ts src/pages/articles/**/*.mdx src/pages/docs/*.mdx", - "sync:content": "bun run ./scripts/generateAllContent.ts", - "build:remote:schema": "bun run ./scripts/generateJsonSchema.ts", - "clean": "rimraf src/pages/schema src/pages/tags src/lib/data", + "build:prep": "bun run clean && bun run build:tags && bun run build:meta", + "clean": "rimraf src/pages/tags src/lib/data", "dev": "bun run build:prep && next dev -p 3333", "deploy": "bun run build && touch out/.nojekyll", "start": "next start", @@ -42,6 +37,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "codehike": "^1.0.7", + "framer-motion": "^12.26.2", "lucide-react": "^0.511.0", "next": "^15.3.2", "nextra": "^^^3.2.4", diff --git a/scripts/generateAllContent.ts b/scripts/generateAllContent.ts deleted file mode 100755 index f31b8fc..0000000 --- a/scripts/generateAllContent.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Copyright 2025 GoodRx, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs"; -import { dirname, relative } from "node:path"; -import matter from "gray-matter"; -import { sync } from "fast-glob"; -import { Command } from "commander"; - -export const findMdxFiles = (dir, ignorePatterns) => { - const globPattern = `${dir}/**/*.mdx`; - return sync(globPattern, { ignore: ignorePatterns }); -}; - -export const extractBodyText = (content) => { - content = content.replace(/import\s.+from\s.+;\n?/g, ""); - content = content.replace(/export\s.+;/g, ""); - - content = content.replace(/<[^>]+>/g, ""); - return content.trim(); -}; - -export const extractFileContent = (filePath, baseDir) => { - const content = readFileSync(filePath, "utf8"); - const { - data: { title = null, description = null, date = null }, - content: rawBody, - } = matter(content); - - const body = extractBodyText(rawBody); - - const path = relative(baseDir, filePath).replace(/\.mdx$/, ""); - - return { - title, - description, - date, - path, - body, - }; -}; - -export const ensureDirectoryExists = (dir) => { - if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); -}; - -export const organizeFileContent = ( - inputDir, - outputFilePath, - ignore, - debug, -) => { - if (debug) console.log(`Scanning directory: ${inputDir}`); - const mdxFiles = findMdxFiles(inputDir, ignore).filter( - (filePath) => !filePath.includes("/tags/"), - ); - - if (debug) - console.log( - `Found ${mdxFiles.length} MDX files (after applying ignore and exclude patterns)`, - ); - - const fileData = mdxFiles.map((file) => extractFileContent(file, inputDir)); - - if (debug) console.log(`Processed content for ${fileData.length} files`); - - const outputDir = dirname(outputFilePath); - ensureDirectoryExists(outputDir); - const content = JSON.stringify(fileData, null, 2); - const outputData = `export const blogContent = ${content};\n`; - writeFileSync(outputFilePath, outputData, "utf8"); - const outJSON = outputFilePath.replace(".ts", ".json"); - console.log(`File content saved to ${outJSON}`); - writeFileSync(outJSON, content, "utf8"); - if (debug) console.log(`File content saved to ${outputFilePath}`); -}; - -export const actionGenerateContent = (options) => { - const { input, output, ignore, debug } = options; - - if (!input || !output) { - console.error("Error: Both --input and --output options are required."); - process.exit(1); - } - - try { - organizeFileContent(input, output, ignore, debug); - } catch (error) { - console.error("Error:", error.message); - process.exit(1); - } -}; - -const program = new Command(); - -program - .name("extract-file-content") - .description("Extract body text from MDX files and save it as a JSON file.") - .option( - "-i, --input ", - "Input directory to scan for MDX files", - "src/pages", - ) - .option( - "-o, --output ", - "Output file path (JSON)", - "src/lib/static/blogcontent/blogcontent.ts", - ) - .option( - "--ignore ", - "Comma-separated list of glob patterns to ignore", - (val) => val.split(","), - ["**/index.mdx", "src/pages/tags/**"], - ) - .option("-d, --debug", "Enable debug logging", false) - .action(actionGenerateContent); - -program.parse(process.argv); diff --git a/scripts/generateBlogroll.ts b/scripts/generateBlogroll.ts deleted file mode 100755 index 36287b6..0000000 --- a/scripts/generateBlogroll.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright 2025 GoodRx, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs"; -import { dirname, relative } from "node:path"; -import matter from "gray-matter"; -import { sync } from "fast-glob"; -import { Command } from "commander"; - -export const findMdxFiles = ( - dir: string, - ignorePatterns: string[], -): string[] => { - const globPattern = `${dir}/**/*.mdx`; - return sync(globPattern, { ignore: ignorePatterns }); -}; - -export const extractFrontmatter = ( - filePath: string, - baseDir: string, -): Record => { - const content = readFileSync(filePath, "utf8"); - const { - data: { title = null, description = null, date = null }, - } = matter(content); - - const path = relative(baseDir, filePath).replace(/\.mdx$/, ""); - - return { - title, - description, - date, - path, - }; -}; - -export const ensureDirectoryExists = (dir: string) => { - if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); -}; - -export const organizeFrontmatter = ( - inputDir: string, - outputFilePath: string, - ignore: string[], - debug: boolean, -) => { - if (debug) console.log(`Scanning directory: ${inputDir}`); - const mdxFiles = findMdxFiles(inputDir, ignore).filter( - (filePath) => !filePath.includes("/tags/"), - ); - - if (debug) - console.log( - `Found ${mdxFiles.length} MDX files (after applying ignore and exclude patterns)`, - ); - - const frontmatterData = mdxFiles.map((file) => - extractFrontmatter(file, inputDir), - ); - - if (debug) - console.log(`Extracted frontmatter for ${frontmatterData.length} files`); - - const outputDir = dirname(outputFilePath); - ensureDirectoryExists(outputDir); - - const tsObject = `export const blogRoll = ${JSON.stringify(frontmatterData, null, 2)};\n`; - - writeFileSync(outputFilePath, tsObject, "utf8"); - if (debug) console.log(`Frontmatter data saved to ${outputFilePath}`); -}; - -export const actionGenerateBlogroll = (options) => { - const { input, output, ignore, debug } = options; - - if (!input || !output) { - console.error("Error: Both --input and --output options are required."); - process.exit(1); - } - - try { - organizeFrontmatter(input, output, ignore, debug); - } catch (error) { - console.error("Error:", error.message); - process.exit(1); - } -}; - -const program = new Command(); - -program - .name("extract-frontmatter") - .description("Extract frontmatter from MDX files and save it to a JSON file.") - .option( - "-i, --input ", - "Input directory to scan for MDX files", - "src/pages", - ) - .option( - "-o, --output ", - "Output file path (JSON)", - "src/lib/data/blogroll/blogroll.ts", - ) - .option( - "--ignore ", - "Comma-separated list of glob patterns to ignore", - (val) => val.split(","), - ["**/index.mdx"], - ) - .option("-d, --debug", "Enable debug logging", false) - .action(actionGenerateBlogroll); - -program.parse(process.argv); diff --git a/scripts/generateJsonSchema.ts b/scripts/generateJsonSchema.ts deleted file mode 100755 index 831e01f..0000000 --- a/scripts/generateJsonSchema.ts +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Copyright 2025 GoodRx, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { mkdir, writeFile } from "node:fs/promises"; -import { join, dirname } from "node:path"; -import { Octokit } from "@octokit/core"; -import { Command } from "commander"; -import yaml from "js-yaml"; -import dotenv from "dotenv"; - -dotenv.config(); - -const auth = process.env.SYNC_LIFECYCLE_DOCS; - -if (!auth) { - console.error("SYNC_LIFECYCLE_DOCS not found in .env file"); - process.exit(1); -} - -const octokit = new Octokit({ auth }); - -export const fetchFileFromRepo = async ({ - owner = "goodrxoss", - repo = "lifecycle", - path = "docs/schema/yaml/1.0.0.yaml", - branch = "main", - debug = false, -}: FetchFileOptions): Promise => { - try { - const sanitizedPath = path.startsWith("/") ? path.slice(1) : path; - - if (debug) { - console.log(`Fetching file: ${owner}/${repo}/${sanitizedPath}@${branch}`); - } - - const response = await octokit.request( - "GET /repos/{owner}/{repo}/contents/{path}", - { - owner, - repo, - path: sanitizedPath, - ref: branch, - }, - ); - - if (!("content" in response.data)) { - throw new Error("File content not found in response"); - } - - const content = Buffer.from(response.data.content, "base64").toString( - "utf-8", - ); - if (debug) console.log("File fetched successfully"); - return content; - } catch (error) { - console.error("Error fetching file:", error); - throw error; - } -}; - -export const convertYamlToJson = >( - yamlContent: string, -): T => { - try { - return yaml.load(yamlContent) as T; - } catch (error) { - console.error("Failed to parse YAML:", error); - throw error; - } -}; - -export const syncYamlFile = async (options: SyncOptions) => { - const { - owner = "goodrxoss", - repo = "lifecycle", - // docs/schema/yaml/2.3.0.yaml - path = "docs/schema/yaml/1.0.0.yaml", - dest = "src/lib/data/lifecycle-schema", - name = "lifecycle", - debug = false, - branch = "main", - } = options; - - try { - const yamlContent = await fetchFileFromRepo({ - owner, - repo, - path, - branch, - debug, - }); - - const yamlFileName = `${name}.yaml.ts`; - const jsonFileName = `${name}.json.ts`; - const yamlFilePath = join(dest, yamlFileName); - const jsonFilePath = join(dest, jsonFileName); - - const dir = dirname(yamlFilePath); - await mkdir(dir, { recursive: true }); - - const yamlExport = `export const yamlContent = \`${yamlContent.replace(/`/g, "\\`")}\`;\n`; - await writeFile(yamlFilePath, yamlExport, "utf-8"); - console.log(`YAML exported as TypeScript string at ${yamlFilePath}`); - const parsedJson = convertYamlToJson(yamlContent); - const jsonExport = `export const jsonContent = ${JSON.stringify(parsedJson, null, 2)};\n`; - await writeFile(jsonFilePath, jsonExport, "utf-8"); - console.log(`JSON exported as TypeScript object at ${jsonFilePath}`); - } catch (error) { - console.error("Error processing YAML file:", error); - } -}; - -const program = new Command(); - -program - .option("-r, --repo ", "Source repository name", "lifecycle") - .option("-p, --path ", "Path to YAML file in source repo") - .option( - "-d, --dest ", - "Path to store YAML and JSON files locally", - "src/lib/data/lifecycle-schema", - ) - .option("-n, --name ", "Base name for output files", "lifecycle") // New option for output file naming - .option("-o, --owner ", "GitHub owner or organization", "goodrxoss") - .option("-b, --branch ", "GitHub branch to fetch from", "main") - .option("--debug", "Enable debug logging", false); - -program.parse(process.argv); -const options = program.opts(); - -(async () => { - await syncYamlFile(options as SyncOptions); -})(); - -export type SyncOptions = { - owner?: string; - repo?: string; - path?: string; - dest?: string; - name?: string; - debug?: boolean; - branch?: string; -}; - -export type FetchFileOptions = { - owner?: string; - repo?: string; - path?: string; - branch?: string; - debug?: boolean; -}; diff --git a/scripts/generateSectionData.ts b/scripts/generateSectionData.ts deleted file mode 100755 index a9e4bee..0000000 --- a/scripts/generateSectionData.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Copyright 2025 GoodRx, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - existsSync, - mkdirSync, - readdirSync, - readFileSync, - statSync, - writeFileSync, -} from "node:fs"; -import { join, relative } from "node:path"; -import matter from "gray-matter"; -import { Command } from "commander"; - -const program = new Command(); - -async function generateDataFiles( - directoryPath: string, - outDir: string, - isDebugging: boolean, -): Promise { - if (isDebugging) console.log(`Processing directory: ${directoryPath}`); - const files = readdirSync(directoryPath); - - const data: { name: string; title: string; description: string }[] = []; - - await Promise.all( - files.map(async (file) => { - const filePath = join(directoryPath, file); - const stats = statSync(filePath); - - if (stats.isDirectory()) { - if (isDebugging) console.log(`Processing directory: ${filePath}`); - await generateDataFiles(filePath, outDir, isDebugging); - } else if (file.endsWith(".mdx")) { - if (isDebugging) console.log(`Processing file: ${filePath}`); - const fileContent = readFileSync(filePath, "utf8"); - const { data: frontmatter } = matter(fileContent); - data.push({ - name: file.replace(".mdx", ""), - title: frontmatter.title, - description: frontmatter.description, - }); - } - }), - ); - - if (data.length > 0) { - const relativeDirPath = relative(program.opts().dir, directoryPath); - const outputDirPath = join(outDir, relativeDirPath); - if (!existsSync(outputDirPath)) { - mkdirSync(outputDirPath, { recursive: true }); - } - - const dataFilePath = join(outputDirPath, `section.data.ts`); - - const dataContent = `export default ${JSON.stringify(data, null, 2)};`; - writeFileSync(dataFilePath, dataContent); - - if (isDebugging) console.log(`Generated data file: ${dataFilePath}`); - } -} - -program - .option("-d, --dir ", "directory of pages", "./src/pages") - .option("-o, --out ", "output directory", "./src/lib/data") - .option("--debug", "enable debug logs", false); - -program.parse(process.argv); - -const { dir, debug, out } = program.opts(); - -(async () => { - await generateDataFiles(dir, out, debug); - console.log("Meta files generated successfully!"); -})(); diff --git a/scripts/syncDocDates.ts b/scripts/syncDocDates.ts deleted file mode 100644 index ee66669..0000000 --- a/scripts/syncDocDates.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * Copyright 2025 GoodRx, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Command } from "commander"; -import { Octokit } from "@octokit/rest"; -import fg from "fast-glob"; -import fs from "node:fs/promises"; -import path from "node:path"; -import matter from "gray-matter"; -import pLimit from "p-limit"; - -import dotenv from "dotenv"; - -dotenv.config(); - -const auth = process.env.SYNC_LIFECYCLE_DOCS; -const octokit = new Octokit({ auth }); - -export const formatDate = (dateString: string): string => { - const date = new Date(dateString); - return date.toISOString().split("T")[0]; -}; - -export const updateFrontmatter = async ( - filePath: string, - commitDate: string, -): Promise => { - try { - const fileContent = await fs.readFile(filePath, "utf-8"); - const { data: frontmatter, content } = matter(fileContent); - if (frontmatter.date) return; - frontmatter.date = commitDate; - const updatedContent = matter.stringify(content, frontmatter); - await fs.writeFile(filePath, updatedContent, "utf-8"); - console.log(`Updated date for ${filePath}: ${commitDate}`); - } catch (error) { - console.error(`Error updating frontmatter for ${filePath}:`, error); - } -}; - -export const getLatestCommitsForFiles = async ({ - files, - owner = "goodrxoss", - repo = "lifecycle-docs", -}): Promise => { - const limit = pLimit(10); - try { - const commitRequests = files.map((file) => - limit(async () => { - const relativePath = path.relative(process.cwd(), file); - const resp = await octokit.repos.listCommits({ - owner, - repo, - path: relativePath, - per_page: 1, - }); - const commits = resp?.data; - const lastCommit = commits?.[0]?.commit; - const rawCommitDate = - lastCommit?.author?.date || new Date().toISOString(); - const commitDate = formatDate(rawCommitDate); - const commitMessage = lastCommit?.message || "No commit message"; - - await updateFrontmatter(file, commitDate); - - return { - fileName: path.basename(file), - filePath: relativePath, - commitDate, - commitMessage, - }; - }), - ); - - const fileCommits = await Promise.all(commitRequests); - return fileCommits.sort( - (a, b) => - new Date(b.commitDate).getTime() - new Date(a.commitDate).getTime(), - ); - } catch (error) { - console.info("Error fetching commits:", error); - return []; - } -}; - -export const syncDocDatesAction = async ({ - files, - owner = "goodrxoss", - repo = "lifecycle-docs", -}: SyncDocDatesActionOptions): Promise => { - const resolvedFiles = await fg(files); - if (resolvedFiles.length === 0) { - console.error(`No matching files found for patterns: ${files.join(", ")}`); - return; - } - console.log(`Found ${resolvedFiles.length} files. Processing...`); - const latestDocs = await getLatestCommitsForFiles({ - owner, - repo, - files: resolvedFiles, - }); - console.log("Latest Docs:"); - latestDocs.forEach((doc) => - console.log(`- ${doc.fileName}: ${doc.commitDate} (${doc.commitMessage})`), - ); -}; - -const program = new Command(); - -program - .description("Update .mdx frontmatter with the latest Git commit date.") - .option("-o, --owner ", "GitHub repository owner", "goodrxoss") - .option("-r, --repo ", "GitHub repository name", "lifecycle-docs") - .arguments("") - .action((files: string[], options: ActionOptions) => - syncDocDatesAction({ ...options, files }), - ); - -program.parseAsync(process.argv); - -export type FileCommit = { - fileName: string; - filePath: string; - commitDate: string; - commitMessage: string; -}; - -export type GetLatestCommitsForFilesOptions = { - owner?: string; - repo?: string; - filePath: string; -}; - -export type ActionOptions = { - owner?: string; - repo?: string; -}; - -export type SyncDocDatesActionOptions = { - owner?: string; - repo?: string; - files: string[]; -}; diff --git a/src/components/home/features/FeatureCard.tsx b/src/components/home/features/FeatureCard.tsx new file mode 100644 index 0000000..c9b4997 --- /dev/null +++ b/src/components/home/features/FeatureCard.tsx @@ -0,0 +1,49 @@ +/** + * Copyright 2025 GoodRx, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +"use client"; + +import { motion } from "framer-motion"; +import type { Feature } from "./types"; + +interface FeatureCardProps { + feature: Feature; + index: number; +} + +export function FeatureCard({ feature, index }: FeatureCardProps) { + const Icon = feature.icon; + + return ( + +
+ +
+

+ {feature.title} +

+

+ {feature.description} +

+
+ ); +} diff --git a/src/components/home/features/data.ts b/src/components/home/features/data.ts new file mode 100644 index 0000000..f19a561 --- /dev/null +++ b/src/components/home/features/data.ts @@ -0,0 +1,70 @@ +/** + * Copyright 2025 GoodRx, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + GitPullRequest, + Network, + Trash2, + GitFork, + Webhook, + MessageSquare, +} from "lucide-react"; +import type { Feature } from "./types"; + +export const features: Feature[] = [ + { + id: "auto-deploy", + title: "Auto-deploy on PR", + description: + "Every pull request automatically gets its own isolated environment. Simple config setup.", + icon: GitPullRequest, + }, + { + id: "multi-service", + title: "Connected Multi-Service", + description: + "Spin up your entire stack - frontend, backend, databases - all connected and working together.", + icon: Network, + }, + { + id: "auto-cleanup", + title: "Automatic Cleanup", + description: + "Environments are automatically torn down when PRs are merged or closed. No resource waste.", + icon: Trash2, + }, + { + id: "cross-repo", + title: "Cross-Repo Composition", + description: + "Test changes across multiple repositories in a single unified environment.", + icon: GitFork, + }, + { + id: "webhooks", + title: "Webhooks & Automation", + description: + "Integrate with your existing CI/CD pipelines and trigger custom workflows on environment events.", + icon: Webhook, + }, + { + id: "mission-control", + title: "Mission Control Comments", + description: + "Get environment URLs, status updates, and deployment logs directly in your PR comments.", + icon: MessageSquare, + }, +]; diff --git a/src/components/home/features/index.tsx b/src/components/home/features/index.tsx new file mode 100644 index 0000000..b8e4d90 --- /dev/null +++ b/src/components/home/features/index.tsx @@ -0,0 +1,55 @@ +/** + * Copyright 2025 GoodRx, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +"use client"; + +import { motion } from "framer-motion"; +import { FeatureCard } from "./FeatureCard"; +import { features } from "./data"; + +export function Features() { + return ( +
+
+ +

+ Everything you need for ephemeral environments +

+

+ Lifecycle provides all the tools to create, manage, and scale your + development environments automatically. +

+
+ +
+ {features.map((feature, index) => ( + + ))} +
+
+
+ ); +} + +export { FeatureCard } from "./FeatureCard"; +export { features } from "./data"; +export type { Feature } from "./types"; diff --git a/src/components/home/latest/types.ts b/src/components/home/features/types.ts similarity index 85% rename from src/components/home/latest/types.ts rename to src/components/home/features/types.ts index 9c8e0e5..72c2bdb 100644 --- a/src/components/home/latest/types.ts +++ b/src/components/home/features/types.ts @@ -14,13 +14,11 @@ * limitations under the License. */ -export type Post = { +import type { LucideIcon } from "lucide-react"; + +export interface Feature { + id: string; title: string; description: string; - path: string; - date: string; -}; - -export type LatestPostsProps = { - blogRoll: Post[]; -}; + icon: LucideIcon; +} diff --git a/src/components/home/hero/HeroContent.tsx b/src/components/home/hero/HeroContent.tsx new file mode 100644 index 0000000..3a002f0 --- /dev/null +++ b/src/components/home/hero/HeroContent.tsx @@ -0,0 +1,76 @@ +/** + * Copyright 2025 GoodRx, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +"use client"; + +import Link from "next/link"; +import { motion } from "framer-motion"; +import { ArrowRight, Github } from "lucide-react"; +import { buttonVariants } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +export function HeroContent() { + return ( +
+ + Enterprise-grade ephemeral environments{" "} + that grow with you + + + + Instantly spin up connected multi-service development environments from + any pull request. Review, test, and iterate faster than ever before. + + + + + Get Started + + + + + View on GitHub + + +
+ ); +} diff --git a/src/components/home/hero/index.tsx b/src/components/home/hero/index.tsx new file mode 100644 index 0000000..c1b7c9e --- /dev/null +++ b/src/components/home/hero/index.tsx @@ -0,0 +1,41 @@ +/** + * Copyright 2025 GoodRx, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +"use client"; + +import { motion } from "framer-motion"; +import { HeroContent } from "./HeroContent"; +import { Iframe } from "@/components/iframe"; + +export function Hero() { + return ( +
+
+ + +