From b9a896840ba81354de306c8fd26fe98ff5bc8f5c Mon Sep 17 00:00:00 2001 From: Maxwell Brown Date: Fri, 14 Jun 2024 11:34:30 -0400 Subject: [PATCH] add landing page for effect tutorials --- .../basics/100-your-first-effect.mdx | 2 - .../tutorials/basics/200-returning-values.mdx | 1 - .../basics/300-combining-effects.mdx | 1 - .../tutorials/basics/400-using-generators.mdx | 1 - content/tutorials/basics/index.mdx | 8 +- .../100-getting-started.mdx | 2 - .../200-dealing-with-errors.mdx | 2 - .../300-handling-requirements.mdx | 2 - .../tutorials/incremental-adoption/index.mdx | 8 +- package.json | 4 + pnpm-lock.yaml | 111 +++++++++++ .../[...slug]/components/Navigation.tsx | 1 + src/app/tutorials/[...slug]/page.tsx | 33 ++-- .../components/DifficultySelector.tsx | 68 +++++++ .../tutorials/components/TutorialsDisplay.tsx | 151 +++++++++++++++ src/app/tutorials/layout.tsx | 6 +- src/app/tutorials/page.tsx | 24 +++ src/components/icons/chevron-down.tsx | 4 +- src/components/ui/card.tsx | 83 +++++++++ src/components/ui/checkbox.tsx | 29 +++ src/components/ui/form.tsx | 176 ++++++++++++++++++ src/components/ui/select.tsx | 164 ++++++++++++++++ src/contentlayer/schema/tutorial.ts | 13 +- 23 files changed, 860 insertions(+), 34 deletions(-) create mode 100644 src/app/tutorials/components/DifficultySelector.tsx create mode 100644 src/app/tutorials/components/TutorialsDisplay.tsx create mode 100644 src/app/tutorials/page.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/form.tsx create mode 100644 src/components/ui/select.tsx diff --git a/content/tutorials/basics/100-your-first-effect.mdx b/content/tutorials/basics/100-your-first-effect.mdx index ed85e9573..36f919207 100644 --- a/content/tutorials/basics/100-your-first-effect.mdx +++ b/content/tutorials/basics/100-your-first-effect.mdx @@ -1,7 +1,5 @@ --- title: Your first Effect -excerpt: Learn the basics of Effect -section: Learn the basics --- ### What is an Effect? diff --git a/content/tutorials/basics/200-returning-values.mdx b/content/tutorials/basics/200-returning-values.mdx index 902f106a5..cae054c4d 100644 --- a/content/tutorials/basics/200-returning-values.mdx +++ b/content/tutorials/basics/200-returning-values.mdx @@ -1,6 +1,5 @@ --- title: Returning values -excerpt: Learn the basics of Effect --- Congratulations on running your first Effect! diff --git a/content/tutorials/basics/300-combining-effects.mdx b/content/tutorials/basics/300-combining-effects.mdx index 201d8115c..c3983d72c 100644 --- a/content/tutorials/basics/300-combining-effects.mdx +++ b/content/tutorials/basics/300-combining-effects.mdx @@ -1,6 +1,5 @@ --- title: Combining Effects -excerpt: Learn the basics of Effect --- Now that you can create and run effects, let's take a look at how you can diff --git a/content/tutorials/basics/400-using-generators.mdx b/content/tutorials/basics/400-using-generators.mdx index 366a5ca57..ec2b5db17 100644 --- a/content/tutorials/basics/400-using-generators.mdx +++ b/content/tutorials/basics/400-using-generators.mdx @@ -1,6 +1,5 @@ --- title: Using generators -excerpt: Learn the basics of Effect --- To make using Effect more approachable, we can use generators to write our diff --git a/content/tutorials/basics/index.mdx b/content/tutorials/basics/index.mdx index ff83b1e1a..e72485d49 100644 --- a/content/tutorials/basics/index.mdx +++ b/content/tutorials/basics/index.mdx @@ -1,7 +1,11 @@ --- title: Welcome -excerpt: Learn the basics of Effect -section: Learn the basics +excerpt: Learn the basics of Effect by exploring fundamental concepts and core data types. +section: The Basics of Effect +difficulty: beginner +prerequisites: + - Familiarity with asynchronous programming in JavaScript + - Basic understanding of TypeScript syntax and features --- Welcome to the Effect tutorials! diff --git a/content/tutorials/incremental-adoption/100-getting-started.mdx b/content/tutorials/incremental-adoption/100-getting-started.mdx index 60b1bbe1f..c7916365a 100644 --- a/content/tutorials/incremental-adoption/100-getting-started.mdx +++ b/content/tutorials/incremental-adoption/100-getting-started.mdx @@ -1,7 +1,5 @@ --- title: Getting Started -excerpt: Learn how to incrementally adopt Effect into your application -section: Incremental Adoption workspace: express --- diff --git a/content/tutorials/incremental-adoption/200-dealing-with-errors.mdx b/content/tutorials/incremental-adoption/200-dealing-with-errors.mdx index 5a043561c..19c14da04 100644 --- a/content/tutorials/incremental-adoption/200-dealing-with-errors.mdx +++ b/content/tutorials/incremental-adoption/200-dealing-with-errors.mdx @@ -1,7 +1,5 @@ --- title: Dealing with Errors -excerpt: Learn how to incrementally adopt Effect into your application -section: Incremental Adoption workspace: express --- diff --git a/content/tutorials/incremental-adoption/300-handling-requirements.mdx b/content/tutorials/incremental-adoption/300-handling-requirements.mdx index c472162b4..95042f4cd 100644 --- a/content/tutorials/incremental-adoption/300-handling-requirements.mdx +++ b/content/tutorials/incremental-adoption/300-handling-requirements.mdx @@ -1,7 +1,5 @@ --- title: Handling Requirements -excerpt: Learn how to incrementally adopt Effect into your application -section: Incremental Adoption workspace: express --- diff --git a/content/tutorials/incremental-adoption/index.mdx b/content/tutorials/incremental-adoption/index.mdx index 76296b71a..1a09544d7 100644 --- a/content/tutorials/incremental-adoption/index.mdx +++ b/content/tutorials/incremental-adoption/index.mdx @@ -1,8 +1,14 @@ --- title: Introduction -excerpt: Learn how to incrementally adopt Effect into your application +excerpt: Explore how Effect can be adopted incrementally into existing applications. section: Incremental Adoption +difficulty: intermediate workspace: express +prerequisites: + - Completion of The Basics of Effect + - Understanding of how effects are executed + - Ability to handle errors with Effect + - Comfort managing requirements with Effect --- ### Incrementally Adopting Effect diff --git a/package.json b/package.json index db9659946..92c02f5cc 100644 --- a/package.json +++ b/package.json @@ -21,13 +21,16 @@ "@effect/rpc-http": "^0.28.44", "@effect/schema": "^0.67.22", "@headlessui/react": "1.7.17", + "@hookform/resolvers": "^3.6.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-radio-group": "^1.1.3", + "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", @@ -55,6 +58,7 @@ "next-themes": "^0.3.0", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.51.5", "react-instantsearch": "^7.4.1", "react-resizable-panels": "^2.0.19", "react-tweet": "^3.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e91a4c1d7..ece502133 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,12 +32,18 @@ importers: '@headlessui/react': specifier: 1.7.17 version: 1.7.17(react-dom@18.0.0(react@18.0.0))(react@18.0.0) + '@hookform/resolvers': + specifier: ^3.6.0 + version: 3.6.0(react-hook-form@7.51.5(react@18.0.0)) '@radix-ui/react-accordion': specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) '@radix-ui/react-alert-dialog': specifier: ^1.0.5 version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) + '@radix-ui/react-checkbox': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) '@radix-ui/react-dialog': specifier: ^1.0.5 version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) @@ -53,6 +59,9 @@ importers: '@radix-ui/react-radio-group': specifier: ^1.1.3 version: 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) + '@radix-ui/react-select': + specifier: ^2.0.0 + version: 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.3.2)(react@18.0.0) @@ -134,6 +143,9 @@ importers: react-dom: specifier: ^18 version: 18.0.0(react@18.0.0) + react-hook-form: + specifier: ^7.51.5 + version: 7.51.5(react@18.0.0) react-instantsearch: specifier: ^7.4.1 version: 7.4.1(algoliasearch@4.23.3)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) @@ -841,6 +853,11 @@ packages: react: ^16 || ^17 || ^18 react-dom: ^16 || ^17 || ^18 + '@hookform/resolvers@3.6.0': + resolution: {integrity: sha512-UBcpyOX3+RR+dNnqBd0lchXpoL8p4xC21XP8H6Meb8uve5Br1GCnmg0PcBoKKqPKgGu9GHQ/oygcmPrQhetwqw==} + peerDependencies: + react-hook-form: ^7.0.0 + '@humanwhocodes/config-array@0.11.14': resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -1273,6 +1290,9 @@ packages: typescript: optional: true + '@radix-ui/number@1.0.1': + resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} + '@radix-ui/primitive@1.0.1': resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} @@ -1315,6 +1335,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-checkbox@1.0.4': + resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collapsible@1.0.3': resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==} peerDependencies: @@ -1555,6 +1588,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-select@2.0.0': + resolution: {integrity: sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.0.2': resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -4504,6 +4550,12 @@ packages: peerDependencies: react: ^18.0.0 + react-hook-form@7.51.5: + resolution: {integrity: sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q==} + engines: {node: '>=12.22.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + react-instantsearch-core@7.4.1: resolution: {integrity: sha512-Nk47IJaCrIecoAfH1vdLStvdOflN+O+TiHqUoLU2NMlQ6cK4OoCQTHG5Vyc1DaiO4uctBkvjcCQhhzZOdk0tRg==} peerDependencies: @@ -5996,6 +6048,10 @@ snapshots: react: 18.0.0 react-dom: 18.0.0(react@18.0.0) + '@hookform/resolvers@3.6.0(react-hook-form@7.51.5(react@18.0.0))': + dependencies: + react-hook-form: 7.51.5(react@18.0.0) + '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -6435,6 +6491,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@radix-ui/number@1.0.1': + dependencies: + '@babel/runtime': 7.24.6 + '@radix-ui/primitive@1.0.1': dependencies: '@babel/runtime': 7.24.6 @@ -6482,6 +6542,23 @@ snapshots: '@types/react': 18.3.2 '@types/react-dom': 18.3.0 + '@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)': + dependencies: + '@babel/runtime': 7.24.6 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.3.2)(react@18.0.0) + react: 18.0.0 + react-dom: 18.0.0(react@18.0.0) + optionalDependencies: + '@types/react': 18.3.2 + '@types/react-dom': 18.3.0 + '@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)': dependencies: '@babel/runtime': 7.24.6 @@ -6761,6 +6838,36 @@ snapshots: '@types/react': 18.3.2 '@types/react-dom': 18.3.0 + '@radix-ui/react-select@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)': + dependencies: + '@babel/runtime': 7.24.6 + '@radix-ui/number': 1.0.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.3.2)(react@18.0.0) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) + aria-hidden: 1.2.4 + react: 18.0.0 + react-dom: 18.0.0(react@18.0.0) + react-remove-scroll: 2.5.5(@types/react@18.3.2)(react@18.0.0) + optionalDependencies: + '@types/react': 18.3.2 + '@types/react-dom': 18.3.0 + '@radix-ui/react-slot@1.0.2(@types/react@18.3.2)(react@18.0.0)': dependencies: '@babel/runtime': 7.24.6 @@ -10371,6 +10478,10 @@ snapshots: react: 18.0.0 scheduler: 0.21.0 + react-hook-form@7.51.5(react@18.0.0): + dependencies: + react: 18.0.0 + react-instantsearch-core@7.4.1(algoliasearch@4.23.3)(react@18.0.0): dependencies: '@babel/runtime': 7.24.6 diff --git a/src/app/tutorials/[...slug]/components/Navigation.tsx b/src/app/tutorials/[...slug]/components/Navigation.tsx index f19bfcb90..fb099c49a 100644 --- a/src/app/tutorials/[...slug]/components/Navigation.tsx +++ b/src/app/tutorials/[...slug]/components/Navigation.tsx @@ -16,6 +16,7 @@ export declare namespace Navigation { } export const Navigation: React.FC = async ({ tutorial }) => { + console.log(groupedTutorials) const group = groupedTutorials[tutorialSection(tutorial)] const index = group.children.indexOf(tutorial) const previous = group.children[index - 1] diff --git a/src/app/tutorials/[...slug]/page.tsx b/src/app/tutorials/[...slug]/page.tsx index 7df1948f3..086a14d6b 100644 --- a/src/app/tutorials/[...slug]/page.tsx +++ b/src/app/tutorials/[...slug]/page.tsx @@ -3,7 +3,10 @@ import { notFound } from "next/navigation" import * as FS from "node:fs/promises" import * as Path from "node:path" import { MDX } from "@/components/atoms/mdx" -import { groupedTutorials, tutorialSection } from "@/workspaces/domain/tutorial" +import { + groupedTutorials, + tutorialSection +} from "@/workspaces/domain/tutorial" import { Navigation } from "./components/Navigation" import { Tutorial } from "./components/Tutorial" @@ -70,19 +73,21 @@ export default async function Page({ ) return ( - } - next={ - next && { - title: next.title, - url: next.urlPath +
+ } + next={ + next && { + title: next.title, + url: next.urlPath + } } - } - > - - + > + + +
) } diff --git a/src/app/tutorials/components/DifficultySelector.tsx b/src/app/tutorials/components/DifficultySelector.tsx new file mode 100644 index 000000000..1872784ea --- /dev/null +++ b/src/app/tutorials/components/DifficultySelector.tsx @@ -0,0 +1,68 @@ +"use client" + +import { useCallback, useState } from "react" +import type { Tutorial } from "contentlayer/generated" +import type { CheckedState } from "@radix-ui/react-checkbox" +import { + AccordionContent, + AccordionItem, + AccordionTrigger +} from "@/components/ui/accordion" +import { Checkbox } from "@/components/ui/checkbox" +import { Label } from "@/components/ui/label" + +export type Difficulty = Tutorial["difficulty"] + +const levels: ReadonlyArray<{ + readonly id: Difficulty + readonly label: string +}> = [ + { id: "beginner", label: "Beginner" }, + { id: "intermediate", label: "Intermediate" }, + { id: "advanced", label: "Advanced" } +] + +export function DifficultySelector({ + onSelect, + onUnselect +}: { + readonly onSelect?: (choice: Difficulty) => void + readonly onUnselect?: (choice: Difficulty) => void +}) { + const handleCheckedChange = useCallback( + (checked: CheckedState, difficulty: Difficulty) => { + if (checked) { + onSelect?.(difficulty) + } else { + onUnselect?.(difficulty) + } + }, + [onSelect, onUnselect] + ) + + return ( + + + Difficulty + + +
+ {levels.map((choice) => ( + + ))} +
+
+
+ ) +} diff --git a/src/app/tutorials/components/TutorialsDisplay.tsx b/src/app/tutorials/components/TutorialsDisplay.tsx new file mode 100644 index 000000000..787faf564 --- /dev/null +++ b/src/app/tutorials/components/TutorialsDisplay.tsx @@ -0,0 +1,151 @@ +"use client" + +import { useReducer } from "react" +import { Data } from "effect" +import type { Tutorial } from "contentlayer/generated" +import { Accordion } from "@/components/ui/accordion" +import { + Card, + CardHeader, + CardDescription, + CardContent, + CardFooter, + CardTitle +} from "@/components/ui/card" +import { Icon } from "@/components/icons" +import { groupedTutorials, TutorialGroup } from "@/workspaces/domain/tutorial" +import { DifficultySelector } from "./DifficultySelector" + +export type Difficulty = Tutorial["difficulty"] + +interface State { + readonly difficulties: ReadonlyArray +} + +type Action = Data.TaggedEnum<{ + readonly SelectDifficulty: { readonly difficulty: Difficulty } + readonly UnselectDifficulty: { readonly difficulty: Difficulty } +}> +const Action = Data.taggedEnum() + +const initialState: State = { + difficulties: [] +} + +function reducer(state: State, action: Action): State { + switch (action._tag) { + case "SelectDifficulty": + return { + ...state, + difficulties: [...state.difficulties, action.difficulty] + } + case "UnselectDifficulty": + return { + ...state, + difficulties: state.difficulties.filter( + (value) => value !== action.difficulty + ) + } + } +} + +function applyDifficultyFilter( + tutorials: ReadonlyArray, + difficulties: ReadonlyArray +): ReadonlyArray { + if (difficulties.length === 0) { + return tutorials + } + return tutorials.filter(({ index }) => + difficulties.includes(index.difficulty) + ) +} + +export function TutorialsDisplay() { + const [state, dispatch] = useReducer(reducer, initialState) + + const groups = Object.values(groupedTutorials) + const byDifficulty = applyDifficultyFilter(groups, state.difficulties) + const tutorials = byDifficulty.map(({ index }) => index) + + return ( +
+
+ + + dispatch(Action.SelectDifficulty({ difficulty })) + } + onUnselect={(difficulty) => + dispatch(Action.UnselectDifficulty({ difficulty })) + } + /> + +
+ + +
+ ) +} diff --git a/src/app/tutorials/layout.tsx b/src/app/tutorials/layout.tsx index ad9aef5cc..bef44e50f 100644 --- a/src/app/tutorials/layout.tsx +++ b/src/app/tutorials/layout.tsx @@ -1,12 +1,12 @@ import { ReactNode } from "react" import { Toaster } from "@/components/ui/toaster" +import { Navigation } from "@/components/layout/navigation" export default function RootLayout({ children }: { children: ReactNode }) { return ( <> -
- {children} -
+ + {children} ) diff --git a/src/app/tutorials/page.tsx b/src/app/tutorials/page.tsx new file mode 100644 index 000000000..7a34335d2 --- /dev/null +++ b/src/app/tutorials/page.tsx @@ -0,0 +1,24 @@ +"use server" + +import { TutorialsDisplay } from "./components/TutorialsDisplay" + +export default async function TutorialsPage() { + return ( +
+
+
+
+

+ Explore Our Tutorials +

+

+ Find the perfect learning path to level-up your understanding of + Effect! Browse our curated selection of educational materials. +

+
+
+
+ +
+ ) +} diff --git a/src/components/icons/chevron-down.tsx b/src/components/icons/chevron-down.tsx index fb71b2495..50971b64f 100644 --- a/src/components/icons/chevron-down.tsx +++ b/src/components/icons/chevron-down.tsx @@ -17,8 +17,8 @@ export const ChevronDownIcon: React.FC = ({ ) diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 000000000..af6a1a8dd --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,83 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent +} diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 000000000..56338aafb --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,29 @@ +"use client" + +import React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Icon } from "@/components/icons" +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 000000000..281e56ffe --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,176 @@ +import React from "react" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext +} from "react-hook-form" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { Label } from "@/components/ui/label" +import { cn } from "@/lib/utils" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +