From 2a4b5fba682eee31ba41f22747afcb611e8ee431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Ag=C3=BCero?= <54730752+EmmanuelAR@users.noreply.github.com> Date: Sun, 24 Nov 2024 23:06:57 -0600 Subject: [PATCH 01/27] [Documentation] Add first version of FAQ file (#66) --- FAQ.md | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 33 -------------------- 2 files changed, 91 insertions(+), 33 deletions(-) create mode 100644 FAQ.md diff --git a/FAQ.md b/FAQ.md new file mode 100644 index 0000000..58325d8 --- /dev/null +++ b/FAQ.md @@ -0,0 +1,91 @@ +# πŸŽ‰ Frequently Asked Questions (FAQ) πŸŽ‰ + +Welcome to the **CofiBlocks FAQ** section! Here you’ll find answers to the most common questions about our platform, including its purpose, setup, features, and how to contribute. If you’re new here, this is a great place to start! πŸš€ + +## πŸ“š Sections +1. [General Questions](#general-questions) +2. [Community Questions](#community-questions) +3. [Technical Questions](#technical-questions) +4. [Contribution Questions](#contribution-questions) + +## General Questions + +### 1. How to get started? πŸ€” + +To begin understanding the **CofiBlocks** project, we recommend following these steps: + +1. **πŸ“„ Read the main README file:** + Get a comprehensive explanation of the project’s purpose and scope. You can find it [here](./README.md). + +2. **πŸ‘₯ Review the Community Guidelines:** + Familiarize yourself with the community standards by reading the [Community Guidelines](./COMMUNITY_GUIDELINES.md). + +3. **πŸ” Understand the project structure:** + The project is divided into two main parts: + - **Backend:** Learn more by reading the [backend README](./apps/snfoundry/README.md). + - **Frontend:** Discover details by checking the [frontend README](./apps/web/README.md). + +## Community Questions + +Discover how to be an active and respectful member of the **CofiBlocks** community! Learn about our values, expectations, and how to contribute positively to our mission. 🌍 + +[πŸ“– Read the full Community Guidelines here.](./COMMUNITY_GUIDELINES.md) + +### 1. 🀝 What is a Collaborative Business? +A Collaborative Business is an organization where customers, producers, and supporters become members. These members share responsibility, decision-making, and benefits (both monetary and otherwise). Our model represents an evolution of the cooperative and DAO (Decentralized Autonomous Organization) concepts, leveraging blockchain technology, smart contracts, and crypto assets (such as NFTs and social currencies). + +### 2. β˜• How CofiBlocks Business Works? + +#### 🏑 Cofi in Your Home +Subscribers receive freshly roasted coffee delivered to their doorsteps monthly, ensuring they enjoy the finest brews while supporting local producers. + +#### βš–οΈ Cofi is Fair +Producers receive fair payments upfront, allowing them to plan and understand demand ahead of time, thanks to our subscription model. + +#### 🎨 Cofi Art & NFTs +In its pilot year, CofiBlocks will create a collection of NFTs featuring original artwork, distributed via coffee labels on each bag sold. + +#### πŸͺ™ Cofi Complementary Currency +Members of the CofiBlocks pilot community on the Cambiatus app can claim their COFI tokensβ€”a complementary blockchain currency that ensures participation in the Collaborative Business's benefits. + +#### πŸ“ˆ Cofi Distribution +At the end of the first year, all CofiBlocks members will receive a percentage of the pilot's financial results in cryptocurrency based on their accumulated COFI tokens. Members will also have the opportunity to participate in decision-making for future stages of the project. + +#### πŸ”— CofiBlocks & Cambiatus + +CofiBlocks is a proud member of Cambiatus's Social Currency and Collaborative Business Ecosystem. + +[🌐 Learn More About Cambiatus](https://cambiatus.com) + +#### πŸš€ CofiBlocks & Starknet + +We're building our Web3 marketplace using Starknet technology. + +[πŸ’» Explore on GitHub](https://github.com/Vagabonds-Labs/marketplace) + +### 3. πŸ—£οΈ How Can I Join the Community Conversations? +Connect with us on our community group chats to discuss ideas, share feedback, and engage with other members! + +[πŸ’¬ Join the Community Here](https://github.com/Vagabonds-Labs/cofiblocks?tab=readme-ov-file#-join-the-community) + +## Technical Questions + +### 1. How can I run the backend locally? πŸ–₯️ + +You can run the **CofiBlocks** backend locally by following the steps in the [backend README](./apps/snfoundry/README.md). + +### 2. How can I run the frontend locally? 🌐 + +To run the **CofiBlocks** frontend locally, follow the steps in the [frontend README](./apps/web/README.md). + +### 3. Where can I find development resources? πŸ“š + +Explore all the resources, tools, and guides for developers working on **CofiBlocks** in our [Development Resources](https://github.com/Vagabonds-Labs/cofiblocks?tab=readme-ov-file#-development-resources) section. + +## Contribution Questions + +Find everything you need to know about contributing to **CofiBlocks**, from submitting pull requests to following our coding standards. πŸ“ˆ + +[Check the Contribution Guide here.](./pull_request_template.md) + +We’re excited to have you on board! Let’s build something amazing together. πŸ’ͺπŸ’₯ \ No newline at end of file diff --git a/README.md b/README.md index 22b7593..da6cce2 100644 --- a/README.md +++ b/README.md @@ -2,39 +2,6 @@ CofiBlocks is the first Collaborative Business connecting traditional coffee-growing communities in Costa Rica and worldwide directly with coffee lovers using Starknet blockchain technology. Our mission is to distribute benefits among all members, ensuring fair trade, community engagement, and technological innovation. -## 🌍 What is a Collaborative Business? - -A Collaborative Business is an organization where customers, producers, and supporters become members. These members share responsibility, decision-making, and benefits (both monetary and otherwise). Our model represents an evolution of the cooperative and DAO (Decentralized Autonomous Organization) concepts, leveraging blockchain technology, smart contracts, and crypto assets (such as NFTs and social currencies). - -## 🌟 How Does It Work? - -### Cofi in Your Home -Subscribers receive freshly roasted coffee delivered to their doorsteps monthly, ensuring they enjoy the finest brews while supporting local producers. - -### Cofi is Fair -Producers receive fair payments upfront, allowing them to plan and understand demand ahead of time, thanks to our subscription model. - -### Cofi Art & NFTs -In its pilot year, CofiBlocks will create a collection of NFTs featuring original artwork, distributed via coffee labels on each bag sold. - -### Cofi Complementary Currency -Members of the CofiBlocks pilot community on the Cambiatus app can claim their COFI tokensβ€”a complementary blockchain currency that ensures participation in the Collaborative Business's benefits. - -### Cofi Distribution -At the end of the first year, all CofiBlocks members will receive a percentage of the pilot's financial results in cryptocurrency based on their accumulated COFI tokens. Members will also have the opportunity to participate in decision-making for future stages of the project. - -## πŸ”— CofiBlocks & Cambiatus - -CofiBlocks is a proud member of Cambiatus's Social Currency and Collaborative Business Ecosystem. - -[Learn More About Cambiatus](https://cambiatus.com) - -## πŸš€ CofiBlocks & Starknet - -We're building our Web3 marketplace using Starknet technology. - -[Explore on GitHub](https://github.com/Vagabonds-Labs/marketplace) - ## πŸ› οΈ Getting Started ### Prerequisites From 8fd57372232e1852d4b3beee6ea7ba2e165127e6 Mon Sep 17 00:00:00 2001 From: pedroco3lho Date: Sun, 24 Nov 2024 21:18:19 -0300 Subject: [PATCH 02/27] feat: add types folder --- apps/web/src/app/user/my-orders/page.tsx | 53 ++++++++---------------- apps/web/src/types/index.ts | 17 ++++++++ 2 files changed, 34 insertions(+), 36 deletions(-) create mode 100644 apps/web/src/types/index.ts diff --git a/apps/web/src/app/user/my-orders/page.tsx b/apps/web/src/app/user/my-orders/page.tsx index 5ede822..4b53e18 100644 --- a/apps/web/src/app/user/my-orders/page.tsx +++ b/apps/web/src/app/user/my-orders/page.tsx @@ -7,28 +7,10 @@ import CheckBox from "@repo/ui/form/checkBox"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; -import { z } from "zod"; import OrderListItem from "~/app/_components/features/OrderListItem"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; import BottomModal from "~/app/_components/ui/BottomModal"; - -const SalesStatusEnum = { - Paid: "Paid", - Prepared: "Prepared", - Shipped: "Shipped", - Delivered: "Delivered", -} as const; - -type SalesStatus = (typeof SalesStatusEnum)[keyof typeof SalesStatusEnum]; - -const filtersSchema = z.object({ - statusPaid: z.boolean().optional(), - statusPrepared: z.boolean().optional(), - statusShipped: z.boolean().optional(), - statusDelivered: z.boolean().optional(), -}); - -type FormValues = z.infer; +import { type FormValues, SalesStatus, filtersSchema } from "~/types"; const mockedOrders = [ { @@ -38,13 +20,13 @@ const mockedOrders = [ id: "1", productName: "Edit profile", sellerName: "seller1_fullname", - status: SalesStatusEnum.Paid as SalesStatus, + status: SalesStatus.Paid, }, { id: "2", productName: "My Orders", sellerName: "seller2_fullname", - status: SalesStatusEnum.Paid as SalesStatus, + status: SalesStatus.Paid, }, ], }, @@ -55,13 +37,13 @@ const mockedOrders = [ id: "3", productName: "productName", sellerName: "seller1_fullname", - status: SalesStatusEnum.Delivered as SalesStatus, + status: SalesStatus.Delivered, }, { id: "4", productName: "productName", sellerName: "seller2_fullname", - status: SalesStatusEnum.Delivered as SalesStatus, + status: SalesStatus.Delivered, }, ], }, @@ -119,14 +101,13 @@ export default function MyOrders() { ]; const matchesStatus = activeStatusFilters.some(Boolean) - ? (activeFilters.statusPaid && - item.status === SalesStatusEnum.Paid) ?? + ? (activeFilters.statusPaid && item.status === SalesStatus.Paid) ?? (activeFilters.statusPrepared && - item.status === SalesStatusEnum.Prepared) ?? + item.status === SalesStatus.Prepared) ?? (activeFilters.statusShipped && - item.status === SalesStatusEnum.Shipped) ?? + item.status === SalesStatus.Shipped) ?? (activeFilters.statusDelivered && - item.status === SalesStatusEnum.Delivered) + item.status === SalesStatus.Delivered) : true; return matchesSearch && matchesStatus; @@ -197,26 +178,26 @@ export default function MyOrders() {
<>


diff --git a/apps/web/src/types/index.ts b/apps/web/src/types/index.ts new file mode 100644 index 0000000..40dd091 --- /dev/null +++ b/apps/web/src/types/index.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +export enum SalesStatus { + Paid = "Paid", + Prepared = "Prepared", + Shipped = "Shipped", + Delivered = "Delivered", +} + +export const filtersSchema = z.object({ + statusPaid: z.boolean().optional(), + statusPrepared: z.boolean().optional(), + statusShipped: z.boolean().optional(), + statusDelivered: z.boolean().optional(), +}); + +export type FormValues = z.infer; From b5119404b055f4bfc000139055dbb405ea576b16 Mon Sep 17 00:00:00 2001 From: pedroco3lho Date: Mon, 25 Nov 2024 00:15:11 -0300 Subject: [PATCH 03/27] feat: centralize types --- apps/web/src/app/user-profile/page.tsx | 13 +-- apps/web/src/app/user/edit-profile/page.tsx | 9 +- apps/web/src/app/user/my-claims/[id]/page.tsx | 16 +--- apps/web/src/app/user/my-claims/page.tsx | 34 ++------ apps/web/src/app/user/my-orders/[id]/page.tsx | 18 ++-- apps/web/src/app/user/my-orders/page.tsx | 35 ++++---- apps/web/src/app/user/my-sales/[id]/page.tsx | 16 +--- apps/web/src/app/user/my-sales/page.tsx | 86 +++++++------------ .../web/src/app/user/register-coffee/page.tsx | 13 +-- apps/web/src/types/index.ts | 55 +++++++++++- 10 files changed, 130 insertions(+), 165 deletions(-) diff --git a/apps/web/src/app/user-profile/page.tsx b/apps/web/src/app/user-profile/page.tsx index 0c1d2d6..768feee 100644 --- a/apps/web/src/app/user-profile/page.tsx +++ b/apps/web/src/app/user-profile/page.tsx @@ -7,22 +7,13 @@ import { useState } from "react"; import { ProfileCard } from "~/app/_components/features/ProfileCard"; import { ProfileOptions } from "~/app/_components/features/ProfileOptions"; import Main from "~/app/_components/layout/Main"; - -type Badge = "Lover" | "Contributor" | "Producer"; - -type UserProfile = { - name: string; - country: string; - memberSince: number; - thumbnailUrl: string; - badges: Badge[]; -}; +import type { UserProfileType } from "~/types"; export default function UserProfile() { const { address } = useAccount(); const { disconnect } = useDisconnect(); - const [user] = useState({ + const [user] = useState({ name: "John Doe", country: "United States", memberSince: 2020, diff --git a/apps/web/src/app/user/edit-profile/page.tsx b/apps/web/src/app/user/edit-profile/page.tsx index 431b49d..c8a4fa9 100644 --- a/apps/web/src/app/user/edit-profile/page.tsx +++ b/apps/web/src/app/user/edit-profile/page.tsx @@ -2,14 +2,7 @@ import { ChevronRightIcon } from "@heroicons/react/24/outline"; import Image from "next/image"; import Link from "next/link"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; - -type EditProfileOption = { - imgUrl: string; - label: string; - href: string; - customClass?: string; - iconColor?: string; -}; +import type { EditProfileOption } from "~/types"; const editProfileOptions: EditProfileOption[] = [ { diff --git a/apps/web/src/app/user/my-claims/[id]/page.tsx b/apps/web/src/app/user/my-claims/[id]/page.tsx index 95ae1bf..9b0043c 100644 --- a/apps/web/src/app/user/my-claims/[id]/page.tsx +++ b/apps/web/src/app/user/my-claims/[id]/page.tsx @@ -4,22 +4,12 @@ import { useParams } from "next/navigation"; import React, { useEffect, useState } from "react"; import ProductStatusDetails from "~/app/_components/features/ProductStatusDetails"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; - -type SaleDetails = { - productName: string; - status: string; - roast: string; - type: string; - quantity: string; - delivery: string; - totalPrice: string; - address?: string; -}; +import type { SaleDetailsType } from "~/types"; export default function MySaleDetails() { const { id: saleId } = useParams(); - const [saleDetails, setSaleDetails] = useState(null); + const [saleDetails, setSaleDetails] = useState(null); // TODO: Fetch user role based on user id or from session/context/token const [isProducer, setIsProducer] = useState(false); @@ -41,7 +31,7 @@ export default function MySaleDetails() { setIsProducer(true); }, [saleId]); - const updateSaleDetails = (productDetails: SaleDetails) => { + const updateSaleDetails = (productDetails: SaleDetailsType) => { // TODO: Implement logic to update sale details setSaleDetails(productDetails); }; diff --git a/apps/web/src/app/user/my-claims/page.tsx b/apps/web/src/app/user/my-claims/page.tsx index b4a6ab8..5b2282d 100644 --- a/apps/web/src/app/user/my-claims/page.tsx +++ b/apps/web/src/app/user/my-claims/page.tsx @@ -11,23 +11,7 @@ import { useEffect, useState } from "react"; import OrderListItem from "~/app/_components/features/OrderListItem"; import OrderListPriceItem from "~/app/_components/features/OrderListPriceItem"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; - -const SalesStatusEnum = { - Paid: "Paid", - Prepared: "Prepared", - Shipped: "Shipped", - Delivered: "Delivered", -} as const; - -type SalesStatus = (typeof SalesStatusEnum)[keyof typeof SalesStatusEnum]; - -const DeliveryMethodEnum = { - Address: "Address", - Meetup: "Meetup", -} as const; - -type DeliveryMethod = - (typeof DeliveryMethodEnum)[keyof typeof DeliveryMethodEnum]; +import { DeliveryMethod, SalesStatusType } from "~/types"; const mockedOrders = [ { @@ -37,8 +21,8 @@ const mockedOrders = [ id: "1", productName: "productName", buyerName: "buyer1_fullname", - status: SalesStatusEnum.Delivered as SalesStatus, - delivery: DeliveryMethodEnum.Address as DeliveryMethod, + status: SalesStatusType.Delivered, + delivery: DeliveryMethod.Address, price: 30, claimed: false, }, @@ -46,8 +30,8 @@ const mockedOrders = [ id: "2", productName: "productName2", buyerName: "buyer2_fullname", - status: SalesStatusEnum.Delivered as SalesStatus, - delivery: DeliveryMethodEnum.Meetup as DeliveryMethod, + status: SalesStatusType.Delivered, + delivery: DeliveryMethod.Meetup, price: 30, claimed: false, }, @@ -60,8 +44,8 @@ const mockedOrders = [ id: "3", productName: "productName3", buyerName: "buyer1_fullname", - status: SalesStatusEnum.Delivered as SalesStatus, - delivery: DeliveryMethodEnum.Address as DeliveryMethod, + status: SalesStatusType.Delivered, + delivery: DeliveryMethod.Address, price: 30, claimed: true, }, @@ -69,8 +53,8 @@ const mockedOrders = [ id: "4", productName: "productName4", buyerName: "buyer2_fullname", - status: SalesStatusEnum.Delivered as SalesStatus, - delivery: DeliveryMethodEnum.Meetup as DeliveryMethod, + status: SalesStatusType.Delivered, + delivery: DeliveryMethod.Meetup, price: 30, claimed: true, }, diff --git a/apps/web/src/app/user/my-orders/[id]/page.tsx b/apps/web/src/app/user/my-orders/[id]/page.tsx index 8e4aacc..e4783b3 100644 --- a/apps/web/src/app/user/my-orders/[id]/page.tsx +++ b/apps/web/src/app/user/my-orders/[id]/page.tsx @@ -4,22 +4,14 @@ import { useParams } from "next/navigation"; import React, { useEffect, useState } from "react"; import ProductStatusDetails from "~/app/_components/features/ProductStatusDetails"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; - -type OrderDetails = { - productName: string; - status: string; - roast: string; - type: string; - quantity: string; - delivery: string; - totalPrice: string; - address?: string; -}; +import type { OrderDetailsType } from "~/types"; export default function OrderDetails() { const { id: orderId } = useParams(); - const [orderDetails, setOrderDetails] = useState(null); + const [orderDetails, setOrderDetails] = useState( + null, + ); // TODO: Fetch user role based on user id or from session/context/token const [isProducer, setIsProducer] = useState(false); @@ -42,7 +34,7 @@ export default function OrderDetails() { setIsProducer(true); }, [orderId]); - const updateProductDetails = (productDetails: OrderDetails) => { + const updateProductDetails = (productDetails: OrderDetailsType) => { // TODO: Implement logic to update order details setOrderDetails(productDetails); }; diff --git a/apps/web/src/app/user/my-orders/page.tsx b/apps/web/src/app/user/my-orders/page.tsx index 4b53e18..b6f61ed 100644 --- a/apps/web/src/app/user/my-orders/page.tsx +++ b/apps/web/src/app/user/my-orders/page.tsx @@ -10,7 +10,7 @@ import { useForm } from "react-hook-form"; import OrderListItem from "~/app/_components/features/OrderListItem"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; import BottomModal from "~/app/_components/ui/BottomModal"; -import { type FormValues, SalesStatus, filtersSchema } from "~/types"; +import { type FormValues, SalesStatusType, filtersSchema } from "~/types"; const mockedOrders = [ { @@ -20,13 +20,13 @@ const mockedOrders = [ id: "1", productName: "Edit profile", sellerName: "seller1_fullname", - status: SalesStatus.Paid, + status: SalesStatusType.Paid, }, { id: "2", productName: "My Orders", sellerName: "seller2_fullname", - status: SalesStatus.Paid, + status: SalesStatusType.Paid, }, ], }, @@ -37,13 +37,13 @@ const mockedOrders = [ id: "3", productName: "productName", sellerName: "seller1_fullname", - status: SalesStatus.Delivered, + status: SalesStatusType.Delivered, }, { id: "4", productName: "productName", sellerName: "seller2_fullname", - status: SalesStatus.Delivered, + status: SalesStatusType.Delivered, }, ], }, @@ -101,13 +101,14 @@ export default function MyOrders() { ]; const matchesStatus = activeStatusFilters.some(Boolean) - ? (activeFilters.statusPaid && item.status === SalesStatus.Paid) ?? + ? (activeFilters.statusPaid && + item.status === SalesStatusType.Paid) ?? (activeFilters.statusPrepared && - item.status === SalesStatus.Prepared) ?? + item.status === SalesStatusType.Prepared) ?? (activeFilters.statusShipped && - item.status === SalesStatus.Shipped) ?? + item.status === SalesStatusType.Shipped) ?? (activeFilters.statusDelivered && - item.status === SalesStatus.Delivered) + item.status === SalesStatusType.Delivered) : true; return matchesSearch && matchesStatus; @@ -178,26 +179,26 @@ export default function MyOrders() {
<>


diff --git a/apps/web/src/app/user/my-sales/[id]/page.tsx b/apps/web/src/app/user/my-sales/[id]/page.tsx index 4ce3807..9c0b29b 100644 --- a/apps/web/src/app/user/my-sales/[id]/page.tsx +++ b/apps/web/src/app/user/my-sales/[id]/page.tsx @@ -4,22 +4,12 @@ import { useParams } from "next/navigation"; import React, { useEffect, useState } from "react"; import ProductStatusDetails from "~/app/_components/features/ProductStatusDetails"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; - -type SaleDetails = { - productName: string; - status: string; - roast: string; - type: string; - quantity: string; - delivery: string; - totalPrice: string; - address?: string; -}; +import type { SaleDetailsType } from "~/types"; export default function SaleDetails() { const { id: saleId } = useParams(); - const [saleDetails, setSaleDetails] = useState(null); + const [saleDetails, setSaleDetails] = useState(null); // TODO: Fetch user role based on user id or from session/context/token const [isProducer, setIsProducer] = useState(false); @@ -41,7 +31,7 @@ export default function SaleDetails() { setIsProducer(true); }, [saleId]); - const updateSaleDetails = (productDetails: SaleDetails) => { + const updateSaleDetails = (productDetails: SaleDetailsType) => { // TODO: Implement logic to update sale details setSaleDetails(productDetails); }; diff --git a/apps/web/src/app/user/my-sales/page.tsx b/apps/web/src/app/user/my-sales/page.tsx index b6bece5..35e6c19 100644 --- a/apps/web/src/app/user/my-sales/page.tsx +++ b/apps/web/src/app/user/my-sales/page.tsx @@ -11,34 +11,12 @@ import { z } from "zod"; import OrderListItem from "~/app/_components/features/OrderListItem"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; import BottomModal from "~/app/_components/ui/BottomModal"; - -const SalesStatusEnum = { - Paid: "Paid", - Prepared: "Prepared", - Shipped: "Shipped", - Delivered: "Delivered", -} as const; - -type SalesStatus = (typeof SalesStatusEnum)[keyof typeof SalesStatusEnum]; - -const DeliveryMethodEnum = { - Address: "Address", - Meetup: "Meetup", -} as const; - -type DeliveryMethod = - (typeof DeliveryMethodEnum)[keyof typeof DeliveryMethodEnum]; - -const filtersSchema = z.object({ - statusPaid: z.boolean().optional(), - statusPrepared: z.boolean().optional(), - statusShipped: z.boolean().optional(), - statusDelivered: z.boolean().optional(), - deliveryAddress: z.boolean().optional(), - deliveryMeetup: z.boolean().optional(), -}); - -type FormValues = z.infer; +import { + DeliveryMethod, + type FormValues, + SalesStatusType, + filtersSchema, +} from "~/types"; const mockedOrders = [ { @@ -48,15 +26,15 @@ const mockedOrders = [ id: "1", productName: "Edit profile", buyerName: "buyer1_fullname", - status: SalesStatusEnum.Paid as SalesStatus, - delivery: DeliveryMethodEnum.Address as DeliveryMethod, + status: SalesStatusType.Paid, + delivery: DeliveryMethod.Address, }, { id: "2", productName: "My Orders", buyerName: "buyer2_fullname", - status: SalesStatusEnum.Paid as SalesStatus, - delivery: DeliveryMethodEnum.Meetup as DeliveryMethod, + status: SalesStatusType.Paid, + delivery: DeliveryMethod.Meetup, }, ], }, @@ -67,15 +45,15 @@ const mockedOrders = [ id: "3", productName: "productName", buyerName: "buyer1_fullname", - status: SalesStatusEnum.Delivered as SalesStatus, - delivery: DeliveryMethodEnum.Address as DeliveryMethod, + status: SalesStatusType.Delivered, + delivery: DeliveryMethod.Address, }, { id: "4", productName: "productName", buyerName: "buyer2_fullname", - status: SalesStatusEnum.Delivered as SalesStatus, - delivery: DeliveryMethodEnum.Meetup as DeliveryMethod, + status: SalesStatusType.Delivered, + delivery: DeliveryMethod.Meetup, }, ], }, @@ -137,13 +115,13 @@ export default function MySales() { const matchesStatus = !activeStatusFilters.some(Boolean) ? true : (activeFilters.statusPaid && - item.status === SalesStatusEnum.Paid) ?? + item.status === SalesStatusType.Paid) ?? (activeFilters.statusPrepared && - item.status === SalesStatusEnum.Prepared) ?? + item.status === SalesStatusType.Prepared) ?? (activeFilters.statusShipped && - item.status === SalesStatusEnum.Shipped) ?? + item.status === SalesStatusType.Shipped) ?? (activeFilters.statusDelivered && - item.status === SalesStatusEnum.Delivered); + item.status === SalesStatusType.Delivered); const activeDeliveryFilters = [ activeFilters.deliveryAddress, @@ -153,9 +131,9 @@ export default function MySales() { const matchesDelivery = !activeDeliveryFilters.some(Boolean) ? true : (activeFilters.deliveryAddress && - item.delivery === DeliveryMethodEnum.Address) ?? + item.delivery === DeliveryMethod.Address) ?? (activeFilters.deliveryMeetup && - item.delivery === DeliveryMethodEnum.Meetup); + item.delivery === DeliveryMethod.Meetup); return matchesSearch && matchesStatus && matchesDelivery; }), @@ -225,14 +203,14 @@ export default function MySales() {
<>
@@ -243,26 +221,26 @@ export default function MySales() {
<>


diff --git a/apps/web/src/app/user/register-coffee/page.tsx b/apps/web/src/app/user/register-coffee/page.tsx index 1e54989..8d85545 100644 --- a/apps/web/src/app/user/register-coffee/page.tsx +++ b/apps/web/src/app/user/register-coffee/page.tsx @@ -12,6 +12,7 @@ import Image from "next/image"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; +import { RoastLevel } from "~/types"; const schema = z.object({ roast: z.string().min(1, "Roast level is required"), @@ -29,14 +30,6 @@ const schema = z.object({ type FormData = z.infer; -const RoastLevel = { - LIGHT: "Light", - MEDIUM: "Medium", - STRONG: "Strong", -} as const; - -type RoastLevelType = (typeof RoastLevel)[keyof typeof RoastLevel]; - export default function RegisterCoffee() { const handleImageUpload = () => { alert("Implement image upload"); @@ -152,7 +145,7 @@ export default function RegisterCoffee() { Roast level
- {(Object.values(RoastLevel) as RoastLevelType[]).map((level) => ( + {Object.values(RoastLevel).map((level) => (
{level} diff --git a/apps/web/src/types/index.ts b/apps/web/src/types/index.ts index 40dd091..eebc816 100644 --- a/apps/web/src/types/index.ts +++ b/apps/web/src/types/index.ts @@ -1,17 +1,70 @@ import { z } from "zod"; -export enum SalesStatus { +export type Badge = "Lover" | "Contributor" | "Producer"; + +export type UserProfileType = { + name: string; + country: string; + memberSince: number; + thumbnailUrl: string; + badges: Badge[]; +}; + +export type EditProfileOption = { + imgUrl: string; + label: string; + href: string; + customClass?: string; + iconColor?: string; +}; + +export enum RoastLevel { + LIGHT = "Light", + MEDIUM = "Medium", + STRONG = "Strong", +} + +export type SaleDetailsType = { + productName: string; + status: string; + roast: string; + type: string; + quantity: string; + delivery: string; + totalPrice: string; + address?: string; +}; + +export enum SalesStatusType { Paid = "Paid", Prepared = "Prepared", Shipped = "Shipped", Delivered = "Delivered", } +export type OrderDetailsType = { + productName: string; + status: string; + roast: string; + type: string; + quantity: string; + delivery: string; + totalPrice: string; + address?: string; +}; + +export enum DeliveryMethod { + Address = "Address", + Meetup = "Meetup", +} + export const filtersSchema = z.object({ statusPaid: z.boolean().optional(), statusPrepared: z.boolean().optional(), statusShipped: z.boolean().optional(), statusDelivered: z.boolean().optional(), + deliveryAddress: z.boolean().optional(), + deliveryMeetup: z.boolean().optional(), }); export type FormValues = z.infer; From 0eca9a4fb3cade6e933eddae99ace469709daab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Jim=C3=A9nez?= <87153882+jimenezz22@users.noreply.github.com> Date: Thu, 28 Nov 2024 23:46:24 -0600 Subject: [PATCH 04/27] Refactor Order Status Management (#67) --- .../features/ProductDetailsList.tsx | 34 +++ .../features/ProductStatusDetails.tsx | 269 +++++------------- .../app/_components/features/StatusBanner.tsx | 49 ++++ .../features/StatusUpdateModal.tsx | 54 ++++ 4 files changed, 209 insertions(+), 197 deletions(-) create mode 100644 apps/web/src/app/_components/features/ProductDetailsList.tsx create mode 100644 apps/web/src/app/_components/features/StatusBanner.tsx create mode 100644 apps/web/src/app/_components/features/StatusUpdateModal.tsx diff --git a/apps/web/src/app/_components/features/ProductDetailsList.tsx b/apps/web/src/app/_components/features/ProductDetailsList.tsx new file mode 100644 index 0000000..0337fa8 --- /dev/null +++ b/apps/web/src/app/_components/features/ProductDetailsList.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +type ProductDetail = { + label: string; + value: string; + address?: string; +}; + +type ProductDetailsListProps = { + details: ProductDetail[]; +}; + +export function ProductDetailsList({ details }: ProductDetailsListProps) { + return ( +
+ {details.map((item, index, array) => ( + +
+
+

{item.label}

+ {item.address && ( +

+ {item.address} +

+ )} +
+

{item.value}

+
+ {index < array.length - 1 &&
} +
+ ))} +
+ ); +} diff --git a/apps/web/src/app/_components/features/ProductStatusDetails.tsx b/apps/web/src/app/_components/features/ProductStatusDetails.tsx index b7ff3f8..ffe1f3e 100644 --- a/apps/web/src/app/_components/features/ProductStatusDetails.tsx +++ b/apps/web/src/app/_components/features/ProductStatusDetails.tsx @@ -6,15 +6,14 @@ import { TruckIcon, WalletIcon, } from "@heroicons/react/24/outline"; -import { LightBulbIcon } from "@heroicons/react/24/solid"; import { zodResolver } from "@hookform/resolvers/zod"; -import Button from "@repo/ui/button"; -import RadioButton from "@repo/ui/form/radioButton"; import Image from "next/image"; import React, { useState, useEffect } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import BottomModal from "~/app/_components/ui/BottomModal"; +import { ProductDetailsList } from "./ProductDetailsList"; +import { StatusBanner } from "./StatusBanner"; +import { StatusUpdateModal } from "./StatusUpdateModal"; type ProductDetails = { productName: string; @@ -27,7 +26,7 @@ type ProductDetails = { address?: string; }; -enum StatusStepsEnum { +export enum StatusStepsEnum { Paid = "Paid", Shipped = "Shipped", Prepared = "Prepared", @@ -56,13 +55,12 @@ export default function ProductStatusDetails({ isProducer, updateProductDetails, }: ProductStatusDetailsProps) { - const [isOrderStatusModalOpen, setIsOrderStatusModalOpen] = - useState(false); + const [isOrderStatusModalOpen, setIsOrderStatusModalOpen] = useState(false); - const { control, handleSubmit, setValue } = useForm({ - defaultValues: { - status: StatusStepsEnum.Paid, - }, + const { control, handleSubmit, setValue } = useForm<{ + status: StatusStepsEnum; + }>({ + defaultValues: { status: StatusStepsEnum.Paid }, resolver: zodResolver(orderStatusSchema), }); @@ -74,64 +72,69 @@ export default function ProductStatusDetails({ const statusStepsKeys = Object.keys(StatusStepsEnum); - const openOrderStatusModal = () => { - setIsOrderStatusModalOpen(true); - }; - - const closeOrderStatusModal = () => { - setIsOrderStatusModalOpen(false); - }; + const openOrderStatusModal = () => setIsOrderStatusModalOpen(true); + const closeOrderStatusModal = () => setIsOrderStatusModalOpen(false); - const onSubmit = (data: FormValues) => { + const onSubmit = (data: { status: StatusStepsEnum }) => { if (productDetails && updateProductDetails) { - updateProductDetails({ - ...productDetails, - status: data.status, - }); + updateProductDetails({ ...productDetails, status: data.status }); } - closeOrderStatusModal(); }; + const handleSubmitForm = handleSubmit(onSubmit); + if (!productDetails) return
Loading...
; - const ProductStatus = ({ - productDetails, - }: { productDetails: ProductDetails }) => { - const stepsByDeliveryType = { - [DeliveryTypeEnum.Meetup]: [ - StatusStepsEnum.Paid, - StatusStepsEnum.Prepared, - StatusStepsEnum.Delivered, - ], - [DeliveryTypeEnum.Delivery]: [ - StatusStepsEnum.Paid, - StatusStepsEnum.Shipped, - StatusStepsEnum.Delivered, - ], - }; - const statusSteps = - stepsByDeliveryType[productDetails.delivery as DeliveryTypeEnum]; - const currentStepIndex = statusSteps.indexOf( - productDetails.status as StatusStepsEnum, - ); - const stepIconMap = { - [StatusStepsEnum.Paid]: WalletIcon, - [StatusStepsEnum.Shipped]: TruckIcon, - [StatusStepsEnum.Prepared]: ShoppingBagIcon, - [StatusStepsEnum.Delivered]: CheckCircleIcon, - }; + const stepsByDeliveryType = { + [DeliveryTypeEnum.Meetup]: [ + StatusStepsEnum.Paid, + StatusStepsEnum.Prepared, + StatusStepsEnum.Delivered, + ], + [DeliveryTypeEnum.Delivery]: [ + StatusStepsEnum.Paid, + StatusStepsEnum.Shipped, + StatusStepsEnum.Delivered, + ], + }; + + const statusSteps = + stepsByDeliveryType[productDetails.delivery as DeliveryTypeEnum]; + const currentStepIndex = statusSteps.indexOf( + productDetails.status as StatusStepsEnum, + ); + + const stepIconMap = { + [StatusStepsEnum.Paid]: WalletIcon, + [StatusStepsEnum.Shipped]: TruckIcon, + [StatusStepsEnum.Prepared]: ShoppingBagIcon, + [StatusStepsEnum.Delivered]: CheckCircleIcon, + }; + + return ( +
+
+ +
+ +
+ Product +

{productDetails.productName}

+
- return ( -
+
{statusSteps.map((step, index) => ( -
+
))}
- ); - }; - - const OrderStatusBanner = ({ - orderStatus, - isProducer, - }: { orderStatus: string; isProducer: boolean }) => { - const statusText = { - [StatusStepsEnum.Paid]: "The Producer is already preparing your order", - [StatusStepsEnum.Shipped]: "The Producer has already shipped your order", - [StatusStepsEnum.Prepared]: - "The Producer has already prepared your order", - [StatusStepsEnum.Delivered]: "Your order has arrived. Enjoy your coffee", - }; - - if (isProducer) { - if (orderStatus === (StatusStepsEnum.Delivered as string)) { - return; - } - return ( -
- -
-

- New order -

-

- You have a new order. Let's start the preparations. -

-
- -
- ); - } - return ( -
-
-

- - {statusText[orderStatus as keyof typeof statusText] || ""} -

-
-
- ); - }; - - return ( -
-
- -
- -
- Product -

{productDetails.productName}

-
- -
- -
- - {isProducer && ( - - )} - -
- {[ + ( - -
-
-

{item.label}

- {item.address && ( -

- {item.address} -

- )} -
-

{item.value}

-
- {index < array.length - 1 &&
} -
- ))} -
+ ]} + /> {isProducer && ( - -

- Select the status -

-
-
- {statusStepsKeys.map((status, index) => ( - - - {index < statusStepsKeys.length - 1 && ( -
- )} -
- ))} -
- -
-
+ onSubmit={handleSubmitForm} + control={control} + statusStepsKeys={statusStepsKeys} + /> )}
); diff --git a/apps/web/src/app/_components/features/StatusBanner.tsx b/apps/web/src/app/_components/features/StatusBanner.tsx new file mode 100644 index 0000000..c9045c1 --- /dev/null +++ b/apps/web/src/app/_components/features/StatusBanner.tsx @@ -0,0 +1,49 @@ +import { LightBulbIcon } from "@heroicons/react/24/solid"; +import React from "react"; + +type StatusBannerProps = { + orderStatus: string; + isProducer: boolean; +}; + +const statusText = { + Paid: "The Producer is already preparing your order", + Shipped: "The Producer has already shipped your order", + Prepared: "The Producer has already prepared your order", + Delivered: "Your order has arrived. Enjoy your coffee", +}; + +export function StatusBanner({ orderStatus, isProducer }: StatusBannerProps) { + if (isProducer && orderStatus !== "Delivered") { + return ( +
+ +
+

+ New order +

+

+ You have a new order. Let's start the preparations. +

+
+ +
+ ); + } + + return ( +
+
+

+ + {statusText[orderStatus as keyof typeof statusText] || ""} +

+
+
+ ); +} diff --git a/apps/web/src/app/_components/features/StatusUpdateModal.tsx b/apps/web/src/app/_components/features/StatusUpdateModal.tsx new file mode 100644 index 0000000..3387e62 --- /dev/null +++ b/apps/web/src/app/_components/features/StatusUpdateModal.tsx @@ -0,0 +1,54 @@ +import Button from "@repo/ui/button"; +import RadioButton from "@repo/ui/form/radioButton"; +import React, { BaseSyntheticEvent, type FormEvent } from "react"; +import { type Control, useForm } from "react-hook-form"; +import BottomModal from "~/app/_components/ui/BottomModal"; +import type { StatusStepsEnum } from "./ProductStatusDetails"; + +type StatusUpdateModalProps = { + isOpen: boolean; + onClose: () => void; + onSubmit: (e?: FormEvent) => Promise; + control: Control<{ status: StatusStepsEnum }>; + statusStepsKeys: string[]; +}; + +export function StatusUpdateModal({ + isOpen, + onClose, + onSubmit, + control, + statusStepsKeys, +}: StatusUpdateModalProps) { + return ( + +

+ Select the status +

+
+ {" "} + {} +
+ {statusStepsKeys.map((status, index) => ( + + + {index < statusStepsKeys.length - 1 && ( +
+ )} +
+ ))} +
+ +
+
+ ); +} From ca0d49932192e601a44509e5049e3baee01b86d5 Mon Sep 17 00:00:00 2001 From: Josue Madrigal <144070203+RJMadrigal@users.noreply.github.com> Date: Thu, 28 Nov 2024 23:53:41 -0600 Subject: [PATCH 05/27] docs: add contributing guidelines (CONTRIBUTING.md) (#68) --- CONTRIBUTING.md | 81 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ed0a423 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,81 @@ +# β˜• Contributing to CofiBlocks + +Thank you for your interest in contributing to **CofiBlocks**! This document outlines the steps to get started, guidelines for contributions, and how to collaborate effectively with the team. + +--- + +## πŸš€ Contribution Guideline + +### Getting Started +- πŸ” Visit the [Issues](https://github.com/Vagabonds-Labs/cofiblocks/issues) tab to see ongoing work or feature requests +- πŸ’¬ If you find an issue that interests you, comment to express your interest +- πŸ“ For new ideas, create a detailed issue with clear descriptions + +### Creating New Issues +- πŸ› For bugs, include steps to reproduce +- ✨ For features, describe the enhancement clearly + +### Start Working + - Once an issue is assigned to you, proceed with the technical steps described below. + +--- + +## πŸ›  How To Contribute + +1. Fork the repository to your GitHub account first. + +2. Clone the repository in your local machine: +```bash +git clone https://github.com/YOUR_USERNAME/cofiblocks.git +cd marketplace +``` + +2. Create and switch to your branch: +```bash +git checkout -b feature/amazing-feature +``` + +3. Make changes: +Make your changes. Ensure your code adheres to the project's coding style and standards. + +4. Add and commit your changes: +```bash +git add . +git commit -m "Add amazing feature" +``` + +5. Push to the branch: +```bash +git push origin feature/amazing-feature +``` + +6. Open a Pull Request. +Once your changes are ready, submit a Pull Request for review. Ensure that: + +Ensure your PR has a clear title, a detailed description of the changes, and references any related issues (if applicable). + +All contributions go through the PR review process to maintain code quality. After review, maintainers will provide feedback or merge it into the main branch. + +--- + +## Code of Conduct + +We are committed to creating a welcoming and inclusive environment. Please read our [Community Guidelines](COMMUNITY_GUIDELINES.md) to ensure a positive experience for everyone involved. + +--- + +## 🌐 Community Resources + +Stay connected with the **CofiBlocks** community! Here are the resources where you can engage, stay informed, and collaborate with the team and other contributors: + +- **Twitter** + Follow us on [Twitter](https://x.com/cofiblocks) + +- **Website** + Visit our [Official Website](https://www.cofiblocks.com/) + +We look forward to your active participation and contributions! πŸš€ + + + + From 39e91d37b28f8e5198abac8194c880fe9f813b09 Mon Sep 17 00:00:00 2001 From: Derian <59376626+derianrddev@users.noreply.github.com> Date: Fri, 29 Nov 2024 19:45:56 -0600 Subject: [PATCH 06/27] Extract shared order filtering logic into custom hook (#74) --- apps/web/src/app/user/my-claims/page.tsx | 10 +- apps/web/src/app/user/my-orders/page.tsx | 116 +++++---------- apps/web/src/app/user/my-sales/page.tsx | 133 +++++------------- apps/web/src/hooks/user/useOrderFiltering.tsx | 108 ++++++++++++++ apps/web/src/types/index.ts | 16 ++- 5 files changed, 204 insertions(+), 179 deletions(-) create mode 100644 apps/web/src/hooks/user/useOrderFiltering.tsx diff --git a/apps/web/src/app/user/my-claims/page.tsx b/apps/web/src/app/user/my-claims/page.tsx index 5b2282d..da0826e 100644 --- a/apps/web/src/app/user/my-claims/page.tsx +++ b/apps/web/src/app/user/my-claims/page.tsx @@ -11,7 +11,7 @@ import { useEffect, useState } from "react"; import OrderListItem from "~/app/_components/features/OrderListItem"; import OrderListPriceItem from "~/app/_components/features/OrderListPriceItem"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; -import { DeliveryMethod, SalesStatusType } from "~/types"; +import { DeliveryMethod, SalesStatus } from "~/types"; const mockedOrders = [ { @@ -21,7 +21,7 @@ const mockedOrders = [ id: "1", productName: "productName", buyerName: "buyer1_fullname", - status: SalesStatusType.Delivered, + status: SalesStatus.Delivered, delivery: DeliveryMethod.Address, price: 30, claimed: false, @@ -30,7 +30,7 @@ const mockedOrders = [ id: "2", productName: "productName2", buyerName: "buyer2_fullname", - status: SalesStatusType.Delivered, + status: SalesStatus.Delivered, delivery: DeliveryMethod.Meetup, price: 30, claimed: false, @@ -44,7 +44,7 @@ const mockedOrders = [ id: "3", productName: "productName3", buyerName: "buyer1_fullname", - status: SalesStatusType.Delivered, + status: SalesStatus.Delivered, delivery: DeliveryMethod.Address, price: 30, claimed: true, @@ -53,7 +53,7 @@ const mockedOrders = [ id: "4", productName: "productName4", buyerName: "buyer2_fullname", - status: SalesStatusType.Delivered, + status: SalesStatus.Delivered, delivery: DeliveryMethod.Meetup, price: 30, claimed: true, diff --git a/apps/web/src/app/user/my-orders/page.tsx b/apps/web/src/app/user/my-orders/page.tsx index b6f61ed..acf88c2 100644 --- a/apps/web/src/app/user/my-orders/page.tsx +++ b/apps/web/src/app/user/my-orders/page.tsx @@ -5,12 +5,12 @@ import { zodResolver } from "@hookform/resolvers/zod"; import Button from "@repo/ui/button"; import CheckBox from "@repo/ui/form/checkBox"; import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import OrderListItem from "~/app/_components/features/OrderListItem"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; import BottomModal from "~/app/_components/ui/BottomModal"; -import { type FormValues, SalesStatusType, filtersSchema } from "~/types"; +import { useOrderFiltering } from "~/hooks/user/useOrderFiltering"; +import { type FormValues, SalesStatus, filtersSchema } from "~/types"; const mockedOrders = [ { @@ -20,13 +20,13 @@ const mockedOrders = [ id: "1", productName: "Edit profile", sellerName: "seller1_fullname", - status: SalesStatusType.Paid, + status: SalesStatus.Paid, }, { id: "2", productName: "My Orders", sellerName: "seller2_fullname", - status: SalesStatusType.Paid, + status: SalesStatus.Paid, }, ], }, @@ -37,92 +37,50 @@ const mockedOrders = [ id: "3", productName: "productName", sellerName: "seller1_fullname", - status: SalesStatusType.Delivered, + status: SalesStatus.Delivered, }, { id: "4", productName: "productName", sellerName: "seller2_fullname", - status: SalesStatusType.Delivered, + status: SalesStatus.Delivered, }, ], }, ]; +const filtersDefaults = { + statusPaid: false, + statusPrepared: false, + statusShipped: false, + statusDelivered: false, +}; + export default function MyOrders() { - const [searchTerm, setSearchTerm] = useState(""); - const [isFiltersModalOpen, setIsFiltersModalOpen] = useState(false); - const [filteredOrders, setFilteredOrders] = useState(mockedOrders); - const [activeFilters, setActiveFilters] = useState({}); + const { + searchTerm, + setSearchTerm, + isFiltersModalOpen, + openFiltersModal, + closeFiltersModal, + filteredOrders, + applyFilters, + } = useOrderFiltering({ + orders: mockedOrders, + searchKey: "sellerName", + filters: filtersDefaults, + }); const router = useRouter(); - const openFiltersModal = () => { - setIsFiltersModalOpen(true); - }; - - const closeFiltersModal = () => { - setIsFiltersModalOpen(false); - }; - const { control, handleSubmit } = useForm({ resolver: zodResolver(filtersSchema), - defaultValues: { - statusPaid: false, - statusPrepared: false, - statusShipped: false, - statusDelivered: false, - }, + defaultValues: filtersDefaults, }); - const onSubmit = (data: FormValues) => { - console.log(data); - setActiveFilters(data); - closeFiltersModal(); - }; - const handleItemClick = (id: string) => { router.push(`/user/my-orders/${id}`); }; - useEffect(() => { - const newFilteredOrders = mockedOrders - .map((orderGroup) => ({ - ...orderGroup, - items: orderGroup.items.filter((item) => { - const matchesSearch = searchTerm - ? item.sellerName.toLowerCase().includes(searchTerm.toLowerCase()) - : true; - - const activeStatusFilters = [ - activeFilters.statusPaid, - activeFilters.statusPrepared, - activeFilters.statusShipped, - activeFilters.statusDelivered, - ]; - - const matchesStatus = activeStatusFilters.some(Boolean) - ? (activeFilters.statusPaid && - item.status === SalesStatusType.Paid) ?? - (activeFilters.statusPrepared && - item.status === SalesStatusType.Prepared) ?? - (activeFilters.statusShipped && - item.status === SalesStatusType.Shipped) ?? - (activeFilters.statusDelivered && - item.status === SalesStatusType.Delivered) - : true; - - return matchesSearch && matchesStatus; - }), - })) - .filter((orderGroup) => orderGroup.items.length > 0); - - if (!searchTerm && !Object.values(activeFilters).some(Boolean)) { - setFilteredOrders(mockedOrders); - } else { - setFilteredOrders(newFilteredOrders); - } - }, [searchTerm, activeFilters]); - return (
@@ -157,7 +115,7 @@ export default function MyOrders() { handleItemClick(order.id)} /> @@ -172,33 +130,33 @@ export default function MyOrders() {
-
+

Status

<>


diff --git a/apps/web/src/app/user/my-sales/page.tsx b/apps/web/src/app/user/my-sales/page.tsx index 35e6c19..5061690 100644 --- a/apps/web/src/app/user/my-sales/page.tsx +++ b/apps/web/src/app/user/my-sales/page.tsx @@ -5,16 +5,15 @@ import { zodResolver } from "@hookform/resolvers/zod"; import Button from "@repo/ui/button"; import CheckBox from "@repo/ui/form/checkBox"; import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; -import { z } from "zod"; import OrderListItem from "~/app/_components/features/OrderListItem"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; import BottomModal from "~/app/_components/ui/BottomModal"; +import { useOrderFiltering } from "~/hooks/user/useOrderFiltering"; import { DeliveryMethod, type FormValues, - SalesStatusType, + SalesStatus, filtersSchema, } from "~/types"; @@ -26,14 +25,14 @@ const mockedOrders = [ id: "1", productName: "Edit profile", buyerName: "buyer1_fullname", - status: SalesStatusType.Paid, + status: SalesStatus.Paid, delivery: DeliveryMethod.Address, }, { id: "2", productName: "My Orders", buyerName: "buyer2_fullname", - status: SalesStatusType.Paid, + status: SalesStatus.Paid, delivery: DeliveryMethod.Meetup, }, ], @@ -45,108 +44,54 @@ const mockedOrders = [ id: "3", productName: "productName", buyerName: "buyer1_fullname", - status: SalesStatusType.Delivered, + status: SalesStatus.Delivered, delivery: DeliveryMethod.Address, }, { id: "4", productName: "productName", buyerName: "buyer2_fullname", - status: SalesStatusType.Delivered, + status: SalesStatus.Delivered, delivery: DeliveryMethod.Meetup, }, ], }, ]; +const filtersDefaults = { + statusPaid: false, + statusPrepared: false, + statusShipped: false, + statusDelivered: false, + deliveryAddress: false, + deliveryMeetup: false, +}; + export default function MySales() { - const [searchTerm, setSearchTerm] = useState(""); - const [isFiltersModalOpen, setIsFiltersModalOpen] = useState(false); - const [filteredOrders, setFilteredOrders] = useState(mockedOrders); - const [activeFilters, setActiveFilters] = useState({}); + const { + searchTerm, + setSearchTerm, + isFiltersModalOpen, + openFiltersModal, + closeFiltersModal, + filteredOrders, + applyFilters, + } = useOrderFiltering({ + orders: mockedOrders, + searchKey: "buyerName", + filters: filtersDefaults, + }); const router = useRouter(); - const openFiltersModal = () => { - setIsFiltersModalOpen(true); - }; - - const closeFiltersModal = () => { - setIsFiltersModalOpen(false); - }; - const { control, handleSubmit } = useForm({ resolver: zodResolver(filtersSchema), - defaultValues: { - statusPaid: false, - statusPrepared: false, - statusShipped: false, - statusDelivered: false, - deliveryAddress: false, - deliveryMeetup: false, - }, + defaultValues: filtersDefaults, }); - const onSubmit = (data: FormValues) => { - console.log(data); - setActiveFilters(data); - closeFiltersModal(); - }; - const handleItemClick = (id: string) => { router.push(`/user/my-sales/${id}`); }; - useEffect(() => { - const newFilteredOrders = mockedOrders - .map((orderGroup) => ({ - ...orderGroup, - items: orderGroup.items.filter((item) => { - const matchesSearch = searchTerm - ? item.buyerName.toLowerCase().includes(searchTerm.toLowerCase()) - : true; - - const activeStatusFilters = [ - activeFilters.statusPaid, - activeFilters.statusPrepared, - activeFilters.statusShipped, - activeFilters.statusDelivered, - ]; - - const matchesStatus = !activeStatusFilters.some(Boolean) - ? true - : (activeFilters.statusPaid && - item.status === SalesStatusType.Paid) ?? - (activeFilters.statusPrepared && - item.status === SalesStatusType.Prepared) ?? - (activeFilters.statusShipped && - item.status === SalesStatusType.Shipped) ?? - (activeFilters.statusDelivered && - item.status === SalesStatusType.Delivered); - - const activeDeliveryFilters = [ - activeFilters.deliveryAddress, - activeFilters.deliveryMeetup, - ]; - - const matchesDelivery = !activeDeliveryFilters.some(Boolean) - ? true - : (activeFilters.deliveryAddress && - item.delivery === DeliveryMethod.Address) ?? - (activeFilters.deliveryMeetup && - item.delivery === DeliveryMethod.Meetup); - - return matchesSearch && matchesStatus && matchesDelivery; - }), - })) - .filter((orderGroup) => orderGroup.items.length > 0); - - if (!searchTerm && !Object.values(activeFilters).some(Boolean)) { - setFilteredOrders(mockedOrders); - } else { - setFilteredOrders(newFilteredOrders); - } - }, [searchTerm, activeFilters]); - return (
@@ -181,7 +126,7 @@ export default function MySales() { handleItemClick(order.id)} /> @@ -196,7 +141,7 @@ export default function MySales() {
- +

Delivery method

@@ -221,26 +166,26 @@ export default function MySales() {
<>


diff --git a/apps/web/src/hooks/user/useOrderFiltering.tsx b/apps/web/src/hooks/user/useOrderFiltering.tsx new file mode 100644 index 0000000..ac6fc02 --- /dev/null +++ b/apps/web/src/hooks/user/useOrderFiltering.tsx @@ -0,0 +1,108 @@ +import { useEffect, useState } from "react"; +import { DeliveryMethod, type Order, SalesStatus } from "~/types"; + +interface UseOrderFilteringProps { + orders: Order[]; + searchKey: "sellerName" | "buyerName"; + filters: Record; +} + +export function useOrderFiltering({ + orders, + searchKey, + filters, +}: UseOrderFilteringProps) { + const [searchTerm, setSearchTerm] = useState(""); + const [isFiltersModalOpen, setIsFiltersModalOpen] = useState(false); + const [filteredOrders, setFilteredOrders] = useState(orders); + const [activeFilters, setActiveFilters] = + useState>(filters); + + const openFiltersModal = () => setIsFiltersModalOpen(true); + const closeFiltersModal = () => setIsFiltersModalOpen(false); + + const applyFilters = (newFilters: Record) => { + setActiveFilters(newFilters); + closeFiltersModal(); + }; + + useEffect(() => { + const newFilteredOrders = orders + .map((orderGroup) => ({ + ...orderGroup, + items: orderGroup.items.filter((item) => { + const matchesSearch = searchTerm + ? item[searchKey]?.toLowerCase().includes(searchTerm.toLowerCase()) + : true; + + const activeStatusFilters = [ + activeFilters.statusPaid, + activeFilters.statusPrepared, + activeFilters.statusShipped, + activeFilters.statusDelivered, + ]; + + const matchesStatus = activeStatusFilters.some(Boolean) + ? [ + { + filter: activeFilters.statusPaid, + status: SalesStatus.Paid, + }, + { + filter: activeFilters.statusPrepared, + status: SalesStatus.Prepared, + }, + { + filter: activeFilters.statusShipped, + status: SalesStatus.Shipped, + }, + { + filter: activeFilters.statusDelivered, + status: SalesStatus.Delivered, + }, + ].some(({ filter, status }) => filter && item.status === status) + : true; + + const activeDeliveryFilters = [ + activeFilters.deliveryAddress, + activeFilters.deliveryMeetup, + ]; + + const matchesDelivery = activeDeliveryFilters.some(Boolean) + ? [ + { + filter: activeFilters.deliveryAddress, + deliveryMethod: DeliveryMethod.Address, + }, + { + filter: activeFilters.deliveryMeetup, + deliveryMethod: DeliveryMethod.Meetup, + }, + ].some( + ({ filter, deliveryMethod }) => + filter && item.delivery === deliveryMethod, + ) + : true; + + return matchesSearch && matchesStatus && matchesDelivery; + }), + })) + .filter((orderGroup) => orderGroup.items.length > 0); + if (!searchTerm && !Object.values(activeFilters).some(Boolean)) { + setFilteredOrders(orders); + } else { + setFilteredOrders(newFilteredOrders); + } + }, [orders, searchKey, searchTerm, activeFilters]); + + return { + searchTerm, + setSearchTerm, + isFiltersModalOpen, + openFiltersModal, + closeFiltersModal, + activeFilters, + filteredOrders, + applyFilters, + }; +} diff --git a/apps/web/src/types/index.ts b/apps/web/src/types/index.ts index eebc816..314e2d6 100644 --- a/apps/web/src/types/index.ts +++ b/apps/web/src/types/index.ts @@ -35,7 +35,7 @@ export type SaleDetailsType = { address?: string; }; -export enum SalesStatusType { +export enum SalesStatus { Paid = "Paid", Prepared = "Prepared", Shipped = "Shipped", @@ -68,3 +68,17 @@ export const filtersSchema = z.object({ }); export type FormValues = z.infer; + +export interface OrderItem { + id: string; + productName: string; + status: SalesStatus; + sellerName?: string; + buyerName?: string; + delivery?: DeliveryMethod; +} + +export interface Order { + date: string; + items: OrderItem[]; +} From 026e70fb97579bc8295ddb1aa748d15ef9c8d87e Mon Sep 17 00:00:00 2001 From: Matias Date: Sat, 30 Nov 2024 22:51:07 -0600 Subject: [PATCH 07/27] Docs/readme (#73) --- CONTRIBUTING.md | 137 +++++++++++++++++----------- README.md | 236 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 259 insertions(+), 114 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed0a423..57640a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,81 +1,116 @@ -# β˜• Contributing to CofiBlocks +# **CONTRIBUTING GUIDE BY COFIBLOCKS** β˜• -Thank you for your interest in contributing to **CofiBlocks**! This document outlines the steps to get started, guidelines for contributions, and how to collaborate effectively with the team. +Thank you for your interest in contributing to CofiBlocks! We appreciate your time and effort in making our project better. ---- +This guide will help you get started with contributing to our project. Please note that we have a code of conduct, which we expect all contributors to adhere to. -## πŸš€ Contribution Guideline +## πŸ“ **Contributing** -### Getting Started -- πŸ” Visit the [Issues](https://github.com/Vagabonds-Labs/cofiblocks/issues) tab to see ongoing work or feature requests -- πŸ’¬ If you find an issue that interests you, comment to express your interest -- πŸ“ For new ideas, create a detailed issue with clear descriptions +We welcome contributions from the community! Here's how you can help: -### Creating New Issues -- πŸ› For bugs, include steps to reproduce -- ✨ For features, describe the enhancement clearly +1. **Clone and Fork Repo**: Click the **Fork** button in the top-right corner to create a copy of the repository under your account. -### Start Working - - Once an issue is assigned to you, proceed with the technical steps described below. + - HERE ---- +# -## πŸ›  How To Contribute +2. **Clone the Fork:** + - Clone the forked repository to your local machine by running the following command: -1. Fork the repository to your GitHub account first. + ```bash + git clone https://github.com/YOUR_USERNAME/REPOSITORY_NAME.git + ``` -2. Clone the repository in your local machine: -```bash -git clone https://github.com/YOUR_USERNAME/cofiblocks.git -cd marketplace -``` + - Replace `YOUR_USERNAME` and `REPOSITORY_NAME` with your GitHub username and the repository name. -2. Create and switch to your branch: -```bash -git checkout -b feature/amazing-feature -``` +# -3. Make changes: -Make your changes. Ensure your code adheres to the project's coding style and standards. +3. **Create a new branch or use the main branch:** When modifying contracts kindly make sure the formatting is correct and all tests pass successfully. -4. Add and commit your changes: -```bash -git add . -git commit -m "Add amazing feature" -``` + - Create a branch name based on the type of change (e.g., `feat/name-related-issue`, `docs/name-related-issue`). -5. Push to the branch: -```bash -git push origin feature/amazing-feature -``` + ``` + git checkout -b branch-name + ``` + - One of ideas on how to implement it for the branch name: + + > `docs/update-readme` or `fix/bottom-bug`. + +# + +4. **Commit:** Commit your changes. + + 1. **git add (file-name)** + 2. **git commit -m "[type] description"** -6. Open a Pull Request. -Once your changes are ready, submit a Pull Request for review. Ensure that: + - Example: + ``` + git add Create_Documentation -Ensure your PR has a clear title, a detailed description of the changes, and references any related issues (if applicable). + git commit -m "[docs]: update documentation" + ``` -All contributions go through the PR review process to maintain code quality. After review, maintainers will provide feedback or merge it into the main branch. +# ---- +5. **Push fork:** Push to your fork and submit a pull request on our `main` branch. Please provide us with some explanation of why you made the changes you made. For new features make sure to explain a standard use case to us. -## Code of Conduct +- Push your changes to your forked repository: + ```bash + git push origin your-branch-name + ``` + > Replace `your-branch-name` with the name of your branch. -We are committed to creating a welcoming and inclusive environment. Please read our [Community Guidelines](COMMUNITY_GUIDELINES.md) to ensure a positive experience for everyone involved. +- Example: ---- + ```bash + git push origin fix/bug-fix + ``` -## 🌐 Community Resources +# -Stay connected with the **CofiBlocks** community! Here are the resources where you can engage, stay informed, and collaborate with the team and other contributors: +6. **Submit a Pull Request:** Submit a pull request to the `main` branch of the Semaphore Stellar SDK repository. -- **Twitter** - Follow us on [Twitter](https://x.com/cofiblocks) + - Summit pull request -- **Website** - Visit our [Official Website](https://www.cofiblocks.com/) +# **πŸ“ Commits** -We look forward to your active participation and contributions! πŸš€ +You can do a regular commit by following the next: +``` [type] significant message ``` +- Learn more about conventional commits +### Type +Add changes you worked on the issue. + +**Examples:** + +```bash +git commit -m "[docs]: update documentation" +``` + +```bash +git commit -m "[fix]: fix bug in code" +``` + +```bash +git commit -m "[test]: add test case" +``` + +# **πŸ”— Branches** +1. There must be a `main` branch, used only for the releases. +2. Avoid long descriptive names for long-lived branches. +3. Use kebab-case (no CamelCase). +4. Use grouping tokens (words) at the beginning of your branch names (in a similar way to the `type` of commit). +5. Define and use short lead tokens to differentiate branches in a way that is meaningful to your workflow. +6. Use slashes to separate parts of your branch names. +7. Remove your branch after merging it if it is not important. +**Examples:** +``` +git branch -b docs/readme +git branch -b test/a-feature +git branch -b feat/sidebar +git branch -b fix/b-feature +``` +# \ No newline at end of file diff --git a/README.md b/README.md index da6cce2..3ca1675 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,129 @@ -# CofiBlocks - La Comunidad del CafΓ© β˜•οΈ +# **CofiBlocks - La Comunidad del CafΓ© β˜•οΈ** CofiBlocks is the first Collaborative Business connecting traditional coffee-growing communities in Costa Rica and worldwide directly with coffee lovers using Starknet blockchain technology. Our mission is to distribute benefits among all members, ensuring fair trade, community engagement, and technological innovation. -## πŸ› οΈ Getting Started +## πŸš€ Roadmap + +### **2022-2023 Season** +Thank you to everyone who participated in our first season and enjoyed coffee from the slopes of VolcΓ‘n PoΓ‘s. + +### **2024-2025 Season** +We are preparing to launch our second season, featuring coffee from additional coffee-growing regions of Costa Rica. + +**Want to bring CofiBlocks to your region?** + +- [Contact Us ](mailto:info@cofiblocks.com) + +## 🌎 Movement +``` +CofiBlocks is more than just a coffee marketplace; it's a movement for a more equitable and sustainable coffee future. With a strong foundation and a passionate community, CofiBlocks is poised to transform the way coffee is produced, distributed, and enjoyed worldwide. Whether you're a coffee enthusiast or a blockchain believer, CofiBlocks invites you to join their journey. +``` + +## πŸ“Œ Fundamental Links Info + +- [Learning about modern collaborative businesses with CofiBlocks](https://medium.com/cambiatus/conociendo-de-modernos-negocios-colaborativos-con-cofiblocks-88ed3ddfa88a) + +- [CofiBlocks: Innovating the Coffee Industry in Costa Rica with Web3](https://mirror.xyz/0xF574753688ABf9740660DFb02E84E4599CA6Eb87/QneFnlPqTRuV_3jt7Kd6foBfFokOKN6zNcJCqpj_xkw) + + + +- [Pitch Deck](https://docs.google.com/presentation/d/16zPeDC-6fMaCRRpaQCTPPIn_ZKdI0d9ZArkTxd2wmAA/edit#slide=id.p1) +## πŸ‘₯ Meet the Team + +### β˜• **FOUNDERS** + +### Omar Hurtado Munguia +**Co-founder** + +Omar's journey from coffee picker to coffee tour guide in PoΓ‘s de Alajuela inspired the creation of CofiBlocks. He brings firsthand knowledge of the challenges small producers face. + +- [History](https://www.youtube.com/watch?v=OjymLl3zKss) + +### Karla CΓ³rdoba Brenes +**Co-founder** + +Karla contributes her extensive experience in blockchain technology and impact-driven solutions. + +### Ranulfo Paiva Sobrinho +**Co-founder** + +Ranulfo's background in blockchain development and collaborative economies helps drive the technical and organizational aspects of CofiBlocks. + +# + +### πŸ’» **MAINTAINERS** + +### Alberto - Brolag +**Full Stack Developer Web3** + +- [Brolag GitHub](https://github.com/brolag) +- [Twitter](https://x.com/brolag) + +### Erick - Evgongora +**Full Stack Developer Web3** + +- [Erick GitHub](https://github.com/evgongora) +- [Twitter](https://x.com/3rickDev) + +### Randall Valenciano +**Full Stack Developer Web3** + +- [Randall GitHub](https://github.com/rvalenciano) +- [Twitter](https://x.com/Ravf226) + +## πŸŽ‰ Join the Community + +- Website: [CofiBlocks Website](https://cofiblocks.com) +- X: [Follow us on Twitter](https://twitter.com/cofiblocks) +- Telegram ODHack: [Join our Telegram ODHACK Group](https://t.me/cofiblocksodhack) +- Telegram for Devs: [Join our Telegram Devs Cofi Group](https://t.me/cofiblocksodhack) + +## πŸ› οΈ **Getting Started** -### Prerequisites +### **Prerequisites** - Node.js (>= 18) - Bun package manager (bun@1.1.24) - Prisma -### Installation +### **Crate file** + +`/cofiblocks/apps/web/sql/init.sql` + +- And REPLACE MYSQL_USER with your mysql user in the .env +```bash +GRANT CREATE ON *.* TO ''@'%'; +GRANT ALL PRIVILEGES ON *.* TO ''@'%'; +``` + +### **Docker-compose.yml** + +Add this to your docker-compose.yml file: + +```bash +version: '3.8' +services: + db: + image: mysql:8.4 + container_name: mysql + restart: always + env_file: + - ./apps/web/.env + ports: + - '3306:3306' + healthcheck: + test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 --password="$$(cat /run/secrets/db-password)" --silent'] + interval: 3s + retries: 5 + start_period: 30s + volumes: + - mysql-data:/var/lib/mysql + - ./apps/web/sql/init.sql:/docker-entrypoint-initdb.d/init.sql + +volumes: + mysql-data: +``` + +### **Installation** 1. Clone the repository: ```bash @@ -17,22 +131,45 @@ CofiBlocks is the first Collaborative Business connecting traditional coffee-gro cd marketplace ``` + - Run `docker compose up`, to access the database. + 2. Install dependencies: ```bash bun install ``` - -3. Generate the Prisma client: +3. Rename + ```bash + mv .env.example to .env + ``` + **And add this in your .env file:** + + ```bash + MYSQL_ROOT_PASSWORD= + MYSQL_DATABASE= + MYSQL_USER= + MYSQL_PASSWORD= + + DATABASE_URL="mysql://${MYSQL_USER}:${MYSQL_PASSWORD + }@localhost:3306/${MYSQL_DATABASE}? + connect_timeout=10" + ``` + **Important:** + - Add and run `docker compose up` in this part. + +4. Generate the Prisma client: ```bash bun prisma generate ``` -4. Run the development server: +5. Run the development server: ```bash bun turbo dev ``` -### Project Structure +**Make sure the values in the .env file are configured correctly for your environment.** + + +### **Project Structure** The project is organized using workspaces: @@ -46,70 +183,34 @@ Key scripts include: - `db:migrate`: Apply database migrations. - `db:seed`: Seed the database with initial data. -## Contributing - -We welcome contributions from the community! Here's how you can help: +### **Key Technologies** -1. Fork the repository and create your branch: +1. **StarkNet** ```bash - git checkout -b feature/amazing-feature + StarkNet is a decentralized, permissionless Layer 2 solution for Ethereum. It uses ZK-STARKs (zero-knowledge proofs) to enable fast and cost-efficient transactions while ensuring security and scalability. Developers can deploy smart contracts, and users benefit from significantly reduced gas fees compared to Ethereum's mainnet. ``` - -2. Commit your changes: +2. **Prisma** ```bash - git commit -m "Add amazing feature" + Prisma is a modern database toolkit for Node.js and TypeScript. It provides an ORM (Object-Relational Mapping) that simplifies working with databases, allowing developers to define models and query data in a type-safe way. Prisma supports multiple databases, including PostgreSQL, MySQL, and MongoDB ``` - -3. Push to the branch: +3. **Bun** ```bash - git push origin feature/amazing-feature + Bun is an all-in-one JavaScript runtime that competes with Node.js and Deno. It’s built for performance and includes a fast bundler, transpiler, and package manager. Bun aims to speed up development workflows, reduce dependency on third-party tools, and execute JavaScript and TypeScript quickly. It’s designed to handle server-side apps, scripts, and front-end tooling. ``` -4. Open a Pull Request. - -## Code of Conduct +## πŸ“š **Code of Conduct** We are committed to creating a welcoming and inclusive environment. Please read our [Community Guidelines](COMMUNITY_GUIDELINES.md) to ensure a positive experience for everyone involved. -## πŸŽ‰ Join the Community - -- Website: [CofiBlocks Website](https://cofiblocks.com) -- Twitter: [Follow us on Twitter](https://twitter.com/cofiblocks) - -## πŸš€ Roadmap - -### 2022-2023 Season -Thank you to everyone who participated in our first season and enjoyed coffee from the slopes of VolcΓ‘n PoΓ‘s. - -### 2024-2025 Season -We are preparing to launch our second season, featuring coffee from additional coffee-growing regions of Costa Rica. - -Want to bring CofiBlocks to your region? [Contact Us](mailto:info@cofiblocks.com) - -## πŸ‘₯ Meet the Team - -### Omar Hurtado Munguia -**Co-founder** - -Omar's journey from coffee picker to coffee tour guide in PoΓ‘s de Alajuela inspired the creation of CofiBlocks. He brings firsthand knowledge of the challenges small producers face. - -### Karla CΓ³rdoba Brenes -**Co-founder** - -Karla contributes her extensive experience in blockchain technology and impact-driven solutions. - -### Ranulfo Paiva Sobrinho -**Co-founder** - -Ranulfo's background in blockchain development and collaborative economies helps drive the technical and organizational aspects of CofiBlocks. - ## πŸ€– Development Resources -### CofiBlocks Dev Assistant +### **CofiBlocks Dev Assistant** We've created a custom GPT to assist with development tasks related to CofiBlocks. This AI-powered assistant is designed to help with coding, answer questions about our tech stack, and provide guidance on best practices. -Access the CofiBlocks Dev Assistant here: [CofiBlocks Dev Assistant](https://chatgpt.com/g/g-JIRAV36d5-cofiblocks-dev-assistant) +Access the CofiBlocks Dev Assistant here: + +- [CofiBlocks Dev Assistant](https://chatgpt.com/g/g-JIRAV36d5-cofiblocks-dev-assistant) Features: - Coding assistance for our tech stack (Next.js, TypeScript, TailwindCSS, etc.) @@ -120,18 +221,27 @@ Features: Feel free to use this resource during your development process to streamline your workflow and get quick answers to your questions. -### Prompt Guide +### **Prompt Guide** To streamline our development process and maintain consistency across the project, we've created a comprehensive prompt guide. This guide covers various custom prompts designed to assist with component generation, code-related tasks, and information retrieval. -Key features of our prompt guide include: +**Key features of our prompt guide include:** -- Instructions for using SudoLang, a powerful pseudocode language for AI collaboration +- Instructions for using SudoLang, a powerful pseudocode language for AI collaboration. - Detailed usage guidelines for our custom prompts: - - V0PromptWithComponent for generating v0.dev prompts - - TailwindReactComponentGenerator for creating React components with Tailwind CSS - - PerplexityBot for efficient information searches + ``` + V0PromptWithComponent for generating v0.dev prompts + ``` + ``` + TailwindReactComponentGenerator for creating React components with Tailwind CSS + ``` + ``` + PerplexityBot for efficient information searches + ``` - Best practices for prompt usage and troubleshooting tips -For full details and usage instructions, please refer to our [Prompt Guide](docs/prompt-guide.md). +**For full details and usage instructions, please refer to our** + +- [Prompt Guide](docs/prompt-guide.md). +# \ No newline at end of file From d9acfd4da0a60e22cfc34bd8a0c496d112f660e3 Mon Sep 17 00:00:00 2001 From: Lazarus Date: Sun, 1 Dec 2024 13:49:24 -0600 Subject: [PATCH 08/27] [feat] Add complete UI translation and localize components (EN, ES, PT) (#75) --- apps/web/next-i18next.config.js | 2 +- apps/web/public/locales/en/common.json | 273 ++++++++++++++++- apps/web/public/locales/es/common.json | 277 ++++++++++++++++- apps/web/public/locales/pt/common.json | 281 +++++++++++++++++- .../app/_components/features/FarmModal.tsx | 17 +- .../app/_components/features/FilterModal.tsx | 33 +- .../app/_components/features/LogoutModal.tsx | 12 +- .../_components/features/OrderListItem.tsx | 9 +- .../features/OrderListPriceItem.tsx | 11 +- .../app/_components/features/ProducerInfo.tsx | 44 +-- .../_components/features/ProductCatalog.tsx | 38 ++- .../_components/features/ProductDetails.tsx | 36 ++- .../features/ProductStatusDetails.tsx | 3 - .../app/_components/features/ProfileCard.tsx | 21 +- .../_components/features/ProfileOptions.tsx | 26 +- .../app/_components/features/SearchBar.tsx | 9 +- .../features/SelectionTypeCard.tsx | 17 +- .../app/_components/features/ShoppingCart.tsx | 6 +- .../features/StatusUpdateModal.tsx | 8 +- .../_components/features/UserWalletsModal.tsx | 4 +- apps/web/src/app/layout.tsx | 14 +- apps/web/src/app/marketplace/page.tsx | 12 +- apps/web/src/app/page.tsx | 24 +- apps/web/src/app/product/[id]/page.tsx | 4 +- apps/web/src/app/shopping-cart/page.tsx | 26 +- apps/web/src/app/user-profile/page.tsx | 15 +- apps/web/src/app/user/collectibles/page.tsx | 27 +- .../user/edit-profile/farm-profile/page.tsx | 46 ++- .../app/user/edit-profile/my-profile/page.tsx | 42 +-- apps/web/src/app/user/edit-profile/page.tsx | 18 +- apps/web/src/app/user/favorites/page.tsx | 18 +- apps/web/src/app/user/my-claims/[id]/page.tsx | 18 +- apps/web/src/app/user/my-claims/page.tsx | 46 +-- apps/web/src/app/user/my-coffee/page.tsx | 8 +- apps/web/src/app/user/my-orders/[id]/page.tsx | 18 +- apps/web/src/app/user/my-orders/page.tsx | 38 +-- apps/web/src/app/user/my-sales/page.tsx | 49 +-- .../web/src/app/user/register-coffee/page.tsx | 49 +-- apps/web/src/app/user/settings/page.tsx | 60 ++-- apps/web/src/i18n.ts | 19 +- apps/web/src/middleware.ts | 18 +- .../src/server/api/routers/mockProducts.ts | 62 ++-- apps/web/src/types/index.ts | 10 +- bun.lockb | Bin 623760 -> 599408 bytes packages/ui/src/alert.tsx | 9 +- packages/ui/src/carousel.tsx | 4 +- packages/ui/src/carouselCard.tsx | 11 +- packages/ui/src/nftCard.tsx | 4 +- packages/ui/src/productCard.tsx | 12 +- 49 files changed, 1376 insertions(+), 432 deletions(-) diff --git a/apps/web/next-i18next.config.js b/apps/web/next-i18next.config.js index 4e0c7ac..f616366 100644 --- a/apps/web/next-i18next.config.js +++ b/apps/web/next-i18next.config.js @@ -2,6 +2,6 @@ export default { i18n: { defaultLocale: "en", locales: ["en", "es", "pt"], - localeDetection: false, + localeDetection: true, }, }; diff --git a/apps/web/public/locales/en/common.json b/apps/web/public/locales/en/common.json index a007329..a43b0d3 100644 --- a/apps/web/public/locales/en/common.json +++ b/apps/web/public/locales/en/common.json @@ -2,11 +2,282 @@ "welcome_to": "Welcome to", "sign": "Sign", "disconnect": "Disconnect", + "connect_wallet": "Connect {{walletName}}", + "argent_x": "Argent X", + "error_signing_message": "Error signing the message", "tag_new": "New", "welcome_coffee_lover": "Welcome Coffee Lover", "featured": "Featured", "find_best_coffee": "Find Best Coffee", "popular": "Popular", - "discover_unique_blends": "Discover Unique Blends" + "discover_unique_blends": "Discover Unique Blends", + + "producing_since": "producing coffee since {{since}}", + "bio_title": "Bio", + "experiences_title": "Experiences", + "good_practices_title": "Good practices", + "edit_button": "Edit", + + "settings": "Settings", + "language": "Language", + "apply": "Apply", + + "filter_by": "Filter by", + "clear": "Clear", + "roast_level": "Roast level", + "region": "Region", + "order_by": "Order by", + "strength": { + "light": "Light", + "medium": "Medium", + "strong": "Strong", + "extra_strong": "Extra Strong", + "mild": "Mild" + }, + "processes": { + "honey": "Honey", + "washed": "Washed", + "natural": "Natural" + }, + "regions": { + "region_a": "Region A", + "region_b": "Region B", + "region_c": "Region C" + }, + "order": { + "highest_price": "Highest price", + "lowest_price": "Lowest price" + }, + + "disconnect_confirmation": "Do you want to disconnect?", + "yes_disconnect": "Yes, disconnect", + "cancel": "Cancel", + "logout_logic": "Please implement logout functionality.", + + "product_image_alt": "Image of {{productName}}", + "order_status": { + "delivered": "Delivered", + "pending": "Pending", + "canceled": "Canceled", + "paid": "Paid", + "prepared": "Prepared", + "shipped": "Shipped" + }, + "ordered_by": "Ordered by {{name}}", + "price_with_currency": "{{price}} USD", + + "my_collectibles": "My collectibles", + "collectible_title_1": "Specialty Coffee 1", + "collectible_description_1": "Description of Specialty Coffee 1.", + "collectible_image_alt_1": "Package of Specialty Coffee 1", + "collectible_title_2": "Specialty Coffee 2", + "collectible_description_2": "Description of Specialty Coffee 2.", + "collectible_image_alt_2": "Package of Specialty Coffee 2", + "collectible_title_3": "Specialty Coffee 3", + "collectible_description_3": "Description of Specialty Coffee 3.", + "collectible_image_alt_3": "Package of Specialty Coffee 3", + + "product_name_1": "Specialty Coffee 1", + "product_description_1": "Description of Specialty Coffee 1.", + "product_image_alt_1": "Package of Specialty Coffee 1", + "product_name_2": "Specialty Coffee 2", + "product_description_2": "Description of Specialty Coffee 2.", + "product_image_alt_2": "Package of Specialty Coffee 2", + "product_name_3": "Specialty Coffee 3", + "product_description_3": "Description of Specialty Coffee 3.", + "product_image_alt_3": "Package of Specialty Coffee 3", + "product_name_4": "Specialty Coffee 4", + "product_description_4": "Description of Specialty Coffee 4.", + "product_image_alt_4": "Package of Specialty Coffee 4", + "product_name_5": "Specialty Coffee 5", + "product_description_5": "Description of Specialty Coffee 5.", + "product_image_alt_5": "Package of Specialty Coffee 5", + "product_name_6": "Specialty Coffee 6", + "product_description_6": "Description of Specialty Coffee 6.", + "product_image_alt_6": "Package of Specialty Coffee 6", + "product_name_7": "Specialty Coffee 7", + "product_description_7": "Description of Specialty Coffee 7.", + "product_image_alt_7": "Package of Specialty Coffee 7", + + "products_found": "{{count}} products found", + "clear_search": "Clear search", + "no_results_image_alt": "No results", + + "farm": "Farm", + "region_by_farm": "{{region}} by {{farmName}}", + "per_unit": "/unit", + + "alert": { + "success": "Operation completed successfully!", + "error": "An error occurred. Please try again.", + "info": "Here is some important information.", + "dismiss_button": "Dismiss" + }, + + "go_to_slide": "Go to slide {{index}}", + + "chat_with_seller": "Chat with the seller", + "bags_available": "Bags Available", + "bags": "bags", + "unit_price": "Unit price ({{weight}})", + "sold_out": "SOLD OUT", + "add_to_favorites": "Add to favorites", + "remove_from_favorites": "Remove from favorites", + "edit_my_farm": "Edit my farm", + "process": "Process", + + "about_the_producer": "About the producer", + "farm_bio_placeholder": "Farm bio description", + "farm_experiences_placeholder": "Farm experiences", + "farm_good_practices_placeholder": "Farm good practices", + "producer_avatar_alt": "Producer Avatar", + "farm_icon_alt": "Farm icon", + "reviews": "Reviews", + "reviews_icon_alt": "Reviews icon", + "star_icon_alt": "Star {{starIndex}}", + "sales_on_cofiblocks": "Sales on Cofiblocks", + "region_icon_alt": "Region icon", + "altitude_icon_alt": "Altitude icon", + "coordinates_icon_alt": "Coordinates icon", + "website_icon_alt": "Website icon", + "altitude": "Altitude", + "altitude_value": "{{altitude}} meters", + "coordinates": "Coordinates", + "website": "Website", + "sales_icon_alt": "Sales icon", + + "select_coffee_type": "Select Coffee Type", + "coffee_type": { + "bean": "Bean", + "grounded": "Grounded" + }, + "total": "total", + "adding_to_cart": "Adding to cart...", + "add_to_cart": "Add to cart", + "shopping_cart_title": "My cart", + "cart_empty_message": "Your cart is empty", + "quantity_label": "quantity", + "total_label": "TOTAL", + "buy_button": "Buy", + "remove_confirmation_title": "Do you want to remove?", + "remove_confirmation_yes": "Yes, remove", + "aria_label_go_back": "Go back", + "aria_label_remove_item": "Remove {{itemName}} from cart", + + "search_placeholder": "Search for your coffee", + "open_filters": "Open filters", + "natural_process": "Natural", + + "edit_my_farm_profile": "Edit my farm profile", + "loading": "Loading...", + "farm_profile_image": "Farm Profile", + "no_image": "No Image", + "choose_photo": "Choose photo", + "farm_name": "Farm Name", + "altitude_meters": "Altitude (meters)", + "save_changes": "Save Changes", + "farm_name_required": "Farm name is required", + "region_required": "Region is required", + "altitude_positive": "Altitude must be a positive number", + "coordinates_invalid": "Invalid coordinates format", + "url_invalid": "Invalid URL", + "farm_profile_updated": "Farm profile updated", + "error_updating_farm": "An error occurred while updating the farm profile", + "user_farm_not_found": "User farm not found. Please contact support.", + "implement_image_upload": "Implement image upload logic", + + "New": "New", + "welcome Coffee Lover": "Welcome Coffee Lover", + "Featured": "Featured", + "Find Best Coffee": "Encontre o Melhor CafΓ©", + "Popular": "Popular", + "Discover Unique Blends": "Discover Unique Blends", + "Paid": "Paid", + "Delivered": "Delivered", + "delivered": "Delivered", + + "user_profile": "User Profile", + "lover": "Lover", + "contributor": "Contributor", + "producer": "Producer", + "united_states": "United States", + "since": "Since", + + "edit_my_profile": "Edit my profile", + "profile_image": "Profile", + "full_name": "Full Name", + "email": "Email", + "physical_address": "Physical Address", + "full_name_required": "Full name is required", + "invalid_email": "Invalid email", + "address_required": "Address is required", + "profile_updated": "Profile updated", + "invalidate_user_data_failed": "Failed to invalidate user data", + + "edit_profile": "Edit profile", + "my_coffee": "My Coffee", + "my_sales": "My Sales", + "my_claims": "My Claims", + "my_orders": "My Orders", + "favorite_products": "Favorite products", + "wallet": "Wallet", + "wallets": "Wallets", + "log_out": "Log out", + + "variety": "Variety", + "badge_text": "Badge Text", + + "search_seller_placeholder": "Search for seller's name", + "date": "Date: {{date}}", + "status": "Status", + "sample_product": "Sample Product", + "strong": "Strong", + "grounded": "Grounded", + "delivery": "Delivery", + + "october_18": "October 18", + "september_20": "September 20", + "product_name": "Sample Product", + + "buyer1_fullname": "Buyer 1", + "buyer2_fullname": "Buyer 2", + "seller1_fullname": "Seller 1", + "seller2_fullname": "Seller 2", + "delivery_method_label": "Delivery method", + "delivery_method": { + "address": "Address", + "meetup": "Meetup" + }, + "quantity_with_unit": "{{count}} {{unit}}", + "total_balance_receivable": "Total Balance Receivable", + "recieve": "Receive", + "history": "History", + "my_sellers": "My Sellers", + "offer_coffee": "Offer my coffee", + "sell_your_coffee": "Would you like to sell your coffee?", + "register_coffee": "Register my Coffee", + "choose_coffee_photo": "Choose coffee photo", + "coffee_variety": "Coffee variety", + "type_here": "Type here", + "coffee_description": "Coffee description", + "coffee_score": "Coffee Score", + "not_mandatory": "not mandatory", + "score_placeholder": "Enter a score between 0 and 100", + "operating_fee": "Operating fee per bag", + "price_per_bag": "Price per bag", + "enter_price_placeholder": "Enter price (e.g. 25.99)", + "producer_value_per_bag": "Value allocated to the producer per bag:", + "total_sales_value_per_bag": "Total sales value (product + fee) per bag:", + "save_and_publish": "Save and publish", + "profile_option_image_alt": "Image for {{label}}", + "details": "Details", + "shopping_cart": "Shopping Cart", + "remove": "Remove", + + "language_name": { + "en": "English", + "es": "Spanish", + "pt": "Portuguese" + } } diff --git a/apps/web/public/locales/es/common.json b/apps/web/public/locales/es/common.json index a056a8e..44de9ab 100644 --- a/apps/web/public/locales/es/common.json +++ b/apps/web/public/locales/es/common.json @@ -2,11 +2,282 @@ "welcome_to": "Bienvenido a", "sign": "Firmar", "disconnect": "Desconectar", + "connect_wallet": "Conectar {{walletName}}", + "argent_x": "Argent X", + "error_signing_message": "Error al firmar el mensaje", - "welcome_coffee_lover": "Bienvenido amante del cafΓ©", "tag_new": "Nuevo", + "welcome_coffee_lover": "Bienvenido Amante del CafΓ©", "featured": "Destacado", - "find_best_coffee": "Encuentra el mejor cafΓ©", + "find_best_coffee": "Encuentra el Mejor CafΓ©", "popular": "Popular", - "discover_unique_blends": "Descubre mezclas ΓΊnicas" + "discover_unique_blends": "Descubre Mezclas Únicas", + + "producing_since": "produciendo cafΓ© desde {{since}}", + "bio_title": "BiografΓ­a", + "experiences_title": "Experiencias", + "good_practices_title": "Buenas prΓ‘cticas", + "edit_button": "Editar", + + "settings": "Configuraciones", + "language": "Idioma", + "apply": "Aplicar", + + "filter_by": "Filtrar por", + "clear": "Limpiar", + "roast_level": "Grado de tueste", + "region": "RegiΓ³n", + "order_by": "Ordenar por", + "strength": { + "light": "Suave", + "medium": "Medio", + "strong": "Fuerte", + "extra_strong": "Extra Fuerte", + "mild": "Ligero" + }, + "processes": { + "honey": "Miel", + "washed": "Lavado", + "natural": "Natural" + }, + "regions": { + "region_a": "RegiΓ³n A", + "region_b": "RegiΓ³n B", + "region_c": "RegiΓ³n C" + }, + "order": { + "highest_price": "Precio mΓ‘s alto", + "lowest_price": "Precio mΓ‘s bajo" + }, + + "disconnect_confirmation": "ΒΏDesea desconectarse?", + "yes_disconnect": "SΓ­, desconectar", + "cancel": "Cancelar", + "logout_logic": "Por favor, implemente la funcionalidad de cierre de sesiΓ³n.", + + "product_image_alt": "Imagen de {{productName}}", + "order_status": { + "delivered": "Entregado", + "pending": "Pendiente", + "canceled": "Cancelado", + "paid": "Pagado", + "prepared": "Preparado", + "shipped": "Enviado" + }, + "ordered_by": "Pedido por {{name}}", + "price_with_currency": "{{price}} USD", + + "my_collectibles": "Mis coleccionables", + "collectible_title_1": "CafΓ© Especialidad 1", + "collectible_description_1": "DescripciΓ³n del CafΓ© Especialidad 1.", + "collectible_image_alt_1": "Paquete de CafΓ© Especialidad 1", + "collectible_title_2": "CafΓ© Especialidad 2", + "collectible_description_2": "DescripciΓ³n del CafΓ© Especialidad 2.", + "collectible_image_alt_2": "Paquete de CafΓ© Especialidad 2", + "collectible_title_3": "CafΓ© Especialidad 3", + "collectible_description_3": "DescripciΓ³n del CafΓ© Especialidad 3.", + "collectible_image_alt_3": "Paquete de CafΓ© Especialidad 3", + + "product_name_1": "CafΓ© Especialidad 1", + "product_description_1": "DescripciΓ³n del CafΓ© Especialidad 1.", + "product_image_alt_1": "Paquete de CafΓ© Especialidad 1", + "product_name_2": "CafΓ© Especialidad 2", + "product_description_2": "DescripciΓ³n del CafΓ© Especialidad 2.", + "product_image_alt_2": "Paquete de CafΓ© Especialidad 2", + "product_name_3": "CafΓ© Especialidad 3", + "product_description_3": "DescripciΓ³n del CafΓ© Especialidad 3.", + "product_image_alt_3": "Paquete de CafΓ© Especialidad 3", + "product_name_4": "CafΓ© Especialidad 4", + "product_description_4": "DescripciΓ³n del CafΓ© Especialidad 4.", + "product_image_alt_4": "Paquete de CafΓ© Especialidad 4", + "product_name_5": "CafΓ© Especialidad 5", + "product_description_5": "DescripciΓ³n del CafΓ© Especialidad 5.", + "product_image_alt_5": "Paquete de CafΓ© Especialidad 5", + "product_name_6": "CafΓ© Especialidad 6", + "product_description_6": "DescripciΓ³n del CafΓ© Especialidad 6.", + "product_image_alt_6": "Paquete de CafΓ© Especialidad 6", + "product_name_7": "CafΓ© Especialidad 7", + "product_description_7": "DescripciΓ³n del CafΓ© Especialidad 7.", + "product_image_alt_7": "Paquete de CafΓ© Especialidad 7", + + "products_found": "{{count}} productos encontrados", + "clear_search": "Limpiar bΓΊsqueda", + "no_results_image_alt": "Sin resultados", + + "farm": "Granja", + "region_by_farm": "{{region}} por {{farmName}}", + "per_unit": "/unidad", + + "alert": { + "success": "Β‘OperaciΓ³n completada con Γ©xito!", + "error": "OcurriΓ³ un error. Por favor, intente nuevamente.", + "info": "AquΓ­ hay informaciΓ³n importante.", + "dismiss_button": "Cerrar" + }, + + "go_to_slide": "Ir a la diapositiva {{index}}", + + "chat_with_seller": "Chatear con el vendedor", + "bags_available": "Bolsas disponibles", + "bags": "bolsas", + "unit_price": "Precio por unidad ({{weight}})", + "sold_out": "AGOTADO", + "add_to_favorites": "Agregar a favoritos", + "remove_from_favorites": "Eliminar de favoritos", + "edit_my_farm": "Editar mi granja", + "process": "Proceso", + + "about_the_producer": "Acerca del productor", + "farm_bio_placeholder": "DescripciΓ³n de la biografΓ­a de la granja", + "farm_experiences_placeholder": "Experiencias de la granja", + "farm_good_practices_placeholder": "Buenas prΓ‘cticas de la granja", + "producer_avatar_alt": "Avatar del productor", + "farm_icon_alt": "Ícono de la granja", + "reviews": "ReseΓ±as", + "reviews_icon_alt": "Ícono de reseΓ±as", + "star_icon_alt": "Estrella {{starIndex}}", + "sales_on_cofiblocks": "Ventas en Cofiblocks", + "region_icon_alt": "Ícono de regiΓ³n", + "altitude_icon_alt": "Ícono de altitud", + "coordinates_icon_alt": "Ícono de coordenadas", + "website_icon_alt": "Ícono de sitio web", + "altitude": "Altitud", + "altitude_value": "{{altitude}} metros", + "coordinates": "Coordenadas", + "website": "Sitio web", + "sales_icon_alt": "Ícono de ventas", + + "select_coffee_type": "Seleccionar tipo de cafΓ©", + "coffee_type": { + "bean": "Grano", + "grounded": "Molido" + }, + "total": "total", + "adding_to_cart": "AΓ±adiendo al carrito...", + "add_to_cart": "AΓ±adir al carrito", + "shopping_cart_title": "Mi carrito", + "cart_empty_message": "Tu carrito estΓ‘ vacΓ­o", + "quantity_label": "cantidad", + "total_label": "TOTAL", + "buy_button": "Comprar", + "remove_confirmation_title": "ΒΏQuieres eliminar?", + "remove_confirmation_yes": "SΓ­, eliminar", + "aria_label_go_back": "Regresar", + "aria_label_remove_item": "Eliminar {{itemName}} del carrito", + + "search_placeholder": "Busca tu cafΓ©", + "open_filters": "Abrir filtros", + "natural_process": "Natural", + + "edit_my_farm_profile": "Editar el perfil de mi granja", + "loading": "Cargando...", + "farm_profile_image": "Perfil de la Granja", + "no_image": "Sin imagen", + "choose_photo": "Elegir foto", + "farm_name": "Nombre de la Granja", + "altitude_meters": "Altitud (metros)", + "save_changes": "Guardar Cambios", + "farm_name_required": "El nombre de la granja es obligatorio", + "region_required": "La regiΓ³n es obligatoria", + "altitude_positive": "La altitud debe ser un nΓΊmero positivo", + "coordinates_invalid": "Formato de coordenadas invΓ‘lido", + "url_invalid": "URL invΓ‘lida", + "farm_profile_updated": "Perfil de la granja actualizado", + "error_updating_farm": "OcurriΓ³ un error al actualizar el perfil de la granja", + "user_farm_not_found": "No se encontrΓ³ la granja del usuario. Por favor, contacte con soporte.", + "implement_image_upload": "Implementar la lΓ³gica de carga de imΓ‘genes", + + "New": "Nuevo", + "welcome Coffee Lover": "Bienvenido Amante del CafΓ©", + "Featured": "Destacado", + "Find Best Coffee": "Encuentra el Mejor CafΓ©", + "Popular": "Popular", + "Discover Unique Blends": "Descubre Mezclas Únicas", + "Paid": "Pagado", + "Delivered": "Entregado", + "delivered": "Entregado", + + "user_profile": "Perfil de usuario", + "lover": "Amante", + "contributor": "Contribuyente", + "producer": "Productor", + "united_states": "Estados Unidos", + "since": "Desde", + + "edit_my_profile": "Editar mi perfil", + "profile_image": "Perfil", + "full_name": "Nombre completo", + "email": "Correo electrΓ³nico", + "physical_address": "DirecciΓ³n fΓ­sica", + "full_name_required": "El nombre completo es obligatorio", + "invalid_email": "Correo electrΓ³nico invΓ‘lido", + "address_required": "La direcciΓ³n es obligatoria", + "profile_updated": "Perfil actualizado", + "invalidate_user_data_failed": "No se pudo invalidar los datos del usuario", + + "edit_profile": "Editar perfil", + "my_coffee": "Mi CafΓ©", + "my_sales": "Mis Ventas", + "my_claims": "Mis Reclamaciones", + "my_orders": "Mis Pedidos", + "favorite_products": "Productos favoritos", + "wallet": "Billetera", + "wallets": "Billeteras", + "log_out": "Cerrar sesiΓ³n", + + "variety": "Variedad", + "badge_text": "Texto del distintivo", + + "search_seller_placeholder": "Buscar por nombre del vendedor", + "date": "Fecha: {{date}}", + "status": "Estado", + "sample_product": "Producto de muestra", + "strong": "Fuerte", + "grounded": "Molido", + "delivery": "Entrega", + + "october_18": "18 de Octubre", + "september_20": "20 de Septiembre", + "product_name": "Producto de muestra", + + "buyer1_fullname": "Comprador 1", + "buyer2_fullname": "Comprador 2", + "seller1_fullname": "Vendedor 1", + "seller2_fullname": "Vendedor 2", + "delivery_method_label": "MΓ©todo de EnvΓ­o", + "delivery_method": { + "address": "DirecciΓ³n", + "meetup": "Encuentro" + }, + "quantity_with_unit": "{{count}} {{unit}}", + "total_balance_receivable": "Saldo total por recibir", + "recieve": "Recibir", + "history": "Historia", + "my_sellers": "Mis Vendedores", + "offer_coffee": "Ofrecer mi CafΓ©", + "sell_your_coffee": "ΒΏTe gustarΓ­a vender tu cafΓ©?", + "register_coffee": "Registrar mi CafΓ©", + "choose_coffee_photo": "Escoger foto para cafΓ©", + "coffee_variety": "Variedad de cafΓ©", + "type_here": "Escribe aquΓ­", + "coffee_description": "DescripciΓ³n del cafΓ©", + "coffee_score": "PuntuaciΓ³n del CafΓ©", + "not_mandatory": "no obligatorio", + "score_placeholder": "Ingresa una puntuaciΓ³n entre 0 y 100", + "operating_fee": "Tarifa de operaciΓ³n por bolsa", + "price_per_bag": "Precio por bolsa", + "enter_price_placeholder": "Ingresar precio (ej. 25.99)", + "producer_value_per_bag": "Valor asignado al productor por bolsa:", + "total_sales_value_per_bag": "Valor total de ventas (producto + comisiΓ³n) por bolsa:", + "save_and_publish": "Guardar y publicar", + "profile_option_image_alt": "Imagen para {{label}}", + "details": "Detalles", + "shopping_cart": "Carrito de Compras", + "remove": "Quitar", + + "language_name": { + "en": "InglΓ©s", + "es": "EspaΓ±ol", + "pt": "PortuguΓ©s" + } } diff --git a/apps/web/public/locales/pt/common.json b/apps/web/public/locales/pt/common.json index c26fad2..a050b1b 100644 --- a/apps/web/public/locales/pt/common.json +++ b/apps/web/public/locales/pt/common.json @@ -2,11 +2,282 @@ "welcome_to": "Bem-vindo a", "sign": "Assinar", "disconnect": "Desconectar", + "connect_wallet": "Conectar {{walletName}}", + "argent_x": "Argent X", + "error_signing_message": "Erro ao assinar a mensagem", - "welcome_coffee_lover": "Bem-vindo amante de cafΓ©", - "tag_new": "New", - "featured": "Destaque", - "find_best_coffee": "Encontre o melhor cafΓ©", + "tag_new": "Novo", + "welcome_coffee_lover": "Bem-vindo Amante de CafΓ©", + "featured": "Em destaque", + "find_best_coffee": "Encontre o Melhor CafΓ©", "popular": "Popular", - "discover_unique_blends": "Descubra misturas ΓΊnicas" + "discover_unique_blends": "Descubra Misturas Únicas", + + "producing_since": "produzindo cafΓ© desde {{since}}", + "bio_title": "Biografia", + "experiences_title": "ExperiΓͺncias", + "good_practices_title": "Boas prΓ‘ticas", + "edit_button": "Editar", + + "settings": "Configuraçáes", + "language": "Idioma", + "apply": "Aplicar", + + "filter_by": "Filtrar por", + "clear": "Limpar", + "roast_level": "NΓ­vel de torra", + "region": "RegiΓ£o", + "order_by": "Ordenar por", + "strength": { + "light": "Suave", + "medium": "MΓ©dio", + "strong": "Forte", + "extra_strong": "Extra Forte", + "mild": "Leve" + }, + "processes": { + "honey": "Mel", + "washed": "Lavado", + "natural": "Natural" + }, + "regions": { + "region_a": "RegiΓ£o A", + "region_b": "RegiΓ£o B", + "region_c": "RegiΓ£o C" + }, + "order": { + "highest_price": "Maior preΓ§o", + "lowest_price": "Menor preΓ§o" + }, + + "disconnect_confirmation": "Deseja se desconectar?", + "yes_disconnect": "Sim, desconectar", + "cancel": "Cancelar", + "logout_logic": "Por favor, implemente a funcionalidade de logout.", + + "product_image_alt": "Imagem de {{productName}}", + "order_status": { + "delivered": "Entregue", + "pending": "Pendente", + "canceled": "Cancelado", + "paid": "Pago", + "prepared": "Preparado", + "shipped": "Enviado" + }, + "ordered_by": "Pedido por {{name}}", + "price_with_currency": "{{price}} USD", + + "my_collectibles": "Meus colecionΓ‘veis", + "collectible_title_1": "CafΓ© Especial 1", + "collectible_description_1": "Descrição do CafΓ© Especial 1.", + "collectible_image_alt_1": "Pacote do CafΓ© Especial 1", + "collectible_title_2": "CafΓ© Especial 2", + "collectible_description_2": "Descrição do CafΓ© Especial 2.", + "collectible_image_alt_2": "Pacote do CafΓ© Especial 2", + "collectible_title_3": "CafΓ© Especial 3", + "collectible_description_3": "Descrição do CafΓ© Especial 3.", + "collectible_image_alt_3": "Pacote do CafΓ© Especial 3", + + "product_name_1": "CafΓ© Especial 1", + "product_description_1": "Descrição do CafΓ© Especial 1.", + "product_image_alt_1": "Pacote do CafΓ© Especial 1", + "product_name_2": "CafΓ© Especial 2", + "product_description_2": "Descrição do CafΓ© Especial 2.", + "product_image_alt_2": "Pacote do CafΓ© Especial 2", + "product_name_3": "CafΓ© Especial 3", + "product_description_3": "Descrição do CafΓ© Especial 3.", + "product_image_alt_3": "Pacote do CafΓ© Especial 3", + "product_name_4": "CafΓ© Especial 4", + "product_description_4": "Descrição do CafΓ© Especial 4.", + "product_image_alt_4": "Pacote do CafΓ© Especial 4", + "product_name_5": "CafΓ© Especial 5", + "product_description_5": "Descrição do CafΓ© Especial 5.", + "product_image_alt_5": "Pacote do CafΓ© Especial 5", + "product_name_6": "CafΓ© Especial 6", + "product_description_6": "Descrição do CafΓ© Especial 6.", + "product_image_alt_6": "Pacote do CafΓ© Especial 6", + "product_name_7": "CafΓ© Especial 7", + "product_description_7": "Descrição do CafΓ© Especial 7.", + "product_image_alt_7": "Pacote do CafΓ© Especial 7", + + "products_found": "{{count}} produtos encontrados", + "clear_search": "Limpar pesquisa", + "no_results_image_alt": "Sem resultados", + + "farm": "Fazenda", + "region_by_farm": "{{region}} por {{farmName}}", + "per_unit": "/unidade", + + "alert": { + "success": "Operação concluΓ­da com sucesso!", + "error": "Ocorreu um erro. Por favor, tente novamente.", + "info": "Aqui estΓ‘ uma informação importante.", + "dismiss_button": "Fechar" + }, + + "go_to_slide": "Ir para o slide {{index}}", + + "chat_with_seller": "Conversar com o vendedor", + "bags_available": "Sacos disponΓ­veis", + "bags": "sacos", + "unit_price": "PreΓ§o por unidade ({{weight}})", + "sold_out": "ESGOTADO", + "add_to_favorites": "Adicionar aos favoritos", + "remove_from_favorites": "Remover dos favoritos", + "edit_my_farm": "Editar minha fazenda", + "process": "Processo", + + "about_the_producer": "Sobre o produtor", + "farm_bio_placeholder": "Descrição da biografia da fazenda", + "farm_experiences_placeholder": "ExperiΓͺncias da fazenda", + "farm_good_practices_placeholder": "Boas prΓ‘ticas da fazenda", + "producer_avatar_alt": "Avatar do produtor", + "farm_icon_alt": "Ícone da fazenda", + "reviews": "Avaliaçáes", + "reviews_icon_alt": "Ícone de avaliaçáes", + "star_icon_alt": "Estrela {{starIndex}}", + "sales_on_cofiblocks": "Vendas na Cofiblocks", + "region_icon_alt": "Ícone de regiΓ£o", + "altitude_icon_alt": "Ícone de altitude", + "coordinates_icon_alt": "Ícone de coordenadas", + "website_icon_alt": "Ícone do site", + "altitude": "Altitude", + "altitude_value": "{{altitude}} metros", + "coordinates": "Coordenadas", + "website": "Site", + "sales_icon_alt": "Ícone de vendas", + + "select_coffee_type": "Selecionar tipo de cafΓ©", + "coffee_type": { + "bean": "GrΓ£o", + "grounded": "MoΓ­do" + }, + "total": "total", + "adding_to_cart": "Adicionando ao carrinho...", + "add_to_cart": "Adicionar ao carrinho", + "shopping_cart_title": "Meu carrinho", + "cart_empty_message": "Seu carrinho estΓ‘ vazio", + "quantity_label": "quantidade", + "total_label": "TOTAL", + "buy_button": "Comprar", + "remove_confirmation_title": "Deseja remover?", + "remove_confirmation_yes": "Sim, remover", + "aria_label_go_back": "Voltar", + "aria_label_remove_item": "Remover {{itemName}} do carrinho", + + "search_placeholder": "Procure seu cafΓ©", + "open_filters": "Abrir filtros", + "natural_process": "Natural", + + "edit_my_farm_profile": "Editar o perfil da minha fazenda", + "loading": "Carregando...", + "farm_profile_image": "Perfil da Fazenda", + "no_image": "Sem imagem", + "choose_photo": "Escolher foto", + "farm_name": "Nome da Fazenda", + "altitude_meters": "Altitude (metros)", + "save_changes": "Salvar alteraçáes", + "farm_name_required": "O nome da fazenda Γ© obrigatΓ³rio", + "region_required": "A regiΓ£o Γ© obrigatΓ³ria", + "altitude_positive": "A altitude deve ser um nΓΊmero positivo", + "coordinates_invalid": "Formato de coordenadas invΓ‘lido", + "url_invalid": "URL invΓ‘lida", + "farm_profile_updated": "Perfil da fazenda atualizado", + "error_updating_farm": "Ocorreu um erro ao atualizar o perfil da fazenda", + "user_farm_not_found": "Fazenda do usuΓ‘rio nΓ£o encontrada. Por favor, contate o suporte.", + "implement_image_upload": "Implementar lΓ³gica de upload de imagens", + + "New": "Novo", + "welcome Coffee Lover": "Bem-vindo Amante de CafΓ©", + "Featured": "Em destaque", + "Find Best Coffee": "Encontre o Melhor CafΓ©", + "Popular": "Popular", + "Discover Unique Blends": "Descubra Misturas Únicas", + "Paid": "Pago", + "Delivered": "Entregue", + "delivered": "Entregue", + + "user_profile": "Perfil do usuΓ‘rio", + "lover": "Amante", + "contributor": "Contribuidor", + "producer": "Produtor", + "united_states": "Estados Unidos", + "since": "Desde", + + "edit_my_profile": "Editar meu perfil", + "profile_image": "Perfil", + "full_name": "Nome completo", + "email": "E-mail", + "physical_address": "EndereΓ§o fΓ­sico", + "full_name_required": "O nome completo Γ© obrigatΓ³rio", + "invalid_email": "E-mail invΓ‘lido", + "address_required": "O endereΓ§o Γ© obrigatΓ³rio", + "profile_updated": "Perfil atualizado", + "invalidate_user_data_failed": "Falha ao invalidar os dados do usuΓ‘rio", + + "edit_profile": "Editar perfil", + "my_coffee": "Meu CafΓ©", + "my_sales": "Minhas Vendas", + "my_claims": "Minhas Reclamaçáes", + "my_orders": "Meus Pedidos", + "favorite_products": "Produtos favoritos", + "wallet": "Carteira", + "wallets": "Carteiras", + "log_out": "Sair", + + "variety": "Variedade", + "badge_text": "Texto do distintivo", + + "search_seller_placeholder": "Procurar pelo nome do vendedor", + "date": "Data: {{date}}", + "status": "Status", + "sample_product": "Produto de amostra", + "strong": "Forte", + "grounded": "MoΓ­do", + "delivery": "Entrega", + + "october_18": "18 de Outubro", + "september_20": "20 de Setembro", + "product_name": "Produto de amostra", + + "buyer1_fullname": "Comprador 1", + "buyer2_fullname": "Comprador 2", + "seller1_fullname": "Vendedor 1", + "seller2_fullname": "Vendedor 2", + "delivery_method_label": "MΓ©todo de Entrega", + "delivery_method": { + "address": "EndereΓ§o", + "meetup": "Encontro" + }, + "quantity_with_unit": "{{count}} {{unit}}", + "total_balance_receivable": "Saldo total a receber", + "recieve": "Receber", + "history": "HistΓ³rico", + "my_sellers": "Meus Vendedores", + "offer_coffee": "Oferecer meu cafΓ©", + "sell_your_coffee": "Gostaria de vender seu cafΓ©?", + "register_coffee": "Registrar meu CafΓ©", + "choose_coffee_photo": "Escolher foto do cafΓ©", + "coffee_variety": "Variedade de cafΓ©", + "type_here": "Digite aqui", + "coffee_description": "Descrição do cafΓ©", + "coffee_score": "Pontuação do CafΓ©", + "not_mandatory": "nΓ£o obrigatΓ³rio", + "score_placeholder": "Insira uma pontuação entre 0 e 100", + "operating_fee": "Taxa de operação por saco", + "price_per_bag": "PreΓ§o por saco", + "enter_price_placeholder": "Insira o preΓ§o (ex. 25.99)", + "producer_value_per_bag": "Valor destinado ao produtor por saco:", + "total_sales_value_per_bag": "Valor total de vendas (produto + taxa) por saco:", + "save_and_publish": "Salvar e publicar", + "profile_option_image_alt": "Imagem para {{label}}", + "details": "Detalhes", + "shopping_cart": "Carrinho de Compras", + "remove": "Remover", + + "language_name": { + "en": "InglΓͺs", + "es": "Espanhol", + "pt": "PortuguΓͺs" + } } diff --git a/apps/web/src/app/_components/features/FarmModal.tsx b/apps/web/src/app/_components/features/FarmModal.tsx index 0a4a35f..1f0978d 100644 --- a/apps/web/src/app/_components/features/FarmModal.tsx +++ b/apps/web/src/app/_components/features/FarmModal.tsx @@ -1,4 +1,5 @@ import Button from "@repo/ui/button"; +import { useTranslation } from "react-i18next"; import BottomModal from "~/app/_components/ui/BottomModal"; interface FarmModalProps { @@ -22,6 +23,8 @@ function FarmModal({ isEditable, onEdit, }: FarmModalProps) { + const { t } = useTranslation("common"); + return (
@@ -32,24 +35,28 @@ function FarmModal({ {farmData.name}

- producing coffee since {farmData.since} + {t("producing_since", { since: farmData.since })}

-

Bio

+

{t("bio_title")}

{farmData.bio}

-

Experiences

+

+ {t("experiences_title")} +

{farmData.experiences}

-

Good practices

+

+ {t("good_practices_title")} +

{farmData.goodPractices}

@@ -57,7 +64,7 @@ function FarmModal({ {isEditable && ( )}
diff --git a/apps/web/src/app/_components/features/FilterModal.tsx b/apps/web/src/app/_components/features/FilterModal.tsx index c506207..18f10d7 100644 --- a/apps/web/src/app/_components/features/FilterModal.tsx +++ b/apps/web/src/app/_components/features/FilterModal.tsx @@ -2,6 +2,7 @@ import { XMarkIcon } from "@heroicons/react/24/outline"; import { motion } from "framer-motion"; import { useAtom } from "jotai"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { isLoadingAtom, quantityOfProducts, @@ -15,6 +16,7 @@ interface FilterModalProps { } export default function FilterModal({ isOpen, onClose }: FilterModalProps) { + const { t } = useTranslation(); const [selectedStrength, setSelectedStrength] = useState(""); const [selectedRegion, setSelectedRegion] = useState(""); const [selectedOrder, setSelectedOrder] = useState(""); @@ -39,13 +41,18 @@ export default function FilterModal({ isOpen, onClose }: FilterModalProps) { const { data } = await refetch(); if (data?.productsFound) { - setSearchResults(data.productsFound); - setQuantityProducts(data.productsFound.length); + const products = data.productsFound.map((product) => ({ + ...product, + region: product.region, + })); + setSearchResults(products); + setQuantityProducts(products.length); } else { setSearchResults([]); setQuantityProducts(0); } } catch (error) { + console.error(error); setSearchResults([]); setQuantityProducts(0); } finally { @@ -83,14 +90,14 @@ export default function FilterModal({ isOpen, onClose }: FilterModalProps) { >
-

Filter by

+

{t("filter_by")}

{hasActiveFilters && ( )}
@@ -101,7 +108,7 @@ export default function FilterModal({ isOpen, onClose }: FilterModalProps) {
-

Roast level

+

{t("roast_level")}

{["Light", "Medium", "Strong"].map((strength) => ( ))}
-

Region

+

{t("region")}

{["Region A", "Region B", "Region C"].map((region) => ( ))}
-

Order by

+

{t("order_by")}

{["Highest price", "Lowest price"].map((order) => ( ))}
@@ -174,7 +185,7 @@ export default function FilterModal({ isOpen, onClose }: FilterModalProps) { onClick={handleApply} className="w-full bg-surface-secondary-default text-black py-3 px-4 rounded-lg font-medium hover:bg-yellow-500 transition-colors" > - Apply + {t("apply")}
diff --git a/apps/web/src/app/_components/features/LogoutModal.tsx b/apps/web/src/app/_components/features/LogoutModal.tsx index 9cb9fbf..6d159b7 100644 --- a/apps/web/src/app/_components/features/LogoutModal.tsx +++ b/apps/web/src/app/_components/features/LogoutModal.tsx @@ -1,5 +1,6 @@ import Button from "@repo/ui/button"; import { useRouter } from "next/navigation"; +import { useTranslation } from "react-i18next"; import BottomModal from "~/app/_components/ui/BottomModal"; interface LogoutModalProps { @@ -8,11 +9,12 @@ interface LogoutModalProps { } function LogoutModal({ isOpen, onClose }: LogoutModalProps) { + const { t } = useTranslation(); const router = useRouter(); const handleLogout = () => { // TODO: Implement logout/disconnect logic - alert("Implement logout logic"); + alert(t("logout_logic")); onClose(); router.push("/"); }; @@ -20,16 +22,14 @@ function LogoutModal({ isOpen, onClose }: LogoutModalProps) { return (

- Do you want to disconnect? + {t("disconnect_confirmation")}

diff --git a/apps/web/src/app/_components/features/OrderListItem.tsx b/apps/web/src/app/_components/features/OrderListItem.tsx index 3a45186..2232305 100644 --- a/apps/web/src/app/_components/features/OrderListItem.tsx +++ b/apps/web/src/app/_components/features/OrderListItem.tsx @@ -3,6 +3,7 @@ import { ChevronRightIcon } from "@heroicons/react/24/outline"; import Image from "next/image"; import Link from "next/link"; +import { useTranslation } from "react-i18next"; interface OrderListItemProps { productName: string; @@ -17,6 +18,8 @@ export default function OrderListItem({ status, onClick, }: OrderListItemProps) { + const { t } = useTranslation(); + return (
Product - {status} + {t(`order_status.${status.toLowerCase()}`)}
diff --git a/apps/web/src/app/_components/features/OrderListPriceItem.tsx b/apps/web/src/app/_components/features/OrderListPriceItem.tsx index 0fb900b..4298a74 100644 --- a/apps/web/src/app/_components/features/OrderListPriceItem.tsx +++ b/apps/web/src/app/_components/features/OrderListPriceItem.tsx @@ -3,6 +3,7 @@ import { ChevronRightIcon } from "@heroicons/react/24/outline"; import Image from "next/image"; import Link from "next/link"; +import { useTranslation } from "react-i18next"; interface OrderListItemProps { productName: string; @@ -17,6 +18,8 @@ export default function OrderListPriceItem({ price, onClick, }: OrderListItemProps) { + const { t } = useTranslation(); + return (
Product

{productName}

-

{name}

+

{t("ordered_by", { name })}

- {price} USD + + {t("price_with_currency", { price })} +
diff --git a/apps/web/src/app/_components/features/ProducerInfo.tsx b/apps/web/src/app/_components/features/ProducerInfo.tsx index b8249b5..4b0d683 100644 --- a/apps/web/src/app/_components/features/ProducerInfo.tsx +++ b/apps/web/src/app/_components/features/ProducerInfo.tsx @@ -2,6 +2,7 @@ import { ChevronRightIcon } from "@heroicons/react/24/outline"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { FarmModal } from "./FarmModal"; interface ProducerInfoProps { @@ -33,13 +34,14 @@ export function ProducerInfo({ }: ProducerInfoProps) { const [isFarmModalOpen, setIsFarmModalOpen] = useState(false); const router = useRouter(); + const { t } = useTranslation(); const farmData = { name: farmName, since: farmSince ?? "2020", - bio: farmBio ?? "Farm bio description", - experiences: farmExperiences ?? "Farm experiences", - goodPractices: farmGoodPractices ?? "Farm good practices", + bio: farmBio ?? t("farm_bio_placeholder"), + experiences: farmExperiences ?? t("farm_experiences_placeholder"), + goodPractices: farmGoodPractices ?? t("farm_good_practices_placeholder"), }; const openFarmModal = () => setIsFarmModalOpen(true); @@ -50,14 +52,14 @@ export function ProducerInfo({
Producer Avatar

- About the producer + {t("about_the_producer")}

Farm icon
- Farm + {t("farm")}
@@ -96,14 +98,14 @@ export function ProducerInfo({
Reviews icon
- Reviews + {t("reviews")}
@@ -115,7 +117,7 @@ export function ProducerInfo({ ? "/images/product-details/producer-info/Star-highlighted.svg" : "/images/product-details/producer-info/Star.svg" } - alt={`Star ${starIndex}`} + alt={t("star_icon_alt", { starIndex })} width={24} height={24} /> @@ -128,14 +130,14 @@ export function ProducerInfo({
Sales icon
- Sales on Cofiblocks + {t("sales_on_cofiblocks")}
@@ -148,14 +150,14 @@ export function ProducerInfo({
Location icon
- Region + {t("region")}
@@ -168,19 +170,19 @@ export function ProducerInfo({
Altitude icon
- Altitude + {t("altitude")}
- {altitude} metros + {t("altitude_value", { altitude })}
@@ -190,14 +192,14 @@ export function ProducerInfo({
Coordinates icon
- Coordinates + {t("coordinates")}
@@ -220,14 +222,14 @@ export function ProducerInfo({
Website icon
- Website + {t("website")}
diff --git a/apps/web/src/app/_components/features/ProductCatalog.tsx b/apps/web/src/app/_components/features/ProductCatalog.tsx index 948f794..110988d 100755 --- a/apps/web/src/app/_components/features/ProductCatalog.tsx +++ b/apps/web/src/app/_components/features/ProductCatalog.tsx @@ -5,6 +5,7 @@ import { useAtom } from "jotai"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { isLoadingAtom, quantityOfProducts, @@ -15,6 +16,7 @@ import { api } from "~/trpc/react"; import type { NftMetadata, Product } from "./types"; export default function ProductCatalog() { + const { t } = useTranslation(); const [products, setProducts] = useState([]); const [results, setSearchResults] = useAtom(searchResultsAtom); const [isLoadingResults, setIsLoading] = useAtom(isLoadingAtom); @@ -22,18 +24,16 @@ export default function ProductCatalog() { const [query, setQuery] = useAtom(searchQueryAtom); const router = useRouter(); - // Using an infinite query to fetch products with pagination const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = api.product.getProducts.useInfiniteQuery( { - limit: 3, // Fetch 3 products per request + limit: 3, }, { getNextPageParam: (lastPage) => lastPage.nextCursor, }, ); - // Effect to update local state whenever new data is loaded useEffect(() => { if (data) { const allProducts = data.pages.flatMap((page) => @@ -46,52 +46,48 @@ export default function ProductCatalog() { } }, [data]); - // Handle infinite scroll for fetching more products as the user scrolls down const handleScroll = useCallback(() => { if ( - window.innerHeight + window.scrollY >= document.body.offsetHeight - 100 && // Check if near bottom + window.innerHeight + window.scrollY >= document.body.offsetHeight - 100 && !isFetchingNextPage && hasNextPage ) { - void fetchNextPage(); // Fetch the next set of products + void fetchNextPage(); } }, [isFetchingNextPage, hasNextPage, fetchNextPage]); - // Attach and detach scroll event listeners useEffect(() => { window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); }, [handleScroll]); - // Placeholder for adding products to the cart const accessProductDetails = (productId: number) => { - router.push(`/product/${productId}`); // Navigate to the product details page + router.push(`/product/${productId}`); }; - // Render each product const renderProduct = (product: Product) => { let metadata: NftMetadata | null = null; if (typeof product.nftMetadata === "string") { try { - metadata = JSON.parse(product.nftMetadata) as NftMetadata; // Try to parse if it's a JSON string + metadata = JSON.parse(product.nftMetadata) as NftMetadata; } catch { - metadata = null; // Fallback in case of an error + metadata = null; } } else { - metadata = product.nftMetadata as NftMetadata; // Assign directly if it's already an object + metadata = product.nftMetadata as NftMetadata; } return ( accessProductDetails(product.id)} // Trigger add-to-cart action + badgeText={t(`strength.${product.strength.toLowerCase()}`)} + onClick={() => accessProductDetails(product.id)} /> ); }; @@ -112,13 +108,13 @@ export default function ProductCatalog() { {results.length > 0 ? (
-
{quantity} products found
+
{t("products_found", { count: quantity })}
{results.map((product) => ( @@ -132,7 +128,7 @@ export default function ProductCatalog() { src="/images/NoResultsImage.png" width={700} height={700} - alt="No results" + alt={t("no_results_image_alt")} />
@@ -141,7 +137,7 @@ export default function ProductCatalog() { size="sm" onClick={() => clearSearch()} > - Clear search + {t("clear_search")}
diff --git a/apps/web/src/app/_components/features/ProductDetails.tsx b/apps/web/src/app/_components/features/ProductDetails.tsx index b155b8c..5c002cc 100644 --- a/apps/web/src/app/_components/features/ProductDetails.tsx +++ b/apps/web/src/app/_components/features/ProductDetails.tsx @@ -8,6 +8,7 @@ import { useAtom, useAtomValue } from "jotai"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { addItemAtom, cartItemsAtom } from "~/store/cartAtom"; import { ProducerInfo } from "./ProducerInfo"; import { SelectionTypeCard } from "./SelectionTypeCard"; @@ -39,6 +40,7 @@ export default function ProductDetails({ product }: ProductDetailsProps) { type, process, } = product; + const { t } = useTranslation(); const [quantity, setQuantity] = useState(1); const [isLiked, setIsLiked] = useState(false); const router = useRouter(); @@ -69,7 +71,9 @@ export default function ProductDetails({ product }: ProductDetailsProps) {
{name}
} + title={ +
{t(product.name)}
+ } showBackButton onBackClick={() => router.back()} showCart={true} @@ -80,7 +84,7 @@ export default function ProductDetails({ product }: ProductDetailsProps) { onClick={() => setIsLiked(!isLiked)} className="p-2" aria-label={ - isLiked ? "Remove from favorites" : "Add to favorites" + isLiked ? t("remove_from_favorites") : t("add_to_favorites") } > {isLiked ? ( @@ -105,39 +109,43 @@ export default function ProductDetails({ product }: ProductDetailsProps) {
-

{name}

+

+ {t(product.name)} +

- {product.description} + {t(product.description)}

console.log("Open chat")} />
@@ -173,7 +181,7 @@ export default function ProductDetails({ product }: ProductDetailsProps) { />
diff --git a/apps/web/src/app/_components/features/ProductStatusDetails.tsx b/apps/web/src/app/_components/features/ProductStatusDetails.tsx index ffe1f3e..30a9745 100644 --- a/apps/web/src/app/_components/features/ProductStatusDetails.tsx +++ b/apps/web/src/app/_components/features/ProductStatusDetails.tsx @@ -42,8 +42,6 @@ const orderStatusSchema = z.object({ status: z.nativeEnum(StatusStepsEnum), }); -type FormValues = z.infer; - interface ProductStatusDetailsProps { productDetails: ProductDetails; isProducer: boolean; @@ -72,7 +70,6 @@ export default function ProductStatusDetails({ const statusStepsKeys = Object.keys(StatusStepsEnum); - const openOrderStatusModal = () => setIsOrderStatusModalOpen(true); const closeOrderStatusModal = () => setIsOrderStatusModalOpen(false); const onSubmit = (data: { status: StatusStepsEnum }) => { diff --git a/apps/web/src/app/_components/features/ProfileCard.tsx b/apps/web/src/app/_components/features/ProfileCard.tsx index 437756c..52e6337 100644 --- a/apps/web/src/app/_components/features/ProfileCard.tsx +++ b/apps/web/src/app/_components/features/ProfileCard.tsx @@ -1,6 +1,7 @@ import Image from "next/image"; +import { useTranslation } from "react-i18next"; -type Badge = "Lover" | "Contributor" | "Producer"; +type Badge = "lover" | "contributor" | "producer"; type UserProfile = { name: string; @@ -15,7 +16,10 @@ type ProfileCardProps = { }; function ProfileCard({ user }: ProfileCardProps) { - const allBadges: Badge[] = ["Lover", "Contributor", "Producer"]; + const { t } = useTranslation(); + + // Define all badges and their translation keys + const allBadges: Badge[] = ["lover", "contributor", "producer"]; return (
@@ -32,7 +36,9 @@ function ProfileCard({ user }: ProfileCardProps) {

{user.name}

{user.country}

-

Since {user.memberSince}

+

+ {t("since")} {user.memberSince} +

@@ -40,13 +46,16 @@ function ProfileCard({ user }: ProfileCardProps) { {allBadges.map((badge) => (
{badge} -

{badge}

+ {/* Translate the badge name */} +

{t(badge)}

))}
diff --git a/apps/web/src/app/_components/features/ProfileOptions.tsx b/apps/web/src/app/_components/features/ProfileOptions.tsx index 39f8bbb..1ff781c 100644 --- a/apps/web/src/app/_components/features/ProfileOptions.tsx +++ b/apps/web/src/app/_components/features/ProfileOptions.tsx @@ -15,6 +15,7 @@ import { cva } from "class-variance-authority"; import cx from "classnames"; import Link from "next/link"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { LogoutModal } from "~/app/_components/features/LogoutModal"; import { UserWalletsModal } from "~/app/_components/features/UserWalletsModal"; @@ -59,6 +60,7 @@ const iconStyles = cva("w-5 h-5 mr-3", { }); function ProfileOptions({ address: _ }: ProfileOptionsProps) { + const { t } = useTranslation(); const [isLogoutModalOpen, setIsLogoutModalOpen] = useState(false); const [isWalletModalOpen, setIsWalletModalOpen] = useState(false); @@ -79,22 +81,26 @@ function ProfileOptions({ address: _ }: ProfileOptionsProps) { }; const profileOptions: ProfileOption[] = [ - { icon: UserIcon, label: "Edit profile", href: "/user/edit-profile" }, - { icon: TicketIcon, label: "My Coffee", href: "/user/my-coffee" }, - { icon: TruckIcon, label: "My Sales", href: "/user/my-sales" }, - { icon: CurrencyDollarIcon, label: "My Claims", href: "/user/my-claims" }, - { icon: ShoppingCartIcon, label: "My Orders", href: "/user/my-orders" }, - { icon: HeartIcon, label: "Favorite products", href: "/user/favorites" }, - { icon: CubeIcon, label: "My collectibles", href: "/user/collectibles" }, - { icon: WalletIcon, label: "Wallet", onClick: openWalletModal }, + { icon: UserIcon, label: t("edit_profile"), href: "/user/edit-profile" }, + { icon: TicketIcon, label: t("my_coffee"), href: "/user/my-coffee" }, + { icon: TruckIcon, label: t("my_sales"), href: "/user/my-sales" }, + { + icon: CurrencyDollarIcon, + label: t("my_claims"), + href: "/user/my-claims", + }, + { icon: ShoppingCartIcon, label: t("my_orders"), href: "/user/my-orders" }, + { icon: HeartIcon, label: t("favorite_products"), href: "/user/favorites" }, + { icon: CubeIcon, label: t("my_collectibles"), href: "/user/collectibles" }, + { icon: WalletIcon, label: t("wallet"), onClick: openWalletModal }, { icon: AdjustmentsHorizontalIcon, - label: "Settings", + label: t("settings"), href: "/user/settings", }, { icon: NoSymbolIcon, - label: "Log out", + label: t("log_out"), customClass: "text-error-default", iconColor: "text-error-default", onClick: openLogoutModal, diff --git a/apps/web/src/app/_components/features/SearchBar.tsx b/apps/web/src/app/_components/features/SearchBar.tsx index 35639fd..6282ad8 100644 --- a/apps/web/src/app/_components/features/SearchBar.tsx +++ b/apps/web/src/app/_components/features/SearchBar.tsx @@ -4,6 +4,7 @@ import InputField from "@repo/ui/form/inputField"; import { useAtom } from "jotai"; import React, { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import { z } from "zod"; import { isLoadingAtom, @@ -26,6 +27,7 @@ export default function SearchBar() { const [, setQuantityProducts] = useAtom(quantityOfProducts); const [, setIsLoading] = useAtom(isLoadingAtom); const [isFilterOpen, setIsFilterOpen] = useState(false); + const { t } = useTranslation(); const { control } = useForm({ resolver: zodResolver(searchSchema), @@ -44,7 +46,7 @@ export default function SearchBar() { if (data?.productsFound) { const productsWithProcess = data.productsFound.map((product) => ({ ...product, - process: product.process ?? "Natural", + process: product.process ?? t("natural_process"), })); setSearchResults(productsWithProcess); setQuantityProducts(productsWithProcess.length); @@ -52,7 +54,7 @@ export default function SearchBar() { setSearchResults([]); setQuantityProducts(0); } - }, [data, isLoading, setIsLoading, setQuantityProducts, setSearchResults]); + }, [data, isLoading, setIsLoading, setQuantityProducts, setSearchResults, t]); const handleInputChange = (value: string) => { setQuery(value); @@ -65,7 +67,7 @@ export default function SearchBar() { name="region" control={control} label="" - placeholder="Search for your coffee" + placeholder={t("search_placeholder")} onChange={(value: string) => handleInputChange(value)} className="gap-0 mr-3 w-3/4" showSearchIcon={true} @@ -74,6 +76,7 @@ export default function SearchBar() { type="button" onClick={() => setIsFilterOpen(true)} className="bg-surface-secondary-default p-3.5 rounded-lg" + aria-label={t("open_filters")} > diff --git a/apps/web/src/app/_components/features/SelectionTypeCard.tsx b/apps/web/src/app/_components/features/SelectionTypeCard.tsx index 5ccba09..a3ac1b9 100644 --- a/apps/web/src/app/_components/features/SelectionTypeCard.tsx +++ b/apps/web/src/app/_components/features/SelectionTypeCard.tsx @@ -2,6 +2,7 @@ import Button from "@repo/ui/button"; import { InfoCard } from "@repo/ui/infoCard"; import { Text } from "@repo/ui/typography"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; interface SelectionTypeCardProps { price: number; @@ -24,15 +25,17 @@ export function SelectionTypeCard({ "bean", ); + const { t } = useTranslation(); + const coffeeOptions = [ { - label: "Bean", + label: t("coffee_type.bean"), iconSrc: "/images/product-details/Menu-4.svg", selected: selectedOption === "bean", onClick: () => setSelectedOption("bean"), }, { - label: "Grounded", + label: t("coffee_type.grounded"), iconSrc: "/images/product-details/Menu-4.svg", selected: selectedOption === "grounded", onClick: () => setSelectedOption("grounded"), @@ -40,16 +43,18 @@ export function SelectionTypeCard({ ]; return ( - +
- Unit price (340g): {price} USD + {t("unit_price", { weight: "340g" })}: {price} USD
{price * quantity} USD - /total + + /{t("total")} +
@@ -78,7 +83,7 @@ export function SelectionTypeCard({
); diff --git a/apps/web/src/app/_components/features/ShoppingCart.tsx b/apps/web/src/app/_components/features/ShoppingCart.tsx index 8607f94..5094da5 100644 --- a/apps/web/src/app/_components/features/ShoppingCart.tsx +++ b/apps/web/src/app/_components/features/ShoppingCart.tsx @@ -3,6 +3,7 @@ import { XMarkIcon } from "@heroicons/react/24/solid"; import { useAtom, useAtomValue } from "jotai"; import { useRouter } from "next/navigation"; +import { useTranslation } from "react-i18next"; import { cartItemsAtom, removeItemAtom } from "~/store/cartAtom"; interface ShoppingCartProps { @@ -10,6 +11,7 @@ interface ShoppingCartProps { } export default function ShoppingCart({ closeCart }: ShoppingCartProps) { + const { t } = useTranslation(); const router = useRouter(); const items = useAtomValue(cartItemsAtom); const [, removeItem] = useAtom(removeItemAtom); @@ -31,7 +33,7 @@ export default function ShoppingCart({ closeCart }: ShoppingCartProps) { return (
-

Shopping Cart

+

{t("shopping_cart")}

@@ -42,7 +44,7 @@ export default function ShoppingCart({ closeCart }: ShoppingCartProps) {

{item.name}

${item.price}

))} diff --git a/apps/web/src/app/_components/features/StatusUpdateModal.tsx b/apps/web/src/app/_components/features/StatusUpdateModal.tsx index 3387e62..82cce73 100644 --- a/apps/web/src/app/_components/features/StatusUpdateModal.tsx +++ b/apps/web/src/app/_components/features/StatusUpdateModal.tsx @@ -1,7 +1,8 @@ import Button from "@repo/ui/button"; import RadioButton from "@repo/ui/form/radioButton"; -import React, { BaseSyntheticEvent, type FormEvent } from "react"; -import { type Control, useForm } from "react-hook-form"; +import React, { type FormEvent } from "react"; +import type { Control } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import BottomModal from "~/app/_components/ui/BottomModal"; import type { StatusStepsEnum } from "./ProductStatusDetails"; @@ -20,6 +21,7 @@ export function StatusUpdateModal({ control, statusStepsKeys, }: StatusUpdateModalProps) { + const { t } = useTranslation(); return (

@@ -46,7 +48,7 @@ export function StatusUpdateModal({ ))}

diff --git a/apps/web/src/app/_components/features/UserWalletsModal.tsx b/apps/web/src/app/_components/features/UserWalletsModal.tsx index 60ae525..e250873 100644 --- a/apps/web/src/app/_components/features/UserWalletsModal.tsx +++ b/apps/web/src/app/_components/features/UserWalletsModal.tsx @@ -1,6 +1,7 @@ import { ChevronRightIcon } from "@heroicons/react/24/outline"; import Image from "next/image"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import BottomModal from "~/app/_components/ui/BottomModal"; interface UserWalletsModalProps { @@ -16,6 +17,7 @@ interface Wallet { function UserWalletsModal({ isOpen, onClose }: UserWalletsModalProps) { const [wallets, setWallets] = useState([]); + const { t } = useTranslation(); useEffect(() => { // TODO: Implement wallet fetching logic @@ -37,7 +39,7 @@ function UserWalletsModal({ isOpen, onClose }: UserWalletsModalProps) { return (

- Wallets + {t("wallets")}

{wallets.map((wallet) => ( diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index a848030..7f6d953 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,14 +1,26 @@ "use client"; +import { useEffect } from "react"; import "~/styles/globals.css"; +import "~/i18n"; import { GeistSans } from "geist/font/sans"; import { SessionProvider } from "next-auth/react"; +import i18n from "~/i18n"; import StarknetProvider from "~/providers/starknet"; import { TRPCReactProvider } from "~/trpc/react"; export default function RootLayout({ children, -}: Readonly<{ children: React.ReactNode }>) { +}: { + children: React.ReactNode; +}) { + useEffect(() => { + const savedLanguage = localStorage.getItem("app_language"); + if (savedLanguage) { + void i18n.changeLanguage(savedLanguage); + } + }, []); + return ( {t("sign")} @@ -183,14 +183,16 @@ export default function LoginPage() { onClick={() => handleConnectWallet(connector)} variant="primary" size="lg" - className="w-full max-w-[15rem] px-4 py-3 text-content-title text-base font-medium font-inter rounded-lg border border-surface-secondary-default transition-all duration-300 hover:bg-surface-secondary-hover" + className="w-full max-w-[15rem] px-4 py-3 text-content-title text-base font-medium font-inter rounded-lg border border-surface-secondary-default transition-all duration-300 hover:bg-surface-secondary-hover" >
- Connect{" "} - {connector.id === "argentX" - ? "Argent X" - : connector.name} + {t("connect_wallet", { + walletName: + connector.id === "argentX" + ? t("argent_x") + : connector.name, + })}
@@ -200,12 +202,6 @@ export default function LoginPage() { )}
- {/* - Sell My Coffee - */} diff --git a/apps/web/src/app/product/[id]/page.tsx b/apps/web/src/app/product/[id]/page.tsx index 41db457..e22f958 100644 --- a/apps/web/src/app/product/[id]/page.tsx +++ b/apps/web/src/app/product/[id]/page.tsx @@ -3,6 +3,7 @@ import SkeletonLoader from "@repo/ui/skeleton"; import { useParams } from "next/navigation"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import ProductDetails from "~/app/_components/features/ProductDetails"; interface Product { @@ -36,6 +37,7 @@ interface NftMetadata { } function ProductPage() { + const { t } = useTranslation(); const params = useParams(); const productId = typeof params.id === "string" ? params.id : params.id?.[0]; const [product, setProduct] = useState(null); @@ -94,7 +96,7 @@ function ProductPage() { if (!product) { return (
-

Product not found

+

{t("product_not_found")}

); } diff --git a/apps/web/src/app/shopping-cart/page.tsx b/apps/web/src/app/shopping-cart/page.tsx index 76b8a24..d1dc0ec 100644 --- a/apps/web/src/app/shopping-cart/page.tsx +++ b/apps/web/src/app/shopping-cart/page.tsx @@ -5,6 +5,7 @@ import { useAtom, useAtomValue } from "jotai"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { cartItemsAtom, removeItemAtom } from "~/store/cartAtom"; import type { CartItem } from "~/store/cartAtom"; @@ -19,6 +20,7 @@ function DeleteConfirmationModal({ onConfirm, onCancel, }: DeleteModalProps) { + const { t } = useTranslation(); if (!isOpen) return null; return ( @@ -26,7 +28,7 @@ function DeleteConfirmationModal({

- Do you want to remove? + {t("remove_confirmation_title")}

@@ -57,7 +59,7 @@ export default function ShoppingCart() { const items = useAtomValue(cartItemsAtom); const [, removeItem] = useAtom(removeItemAtom); const [itemToDelete, setItemToDelete] = useState(null); - + const { t } = useTranslation(); const handleRemove = (item: CartItem) => { setItemToDelete(item); }; @@ -89,13 +91,13 @@ export default function ShoppingCart() { > -

My cart

+

{t("shopping_cart_title")}

{items.length === 0 ? (
- Your cart is empty + {t("cart_empty_message")}
) : ( items.map((item) => ( @@ -112,9 +114,9 @@ export default function ShoppingCart() { className="rounded-lg object-cover bg-gray-100" />
-

{item.name}

+

{t(item.name)}

- quantity: {item.quantity} + {t("quantity_label")}: {item.quantity}

@@ -140,7 +142,9 @@ export default function ShoppingCart() { <>
- TOTAL + + {t("total_label")} + {totalPrice} USD @@ -151,7 +155,9 @@ export default function ShoppingCart() { type="button" className="w-full py-3.5 px-4 bg-surface-secondary-default rounded-lg border border-surface-secondary-defaul flex justify-center items-center" > - Buy + + {t("buy_button")} +
diff --git a/apps/web/src/app/user-profile/page.tsx b/apps/web/src/app/user-profile/page.tsx index 768feee..9edc9a8 100644 --- a/apps/web/src/app/user-profile/page.tsx +++ b/apps/web/src/app/user-profile/page.tsx @@ -4,21 +4,23 @@ import PageHeader from "@repo/ui/pageHeader"; import { useAccount, useDisconnect } from "@starknet-react/core"; import { useRouter } from "next/navigation"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { ProfileCard } from "~/app/_components/features/ProfileCard"; import { ProfileOptions } from "~/app/_components/features/ProfileOptions"; import Main from "~/app/_components/layout/Main"; import type { UserProfileType } from "~/types"; export default function UserProfile() { + const { t } = useTranslation(); const { address } = useAccount(); const { disconnect } = useDisconnect(); const [user] = useState({ name: "John Doe", - country: "United States", + country: "united_states", memberSince: 2020, thumbnailUrl: "/images/user-profile/avatar.svg", - badges: ["Lover"], + badges: ["lover", "contributor"], }); const router = useRouter(); @@ -27,14 +29,19 @@ export default function UserProfile() {
router.back()} showBlockie={false} /> - +
diff --git a/apps/web/src/app/user/collectibles/page.tsx b/apps/web/src/app/user/collectibles/page.tsx index b99698f..0003023 100644 --- a/apps/web/src/app/user/collectibles/page.tsx +++ b/apps/web/src/app/user/collectibles/page.tsx @@ -1,39 +1,44 @@ +"use client"; + import NFTCard from "@repo/ui/nftCard"; +import { useTranslation } from "react-i18next"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; // TODO: Load collectibles from database and/or blockchain const collectibles = [ { id: 1, - title: "CafΓ© de Especialidad 1", - description: "DescripciΓ³n del CafΓ© de Especialidad 1.", + title: "collectible_title_1", + description: "collectible_description_1", imageUrl: "/images/cafe1.webp", - imageAlt: "Paquete de CafΓ© de Especialidad 1", + imageAlt: "collectible_image_alt_1", }, { id: 2, - title: "CafΓ© de Especialidad 2", - description: "DescripciΓ³n del CafΓ© de Especialidad 2.", + title: "collectible_title_2", + description: "collectible_description_2", imageUrl: "/images/cafe2.webp", - imageAlt: "Paquete de CafΓ© de Especialidad 2", + imageAlt: "collectible_image_alt_2", }, { id: 3, - title: "CafΓ© de Especialidad 3", - description: "DescripciΓ³n del CafΓ© de Especialidad 3.", + title: "collectible_title_3", + description: "collectible_description_3", imageUrl: "/images/cafe3.webp", - imageAlt: "Paquete de CafΓ© de Especialidad 3", + imageAlt: "collectible_image_alt_3", }, ]; export default function Collectibles() { + const { t } = useTranslation(); + return ( - +
{collectibles.map((collectible) => ( ))} diff --git a/apps/web/src/app/user/edit-profile/farm-profile/page.tsx b/apps/web/src/app/user/edit-profile/farm-profile/page.tsx index c8e245b..384af9f 100644 --- a/apps/web/src/app/user/edit-profile/farm-profile/page.tsx +++ b/apps/web/src/app/user/edit-profile/farm-profile/page.tsx @@ -7,30 +7,31 @@ import InputField from "@repo/ui/form/inputField"; import Image from "next/image"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import { z } from "zod"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; import { api } from "~/trpc/react"; const schema = z.object({ - farmName: z.string().min(1, "Farm name is required"), - region: z.string().min(1, "Region is required"), - altitude: z.number().min(0, "Altitude must be a positive number"), + farmName: z.string().min(1, "farm_name_required"), + region: z.string().min(1, "region_required"), + altitude: z.number().min(0, "altitude_positive"), coordinates: z .string() - .regex(/^-?\d+(\.\d+)?,\s*-?\d+(\.\d+)?$/, "Invalid coordinates format"), - website: z.string().url("Invalid URL").optional().or(z.literal("")), + .regex(/^-?\d+(\.\d+)?,\s*-?\d+(\.\d+)?$/, "coordinates_invalid"), + website: z.string().url("url_invalid").optional().or(z.literal("")), }); type FormData = z.infer; function EditMyFarmProfile() { const utils = api.useUtils(); + const { t } = useTranslation(); const [image, setImage] = useState(null); const userId = 1; // Assume you have the logic to get the userId const { data: userFarm, isLoading } = api.user.getUserFarm.useQuery({ - // TODO: get user id from session or prop userId, }); @@ -44,12 +45,11 @@ function EditMyFarmProfile() { onSuccess: () => { void utils.user.getUser.invalidate({ userId: userId.toString() }); void utils.user.getUserFarm.invalidate({ userId }); - // TODO: display notification - alert("Farm profile updated"); + alert(t("farm_profile_updated")); }, onError: (error) => { console.error("Error updating farm profile:", error); - alert("An error occurred while updating the farm profile"); + alert(t("error_updating_farm")); }, }); @@ -66,8 +66,7 @@ function EditMyFarmProfile() { const onSubmit = (data: FormData) => { if (!userFarm) { - // TODO: Display error in UI - alert("User farm not found. Please contact support."); + alert(t("user_farm_not_found")); return; } @@ -83,18 +82,17 @@ function EditMyFarmProfile() { }; const handleImageUpload = () => { - // TODO: Implement image upload logic - alert("Implement image upload logic"); + alert(t("implement_image_upload")); setImage(null); }; return ( {isLoading ? ( -
Loading...
+
{t("loading")}
) : ( <>
@@ -102,14 +100,14 @@ function EditMyFarmProfile() { {image ? ( Farm Profile ) : (
- No Image + {t("no_image")}
)}
@@ -118,13 +116,13 @@ function EditMyFarmProfile() { onClick={handleImageUpload} > - Choose photo + {t("choose_photo")}
{ void register("farmName").onChange({ target: { value } }); @@ -133,7 +131,7 @@ function EditMyFarmProfile() { className="mb-4" /> { void register("region").onChange({ target: { value } }); @@ -142,7 +140,7 @@ function EditMyFarmProfile() { className="mb-4" /> { void register("altitude").onChange({ @@ -152,7 +150,7 @@ function EditMyFarmProfile() { control={control} /> { void register("coordinates").onChange({ target: { value } }); @@ -160,7 +158,7 @@ function EditMyFarmProfile() { control={control} /> { void register("website").onChange({ target: { value } }); @@ -168,7 +166,7 @@ function EditMyFarmProfile() { control={control} /> diff --git a/apps/web/src/app/user/edit-profile/my-profile/page.tsx b/apps/web/src/app/user/edit-profile/my-profile/page.tsx index 55949ef..bb6b96d 100644 --- a/apps/web/src/app/user/edit-profile/my-profile/page.tsx +++ b/apps/web/src/app/user/edit-profile/my-profile/page.tsx @@ -7,26 +7,27 @@ import InputField from "@repo/ui/form/inputField"; import Image from "next/image"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import { z } from "zod"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; import { api } from "~/trpc/react"; const schema = z.object({ - fullName: z.string().min(1, "Full name is required"), - email: z.string().email("Invalid email").optional(), - physicalAddress: z.string().min(1, "Address is required"), + fullName: z.string().min(1, "full_name_required"), + email: z.string().email("invalid_email").optional(), + physicalAddress: z.string().min(1, "address_required"), }); type FormData = z.infer; function EditMyProfile() { const utils = api.useUtils(); + const { t } = useTranslation(); const [image, setImage] = useState(null); const userId = "1"; // Assume you have the logic to get the userId const { data: user, isLoading } = api.user.getUser.useQuery({ - // TODO: get user id from session or prop userId, }); @@ -40,10 +41,9 @@ function EditMyProfile() { onSuccess: async () => { try { await utils.user.getUser.invalidate({ userId }); - // TODO: display notification - alert("Profile updated"); + alert(t("profile_updated")); } catch (error) { - console.error("Failed to invalidate user data:", error); + console.error(t("invalidate_user_data_failed"), error); } }, }); @@ -59,7 +59,7 @@ function EditMyProfile() { const onSubmit = (data: FormData) => { void updateProfile({ - userId: userId, + userId, name: data.fullName, physicalAddress: data.physicalAddress, image: image ?? undefined, @@ -67,17 +67,19 @@ function EditMyProfile() { }; const handleImageUpload = () => { - // TODO: Implement image upload logic void (async () => { - alert("Implement image upload logic"); + alert(t("implement_image_upload")); setImage(null); })(); }; return ( - + {isLoading ? ( -
Loading...
+
{t("loading")}
) : ( <>
@@ -85,14 +87,14 @@ function EditMyProfile() { {image ? ( Profile ) : (
- No Image + {t("no_image")}
)}
@@ -101,13 +103,13 @@ function EditMyProfile() { onClick={handleImageUpload} > - Choose photo + {t("choose_photo")}
{ void register("fullName").onChange({ target: { value } }); @@ -116,7 +118,7 @@ function EditMyProfile() { className="mb-4" /> { void register("email").onChange({ target: { value } }); @@ -124,12 +126,10 @@ function EditMyProfile() { control={control} className="mb-4" disabled - // TODO: update input style (set #F8FAFC as bg color and #788788 as text color) - // TODO: Add support to add input icon (add email icon at the beginning of the input) inputClassName="cursor-not-allowed" /> { void register("physicalAddress").onChange({ @@ -140,7 +140,7 @@ function EditMyProfile() { className="mb-4" /> diff --git a/apps/web/src/app/user/edit-profile/page.tsx b/apps/web/src/app/user/edit-profile/page.tsx index c8a4fa9..ceeb487 100644 --- a/apps/web/src/app/user/edit-profile/page.tsx +++ b/apps/web/src/app/user/edit-profile/page.tsx @@ -1,30 +1,34 @@ +"use client"; + import { ChevronRightIcon } from "@heroicons/react/24/outline"; import Image from "next/image"; import Link from "next/link"; +import { useTranslation } from "react-i18next"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; import type { EditProfileOption } from "~/types"; const editProfileOptions: EditProfileOption[] = [ { imgUrl: "/images/user-profile/avatar.svg", - label: "Edit my profile", + labelKey: "edit_my_profile", href: "/user/edit-profile/my-profile", }, { imgUrl: "/images/user-profile/farm-avatar.svg", - label: "Edit my farm profile", + labelKey: "edit_my_farm_profile", href: "/user/edit-profile/farm-profile", }, ]; export default function EditProfile() { + const { t } = useTranslation(); const lastOptionIndex = editProfileOptions.length - 1; return ( - +
{editProfileOptions.map((option, index) => ( {option.label} - {option.label} + {t(option.labelKey)}
(null); - const items = useAtomValue(cartItemsAtom); const [, addItem] = useAtom(addItemAtom); const handleAddToCart = (productId: number) => { @@ -44,17 +46,17 @@ export default function Favorites() { }; return ( - +
{userFavoriteProducts.map(({ id, imageUrl }) => (
handleAddToCart(id)} isAddingToShoppingCart={addedProduct === id} /> diff --git a/apps/web/src/app/user/my-claims/[id]/page.tsx b/apps/web/src/app/user/my-claims/[id]/page.tsx index 9b0043c..12f45ba 100644 --- a/apps/web/src/app/user/my-claims/[id]/page.tsx +++ b/apps/web/src/app/user/my-claims/[id]/page.tsx @@ -2,12 +2,14 @@ import { useParams } from "next/navigation"; import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import ProductStatusDetails from "~/app/_components/features/ProductStatusDetails"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; import type { SaleDetailsType } from "~/types"; export default function MySaleDetails() { const { id: saleId } = useParams(); + const { t } = useTranslation(); const [saleDetails, setSaleDetails] = useState(null); // TODO: Fetch user role based on user id or from session/context/token @@ -17,19 +19,19 @@ export default function MySaleDetails() { // TODO: Fetch sale details based on saleId if (saleId) { setSaleDetails({ - productName: "Product Name", - status: "Delivered", - roast: "strong", - type: "grounded", - quantity: "5 bags", - delivery: "Meetup", - totalPrice: "50 USD", + productName: t("product_name"), + status: t("order_status.delivered"), + roast: t("roast.strong"), + type: t("coffee_type.grounded"), + quantity: t("quantity_with_unit", { count: 5, unit: t("bags") }), + delivery: t("delivery_method.meetup"), + totalPrice: t("price_with_currency", { price: 50, currency: "USD" }), }); } // TODO: Fetch user role based on user id or from session/context/token setIsProducer(true); - }, [saleId]); + }, [saleId, t]); const updateSaleDetails = (productDetails: SaleDetailsType) => { // TODO: Implement logic to update sale details diff --git a/apps/web/src/app/user/my-claims/page.tsx b/apps/web/src/app/user/my-claims/page.tsx index da0826e..2404540 100644 --- a/apps/web/src/app/user/my-claims/page.tsx +++ b/apps/web/src/app/user/my-claims/page.tsx @@ -13,13 +13,15 @@ import OrderListPriceItem from "~/app/_components/features/OrderListPriceItem"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; import { DeliveryMethod, SalesStatus } from "~/types"; +import { useTranslation } from "react-i18next"; + const mockedOrders = [ { - date: "October 18", + date: "october_18", items: [ { id: "1", - productName: "productName", + productName: "product_name_1", buyerName: "buyer1_fullname", status: SalesStatus.Delivered, delivery: DeliveryMethod.Address, @@ -28,7 +30,7 @@ const mockedOrders = [ }, { id: "2", - productName: "productName2", + productName: "product_name_2", buyerName: "buyer2_fullname", status: SalesStatus.Delivered, delivery: DeliveryMethod.Meetup, @@ -38,11 +40,11 @@ const mockedOrders = [ ], }, { - date: "September 20", + date: "september_20", items: [ { id: "3", - productName: "productName3", + productName: "product_name_3", buyerName: "buyer1_fullname", status: SalesStatus.Delivered, delivery: DeliveryMethod.Address, @@ -51,7 +53,7 @@ const mockedOrders = [ }, { id: "4", - productName: "productName4", + productName: "product_name_4", buyerName: "buyer2_fullname", status: SalesStatus.Delivered, delivery: DeliveryMethod.Meetup, @@ -67,7 +69,8 @@ export default function MyClaims() { const [ClaimedOrders, setClaimedOrders] = useState(mockedOrders); const [searchTerm, setSearchTerm] = useState(""); const [MoneyToClaim, setMoneyToClaim] = useState(0); - const [isFiltersModalOpen, setIsFiltersModalOpen] = useState(false); + const [, setIsFiltersModalOpen] = useState(false); + const { t } = useTranslation(); const router = useRouter(); @@ -112,11 +115,11 @@ export default function MyClaims() { }; return ( - +

- Total balance receivable + {t("total_balance_receivable")}

@@ -134,22 +137,21 @@ export default function MyClaims() { {OrdersToClaim.map((orderGroup, index) => (

- {orderGroup.date} + {t(orderGroup.date)}

{orderGroup.items.map((order, orderIndex) => ( - <> +
handleItemClick(order.id)} /> {orderIndex < orderGroup.items.length - 1 && (
)} - +
))}
@@ -160,12 +162,12 @@ export default function MyClaims() { className="mx-auto mt-5 w-[90%] h-15 px-2" onClick={() => router.push("/user/register-coffee")} > - Receive {MoneyToClaim.toFixed(2)} USD + {t("recieve")} {MoneyToClaim.toFixed(2)} USD

- History + {t("history")}

@@ -174,7 +176,7 @@ export default function MyClaims() { setSearchTerm(e.target.value)} @@ -193,16 +195,16 @@ export default function MyClaims() { {ClaimedOrders.map((orderGroup, index) => (

- {orderGroup.date} + {t(orderGroup.date)}

{orderGroup.items.map((order, orderIndex) => ( <> handleItemClick(order.id)} /> {orderIndex < orderGroup.items.length - 1 && ( diff --git a/apps/web/src/app/user/my-coffee/page.tsx b/apps/web/src/app/user/my-coffee/page.tsx index df2b28b..28d8d7b 100644 --- a/apps/web/src/app/user/my-coffee/page.tsx +++ b/apps/web/src/app/user/my-coffee/page.tsx @@ -3,16 +3,18 @@ import Button from "@repo/ui/button"; import Image from "next/image"; import { useRouter } from "next/navigation"; +import { useTranslation } from "react-i18next"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; export default function MyCoffee() { + const { t } = useTranslation(); const router = useRouter(); const handleOfferMyCoffee = () => { router.push("/user/register-coffee"); }; return ( - +

- Would you like to sell your coffee? + {t("sell_your_coffee")}

diff --git a/apps/web/src/app/user/my-orders/[id]/page.tsx b/apps/web/src/app/user/my-orders/[id]/page.tsx index e4783b3..b9c0ed9 100644 --- a/apps/web/src/app/user/my-orders/[id]/page.tsx +++ b/apps/web/src/app/user/my-orders/[id]/page.tsx @@ -2,11 +2,13 @@ import { useParams } from "next/navigation"; import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import ProductStatusDetails from "~/app/_components/features/ProductStatusDetails"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; import type { OrderDetailsType } from "~/types"; export default function OrderDetails() { + const { t } = useTranslation(); const { id: orderId } = useParams(); const [orderDetails, setOrderDetails] = useState( @@ -19,20 +21,20 @@ export default function OrderDetails() { // TODO: Fetch order details based on orderId if (orderId) { setOrderDetails({ - productName: "Sample Product", - status: "Paid", - roast: "strong", - type: "grounded", - quantity: "5 bags", - delivery: "Delivery", + productName: t("sample_product"), + status: t("paid"), + roast: t("strong"), + type: t("grounded"), + quantity: `5 ${t("bags")}`, + delivery: t("delivery"), address: "Av Portugal 375, ap 410 SΓ£o Paulo/SP CEP 66010-100", - totalPrice: "50 USD", + totalPrice: `50 ${t("usd")}`, }); } // TODO: Fetch user role based on user id or from session/context/token setIsProducer(true); - }, [orderId]); + }, [orderId, t]); const updateProductDetails = (productDetails: OrderDetailsType) => { // TODO: Implement logic to update order details diff --git a/apps/web/src/app/user/my-orders/page.tsx b/apps/web/src/app/user/my-orders/page.tsx index acf88c2..6fed727 100644 --- a/apps/web/src/app/user/my-orders/page.tsx +++ b/apps/web/src/app/user/my-orders/page.tsx @@ -6,6 +6,7 @@ import Button from "@repo/ui/button"; import CheckBox from "@repo/ui/form/checkBox"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import OrderListItem from "~/app/_components/features/OrderListItem"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; import BottomModal from "~/app/_components/ui/BottomModal"; @@ -14,34 +15,34 @@ import { type FormValues, SalesStatus, filtersSchema } from "~/types"; const mockedOrders = [ { - date: "October 18", + date: "october_18", items: [ { id: "1", - productName: "Edit profile", + productName: "product_name_1", sellerName: "seller1_fullname", status: SalesStatus.Paid, }, { id: "2", - productName: "My Orders", + productName: "product_name_2", sellerName: "seller2_fullname", status: SalesStatus.Paid, }, ], }, { - date: "September 20", + date: "september_20", items: [ { id: "3", - productName: "productName", + productName: "product_name_3", sellerName: "seller1_fullname", status: SalesStatus.Delivered, }, { id: "4", - productName: "productName", + productName: "product_name_4", sellerName: "seller2_fullname", status: SalesStatus.Delivered, }, @@ -57,6 +58,7 @@ const filtersDefaults = { }; export default function MyOrders() { + const { t } = useTranslation(); const { searchTerm, setSearchTerm, @@ -82,14 +84,14 @@ export default function MyOrders() { }; return ( - +
setSearchTerm(e.target.value)} @@ -107,16 +109,16 @@ export default function MyOrders() { {filteredOrders.map((orderGroup, index) => (

- {orderGroup.date} + {t(orderGroup.date)}

{orderGroup.items.map((order, orderIndex) => ( <> handleItemClick(order.id)} /> {orderIndex < orderGroup.items.length - 1 && ( @@ -132,37 +134,37 @@ export default function MyOrders() {

- Status + {t("status")}

<>


diff --git a/apps/web/src/app/user/my-sales/page.tsx b/apps/web/src/app/user/my-sales/page.tsx index 5061690..ede4b91 100644 --- a/apps/web/src/app/user/my-sales/page.tsx +++ b/apps/web/src/app/user/my-sales/page.tsx @@ -6,6 +6,7 @@ import Button from "@repo/ui/button"; import CheckBox from "@repo/ui/form/checkBox"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import OrderListItem from "~/app/_components/features/OrderListItem"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; import BottomModal from "~/app/_components/ui/BottomModal"; @@ -19,18 +20,18 @@ import { const mockedOrders = [ { - date: "October 18", + date: "october_18", items: [ { id: "1", - productName: "Edit profile", + productName: "product_name_1", buyerName: "buyer1_fullname", status: SalesStatus.Paid, delivery: DeliveryMethod.Address, }, { id: "2", - productName: "My Orders", + productName: "product_name_2", buyerName: "buyer2_fullname", status: SalesStatus.Paid, delivery: DeliveryMethod.Meetup, @@ -38,18 +39,18 @@ const mockedOrders = [ ], }, { - date: "September 20", + date: "september_20", items: [ { id: "3", - productName: "productName", + productName: "product_name_1", buyerName: "buyer1_fullname", status: SalesStatus.Delivered, delivery: DeliveryMethod.Address, }, { id: "4", - productName: "productName", + productName: "product_name_2", buyerName: "buyer2_fullname", status: SalesStatus.Delivered, delivery: DeliveryMethod.Meetup, @@ -68,6 +69,9 @@ const filtersDefaults = { }; export default function MySales() { + const { t } = useTranslation(); + const router = useRouter(); + const { searchTerm, setSearchTerm, @@ -81,7 +85,6 @@ export default function MySales() { searchKey: "buyerName", filters: filtersDefaults, }); - const router = useRouter(); const { control, handleSubmit } = useForm({ resolver: zodResolver(filtersSchema), @@ -93,14 +96,14 @@ export default function MySales() { }; return ( - +
setSearchTerm(e.target.value)} @@ -118,15 +121,15 @@ export default function MySales() { {filteredOrders.map((orderGroup, index) => (

- {orderGroup.date} + {t(orderGroup.date)}

{orderGroup.items.map((order, orderIndex) => ( <> handleItemClick(order.id)} /> @@ -143,55 +146,59 @@ export default function MySales() {

- Delivery method + {t("delivery_method_label")}

<>

- Status + {t("status")}

<>


diff --git a/apps/web/src/app/user/register-coffee/page.tsx b/apps/web/src/app/user/register-coffee/page.tsx index 8d85545..88efa5f 100644 --- a/apps/web/src/app/user/register-coffee/page.tsx +++ b/apps/web/src/app/user/register-coffee/page.tsx @@ -10,6 +10,7 @@ import Button from "@repo/ui/button"; import RadioButton from "@repo/ui/form/radioButton"; import Image from "next/image"; import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import { z } from "zod"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; import { RoastLevel } from "~/types"; @@ -31,8 +32,9 @@ const schema = z.object({ type FormData = z.infer; export default function RegisterCoffee() { + const { t } = useTranslation(); const handleImageUpload = () => { - alert("Implement image upload"); + alert(t("implement_image_upload")); }; const { register, handleSubmit, control, getValues, setValue } = @@ -54,7 +56,10 @@ export default function RegisterCoffee() { }; return ( - +
@@ -72,34 +77,36 @@ export default function RegisterCoffee() { type="button" > - Choose coffee photo + {t("choose_coffee_photo")}