├── .gitignore
├── README.md
└── examples
├── 2024-06-24-complex-navigation-expo
├── .prettier.config.js
├── .prettierignore
├── README.md
├── app.json
├── app
│ ├── (login)
│ │ ├── (auth)
│ │ │ ├── _layout.tsx
│ │ │ ├── index.tsx
│ │ │ └── register.tsx
│ │ ├── _layout.tsx
│ │ └── forgot-password.tsx
│ ├── (main)
│ │ ├── (home)
│ │ │ ├── _layout.tsx
│ │ │ ├── details.tsx
│ │ │ ├── index.tsx
│ │ │ └── options.tsx
│ │ ├── _layout.tsx
│ │ └── settings.tsx
│ ├── +html.tsx
│ ├── +not-found.tsx
│ └── _layout.tsx
├── assets
│ ├── fonts
│ │ └── SpaceMono-Regular.ttf
│ └── images
│ │ ├── adaptive-icon.png
│ │ ├── favicon.png
│ │ ├── icon.png
│ │ ├── partial-react-logo.png
│ │ ├── react-logo.png
│ │ ├── react-logo@2x.png
│ │ ├── react-logo@3x.png
│ │ └── splash.png
├── babel.config.js
├── components
│ ├── Collapsible.tsx
│ ├── ExternalLink.tsx
│ ├── HelloWave.tsx
│ ├── ParallaxScrollView.tsx
│ ├── ThemedText.tsx
│ ├── ThemedView.tsx
│ ├── __tests__
│ │ ├── ThemedText-test.tsx
│ │ └── __snapshots__
│ │ │ └── ThemedText-test.tsx.snap
│ └── navigation
│ │ └── TabBarIcon.tsx
├── constants
│ └── Colors.ts
├── eslint.config.mjs
├── hooks
│ ├── useColorScheme.ts
│ ├── useColorScheme.web.ts
│ └── useThemeColor.ts
├── package-lock.json
├── package.json
├── scripts
│ └── reset-project.js
└── tsconfig.json
├── 2024-07-04-master-higher-order-components
├── layout.jsx
├── with-layout.jsx
└── with-layout.test.jsx
├── 2024-07-16-set-up-next-js-production
├── .github
│ └── workflows
│ │ └── pull-request.yml
├── .gitignore
├── .husky
│ ├── pre-commit
│ └── prepare-commit-msg
├── README.md
├── components.json
├── eslint.config.mjs
├── next.config.mjs
├── package-lock.json
├── package.json
├── playwright.config.ts
├── playwright
│ └── example.spec.ts
├── postcss.config.mjs
├── prettier.config.js
├── prisma
│ ├── schema.prisma
│ └── seed.ts
├── public
│ ├── next.svg
│ └── vercel.svg
├── src
│ ├── app
│ │ ├── [lang]
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ └── login
│ │ │ ├── counter-component.tsx
│ │ │ └── page.tsx
│ ├── features
│ │ ├── internationalization
│ │ │ ├── dictionaries
│ │ │ │ └── en-US.json
│ │ │ ├── get-dictionaries.ts
│ │ │ ├── i18n-config.ts
│ │ │ ├── localization-middleware.ts
│ │ │ └── use-switch-locale-href.ts
│ │ ├── user-authentication
│ │ │ └── example.test.tsx
│ │ └── user-profile
│ │ │ └── user-profile-model.ts
│ ├── lib
│ │ ├── prisma.ts
│ │ └── utils.ts
│ ├── middleware.ts
│ └── tests
│ │ ├── react-test-utils.tsx
│ │ └── setup-test-environment.ts
├── tailwind.config.ts
├── tsconfig.json
└── vitest.config.ts
├── 2024-08-09-redux-basics
├── .eslintrc.json
├── .gitignore
├── README.md
├── eslint.config.mjs
├── jsconfig.json
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── prettier.config.js
├── public
│ ├── next.svg
│ └── vercel.svg
├── src
│ └── app
│ │ ├── example-reducer.js
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── hooks.js
│ │ ├── layout.js
│ │ ├── page-2.js
│ │ ├── page-3.js
│ │ ├── page.js
│ │ ├── store-2.js
│ │ ├── store-3.js
│ │ ├── store-provider.js
│ │ ├── store.js
│ │ ├── user-profile-component.js
│ │ ├── user-profile-container.js
│ │ └── user-profile.js
└── tailwind.config.js
├── 2024-08-17-memoization
├── .gitignore
├── example-1.js
├── example-2.js
├── example-3.js
├── example-4.ts
├── example-5.ts
├── package-lock.json
├── package.json
└── tsconfig.json
├── 2024-08-20-redux-saga
├── .eslintrc.json
├── .gitignore
├── README.md
├── components.json
├── jsconfig.json
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── prettier.config.js
├── public
│ ├── next.svg
│ └── vercel.svg
├── src
│ ├── app
│ │ ├── dashboard
│ │ │ └── page.js
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── layout.js
│ │ ├── login
│ │ │ └── page.js
│ │ └── page.js
│ ├── components
│ │ └── ui
│ │ │ ├── button.jsx
│ │ │ ├── card.jsx
│ │ │ ├── input.jsx
│ │ │ └── label.jsx
│ ├── features
│ │ ├── dashboard
│ │ │ ├── dashboard-page-component.js
│ │ │ └── dashboard-page-container.js
│ │ ├── example
│ │ │ ├── example-reducer.js
│ │ │ └── example-saga.js
│ │ ├── user-authentication
│ │ │ ├── user-authentication-page-component.js
│ │ │ └── user-authentication-page-container.js
│ │ └── user-profiles
│ │ │ ├── user-profile-api.js
│ │ │ ├── user-profile-reducer.js
│ │ │ └── user-profile-saga.js
│ ├── hocs
│ │ └── with-router.js
│ ├── lib
│ │ └── utils.js
│ └── redux
│ │ ├── effects.js
│ │ ├── root-reducer.js
│ │ ├── root-saga.js
│ │ ├── saga-middleware.js
│ │ ├── store-provider.js
│ │ └── store.js
├── tailwind.config.js
└── tsconfig.json
└── 2024-09-01-redux-for-production
├── .eslintrc.json
├── .gitignore
├── README.md
├── components.json
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── prettier.config.js
├── public
├── next.svg
└── vercel.svg
├── src
├── app
│ ├── dashboard
│ │ └── page.ts
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ ├── login
│ │ └── page.ts
│ ├── page.tsx
│ └── posts
│ │ └── page.ts
├── components
│ ├── spinner.tsx
│ └── ui
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── input.tsx
│ │ └── label.tsx
├── features
│ ├── app-loading
│ │ ├── app-loading-component.tsx
│ │ ├── app-loading-container.tsx
│ │ ├── app-loading-reducer.ts
│ │ └── app-loading-saga.ts
│ ├── dashboard
│ │ ├── dashboard-page-component.tsx
│ │ └── dashboard-page-container.tsx
│ ├── posts
│ │ ├── add-post-component.tsx
│ │ ├── posts-api.ts
│ │ ├── posts-list-component.tsx
│ │ ├── posts-page-component.tsx
│ │ └── posts-types.ts
│ ├── user-authentication
│ │ ├── user-authentication-api.ts
│ │ ├── user-authentication-component.tsx
│ │ ├── user-authentication-container.ts
│ │ ├── user-authentication-reducer.ts
│ │ └── user-authentication-saga.ts
│ └── user-profiles
│ │ ├── user-profiles-api.ts
│ │ ├── user-profiles-reducer.ts
│ │ ├── user-profiles-saga.ts
│ │ └── user-profiles-types.ts
├── hocs
│ ├── authenticated-page.ts
│ ├── hoist-statics.ts
│ ├── public-page.ts
│ ├── redirect-if-logged-in.ts
│ ├── redirect.tsx
│ ├── requires-permission
│ │ ├── index.ts
│ │ └── requires-permission-component.tsx
│ ├── with-auth.ts
│ └── with-loading.ts
├── lib
│ └── utils.ts
└── redux
│ ├── clear.ts
│ ├── hooks.ts
│ ├── root-reducer.ts
│ ├── root-saga.ts
│ ├── store-provider.tsx
│ └── store.ts
├── tailwind.config.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | examples/2024-06-24-complex-navigation-expo/.gitignore
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Jan Hesters & ReactSquad Tutorials
2 |
3 | In this repository, you can find all code examples and resources for the Jan Hesters' and ReactSquad's tutorials on YouTube, X, LinkedIn etc.
4 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/.prettier.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable unicorn/prefer-module */
2 | module.exports = {
3 | arrowParens: "avoid",
4 | bracketSameLine: false,
5 | bracketSpacing: true,
6 | htmlWhitespaceSensitivity: "css",
7 | insertPragma: false,
8 | jsxSingleQuote: false,
9 | plugins: ["prettier-plugin-tailwindcss"],
10 | printWidth: 80,
11 | proseWrap: "always",
12 | quoteProps: "as-needed",
13 | requirePragma: false,
14 | semi: true,
15 | singleQuote: true,
16 | tabWidth: 2,
17 | trailingComma: "all",
18 | useTabs: false,
19 | };
20 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .next
3 | yarn.lock
4 | package-lock.json
5 | public
6 | coverage
7 | templates
8 | build
9 | playwright-report
10 | test-results
11 | .expo
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/README.md:
--------------------------------------------------------------------------------
1 | # Buliding a Recat Native App with Complex Navigation in 2024
2 |
3 | This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
4 |
5 | ## Get started
6 |
7 | 1. Install dependencies
8 |
9 | ```bash
10 | npm install
11 | ```
12 |
13 | 2. Start the app
14 |
15 | ```bash
16 | npx expo start
17 | ```
18 |
19 | In the output, you'll find options to open the app in a
20 |
21 | - [development build](https://docs.expo.dev/develop/development-builds/introduction/)
22 | - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
23 | - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
24 | - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
25 |
26 | You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
27 |
28 | ## Get a fresh project
29 |
30 | When you're ready, run:
31 |
32 | ```bash
33 | npm run reset-project
34 | ```
35 |
36 | This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
37 |
38 | ## Learn more
39 |
40 | To learn more about developing your project with Expo, look at the following resources:
41 |
42 | - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
43 | - [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
44 |
45 | ## Join the community
46 |
47 | Join our community of developers creating universal apps.
48 |
49 | - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
50 | - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
51 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "complex-navigation-expo",
4 | "slug": "complex-navigation-expo",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/images/icon.png",
8 | "scheme": "myapp",
9 | "userInterfaceStyle": "automatic",
10 | "splash": {
11 | "image": "./assets/images/splash.png",
12 | "resizeMode": "contain",
13 | "backgroundColor": "#ffffff"
14 | },
15 | "ios": {
16 | "supportsTablet": true
17 | },
18 | "android": {
19 | "adaptiveIcon": {
20 | "foregroundImage": "./assets/images/adaptive-icon.png",
21 | "backgroundColor": "#ffffff"
22 | }
23 | },
24 | "web": {
25 | "bundler": "metro",
26 | "output": "static",
27 | "favicon": "./assets/images/favicon.png"
28 | },
29 | "plugins": [
30 | "expo-router"
31 | ],
32 | "experiments": {
33 | "typedRoutes": true
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/app/(login)/(auth)/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Tabs } from "expo-router";
2 | import React from "react";
3 |
4 | import { TabBarIcon } from "@/components/navigation/TabBarIcon";
5 | import { Colors } from "@/constants/Colors";
6 | import { useColorScheme } from "@/hooks/useColorScheme";
7 |
8 | export default function AuthLayout() {
9 | const colorScheme = useColorScheme();
10 |
11 | return (
12 |
18 | (
23 |
27 | ),
28 | }}
29 | />
30 | (
35 |
39 | ),
40 | }}
41 | />
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/app/(login)/(auth)/index.tsx:
--------------------------------------------------------------------------------
1 | import { ThemedText } from "@/components/ThemedText";
2 | import { ThemedView } from "@/components/ThemedView";
3 | import { StyleSheet } from "react-native";
4 | import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
5 | import { Button } from "@rneui/themed";
6 | import { Link } from "expo-router";
7 | import { router } from "expo-router";
8 |
9 | export default function LoginView() {
10 | return (
11 |
12 |
13 |
14 | Hello from the Login view
15 |
16 |
17 | Forgot password
18 |
19 |
20 |
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | const styles = StyleSheet.create({
34 | container: {
35 | flex: 1,
36 | },
37 | innerContainer: {
38 | flex: 1,
39 | justifyContent: "space-around",
40 | alignItems: "center",
41 | },
42 | link: {
43 | lineHeight: 30,
44 | fontSize: 16,
45 | },
46 | });
47 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/app/(login)/(auth)/register.tsx:
--------------------------------------------------------------------------------
1 | import { ThemedText } from "@/components/ThemedText";
2 | import { ThemedView } from "@/components/ThemedView";
3 | import { StyleSheet } from "react-native";
4 | import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
5 |
6 | export default function RegisterView() {
7 | return (
8 |
9 |
10 |
11 | Register view
12 |
13 |
14 |
15 | );
16 | }
17 |
18 | const styles = StyleSheet.create({
19 | container: {
20 | flex: 1,
21 | },
22 | innerContainer: {
23 | flex: 1,
24 | justifyContent: "space-around",
25 | alignItems: "center",
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/app/(login)/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Stack } from "expo-router";
2 | import "react-native-reanimated";
3 |
4 | export default function LoginLayout() {
5 | return (
6 |
7 |
8 |
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/app/(login)/forgot-password.tsx:
--------------------------------------------------------------------------------
1 | import { ThemedText } from "@/components/ThemedText";
2 | import { ThemedView } from "@/components/ThemedView";
3 | import { StyleSheet } from "react-native";
4 | import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
5 | import { Link } from "expo-router";
6 |
7 | export default function ForgotPasswordView() {
8 | return (
9 |
10 |
11 |
12 | Forgot password view
13 |
14 |
15 | Back to Login
16 |
17 |
18 |
19 |
20 | );
21 | }
22 |
23 | const styles = StyleSheet.create({
24 | container: {
25 | flex: 1,
26 | },
27 | innerContainer: {
28 | flex: 1,
29 | justifyContent: "space-around",
30 | alignItems: "center",
31 | },
32 | link: {
33 | lineHeight: 30,
34 | fontSize: 16,
35 | },
36 | });
37 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/app/(main)/(home)/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Stack } from "expo-router";
2 | import "react-native-reanimated";
3 |
4 | export default function HomeLayout() {
5 | return (
6 |
7 |
11 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/app/(main)/(home)/details.tsx:
--------------------------------------------------------------------------------
1 | import { ThemedText } from "@/components/ThemedText";
2 | import { ThemedView } from "@/components/ThemedView";
3 | import { StyleSheet } from "react-native";
4 | import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
5 |
6 | export default function DetailsView() {
7 | return (
8 |
9 |
10 |
11 | Details view
12 |
13 |
14 |
15 | );
16 | }
17 |
18 | const styles = StyleSheet.create({
19 | container: {
20 | flex: 1,
21 | },
22 | innerContainer: {
23 | flex: 1,
24 | justifyContent: "space-around",
25 | alignItems: "center",
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/app/(main)/(home)/index.tsx:
--------------------------------------------------------------------------------
1 | import { ThemedText } from "@/components/ThemedText";
2 | import { ThemedView } from "@/components/ThemedView";
3 | import { Link } from "expo-router";
4 | import { StyleSheet } from "react-native";
5 | import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
6 |
7 | export default function HomeView() {
8 | return (
9 |
10 |
11 |
12 | Home view
13 |
14 |
15 | Options
16 |
17 |
18 |
19 | Details
20 |
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | const styles = StyleSheet.create({
28 | container: {
29 | flex: 1,
30 | },
31 | innerContainer: {
32 | flex: 1,
33 | justifyContent: "space-around",
34 | alignItems: "center",
35 | },
36 | link: {
37 | lineHeight: 30,
38 | fontSize: 16,
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/app/(main)/(home)/options.tsx:
--------------------------------------------------------------------------------
1 | import { ThemedText } from "@/components/ThemedText";
2 | import { ThemedView } from "@/components/ThemedView";
3 | import { StyleSheet } from "react-native";
4 | import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
5 |
6 | export default function OptionsView() {
7 | return (
8 |
9 |
10 |
11 | Options view
12 |
13 |
14 |
15 | );
16 | }
17 |
18 | const styles = StyleSheet.create({
19 | container: {
20 | flex: 1,
21 | },
22 | innerContainer: {
23 | flex: 1,
24 | justifyContent: "space-around",
25 | alignItems: "center",
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/app/(main)/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { router, Tabs, usePathname } from "expo-router";
2 | import { Drawer } from "expo-router/drawer";
3 | import React from "react";
4 | import { Pressable, Platform, StyleSheet } from "react-native";
5 | import { GestureHandlerRootView } from "react-native-gesture-handler";
6 |
7 | import { TabBarIcon } from "@/components/navigation/TabBarIcon";
8 | import { Colors } from "@/constants/Colors";
9 | import { useColorScheme } from "@/hooks/useColorScheme";
10 |
11 | export default function MainLayout() {
12 | const colorScheme = useColorScheme();
13 | const pathname = usePathname();
14 | const isHome = pathname === "/";
15 |
16 | if (Platform.OS === "android") {
17 | return (
18 |
19 |
20 |
28 | (
34 | {
37 | // In the real world, you should use a logout function here
38 | // and then auto redirect using the root layout ❗️
39 | router.replace("(login)");
40 | }}
41 | >
42 |
43 |
44 | ),
45 | }}
46 | />
47 |
48 |
49 | );
50 | }
51 |
52 | return (
53 |
58 | (
64 |
68 | ),
69 | }}
70 | />
71 | (
76 |
77 | ),
78 | headerLeft: () => (
79 | {
82 | // In the real world, you should use a logout function here
83 | // and then auto redirect using the root layout ❗️
84 | router.replace("(login)");
85 | }}
86 | >
87 |
88 |
89 | ),
90 | }}
91 | />
92 |
93 | );
94 | }
95 |
96 | const styles = StyleSheet.create({
97 | headerButton: {
98 | paddingHorizontal: 16,
99 | },
100 | });
101 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/app/(main)/settings.tsx:
--------------------------------------------------------------------------------
1 | import { ThemedText } from "@/components/ThemedText";
2 | import { ThemedView } from "@/components/ThemedView";
3 | import { StyleSheet } from "react-native";
4 | import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
5 |
6 | export default function SettingsView() {
7 | return (
8 |
9 |
10 |
11 | Settings view
12 |
13 |
14 |
15 | );
16 | }
17 |
18 | const styles = StyleSheet.create({
19 | container: {
20 | flex: 1,
21 | },
22 | innerContainer: {
23 | flex: 1,
24 | justifyContent: "space-around",
25 | alignItems: "center",
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/app/+html.tsx:
--------------------------------------------------------------------------------
1 | import { ScrollViewStyleReset } from "expo-router/html";
2 | import { type PropsWithChildren } from "react";
3 |
4 | /**
5 | * This file is web-only and used to configure the root HTML for every web page during static rendering.
6 | * The contents of this function only run in Node.js environments and do not have access to the DOM or browser APIs.
7 | */
8 | export default function Root({ children }: PropsWithChildren) {
9 | return (
10 |
11 |
12 |
13 |
14 |
18 |
19 | {/*
20 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
21 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
22 | */}
23 |
24 |
25 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
26 |
27 | {/* Add any additional elements that you want globally available on web... */}
28 |
29 | {children}
30 |
31 | );
32 | }
33 |
34 | const responsiveBackground = `
35 | body {
36 | background-color: #fff;
37 | }
38 | @media (prefers-color-scheme: dark) {
39 | body {
40 | background-color: #000;
41 | }
42 | }`;
43 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/app/+not-found.tsx:
--------------------------------------------------------------------------------
1 | import { Link, Stack } from "expo-router";
2 | import { StyleSheet } from "react-native";
3 |
4 | import { ThemedText } from "@/components/ThemedText";
5 | import { ThemedView } from "@/components/ThemedView";
6 |
7 | export default function NotFoundScreen() {
8 | return (
9 | <>
10 |
11 |
12 | This screen doesn't exist.
13 |
14 | Go to home screen!
15 |
16 |
17 | >
18 | );
19 | }
20 |
21 | const styles = StyleSheet.create({
22 | container: {
23 | flex: 1,
24 | alignItems: "center",
25 | justifyContent: "center",
26 | padding: 20,
27 | },
28 | link: {
29 | marginTop: 15,
30 | paddingVertical: 15,
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DarkTheme,
3 | DefaultTheme,
4 | ThemeProvider,
5 | } from "@react-navigation/native";
6 | import { Platform } from "react-native";
7 | import {
8 | lightColors,
9 | createTheme,
10 | ThemeProvider as RNEThemeProvider,
11 | } from "@rneui/themed";
12 | import { useFonts } from "expo-font";
13 | import { Stack } from "expo-router";
14 | import * as SplashScreen from "expo-splash-screen";
15 | import { useEffect } from "react";
16 | import "react-native-reanimated";
17 |
18 | import { useColorScheme } from "@/hooks/useColorScheme";
19 |
20 | // Prevent the splash screen from auto-hiding before asset loading is complete.
21 | SplashScreen.preventAutoHideAsync();
22 |
23 | const theme = createTheme({
24 | lightColors: {
25 | ...Platform.select({
26 | default: lightColors.platform.android,
27 | ios: lightColors.platform.ios,
28 | }),
29 | },
30 | });
31 |
32 | export default function RootLayout() {
33 | const colorScheme = useColorScheme();
34 | const [loaded] = useFonts({
35 | SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
36 | });
37 |
38 | useEffect(() => {
39 | if (loaded) {
40 | SplashScreen.hideAsync();
41 | }
42 | }, [loaded]);
43 |
44 | if (!loaded) {
45 | return null;
46 | }
47 |
48 | return (
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/assets/fonts/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EarlyNode/tutorials/e78d678cd56f528932fedc0b664cda7f8874d228/examples/2024-06-24-complex-navigation-expo/assets/fonts/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EarlyNode/tutorials/e78d678cd56f528932fedc0b664cda7f8874d228/examples/2024-06-24-complex-navigation-expo/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EarlyNode/tutorials/e78d678cd56f528932fedc0b664cda7f8874d228/examples/2024-06-24-complex-navigation-expo/assets/images/favicon.png
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EarlyNode/tutorials/e78d678cd56f528932fedc0b664cda7f8874d228/examples/2024-06-24-complex-navigation-expo/assets/images/icon.png
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/assets/images/partial-react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EarlyNode/tutorials/e78d678cd56f528932fedc0b664cda7f8874d228/examples/2024-06-24-complex-navigation-expo/assets/images/partial-react-logo.png
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/assets/images/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EarlyNode/tutorials/e78d678cd56f528932fedc0b664cda7f8874d228/examples/2024-06-24-complex-navigation-expo/assets/images/react-logo.png
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/assets/images/react-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EarlyNode/tutorials/e78d678cd56f528932fedc0b664cda7f8874d228/examples/2024-06-24-complex-navigation-expo/assets/images/react-logo@2x.png
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/assets/images/react-logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EarlyNode/tutorials/e78d678cd56f528932fedc0b664cda7f8874d228/examples/2024-06-24-complex-navigation-expo/assets/images/react-logo@3x.png
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/assets/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EarlyNode/tutorials/e78d678cd56f528932fedc0b664cda7f8874d228/examples/2024-06-24-complex-navigation-expo/assets/images/splash.png
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ["babel-preset-expo"],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/components/Collapsible.tsx:
--------------------------------------------------------------------------------
1 | import Ionicons from "@expo/vector-icons/Ionicons";
2 | import { PropsWithChildren, useState } from "react";
3 | import { StyleSheet, TouchableOpacity, useColorScheme } from "react-native";
4 |
5 | import { ThemedText } from "@/components/ThemedText";
6 | import { ThemedView } from "@/components/ThemedView";
7 | import { Colors } from "@/constants/Colors";
8 |
9 | export function Collapsible({
10 | children,
11 | title,
12 | }: PropsWithChildren & { title: string }) {
13 | const [isOpen, setIsOpen] = useState(false);
14 | const theme = useColorScheme() ?? "light";
15 |
16 | return (
17 |
18 | setIsOpen((value) => !value)}
21 | activeOpacity={0.8}
22 | >
23 |
28 | {title}
29 |
30 | {isOpen && {children}}
31 |
32 | );
33 | }
34 |
35 | const styles = StyleSheet.create({
36 | heading: {
37 | flexDirection: "row",
38 | alignItems: "center",
39 | gap: 6,
40 | },
41 | content: {
42 | marginTop: 6,
43 | marginLeft: 24,
44 | },
45 | });
46 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/components/ExternalLink.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "expo-router";
2 | import { openBrowserAsync } from "expo-web-browser";
3 | import { type ComponentProps } from "react";
4 | import { Platform } from "react-native";
5 |
6 | type Props = Omit, "href"> & { href: string };
7 |
8 | export function ExternalLink({ href, ...rest }: Props) {
9 | return (
10 | {
15 | if (Platform.OS !== "web") {
16 | // Prevent the default behavior of linking to the default browser on native.
17 | event.preventDefault();
18 | // Open the link in an in-app browser.
19 | await openBrowserAsync(href);
20 | }
21 | }}
22 | />
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/components/HelloWave.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from "react-native";
2 | import Animated, {
3 | useSharedValue,
4 | useAnimatedStyle,
5 | withTiming,
6 | withRepeat,
7 | withSequence,
8 | } from "react-native-reanimated";
9 |
10 | import { ThemedText } from "@/components/ThemedText";
11 |
12 | export function HelloWave() {
13 | const rotationAnimation = useSharedValue(0);
14 |
15 | rotationAnimation.value = withRepeat(
16 | withSequence(
17 | withTiming(25, { duration: 150 }),
18 | withTiming(0, { duration: 150 }),
19 | ),
20 | 4, // Run the animation 4 times
21 | );
22 |
23 | const animatedStyle = useAnimatedStyle(() => ({
24 | transform: [{ rotate: `${rotationAnimation.value}deg` }],
25 | }));
26 |
27 | return (
28 |
29 | 👋
30 |
31 | );
32 | }
33 |
34 | const styles = StyleSheet.create({
35 | text: {
36 | fontSize: 28,
37 | lineHeight: 32,
38 | marginTop: -6,
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/components/ParallaxScrollView.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren, ReactElement } from "react";
2 | import { StyleSheet, useColorScheme } from "react-native";
3 | import Animated, {
4 | interpolate,
5 | useAnimatedRef,
6 | useAnimatedStyle,
7 | useScrollViewOffset,
8 | } from "react-native-reanimated";
9 |
10 | import { ThemedView } from "@/components/ThemedView";
11 |
12 | const HEADER_HEIGHT = 250;
13 |
14 | type Props = PropsWithChildren<{
15 | headerImage: ReactElement;
16 | headerBackgroundColor: { dark: string; light: string };
17 | }>;
18 |
19 | export default function ParallaxScrollView({
20 | children,
21 | headerImage,
22 | headerBackgroundColor,
23 | }: Props) {
24 | const colorScheme = useColorScheme() ?? "light";
25 | const scrollRef = useAnimatedRef();
26 | const scrollOffset = useScrollViewOffset(scrollRef);
27 |
28 | const headerAnimatedStyle = useAnimatedStyle(() => {
29 | return {
30 | transform: [
31 | {
32 | translateY: interpolate(
33 | scrollOffset.value,
34 | [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
35 | [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75],
36 | ),
37 | },
38 | {
39 | scale: interpolate(
40 | scrollOffset.value,
41 | [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
42 | [2, 1, 1],
43 | ),
44 | },
45 | ],
46 | };
47 | });
48 |
49 | return (
50 |
51 |
52 |
59 | {headerImage}
60 |
61 | {children}
62 |
63 |
64 | );
65 | }
66 |
67 | const styles = StyleSheet.create({
68 | container: {
69 | flex: 1,
70 | },
71 | header: {
72 | height: 250,
73 | overflow: "hidden",
74 | },
75 | content: {
76 | flex: 1,
77 | padding: 32,
78 | gap: 16,
79 | overflow: "hidden",
80 | },
81 | });
82 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/components/ThemedText.tsx:
--------------------------------------------------------------------------------
1 | import { Text, type TextProps, StyleSheet } from "react-native";
2 |
3 | import { useThemeColor } from "@/hooks/useThemeColor";
4 |
5 | export type ThemedTextProps = TextProps & {
6 | lightColor?: string;
7 | darkColor?: string;
8 | type?: "default" | "title" | "defaultSemiBold" | "subtitle" | "link";
9 | };
10 |
11 | export function ThemedText({
12 | style,
13 | lightColor,
14 | darkColor,
15 | type = "default",
16 | ...rest
17 | }: ThemedTextProps) {
18 | const color = useThemeColor({ light: lightColor, dark: darkColor }, "text");
19 |
20 | return (
21 |
33 | );
34 | }
35 |
36 | const styles = StyleSheet.create({
37 | default: {
38 | fontSize: 16,
39 | lineHeight: 24,
40 | },
41 | defaultSemiBold: {
42 | fontSize: 16,
43 | lineHeight: 24,
44 | fontWeight: "600",
45 | },
46 | title: {
47 | fontSize: 32,
48 | fontWeight: "bold",
49 | lineHeight: 32,
50 | },
51 | subtitle: {
52 | fontSize: 20,
53 | fontWeight: "bold",
54 | },
55 | link: {
56 | lineHeight: 30,
57 | fontSize: 16,
58 | color: "#0a7ea4",
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/components/ThemedView.tsx:
--------------------------------------------------------------------------------
1 | import { View, type ViewProps } from "react-native";
2 |
3 | import { useThemeColor } from "@/hooks/useThemeColor";
4 |
5 | export type ThemedViewProps = ViewProps & {
6 | lightColor?: string;
7 | darkColor?: string;
8 | };
9 |
10 | export function ThemedView({
11 | style,
12 | lightColor,
13 | darkColor,
14 | ...otherProps
15 | }: ThemedViewProps) {
16 | const backgroundColor = useThemeColor(
17 | { light: lightColor, dark: darkColor },
18 | "background",
19 | );
20 |
21 | return ;
22 | }
23 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/components/__tests__/ThemedText-test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import renderer from "react-test-renderer";
3 |
4 | import { ThemedText } from "../ThemedText";
5 |
6 | it(`renders correctly`, () => {
7 | const tree = renderer
8 | .create(Snapshot test!)
9 | .toJSON();
10 |
11 | expect(tree).toMatchSnapshot();
12 | });
13 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/components/__tests__/__snapshots__/ThemedText-test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renders correctly 1`] = `
4 |
22 | Snapshot test!
23 |
24 | `;
25 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/components/navigation/TabBarIcon.tsx:
--------------------------------------------------------------------------------
1 | // You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
2 |
3 | import Ionicons from "@expo/vector-icons/Ionicons";
4 | import { type IconProps } from "@expo/vector-icons/build/createIconSet";
5 | import { type ComponentProps } from "react";
6 |
7 | export function TabBarIcon({
8 | style,
9 | ...rest
10 | }: IconProps["name"]>) {
11 | return ;
12 | }
13 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/constants/Colors.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
3 | * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
4 | */
5 |
6 | const tintColorLight = "#0a7ea4";
7 | const tintColorDark = "#fff";
8 |
9 | export const Colors = {
10 | light: {
11 | text: "#11181C",
12 | background: "#fff",
13 | tint: tintColorLight,
14 | icon: "#687076",
15 | tabIconDefault: "#687076",
16 | tabIconSelected: tintColorLight,
17 | },
18 | dark: {
19 | text: "#ECEDEE",
20 | background: "#151718",
21 | tint: tintColorDark,
22 | icon: "#9BA1A6",
23 | tabIconDefault: "#9BA1A6",
24 | tabIconSelected: tintColorDark,
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import prettier from "eslint-plugin-prettier";
2 | import path from "node:path";
3 | import { fileURLToPath } from "node:url";
4 | import js from "@eslint/js";
5 | import { FlatCompat } from "@eslint/eslintrc";
6 |
7 | const __filename = fileURLToPath(import.meta.url);
8 | const __dirname = path.dirname(__filename);
9 | const compat = new FlatCompat({
10 | baseDirectory: __dirname,
11 | recommendedConfig: js.configs.recommended,
12 | allConfig: js.configs.all,
13 | });
14 |
15 | export default [
16 | ...compat.extends("expo", "prettier"),
17 | {
18 | plugins: {
19 | prettier,
20 | },
21 | rules: {
22 | "prettier/prettier": "error",
23 | },
24 | },
25 | ];
26 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/hooks/useColorScheme.ts:
--------------------------------------------------------------------------------
1 | export { useColorScheme } from "react-native";
2 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/hooks/useColorScheme.web.ts:
--------------------------------------------------------------------------------
1 | // NOTE: The default React Native styling doesn't support server rendering.
2 | // Server rendered styles should not change between the first render of the HTML
3 | // and the first render on the client. Typically, web developers will use CSS media queries
4 | // to render different styles on the client and server, these aren't directly supported in React Native
5 | // but can be achieved using a styling library like Nativewind.
6 | export function useColorScheme() {
7 | return "light";
8 | }
9 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/hooks/useThemeColor.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Learn more about light and dark modes:
3 | * https://docs.expo.dev/guides/color-schemes/
4 | */
5 |
6 | import { useColorScheme } from "react-native";
7 |
8 | import { Colors } from "@/constants/Colors";
9 |
10 | export function useThemeColor(
11 | props: { light?: string; dark?: string },
12 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark,
13 | ) {
14 | const theme = useColorScheme() ?? "light";
15 | const colorFromProps = props[theme];
16 |
17 | if (colorFromProps) {
18 | return colorFromProps;
19 | } else {
20 | return Colors[theme][colorName];
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "complex-navigation-expo",
3 | "main": "expo-router/entry",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "start": "expo start",
7 | "reset-project": "node ./scripts/reset-project.js",
8 | "android": "expo start --android",
9 | "ios": "expo start --ios",
10 | "web": "expo start --web",
11 | "test": "jest --watchAll",
12 | "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
13 | "lint:fix": "eslint . --fix"
14 | },
15 | "jest": {
16 | "preset": "jest-expo"
17 | },
18 | "dependencies": {
19 | "@expo/vector-icons": "^14.0.0",
20 | "@react-navigation/drawer": "6.6.15",
21 | "@react-navigation/native": "^6.0.2",
22 | "@rneui/base": "4.0.0-rc.7",
23 | "@rneui/themed": "4.0.0-rc.8",
24 | "expo": "~51.0.14",
25 | "expo-constants": "~16.0.2",
26 | "expo-font": "~12.0.7",
27 | "expo-linking": "~6.3.1",
28 | "expo-router": "~3.5.16",
29 | "expo-splash-screen": "~0.27.5",
30 | "expo-status-bar": "~1.12.1",
31 | "expo-system-ui": "~3.0.6",
32 | "expo-web-browser": "~13.0.3",
33 | "react": "18.2.0",
34 | "react-dom": "18.2.0",
35 | "react-native": "0.74.2",
36 | "react-native-gesture-handler": "~2.16.1",
37 | "react-native-reanimated": "~3.10.1",
38 | "react-native-safe-area-context": "4.10.1",
39 | "react-native-screens": "3.31.1",
40 | "react-native-web": "~0.19.10"
41 | },
42 | "devDependencies": {
43 | "@babel/core": "^7.20.0",
44 | "@eslint/eslintrc": "3.1.0",
45 | "@eslint/js": "9.5.0",
46 | "@types/jest": "^29.5.12",
47 | "@types/react": "~18.2.45",
48 | "@types/react-test-renderer": "^18.0.7",
49 | "eslint-config-expo": "7.1.2",
50 | "eslint-config-prettier": "9.1.0",
51 | "eslint-plugin-prettier": "5.1.3",
52 | "jest": "^29.2.1",
53 | "jest-expo": "~51.0.1",
54 | "prettier": "3.3.2",
55 | "react-test-renderer": "18.2.0",
56 | "typescript": "~5.3.3"
57 | },
58 | "private": true
59 | }
60 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/scripts/reset-project.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * This script is used to reset the project to a blank state.
5 | * It moves the /app directory to /app-example and creates a new /app directory with an index.tsx and _layout.tsx file.
6 | * You can remove the `reset-project` script from package.json and safely delete this file after running it.
7 | */
8 |
9 | const fs = require("fs");
10 | const path = require("path");
11 |
12 | const root = process.cwd();
13 | const oldDirPath = path.join(root, "app");
14 | const newDirPath = path.join(root, "app-example");
15 | const newAppDirPath = path.join(root, "app");
16 |
17 | const indexContent = `import { Text, View } from "react-native";
18 |
19 | export default function Index() {
20 | return (
21 |
28 | Edit app/index.tsx to edit this screen.
29 |
30 | );
31 | }
32 | `;
33 |
34 | const layoutContent = `import { Stack } from "expo-router";
35 |
36 | export default function RootLayout() {
37 | return (
38 |
39 |
40 |
41 | );
42 | }
43 | `;
44 |
45 | fs.rename(oldDirPath, newDirPath, (error) => {
46 | if (error) {
47 | return console.error(`Error renaming directory: ${error}`);
48 | }
49 | console.log("/app moved to /app-example.");
50 |
51 | fs.mkdir(newAppDirPath, { recursive: true }, (error) => {
52 | if (error) {
53 | return console.error(`Error creating new app directory: ${error}`);
54 | }
55 | console.log("New /app directory created.");
56 |
57 | const indexPath = path.join(newAppDirPath, "index.tsx");
58 | fs.writeFile(indexPath, indexContent, (error) => {
59 | if (error) {
60 | return console.error(`Error creating index.tsx: ${error}`);
61 | }
62 | console.log("app/index.tsx created.");
63 |
64 | const layoutPath = path.join(newAppDirPath, "_layout.tsx");
65 | fs.writeFile(layoutPath, layoutContent, (error) => {
66 | if (error) {
67 | return console.error(`Error creating _layout.tsx: ${error}`);
68 | }
69 | console.log("app/_layout.tsx created.");
70 | });
71 | });
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/examples/2024-06-24-complex-navigation-expo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "paths": {
6 | "@/*": [
7 | "./*"
8 | ]
9 | }
10 | },
11 | "include": [
12 | "**/*.ts",
13 | "**/*.tsx",
14 | ".expo/types/**/*.ts",
15 | "expo-env.d.ts"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/examples/2024-07-04-master-higher-order-components/layout.jsx:
--------------------------------------------------------------------------------
1 | export function Layout({ children, showHeader = true }) {
2 | return (
3 |
4 | {showHeader && (
5 |
8 | )}
9 |
10 |
{children}
11 |
12 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/examples/2024-07-04-master-higher-order-components/with-layout.jsx:
--------------------------------------------------------------------------------
1 | import { Layout } from './layout';
2 |
3 | export default ({ showHeader = true } = {}) =>
4 | Component =>
5 | props => (
6 |
7 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/examples/2024-07-04-master-higher-order-components/with-layout.test.jsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import { describe, expect, test } from 'vitest';
3 |
4 | import withLayout from './with-layout';
5 |
6 | function MyComponent({ title = 'Hello' }) {
7 | return {title}
;
8 | }
9 |
10 | describe('withLayout', () => {
11 | test('given a component: returns the component with a default title', () => {
12 | const WrappedComponent = withLayout()(MyComponent);
13 |
14 | render();
15 |
16 | expect(screen.getByText('Hello')).toHaveTextContent('Hello');
17 | });
18 |
19 | test('given a component: renders the layout around the component', () => {
20 | const WrappedComponent = withLayout()(MyComponent);
21 |
22 | render();
23 |
24 | expect(screen.getByRole('heading')).toHaveTextContent(/some title/i);
25 | expect(screen.getByRole('main')).toContainElement(
26 | screen.getByText('Hello'),
27 | );
28 | expect(screen.getByRole('contentinfo')).toHaveTextContent(/some footer/i);
29 | });
30 |
31 | test('given props for the wrapped component: passes on the props to the wrapped component', () => {
32 | const WrappedComponent = withLayout()(MyComponent);
33 | const customTitle = 'Custom Title';
34 |
35 | render();
36 |
37 | expect(screen.getByText(customTitle)).toHaveTextContent(customTitle);
38 | });
39 |
40 | test('given used in composition with other HOCs: passes on the props of the other HOCs', () => {
41 | const compose =
42 | (...fns) =>
43 | x =>
44 | fns.reduceRight((y, f) => f(y), x);
45 | const withTitle = Component => props => (
46 |
47 | );
48 | const ComposedComponent = compose(withLayout(), withTitle)(MyComponent);
49 |
50 | render();
51 |
52 | expect(screen.getByText('foo')).toHaveTextContent('foo');
53 | });
54 |
55 | test('given a component and NOT rendering the header: does NOT render the header', () => {
56 | const WrappedComponent = withLayout({ showHeader: false })(MyComponent);
57 |
58 | render();
59 |
60 | expect(screen.queryByRole('heading')).toBeNull();
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/.github/workflows/pull-request.yml:
--------------------------------------------------------------------------------
1 | name: Pull Request
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | lint:
7 | name: ⬣ ESLint
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: ⬇️ Checkout repo
11 | uses: actions/checkout@v3
12 |
13 | - name: ⎔ Setup node
14 | uses: actions/setup-node@v3
15 | with:
16 | node-version: 20
17 |
18 | - name: 📥 Download deps
19 | uses: bahmutov/npm-install@v1
20 |
21 | - name: 🔬 Lint
22 | run: npm run lint
23 |
24 | type-check:
25 | name: ʦ TypeScript
26 | runs-on: ubuntu-latest
27 | steps:
28 | - name: ⬇️ Checkout repo
29 | uses: actions/checkout@v3
30 |
31 | - name: ⎔ Setup node
32 | uses: actions/setup-node@v3
33 | with:
34 | node-version: 20
35 |
36 | - name: 📥 Download deps
37 | uses: bahmutov/npm-install@v1
38 |
39 | - name: 🔎 Type check
40 | run: npm run type-check --if-present
41 |
42 | commitlint:
43 | name: ⚙️ commitlint
44 | runs-on: ubuntu-latest
45 |
46 | if: github.actor != 'dependabot[bot]'
47 | env:
48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49 |
50 | steps:
51 | - name: ⬇️ Checkout repo
52 | uses: actions/checkout@v3
53 | with:
54 | fetch-depth: 0
55 | - name: ⚙️ commitlint
56 | uses: wagoid/commitlint-github-action@v4
57 |
58 | vitest:
59 | name: ⚡ Vitest
60 | runs-on: ubuntu-latest
61 | steps:
62 | - name: ⬇️ Checkout repo
63 | uses: actions/checkout@v3
64 |
65 | - name: ⎔ Setup node
66 | uses: actions/setup-node@v3
67 | with:
68 | node-version: 20
69 |
70 | - name: 📥 Download deps
71 | uses: bahmutov/npm-install@v1
72 |
73 | - name: 🛠 Setup Database
74 | run: npm run prisma:wipe
75 | env:
76 | DATABASE_URL: ${{ secrets.DATABASE_URL }}
77 |
78 | - name: ⚡ Run vitest
79 | run: npm run test -- --coverage
80 | env:
81 | DATABASE_URL: ${{ secrets.DATABASE_URL }}
82 |
83 | playwright:
84 | name: 🎭 Playwright
85 | runs-on: ubuntu-latest
86 | steps:
87 | - name: ⬇️ Checkout repo
88 | uses: actions/checkout@v3
89 |
90 | - name: ⎔ Setup node
91 | uses: actions/setup-node@v3
92 | with:
93 | node-version: 20
94 |
95 | - name: 📥 Download deps
96 | uses: bahmutov/npm-install@v1
97 |
98 | - name: 🌐 Install Playwright Browsers
99 | run: npx playwright install --with-deps
100 |
101 | - name: 🛠 Setup Database
102 | run: npm run prisma:wipe
103 | env:
104 | DATABASE_URL: ${{ secrets.DATABASE_URL }}
105 |
106 | - name: 🎭 Playwright Run
107 | run: npx playwright test
108 | env:
109 | DATABASE_URL: ${{ secrets.DATABASE_URL }}
110 |
111 | - name: 📸 Playwright Screenshots
112 | uses: actions/upload-artifact@v4
113 | if: failure()
114 | with:
115 | name: playwright-report
116 | path: playwright-report/
117 | retention-days: 30
118 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 | /test-results/
38 | /playwright-report/
39 | /blob-report/
40 | /playwright/.cache/
41 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npm run lint && npm run type-check
5 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/.husky/prepare-commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | exec < /dev/tty && npx cz --hook || true
5 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with
2 | [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
3 |
4 | ## Getting Started
5 |
6 | First, run the development server:
7 |
8 | ```bash
9 | npm run dev
10 | # or
11 | yarn dev
12 | # or
13 | pnpm dev
14 | # or
15 | bun dev
16 | ```
17 |
18 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the
19 | result.
20 |
21 | You can start editing the page by modifying `app/page.tsx`. The page
22 | auto-updates as you edit the file.
23 |
24 | This project uses
25 | [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to
26 | automatically optimize and load Inter, a custom Google Font.
27 |
28 | ## Learn More
29 |
30 | To learn more about Next.js, take a look at the following resources:
31 |
32 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
33 | features and API.
34 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
35 |
36 | You can check out
37 | [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your
38 | feedback and contributions are welcome!
39 |
40 | ## Deploy on Vercel
41 |
42 | The easiest way to deploy your Next.js app is to use the
43 | [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme)
44 | from the creators of Next.js.
45 |
46 | Check out our
47 | [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more
48 | details.
49 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import { fileURLToPath } from 'node:url';
3 |
4 | import { fixupConfigRules } from '@eslint/compat';
5 | import { FlatCompat } from '@eslint/eslintrc';
6 | import js from '@eslint/js';
7 | import simpleImportSort from 'eslint-plugin-simple-import-sort';
8 |
9 | const __filename = fileURLToPath(import.meta.url);
10 | const __dirname = path.dirname(__filename);
11 | const compat = new FlatCompat({
12 | baseDirectory: __dirname,
13 | recommendedConfig: js.configs.recommended,
14 | allConfig: js.configs.all,
15 | });
16 |
17 | export default [
18 | ...fixupConfigRules(
19 | compat.extends(
20 | 'next/core-web-vitals',
21 | 'plugin:unicorn/recommended',
22 | 'plugin:import/recommended',
23 | 'plugin:playwright/recommended',
24 | 'plugin:prettier/recommended',
25 | ),
26 | ),
27 | {
28 | plugins: {
29 | 'simple-import-sort': simpleImportSort,
30 | },
31 | rules: {
32 | 'simple-import-sort/exports': 'error',
33 | 'simple-import-sort/imports': 'error',
34 | 'unicorn/no-array-callback-reference': 'off',
35 | 'unicorn/no-array-for-each': 'off',
36 | 'unicorn/no-array-reduce': 'off',
37 |
38 | 'unicorn/prevent-abbreviations': [
39 | 'error',
40 | {
41 | allowList: {
42 | e2e: true,
43 | },
44 | replacements: {
45 | props: false,
46 | ref: false,
47 | params: false,
48 | },
49 | },
50 | ],
51 | },
52 | },
53 | {
54 | files: ['**/*.js'],
55 |
56 | rules: {
57 | 'unicorn/prefer-module': 'off',
58 | },
59 | },
60 | ];
61 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "commitizen": {
4 | "path": "cz-conventional-changelog"
5 | }
6 | },
7 | "dependencies": {
8 | "@formatjs/intl-localematcher": "0.5.4",
9 | "@prisma/client": "5.17.0",
10 | "@radix-ui/react-icons": "1.3.0",
11 | "class-variance-authority": "0.7.0",
12 | "clsx": "2.1.1",
13 | "negotiator": "0.6.3",
14 | "next": "15.0.1",
15 | "prisma": "5.17.0",
16 | "react": "19.0.0-rc-69d4b800-20241021",
17 | "react-dom": "19.0.0-rc-69d4b800-20241021",
18 | "tailwind-merge": "2.4.0",
19 | "tailwindcss-animate": "1.0.7"
20 | },
21 | "devDependencies": {
22 | "@commitlint/cli": "19.3.0",
23 | "@commitlint/config-conventional": "19.2.2",
24 | "@eslint/compat": "1.1.1",
25 | "@playwright/test": "1.45.3",
26 | "@testing-library/dom": "10.3.2",
27 | "@testing-library/jest-dom": "6.4.6",
28 | "@testing-library/react": "16.0.0",
29 | "@testing-library/user-event": "14.5.2",
30 | "@types/negotiator": "0.6.3",
31 | "@types/node": "20.14.10",
32 | "@types/react": "npm:types-react@19.0.0-rc.1",
33 | "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
34 | "@vitejs/plugin-react": "4.3.1",
35 | "commitizen": "4.3.0",
36 | "cz-conventional-changelog": "3.3.0",
37 | "dotenv": "16.4.5",
38 | "eslint": "9.7.0",
39 | "eslint-config-next": "15.0.1",
40 | "eslint-config-prettier": "9.1.0",
41 | "eslint-plugin-import": "2.29.1",
42 | "eslint-plugin-playwright": "1.6.2",
43 | "eslint-plugin-prettier": "5.1.3",
44 | "eslint-plugin-simple-import-sort": "12.1.1",
45 | "eslint-plugin-unicorn": "54.0.0",
46 | "happy-dom": "14.12.3",
47 | "husky": "8.0.0",
48 | "postcss": "8.4.39",
49 | "prettier": "3.3.3",
50 | "prettier-plugin-tailwindcss": "0.6.5",
51 | "tailwindcss": "3.4.5",
52 | "tsx": "4.16.5",
53 | "typescript": "5.5.3",
54 | "vite-tsconfig-paths": "4.3.2",
55 | "vitest": "2.0.3"
56 | },
57 | "name": "reactsquad-production",
58 | "private": true,
59 | "scripts": {
60 | "build": "next build",
61 | "dev": "next dev --turbopack",
62 | "format": "prettier --write .",
63 | "lint": "npx eslint ./src",
64 | "lint:fix": "npm run lint -- --fix",
65 | "prepare": "husky install",
66 | "prisma:deploy": "npx prisma migrate deploy && npx prisma generate",
67 | "prisma:migrate": "npx prisma migrate dev --name",
68 | "prisma:push": "npx prisma db push && npx prisma generate",
69 | "prisma:reset-dev": "run-s prisma:wipe prisma:seed dev",
70 | "prisma:seed": "tsx ./prisma/seed.ts",
71 | "prisma:setup": "prisma generate && prisma migrate deploy && prisma db push",
72 | "prisma:studio": "npx prisma studio",
73 | "prisma:wipe": "npx prisma migrate reset --force && npx prisma db push",
74 | "start": "next start",
75 | "test": "vitest --reporter=verbose",
76 | "test:e2e": "npx playwright test",
77 | "test:e2e:ui": "npx playwright test --ui",
78 | "type-check": "tsc -b"
79 | },
80 | "version": "0.1.0",
81 | "overrides": {
82 | "@types/react": "npm:types-react@19.0.0-rc.1",
83 | "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from '@playwright/test';
2 |
3 | /**
4 | * Read environment variables from file.
5 | * https://github.com/motdotla/dotenv
6 | */
7 | // import dotenv from 'dotenv';
8 | // dotenv.config({ path: path.resolve(__dirname, '.env') });
9 |
10 | /**
11 | * See https://playwright.dev/docs/test-configuration.
12 | */
13 | export default defineConfig({
14 | testDir: './playwright',
15 | /* Run tests in files in parallel */
16 | fullyParallel: true,
17 | /* Fail the build on CI if you accidentally left test.only in the source code. */
18 | forbidOnly: !!process.env.CI,
19 | /* Retry on CI only */
20 | retries: process.env.CI ? 2 : 0,
21 | /* Opt out of parallel tests on CI. */
22 | workers: process.env.CI ? 1 : undefined,
23 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
24 | reporter: 'html',
25 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
26 | use: {
27 | /* Base URL to use in actions like `await page.goto('/')`. */
28 | // baseURL: 'http://127.0.0.1:3000',
29 |
30 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
31 | trace: 'on-first-retry',
32 | },
33 |
34 | /* Configure projects for major browsers */
35 | projects: [
36 | {
37 | name: 'chromium',
38 | use: { ...devices['Desktop Chrome'] },
39 | },
40 |
41 | {
42 | name: 'firefox',
43 | use: { ...devices['Desktop Firefox'] },
44 | },
45 |
46 | {
47 | name: 'webkit',
48 | use: { ...devices['Desktop Safari'] },
49 | },
50 |
51 | /* Test against mobile viewports. */
52 | // {
53 | // name: 'Mobile Chrome',
54 | // use: { ...devices['Pixel 5'] },
55 | // },
56 | // {
57 | // name: 'Mobile Safari',
58 | // use: { ...devices['iPhone 12'] },
59 | // },
60 |
61 | /* Test against branded browsers. */
62 | // {
63 | // name: 'Microsoft Edge',
64 | // use: { ...devices['Desktop Edge'], channel: 'msedge' },
65 | // },
66 | // {
67 | // name: 'Google Chrome',
68 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
69 | // },
70 | ],
71 |
72 | /* Run your local dev server before starting the tests */
73 | webServer: {
74 | command: process.env.CI ? 'npm run build && npm run start' : 'npm run dev',
75 | port: 3000,
76 | },
77 | });
78 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/playwright/example.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@playwright/test';
2 |
3 | test.describe('landing page', () => {
4 | test('given any user: shows the test user', async ({ page }) => {
5 | await page.goto('/');
6 |
7 | await expect(page.getByText('Jan Hesters')).toBeVisible();
8 | await expect(page.getByText('jan@reactsquad.io')).toBeVisible();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | arrowParens: 'avoid',
3 | bracketSameLine: false,
4 | bracketSpacing: true,
5 | htmlWhitespaceSensitivity: 'css',
6 | insertPragma: false,
7 | jsxSingleQuote: false,
8 | plugins: ['prettier-plugin-tailwindcss'],
9 | printWidth: 80,
10 | proseWrap: 'always',
11 | quoteProps: 'as-needed',
12 | requirePragma: false,
13 | semi: true,
14 | singleQuote: true,
15 | tabWidth: 2,
16 | trailingComma: 'all',
17 | useTabs: false,
18 | };
19 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "postgresql"
7 | // Uses connection pooling
8 | url = env("DATABASE_URL")
9 | }
10 |
11 | model UserProfile {
12 | id String @id @default(cuid())
13 | createdAt DateTime @default(now())
14 | updatedAt DateTime @updatedAt
15 | email String @unique
16 | name String @default("")
17 | acceptedTermsAndConditions Boolean @default(false)
18 | }
19 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { exit } from 'node:process';
2 |
3 | import { PrismaClient } from '@prisma/client';
4 | import dotenv from 'dotenv';
5 |
6 | dotenv.config({ path: '.env.local' });
7 |
8 | const prisma = new PrismaClient();
9 |
10 | const prettyPrint = (object: any) =>
11 | console.log(JSON.stringify(object, undefined, 2));
12 |
13 | async function seed() {
14 | const user = await prisma.userProfile.create({
15 | data: {
16 | email: 'jan@reactsquad.io',
17 | name: 'Jan Hesters',
18 | acceptedTermsAndConditions: true,
19 | },
20 | });
21 |
22 | console.log('========= 🌱 result of seed: =========');
23 | prettyPrint({ user });
24 | }
25 |
26 | seed()
27 | .then(async () => {
28 | await prisma.$disconnect();
29 | })
30 | // eslint-disable-next-line unicorn/prefer-top-level-await
31 | .catch(async error => {
32 | console.error(error);
33 | await prisma.$disconnect();
34 | exit(1);
35 | });
36 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/app/[lang]/layout.tsx:
--------------------------------------------------------------------------------
1 | import '../globals.css';
2 |
3 | import type { Metadata } from 'next';
4 | import { Inter } from 'next/font/google';
5 |
6 | import { Locale } from '@/features/internationalization/i18n-config';
7 |
8 | const inter = Inter({ subsets: ['latin'] });
9 |
10 | export const metadata: Metadata = {
11 | title: 'Create Next App',
12 | description: 'Generated by create next app',
13 | };
14 |
15 | export default function RootLayout({
16 | children,
17 | params,
18 | }: Readonly<{
19 | children: React.ReactNode;
20 | params: { lang: Locale };
21 | }>) {
22 | return (
23 |
24 | {children}
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/app/[lang]/page.tsx:
--------------------------------------------------------------------------------
1 | import { retrieveUserProfileFromDatabaseByEmail } from '@/features/user-profile/user-profile-model';
2 |
3 | export default async function Dashboard() {
4 | const user =
5 | await retrieveUserProfileFromDatabaseByEmail('jan@reactsquad.io');
6 |
7 | return (
8 |
9 |
User Profile
10 | {user ? (
11 |
12 | - Name: {user.name}
13 | - Email: {user.email}
14 |
15 | ) : (
16 |
User not found.
17 | )}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EarlyNode/tutorials/e78d678cd56f528932fedc0b664cda7f8874d228/examples/2024-07-16-set-up-next-js-production/src/app/favicon.ico
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 222.2 84% 4.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 222.2 84% 4.9%;
13 | --primary: 222.2 47.4% 11.2%;
14 | --primary-foreground: 210 40% 98%;
15 | --secondary: 210 40% 96.1%;
16 | --secondary-foreground: 222.2 47.4% 11.2%;
17 | --muted: 210 40% 96.1%;
18 | --muted-foreground: 215.4 16.3% 46.9%;
19 | --accent: 210 40% 96.1%;
20 | --accent-foreground: 222.2 47.4% 11.2%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 210 40% 98%;
23 | --border: 214.3 31.8% 91.4%;
24 | --input: 214.3 31.8% 91.4%;
25 | --ring: 222.2 84% 4.9%;
26 | --radius: 0.5rem;
27 | --chart-1: 12 76% 61%;
28 | --chart-2: 173 58% 39%;
29 | --chart-3: 197 37% 24%;
30 | --chart-4: 43 74% 66%;
31 | --chart-5: 27 87% 67%;
32 | }
33 |
34 | .dark {
35 | --background: 222.2 84% 4.9%;
36 | --foreground: 210 40% 98%;
37 | --card: 222.2 84% 4.9%;
38 | --card-foreground: 210 40% 98%;
39 | --popover: 222.2 84% 4.9%;
40 | --popover-foreground: 210 40% 98%;
41 | --primary: 210 40% 98%;
42 | --primary-foreground: 222.2 47.4% 11.2%;
43 | --secondary: 217.2 32.6% 17.5%;
44 | --secondary-foreground: 210 40% 98%;
45 | --muted: 217.2 32.6% 17.5%;
46 | --muted-foreground: 215 20.2% 65.1%;
47 | --accent: 217.2 32.6% 17.5%;
48 | --accent-foreground: 210 40% 98%;
49 | --destructive: 0 62.8% 30.6%;
50 | --destructive-foreground: 210 40% 98%;
51 | --border: 217.2 32.6% 17.5%;
52 | --input: 217.2 32.6% 17.5%;
53 | --ring: 212.7 26.8% 83.9%;
54 | --chart-1: 220 70% 50%;
55 | --chart-2: 160 60% 45%;
56 | --chart-3: 30 80% 55%;
57 | --chart-4: 280 65% 60%;
58 | --chart-5: 340 75% 55%;
59 | }
60 | }
61 |
62 | @layer base {
63 | * {
64 | @apply border-border;
65 | }
66 | body {
67 | @apply bg-background text-foreground;
68 | }
69 | }
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/app/login/counter-component.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState } from 'react';
4 |
5 | import type { getDictionary } from '@/features/internationalization/get-dictionaries';
6 |
7 | export function CounterComponent({
8 | dictionary,
9 | }: {
10 | dictionary: Awaited>['counter'];
11 | }) {
12 | const [count, setCount] = useState(0);
13 | return (
14 |
15 | This component is rendered on client:
16 |
19 | {count}
20 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/app/login/page.tsx:
--------------------------------------------------------------------------------
1 | import { getDictionary } from '@/features/internationalization/get-dictionaries';
2 | import { Locale } from '@/features/internationalization/i18n-config';
3 |
4 | import { CounterComponent } from './counter-component';
5 |
6 | export default async function IndexPage({
7 | params: { lang },
8 | }: {
9 | params: { lang: Locale };
10 | }) {
11 | const dictionary = await getDictionary(lang);
12 |
13 | return (
14 |
15 |
Current locale: {lang}
16 |
This text is rendered on the server: {dictionary.landing.welcome}
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/features/internationalization/dictionaries/en-US.json:
--------------------------------------------------------------------------------
1 | {
2 | "counter": {
3 | "decrement": "Decrement",
4 | "increment": "Increment"
5 | },
6 | "landing": {
7 | "welcome": "Welcome"
8 | }
9 | }
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/features/internationalization/get-dictionaries.ts:
--------------------------------------------------------------------------------
1 | import 'server-only';
2 |
3 | import type { Locale } from './i18n-config';
4 |
5 | // We enumerate all dictionaries here for better linting and typescript support
6 | // We also get the default import for cleaner types
7 | const dictionaries = {
8 | 'en-US': () =>
9 | import('./dictionaries/en-US.json').then(module => module.default),
10 | };
11 |
12 | export const getDictionary = async (locale: Locale) =>
13 | dictionaries[locale]?.() ?? dictionaries['en-US']();
14 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/features/internationalization/i18n-config.ts:
--------------------------------------------------------------------------------
1 | export const i18n = {
2 | defaultLocale: 'en-US',
3 | locales: ['en-US'],
4 | } as const;
5 |
6 | export type Locale = (typeof i18n)['locales'][number];
7 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/features/internationalization/localization-middleware.ts:
--------------------------------------------------------------------------------
1 | import { match } from '@formatjs/intl-localematcher';
2 | import Negotiator from 'negotiator';
3 | import { type NextRequest, NextResponse } from 'next/server';
4 |
5 | import { i18n } from './i18n-config';
6 |
7 | function getLocale(request: NextRequest) {
8 | const headers = {
9 | 'accept-language': request.headers.get('accept-language') ?? '',
10 | };
11 | const languages = new Negotiator({ headers }).languages();
12 | return match(languages, i18n.locales, i18n.defaultLocale);
13 | }
14 |
15 | export function localizationMiddleware(request: NextRequest) {
16 | const { pathname } = request.nextUrl;
17 | const pathnameHasLocale = i18n.locales.some(
18 | locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
19 | );
20 |
21 | // // `/_next/` and `/api/` are ignored by the watcher, but we need to
22 | // ignore files in `public` manually.
23 | if (
24 | pathnameHasLocale ||
25 | [
26 | '/manifest.json',
27 | '/favicon.ico',
28 | // Your other files in `public`.
29 | ].includes(pathname)
30 | ) {
31 | return;
32 | }
33 |
34 | const locale = getLocale(request);
35 | request.nextUrl.pathname = `/${locale}${pathname}`;
36 | return NextResponse.redirect(request.nextUrl);
37 | }
38 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/features/internationalization/use-switch-locale-href.ts:
--------------------------------------------------------------------------------
1 | import { usePathname } from 'next/navigation';
2 |
3 | import { Locale } from './i18n-config';
4 |
5 | export function useSwitchLocaleHref() {
6 | const pathName = usePathname();
7 |
8 | const getSwitchLocaleHref = (locale: Locale) => {
9 | if (!pathName) return '/';
10 | const segments = pathName.split('/');
11 | segments[1] = locale;
12 | return segments.join('/');
13 | };
14 |
15 | return getSwitchLocaleHref;
16 | }
17 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/features/user-authentication/example.test.tsx:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest';
2 |
3 | import { render, screen } from '@/tests/react-test-utils';
4 |
5 | function MyReactComponent() {
6 | return My React Component
;
7 | }
8 |
9 | describe('MyReactComponent', () => {
10 | test('given no props: renders a text', () => {
11 | render();
12 |
13 | expect(screen.getByText('My React Component')).toBeInTheDocument();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/features/user-profile/user-profile-model.ts:
--------------------------------------------------------------------------------
1 | import { UserProfile } from '@prisma/client';
2 |
3 | import prisma from '@/lib/prisma';
4 |
5 | export type PartialUserProfileParameters = Pick<
6 | Parameters[0]['data'],
7 | 'acceptedTermsAndConditions' | 'email' | 'id' | 'name'
8 | >;
9 |
10 | // CREATE
11 |
12 | /**
13 | * Saves a new user profile to the database.
14 | *
15 | * @param user profile - Parameters of the user profile that should be created.
16 | * @returns The newly created user profile.
17 | */
18 | export async function saveUserProfileToDatabase(
19 | userProfile: PartialUserProfileParameters,
20 | ) {
21 | return prisma.userProfile.create({ data: userProfile });
22 | }
23 |
24 | // READ
25 |
26 | /**
27 | * Returns the first user profile that exists in the database with the given
28 | * email.
29 | *
30 | * @param email - The email of the user profile to retrieve.
31 | * @returns The user profile with the given email, or null if it wasn't found.
32 | */
33 | export async function retrieveUserProfileFromDatabaseByEmail(
34 | email: UserProfile['email'],
35 | ) {
36 | return await prisma.userProfile.findUnique({ where: { email } });
37 | }
38 |
39 | // DELETE
40 |
41 | /**
42 | * Removes a user profile from the database.
43 | *
44 | * @param id - The id of the user profile you want to delete.
45 | * @returns The user profile that was deleted.
46 | */
47 | export async function deleteUserProfileFromDatabaseById(id: UserProfile['id']) {
48 | return prisma.userProfile.delete({ where: { id } });
49 | }
50 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/lib/prisma.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client';
2 |
3 | declare global {
4 | var __database__: PrismaClient;
5 | }
6 |
7 | let prisma: PrismaClient;
8 |
9 | if (process.env.NODE_ENV === 'production') {
10 | prisma = new PrismaClient();
11 | } else {
12 | if (!global.__database__) {
13 | global.__database__ = new PrismaClient();
14 | }
15 | prisma = global.__database__;
16 | }
17 |
18 | export default prisma;
19 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest } from 'next/server';
2 |
3 | import { localizationMiddleware } from './features/internationalization/localization-middleware';
4 |
5 | // Matcher ignoring `/_next/` and `/api/` and svg files.
6 | export const config = { matcher: ['/((?!api|_next|.*.svg$).*)'] };
7 |
8 | export function middleware(request: NextRequest) {
9 | return localizationMiddleware(request);
10 | }
11 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/tests/react-test-utils.tsx:
--------------------------------------------------------------------------------
1 | import type { RenderOptions } from '@testing-library/react';
2 | import { render } from '@testing-library/react';
3 | import type { ReactElement } from 'react';
4 |
5 | const customRender = (
6 | ui: ReactElement,
7 | options?: Omit,
8 | ) =>
9 | render(ui, {
10 | wrapper: ({ children }) => <>{children}>,
11 | ...options,
12 | });
13 |
14 | // re-export everything
15 | export * from '@testing-library/react';
16 |
17 | // override render method
18 | export { customRender as render };
19 | export { default as userEvent } from '@testing-library/user-event';
20 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/src/tests/setup-test-environment.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/vitest';
2 |
3 | // See https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#configuring-your-testing-environment.
4 | // @ts-ignore
5 | globalThis.IS_REACT_ACT_ENVIRONMENT = true;
6 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | },
71 | animation: {
72 | "accordion-down": "accordion-down 0.2s ease-out",
73 | "accordion-up": "accordion-up 0.2s ease-out",
74 | },
75 | },
76 | },
77 | plugins: [require("tailwindcss-animate")],
78 | } satisfies Config
79 |
80 | export default config
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/examples/2024-07-16-set-up-next-js-production/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import react from '@vitejs/plugin-react';
2 | import tsconfigPaths from 'vite-tsconfig-paths';
3 | import { defineConfig } from 'vitest/config';
4 |
5 | export default defineConfig({
6 | plugins: [react(), tsconfigPaths()],
7 | server: {
8 | port: 3000,
9 | },
10 | test: {
11 | environment: 'happy-dom',
12 | globals: true,
13 | setupFiles: ['./src/tests/setup-test-environment.ts'],
14 | include: ['./src/**/*.{spec,test}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
15 | // watch: {
16 | // ignored:[
17 | // '.*\\/node_modules\\/.*',
18 | // '.*\\/build\\/.*',
19 | // '.*\\/postgres-data\\/.*',
20 | // ],
21 | // },
22 | coverage: {
23 | reporter: ['text', 'json', 'html'],
24 | },
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with
2 | [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
3 |
4 | ## Getting Started
5 |
6 | First, run the development server:
7 |
8 | ```bash
9 | npm run dev
10 | # or
11 | yarn dev
12 | # or
13 | pnpm dev
14 | # or
15 | bun dev
16 | ```
17 |
18 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the
19 | result.
20 |
21 | You can start editing the page by modifying `app/page.js`. The page auto-updates
22 | as you edit the file.
23 |
24 | This project uses
25 | [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to
26 | automatically optimize and load Inter, a custom Google Font.
27 |
28 | ## Learn More
29 |
30 | To learn more about Next.js, take a look at the following resources:
31 |
32 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
33 | features and API.
34 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
35 |
36 | You can check out
37 | [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your
38 | feedback and contributions are welcome!
39 |
40 | ## Deploy on Vercel
41 |
42 | The easiest way to deploy your Next.js app is to use the
43 | [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme)
44 | from the creators of Next.js.
45 |
46 | Check out our
47 | [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more
48 | details.
49 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import { fileURLToPath } from 'node:url';
3 |
4 | import { fixupConfigRules } from '@eslint/compat';
5 | import { FlatCompat } from '@eslint/eslintrc';
6 | import js from '@eslint/js';
7 | import simpleImportSort from 'eslint-plugin-simple-import-sort';
8 |
9 | const __filename = fileURLToPath(import.meta.url);
10 | const __dirname = path.dirname(__filename);
11 | const compat = new FlatCompat({
12 | baseDirectory: __dirname,
13 | recommendedConfig: js.configs.recommended,
14 | allConfig: js.configs.all,
15 | });
16 |
17 | export default [
18 | ...fixupConfigRules(
19 | compat.extends(
20 | 'next/core-web-vitals',
21 | 'plugin:unicorn/recommended',
22 | 'plugin:import/recommended',
23 | 'plugin:playwright/recommended',
24 | 'plugin:prettier/recommended',
25 | ),
26 | ),
27 | {
28 | plugins: {
29 | 'simple-import-sort': simpleImportSort,
30 | },
31 | rules: {
32 | 'simple-import-sort/exports': 'error',
33 | 'simple-import-sort/imports': 'error',
34 | 'unicorn/no-array-callback-reference': 'off',
35 | 'unicorn/no-array-for-each': 'off',
36 | 'unicorn/no-array-reduce': 'off',
37 |
38 | 'unicorn/prevent-abbreviations': [
39 | 'error',
40 | {
41 | allowList: {
42 | e2e: true,
43 | },
44 | replacements: {
45 | props: false,
46 | ref: false,
47 | params: false,
48 | },
49 | },
50 | ],
51 | },
52 | },
53 | {
54 | files: ['**/*.js'],
55 |
56 | rules: {
57 | 'unicorn/prefer-module': 'off',
58 | },
59 | },
60 | ];
61 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "paths": {
4 | "@/*": ["./src/*"]
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "next": "14.2.5",
4 | "ramda": "0.30.1",
5 | "react": "^18",
6 | "react-dom": "^18",
7 | "react-redux": "9.1.2",
8 | "redux": "5.0.1",
9 | "redux-logger": "3.0.6",
10 | "redux-thunk": "3.1.0"
11 | },
12 | "devDependencies": {
13 | "@eslint/compat": "1.1.1",
14 | "eslint": "8.57.0",
15 | "eslint-config-next": "14.2.5",
16 | "eslint-config-prettier": "9.1.0",
17 | "eslint-plugin-import": "2.29.1",
18 | "eslint-plugin-playwright": "1.6.2",
19 | "eslint-plugin-prettier": "5.2.1",
20 | "eslint-plugin-simple-import-sort": "12.1.1",
21 | "eslint-plugin-unicorn": "55.0.0",
22 | "postcss": "^8",
23 | "prettier": "3.3.3",
24 | "prettier-plugin-tailwindcss": "0.6.5",
25 | "tailwindcss": "^3.4.1"
26 | },
27 | "name": "2024-08-09-redux-basics",
28 | "private": true,
29 | "scripts": {
30 | "build": "next build",
31 | "dev": "next dev",
32 | "format": "prettier --write .",
33 | "lint": "npx eslint ./src",
34 | "lint:fix": "npm run lint -- --fix",
35 | "start": "next start"
36 | },
37 | "version": "0.1.0"
38 | }
39 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | arrowParens: 'avoid',
3 | bracketSameLine: false,
4 | bracketSpacing: true,
5 | htmlWhitespaceSensitivity: 'css',
6 | insertPragma: false,
7 | jsxSingleQuote: false,
8 | plugins: ['prettier-plugin-tailwindcss'],
9 | printWidth: 80,
10 | proseWrap: 'always',
11 | quoteProps: 'as-needed',
12 | requirePragma: false,
13 | semi: true,
14 | singleQuote: true,
15 | tabWidth: 2,
16 | trailingComma: 'all',
17 | useTabs: false,
18 | };
19 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/src/app/example-reducer.js:
--------------------------------------------------------------------------------
1 | export const increment = () => ({ type: 'INCREMENT' });
2 | export const incrementBy = payload => ({ type: 'INCREMENT_BY', payload });
3 | export const reset = () => ({ type: 'RESET' });
4 |
5 | export const slice = 'example';
6 | const initialState = { count: 0 };
7 |
8 | export const reducer = (state = initialState, { type, payload } = {}) => {
9 | switch (type) {
10 | case increment().type: {
11 | return { ...state, count: state.count + 1 };
12 | }
13 | case incrementBy().type: {
14 | return { ...state, count: state.count + payload };
15 | }
16 | case reset().type: {
17 | return initialState;
18 | }
19 | default: {
20 | return state;
21 | }
22 | }
23 | };
24 |
25 | export const selectCount = state => state[slice].count;
26 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EarlyNode/tutorials/e78d678cd56f528932fedc0b664cda7f8874d228/examples/2024-08-09-redux-basics/src/app/favicon.ico
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | @media (prefers-color-scheme: dark) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | }
18 |
19 | body {
20 | color: rgb(var(--foreground-rgb));
21 | background: linear-gradient(
22 | to bottom,
23 | transparent,
24 | rgb(var(--background-end-rgb))
25 | )
26 | rgb(var(--background-start-rgb));
27 | }
28 |
29 | @layer utilities {
30 | .text-balance {
31 | text-wrap: balance;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/src/app/hooks.js:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector, useStore } from 'react-redux';
2 |
3 | export const useAppDispatch = useDispatch.withTypes();
4 | export const useAppSelector = useSelector.withTypes();
5 | export const useAppStore = useStore.withTypes();
6 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/src/app/layout.js:
--------------------------------------------------------------------------------
1 | import './globals.css';
2 |
3 | import { Inter } from 'next/font/google';
4 |
5 | import { StoreProvider } from './store-provider';
6 |
7 | const inter = Inter({ subsets: ['latin'] });
8 |
9 | export const metadata = {
10 | title: 'Create Next App',
11 | description: 'Generated by create next app',
12 | };
13 |
14 | export default function RootLayout({ children }) {
15 | return (
16 |
17 |
18 | {children}
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/src/app/page-2.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { connect } from 'react-redux';
4 |
5 | import { increment, selectCount } from './example-reducer';
6 |
7 | function Home({ count, increment }) {
8 | return (
9 |
10 | Redux Basics
11 |
12 |
13 |
Count: {count}
14 |
15 |
23 |
24 |
25 | );
26 | }
27 |
28 | const mapStateToProps = state => ({ count: selectCount(state) });
29 |
30 | const mapDispatchToProps = { increment };
31 |
32 | export default connect(mapStateToProps, mapDispatchToProps)(Home);
33 |
34 | const mergeProps = (stateProps, dispatchProps, ownProps) => ({
35 | ...stateProps, // from mapStateToProps
36 | ...dispatchProps, // from mapDispatchToProps
37 | ...ownProps, // passed in to the component wrapped by connect from its parent
38 | });
39 |
40 | connect(mapStateToProps, mapDispatchToProps, mergeProps);
41 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/src/app/page-3.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | import { increment, incrementBy, init, selectCount } from './example-reducer';
4 | import { useAppDispatch, useAppSelector } from './hooks';
5 |
6 | async function fetchUser(id) {
7 | const response = await fetch(
8 | `https://jsonplaceholder.typicode.com/users/${id}`,
9 | );
10 | return await response.json();
11 | }
12 |
13 | export function MyComponent() {
14 | const dispatch = useAppDispatch();
15 | const currentCount = useAppSelector(selectCount);
16 | const hasFetched = useRef(false);
17 |
18 | useEffect(() => {
19 | dispatch(init());
20 | dispatch(increment());
21 | }, [dispatch]);
22 |
23 | useEffect(() => {
24 | const fetchAndIncrement = async () => {
25 | try {
26 | const user = await fetchUser(currentCount);
27 | dispatch(incrementBy(user.name.length));
28 | hasFetched.current = true;
29 | } catch (error) {
30 | console.error('Failed to fetch user:', error);
31 | }
32 | };
33 |
34 | if (currentCount === 1 && !hasFetched.current) {
35 | fetchAndIncrement();
36 | }
37 | }, [currentCount, dispatch]);
38 |
39 | return Current Count: {currentCount}
;
40 | }
41 |
42 | export const selectCurrentUserId = state => state[slice].currentUserId;
43 |
44 | export const selectUsers = state => state[slice].users;
45 |
46 | export const selectCurrentUser = state =>
47 | state[slice].users[state[slice].currentUserId];
48 |
49 | export const selectCurrentUsersEmail = state =>
50 | state[slice].users[state[slice].currentUserId]?.email ?? '';
51 |
52 | export const selectCurrentUsersFullName = state =>
53 | `${state[slice].users[state[slice].currentUserId]?.firstName ?? ''} ${state[slice].users[state[slice].currentUserId]?.lastName ?? ''}`;
54 |
55 | export const selectIsLoggedIn = state => Boolean(state[slice].currentUserId);
56 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/src/app/page.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { increment, selectCount } from './example-reducer';
4 | import { useAppDispatch, useAppSelector } from './hooks';
5 | import UserProfileContainer from './user-profile-container';
6 |
7 | export default function Home() {
8 | const count = useAppSelector(selectCount);
9 | const dispatch = useAppDispatch();
10 |
11 | return (
12 |
13 | Redux Basics
14 |
15 |
16 |
Count: {count}
17 |
18 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/src/app/store-2.js:
--------------------------------------------------------------------------------
1 | import { combineReducers, legacy_createStore as createStore } from 'redux';
2 |
3 | import {
4 | incrementBy,
5 | reducer as exampleReducer,
6 | slice as exampleSlice,
7 | } from './example-reducer';
8 | import {
9 | fetchedUsers,
10 | loginSuccess,
11 | reducer as userProfileReducer,
12 | slice as userProfileSlice,
13 | } from './user-profile';
14 |
15 | const rootReducer = combineReducers({
16 | [exampleSlice]: exampleReducer,
17 | [userProfileSlice]: userProfileReducer,
18 | });
19 |
20 | export const makeStore = () => {
21 | return createStore(rootReducer, rootReducer());
22 | };
23 |
24 | const store = makeStore();
25 |
26 | store.dispatch(incrementBy(10));
27 | store.dispatch(
28 | loginSuccess({
29 | id: 'user123',
30 | email: 'johndoe@example.com',
31 | firstName: 'John',
32 | lastName: 'Doe',
33 | }),
34 | );
35 | store.dispatch(
36 | fetchedUsers([
37 | {
38 | id: 'user123',
39 | email: 'johndoe@example.com',
40 | firstName: 'John',
41 | lastName: 'Doe',
42 | },
43 | {
44 | id: 'user456',
45 | email: 'janesmith@example.com',
46 | firstName: 'Jane',
47 | lastName: 'Smith',
48 | },
49 | ]),
50 | );
51 |
52 | console.log('state', store.getState());
53 | // state {
54 | // "example": {
55 | // "count": 10
56 | // },
57 | // "userProfile": {
58 | // "currentUserId": "user123",
59 | // "users": {
60 | // "user123": {
61 | // "id": "user123",
62 | // "email": "johndoe@example.com",
63 | // "firstName": "John",
64 | // "lastName": "Doe",
65 | // },
66 | // "user456": {
67 | // "id": "user456",
68 | // "email": "janesmith@example.com",
69 | // "firstName": "Jane",
70 | // "lastName": "Smith",
71 | // }
72 | // }
73 | // }
74 | // }
75 |
76 | const selectCurrentUser = state =>
77 | state.userProfile.users[state.userProfile.currentUserId]?.email;
78 |
79 | const currentUserEmail = selectCurrentUser(store.getState());
80 | console.log('currentUserEmail', currentUserEmail); // johndoe@example.com
81 |
82 | const selectUserFullNameById = (state, userId) => {
83 | const user = state.userProfile.users[userId];
84 | return user ? `${user.firstName} ${user.lastName}` : 'User not found';
85 | };
86 |
87 | const user456FullName = selectUserFullNameById(store.getState(), 'user456');
88 | console.log('user456FullName', user456FullName); // Jane Smith
89 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/src/app/store-provider.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useRef } from 'react';
3 | import { Provider } from 'react-redux';
4 |
5 | import { makeStore } from './store';
6 |
7 | export function StoreProvider({ children }) {
8 | const storeRef = useRef();
9 |
10 | if (!storeRef.current) {
11 | storeRef.current = makeStore();
12 | }
13 |
14 | return {children};
15 | }
16 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/src/app/store.js:
--------------------------------------------------------------------------------
1 | import {
2 | applyMiddleware,
3 | combineReducers,
4 | legacy_createStore as createStore,
5 | } from 'redux';
6 | import logger from 'redux-logger';
7 | import { thunk } from 'redux-thunk';
8 |
9 | import {
10 | reducer as exampleReducer,
11 | slice as exampleSlice,
12 | } from './example-reducer';
13 | import {
14 | reducer as userProfileReducer,
15 | slice as userProfileSlice,
16 | } from './user-profile';
17 |
18 | const rootReducer = combineReducers({
19 | [exampleSlice]: exampleReducer,
20 | [userProfileSlice]: userProfileReducer,
21 | });
22 |
23 | export const makeStore = () => {
24 | return createStore(
25 | rootReducer,
26 | rootReducer(),
27 | applyMiddleware(thunk, logger),
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/src/app/user-profile-component.js:
--------------------------------------------------------------------------------
1 | export const UserProfileComponent = ({ isLoggedIn, email, onLoginClicked }) => (
2 |
3 | {isLoggedIn ? (
4 |
Email: {email}
5 | ) : (
6 |
14 | )}
15 |
16 | );
17 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/src/app/user-profile-container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import {
4 | getCurrentUsersEmail,
5 | loginSuccess,
6 | selectIsLoggedIn,
7 | } from './user-profile';
8 | import { UserProfileComponent } from './user-profile-component';
9 |
10 | const mapStateToProps = state => ({
11 | email: getCurrentUsersEmail(state),
12 | isLoggedIn: selectIsLoggedIn(state),
13 | });
14 |
15 | const mapDispatchToProps = { onLoginClicked: loginSuccess };
16 |
17 | export default connect(
18 | mapStateToProps,
19 | mapDispatchToProps,
20 | )(UserProfileComponent);
21 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/src/app/user-profile.js:
--------------------------------------------------------------------------------
1 | import { converge, pipe, prop, propOr } from 'ramda';
2 |
3 | export const loginSuccess = payload => ({ type: 'LOGIN_SUCCESS', payload });
4 | export const fetchedUsers = payload => ({ type: 'FETCHED_USERS', payload });
5 |
6 | export const slice = 'userProfile';
7 | const initialState = { currentUserId: null, users: {} };
8 |
9 | export const reducer = (state = initialState, { type, payload } = {}) => {
10 | switch (type) {
11 | case loginSuccess().type: {
12 | return {
13 | ...state,
14 | currentUserId: payload.id,
15 | users: { ...state.users, [payload.id]: payload },
16 | };
17 | }
18 | case fetchedUsers().type: {
19 | const newUsers = { ...state.users };
20 |
21 | payload.forEach(user => {
22 | newUsers[user.id] = user;
23 | });
24 |
25 | return { ...state, users: newUsers };
26 | }
27 | default: {
28 | return state;
29 | }
30 | }
31 | };
32 |
33 | const selectUserProfileSlice = prop(slice);
34 |
35 | const selectCurrentUserId = pipe(selectUserProfileSlice, prop('currentUserId'));
36 |
37 | const selectUsers = pipe(selectUserProfileSlice, prop('users'));
38 |
39 | export const selectCurrentUser = converge(prop, [
40 | selectCurrentUserId,
41 | selectUsers,
42 | ]);
43 |
44 | export const getCurrentUsersEmail = pipe(
45 | selectCurrentUser,
46 | propOr('', 'email'),
47 | );
48 |
49 | export const selectIsLoggedIn = pipe(selectCurrentUserId, Boolean);
50 |
51 | export const selectCurrentUsersFullName = pipe(
52 | selectCurrentUser,
53 | converge(
54 | (firstName, lastName) => `${firstName} ${lastName}`,
55 | [propOr('', 'firstName'), propOr('', 'lastName')],
56 | ),
57 | );
58 |
--------------------------------------------------------------------------------
/examples/2024-08-09-redux-basics/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
5 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
6 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
7 | ],
8 | theme: {
9 | extend: {
10 | backgroundImage: {
11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
12 | 'gradient-conic':
13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
14 | },
15 | },
16 | },
17 | plugins: [],
18 | };
19 |
--------------------------------------------------------------------------------
/examples/2024-08-17-memoization/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 |
--------------------------------------------------------------------------------
/examples/2024-08-17-memoization/example-1.js:
--------------------------------------------------------------------------------
1 | function memoize(fn) {
2 | const cache = new Map();
3 |
4 | return function(...args) {
5 | const key = JSON.stringify(args);
6 |
7 | if (cache.has(key)) {
8 | return cache.get(key);
9 | }
10 |
11 | const result = fn.apply(this, args);
12 | cache.set(key, result);
13 |
14 | return result;
15 | };
16 | }
17 |
18 | function add(a, b) {
19 | return a + b;
20 | }
21 |
22 | const memoizedAdd = memoize(add);
23 |
24 | console.log(memoizedAdd(2, 3)); // Calculates and caches result.
25 | console.log(memoizedAdd(2, 3)); // Returns cached result.
26 |
--------------------------------------------------------------------------------
/examples/2024-08-17-memoization/example-2.js:
--------------------------------------------------------------------------------
1 | function memoize(fn) {
2 | const cache = new Map();
3 |
4 | function memoizedFunction(...args) {
5 | const key = JSON.stringify(args);
6 |
7 | if (cache.has(key)) {
8 | return cache.get(key);
9 | }
10 |
11 | const result = fn.apply(this, args);
12 | cache.set(key, result);
13 |
14 | return result;
15 | }
16 |
17 | Object.defineProperty(memoizedFunction, 'name', {
18 | value: `memoized_${fn.name}`,
19 | configurable: true
20 | });
21 |
22 | return memoizedFunction;
23 | }
24 |
25 | function fibonacci(n) {
26 | if (n <= 1) {
27 | return n;
28 | }
29 |
30 | return fibonacci(n - 1) + fibonacci(n - 2);
31 | }
32 |
33 | const memoizedFibonacci = memoize(fibonacci);
34 |
35 | function measurePerformance(func, arg) {
36 | const startTime = process.hrtime.bigint();
37 | const result = func(arg);
38 | const endTime = process.hrtime.bigint();
39 | // Convert nanoseconds to milliseconds.
40 | const duration = (endTime - startTime) / BigInt(1000000);
41 | console.log(`${func.name}(${arg}) = ${result}, Time: ${duration}ms`);
42 | }
43 |
44 | const n = 42;
45 |
46 | console.log("Starting performance measurement:");
47 | measurePerformance(fibonacci, n);
48 | measurePerformance(memoizedFibonacci, n);
49 | // Second call to show caching effect.
50 | measurePerformance(memoizedFibonacci, n);
51 |
--------------------------------------------------------------------------------
/examples/2024-08-17-memoization/example-3.js:
--------------------------------------------------------------------------------
1 | function memoize(fn) {
2 | const cache = new Map();
3 |
4 | function memoizedFunction(...args) {
5 | const key = JSON.stringify(args);
6 |
7 | if (cache.has(key)) {
8 | return cache.get(key);
9 | }
10 |
11 | const result = fn.apply(this, args);
12 | cache.set(key, result);
13 |
14 | return result;
15 | }
16 |
17 | memoizedFunction.clear = function clear() {
18 | cache.clear();
19 | };
20 |
21 | Object.defineProperty(memoizedFunction, 'name', {
22 | value: `memoized_${fn.name}`,
23 | configurable: true
24 | });
25 |
26 | return memoizedFunction;
27 | }
28 |
29 | function fibonacci(n) {
30 | if (n <= 1) {
31 | return n;
32 | }
33 |
34 | return fibonacci(n - 1) + fibonacci(n - 2);
35 | }
36 |
37 | const memoizedFibonacci = memoize(fibonacci);
38 |
39 | function measurePerformance(func, arg) {
40 | const startTime = process.hrtime.bigint();
41 | const result = func(arg);
42 | const endTime = process.hrtime.bigint();
43 | // Convert nanoseconds to milliseconds.
44 | const duration = (endTime - startTime) / BigInt(1000000);
45 | console.log(`${func.name}(${arg}) = ${result}, Time: ${duration}ms`);
46 | }
47 |
48 | const n = 42;
49 |
50 | // Measure memoized Fibonacci.
51 | measurePerformance(memoizedFibonacci, n);
52 |
53 | // Measure memoized Fibonacci second call.
54 | measurePerformance(memoizedFibonacci, n);
55 |
56 | // Clear cache and measure again.
57 | console.log('Clearing cache and measuring again:');
58 | memoizedFibonacci.clear();
59 | measurePerformance(memoizedFibonacci, n);
60 |
--------------------------------------------------------------------------------
/examples/2024-08-17-memoization/example-4.ts:
--------------------------------------------------------------------------------
1 | type AnyFunction = (...args: any[]) => any;
2 |
3 | interface MemoizedFunction extends CallableFunction {
4 | (...args: Parameters): ReturnType;
5 | clear: () => void;
6 | }
7 |
8 | function memoize(fn: T): MemoizedFunction {
9 | const cache = new Map>();
10 |
11 | const memoizedFunction = function(...args: Parameters): ReturnType {
12 | const key = JSON.stringify(args);
13 |
14 | if (cache.has(key)) {
15 | return cache.get(key)!;
16 | }
17 |
18 | const result = fn(...args);
19 | cache.set(key, result);
20 |
21 | return result;
22 | } as MemoizedFunction;
23 |
24 | memoizedFunction.clear = function clear() {
25 | cache.clear();
26 | };
27 |
28 | Object.defineProperty(memoizedFunction, 'name', {
29 | value: `memoized_${fn.name}`,
30 | configurable: true
31 | });
32 |
33 | return memoizedFunction;
34 | }
35 |
36 | // Example of using the memoized function with TypeScript.
37 | function fibonacci(n: number): number {
38 | if (n <= 1) {
39 | return n;
40 | }
41 |
42 | return fibonacci(n - 1) + fibonacci(n - 2);
43 | }
44 |
45 | const memoizedFibonacci = memoize(fibonacci);
46 |
47 | function measurePerformance(func: MemoizedFunction, arg: number) {
48 | const startTime = process.hrtime.bigint();
49 | const result = func(arg);
50 | const endTime = process.hrtime.bigint();
51 | // Convert nanoseconds to milliseconds.
52 | const duration = (endTime - startTime) / BigInt(1000000);
53 | console.log(`${func.name}(${arg}) = ${result}, Time: ${duration}ms`);
54 | }
55 |
56 | const n = 42;
57 |
58 | // Measure memoized Fibonacci.
59 | measurePerformance(memoizedFibonacci, n);
60 |
61 | // Measure memoized Fibonacci second call.
62 | measurePerformance(memoizedFibonacci, n);
63 |
64 | // Clear cache and measure again.
65 | memoizedFibonacci.clear();
66 | measurePerformance(memoizedFibonacci, n);
67 |
--------------------------------------------------------------------------------
/examples/2024-08-17-memoization/example-5.ts:
--------------------------------------------------------------------------------
1 | type AnyFunction = (...args: any[]) => any;
2 |
3 | interface MemoizedFunction extends CallableFunction {
4 | (...args: Parameters): ReturnType;
5 | clear: () => void;
6 | }
7 |
8 | function memoize(fn: T): MemoizedFunction {
9 | const cache = new Map>();
10 |
11 | const memoizedFunction = function(...args: Parameters): ReturnType {
12 | const key = JSON.stringify(args);
13 |
14 | if (cache.has(key)) {
15 | return cache.get(key)!;
16 | }
17 |
18 | const result = fn(...args);
19 | cache.set(key, result);
20 |
21 | return result;
22 | } as MemoizedFunction;
23 |
24 | memoizedFunction.clear = function clear() {
25 | cache.clear();
26 | };
27 |
28 | Object.defineProperty(memoizedFunction, 'name', {
29 | value: `memoized_${fn.name}`,
30 | configurable: true
31 | });
32 |
33 | return memoizedFunction;
34 | }
35 |
36 | function fibonacci(n: number): number {
37 | if (n <= 1) {
38 | return n;
39 | }
40 |
41 | return memoizedFibonacci(n - 1) + memoizedFibonacci(n - 2);
42 | }
43 |
44 | const memoizedFibonacci = memoize(fibonacci);
45 |
46 | function measurePerformance(func: MemoizedFunction, arg: number) {
47 | const startTime = process.hrtime.bigint();
48 | const result = func(arg);
49 | const endTime = process.hrtime.bigint();
50 | // Convert nanoseconds to milliseconds.
51 | const duration = (endTime - startTime) / BigInt(1000000);
52 | console.log(`${func.name}(${arg}) = ${result}, Time: ${duration}ms`);
53 | }
54 |
55 | const numbers = [10, 20, 30, 40, 42, 43, 500];
56 |
57 | numbers.forEach(n => {
58 | measurePerformance(memoizedFibonacci, n);
59 | });
60 |
--------------------------------------------------------------------------------
/examples/2024-08-17-memoization/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "2024-08-17-memoization",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "2024-08-17-memoization",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "devDependencies": {
12 | "@types/node": "22.4.0",
13 | "typescript": "5.5.4"
14 | }
15 | },
16 | "node_modules/@types/node": {
17 | "version": "22.4.0",
18 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.0.tgz",
19 | "integrity": "sha512-49AbMDwYUz7EXxKU/r7mXOsxwFr4BYbvB7tWYxVuLdb2ibd30ijjXINSMAHiEEZk5PCRBmW1gUeisn2VMKt3cQ==",
20 | "dev": true,
21 | "license": "MIT",
22 | "dependencies": {
23 | "undici-types": "~6.19.2"
24 | }
25 | },
26 | "node_modules/typescript": {
27 | "version": "5.5.4",
28 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
29 | "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
30 | "dev": true,
31 | "license": "Apache-2.0",
32 | "bin": {
33 | "tsc": "bin/tsc",
34 | "tsserver": "bin/tsserver"
35 | },
36 | "engines": {
37 | "node": ">=14.17"
38 | }
39 | },
40 | "node_modules/undici-types": {
41 | "version": "6.19.6",
42 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.6.tgz",
43 | "integrity": "sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==",
44 | "dev": true,
45 | "license": "MIT"
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/examples/2024-08-17-memoization/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "2024-08-17-memoization",
3 | "version": "1.0.0",
4 | "main": "example-1.js",
5 | "scripts": {
6 | "test": "echo \"Error: no test specified\" && exit 1"
7 | },
8 | "author": "",
9 | "license": "ISC",
10 | "description": "",
11 | "devDependencies": {
12 | "@types/node": "22.4.0",
13 | "typescript": "5.5.4"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "next/core-web-vitals",
4 | "plugin:unicorn/recommended",
5 | "plugin:import/recommended",
6 | "plugin:playwright/recommended",
7 | "plugin:prettier/recommended"
8 | ],
9 | "plugins": ["simple-import-sort"],
10 | "rules": {
11 | "simple-import-sort/exports": "error",
12 | "simple-import-sort/imports": "error",
13 | "unicorn/no-array-callback-reference": "off",
14 | "unicorn/no-array-for-each": "off",
15 | "unicorn/no-array-reduce": "off",
16 | "unicorn/no-null": "off",
17 | "unicorn/prevent-abbreviations": [
18 | "error",
19 | {
20 | "allowList": {
21 | "e2e": true
22 | },
23 | "replacements": {
24 | "props": false,
25 | "ref": false,
26 | "params": false
27 | }
28 | }
29 | ]
30 | },
31 | "overrides": [
32 | {
33 | "files": ["*.js"],
34 | "rules": {
35 | "unicorn/prefer-module": "off"
36 | }
37 | }
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": false,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "paths": {
4 | "@/*": ["./src/*"]
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@radix-ui/react-icons": "1.3.0",
4 | "@radix-ui/react-label": "2.1.0",
5 | "@radix-ui/react-slot": "1.1.0",
6 | "class-variance-authority": "0.7.0",
7 | "clsx": "2.1.1",
8 | "next": "14.2.6",
9 | "ramda": "0.30.1",
10 | "react": "^18",
11 | "react-dom": "^18",
12 | "react-redux": "9.1.2",
13 | "redux": "5.0.1",
14 | "redux-saga": "1.3.0",
15 | "tailwind-merge": "2.5.2",
16 | "tailwindcss-animate": "1.0.7"
17 | },
18 | "devDependencies": {
19 | "@types/node": "22.5.1",
20 | "@types/react": "18.3.4",
21 | "@typescript-eslint/parser": "8.2.0",
22 | "eslint": "^8",
23 | "eslint-config-next": "14.2.6",
24 | "eslint-config-prettier": "9.1.0",
25 | "eslint-plugin-import": "2.29.1",
26 | "eslint-plugin-playwright": "1.6.2",
27 | "eslint-plugin-prettier": "5.2.1",
28 | "eslint-plugin-simple-import-sort": "12.1.1",
29 | "eslint-plugin-unicorn": "55.0.0",
30 | "postcss": "^8",
31 | "prettier": "3.3.3",
32 | "prettier-plugin-tailwindcss": "0.6.6",
33 | "tailwindcss": "^3.4.1"
34 | },
35 | "name": "2024-08-20-redux-saga",
36 | "private": true,
37 | "scripts": {
38 | "build": "next build",
39 | "dev": "next dev",
40 | "format": "prettier --write .",
41 | "lint": "next lint",
42 | "lint:fix": "next lint --fix",
43 | "start": "next start"
44 | },
45 | "version": "0.1.0"
46 | }
47 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | arrowParens: 'avoid',
3 | bracketSameLine: false,
4 | bracketSpacing: true,
5 | htmlWhitespaceSensitivity: 'css',
6 | insertPragma: false,
7 | jsxSingleQuote: false,
8 | plugins: ['prettier-plugin-tailwindcss'],
9 | printWidth: 80,
10 | proseWrap: 'always',
11 | quoteProps: 'as-needed',
12 | requirePragma: false,
13 | semi: true,
14 | singleQuote: true,
15 | tabWidth: 2,
16 | trailingComma: 'all',
17 | useTabs: false,
18 | };
19 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/app/dashboard/page.js:
--------------------------------------------------------------------------------
1 | export { default } from '../../features/dashboard/dashboard-page-container';
2 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EarlyNode/tutorials/e78d678cd56f528932fedc0b664cda7f8874d228/examples/2024-08-20-redux-saga/src/app/favicon.ico
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 222.2 84% 4.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 222.2 84% 4.9%;
13 | --primary: 222.2 47.4% 11.2%;
14 | --primary-foreground: 210 40% 98%;
15 | --secondary: 210 40% 96.1%;
16 | --secondary-foreground: 222.2 47.4% 11.2%;
17 | --muted: 210 40% 96.1%;
18 | --muted-foreground: 215.4 16.3% 46.9%;
19 | --accent: 210 40% 96.1%;
20 | --accent-foreground: 222.2 47.4% 11.2%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 210 40% 98%;
23 | --border: 214.3 31.8% 91.4%;
24 | --input: 214.3 31.8% 91.4%;
25 | --ring: 222.2 84% 4.9%;
26 | --radius: 0.5rem;
27 | --chart-1: 12 76% 61%;
28 | --chart-2: 173 58% 39%;
29 | --chart-3: 197 37% 24%;
30 | --chart-4: 43 74% 66%;
31 | --chart-5: 27 87% 67%;
32 | }
33 |
34 | .dark {
35 | --background: 222.2 84% 4.9%;
36 | --foreground: 210 40% 98%;
37 | --card: 222.2 84% 4.9%;
38 | --card-foreground: 210 40% 98%;
39 | --popover: 222.2 84% 4.9%;
40 | --popover-foreground: 210 40% 98%;
41 | --primary: 210 40% 98%;
42 | --primary-foreground: 222.2 47.4% 11.2%;
43 | --secondary: 217.2 32.6% 17.5%;
44 | --secondary-foreground: 210 40% 98%;
45 | --muted: 217.2 32.6% 17.5%;
46 | --muted-foreground: 215 20.2% 65.1%;
47 | --accent: 217.2 32.6% 17.5%;
48 | --accent-foreground: 210 40% 98%;
49 | --destructive: 0 62.8% 30.6%;
50 | --destructive-foreground: 210 40% 98%;
51 | --border: 217.2 32.6% 17.5%;
52 | --input: 217.2 32.6% 17.5%;
53 | --ring: 212.7 26.8% 83.9%;
54 | --chart-1: 220 70% 50%;
55 | --chart-2: 160 60% 45%;
56 | --chart-3: 30 80% 55%;
57 | --chart-4: 280 65% 60%;
58 | --chart-5: 340 75% 55%;
59 | }
60 | }
61 |
62 | @layer base {
63 | * {
64 | @apply border-border;
65 | }
66 | body {
67 | @apply bg-background text-foreground;
68 | }
69 | }
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/app/layout.js:
--------------------------------------------------------------------------------
1 | import './globals.css';
2 |
3 | import { Inter } from 'next/font/google';
4 |
5 | import { StoreProvider } from './redux/store-provider';
6 |
7 | const inter = Inter({ subsets: ['latin'] });
8 |
9 | export const metadata = {
10 | title: 'Jan Hesters Redux Tutorial',
11 | description: 'Part one of three to master Redux.',
12 | };
13 |
14 | export default function RootLayout({ children }) {
15 | return (
16 |
17 |
18 | {children}
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/app/login/page.js:
--------------------------------------------------------------------------------
1 | export { default } from '../../features/user-authentication/user-authentication-page-container';
2 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/components/ui/button.jsx:
--------------------------------------------------------------------------------
1 | import { Slot } from '@radix-ui/react-slot';
2 | import { cva } from 'class-variance-authority';
3 | import * as React from 'react';
4 |
5 | import { cn } from '../../lib/utils';
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
14 | destructive:
15 | 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
16 | outline:
17 | 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
18 | secondary:
19 | 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
20 | ghost: 'hover:bg-accent hover:text-accent-foreground',
21 | link: 'text-primary underline-offset-4 hover:underline',
22 | },
23 | size: {
24 | default: 'h-9 px-4 py-2',
25 | sm: 'h-8 rounded-md px-3 text-xs',
26 | lg: 'h-10 rounded-md px-8',
27 | icon: 'h-9 w-9',
28 | },
29 | },
30 | defaultVariants: {
31 | variant: 'default',
32 | size: 'default',
33 | },
34 | },
35 | );
36 |
37 | const Button = React.forwardRef(
38 | ({ className, variant, size, asChild = false, ...props }, ref) => {
39 | const Comp = asChild ? Slot : 'button';
40 | return (
41 |
46 | );
47 | },
48 | );
49 | Button.displayName = 'Button';
50 |
51 | export { Button, buttonVariants };
52 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/components/ui/card.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '../../lib/utils';
4 |
5 | const Card = React.forwardRef(({ className, ...props }, ref) => (
6 |
14 | ));
15 | Card.displayName = 'Card';
16 |
17 | const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
18 |
23 | ));
24 | CardHeader.displayName = 'CardHeader';
25 |
26 | const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
27 |
32 | ));
33 | CardTitle.displayName = 'CardTitle';
34 |
35 | const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
36 |
41 | ));
42 | CardDescription.displayName = 'CardDescription';
43 |
44 | const CardContent = React.forwardRef(({ className, ...props }, ref) => (
45 |
46 | ));
47 | CardContent.displayName = 'CardContent';
48 |
49 | const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
50 |
55 | ));
56 | CardFooter.displayName = 'CardFooter';
57 |
58 | export {
59 | Card,
60 | CardContent,
61 | CardDescription,
62 | CardFooter,
63 | CardHeader,
64 | CardTitle,
65 | };
66 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/components/ui/input.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '../../lib/utils';
4 |
5 | const Input = React.forwardRef(({ className, type, ...props }, ref) => {
6 | return (
7 |
16 | );
17 | });
18 | Input.displayName = 'Input';
19 |
20 | export { Input };
21 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/components/ui/label.jsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as LabelPrimitive from '@radix-ui/react-label';
4 | import { cva } from 'class-variance-authority';
5 | import * as React from 'react';
6 |
7 | import { cn } from '../../lib/utils';
8 |
9 | const labelVariants = cva(
10 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
11 | );
12 |
13 | const Label = React.forwardRef(({ className, ...props }, ref) => (
14 |
19 | ));
20 | Label.displayName = LabelPrimitive.Root.displayName;
21 |
22 | export { Label };
23 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/features/dashboard/dashboard-page-component.js:
--------------------------------------------------------------------------------
1 | export function DashboardPageComponent({ currentUsersEmail }) {
2 | return (
3 |
4 |
Welcome back, {currentUsersEmail}!
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/features/dashboard/dashboard-page-container.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { connect } from 'react-redux';
3 |
4 | import { selectCurrentUsersEmail } from '../user-profiles/user-profile-reducer';
5 | import { DashboardPageComponent } from './dashboard-page-component';
6 |
7 | const mapStateToProps = state => ({
8 | currentUsersEmail: selectCurrentUsersEmail(state),
9 | });
10 |
11 | export default connect(mapStateToProps)(DashboardPageComponent);
12 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/features/example/example-reducer.js:
--------------------------------------------------------------------------------
1 | import { pipe, prop } from 'ramda';
2 |
3 | export const slice = 'example';
4 |
5 | export const increment = () => ({ type: `${slice}/increment` });
6 | export const incrementBy = payload => ({
7 | type: `${slice}/incrementBy`,
8 | payload,
9 | });
10 |
11 | const initialState = {
12 | count: 0,
13 | };
14 |
15 | export const reducer = (state = initialState, { type, payload } = {}) => {
16 | switch (type) {
17 | case increment().type: {
18 | return { ...state, count: state.count + 1 };
19 | }
20 | case incrementBy().type: {
21 | return { ...state, count: state.count + payload };
22 | }
23 | default: {
24 | return state;
25 | }
26 | }
27 | };
28 |
29 | export const selectExampleState = prop(slice);
30 |
31 | export const selectCount = pipe(selectExampleState, prop('count'));
32 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/features/example/example-saga.js:
--------------------------------------------------------------------------------
1 | import { call, put, select, take } from '../../app/redux/effects';
2 | import { increment, incrementBy, selectCount } from './example-reducer';
3 |
4 | const fetchUser = async id => {
5 | const response = await fetch(
6 | `https://jsonplaceholder.typicode.com/users/${id}`,
7 | );
8 | return await response.json();
9 | };
10 |
11 | export function* exampleSaga() {
12 | yield take('init');
13 | yield put(increment());
14 | const currentCount = yield select(selectCount);
15 | const user = yield call(fetchUser, currentCount);
16 | yield put(incrementBy(user.name.length));
17 | }
18 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/features/user-authentication/user-authentication-page-component.js:
--------------------------------------------------------------------------------
1 | import { Button } from '../../components/ui/button';
2 | import {
3 | Card,
4 | CardContent,
5 | CardDescription,
6 | CardFooter,
7 | CardHeader,
8 | CardTitle,
9 | } from '../../components/ui/card';
10 | import { Input } from '../../components/ui/input';
11 | import { Label } from '../../components/ui/label';
12 |
13 | export function UserAuthenticationPageComponent({
14 | isLoading,
15 | onLoginClicked,
16 | router,
17 | }) {
18 | return (
19 |
20 |
21 |
22 | Login
23 |
24 | Enter your email below to login to your account.
25 |
26 |
27 |
56 |
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/features/user-authentication/user-authentication-page-container.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { connect } from 'react-redux';
4 | import { compose } from 'redux';
5 |
6 | import { withRouter } from '../../hocs/with-router';
7 | import {
8 | loginClicked,
9 | selectIsLoading,
10 | } from '../user-profiles/user-profile-reducer';
11 | import { UserAuthenticationPageComponent } from './user-authentication-page-component';
12 |
13 | const mapStateToProps = state => ({
14 | isLoading: selectIsLoading(state),
15 | });
16 |
17 | const mapDispatchToProps = {
18 | onLoginClicked: loginClicked,
19 | };
20 |
21 | export default compose(
22 | withRouter,
23 | connect(mapStateToProps, mapDispatchToProps),
24 | )(UserAuthenticationPageComponent);
25 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/features/user-profiles/user-profile-api.js:
--------------------------------------------------------------------------------
1 | export const fetchUserById = async id => {
2 | const response = await fetch(
3 | `https://jsonplaceholder.typicode.com/users/${id}`,
4 | );
5 | return await response.json();
6 | };
7 |
8 | export const fetchUsers = async () => {
9 | const response = await fetch('https://jsonplaceholder.typicode.com/users');
10 | return await response.json();
11 | };
12 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/features/user-profiles/user-profile-reducer.js:
--------------------------------------------------------------------------------
1 | import { converge, pipe, prop, propOr } from 'ramda';
2 |
3 | export const slice = 'userProfiles';
4 |
5 | /**
6 | * Actions
7 | */
8 |
9 | export const loginClicked = payload => ({
10 | type: `${slice}/loginClicked`,
11 | payload,
12 | });
13 | export const loginSucceeded = payload => ({
14 | type: `${slice}/loginSucceeded`,
15 | payload,
16 | });
17 | export const fetchedUsers = payload => ({
18 | type: `${slice}/fetchedUsers`,
19 | payload,
20 | });
21 |
22 | /**
23 | * Reducer
24 | */
25 |
26 | export const initialState = {
27 | currentUsersId: null,
28 | isLoading: false,
29 | users: {},
30 | };
31 |
32 | export const reducer = (state = initialState, { type, payload } = {}) => {
33 | switch (type) {
34 | case loginClicked().type: {
35 | return { ...state, isLoading: true };
36 | }
37 | case loginSucceeded().type: {
38 | return { ...state, currentUsersId: payload.id };
39 | }
40 | case fetchedUsers().type: {
41 | const users = payload.reduce((normalizedUsers, user) => {
42 | normalizedUsers[user.id] = user;
43 | return normalizedUsers;
44 | }, state.users);
45 |
46 | return { ...state, users, isLoading: false };
47 | }
48 | default: {
49 | return state;
50 | }
51 | }
52 | };
53 |
54 | /**
55 | * Selectors
56 | */
57 |
58 | export const selectUserProfilesSlice = prop(slice);
59 |
60 | const selectCurrentUsersId = pipe(
61 | selectUserProfilesSlice,
62 | prop('currentUsersId'),
63 | );
64 |
65 | const selectUsers = pipe(selectUserProfilesSlice, prop('users'));
66 |
67 | export const selectCurrentUser = converge(prop, [
68 | selectCurrentUsersId,
69 | selectUsers,
70 | ]);
71 |
72 | export const selectCurrentUsersEmail = pipe(
73 | selectCurrentUser,
74 | propOr('', 'email'),
75 | );
76 |
77 | export const selectIsLoggedIn = pipe(selectCurrentUsersId, Boolean);
78 |
79 | export const selectIsLoading = pipe(selectUserProfilesSlice, prop('isLoading'));
80 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/features/user-profiles/user-profile-saga.js:
--------------------------------------------------------------------------------
1 | import { call, put, takeLeading } from 'redux-saga/effects';
2 |
3 | import { fetchUserById, fetchUsers } from './user-profile-api';
4 | import {
5 | fetchedUsers,
6 | loginClicked,
7 | loginSucceeded,
8 | } from './user-profile-reducer';
9 |
10 | function* handleLoginClicked({ payload: { id, router } }) {
11 | const user = yield call(fetchUserById, id);
12 | yield put(loginSucceeded(user));
13 | const users = yield call(fetchUsers);
14 | yield put(fetchedUsers(users));
15 | yield call(router.push, '/dashboard');
16 | }
17 |
18 | export function* watchHandleLoginClicked() {
19 | yield takeLeading(loginClicked().type, handleLoginClicked);
20 | }
21 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/hocs/with-router.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useRouter } from 'next/navigation';
3 |
4 | export const withRouter = Component => {
5 | function WithRouter(props) {
6 | const router = useRouter();
7 |
8 | return ;
9 | }
10 |
11 | return WithRouter;
12 | };
13 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/lib/utils.js:
--------------------------------------------------------------------------------
1 | import { clsx } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/redux/effects.js:
--------------------------------------------------------------------------------
1 | export const take = actionType => ({
2 | '@@redux-saga/IO': true,
3 | type: 'TAKE',
4 | payload: { actionType },
5 | });
6 |
7 | export const put = action => ({
8 | '@@redux-saga/IO': true,
9 | type: 'PUT',
10 | payload: { action },
11 | });
12 |
13 | export const select = (selector, ...arguments_) => ({
14 | '@@redux-saga/IO': true,
15 | type: 'SELECT',
16 | payload: {
17 | selector,
18 | args: arguments_,
19 | },
20 | });
21 |
22 | export const call = (functionOrContextAndFunction, ...arguments_) =>
23 | Array.isArray(functionOrContextAndFunction)
24 | ? {
25 | '@@redux-saga/IO': true,
26 | type: 'CALL',
27 | payload: {
28 | fn: functionOrContextAndFunction[1],
29 | args: arguments_,
30 | context: functionOrContextAndFunction[0],
31 | },
32 | }
33 | : {
34 | '@@redux-saga/IO': true,
35 | type: 'CALL',
36 | payload: {
37 | fn: functionOrContextAndFunction,
38 | args: arguments_,
39 | context: null,
40 | },
41 | };
42 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/redux/root-reducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import {
4 | reducer as exampleReducer,
5 | slice as exampleSlice,
6 | } from '../../features/example/example-reducer';
7 | import {
8 | reducer as userProfileReducer,
9 | slice as userProfileSlice,
10 | } from '../../features/user-profiles/user-profile-reducer';
11 |
12 | export const rootReducer = combineReducers({
13 | [exampleSlice]: exampleReducer,
14 | [userProfileSlice]: userProfileReducer,
15 | });
16 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/redux/root-saga.js:
--------------------------------------------------------------------------------
1 | import { all } from 'redux-saga/effects';
2 |
3 | import { watchHandleLoginClicked } from '../../features/user-profiles/user-profile-saga';
4 |
5 | export function* rootSaga() {
6 | yield all([watchHandleLoginClicked()]);
7 | }
8 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/redux/saga-middleware.js:
--------------------------------------------------------------------------------
1 | export function createSagaMiddleware() {
2 | let sagas = [];
3 |
4 | const middleware = store => next => action => {
5 | const result = next(action);
6 |
7 | sagas.forEach(saga => {
8 | const sagaIterator = saga();
9 |
10 | let lastValue;
11 | let effectHandled = false;
12 |
13 | const handleEffect = async effect => {
14 | if (!effect || !effect['@@redux-saga/IO']) {
15 | effectHandled = true;
16 | return;
17 | }
18 |
19 | switch (effect.type) {
20 | case 'TAKE': {
21 | if (effect.payload.actionType === action.type) {
22 | return action;
23 | } else {
24 | effectHandled = true;
25 | return;
26 | }
27 | }
28 | case 'PUT': {
29 | store.dispatch(effect.payload.action);
30 | break;
31 | }
32 | case 'SELECT': {
33 | return effect.payload.selector(store.getState());
34 | }
35 | case 'CALL': {
36 | try {
37 | const { fn, args, context } = effect.payload;
38 | const functionResult = fn.apply(context, args);
39 | if (functionResult instanceof Promise) {
40 | return await functionResult;
41 | }
42 | return functionResult;
43 | } catch (error) {
44 | effectHandled = true;
45 | return sagaIterator.throw(error);
46 | }
47 | }
48 | default: {
49 | effectHandled = true;
50 | return;
51 | }
52 | }
53 | };
54 |
55 | const processSaga = async () => {
56 | try {
57 | while (!effectHandled) {
58 | const { value, done } = sagaIterator.next(lastValue);
59 |
60 | if (done) {
61 | break;
62 | }
63 |
64 | lastValue = await handleEffect(value);
65 | }
66 | } catch (error) {
67 | sagaIterator.throw(error);
68 | }
69 | };
70 |
71 | processSaga();
72 | });
73 |
74 | return result;
75 | };
76 |
77 | middleware.run = saga => {
78 | sagas.push(saga);
79 | };
80 |
81 | return middleware;
82 | }
83 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/redux/store-provider.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useRef } from 'react';
3 | import { Provider } from 'react-redux';
4 |
5 | import { makeStore } from './store';
6 |
7 | export function StoreProvider({ children }) {
8 | const storeRef = useRef();
9 |
10 | if (!storeRef.current) {
11 | storeRef.current = makeStore();
12 | }
13 |
14 | return {children};
15 | }
16 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, legacy_createStore as createStore } from 'redux';
2 | import createSagaMiddleware from 'redux-saga';
3 |
4 | import { rootReducer } from './root-reducer';
5 | import { rootSaga } from './root-saga';
6 |
7 | const logger = store => next => action => {
8 | console.log('dispatching', action);
9 | let result = next(action);
10 | console.log('next state', store.getState());
11 | return result;
12 | };
13 |
14 | export const makeStore = () => {
15 | const sagaMiddleware = createSagaMiddleware();
16 | const store = createStore(
17 | rootReducer,
18 | rootReducer(),
19 | applyMiddleware(logger, sagaMiddleware),
20 | );
21 | sagaMiddleware.run(rootSaga);
22 | return store;
23 | };
24 |
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{js,jsx}',
6 | './components/**/*.{js,jsx}',
7 | './app/**/*.{js,jsx}',
8 | './src/**/*.{js,jsx}',
9 | ],
10 | prefix: "",
11 | theme: {
12 | container: {
13 | center: true,
14 | padding: "2rem",
15 | screens: {
16 | "2xl": "1400px",
17 | },
18 | },
19 | extend: {
20 | colors: {
21 | border: "hsl(var(--border))",
22 | input: "hsl(var(--input))",
23 | ring: "hsl(var(--ring))",
24 | background: "hsl(var(--background))",
25 | foreground: "hsl(var(--foreground))",
26 | primary: {
27 | DEFAULT: "hsl(var(--primary))",
28 | foreground: "hsl(var(--primary-foreground))",
29 | },
30 | secondary: {
31 | DEFAULT: "hsl(var(--secondary))",
32 | foreground: "hsl(var(--secondary-foreground))",
33 | },
34 | destructive: {
35 | DEFAULT: "hsl(var(--destructive))",
36 | foreground: "hsl(var(--destructive-foreground))",
37 | },
38 | muted: {
39 | DEFAULT: "hsl(var(--muted))",
40 | foreground: "hsl(var(--muted-foreground))",
41 | },
42 | accent: {
43 | DEFAULT: "hsl(var(--accent))",
44 | foreground: "hsl(var(--accent-foreground))",
45 | },
46 | popover: {
47 | DEFAULT: "hsl(var(--popover))",
48 | foreground: "hsl(var(--popover-foreground))",
49 | },
50 | card: {
51 | DEFAULT: "hsl(var(--card))",
52 | foreground: "hsl(var(--card-foreground))",
53 | },
54 | },
55 | borderRadius: {
56 | lg: "var(--radius)",
57 | md: "calc(var(--radius) - 2px)",
58 | sm: "calc(var(--radius) - 4px)",
59 | },
60 | keyframes: {
61 | "accordion-down": {
62 | from: { height: "0" },
63 | to: { height: "var(--radix-accordion-content-height)" },
64 | },
65 | "accordion-up": {
66 | from: { height: "var(--radix-accordion-content-height)" },
67 | to: { height: "0" },
68 | },
69 | },
70 | animation: {
71 | "accordion-down": "accordion-down 0.2s ease-out",
72 | "accordion-up": "accordion-up 0.2s ease-out",
73 | },
74 | },
75 | },
76 | plugins: [require("tailwindcss-animate")],
77 | }
--------------------------------------------------------------------------------
/examples/2024-08-20-redux-saga/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": false,
11 | "noEmit": true,
12 | "incremental": true,
13 | "module": "esnext",
14 | "esModuleInterop": true,
15 | "moduleResolution": "node",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "jsx": "preserve",
19 | "plugins": [
20 | {
21 | "name": "next"
22 | }
23 | ]
24 | },
25 | "include": [
26 | "next-env.d.ts",
27 | ".next/types/**/*.ts",
28 | "**/*.ts",
29 | "**/*.tsx"
30 | ],
31 | "exclude": [
32 | "node_modules"
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "next/core-web-vitals",
4 | "plugin:unicorn/recommended",
5 | "plugin:import/recommended",
6 | "plugin:playwright/recommended",
7 | "plugin:prettier/recommended"
8 | ],
9 | "plugins": ["simple-import-sort"],
10 | "rules": {
11 | "simple-import-sort/exports": "error",
12 | "simple-import-sort/imports": "error",
13 | "unicorn/no-array-callback-reference": "off",
14 | "unicorn/no-array-for-each": "off",
15 | "unicorn/no-array-reduce": "off",
16 | "unicorn/prefer-spread": "off",
17 | "unicorn/no-null": "off",
18 | "unicorn/prevent-abbreviations": [
19 | "error",
20 | {
21 | "allowList": {
22 | "e2e": true
23 | },
24 | "replacements": {
25 | "props": false,
26 | "ref": false,
27 | "params": false
28 | }
29 | }
30 | ]
31 | },
32 | "overrides": [
33 | {
34 | "files": ["*.js"],
35 | "rules": {
36 | "unicorn/prefer-module": "off"
37 | }
38 | }
39 | ]
40 | }
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@radix-ui/react-icons": "1.3.0",
4 | "@radix-ui/react-label": "2.1.0",
5 | "@radix-ui/react-slot": "1.1.0",
6 | "@reduxjs/toolkit": "2.2.7",
7 | "axios": "1.7.7",
8 | "class-variance-authority": "0.7.0",
9 | "clsx": "2.1.1",
10 | "hoist-non-react-statics": "3.3.2",
11 | "lucide-react": "0.438.0",
12 | "next": "14.2.7",
13 | "ramda": "0.30.1",
14 | "react": "^18",
15 | "react-dom": "^18",
16 | "react-redux": "9.1.2",
17 | "redux-saga": "1.3.0",
18 | "tailwind-merge": "2.5.2",
19 | "tailwindcss-animate": "1.0.7"
20 | },
21 | "devDependencies": {
22 | "@types/hoist-non-react-statics": "3.3.5",
23 | "@types/node": "^20",
24 | "@types/ramda": "0.30.2",
25 | "@types/react": "^18",
26 | "@types/react-dom": "^18",
27 | "@typescript-eslint/parser": "8.3.0",
28 | "eslint": "^8",
29 | "eslint-config-next": "14.2.7",
30 | "eslint-config-prettier": "9.1.0",
31 | "eslint-plugin-import": "2.29.1",
32 | "eslint-plugin-playwright": "1.6.2",
33 | "eslint-plugin-prettier": "5.2.1",
34 | "eslint-plugin-simple-import-sort": "12.1.1",
35 | "eslint-plugin-unicorn": "55.0.0",
36 | "postcss": "^8",
37 | "prettier": "3.3.3",
38 | "prettier-plugin-tailwindcss": "0.6.6",
39 | "tailwindcss": "^3.4.1",
40 | "typescript": "^5"
41 | },
42 | "name": "2024-09-01-redux-for-production",
43 | "private": true,
44 | "scripts": {
45 | "build": "next build",
46 | "dev": "next dev",
47 | "format": "prettier --write .",
48 | "lint": "next lint",
49 | "lint:fix": "next lint --fix",
50 | "start": "next start"
51 | },
52 | "version": "0.1.0"
53 | }
54 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | arrowParens: 'avoid',
3 | bracketSameLine: false,
4 | bracketSpacing: true,
5 | htmlWhitespaceSensitivity: 'css',
6 | insertPragma: false,
7 | jsxSingleQuote: false,
8 | plugins: ['prettier-plugin-tailwindcss'],
9 | printWidth: 80,
10 | proseWrap: 'always',
11 | quoteProps: 'as-needed',
12 | requirePragma: false,
13 | semi: true,
14 | singleQuote: true,
15 | tabWidth: 2,
16 | trailingComma: 'all',
17 | useTabs: false,
18 | };
19 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/app/dashboard/page.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import DashboardPage from '@/features/dashboard/dashboard-page-container';
3 | import authenticatedPage from '@/hocs/authenticated-page';
4 |
5 | export default authenticatedPage(DashboardPage);
6 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EarlyNode/tutorials/e78d678cd56f528932fedc0b664cda7f8874d228/examples/2024-09-01-redux-for-production/src/app/favicon.ico
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | @media (prefers-color-scheme: dark) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | }
18 |
19 | @layer utilities {
20 | .text-balance {
21 | text-wrap: balance;
22 | }
23 | }
24 |
25 | @layer base {
26 | :root {
27 | --background: 0 0% 100%;
28 | --foreground: 0 0% 3.9%;
29 | --card: 0 0% 100%;
30 | --card-foreground: 0 0% 3.9%;
31 | --popover: 0 0% 100%;
32 | --popover-foreground: 0 0% 3.9%;
33 | --primary: 0 0% 9%;
34 | --primary-foreground: 0 0% 98%;
35 | --secondary: 0 0% 96.1%;
36 | --secondary-foreground: 0 0% 9%;
37 | --muted: 0 0% 96.1%;
38 | --muted-foreground: 0 0% 45.1%;
39 | --accent: 0 0% 96.1%;
40 | --accent-foreground: 0 0% 9%;
41 | --destructive: 0 84.2% 60.2%;
42 | --destructive-foreground: 0 0% 98%;
43 | --border: 0 0% 89.8%;
44 | --input: 0 0% 89.8%;
45 | --ring: 0 0% 3.9%;
46 | --chart-1: 12 76% 61%;
47 | --chart-2: 173 58% 39%;
48 | --chart-3: 197 37% 24%;
49 | --chart-4: 43 74% 66%;
50 | --chart-5: 27 87% 67%;
51 | --radius: 0.5rem;
52 | }
53 | .dark {
54 | --background: 0 0% 3.9%;
55 | --foreground: 0 0% 98%;
56 | --card: 0 0% 3.9%;
57 | --card-foreground: 0 0% 98%;
58 | --popover: 0 0% 3.9%;
59 | --popover-foreground: 0 0% 98%;
60 | --primary: 0 0% 98%;
61 | --primary-foreground: 0 0% 9%;
62 | --secondary: 0 0% 14.9%;
63 | --secondary-foreground: 0 0% 98%;
64 | --muted: 0 0% 14.9%;
65 | --muted-foreground: 0 0% 63.9%;
66 | --accent: 0 0% 14.9%;
67 | --accent-foreground: 0 0% 98%;
68 | --destructive: 0 62.8% 30.6%;
69 | --destructive-foreground: 0 0% 98%;
70 | --border: 0 0% 14.9%;
71 | --input: 0 0% 14.9%;
72 | --ring: 0 0% 83.1%;
73 | --chart-1: 220 70% 50%;
74 | --chart-2: 160 60% 45%;
75 | --chart-3: 30 80% 55%;
76 | --chart-4: 280 65% 60%;
77 | --chart-5: 340 75% 55%;
78 | }
79 | }
80 |
81 | @layer base {
82 | * {
83 | @apply border-border;
84 | }
85 | body {
86 | @apply bg-background text-foreground;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import './globals.css';
2 |
3 | import type { Metadata } from 'next';
4 | import { Inter } from 'next/font/google';
5 |
6 | import StoreProvider from '@/redux/store-provider';
7 |
8 | const inter = Inter({ subsets: ['latin'] });
9 |
10 | export const metadata: Metadata = {
11 | title: 'Jan Hesters Production Redux Tutorial',
12 | description: 'Part three of five to master Redux.',
13 | };
14 |
15 | export default function RootLayout({
16 | children,
17 | }: Readonly<{
18 | children: React.ReactNode;
19 | }>) {
20 | return (
21 |
22 |
23 | {children}
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/app/login/page.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { compose } from 'redux';
3 |
4 | import UserAuthentication from '@/features/user-authentication/user-authentication-container';
5 | import withPublicPage from '@/hocs/public-page';
6 | import redirectIfLoggedIn from '@/hocs/redirect-if-logged-in';
7 |
8 | export default compose(
9 | redirectIfLoggedIn('/dashboard'),
10 | withPublicPage,
11 | )(UserAuthentication);
12 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/app/posts/page.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { PostsPageComponent } from '@/features/posts/posts-page-component';
3 | import authenticatedPage from '@/hocs/authenticated-page';
4 |
5 | export default authenticatedPage(PostsPageComponent);
6 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/components/spinner.tsx:
--------------------------------------------------------------------------------
1 | import { SVGProps } from 'react';
2 |
3 | export const Spinner = (props: SVGProps) => (
4 |
19 | );
20 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Card.displayName = 'Card';
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | CardHeader.displayName = 'CardHeader';
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ));
42 | CardTitle.displayName = 'CardTitle';
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLParagraphElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ));
54 | CardDescription.displayName = 'CardDescription';
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ));
62 | CardContent.displayName = 'CardContent';
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ));
74 | CardFooter.displayName = 'CardFooter';
75 |
76 | export {
77 | Card,
78 | CardContent,
79 | CardDescription,
80 | CardFooter,
81 | CardHeader,
82 | CardTitle,
83 | };
84 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/app-loading/app-loading-component.tsx:
--------------------------------------------------------------------------------
1 | import { Spinner } from '@/components/spinner';
2 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
3 |
4 | export function AppLoadingComponent() {
5 | return (
6 |
7 |
8 |
9 | Loading ...
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/app-loading/app-loading-container.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useEffect } from 'react';
3 | import type { ConnectedProps } from 'react-redux';
4 | import { connect } from 'react-redux';
5 |
6 | import { AppLoadingComponent } from './app-loading-component';
7 | import { loadApp } from './app-loading-saga';
8 |
9 | const mapDispatchToProps = { loadApp };
10 |
11 | const connector = connect(undefined, mapDispatchToProps);
12 |
13 | type AppLoadingPropsFromRedux = ConnectedProps;
14 |
15 | function AppLoadingContainer({ loadApp }: AppLoadingPropsFromRedux) {
16 | useEffect(() => {
17 | loadApp();
18 | }, [loadApp]);
19 |
20 | return ;
21 | }
22 |
23 | export default connector(AppLoadingContainer);
24 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/app-loading/app-loading-reducer.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 | import { not, pipe, prop } from 'ramda';
3 |
4 | const initialState = { appIsLoading: true };
5 |
6 | export const {
7 | actions: { finishedAppLoading },
8 | name,
9 | reducer,
10 | selectors: { selectAppIsLoading },
11 | } = createSlice({
12 | name: 'appLoading',
13 | initialState,
14 | reducers: {
15 | finishedAppLoading: state => {
16 | state.appIsLoading = false;
17 | },
18 | },
19 | selectors: {
20 | selectAppIsLoading: prop<'appIsLoading'>('appIsLoading'),
21 | },
22 | });
23 |
24 | /**
25 | * SELECTORS
26 | */
27 |
28 | export const selectAppFinishedLoading = pipe(selectAppIsLoading, not);
29 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/app-loading/app-loading-saga.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '@reduxjs/toolkit';
2 | import { call, put, takeLeading } from 'redux-saga/effects';
3 |
4 | import { handleFetchCurrentUsersProfile } from '@/features/user-profiles/user-profiles-saga';
5 |
6 | import { finishedAppLoading, name } from './app-loading-reducer';
7 |
8 | export const loadApp = createAction(`${name}/loadApp`);
9 |
10 | function* handleLoadApp() {
11 | yield call(handleFetchCurrentUsersProfile);
12 | yield put(finishedAppLoading());
13 | }
14 |
15 | export function* watchLoadApp() {
16 | yield takeLeading(loadApp.type, handleLoadApp);
17 | }
18 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/dashboard/dashboard-page-component.tsx:
--------------------------------------------------------------------------------
1 | import { Spinner } from '@/components/spinner';
2 | import { Button } from '@/components/ui/button';
3 | import {
4 | Card,
5 | CardContent,
6 | CardDescription,
7 | CardFooter,
8 | CardHeader,
9 | CardTitle,
10 | } from '@/components/ui/card';
11 |
12 | import { DashboardPagePropsFromRedux } from './dashboard-page-container';
13 |
14 | export function DashBoardPageComponent({
15 | currentUsersName,
16 | isLoading,
17 | users,
18 | onLogout,
19 | }: Omit) {
20 | return (
21 |
22 |
23 |
24 | Dashboard
25 | Welcome back, {currentUsersName}!
26 |
27 |
28 |
29 | {isLoading ? (
30 |
31 | ) : (
32 |
33 | {users.map(user => (
34 | - {user.name}
35 | ))}
36 |
37 | )}
38 |
39 |
40 |
41 |
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/dashboard/dashboard-page-container.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useEffect } from 'react';
3 | import type { ConnectedProps } from 'react-redux';
4 | import { connect } from 'react-redux';
5 |
6 | import { RootState } from '@/redux/store';
7 |
8 | import { logout } from '../user-authentication/user-authentication-saga';
9 | import {
10 | selectCurrentUsersName,
11 | selectUserProfilesAreLoading,
12 | selectUsersList,
13 | } from '../user-profiles/user-profiles-reducer';
14 | import { fetchUserProfiles } from '../user-profiles/user-profiles-saga';
15 | import { DashBoardPageComponent } from './dashboard-page-component';
16 |
17 | const mapStateToProps = (state: RootState) => ({
18 | currentUsersName: selectCurrentUsersName(state),
19 | isLoading: selectUserProfilesAreLoading(state),
20 | users: selectUsersList(state),
21 | });
22 |
23 | const mapDispatchToProps = {
24 | fetchUserProfiles,
25 | onLogout: logout,
26 | };
27 |
28 | const connector = connect(mapStateToProps, mapDispatchToProps);
29 |
30 | export type DashboardPagePropsFromRedux = ConnectedProps;
31 |
32 | function DashboardContainer({
33 | fetchUserProfiles,
34 | ...props
35 | }: DashboardPagePropsFromRedux) {
36 | useEffect(() => {
37 | fetchUserProfiles();
38 | }, [fetchUserProfiles]);
39 |
40 | return ;
41 | }
42 |
43 | export default connector(DashboardContainer);
44 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/posts/add-post-component.tsx:
--------------------------------------------------------------------------------
1 | import { Spinner } from '@/components/spinner';
2 | import { Button } from '@/components/ui/button';
3 | import { Input } from '@/components/ui/input';
4 | import { Label } from '@/components/ui/label';
5 |
6 | import { useAddPostMutation } from './posts-api';
7 |
8 | export function AddPostComponent() {
9 | const [addPost, { isLoading }] = useAddPostMutation();
10 |
11 | const onSubmit = async (event: React.FormEvent) => {
12 | event.preventDefault();
13 |
14 | try {
15 | const formData = new FormData(event.currentTarget);
16 | const title = formData.get('post-title') as string;
17 | const body = formData.get('post-body') as string;
18 |
19 | await addPost({ title, body }).unwrap();
20 | } catch (error) {
21 | console.error('Failed to save the post:', error);
22 | }
23 | };
24 |
25 | return (
26 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/posts/posts-api.ts:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
2 |
3 | import { Post } from './posts-types';
4 |
5 | export const {
6 | middleware,
7 | reducer,
8 | reducerPath,
9 | useAddPostMutation,
10 | useGetPostsQuery,
11 | util,
12 | } = createApi({
13 | reducerPath: 'postsApi',
14 | baseQuery: fetchBaseQuery({
15 | baseUrl: 'https://jsonplaceholder.typicode.com/',
16 | // Can have more options, such as prepareHeaders, credentials etc.
17 | }),
18 | tagTypes: ['Posts'],
19 | endpoints: builder => ({
20 | getPosts: builder.query({
21 | query: () => 'posts',
22 | providesTags: ['Posts'],
23 | }),
24 | addPost: builder.mutation>({
25 | query: body => ({
26 | url: 'posts',
27 | method: 'POST',
28 | body,
29 | }),
30 | invalidatesTags: ['Posts'],
31 | async onQueryStarted(argument, { dispatch, queryFulfilled }) {
32 | const patchResult = dispatch(
33 | util.updateQueryData('getPosts', undefined, draft => {
34 | draft.unshift({ id: Date.now(), ...argument } as Post);
35 | }),
36 | );
37 | try {
38 | await queryFulfilled;
39 | } catch {
40 | patchResult.undo();
41 | }
42 | },
43 | }),
44 | }),
45 | });
46 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/posts/posts-list-component.tsx:
--------------------------------------------------------------------------------
1 | import { Spinner } from '@/components/spinner';
2 | import { Button } from '@/components/ui/button';
3 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
4 |
5 | import { useGetPostsQuery } from './posts-api';
6 |
7 | export function PostsListComponent() {
8 | const { data: posts, error, isLoading, refetch } = useGetPostsQuery();
9 |
10 | if (isLoading) {
11 | return ;
12 | }
13 |
14 | if (error) {
15 | // Determine the error message based on the type of error
16 | let errorMessage = 'An unknown error occurred';
17 | if ('status' in error) {
18 | errorMessage = `An error occurred: ${error.status} ${error.data ? JSON.stringify(error.data) : 'Unknown error'}`;
19 | } else if ('message' in error) {
20 | errorMessage = `An error occurred: ${error.message}`;
21 | }
22 |
23 | return (
24 |
25 | {errorMessage}
26 |
27 |
28 | );
29 | }
30 |
31 | return (
32 |
33 |
34 | Latest Posts
35 |
36 |
37 |
38 |
39 | {posts?.slice(0, 5).map(post => (
40 | -
41 | {post.title}
42 |
{post.body}
43 |
44 | ))}
45 |
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/posts/posts-page-component.tsx:
--------------------------------------------------------------------------------
1 | import { AddPostComponent } from './add-post-component';
2 | import { PostsListComponent } from './posts-list-component';
3 |
4 | export function PostsPageComponent() {
5 | return (
6 |
7 |
10 |
11 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/posts/posts-types.ts:
--------------------------------------------------------------------------------
1 | export type Post = {
2 | userId: number;
3 | id: number;
4 | title: string;
5 | body: string;
6 | };
7 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/user-authentication/user-authentication-api.ts:
--------------------------------------------------------------------------------
1 | export const loginRequest = (email: string, password: string) => {
2 | return new Promise((resolve, reject) => {
3 | setTimeout(() => {
4 | resolve('token');
5 | }, 2000);
6 | });
7 | };
8 |
9 | export const logoutRequest = () => {
10 | return new Promise((resolve, reject) => {
11 | setTimeout(() => {
12 | resolve();
13 | }, 2000);
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/user-authentication/user-authentication-component.tsx:
--------------------------------------------------------------------------------
1 | import { Spinner } from '@/components/spinner';
2 | import { Button } from '@/components/ui/button';
3 | import {
4 | Card,
5 | CardContent,
6 | CardDescription,
7 | CardFooter,
8 | CardHeader,
9 | CardTitle,
10 | } from '@/components/ui/card';
11 | import { Input } from '@/components/ui/input';
12 | import { Label } from '@/components/ui/label';
13 |
14 | import { UserAuthenticationPropsFromRedux } from './user-authentication-container';
15 |
16 | export function UserAuthenticationComponent({
17 | isLoading,
18 | onLogin,
19 | }: UserAuthenticationPropsFromRedux) {
20 | return (
21 |
22 |
23 |
24 | Login
25 |
26 |
27 | Enter your email below to login to your account.
28 |
29 |
30 |
31 |
75 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/user-authentication/user-authentication-container.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import type { ConnectedProps } from 'react-redux';
3 | import { connect } from 'react-redux';
4 |
5 | import { RootState } from '@/redux/store';
6 |
7 | import { UserAuthenticationComponent } from './user-authentication-component';
8 | import { login, selectIsAuthenticating } from './user-authentication-reducer';
9 |
10 | const mapStateToProps = (state: RootState) => ({
11 | isLoading: selectIsAuthenticating(state),
12 | });
13 |
14 | const mapDispatchToProps = { onLogin: login };
15 |
16 | const connector = connect(mapStateToProps, mapDispatchToProps);
17 |
18 | export type UserAuthenticationPropsFromRedux = ConnectedProps;
19 |
20 | export default connector(UserAuthenticationComponent);
21 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/user-authentication/user-authentication-reducer.ts:
--------------------------------------------------------------------------------
1 | import type { PayloadAction } from '@reduxjs/toolkit';
2 | import { createSlice } from '@reduxjs/toolkit';
3 | import { prop } from 'ramda';
4 |
5 | import { clear } from '@/redux/clear';
6 |
7 | const initialState = { isAuthenticating: false, token: '' };
8 |
9 | export const {
10 | actions: { login, loginSucceeded, stopAuthenticating },
11 | name,
12 | reducer,
13 | selectors: { selectIsAuthenticating, selectAuthenticationToken },
14 | } = createSlice({
15 | name: 'userAuthentication',
16 | initialState,
17 | reducers: {
18 | login: (
19 | state,
20 | { payload }: PayloadAction<{ email: string; password: string }>,
21 | ) => {
22 | state.isAuthenticating = true;
23 | },
24 | loginSucceeded: (state, { payload }: PayloadAction<{ token: string }>) => {
25 | state.token = payload.token;
26 | },
27 | stopAuthenticating: state => {
28 | state.isAuthenticating = false;
29 | },
30 | },
31 | extraReducers: builder => {
32 | builder.addCase(clear, () => initialState);
33 | },
34 | selectors: {
35 | selectIsAuthenticating: prop<'isAuthenticating'>('isAuthenticating'),
36 | selectAuthenticationToken: prop<'token'>('token'),
37 | },
38 | });
39 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/user-authentication/user-authentication-saga.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '@reduxjs/toolkit';
2 | import { call, put, takeLeading } from 'redux-saga/effects';
3 |
4 | import { clear } from '@/redux/clear';
5 |
6 | import { handleFetchCurrentUsersProfile } from '../user-profiles/user-profiles-saga';
7 | import { loginRequest, logoutRequest } from './user-authentication-api';
8 | import {
9 | login,
10 | loginSucceeded,
11 | name,
12 | stopAuthenticating,
13 | } from './user-authentication-reducer';
14 |
15 | function* handleLogin({
16 | payload: { email, password },
17 | }: ReturnType) {
18 | try {
19 | const token: Awaited> = yield call(
20 | loginRequest,
21 | email,
22 | password,
23 | );
24 | yield put(loginSucceeded({ token }));
25 | yield call(handleFetchCurrentUsersProfile);
26 | } finally {
27 | yield put(stopAuthenticating());
28 | }
29 | }
30 |
31 | export function* watchLogin() {
32 | yield takeLeading(login.type, handleLogin);
33 | }
34 |
35 | export const logout = createAction(`${name}/logout`);
36 |
37 | export function* handleLogout() {
38 | yield put(clear());
39 | yield call(logoutRequest);
40 | }
41 |
42 | export function* watchLogout() {
43 | yield takeLeading(logout.type, handleLogout);
44 | }
45 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/user-profiles/user-profiles-api.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | import { UserProfile } from './user-profiles-types';
4 |
5 | export const getCurrentUserRequest = (
6 | token: string,
7 | ): Promise =>
8 | token
9 | ? axios
10 | .get(`https://jsonplaceholder.typicode.com/users/1`)
11 | .then(({ data }) => data)
12 | : new Promise(resolve => {
13 | setTimeout(() => {
14 | resolve(null);
15 | }, 1000);
16 | });
17 |
18 | export const getUsersRequest = (token: string) =>
19 | axios
20 | .get('https://jsonplaceholder.typicode.com/users')
21 | .then(({ data }) => data);
22 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/user-profiles/user-profiles-reducer.ts:
--------------------------------------------------------------------------------
1 | import type { PayloadAction } from '@reduxjs/toolkit';
2 | import {
3 | createEntityAdapter,
4 | createSelector,
5 | createSlice,
6 | } from '@reduxjs/toolkit';
7 | import { complement, isNil, prop } from 'ramda';
8 |
9 | import { clear } from '@/redux/clear';
10 | import { RootState } from '@/redux/store';
11 |
12 | import { UserProfile } from './user-profiles-types';
13 |
14 | const userProfilesAdapter = createEntityAdapter({
15 | sortComparer: (a, b) => a.email.localeCompare(b.email),
16 | });
17 |
18 | const initialState = userProfilesAdapter.getInitialState({
19 | currentUsersId: '',
20 | isLoading: true,
21 | });
22 |
23 | export const {
24 | actions: { currentUserProfileFetched, usersListFetched },
25 | name,
26 | reducer,
27 | selectSlice: selectUserProfileSlice,
28 | selectors: { selectCurrentUsersId, selectUserProfilesAreLoading },
29 | } = createSlice({
30 | name: 'userProfiles',
31 | initialState,
32 | reducers: {
33 | currentUserProfileFetched: (
34 | state,
35 | { payload }: PayloadAction,
36 | ) => {
37 | state.currentUsersId = payload.id;
38 | userProfilesAdapter.upsertOne(state, payload);
39 | },
40 | usersListFetched: (state, { payload }: PayloadAction) => {
41 | userProfilesAdapter.setMany(state, payload);
42 | state.isLoading = false;
43 | },
44 | },
45 | extraReducers: builder => {
46 | builder.addCase(clear, () => initialState);
47 | },
48 | selectors: {
49 | selectCurrentUsersId: prop<'currentUsersId'>('currentUsersId'),
50 | selectUserProfilesAreLoading: prop<'isLoading'>('isLoading'),
51 | },
52 | });
53 |
54 | const userProfileSelectors = userProfilesAdapter.getSelectors(
55 | selectUserProfileSlice,
56 | );
57 |
58 | const selectCurrentUsersProfile = (state: RootState) =>
59 | userProfileSelectors.selectById(state, selectCurrentUsersId(state));
60 |
61 | export const selectCurrentUsersName = (state: RootState) =>
62 | selectCurrentUsersProfile(state)?.name || 'Anonymous';
63 |
64 | export const selectIsAuthenticated = createSelector(
65 | selectCurrentUsersProfile,
66 | complement(isNil),
67 | );
68 |
69 | export const selectUsersList = userProfileSelectors.selectAll;
70 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/user-profiles/user-profiles-saga.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '@reduxjs/toolkit';
2 | import { call, put, select, takeLeading } from 'redux-saga/effects';
3 |
4 | import { selectAuthenticationToken } from '../user-authentication/user-authentication-reducer';
5 | import {
6 | currentUserProfileFetched,
7 | name,
8 | usersListFetched,
9 | } from '../user-profiles/user-profiles-reducer';
10 | import { getCurrentUserRequest, getUsersRequest } from './user-profiles-api';
11 |
12 | export function* handleFetchCurrentUsersProfile() {
13 | const token: ReturnType = yield select(
14 | selectAuthenticationToken,
15 | );
16 | const user: Awaited> = yield call(
17 | getCurrentUserRequest,
18 | token,
19 | );
20 |
21 | if (user) {
22 | yield put(currentUserProfileFetched(user));
23 | }
24 | }
25 |
26 | function* handleFetchUserProfiles() {
27 | const token: ReturnType = yield select(
28 | selectAuthenticationToken,
29 | );
30 | const users: Awaited> = yield call(
31 | getUsersRequest,
32 | token,
33 | );
34 |
35 | yield put(usersListFetched(users));
36 | }
37 |
38 | export const fetchUserProfiles = createAction(`${name}/fetchUserProfiles`);
39 |
40 | export function* watchFetchUserProfiles() {
41 | yield takeLeading(fetchUserProfiles.type, handleFetchUserProfiles);
42 | }
43 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/features/user-profiles/user-profiles-types.ts:
--------------------------------------------------------------------------------
1 | export type UserProfile = {
2 | /**
3 | * Email of the user.
4 | */
5 | email: string;
6 | /**
7 | * The users ID.
8 | */
9 | id: string;
10 | /**
11 | * Name of the user.
12 | */
13 | name: string;
14 | };
15 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/hocs/authenticated-page.ts:
--------------------------------------------------------------------------------
1 | import { compose } from '@reduxjs/toolkit';
2 |
3 | import withAuth from './with-auth';
4 | import withLoading from './with-loading';
5 |
6 | export default compose(withLoading, withAuth);
7 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/hocs/hoist-statics.ts:
--------------------------------------------------------------------------------
1 | import hoistNonReactStatics from 'hoist-non-react-statics';
2 |
3 | type HOC = (
4 | Component: React.ComponentType,
5 | ) => React.ComponentType;
6 |
7 | const hoistStatics =
8 | (
9 | higherOrderComponent: HOC,
10 | ): HOC =>
11 | (BaseComponent: React.ComponentType) => {
12 | const NewComponent = higherOrderComponent(BaseComponent);
13 | hoistNonReactStatics(NewComponent, BaseComponent);
14 | return NewComponent;
15 | };
16 |
17 | export default hoistStatics;
18 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/hocs/public-page.ts:
--------------------------------------------------------------------------------
1 | import { compose } from '@reduxjs/toolkit';
2 |
3 | import withLoading from './with-loading';
4 |
5 | export default compose(withLoading);
6 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/hocs/redirect-if-logged-in.ts:
--------------------------------------------------------------------------------
1 | import { selectIsAuthenticated } from '@/features/user-profiles/user-profiles-reducer';
2 |
3 | import redirect from './redirect';
4 |
5 | export default redirect(selectIsAuthenticated);
6 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/hocs/redirect.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/navigation';
2 | import { curry } from 'ramda';
3 | import { PropsWithChildren, useEffect } from 'react';
4 | import type { ConnectedProps } from 'react-redux';
5 | import { connect } from 'react-redux';
6 |
7 | import { RootState } from '@/redux/store';
8 |
9 | function redirect(predicate: (state: RootState) => boolean, path: string) {
10 | const isExternal = path.startsWith('http');
11 |
12 | const mapStateToProps = (
13 | state: RootState,
14 | ): { shouldRedirect: boolean } & Record => ({
15 | shouldRedirect: predicate(state),
16 | });
17 |
18 | const connector = connect(mapStateToProps);
19 |
20 | return function (
21 | Component: React.ComponentType>,
22 | ) {
23 | function Redirect({
24 | shouldRedirect,
25 | ...props
26 | }: PropsWithChildren>): JSX.Element {
27 | const router = useRouter();
28 |
29 | useEffect(() => {
30 | if (shouldRedirect) {
31 | if (isExternal && window) {
32 | window.location.assign(path);
33 | } else {
34 | router.push(path);
35 | }
36 | }
37 | }, [shouldRedirect, router]);
38 |
39 | return ;
40 | }
41 |
42 | return connector(Redirect);
43 | };
44 | }
45 |
46 | export default curry(redirect);
47 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/hocs/requires-permission/index.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { curry } from 'ramda';
3 | import { connect } from 'react-redux';
4 |
5 | import { RootState } from '@/redux/store';
6 |
7 | import { RequiresPermission } from './requires-permission-component';
8 |
9 | function requiresPermission(
10 | NotPermittedComponent: React.ComponentType,
11 | selector: (state: RootState) => boolean,
12 | PermittedComponent: React.ComponentType,
13 | ) {
14 | const mapStateToProps = (state: RootState) => ({
15 | NotPermittedComponent,
16 | PermittedComponent,
17 | isPermitted: selector(state),
18 | });
19 |
20 | return connect(mapStateToProps)(RequiresPermission);
21 | }
22 |
23 | export default curry(requiresPermission);
24 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/hocs/requires-permission/requires-permission-component.tsx:
--------------------------------------------------------------------------------
1 | interface Props {
2 | NotPermittedComponent: React.ComponentType;
3 | PermittedComponent: React.ComponentType;
4 | isPermitted: boolean;
5 | }
6 |
7 | export const RequiresPermission = (props: Props & A & B) => {
8 | const { NotPermittedComponent, PermittedComponent, isPermitted } = props;
9 |
10 | return isPermitted ? (
11 |
12 | ) : (
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/hocs/with-auth.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import UserAuthentication from '@/features/user-authentication/user-authentication-container';
3 | import { selectIsAuthenticated } from '@/features/user-profiles/user-profiles-reducer';
4 |
5 | import requiresPermission from './requires-permission';
6 |
7 | export default requiresPermission(UserAuthentication, selectIsAuthenticated);
8 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/hocs/with-loading.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import AppLoading from '@/features/app-loading/app-loading-container';
3 | import { selectAppFinishedLoading } from '@/features/app-loading/app-loading-reducer';
4 |
5 | import requiresPermission from './requires-permission';
6 |
7 | export default requiresPermission(AppLoading, selectAppFinishedLoading);
8 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/redux/clear.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '@reduxjs/toolkit';
2 |
3 | export const clear = createAction('all/clear');
4 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/redux/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector, useStore } from 'react-redux';
2 |
3 | import type { AppDispatch, AppStore, RootState } from './store';
4 |
5 | // Use throughout your app instead of plain `useDispatch` and `useSelector`.
6 | export const useAppDispatch = useDispatch.withTypes();
7 | export const useAppSelector = useSelector.withTypes();
8 | export const useAppStore = useStore.withTypes();
9 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/redux/root-reducer.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from '@reduxjs/toolkit';
2 |
3 | import {
4 | name as appLoadingSliceName,
5 | reducer as appLoadingReducer,
6 | } from '@/features/app-loading/app-loading-reducer';
7 | import {
8 | reducer as postsReducer,
9 | reducerPath as postsSliceName,
10 | } from '@/features/posts/posts-api';
11 | import {
12 | name as userAuthenticationSliceName,
13 | reducer as userAuthenticationReducer,
14 | } from '@/features/user-authentication/user-authentication-reducer';
15 | import {
16 | name as userProfileSliceName,
17 | reducer as userProfileReducer,
18 | } from '@/features/user-profiles/user-profiles-reducer';
19 |
20 | export const rootReducer = combineReducers({
21 | [appLoadingSliceName]: appLoadingReducer,
22 | [postsSliceName]: postsReducer,
23 | [userAuthenticationSliceName]: userAuthenticationReducer,
24 | [userProfileSliceName]: userProfileReducer,
25 | });
26 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/redux/root-saga.ts:
--------------------------------------------------------------------------------
1 | import { all } from 'redux-saga/effects';
2 |
3 | import { watchLoadApp } from '@/features/app-loading/app-loading-saga';
4 | import {
5 | watchLogin,
6 | watchLogout,
7 | } from '@/features/user-authentication/user-authentication-saga';
8 | import { watchFetchUserProfiles } from '@/features/user-profiles/user-profiles-saga';
9 |
10 | export function* rootSaga() {
11 | yield all([
12 | watchFetchUserProfiles(),
13 | watchLoadApp(),
14 | watchLogin(),
15 | watchLogout(),
16 | ]);
17 | }
18 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/redux/store-provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useRef } from 'react';
3 | import { Provider } from 'react-redux';
4 |
5 | import { AppStore, makeStore } from './store';
6 |
7 | export default function StoreProvider({
8 | children,
9 | }: {
10 | children: React.ReactNode;
11 | }) {
12 | const storeRef = useRef();
13 |
14 | if (!storeRef.current) {
15 | // Creates the store instance the first time this renders.
16 | const store = makeStore();
17 | storeRef.current = store;
18 | }
19 |
20 | return {children};
21 | }
22 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/src/redux/store.ts:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit';
2 | import createSagaMiddleware from 'redux-saga';
3 |
4 | import { middleware as postsMiddleware } from '@/features/posts/posts-api';
5 |
6 | import { rootReducer } from './root-reducer';
7 | import { rootSaga } from './root-saga';
8 |
9 | export const makeStore = () => {
10 | const sagaMiddleware = createSagaMiddleware();
11 | const store = configureStore({
12 | reducer: rootReducer,
13 | middleware: getDefaultMiddleware =>
14 | getDefaultMiddleware().concat(sagaMiddleware, postsMiddleware),
15 | });
16 | sagaMiddleware.run(rootSaga);
17 | return store;
18 | };
19 |
20 | // Infer the type of makeStore's store.
21 | export type AppStore = ReturnType;
22 |
23 | // Infer the `RootState` and `AppDispatch` types from the store itself.
24 | export type RootState = ReturnType;
25 | export type AppDispatch = AppStore['dispatch'];
26 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss';
2 |
3 | const config: Config = {
4 | darkMode: ['class'],
5 | content: [
6 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
7 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
8 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
9 | './src/features/**/*.{js,ts,jsx,tsx,mdx}', // Make sure to include this!
10 | ],
11 | theme: {
12 | extend: {
13 | backgroundImage: {
14 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
15 | 'gradient-conic':
16 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
17 | },
18 | borderRadius: {
19 | lg: 'var(--radius)',
20 | md: 'calc(var(--radius) - 2px)',
21 | sm: 'calc(var(--radius) - 4px)',
22 | },
23 | colors: {
24 | background: 'hsl(var(--background))',
25 | foreground: 'hsl(var(--foreground))',
26 | card: {
27 | DEFAULT: 'hsl(var(--card))',
28 | foreground: 'hsl(var(--card-foreground))',
29 | },
30 | popover: {
31 | DEFAULT: 'hsl(var(--popover))',
32 | foreground: 'hsl(var(--popover-foreground))',
33 | },
34 | primary: {
35 | DEFAULT: 'hsl(var(--primary))',
36 | foreground: 'hsl(var(--primary-foreground))',
37 | },
38 | secondary: {
39 | DEFAULT: 'hsl(var(--secondary))',
40 | foreground: 'hsl(var(--secondary-foreground))',
41 | },
42 | muted: {
43 | DEFAULT: 'hsl(var(--muted))',
44 | foreground: 'hsl(var(--muted-foreground))',
45 | },
46 | accent: {
47 | DEFAULT: 'hsl(var(--accent))',
48 | foreground: 'hsl(var(--accent-foreground))',
49 | },
50 | destructive: {
51 | DEFAULT: 'hsl(var(--destructive))',
52 | foreground: 'hsl(var(--destructive-foreground))',
53 | },
54 | border: 'hsl(var(--border))',
55 | input: 'hsl(var(--input))',
56 | ring: 'hsl(var(--ring))',
57 | chart: {
58 | '1': 'hsl(var(--chart-1))',
59 | '2': 'hsl(var(--chart-2))',
60 | '3': 'hsl(var(--chart-3))',
61 | '4': 'hsl(var(--chart-4))',
62 | '5': 'hsl(var(--chart-5))',
63 | },
64 | },
65 | },
66 | },
67 | plugins: [require('tailwindcss-animate')],
68 | };
69 | export default config;
70 |
71 | /* eslint-disable */
72 | type MyFancyApi = {
73 | despair: boolean;
74 | }
75 |
76 | function youSpentOneWeekOnThis({
77 | despair = true,
78 | }: MyFancyApi) {
79 | return "Please help!!!1"
80 | }
81 |
82 |
83 | youSpentOneWeekOnThis({despair: true});
84 |
85 |
86 |
87 | /* eslint-enable */
88 |
--------------------------------------------------------------------------------
/examples/2024-09-01-redux-for-production/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------