diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd0519701..2cba3c0a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,10 +30,6 @@ jobs: run: | npm install npx prettier --check "**/*.{graphql,yml,json,md,sh,ts,tsx,js}" - cd ./publish-externals - npm run generate - cd .. - npm run typecheck npm run build - name: Deploy 🚀 diff --git a/blog/graphql-schema-2024-07-11.md b/blog/graphql-schema-2024-07-11.md index eb7936c25..299175779 100644 --- a/blog/graphql-schema-2024-07-11.md +++ b/blog/graphql-schema-2024-07-11.md @@ -16,8 +16,18 @@ slug: graphql-schema Designing a robust, scalable GraphQL schema is critical for building production-ready APIs that can evolve with your application's needs. In this comprehensive guide, we'll walk through the process of crafting a GraphQL schema for a real-world application, highlighting best practices and considerations along the way. +If you are thinking how we could possibly cover all of the lovely intricacies associated with this topic in one go, you are right, we can't and so we are not! We have created an amazing series to take you through the nuances of working with GraphQL schemas. + +Let's break our job into puzzle pieces. Let's start by simply creating designing a brand new schema! + +
+ +puzzle piece to visualise the series + +
+ If you're new to GraphQL Schema, check out our [GraphQL Schema Tutorial](https://tailcall.run/graphql/schemas-and-types/) to get up to speed with the basics. ## The Power of GraphQL Schemas @@ -474,3 +484,9 @@ Remember, your schema is a living document. As your application evolves, so too The TechTalent example we've explored here demonstrates many real-world considerations, but every application will have its unique requirements. Always design with your specific use cases in mind, and don't be afraid to iterate as you learn more about how your API is used in practice. By investing time in thoughtful schema design upfront, you'll create a solid foundation for your GraphQL API, enabling efficient development and a great experience for your API consumers. + +Alright greatttt! You have successfully completed the first part of a very intricate series!! Pat yourslef and maybe high five your cat! Here are the links to the next blogs in the series that have already been published. + +![cat giving high five](../static/images/blog/cat-high-five-gif.webp) + +- [Next Part](/blog/graphql-schema-part-2-1) diff --git a/blog/graphql-schema-part-2-1-2024-07-21.mdx b/blog/graphql-schema-part-2-1-2024-07-21.mdx new file mode 100644 index 000000000..d05c02ce7 --- /dev/null +++ b/blog/graphql-schema-part-2-1-2024-07-21.mdx @@ -0,0 +1,345 @@ +--- +title: Design a GraphQL Schema So Good, It'll Make REST APIs Cry - Part 2.1 +authors: + - name: Amit Singh + title: Head of Growth and Strategy @ Tailcall + url: https://github.com/amitksingh1490 + image_url: https://avatars.githubusercontent.com/u/23661702?v=5 +tags: [GraphQL, API, Schema, Design, Best Practices] +description: Learn how to make additive changes to your GraphQL schema without causing disruptions. +image: /images/graphql/graphql-schema-structure.png +hide_table_of_contents: true +slug: graphql-schema-part-2-1 +--- + +import Quiz from "../src/components/quiz/quiz.tsx" + +![GraphQL Schema Structure](../static/images/graphql/graphql-schema-structure.png) + +In our [previous post](/blog/graphql-schema), we learned scalable GraphQL schema is critical for building production-ready APIs that can evolve with your application's needs. + +In this post, we will dive deeper into how to **continuously** evolve your schema to meet your application's changing requirements without hard-coded versioning. + + + +## Adding Without Breaking: The Art of Additive Changes + +You know that feeling when you're working on a project, and suddenly you realize your schema needs to change? Maybe you need to add a new field, modify an existing one, or even remove something entirely. It's enough to make any developer break out in a cold sweat, right? + +But fear not! I'm here to show you **how to evolve your schema like a pro**, keeping your API fresh and exciting without causing your clients to tear their hair out. + +## The Good, The Bad, and The Ugly of Schema Changes + +Not all changes are created equal. In this section, we’ll analyze a few different types of changes and what makes them safe or unsafe. + +First things first, let's break down the types of changes we might make to our schema: + +1. **Safe Changes:** These are the golden children of schema evolution. You can make these changes anytime, and your clients won't even bat an eyelash. +2. **Dangerous Changes:** These are the sneaky ones. They might not break anything outright, but they can cause subtle issues that'll have your clients scratching their heads. We'll need to proceed carefully here. +3. **Breaking Changes:** The name says it all. These changes will send your clients' applications crashing down faster than you can say "**GraphQL**". We want to avoid these like the plague, but sometimes they're necessary. Don't worry, I'll show you how to handle them like a pro. + +## Additive Changes + +Most of the time, these are safe as houses. + +For example, adding fields & adding types is unlikely to cause issues for clients. But, there are a few tricky scenarios to watch out for. + +### The Optional Argument Conundrum + +Adding optional arguments is generally safe - it's like offering your clients a shiny new toy without forcing them to play with it. + +However, there's a catch. Check this out: + +```diff + type Query { +- products(category: String): [Product!]! ++ products(category: String, inStock: Boolean): [Product!]! + } +``` + +See what we did there? We added an optional `inStock` argument. Seems harmless, right? + +Let's dive deeper into why changing the behavior of a resolver when an optional argument isn't provided can be problematic: + +```graphql +type Query { + products(category: String, inStock: Boolean): [Product!]! +} +``` + +Imagine you have clients that have been using this query: + +```graphql +query { + products(category: "Electronics") { + name + price + } +} +``` + +If your resolver suddenly starts filtering out out-of-stock products when `inStock` isn't provided, these clients will unexpectedly receive fewer results. This could break their UI or data processing logic. + +To avoid this issue, you can implement a strategy to handle the absence of the `inStock` argument gracefully in your resolver, so that the behavior remains consistent for clients. + +### The Required Argument Trap + +Now, this is where things get spicy 🌶️. + +Adding a required argument is almost always a **breaking change**. + +But, fear not! There's a way out: + +```diff + type Query { +- products(category: String): [Product!]! ++ products(category: String, sortBy: SortOption!): [Product!]! + } +``` + +This change is **breaking**, but it doesn't have to be. + +You can provide a **default value** for the new argument to keep your existing clients happy. + +```diff +type Query { +- products(category: String): [Product!]! ++ products(category: String, sortBy: SortOption! = POPULARITY): [Product!]! +} +``` + +See that `= POPULARITY`? That's your get-out-of-jail-free card. By providing a default value, you've made this addition safe. + +Existing clients will use the default, and new clients can take advantage of the sorting option. + +### The Interface and Union Twist + +Now, let's talk about some trickier additive changes that can catch you off guard if you're not careful. + +### Adding New Interface Implementations + +Adding a new type that implements an existing interface might seem harmless, but it can cause some unexpected behavior. Check this out: + +```graphql +interface Node { + id: ID! +} + +type User implements Node { + id: ID! + name: String! +} + +type Team implements Node { + id: ID! + name: String! +} + +# highlight-start +type Organization implements Node { + id: ID! + name: String! + employees: [User!]! +} +# highlight-end +``` + +By adding the `Organization` type, we've expanded what could be returned by queries selecting for `Node`. This could break clients that aren't prepared to handle new types. Always encourage clients to use proper type checking. + +```graphql +query { + node(id: "1") { + ... on User { + name + } + ... on Team { + name + } + ... on Organization { + name + employees { + name + } + } + } +} +``` + +Without proper type checking, clients might encounter these issues: + +1. **Runtime Errors:** If a client assumes all Node types have only a name field, they might try to access `employees` on a `User` or `Team`, causing errors. +2. **Missing Data:** Clients might not display Organization-specific data if they're not prepared to handle it. +3. **Incorrect Data Processing:** Business logic that assumes only `User` and `Team` types exist might produce incorrect results. + +To mitigate these issues: + +1. Use TypeScript or Flow on the client-side to catch type errors at compile-time. +2. Implement exhaustive type checking in your client code: + +```typescript +function handleNode(node: Node) { + switch (node.__typename) { + case "User": + return handleUser(node) + case "Team": + return handleTeam(node) + case "Organization": + return handleOrganization(node) + default: + const _exhaustiveCheck: never = node + throw new Error(`Unhandled node type: ${(_exhaustiveCheck as any).__typename}`) + } +} +``` + +This approach ensures that if a new type is added in the future, TypeScript will raise a compile-time error, prompting developers to update their code. + +### The Union Expansion Conundrum + +Similar to interfaces, adding new members to a union can cause runtime surprises. Consider this: + +```diff +- union SearchResult = User | Post ++ union SearchResult = User | Post | Comment +``` + +Surprise! Your clients might suddenly receive a type they weren't expecting. It's like opening a box of chocolates and finding a pickle - not necessarily bad, but definitely unexpected. Make sure to document how clients should handle these surprise types. + +Let's delve into why union expansions can be tricky and how to handle them gracefully: + +When you add `Comment` to the `SearchResult` union, existing clients might break in subtle ways: + +1. **Incomplete UI:** If the client only has UI components for `User` and `Post`, `Comment` results won't be displayed. +2. **Runtime Errors:** Code that assumes only `User` and `Post` types exist might throw errors when encountering a `Comment`. + +To handle this gracefully: + +1. Implement a fallback UI component for unknown types: + + ```tsx + function SearchResultItem({result}) { + switch (result.__typename) { + case "User": + return + case "Post": + return + case "Comment": + return + default: + return + } + } + ``` + +2. Encourage clients to use introspection queries to stay updated on schema changes: + + ```graphql + query { + __type(name: "SearchResult") { + kinds + possibleTypes { + name + } + } + } + ``` + +By implementing these strategies, clients can gracefully handle new union members without breaking existing functionality. + +### The Enum Evolution + +Adding new enum values seems innocent enough, but it can impact client-side logic. Let's look at an example: + +```diff + enum OrderStatus { + PENDING + COMPLETED ++ CANCELED ++ REFUNDED +} +``` + +Clients that were using exhaustive switches might now have incomplete logic. Encourage clients to use default cases to handle new enum values. + +```typescript +switch (order.status) { + case "PENDING": + return "Order is pending" + case "COMPLETED": + return "Order is completed" + default: + return "Order status unknown" +} +``` + +## Quiz Time! 🎉 + + + +## Conclusion + +Evolving a GraphQL schema through additive changes allows you to expand your API's capabilities while maintaining backward compatibility. By following the principles and strategies outlined in this article, you can confidently add new fields, types, and arguments without causing disruptions to your clients. + +Remember these key takeaways: + +1. **Favor Additive Changes**: Whenever possible, add new fields, types, or arguments instead of modifying existing ones. This approach maintains backward compatibility while allowing your schema to grow. + +2. **Provide Transition Paths**: Introduce new features alongside existing ones to allow gradual client adoption. + +By treating your GraphQL schema as a product with its own lifecycle and evolution strategy, you can build APIs that are both powerful and adaptable. This approach allows you to innovate rapidly while providing a stable and reliable service to your clients. + +Stay tuned for the [next part](/blog/graphql-schema-part-2-2) of this series, where we will dive into removing schema elements and handling breaking changes! diff --git a/blog/graphql-schema-part-2-2-2024-07-22.mdx b/blog/graphql-schema-part-2-2-2024-07-22.mdx new file mode 100644 index 000000000..b1f745673 --- /dev/null +++ b/blog/graphql-schema-part-2-2-2024-07-22.mdx @@ -0,0 +1,154 @@ +--- +title: Design a GraphQL Schema So Good, It'll Make REST APIs Cry - Part 2.2 +authors: + - name: Amit Singh + title: Head of Growth and Strategy @ Tailcall + url: https://github.com/amitksingh1490 + image_url: https://avatars.githubusercontent.com/u/23661702?v=5 +tags: [GraphQL, API, Schema, Design, Best Practices] +description: Learn how to modify existing schema elements in GraphQL without causing disruptions. +image: /images/graphql/graphql-schema-structure.png +hide_table_of_contents: true +slug: graphql-schema-part-2-2 +--- + +import Quiz from "../src/components/quiz/quiz.tsx" + +![GraphQL Schema Structure](../static/images/graphql/graphql-schema-structure.png) + +## Modifying Without Breaking: Navigating the Modification Minefield + +In our [previous post](/blog/graphql-schema-part-2-1), we explored how to make additive changes to your GraphQL schema without causing disruptions. Now, we'll dive into the tricky territory of modifying existing schema elements. + + + +## Recap of Additive Changes + +In [Part 2.1](/blog/graphql-schema-part-2-1), we discussed the importance of making additive changes to your GraphQL schema to expand its capabilities while maintaining backward compatibility. By adding new fields, types, and arguments, you can enhance your API without causing disruptions to existing clients. We also emphasized the importance of providing transition paths to ensure a smooth adoption process. + +### Safe, Dangerous, and Breaking Changes + +1. **Safe Changes:** Additive changes such as adding new fields or types that do not affect existing queries or functionality. +2. **Dangerous Changes:** Modifications that might not break the schema immediately but can cause subtle issues, such as changing default values or making non-nullable fields nullable. +3. **Breaking Changes:** Changes that will definitely break existing queries and require clients to update their code, such as removing fields or changing field types. + +## The Modification Minefield + +Now, let's talk about modifying existing parts of your schema. This is where things can get really hairy. For example, changing a field’s type or changing the name of a type is a big-breaking change. + +### The Default Value Dilemma + +Changing default values might seem innocent, but it can cause some serious headaches. Consider this: + +```diff +type Query { +- products(category: String, showOutOfStock: Boolean = False): [Product!]! ++ products(category: String, showOutOfStock: Boolean = True): [Product!]! +} +``` + +Changing the default value of an argument or input field is unlikely to be a breaking change in terms of the schema itself, but is very likely to cause issues at runtime if the default value affects the runtime behavior of the field. + +Avoid this change in general, but it may be possible to achieve if the behavior of the field does not change. + +### The Non-Null to Null Transformation + +This is one of the trickiest changes to make. You thought making a field non-null was a good idea, but now you need to change it back. Here's how to handle it: + +For scalar fields, you might be able to save your users from errors by returning the `default value` instead of null. + +For object types, sometimes it’s possible to use a `Default Object` when the result is null. + +This approach can help prevent null pointer exceptions on the client side. + +### Changing a Field Type + +Changing a field’s type is not a change we can make easily. Once again, approaching the change in an additive is often your best bet. + +```diff +type User { + bestFriend: String! @deprecated(reason: “Use `bestFriendObject` instead.”) ++ bestFriendObject: User! +} +``` + +As you might have noticed, the downside of additive changes is that often the best names are already taken. If wanted, you may remove the original field and reintroduce it under the new object at that point. + +```diff +type User { +- bestFriend: String! @deprecated(reason: “Use `bestFriendObject` instead.”) + bestFriendObject: User! ++ bestFriend: User! +} +``` + +### Changing Description or Deprecation + +Changing the description of fields, types and any member is unlikely to cause any harm to clients. Clients should not depend on schema descriptions for runtime logic! + +## Quiz Time! 🎉 + + + +## Conclusion + +Modifying existing schema elements requires careful planning and execution to avoid breaking changes. By following the principles and strategies outlined in this article, you can confidently make necessary modifications while minimizing disruption to your clients. + +Remember these key takeaways: + +1. **Deprecate Cautiously**: Use deprecation notices, schema descriptions, and out-of-band communication to keep your clients informed about upcoming changes. +2. **Provide Transition Paths**: When breaking changes are necessary, offer clear migration paths. This might involve introducing new fields alongside deprecated ones or providing new query structures that achieve the same results. +3. **Leverage Schema Design Tools**: Use schema comparison tools like [GraphQL Editor](https://github.com/graphql-editor/graphql-editor). + +By treating your GraphQL schema as a product with its own lifecycle and evolution strategy, you can build APIs that are both powerful and adaptable. This approach allows you to innovate rapidly while providing a stable and reliable service to your clients. + +Stay tuned for the [next part](/blog/graphql-schema-part-2-3) of this series, where we will dive into removing schema elements and handling breaking changes! diff --git a/blog/graphql-schema-part-2-3-2024-07-23.mdx b/blog/graphql-schema-part-2-3-2024-07-23.mdx new file mode 100644 index 000000000..fc8398b72 --- /dev/null +++ b/blog/graphql-schema-part-2-3-2024-07-23.mdx @@ -0,0 +1,200 @@ +--- +title: Design a GraphQL Schema So Good, It'll Make REST APIs Cry - Part 2.3 +authors: + - name: Amit Singh + title: Head of Growth and Strategy @ Tailcall + url: https://github.com/amitksingh1490 + image_url: https://avatars.githubusercontent.com/u/23661702?v=5 +tags: [GraphQL, API, Schema, Design, Best Practices] +description: Learn how to remove schema elements without causing disruptions. Strategies to handle breaking changes and maintain backward compatibility. +image: /images/graphql/graphql-schema-structure.png +hide_table_of_contents: true +slug: graphql-schema-part-2-3 +--- + +import Quiz from "../src/components/quiz/quiz.tsx" + +![GraphQL Schema Structure](../static/images/graphql/graphql-schema-structure.png) + +## Removing Without Breaking: The Subtraction Subterfuge + +In our [previous post](/blog/graphql-schema-part-2-2), we explored how to modify existing schema elements without causing disruptions. Now, we'll tackle the most challenging part: removing schema elements and handling breaking changes. + + + +## Recap of Additive Changes and Modifications + +In [Part 2.1](/blog/graphql-schema-part-2-1), we focused on additive changes, emphasizing the importance of expanding your schema's capabilities without disrupting existing clients. By adding new fields, types, and arguments, you can enhance your API while maintaining backward compatibility. + +In [Part 2.2](/blog/graphql-schema-part-2-2), we delved into modifying existing schema elements, discussing how to handle changes such as updating default values, transforming non-null fields to nullable, and changing field types. We highlighted the need for clear communication, providing transition paths, and leveraging schema design tools to minimize client disruptions. + +### Safe, Dangerous, and Breaking Changes + +1. **Safe Changes:** Additive changes such as adding new fields or types that do not affect existing queries or functionality. +2. **Dangerous Changes:** Modifications that might not break the schema immediately but can cause subtle issues, such as changing default values or making non-nullable fields nullable. +3. **Breaking Changes:** Changes that will definitely break existing queries and require clients to update their code, such as removing fields or changing field types. + +## The Subtraction Subterfuge + +Removing things from your schema is almost always a breaking change. If you remove a field, type, or argument, clients that depend on it will break. You can't just take things away without consequences. + +But sometimes, it's necessary. Here's how to do it without causing a riot. + +### The Field Farewell + +Let's say we want to remove a field because it's causing performance issues. Here's the smart way to do it: + +1. Introduce a replacement +2. Deprecate the old field +3. Wait (patiently!) +4. Remove when usage has died down + +```diff +type Query { +- products(first: Int!): [Product!]! ++ products(first: Int!): [Product!]! @deprecated(reason: “products is deprecated and is getting replaced by the field `topProducts`.”) ++ topProducts(first: Int!): [Product!]! +} +``` + +By introducing `topProducts` and deprecating `products`, we give our clients time to adapt. And hey, we've even improved our API in the process! + +The old field may be removed after a certain period and if the usage for it has gone down. Keep in mind you don’t necessarily have to make the change unless absolutely needed. Additive changes and deprecations are sometimes enough to keep evolving the API. + +### The Argument Abandonment + +Removing an argument is similar to removing a field. You can deprecate it and + +introduce a new field with the desired behavior. Clients will have time to migrate to the new field before the old one is removed. + +```diff +type Query { +- products(first: Int!, featured: Boolean): String! ++ products(first: Int!, featured: Boolean): String! @deprecated(reason: “products is deprecated, use `allProducts` for products and `featuredProducts` to get products that are featured”) ++ allProducts(first: Int!): String! ++ featuredProducts(first: Int!): String! +} +``` + +If you need to make a change to an existing field, because arguments can’t be deprecated just yet, you should indicate that the argument is deprecated through its description. + +```diff +type Query { +- products(first: Int!, featured: Boolean): String! ++ products(first: Int!, ++ # DEPRECATED: This argument will be removed. Use query `featuredProducts`. ++ featured: Boolean ++ ): String! +} +``` + +### The Type Deletion Dilemma + +Sometimes, you need to remove an entire type from your schema. This is a major operation and requires careful planning. + +1. First, deprecate all fields that return this type: + +```graphql +type Query { + oldUser(id: ID!): OldUser @deprecated(reason: "Use `user` query with new User type instead") + user(id: ID!): User +} +``` + +2. If the type is part of a union or implements an interface, you'll need to be extra cautious. These can't be easily deprecated, so clear communication is key. +3. Finally, after a long deprecation period and when usage has dropped to zero, you can remove the type entirely. + +Note that you might want to deprecate using that type within your codebase to avoid developers to use that User type for new fields. Removing a type is even trickier when it’s part of union types or implements interfaces. Once again, union members and interface implementations cannot be marked as deprecated. This means that fields like node may stop working correctly if the type you’re removing was reachable through that field. + +Your best bet in these cases are to either keep this type as part of unions and through interfaces or to communicate that change very carefully through descriptions and out of band communication like documentation and emails. + +## Quiz Time! 🎉 + + + +## Conclusion + +Removing schema elements is a delicate process that requires strategic planning and clear communication to avoid breaking changes. By following the principles and strategies outlined in this article, you can confidently remove fields, arguments, and types while minimizing disruption to your clients. + +Remember these key takeaways: + +1. **Deprecate Cautiously**: Use deprecation notices, schema descriptions, and out-of-band communication to keep your clients informed about upcoming changes. +2. **Provide Transition Paths**: When breaking changes are necessary, offer clear migration paths. This might involve introducing new fields alongside deprecated ones or providing new query structures that achieve the same results. +3. **Monitor Usage**: Keep an eye on usage metrics to determine when it's safe to remove deprecated elements. Don't rush the process – give your clients time to adapt. + :::note + Tailcall supports a variety of integrations with monitoring tools to help you track usage metrics and make informed decisions about schema changes. you can check out our [documentation](/docs/graphql-data-dog-telemetry-tailcall/) for more information. + ::: + +By treating your GraphQL schema as a product with its own lifecycle and evolution strategy, you can build APIs that are both powerful and adaptable. This approach allows you to innovate rapidly while providing a stable and reliable service to your clients. + +Remember, a great GraphQL schema is never truly finished – it's a living, breathing entity that grows and evolves with your application's needs. Embrace this continuous evolution, and you'll create APIs that stand the test of time. diff --git a/docusaurus.config.ts b/docusaurus.config.ts index c27957c91..018a61fd4 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -114,7 +114,7 @@ export default { prism: { theme: prismTheme, darkTheme: prismThemes.dracula, - additionalLanguages: ["protobuf", "json"], + additionalLanguages: ["protobuf", "json", "diff"], }, colorMode: { disableSwitch: true, diff --git a/package-lock.json b/package-lock.json index f905e7c05..31c95d25b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "graphiql": "^3.3.1", "graphql-ws": "^5.16.0", "lottie-light-react": "^2.4.0", + "lucide-react": "^0.414.0", "postcss": "^8.4.38", "prism-react-renderer": "^2.3.1", "react": "^18.3.1", @@ -13961,6 +13962,14 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.414.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.414.0.tgz", + "integrity": "sha512-Krr/MHg9AWoJc52qx8hyJ64X9++JNfS1wjaJviLM1EP/68VNB7Tv0VMldLCB1aUe6Ka9QxURPhQm/eB6cqOM3A==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/lunr": { "version": "2.3.9", "license": "MIT" diff --git a/package.json b/package.json index 85443aa0e..1688ca83c 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "graphiql": "^3.3.1", "graphql-ws": "^5.16.0", "lottie-light-react": "^2.4.0", + "lucide-react": "^0.414.0", "postcss": "^8.4.38", "prism-react-renderer": "^2.3.1", "react": "^18.3.1", diff --git a/src/components/quiz/quiz.tsx b/src/components/quiz/quiz.tsx new file mode 100644 index 000000000..ce1527140 --- /dev/null +++ b/src/components/quiz/quiz.tsx @@ -0,0 +1,146 @@ +import React, {useState} from "react" +import {Check, X, Frown} from "lucide-react" +import {analyticsHandler} from "@site/src/utils" + +type Question = { + id: number + text: string + options: string[] + correctAnswer: number +} + +type QuizProps = { + questions: Question[] + title: string +} + +const TwitterLogo = () => ( + + + +) + +const Quiz: React.FC = ({questions, title}) => { + const [currentQuestion, setCurrentQuestion] = useState(0) + const [score, setScore] = useState(0) + const [showScore, setShowScore] = useState(false) + const [selectedAnswer, setSelectedAnswer] = useState(null) + + const handleAnswerClick = (selectedOption: number) => { + analyticsHandler(title, "Click", "Answer Clicked") + setSelectedAnswer(selectedOption) + + if (selectedOption === questions[currentQuestion].correctAnswer) { + setScore(score + 1) + } + + setTimeout(() => { + setSelectedAnswer(null) + const nextQuestion = currentQuestion + 1 + if (nextQuestion < questions.length) { + setCurrentQuestion(nextQuestion) + } else { + setShowScore(true) + } + }, 1000) + } + + const resetQuiz = () => { + analyticsHandler(title, "Click", "reset Clicked") + setCurrentQuestion(0) + setScore(0) + setShowScore(false) + setSelectedAnswer(null) + } + + const shareScore = (platform: "twitter" | "linkedin", score: number, questionsLength: number, title: string) => { + analyticsHandler(title, "score", "score shared") + const text = `I scored ${score} out of ${questionsLength} on the ${title} Quiz! Test your knowledge too!` + const url = document.location.href + let shareUrl = "" + + if (platform === "twitter") { + shareUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}&via=tailcallhq` + } + + window.open(shareUrl, "_blank") + } + + return ( +
+

{title} Quiz!

+ {showScore ? ( +
+

+ You scored {score} out of {questions.length} +

+ {score > 3 ? ( +
+

🎉

+

+ Congratulations! You're a GraphQL schema change expert! +

+
+ ) : ( +
+ +

Keep learning about schema changes. You'll get there!

+
+ )} +
+ +
+ +
+ ) : ( +
+

+ Question {currentQuestion + 1}/{questions.length} +

+

{questions[currentQuestion].text}

+
+ {questions[currentQuestion].options.map((option, index) => ( + + ))} +
+
+ )} +
+ ) +} + +export default Quiz diff --git a/src/css/custom.css b/src/css/custom.css index 2c4d0a723..4d345d002 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -576,3 +576,7 @@ pre.prism-code { div[class*="codeBlockContent"] div[class*="buttonGroup"] { display: none !important; } + +span.token.tag.script.language-javascript { + color: #fff !important; +} diff --git a/static/images/blog/cat-high-five-gif.webp b/static/images/blog/cat-high-five-gif.webp new file mode 100644 index 000000000..9d54ae2a0 Binary files /dev/null and b/static/images/blog/cat-high-five-gif.webp differ diff --git a/static/images/blog/puzzle-graphql-schema-1.png b/static/images/blog/puzzle-graphql-schema-1.png new file mode 100644 index 000000000..f03a76b4f Binary files /dev/null and b/static/images/blog/puzzle-graphql-schema-1.png differ