diff --git a/docusaurus.config.ts b/docusaurus.config.ts index a662aeb..bb849c5 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -101,7 +101,7 @@ const config: Config = { }, docs: { includeCurrentVersion: false, - lastVersion: 'v1.9', + lastVersion: "v1.9", docVersionRootComponent: "@theme/DocVersionRoot", versions: { "v1.6": { @@ -122,12 +122,12 @@ const config: Config = { label: "1.9.x", path: "", }, - "main": { + main: { label: "Development", path: "main", banner: "unreleased", - noIndex: true - } + noIndex: true, + }, }, routeBasePath: "/docs", editUrl: ({ version, docPath }) => { @@ -212,9 +212,8 @@ const config: Config = { announcementBar: { id: "opentofu-ga", content: - 'Help us test OpenTofu 1.10.0-alpha2! Check it out here.', - backgroundColor: "#ffda18", - textColor: "#1b1d20", + '
🚀 OpenTofu 1.10.0-alpha2 is released! →
', + backgroundColor: "#00000000", isCloseable: false, }, algolia: { @@ -224,14 +223,6 @@ const config: Config = { }, footer: { links: [ - { - label: "Manifesto", - href: "/manifesto", - }, - { - label: "Supporters", - href: "/supporters", - }, { label: "FAQs", href: "/faq", @@ -249,16 +240,6 @@ const config: Config = { navbar: { hideOnScroll: true, items: [ - { - to: "/manifesto", - label: "Manifesto", - position: "left", - }, - { - to: "/supporters", - label: "Supporters", - position: "left", - }, { to: "/faq", label: "FAQs", @@ -269,6 +250,11 @@ const config: Config = { label: "Blog", position: "left", }, + { + label: "Registry", + href: "https://search.opentofu.org", + position: "left", + }, { label: "Roadmap", href: "https://github.com/opentofu/opentofu/milestones", @@ -282,27 +268,26 @@ const config: Config = { items: [ { label: "v1.9.x (current)", - href: "/docs/" + href: "/docs/", }, { label: "v1.8.x", - href: "/docs/v1.8/" + href: "/docs/v1.8/", }, { label: "v1.7.x", - href: "/docs/v1.7/" + href: "/docs/v1.7/", }, { label: "v1.6.x", - href: "/docs/v1.6/" + href: "/docs/v1.6/", }, { label: "Development", - href: "/docs/main/" + href: "/docs/main/", }, ], }, - // TODO: This link is important but there's no design for it yet // { // type: "dropdown", diff --git a/faq.mdx b/faq.mdx index 526530f..31d0520 100644 --- a/faq.mdx +++ b/faq.mdx @@ -1,26 +1,12 @@ - + -OpenTofu is an infrastructure as code tool that lets you define both cloud and on-prem resources in human-readable configuration files that you can version, reuse, and share. You can then use a consistent workflow to provision and manage all of your infrastructure throughout its lifecycle. OpenTofu can manage low-level components like compute, storage, and networking resources, as well as high-level components like DNS entries and SaaS features. - - - - - -OpenTofu is a Terraform fork, created as an initiative of Gruntwork, Spacelift, Harness, Env0, Scalr, and others, in response to HashiCorp’s switch from an open-source license to the BUSL. The initiative has many supporters, all of whom are listed [here](/supporters). +OpenTofu is a Terraform fork, created as an initiative of Gruntwork, Spacelift, Harness, Env0, Scalr, and others, in response to HashiCorp's switch from an open-source license to the BUSL. The initiative has many supporters, all of whom are listed [here](/supporters). The BUSL and the additional use grant outlined by the HashiCorp team are ambiguous, which makes it challenging for companies, vendors, and developers using Terraform to decide whether their actions could be interpreted as being outside the permitted scope of use. -Hashicorp’s FAQs give some peace of mind to end users and system integrators for now, but the licensing terms’ implications for future usage are unclear. The possibility that the company’s definition of _“competitive”_ or _“embedding”_ could change or the license could be further modified to make it closed source prompts uncertainty for Terraform users. +HashiCorp's FAQs give some peace of mind to end users and system integrators for now, but the licensing terms' implications for future usage are unclear. The possibility that the company's definition of _"competitive"_ or _"embedding"_ could change or the license could be further modified to make it closed source prompts uncertainty for Terraform users. -We firmly believe that Terraform should remain open-source because it is a project many companies use, and many contributors have made Terraform what it is today. Terraform’s success would not have been possible without the community’s work to build many supporting projects around it. - - - - - -On the technical level, OpenTofu 1.6.x is very similar feature-wise to Terraform 1.6.x. In the future, the projects feature sets will diverge. - -The other main difference is that OpenTofu is open-source, and its goal is to be driven in a collaborative way with no single company being able to dictate the roadmap. +We firmly believe that Terraform should remain open-source because it is a project many companies use, and many contributors have made Terraform what it is today. Terraform's success would not have been possible without the community's work to build many supporting projects around it. @@ -32,11 +18,11 @@ Initial impressions suggest you could use either OpenTofu or Terraform for perso #### Consultants -A consultant should offer their clients the best possible solution that aligns with their budget. OpenTofu will be on par with Terraform, and one of the project’s central objectives is to listen to the community’s issues, so it makes sense to recommend a project that will always stay open-source. Anyone who has used Terraform in the last eight years has probably come across issues that took some time to be resolved. The large community involved in developing OpenTofu means this will no longer be the case. +A consultant should offer their clients the best possible solution that aligns with their budget. OpenTofu will be on par with Terraform, and one of the project's central objectives is to listen to the community's issues, so it makes sense to recommend a project that will always stay open-source. Anyone who has used Terraform in the last eight years has probably come across issues that took some time to be resolved. The large community involved in developing OpenTofu means this will no longer be the case. #### Companies -Companies will encounter more difficulties with the situation. Switching to a new project carries risks, but staying with a project that changes its license without warning is far riskier. This risk is minimized by giving OpenTofu to the Linux Foundation, and OpenTofu’s aim of maintaining feature parity with Terraform for future releases reduces the technical risks. +Companies will encounter more difficulties with the situation. Switching to a new project carries risks, but staying with a project that changes its license without warning is far riskier. This risk is minimized by giving OpenTofu to the Linux Foundation, and OpenTofu's aim of maintaining feature parity with Terraform for future releases reduces the technical risks. @@ -48,22 +34,20 @@ If you're missing a feature in OpenTofu that's available in Terraform, feel free - - -Right now, OpenTofu is a drop-in replacement for Terraform, as it's compatible with Terraform versions 1.5.x and most of 1.6.x. You don’t need to make any changes to your code to ensure compatibility. - -OpenTofu is suitable for production use cases without any exception. - -Please see [our migration guide](/docs/intro/migration) for more information. - - - OpenTofu will work with existing state files up to those created with Terraform versions 1.5.x. + + +The OpenTofu Registry hosts thousands of providers and modules that are compatible with OpenTofu. These include all the popular cloud providers, third-party services, and community-maintained resources you might need for your infrastructure. + +You can search for providers, modules, and their documentation at [search.opentofu.org](https://search.opentofu.org). The registry provides seamless access to the same ecosystem of providers and modules that you're familiar with, ensuring you have all the tools needed to build and manage your infrastructure. + + + OpenTofu will not have its own providers. Terraform providers have not altered their licenses, and the potential for such a change is virtually zero. OpenTofu works with the current Terraform providers, but it uses a separate registry. diff --git a/package-lock.json b/package-lock.json index 2f06f29..5977e63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", + "framer-motion": "^12.7.4", "postcss": "^8.4.33", "prettier": "^3.1.1", "prism-react-renderer": "^2.3.1", @@ -33,6 +34,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-github-btn": "^1.4.0", + "swiper": "^11.2.6", "tailwindcss": "^3.4.1", "typescript": "~5.3.3" }, @@ -7643,6 +7645,32 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.7.4", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.7.4.tgz", + "integrity": "sha512-jX0bPsTmU0oPZTYz/dVyD0dmOyEOEJvdn0TaZBE5I8g2GvVnnQnW9f65cJnoVfUkY3WZWNXGXnPbVA9YnaIfVA==", + "dependencies": { + "motion-dom": "^12.7.4", + "motion-utils": "^12.7.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -12221,6 +12249,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/motion-dom": { + "version": "12.7.4", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.7.4.tgz", + "integrity": "sha512-1ZUHAoSUMMxP6jPqyxlk9XUfb6NxMsnWPnH2YGhrOhTURLcXWbETi6eemoKb60Pe32NVJYduL4B62VQSO5Jq8Q==", + "dependencies": { + "motion-utils": "^12.7.2" + } + }, + "node_modules/motion-utils": { + "version": "12.7.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.7.2.tgz", + "integrity": "sha512-XhZwqctxyJs89oX00zn3OGCuIIpVevbTa+u82usWBC6pSHUd2AoNWiYa7Du8tJxJy9TFbZ82pcn5t7NOm1PHAw==" + }, "node_modules/mrmime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", @@ -15900,6 +15941,24 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/swiper": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.2.6.tgz", + "integrity": "sha512-8aXpYKtjy3DjcbzZfz+/OX/GhcU5h+looA6PbAzHMZT6ESSycSp9nAjPCenczgJyslV+rUGse64LMGpWE3PX9Q==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], + "engines": { + "node": ">= 4.7.0" + } + }, "node_modules/tailwindcss": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", diff --git a/package.json b/package.json index 4b34a14..efdedeb 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "clear": "docusaurus clear", "typecheck": "tsc", "sync-supporters": "node ./sync-supporters.js", + "sync-contributor-stats": "node ./scripts/fetch-contributor-stats.js", "prepare": "husky install", "lint": "eslint --cache \"**/*.{js,jsx,ts,tsx}\"" }, @@ -31,6 +32,7 @@ "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", + "framer-motion": "^12.7.4", "postcss": "^8.4.33", "prettier": "^3.1.1", "prism-react-renderer": "^2.3.1", @@ -38,6 +40,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-github-btn": "^1.4.0", + "swiper": "^11.2.6", "tailwindcss": "^3.4.1", "typescript": "~5.3.3" }, diff --git a/scripts/fetch-contributor-stats.js b/scripts/fetch-contributor-stats.js new file mode 100755 index 0000000..6932531 --- /dev/null +++ b/scripts/fetch-contributor-stats.js @@ -0,0 +1,212 @@ +#!/usr/bin/env node + +/** + * Script to fetch OpenTofu contributor statistics + * + * This script fetches contributor data from OpenTofu GitHub repositories + * and generates a JSON file containing contributor counts. + * + * Usage: + * node scripts/fetch-contributor-stats.js + * + * Note: For more accurate results, you can set a GitHub token: + * GITHUB_TOKEN=your_token node scripts/fetch-contributor-stats.js + */ + +const https = require("https"); +const fs = require("fs"); +const path = require("path"); + +// Configuration +const FORK_DATE = "2023-08-25"; // OpenTofu was officially announced on this date +const OUTPUT_FILE = path.join( + __dirname, + "..", + "src", + "data", + "contributor-stats.json", +); +const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ""; // Optional GitHub token + +// For fallback purposes, let's hardcode the contributor count we found earlier +// If GitHub API access is fixed later, this will be replaced with the actual count +const FALLBACK_CONTRIBUTOR_COUNT = 180; + +// Create the data directory if it doesn't exist +const dataDir = path.dirname(OUTPUT_FILE); +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); +} + +// Main repositories to check (excluding manifesto, roadmap, .github, legal-documents) +const REPOSITORIES = [ + "opentofu", + "opentofu.org", + "brand-artifacts", + "equivalence-testing", + "scripts", + "registry-alpha", + "registry-address", + "setup-opentofu", + "tofu-exec", + "get.opentofu.org", + "tfenv", +]; + +// Helper function to make GitHub API requests +function makeRequest(url) { + return new Promise((resolve, reject) => { + const options = { + headers: { + "User-Agent": "OpenTofu-Stats-Collector", + ...(GITHUB_TOKEN && { Authorization: `token ${GITHUB_TOKEN}` }), + }, + }; + + https + .get(url, options, (response) => { + if (response.statusCode === 403) { + console.warn("Rate limit exceeded. Consider using a GitHub token."); + return resolve([]); + } + + if (response.statusCode !== 200) { + return reject( + new Error(`Request failed with status code ${response.statusCode}`), + ); + } + + let data = ""; + + response.on("data", (chunk) => { + data += chunk; + }); + + response.on("end", () => { + try { + resolve(JSON.parse(data)); + } catch (error) { + reject( + new Error( + `Failed to parse response from ${url}: ${error.message}`, + ), + ); + } + }); + }) + .on("error", (error) => { + reject(new Error(`Request to ${url} failed: ${error.message}`)); + }); + }); +} + +// Fetch contributors for a repository after the fork date +async function fetchContributors(repo) { + console.log(`Fetching contributors for ${repo}...`); + const contributors = new Set(); + let page = 1; + let hasMore = true; + + while (hasMore) { + const url = `https://api.github.com/repos/opentofu/${repo}/commits?since=${FORK_DATE}T00:00:00Z&per_page=100&page=${page}`; + + try { + const commits = await makeRequest(url); + + if (commits.length === 0 || !Array.isArray(commits)) { + hasMore = false; + continue; + } + + // Extract unique contributor logins + commits.forEach((commit) => { + if (commit.author && commit.author.login) { + contributors.add(commit.author.login); + } + }); + + // If we got fewer than 100 commits, we've reached the last page + if (commits.length < 100) { + hasMore = false; + } else { + page++; + // Add a small delay to avoid rate limits + await new Promise((resolve) => setTimeout(resolve, 500)); + } + } catch (error) { + console.error( + `Error fetching commits for ${repo} (page ${page}):`, + error.message, + ); + hasMore = false; + } + } + + return Array.from(contributors); +} + +// Main function +async function main() { + console.log(`Gathering contributor statistics since ${FORK_DATE}...`); + + const allContributors = new Set(); + let apiSuccessful = false; + + // Process each repository + for (const repo of REPOSITORIES) { + try { + const contributors = await fetchContributors(repo); + console.log(`Found ${contributors.length} contributors in ${repo}`); + + contributors.forEach((contributor) => allContributors.add(contributor)); + + if (contributors.length > 0) { + apiSuccessful = true; + } + } catch (error) { + console.error(`Failed to process repository ${repo}:`, error.message); + } + } + + // If API requests didn't yield results, use fallback count + const contributorCount = apiSuccessful + ? allContributors.size + : FALLBACK_CONTRIBUTOR_COUNT; + + // Generate stats + const stats = { + timestamp: new Date().toISOString(), + stats: { + contributors: { + total: contributorCount, + as_of_date: FORK_DATE, + }, + }, + }; + + // Save to JSON file + fs.writeFileSync(OUTPUT_FILE, JSON.stringify(stats, null, 2)); + + console.log(`\nContributor statistics:`); + console.log(`Total unique contributors: ${contributorCount}`); + console.log(`\nSaved stats to ${OUTPUT_FILE}`); + + if (!apiSuccessful) { + console.warn( + "\nWarning: GitHub API requests did not return contributor data.", + ); + console.warn( + "Using fallback contributor count of", + FALLBACK_CONTRIBUTOR_COUNT, + ); + console.warn("For more accurate results, set a GitHub token:"); + console.warn( + " GITHUB_TOKEN=your_token node scripts/fetch-contributor-stats.js", + ); + } +} + +main().catch((error) => { + console.error("Error executing script:", error); + process.exit(1); +}); diff --git a/src/components/Accordion/Item.tsx b/src/components/Accordion/Item.tsx index 9ef06ba..7f1996d 100644 --- a/src/components/Accordion/Item.tsx +++ b/src/components/Accordion/Item.tsx @@ -23,8 +23,8 @@ const classNames = [ "max-w-none", "text-gray-900", "dark:text-gray-100", - "pb-6", - "px-6", + "pb-4 sm:pb-6", + "px-3 sm:px-6", "leading-6", "mt-2", "text-base", @@ -105,7 +105,7 @@ const AccordionItem = ({ > {summary} void; + onPrev: () => void; + onNext: () => void; +} + +export const CustomPagination = ({ + totalSlides, + activeIndex, + onBulletClick, + onPrev, + onNext, +}: CustomPaginationProps) => { + const arrowClassName = + "p-1.5 rounded-full transition-colors text-gray-700 dark:text-gray-300"; + + return ( +
+ {/* Left Arrow */} + + + {/* Pagination dots */} +
+ {Array.from({ length: totalSlides }).map((_, index) => ( + onBulletClick(index)} + slideNumber={index + 1} + /> + ))} +
+ + {/* Right Arrow */} + +
+ ); +}; + +export default CustomPagination; diff --git a/src/components/Carousel/PaginationBullet.tsx b/src/components/Carousel/PaginationBullet.tsx new file mode 100644 index 0000000..160e9ce --- /dev/null +++ b/src/components/Carousel/PaginationBullet.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import clsx from "clsx"; + +interface PaginationBulletProps { + isActive: boolean; + onClick: () => void; + slideNumber: number; +} + +export const PaginationBullet = ({ + isActive, + onClick, + slideNumber, +}: PaginationBulletProps) => { + const className = clsx( + "w-2.5 h-2.5 rounded-full transition-colors", + isActive ? "bg-gray-700 dark:bg-gray-300" : "bg-gray-400 dark:bg-gray-700", + ); + + return ( + +
+ +
diff --git a/src/components/Features/index.tsx b/src/components/Features/index.tsx new file mode 100644 index 0000000..031eef9 --- /dev/null +++ b/src/components/Features/index.tsx @@ -0,0 +1,225 @@ +import React from "react"; +import { IDE } from "../IDE"; +import versions from "../../../versions.json"; + +type FeatureShowcaseProps = { + title: string; + description: string; + version: string; + codeExample: string; + align: "left" | "right"; + color: string; + filename?: string; + docsUrl?: string; +}; + +function FeatureShowcase({ + title, + description, + version, + codeExample, + align, + color, + filename = "main.tf", + docsUrl, +}: FeatureShowcaseProps) { + const isRight = align === "right"; + + // Read the latest version, this is needed because of how versioned documentation works. + // If the version referenced is actually the latest version, we link to the "What's New" page + // otherwise we link to the versioned "What's New" page. + // This is a bit of a hack, but it works for now. + const latestVersion = versions[0].replace("v", ""); + const versionHref = + version === latestVersion + ? "/docs/intro/whats-new/" + : `/docs/v${version}/intro/whats-new/`; + + return ( +
+ {/* Code example side */} +
+ +
+ + {/* Content side */} +
+
+

{title}

+ + {`v${version}`} + +
+

+ {description} +

+ {docsUrl && ( + + Learn more + + + + + )} +
+
+ ); +} + +export default function Features() { + return ( +
+
+

+ Features Unique to OpenTofu +

+

+ Powerful capabilities — built by the community to solve real-world + challenges in infrastructure management +

+
+ +
+ + + + + + + +
+
+ ); +} diff --git a/src/components/GitHubStarsNavbarItem/index.tsx b/src/components/GitHubStarsNavbarItem/index.tsx index c31de36..113debc 100644 --- a/src/components/GitHubStarsNavbarItem/index.tsx +++ b/src/components/GitHubStarsNavbarItem/index.tsx @@ -2,7 +2,7 @@ import React from "react"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore should be fixed when upgrating to docusaurus v3 import GitHubButton from "react-github-btn"; -import { useColorMode } from "@docusaurus/theme-common"; +import { useTheme } from "../../utils/useTheme"; type GitHubStartNavbarItemProps = { ghRepoUrl: string; @@ -13,15 +13,13 @@ export default function GitHubStartNavbarItem({ ghRepoUrl, buttonLabel, }: GitHubStartNavbarItemProps) { - const { colorMode } = useColorMode(); - + const isDarkTheme = useTheme(); + return (
>; - title: string; - description: string; -}; - -function Goal({ icon: Icon, title, description }: GoalProps) { - return ( -
- -

{title}

-

{description}

-
- ); -} - -export default function Goals() { - return ( -
-

- Our Goals -

-
- - - - - -
-
- ); -} diff --git a/src/components/Headline/index.tsx b/src/components/Headline/index.tsx index b3905e0..680d810 100644 --- a/src/components/Headline/index.tsx +++ b/src/components/Headline/index.tsx @@ -10,7 +10,7 @@ export default function Headline({ children, className }: HeadlineProps) { return (

diff --git a/src/components/Hero/index.tsx b/src/components/Hero/index.tsx index 1a9e587..1eab5f8 100644 --- a/src/components/Hero/index.tsx +++ b/src/components/Hero/index.tsx @@ -1,50 +1,96 @@ -import React from "react"; -import Button from "../Button"; -import PatternBg from "../PatternBg"; +import React, { useState, useEffect } from "react"; import Headline from "../Headline"; +import LFLogo from "../LFLogo"; +import { Examples } from "../Examples"; +import PatternBg from "../PatternBg"; +import Button from "../Button"; export default function Hero() { - return ( -
- - - - - - - - - The open source infrastructure as code tool. - -

- OpenTofu is a fork of Terraform that is - open-source, community-driven, and managed by the Linux Foundation. -

+ const [showArrow, setShowArrow] = useState(true); -
- - + useEffect(() => { + const handleScroll = () => { + setShowArrow(window.scrollY < 100); + }; + + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + const scrollToFeatures = () => { + const featuresSection = document.getElementById("features"); + if (featuresSection) { + featuresSection.scrollIntoView({ behavior: "smooth" }); + } + }; + + return ( +
+ + +
+
+
+ +
+
+ Open-Source Infrastructure as Code +

+ OpenTofu is a reliable, flexible, community-driven infrastructure + as code tool under the Linux Foundation's stewardship. It serves + as a drop-in replacement for Terraform, + preserving your existing workflows and configurations. +

+

+ With a thriving ecosystem of 3,900+ providers and{" "} + 23,600+ modules, you can build and manage + infrastructure across every cloud platform with confidence. +

+
+ + +
+
+
+ +
+ +
+ + {showArrow && ( + + )}
); } diff --git a/src/components/HowToContribute/index.tsx b/src/components/HowToContribute/index.tsx index abbc97e..47bf618 100644 --- a/src/components/HowToContribute/index.tsx +++ b/src/components/HowToContribute/index.tsx @@ -1,35 +1,61 @@ import React from "react"; -import Link from "@docusaurus/Link"; import Button from "../Button"; -// TODO enter final links once ready export default function HowToContribute() { return ( -
+
-

- How to contribute to OpenTofu? -

-

- The best way to show practical support for the OpenTofu initiative is - to contribute. This{" "} - - contribution guide - {" "} - explains OpenTofu contribution recommended practices, including how to - submit issues, how to get involved in the discussion, how to work on - the code, and how to contribute code changes. -

- +
+ {/* Content */} +
+

+ Join Our Community +

+

+ OpenTofu thrives on community contributions. Whether you're fixing + bugs, adding features, improving docs, or providing feedback, your + input makes a difference. +

+
+ + +
+
+ + {/* Card */} +
+
+

Get Involved

+
    + {[ + "Join GitHub discussions to share ideas", + "Open issues for bugs or feature suggestions", + "Participate in RFC discussions and reviews", + "Contribute code after community discussion", + ].map((text, index) => ( +
  • + + {index + 1} + +

    {text}

    +
  • + ))} +
+
+
+
); diff --git a/src/components/IDE/CheckIcon.tsx b/src/components/IDE/CheckIcon.tsx new file mode 100644 index 0000000..ceb2563 --- /dev/null +++ b/src/components/IDE/CheckIcon.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +export default function CheckIcon() { + return ( + + + + ); +} diff --git a/src/components/IDE/CopyIcon.tsx b/src/components/IDE/CopyIcon.tsx new file mode 100644 index 0000000..768eee1 --- /dev/null +++ b/src/components/IDE/CopyIcon.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +export default function CopyIcon() { + return ( + + + + ); +} diff --git a/src/components/IDE/DefaultFileIcon.tsx b/src/components/IDE/DefaultFileIcon.tsx new file mode 100644 index 0000000..f84d394 --- /dev/null +++ b/src/components/IDE/DefaultFileIcon.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +export default function DefaultFileIcon() { + return ( + + + + ); +} diff --git a/src/components/IDE/OpenTofuLogo.tsx b/src/components/IDE/OpenTofuLogo.tsx new file mode 100644 index 0000000..b3ca191 --- /dev/null +++ b/src/components/IDE/OpenTofuLogo.tsx @@ -0,0 +1,25 @@ +import React from "react"; + +export default function OpenTofuLogo() { + return ( + + + + + + ); +} diff --git a/src/components/IDE/index.tsx b/src/components/IDE/index.tsx new file mode 100644 index 0000000..e877e75 --- /dev/null +++ b/src/components/IDE/index.tsx @@ -0,0 +1,79 @@ +import React, { useState } from "react"; +import { Highlight, themes } from "prism-react-renderer"; +import OpenTofuLogo from "./OpenTofuLogo"; +import CopyIcon from "./CopyIcon"; +import CheckIcon from "./CheckIcon"; +import DefaultFileIcon from "./DefaultFileIcon"; +import { useTheme } from "../../utils/useTheme"; + +interface IDEHeaderProps { + filename?: string; +} + +const tfFileExtensions = [".tf", ".tfvars", ".tofu"]; + +function IDEHeader({ filename = "main.tf" }: IDEHeaderProps) { + const isTofuFile = tfFileExtensions.some((ext) => filename.endsWith(ext)); + + return ( +
+
+
+ {isTofuFile ? : } +
+ {filename} +
+
+ ); +} + +interface IDEProps { + code: string; + language?: string; + filename?: string; +} + +export function IDE({ code, language = "hcl", filename }: IDEProps) { + const [copied, setCopied] = useState(false); + const isDarkTheme = useTheme(); + + const copyToClipboard = () => { + navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+ +
+ + + {({ tokens, getLineProps, getTokenProps }) => ( +
+              
+                {tokens.map((line, i) => (
+                  
+ {line.map((token, key) => ( + + ))} +
+ ))} +
+
+ )} +
+
+
+ ); +} diff --git a/src/components/LFLogo/index.tsx b/src/components/LFLogo/index.tsx new file mode 100644 index 0000000..20a1aa3 --- /dev/null +++ b/src/components/LFLogo/index.tsx @@ -0,0 +1,28 @@ +import React from "react"; + +export default function LFLogo() { + return ( + + + + + + + ); +} diff --git a/src/components/PatternBg/index.tsx b/src/components/PatternBg/index.tsx index 876db1a..74a0b0b 100644 --- a/src/components/PatternBg/index.tsx +++ b/src/components/PatternBg/index.tsx @@ -3,7 +3,7 @@ import styles from "./styles.module.css"; export default function PatternBg() { return ( -
+
diff --git a/src/components/Supporters/index.tsx b/src/components/Supporters/index.tsx index d956095..6141923 100644 --- a/src/components/Supporters/index.tsx +++ b/src/components/Supporters/index.tsx @@ -1,69 +1,100 @@ import React from "react"; -import { Suporter } from "@site/src/components/SupportersList/types"; -import supporters from "../../../supporters.json"; +import { ENGINEERING_CONTRIBUTORS, SERVICE_SPONSORS } from "./supporters"; +import contributorStats from "../../data/contributor-stats.json"; -import Button from "../Button"; -import { groupSupportersByType } from "../../utils/groupSupportersByType"; -import SupportersList from "../SupportersList"; +interface SupporterLogoProps { + supporter: { + name: string; + url: string; + logoDark: string; + logoLight: string; + }; +} -type SupporterTypeProps = { - type: string; - count: number; - withSeparator?: boolean; -}; - -function SupporterType({ type, withSeparator, count }: SupporterTypeProps) { +function SupporterLogo({ supporter }: SupporterLogoProps) { return ( -
  • - {type} - - Supporting {type}: {count} - - {count && ( - - {count} - - )} - {withSeparator && ( - - • - - )} -
  • + +
    + <> + {`${supporter.name} + {`${supporter.name} + +
    +
    ); } -const MAX_LIST_COUNT = 5; +function CommunityContributorsTile() { + return ( +
    +
    + + {Math.floor(contributorStats.stats.contributors.total / 10) * 10}+ + +

    + Community Contributors +

    +
    +
    + ); +} export default function Supporters() { - const groupedSupporters = groupSupportersByType(supporters); - const types = Object.entries(groupedSupporters); - const list = supporters.slice(0, MAX_LIST_COUNT); - return ( -
    -

    - Supporters -

    -
      - {types.map(([type, supporters], index) => ( - - ))} -
    - -
    - +
    +
    +

    + Our Supporters +

    +
    + +
    +

    + Engineering Contributors +

    +
    + {ENGINEERING_CONTRIBUTORS.map((supporter) => ( +
    + +
    + ))} +
    + +
    +
    +
    + +
    +

    + Service Sponsors +

    +
    + {SERVICE_SPONSORS.map((sponsor) => ( +
    + +

    + {sponsor.contribution} +

    +
    + ))} +
    ); diff --git a/src/components/Supporters/supporters.ts b/src/components/Supporters/supporters.ts new file mode 100644 index 0000000..93b95bc --- /dev/null +++ b/src/components/Supporters/supporters.ts @@ -0,0 +1,50 @@ +export const ENGINEERING_CONTRIBUTORS = [ + { + name: "Harness", + logoLight: "/img/supporters/harness-light.svg", + logoDark: "/img/supporters/harness-dark.svg", + url: "https://www.harness.io/", + }, + { + name: "Gruntwork", + logoLight: "/img/supporters/gruntwork-light.svg", + logoDark: "/img/supporters/gruntwork-dark.svg", + url: "https://gruntwork.io", + }, + { + name: "Spacelift", + logoLight: "/img/supporters/spacelift-light.svg", + logoDark: "/img/supporters/spacelift-dark.svg", + url: "https://spacelift.io", + }, + { + name: "env0", + // TODO: Get good env0 logos + logoLight: "/img/supporters/env0-light.svg", + logoDark: "/img/supporters/env0-dark.svg", + url: "https://www.env0.com/", + }, + { + name: "Scalr", + logoLight: "/img/supporters/scalr-light.svg", + logoDark: "/img/supporters/scalr-dark.svg", + url: "https://www.scalr.com/", + }, +]; + +export const SERVICE_SPONSORS = [ + { + name: "Buildkite", + logoLight: "/img/supporters/buildkite-light.svg", + logoDark: "/img/supporters/buildkite-dark.svg", + url: "https://buildkite.com/", + contribution: "Package hosting", + }, + { + name: "Cloudflare", + logoLight: "/img/supporters/cloudflare-light.svg", + logoDark: "/img/supporters/cloudflare-dark.svg", + url: "https://www.cloudflare.com/", + contribution: "Website hosting & additional services", + }, +]; diff --git a/src/css/custom.css b/src/css/custom.css index c410d7f..eaf28e7 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -96,12 +96,28 @@ body { display: none; } -.DocSearch .DocSearch-Hit[aria-selected="true"] a { - background-color: var(--docsearch-highlight-background-selected); +.DocSearch-Button { + border-radius: 4px !important; + height: 32px !important; + padding: 0 12px !important; + margin: 0 !important; + border: 1px solid transparent !important; + transition: all 0.2s ease !important; } -.DocSearch .DocSearch-Hits mark { - color: var(--docsearch-highlight-mark-color); +.DocSearch-Button:hover { + background: var(--docsearch-searchbox-background) !important; + box-shadow: 0 0 0 0 !important; + border: 1px solid rgba(125, 125, 125, 0.2) !important; +} + +.DocSearch-Button-Placeholder { + font-size: 14px !important; + padding: 0 8px 0 6px !important; +} + +.DocSearch .DocSearch-Hit[aria-selected="true"] a { + background-color: var(--docsearch-highlight-background-selected); } .link:hover { @@ -118,4 +134,64 @@ body { h1 { text-wrap: balance; -} \ No newline at end of file +} + +div[class*="announcementBar"] { + position: relative !important; + top: 6px !important; + border-bottom: none !important; +} + +.announcement-bar { + min-height: 48px !important; + display: flex; + align-items: center; + justify-content: center; + font-weight: 500; +} + +html[data-theme="dark"] .announcement-bar-link { + color: white; + background-color: rgba(255, 255, 255, 0.05); +} + +html[data-theme="dark"] .announcement-bar-link:hover { + background-color: rgba(255, 255, 255, 0.15); +} + +html[data-theme="light"] .announcement-bar-link { + background-color: rgba(0, 0, 0, 0.05); +} + +html[data-theme="light"] .announcement-bar-link:hover { + background-color: rgba(0, 0, 0, 0.1); +} + +.announcement-bar-link { + display: block; + width: 100%; + text-decoration: none !important; + transition: all 0.2s ease; + font-weight: 600; +} + +.announcement-bar-content { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + font-size: 15px; + padding: 6px 0; +} + +.announcement-arrow { + display: inline-flex; + align-items: center; + font-weight: bold; + margin-left: 4px; + transition: transform 0.2s ease; +} + +.announcement-bar-link:hover .announcement-arrow { + transform: translateX(0.25rem); +} diff --git a/src/data/contributor-stats.json b/src/data/contributor-stats.json new file mode 100644 index 0000000..b08cf5a --- /dev/null +++ b/src/data/contributor-stats.json @@ -0,0 +1,9 @@ +{ + "timestamp": "2025-04-22T10:32:33.300Z", + "stats": { + "contributors": { + "total": 187, + "as_of_date": "2023-08-25" + } + } +} diff --git a/src/icons/opentofu.svg b/src/icons/opentofu.svg new file mode 100644 index 0000000..c9e0bc7 --- /dev/null +++ b/src/icons/opentofu.svg @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 0894f0b..76055b7 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,16 +1,16 @@ import React from "react"; import Layout from "@theme/Layout"; import Hero from "../components/Hero"; -import Goals from "../components/Goals"; +import Features from "../components/Features"; import Supporters from "../components/Supporters"; import FAQ from "../components/FAQ"; import HowToContribute from "../components/HowToContribute"; export default function Home() { return ( - + - + diff --git a/src/theme/NavbarItem/DropdownNavbarItem.tsx b/src/theme/NavbarItem/DropdownNavbarItem.tsx index 61a2bd1..afcf833 100644 --- a/src/theme/NavbarItem/DropdownNavbarItem.tsx +++ b/src/theme/NavbarItem/DropdownNavbarItem.tsx @@ -57,7 +57,7 @@ function DropdownNavbarItemDesktop({ } />