09 Order History & User Profile

09 Order History & User Profile
😀

1. Section Intro

Now we are going to create the user area. This will be where users can see their orders as well as update their profile. 现在我们将创建用户区域。这里是用户查看订单和更新个人资料的地方。
We'll start with the user area layout and the menu, which will have the view orders and update profile links. 我们将从用户区域布局和菜单开始,菜单中将包含查看订单和更新个人资料的链接。
We'll create the user orders page and the action to get all of their orders and then add pagination ability to it. 我们将创建用户订单页面和获取所有订单的操作,然后为其添加分页功能。
The user profile will be really simple, but I want to let the user change their name so we will first create an action to update their profile. 用户个人资料会非常简单,但我想让用户能够更改他们的姓名,所以我们首先将创建一个更新个人资料的操作。
Then we'll create the update user profile form. 然后我们将创建更新用户个人资料的表单。
Overall, this should be a pretty easy section to get through. 总的来说,这应该是一个相当容易完成的章节。

2. User Layout & Menu 用户布局和菜单

Now that we can make payment on items, I want to have an order history page. Before we do that though, we will create a layout for the user pages. The user pages will be the order history and user profile. 既然我们可以对商品进行支付了,我想创建一个订单历史页面。不过在那之前,我们将为用户页面创建一个布局。用户页面将包括订单历史和用户个人资料。

User Layout 用户布局

Let's create the layout. Create a file at app/user/layout.tsx and add the following code: 让我们创建布局。在 app/user/layout.tsx 创建一个文件并添加以下代码:
So we have a customized version of the header. We have the user navigation on the left as with the other pages and we have a placeholder for the new user main menu. Which we will create soon. 所以我们有一个自定义版本的头部。我们在左侧有与其他页面一样的用户导航,并且有一个新的用户主菜单的占位符。我们很快就会创建它。

Orders Page Test 订单页面测试

Now let's create the orders page so we can test the layout. 现在让我们创建订单页面,以便测试布局。
Create a file at app/user/orders/page.tsx and add the following code: 在 app/user/orders/page.tsx 创建一个文件并添加以下代码:
Since this page is in the user folder, it should use the layout. So go to the browser and navigate to /user/orders. You should see the orders page. 由于此页面位于用户文件夹中,它应该使用该布局。所以转到浏览器并导航到 /user/orders。你应该能看到订单页面。
同时创建Profile页面,/user/profile/page.tsx

User Main Nav 用户主导航

We are going to have a main menu for the user area (orders, profile). Create a new file at app/user/main-nav.tsx and make it a client component and add the following code: 我们将为用户区域(订单、个人资料)创建一个主菜单。在 app/user/main-nav.tsx 创建一个新文件,使其成为客户端组件并添加以下代码:
We are importing the usePathname hook from next/navigation to get the current pathname. We are also importing the cn function from @/lib/utils to conditionally add classes to the navigation. We are also importing the Link component from next/link to create links. Finally, we're importing React from react because we are going to be using the React.HTMLAttributes type to define the props for the component. This way you can pass any of your standard attributes like class, id, style, etc to this component. 我们从 next/navigation 导入 usePathname 钩子来获取当前路径名。我们还从 @/lib/utils 导入 cn 函数来有条件地向导航添加类。我们还从 next/link 导入 Link 组件来创建链接。最后,我们从 react 导入 React,因为我们将使用 React.HTMLAttributes 类型来定义组件的 props。这样你就可以向该组件传递任何标准属性,如 class、id、style 等。
The component takes in a className prop that we can use to add classes to the navigation. We are also using the ...props to pass through any other props that are passed to the component. So if you want to pass an id or another attribute to the navigation, you can do so. 该组件接受一个 className prop,我们可以用它来向导航添加类。我们还使用 ...props 来传递传递给该组件的任何其他 props。所以如果你想向导航传递一个 id 或另一个属性,你可以这样做。
Then we are just intializing the pathname variable to the usePathname hook. 然后我们只是将 pathname 变量初始化为 usePathname 钩子。
Now let's add an array of links under the imports: 现在让我们在导入下方添加一个链接数组:
We have a link for the profile and orders page. 我们有个人资料和订单页面的链接。
In the return, add the following code: 在 return 中,添加以下代码:
We are mapping over the links array and creating a link for each item. We are also using the pathname variable to conditionally add a class to the link. If the pathname includes the href of the link, we are adding an empty string to the class. Otherwise, we are adding the text-muted-foreground class. 我们正在遍历 links 数组并为每个项目创建一个链接。我们还使用 pathname 变量有条件地向链接添加类。如果路径名包含链接的 href,我们向类添加一个空字符串。否则,我们添加 text-muted-foreground 类。

Add Menu To Layout 将菜单添加到布局

Now open the app/user/layout.tsx file and add the following below the closing </Link> tag where the comment is: 现在打开 app/user/layout.tsx 文件,并在注释所在的闭合 </Link> 标签下方添加以下内容:
Ne sure to import it: 确保导入它:
Now you should see the links. Click on the orders link and you should see the orders page. 现在你应该能看到链接了。点击订单链接,你应该能看到订单页面。

Add Links To User Button 将链接添加到用户按钮

We need a way to get to this page from other areas. Open the components/shared/header/user-button.tsx file and add the following above the <DropdownMenuItem> for the sign out: 我们需要一种从其他区域访问此页面的方法。打开 components/shared/header/user-button.tsx 文件,并在退出的 <DropdownMenuItem> 上方添加以下内容:
You should see the link in the dropdown menu. Click on the order history link and you should be taken to the orders page. 你应该能在下拉菜单中看到链接。点击订单历史链接,你应该会被带到订单页面。

3. Get My Orders Action 获取我的订单操作

In the last lesson, we created the orders page but that's it. We did not add anything to it except a return with some text to test the page and layout. 在上一课中,我们创建了订单页面,但仅此而已。除了返回一些文本来测试页面和布局外,我们没有添加任何其他内容。
Before we add to the orders page, let's create the action that gets the user's orders from the database. We are also going to implement pagination. 在为订单页面添加内容之前,让我们创建一个从数据库获取用户订单的操作。我们还将实现分页功能。

PAGE_SIZE Constant 页面大小常量

Since we will be using pagination, let's create a new constant in the constants/index.ts file: 由于我们将使用分页功能,让我们在 constants/index.ts 文件中创建一个新常量:
We are saying look at the PAGE_SIZE environment variable and if it is not set, use 2. The reason that I set it so low is so that we can test it. Normally, you would want to set it to something like 10 or 20. 我们的意思是查看 PAGE_SIZE 环境变量,如果未设置,则使用 2。我将其设置得如此之低的原因是为了方便测试。通常情况下,你应该将其设置为 10 或 20 之类的数值。
Open the file lib/actions/order.actions.tsx and import the PAGE_SIZE constant: 打开文件 lib/actions/order.actions.tsx 并导入 PAGE_SIZE 常量:
Now create the getMyOrders function: 现在创建 getMyOrders 函数:
This function takes in a limit and page number. The limit is the number of orders to return and the page is the page number. The page number is used to calculate the offset. The offset is the number of orders to skip. We are returning the data and the total number of pages so that we can use it to calculate the pagination. 该函数接受一个限制数量和页码。限制数量是要返回的订单数量,页码是页码。页码用于计算偏移量。偏移量是要跳过的订单数量。我们返回数据和总页数,以便可以使用它们来计算分页。
In the next lesson, we will create the page that uses this action. 在下一课中,我们将创建使用此操作的页面。

4. User Orders Page 用户订单页面

Now we want to create the page to show the user's orders. 现在我们要创建一个显示用户订单的页面。
Open the file app/user/orders/page.tsx and replace the contents with the following: 打开文件 app/user/orders/page.tsx 并将内容替换为以下内容:
We added all our imports, some metadata and passed in searchParams as a prop. We then set the page number to 1 if it's not provided and fetched the orders using the getMyOrders function. The getMyOrders function takes in the page number as a parameter. You could also pass in a limit to override whatever is in the PAGE_SIZE constant. 我们添加了所有导入、一些元数据,并将 searchParams 作为属性传入。然后我们将页码设置为 1(如果未提供),并使用 getMyOrders 函数获取订单。getMyOrders 函数接受页码作为参数。你还可以传入一个限制值来覆盖 PAGE_SIZE 常量中的任何设置。
We then logged the orders to the console and returned a placeholder text. You should see the orders in the server console. 然后我们将订单记录到控制台,并返回一个占位符文本。你应该能在服务器控制台中看到订单。
Now let's create the UI for the orders page. Replace the return statement with the following: 现在让我们为订单页面创建 UI。将 return 语句替换为以下内容:
This is pretty straightforward. We're using the Table component from the @/components/ui/table package to display the orders in a table. We're also using the formatCurrency and formatDateTime functions from the @/lib/utils package to format the currency and date. 这非常简单。我们使用来自 @/components/ui/table 包的 Table 组件在表格中显示订单。我们还使用来自 @/lib/utils 包的 formatCurrencyformatDateTime 函数来格式化货币和日期。
Now you should see the orders in the table. 现在你应该能在表格中看到订单了。
Next, let's add the pagination. 接下来,让我们添加分页功能。

5. Orders Pagination 订单分页

Now we will add the pagination to the orders. We already have a PAGE_SIZE constant in the constants.js file. I have it set to 2. So I am going to go through the app and make sure I have at least 3 orders. You should do the same. 现在我们将为订单添加分页功能。我们在 constants.js 文件中已经有了 PAGE_SIZE 常量。我将其设置为 2。所以我要在应用中确保至少有 3 个订单。你也应该这样做。

Pagination Component 分页组件

We are going to create a pagination component. Create a file at components/shared/pagination.tsx and add the following code: 我们将创建一个分页组件。在 components/shared/pagination.tsx 创建一个文件,并添加以下代码:
We are going to use the useRouter and useSearchParams hooks from Next.js. We are going to pass in the page, totalPages, and urlParamName props. We are going to use the useRouter hook to get the router object. We are going to use the useSearchParams hook to get the search params object. 我们将使用 Next.js 中的 useRouteruseSearchParams hooks。我们将传入 pagetotalPagesurlParamName 属性。我们将使用 useRouter hook 获取路由对象。我们将使用 useSearchParams hook 获取搜索参数对象。
Then we return a div with two buttons. The first button is disabled if the page is less than or equal to 1. The second button is disabled if the page is greater than or equal to the total pages. 然后我们返回一个带有两个按钮的 div。如果页码小于或等于 1,则第一个按钮被禁用。如果页码大于或等于总页数,则第二个按钮被禁用。
Before we add the rest of the functionality, let's add the component to the orders page. 在添加其余功能之前,让我们将组件添加到订单页面。
Open the app/user/orders/page.tsx file and import the pagination component: 打开 app/user/orders/page.tsx 文件并导入分页组件:
Now under the closing </Table> element, add the following code: 现在在闭合的 </Table> 元素下方添加以下代码:
We are checking to see if there are more than 1 page of orders. If there are, we are going to render the pagination component. We are going to pass in the page and totalPages. 我们正在检查是否有超过 1 页的订单。如果有,我们将渲染分页组件。我们将传入 pagetotalPages
You should now see 2 orders and the buttons on the orders page. Right now, clicking the buttons will not do anything. We will add that functionality now. 现在你应该在订单页面上看到 2 个订单和按钮。现在,点击按钮不会有任何作用。我们现在将添加该功能。

Pagination Functionality 分页功能

Add the following click handler above the return statement: 在 return 语句上方添加以下点击处理程序:
This function takes in a btnType parameter. If the btnType is next, we increment the page number by 1. If the btnType is prev, we decrement the page number by 1. We then log the new page number to the console. 这个函数接受一个 btnType 参数。如果 btnTypenext,我们将页码加 1。如果 btnTypeprev,我们将页码减 1。然后我们将新页码记录到控制台。
Add the handler to the buttons: 将处理程序添加到按钮:
Click on the next button and you should see the page number in the console. 点击下一页按钮,你应该能在控制台中看到页码。
We need to build the URL to navigate to. To keep this clean, I'm going to create a utility function for this. We're also going to use the query-string package to build the URL. This package is used to parse and stringify query strings. 我们需要构建要导航到的 URL。为了保持代码整洁,我将为此创建一个工具函数。我们还将使用 query-string 包来构建 URL。这个包用于解析和字符串化查询字符串。
Open your terminal and type the following command: 打开终端并输入以下命令:
Open the lib/utils.ts file and add the following import: 打开 lib/utils.ts 文件并添加以下导入:
Now add the following function to the file: 现在将以下函数添加到文件中:
This function takes in 3 parameters: 这个函数接受 3 个参数:
  • params: a string representing the current URL’s query parameters. For instance, if we have the URL https://example.com?page=1&limit=10, the params would be page=1&limit=10.
    • params:一个表示当前 URL 查询参数的字符串。例如,如果我们有 URL https://example.com?page=1&limit=10,那么 params 将是 page=1&limit=10
  • key: a string representing the key to be updated in the query parameters. For instance, if we want to update the page parameter, the key would be page. That's what it will be by default.
    • key:一个表示要在查询参数中更新的键的字符串。例如,如果我们要更新 page 参数,那么 key 将是 page。这就是默认值。
  • value: a string representing the new value to be set for the key. For instance, if we want to update the page parameter to 2, the value would be 2.
    • value:一个表示要为该键设置的新值的字符串。例如,如果我们要将 page 参数更新为 2,那么 value 将是 2
The function first parses the current URL's query parameters using qs.parse(params). It then updates the specified key with the new value. Finally, it uses qs.stringifyUrl to build the new URL with the updated query parameters. 该函数首先使用 qs.parse(params) 解析当前 URL 的查询参数。然后它用新值更新指定的键。最后,它使用 qs.stringifyUrl 构建带有更新后查询参数的新 URL。
The skipNull option in qs.stringifyUrl (from the qs library) ensures that query parameters with null values are omitted from the generated URL string. qs.stringifyUrl 中的 skipNull 选项(来自 qs 库)确保具有 null 值的查询参数从生成的 URL 字符串中被省略。
Now bring the function into the components/shared/pagination.tsx file and add the following import: 现在将函数引入 components/shared/pagination.tsx 文件并添加以下导入:
In the onClick function, add the following code: 在 onClick 函数中,添加以下代码:
This will build the URL with the new page number. Then we are just pushing the new URL to the router. 这将构建带有新页码的 URL。然后我们只需将新 URL 推送到路由。
You should now be able to click the next and previous buttons and see the page number change and see the correct orders. 现在你应该能够点击下一页和上一页按钮,看到页码变化并看到正确的订单。
Now let's change the PAGE_SIZE constant to 10 in the lib/constants/index.ts file. Now you won't see the pagination until there are at least 11 orders. 现在让我们将 lib/constants/index.ts 文件中的 PAGE_SIZE 常量更改为 10。现在,直到至少有 11 个订单时,你才会看到分页。

6. Update User Profile Action 更新用户资料操作

We are going to have a profile form where we can update the name. So I want to create the Zod schems and the action for that. 我们将创建一个可以更新姓名的个人资料表单。所以我要为此创建 Zod 验证和操作。

User Profile Schema 用户资料验证模式

Open the file lib/validators.ts and add the following code: 打开文件 lib/validators.ts 并添加以下代码:

Action 操作

Open the lib/actions/user.actions.ts file and add the following code: 打开 lib/actions/user.actions.ts 文件并添加以下代码:
This is pretty simple. We are just getting the session, finding the user, and updating the name field. We are also returning a success message if everything goes well. 这非常简单。我们只是获取会话,查找用户,然后更新姓名字段。如果一切顺利,我们还会返回一个成功消息。

7. User Profile Form 用户资料表单

Now we will create the profile page and form. 现在我们将创建个人资料页面和表单。
If you did not create a page for the profile yet, create a new file at app/user/profile/page.tsx and add the following: 如果你还没有创建个人资料页面,请在 app/user/profile/page.tsx 创建一个新文件,并添加以下内容:
Now if you click on the profile option in the user menu, you should see the heading. 现在如果你点击用户菜单中的个人资料选项,你应该能看到标题。

Session Provider 会话提供者

In order to get the user info from the session within the embedded client component that we're going to create, we need to use the SessionProvider from Next Auth. 为了在我们要创建的嵌入式客户端组件中从会话获取用户信息,我们需要使用 Next Auth 中的 SessionProvider
Add the following imports: 添加以下导入:
We are bringing in the redirect function from next/navigation to redirect the user to the login page if they are not authenticated. We are also bringing in the SessionProvider from next-auth/react to provide the session to the page. We are also bringing in the auth function from @/auth to get the session. 我们从 next/navigation 引入 redirect 函数,以便在用户未经过身份验证时将其重定向到登录页面。我们还从 next-auth/react 引入 SessionProvider,以便为页面提供会话。我们还从 @/auth 引入 auth 函数来获取会话。
Now wrap the return in the SessionProvider: 现在用 SessionProvider 包裹 return 语句:
You can get to this page through the user menu in the user button component. You should see the user name in the page. You can delete the test user line. 你可以通过用户按钮组件中的用户菜单进入该页面。你应该能在页面中看到用户名。你可以删除测试用户行。

User Profile Form 用户资料表单

Now we want to create the form to update the user profile. Create a new file at app/user/profile/profile-form.tsx and add the following code: 现在我们要创建用于更新用户资料的表单。在 app/user/profile/profile-form.tsx 创建一个新文件,并添加以下代码:
We are using the useSession hook to get the user session. We are using React Hook Form and the useForm hook to create the form. We are using the zodResolver to validate the form. We are passing in the default values from the session and we are using the nullish coalescing operator to set the default values to an empty string if the session is null. 我们使用 useSession hook 来获取用户会话。我们使用 React Hook Form 和 useForm hook 来创建表单。我们使用 zodResolver 来验证表单。我们从会话中传入默认值,并使用空值合并运算符在会话为 null 时将默认值设置为空字符串。
Then we construct the form using various components from ShadCN. I made the email field disabled because we don't want the user to be able to change their email at this point. 然后我们使用 ShadCN 的各种组件构建表单。我将电子邮件字段设置为禁用,因为我们不希望用户在此时更改他们的电子邮件。
The form will not submit just yet. I just want to get it displayed on the page. 表单现在还不能提交。我只是想让它显示在页面上。

8. Profile Form Submission 表单提交

Now let's have the form submit to the action. 现在让我们让表单提交到操作。
Add the following code to the onSubmit handler: 将以下代码添加到 onSubmit 处理程序:
In the function, we are calling the updateProfile action with the form values. If the action is successful, we are updating the session and showing a success message. If the action fails, we are showing an error message. 在函数中,我们用表单值调用 updateProfile 操作。如果操作成功,我们会更新会话并显示成功消息。如果操作失败,我们会显示错误消息。
Go ahead and try it out and you should be able to submit the form and update the user name. 继续尝试,你应该能够提交表单并更新用户名。
If you check the database and the name updates but if you refresh the form and the session still has the old name, then you need to open the auth.ts file and add the following to the bottom of the jwt callback abover the return: 如果你检查了数据库,名字已经更新,但刷新表单后会话仍然显示旧名字,那么你需要打开 auth.ts 文件,在 jwt 回调的 return 语句上方添加以下内容:
We need this because the session data comes from the token and we need to check for an update in the session and also update the token. Otherwise, you will not see the change on the client. 我们需要这个是因为会话数据来自 token,我们需要检查会话的更新并同时更新 token。否则,你在客户端看不到变化。

📎 参考文章

  • 一些引用
  • 引用文章
 
💡
欢迎您在底部评论区留言,一起交流~
上一篇
10 Admin Overview & Orders
下一篇
08 PayPal Payments
Loading...