diff --git a/.gitignore b/.gitignore index ad46469..6f77611 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ yarn-error.log* # Storybook storybook-static + +# Instructions folder +instructions/ diff --git a/apps/snfoundry/scripts-ts/helpers/error-handler.ts b/apps/snfoundry/scripts-ts/helpers/error-handler.ts index e4b20cb..1beb0b1 100644 --- a/apps/snfoundry/scripts-ts/helpers/error-handler.ts +++ b/apps/snfoundry/scripts-ts/helpers/error-handler.ts @@ -1,108 +1,113 @@ -import { Provider } from "starknet"; -import { DeploymentError, DeploymentErrorType, RetryConfig, Networks } from "../types"; +import type { Provider } from "starknet"; +import { + DeploymentError, + DeploymentErrorType, + Networks, + type RetryConfig, +} from "../types"; class Logger { - private formatMessage(level: string, message: any): string { - const timestamp = new Date().toISOString(); - return `[${timestamp}] ${level}: ${typeof message === 'string' ? message : JSON.stringify(message)}`; - } - - info(message: any): void { - console.log(this.formatMessage('INFO', message)); - } - - warn(message: any): void { - console.warn(this.formatMessage('WARN', message)); - } - - error(message: any): void { - console.error(this.formatMessage('ERROR', message)); - } + private formatMessage(level: string, message: unknown): string { + const timestamp = new Date().toISOString(); + return `[${timestamp}] ${level}: ${typeof message === "string" ? message : JSON.stringify(message)}`; + } + + info(message: unknown): void { + console.log(this.formatMessage("INFO", message)); + } + + warn(message: unknown): void { + console.warn(this.formatMessage("WARN", message)); + } + + error(message: unknown): void { + console.error(this.formatMessage("ERROR", message)); + } } export const logger = new Logger(); const defaultRetryConfig: RetryConfig = { - maxAttempts: 3, - initialDelay: 1000, - maxDelay: 10000, - factor: 2 + maxAttempts: 3, + initialDelay: 1000, + maxDelay: 10000, + factor: 2, }; export async function withRetry( - operation: () => Promise, - config: RetryConfig = defaultRetryConfig, - context: string + operation: () => Promise, + context: string, + config: RetryConfig = defaultRetryConfig, ): Promise { - let delay = config.initialDelay; - let attempt = 0; - - while (attempt < config.maxAttempts) { - try { - return await operation(); - } catch (error: any) { - attempt++; - - const errorType = classifyStarknetError(error); - logger.warn({ - message: `Retry attempt ${attempt}/${config.maxAttempts} for ${context}`, - error: error.message, - type: errorType - }); - - if (attempt === config.maxAttempts || !isRetryableError(errorType)) { - throw new DeploymentError(errorType, error.message); - } - - await sleep(delay); - delay = Math.min(delay * config.factor, config.maxDelay); - } - } - - throw new DeploymentError( - DeploymentErrorType.UNKNOWN_ERROR, - `Max retry attempts (${config.maxAttempts}) reached for ${context}` - ); + let delay = config.initialDelay; + let attempt = 1; + + while (attempt <= config.maxAttempts) { + try { + return await operation(); + } catch (error: unknown) { + attempt++; + + const errorType = classifyStarknetError(error as Error); + logger.warn({ + message: `Retry attempt ${attempt}/${config.maxAttempts} for ${context}`, + error: (error as Error).message, + type: errorType, + }); + + if (attempt === config.maxAttempts || !isRetryableError(errorType)) { + throw new DeploymentError(errorType, (error as Error).message); + } + + await sleep(delay); + delay = Math.min(delay * config.factor, config.maxDelay); + } + } + + throw new DeploymentError( + DeploymentErrorType.UNKNOWN_ERROR, + `Max retry attempts (${config.maxAttempts}) reached for ${context}`, + ); } -function classifyStarknetError(error: any): DeploymentErrorType { - const errorMsg = error.message.toLowerCase(); - - if (errorMsg.includes("insufficient max fee")) { - return DeploymentErrorType.GAS_ERROR; - } - if (errorMsg.includes("invalid transaction nonce")) { - return DeploymentErrorType.NONCE_ERROR; - } - if (errorMsg.includes("network") || errorMsg.includes("timeout")) { - return DeploymentErrorType.NETWORK_ERROR; - } - if (errorMsg.includes("contract") || errorMsg.includes("class hash")) { - return DeploymentErrorType.CONTRACT_ERROR; - } - if (errorMsg.includes("invalid") || errorMsg.includes("validation")) { - return DeploymentErrorType.VALIDATION_ERROR; - } - return DeploymentErrorType.UNKNOWN_ERROR; +function classifyStarknetError(error: Error): DeploymentErrorType { + const errorMsg = error.message.toLowerCase(); + + if (errorMsg.includes("insufficient max fee")) { + return DeploymentErrorType.GAS_ERROR; + } + if (errorMsg.includes("invalid transaction nonce")) { + return DeploymentErrorType.NONCE_ERROR; + } + if (errorMsg.includes("network") || errorMsg.includes("timeout")) { + return DeploymentErrorType.NETWORK_ERROR; + } + if (errorMsg.includes("contract") || errorMsg.includes("class hash")) { + return DeploymentErrorType.CONTRACT_ERROR; + } + if (errorMsg.includes("invalid") || errorMsg.includes("validation")) { + return DeploymentErrorType.VALIDATION_ERROR; + } + return DeploymentErrorType.UNKNOWN_ERROR; } function isRetryableError(errorType: DeploymentErrorType): boolean { - return [ - DeploymentErrorType.NETWORK_ERROR, - DeploymentErrorType.GAS_ERROR, - DeploymentErrorType.NONCE_ERROR - ].includes(errorType); + return [ + DeploymentErrorType.NETWORK_ERROR, + DeploymentErrorType.GAS_ERROR, + DeploymentErrorType.NONCE_ERROR, + ].includes(errorType); } export async function validateNetwork(provider: Provider): Promise { - try { - await provider.getChainId(); - } catch (error) { - throw new DeploymentError( - DeploymentErrorType.NETWORK_ERROR, - "Failed to validate network connection" - ); - } + try { + await provider.getChainId(); + } catch (error) { + throw new DeploymentError( + DeploymentErrorType.NETWORK_ERROR, + "Failed to validate network connection", + ); + } } -const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); \ No newline at end of file +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/apps/snfoundry/scripts-ts/types.ts b/apps/snfoundry/scripts-ts/types.ts index f77e080..3f94827 100644 --- a/apps/snfoundry/scripts-ts/types.ts +++ b/apps/snfoundry/scripts-ts/types.ts @@ -29,7 +29,7 @@ export class DeploymentError extends Error { public type: DeploymentErrorType, message: string, public txHash?: string, - public contractAddress?: string + public contractAddress?: string, ) { super(message); this.name = "DeploymentError"; @@ -45,7 +45,7 @@ export interface RetryConfig { export interface TransactionQueueItem { id: string; - execute: () => Promise; + execute: () => Promise; priority: number; network: keyof Networks; dependencies?: string[]; diff --git a/apps/web/package.json b/apps/web/package.json index 74ccc86..4941ada 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@auth/prisma-adapter": "^1.6.0", - "@prisma/client": "^6.2.1", + "@prisma/client": "5.22.0", "@repo/ui": "*", "@starknet-react/chains": "^0.1.7", "@starknet-react/core": "^2.9.0", diff --git a/apps/web/src/app/_components/features/OrderListItem.tsx b/apps/web/src/app/_components/features/OrderListItem.tsx index 2232305..8ff21ac 100644 --- a/apps/web/src/app/_components/features/OrderListItem.tsx +++ b/apps/web/src/app/_components/features/OrderListItem.tsx @@ -21,16 +21,15 @@ export default function OrderListItem({ const { t } = useTranslation(); return ( - -
{ - if (e.key === "Enter" || e.key === " ") { + +
+ ); } diff --git a/apps/web/src/app/_components/features/ProductCatalog.tsx b/apps/web/src/app/_components/features/ProductCatalog.tsx index a423ea8..c882cc1 100755 --- a/apps/web/src/app/_components/features/ProductCatalog.tsx +++ b/apps/web/src/app/_components/features/ProductCatalog.tsx @@ -86,7 +86,7 @@ export default function ProductCatalog() { farmName={metadata?.farmName ?? ""} variety={t(product.name)} price={product.price} - badgeText={t(`strength.${metadata?.strength.toLowerCase()}`)} + badgeText={t(`strength.${metadata?.strength?.toLowerCase()}`)} onClick={() => accessProductDetails(product.id)} /> ); diff --git a/apps/web/src/app/_components/features/ProductDetails.tsx b/apps/web/src/app/_components/features/ProductDetails.tsx index 41a0ab0..329ea5b 100644 --- a/apps/web/src/app/_components/features/ProductDetails.tsx +++ b/apps/web/src/app/_components/features/ProductDetails.tsx @@ -9,6 +9,7 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { addItemAtom, cartItemsAtom } from "~/store/cartAtom"; +import { api } from "~/trpc/react"; import { ProducerInfo } from "./ProducerInfo"; import { SelectionTypeCard } from "./SelectionTypeCard"; @@ -45,27 +46,35 @@ export default function ProductDetails({ product }: ProductDetailsProps) { const [isLiked, setIsLiked] = useState(false); const router = useRouter(); const [isAddingToCart, setIsAddingToCart] = useState(false); - const [, addItem] = useAtom(addItemAtom); - const items = useAtomValue(cartItemsAtom); - const cartItemsCount = items.reduce( - (total, item) => total + item.quantity, - 0, - ); + const { data: cart, refetch: refetchCart } = api.cart.getUserCart.useQuery(); + const { mutate: addToCart } = api.cart.addToCart.useMutation({ + onSuccess: () => { + void refetchCart(); + }, + }); + const cartItemsCount = + cart?.items?.reduce((total, item) => total + item.quantity, 0) ?? 0; const isSoldOut = type === "SoldOut"; const isFarmer = type === "Farmer"; const handleAddToCart = () => { setIsAddingToCart(true); - addItem({ - id: String(product.id), - tokenId: product.tokenId, - name: product.name, - quantity: quantity, - price: product.price, - imageUrl: product.image, - }); - setIsAddingToCart(false); + addToCart( + { + productId: product.id, + quantity: quantity, + }, + { + onSuccess: () => { + setIsAddingToCart(false); + }, + onError: () => { + setIsAddingToCart(false); + // TODO: Show error toast + }, + }, + ); }; return ( diff --git a/apps/web/src/app/_components/features/ProfileCard.tsx b/apps/web/src/app/_components/features/ProfileCard.tsx index 52e6337..8e27c42 100644 --- a/apps/web/src/app/_components/features/ProfileCard.tsx +++ b/apps/web/src/app/_components/features/ProfileCard.tsx @@ -1,13 +1,19 @@ +import dynamic from "next/dynamic"; import Image from "next/image"; import { useTranslation } from "react-i18next"; +const BlockiesSvg = dynamic<{ address: string; size: number; scale: number }>( + () => import("blockies-react-svg"), + { ssr: false }, +); + type Badge = "lover" | "contributor" | "producer"; type UserProfile = { name: string; country: string; memberSince: number; - thumbnailUrl: string; + walletAddress: string; badges: Badge[]; }; @@ -21,41 +27,47 @@ function ProfileCard({ user }: ProfileCardProps) { // Define all badges and their translation keys const allBadges: Badge[] = ["lover", "contributor", "producer"]; + // Format country name for display + const formattedCountry = user.country.replace("_", " "); + return (
-
+
-
- {user.name} -
-

{user.name}

-

{user.country}

-

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

+
+
+ {/* Blockie Avatar */} +
+ +
+
+

+ {user.name} +

+

+ {formattedCountry} +

+

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

+
-
+
{allBadges.map((badge) => (
{badge} {/* Translate the badge name */} -

{t(badge)}

+

{t(badge)}

))}
diff --git a/apps/web/src/app/_components/features/ShoppingCart.tsx b/apps/web/src/app/_components/features/ShoppingCart.tsx index 5094da5..9055de9 100644 --- a/apps/web/src/app/_components/features/ShoppingCart.tsx +++ b/apps/web/src/app/_components/features/ShoppingCart.tsx @@ -1,10 +1,9 @@ "use client"; 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"; +import { api } from "~/trpc/react"; interface ShoppingCartProps { closeCart: () => void; @@ -13,11 +12,17 @@ interface ShoppingCartProps { export default function ShoppingCart({ closeCart }: ShoppingCartProps) { const { t } = useTranslation(); const router = useRouter(); - const items = useAtomValue(cartItemsAtom); - const [, removeItem] = useAtom(removeItemAtom); - const handleRemoveItem = (itemId: string) => { - removeItem(itemId); + // Get cart data from server + const { data: cart, refetch: refetchCart } = api.cart.getUserCart.useQuery(); + const { mutate: removeFromCart } = api.cart.removeFromCart.useMutation({ + onSuccess: () => { + void refetchCart(); + }, + }); + + const handleRemoveItem = (cartItemId: string) => { + removeFromCart({ cartItemId }); }; const handleCheckout = () => { @@ -25,10 +30,11 @@ export default function ShoppingCart({ closeCart }: ShoppingCartProps) { router.push("/shopping-cart"); }; - const totalPrice = items.reduce( - (total, item) => total + item.price * item.quantity, - 0, - ); + const totalPrice = + cart?.items.reduce( + (total, item) => total + item.product.price * item.quantity, + 0, + ) ?? 0; return (
@@ -39,10 +45,10 @@ export default function ShoppingCart({ closeCart }: ShoppingCartProps) {
- {items.map((item) => ( + {cart?.items.map((item) => (
-

{item.name}

-

${item.price}

+

{item.product.name}

+

${item.product.price}

diff --git a/apps/web/src/app/_components/layout/Main.tsx b/apps/web/src/app/_components/layout/Main.tsx index c50e705..76e6671 100644 --- a/apps/web/src/app/_components/layout/Main.tsx +++ b/apps/web/src/app/_components/layout/Main.tsx @@ -6,8 +6,11 @@ export default function Main({ children }: MainProps) { return ( <> {/* */} -
-
{children}
+
+ {/* Responsive container with proper breakpoints */} +
+
{children}
+
); diff --git a/apps/web/src/app/_components/ui/Spinner.tsx b/apps/web/src/app/_components/ui/Spinner.tsx index f6fbbdf..f9f9d56 100644 --- a/apps/web/src/app/_components/ui/Spinner.tsx +++ b/apps/web/src/app/_components/ui/Spinner.tsx @@ -4,10 +4,10 @@ interface SpinnerProps { export default function Spinner({ className = "" }: SpinnerProps) { return ( -
+ Loading... -
+ ); } diff --git a/apps/web/src/app/marketplace/page.tsx b/apps/web/src/app/marketplace/page.tsx index bd9b663..f7cd1a1 100755 --- a/apps/web/src/app/marketplace/page.tsx +++ b/apps/web/src/app/marketplace/page.tsx @@ -42,11 +42,7 @@ export default function Home() {
- {query.length <= 0 && ( - <> - - - )} + {query.length <= 0 && } ); diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index e04f9d2..4d6ef6f 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,6 +1,7 @@ "use client"; import "../i18n"; +import { ChevronRightIcon } from "@heroicons/react/24/outline"; import Button from "@repo/ui/button"; import { H1, Text } from "@repo/ui/typography"; import { @@ -13,6 +14,7 @@ import { import { motion, useAnimation } from "framer-motion"; import { signIn, signOut } from "next-auth/react"; import { useSession } from "next-auth/react"; +import Image from "next/image"; import { redirect } from "next/navigation"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -181,19 +183,20 @@ export default function LoginPage() { ))} diff --git a/apps/web/src/app/shopping-cart/page.tsx b/apps/web/src/app/shopping-cart/page.tsx index 5b167cd..f08d27d 100644 --- a/apps/web/src/app/shopping-cart/page.tsx +++ b/apps/web/src/app/shopping-cart/page.tsx @@ -3,13 +3,11 @@ import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/24/outline"; import { useAccount } from "@starknet-react/core"; import { useProvider } from "@starknet-react/core"; -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"; +import { api } from "~/trpc/react"; import { ContractsError, ContractsInterface, @@ -17,7 +15,6 @@ import { useMarketplaceContract, useStarkContract, } from "../../services/contractsInterface"; -//import { api } from "~/trpc/server"; interface DeleteModalProps { isOpen: boolean; @@ -64,11 +61,19 @@ function DeleteConfirmationModal({ ); } +interface NftMetadata { + description: string; + imageUrl: string; + imageAlt: string; + region?: string; + farmName?: string; + strength?: string; +} + export default function ShoppingCart() { const router = useRouter(); - const items = useAtomValue(cartItemsAtom); - const [, removeItem] = useAtom(removeItemAtom); - const [itemToDelete, setItemToDelete] = useState(null); + const { t } = useTranslation(); + const [itemToDelete, setItemToDelete] = useState(null); const { provider } = useProvider(); const contract = new ContractsInterface( useAccount(), @@ -77,14 +82,23 @@ export default function ShoppingCart() { useStarkContract(), provider, ); - const { t } = useTranslation(); - const handleRemove = (item: CartItem) => { - setItemToDelete(item); + + // Get cart data from server + const { data: cart, refetch: refetchCart } = api.cart.getUserCart.useQuery(); + const { mutate: removeFromCart } = api.cart.removeFromCart.useMutation({ + onSuccess: () => { + void refetchCart(); + }, + }); + const { mutate: clearCart } = api.cart.clearCart.useMutation(); + + const handleRemove = (cartItemId: string) => { + setItemToDelete(cartItemId); }; const confirmDelete = () => { if (itemToDelete) { - removeItem(itemToDelete.id); + removeFromCart({ cartItemId: itemToDelete }); setItemToDelete(null); } }; @@ -94,8 +108,15 @@ export default function ShoppingCart() { }; const handleBuy = async () => { - const token_ids = items.map((item) => item.tokenId); - const token_amounts = items.map((item) => item.quantity); + if (!cart) return; + + const token_ids = cart.items.map((item) => item.product.tokenId); + const token_amounts = cart.items.map((item) => item.quantity); + const totalPrice = cart.items.reduce( + (total, item) => total + item.product.price * item.quantity, + 0, + ); + console.log("buying items", token_ids, token_amounts, totalPrice); try { const tx_hash = await contract.buy_product( @@ -104,9 +125,9 @@ export default function ShoppingCart() { totalPrice, ); alert(`Items bought successfully tx hash: ${tx_hash}`); - for (const item of items) { - removeItem(item.id); - } + // Clear cart after successful purchase + clearCart(); + void refetchCart(); } catch (error) { if (error instanceof ContractsError) { alert(error.message); @@ -115,10 +136,23 @@ export default function ShoppingCart() { } }; - const totalPrice = items.reduce( - (total, item) => total + item.price * item.quantity, - 0, - ); + const totalPrice = + cart?.items.reduce( + (total, item) => total + item.product.price * item.quantity, + 0, + ) ?? 0; + + const getImageUrl = (nftMetadata: unknown): string => { + if (typeof nftMetadata !== "string") return "/images/default.webp"; + try { + const metadata = JSON.parse(nftMetadata) as NftMetadata; + return metadata.imageUrl; + } catch { + return "/images/default.webp"; + } + }; + + const hasItems = Boolean(cart?.items && cart.items.length > 0); return (
@@ -135,26 +169,28 @@ export default function ShoppingCart() {
- {items.length === 0 ? ( + {!hasItems ? (
{t("cart_empty_message")}
) : ( - items.map((item) => ( + cart?.items.map((item) => (
{item.name}
-

{t(item.name)}

+

+ {t(item.product.name)} +

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

@@ -162,13 +198,13 @@ export default function ShoppingCart() {
- {item.price * item.quantity} USD + {item.product.price * item.quantity} USD @@ -178,7 +214,7 @@ export default function ShoppingCart() { )}
- {items.length > 0 && ( + {hasItems && ( <>
diff --git a/apps/web/src/app/user-profile/page.tsx b/apps/web/src/app/user-profile/page.tsx index 9edc9a8..3c7ab7f 100644 --- a/apps/web/src/app/user-profile/page.tsx +++ b/apps/web/src/app/user-profile/page.tsx @@ -1,47 +1,82 @@ "use client"; -import PageHeader from "@repo/ui/pageHeader"; import { useAccount, useDisconnect } from "@starknet-react/core"; +import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; -import { useState } from "react"; +import { useEffect } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { ProfileCard } from "~/app/_components/features/ProfileCard"; import { ProfileOptions } from "~/app/_components/features/ProfileOptions"; +import Header from "~/app/_components/layout/Header"; import Main from "~/app/_components/layout/Main"; -import type { UserProfileType } from "~/types"; +import { api } from "~/trpc/react"; + +type Badge = "lover" | "contributor" | "producer"; export default function UserProfile() { const { t } = useTranslation(); const { address } = useAccount(); const { disconnect } = useDisconnect(); + const router = useRouter(); + const { data: session, status } = useSession(); + const userId = session?.user?.id; - const [user] = useState({ - name: "John Doe", - country: "united_states", - memberSince: 2020, - thumbnailUrl: "/images/user-profile/avatar.svg", - badges: ["lover", "contributor"], - }); + const { data: user, isLoading } = api.user.getUser.useQuery( + { userId: userId ?? "" }, + { + enabled: !!userId, + retry: false, + }, + ); - const router = useRouter(); + // Handle errors with useEffect + useEffect(() => { + if (!user && !isLoading && status === "authenticated") { + toast.error(t("error_fetching_profile")); + router.push("/"); + } + }, [user, isLoading, router, t, status]); + + // Handle session status + useEffect(() => { + if (status === "unauthenticated") { + router.push("/"); + } + }, [status, router]); + + // Show loading state + if (status === "loading" || isLoading) { + return ( +
+
+
+
+
+
+
+
+ ); + } + + // Return null if no user data + if (!user || status === "unauthenticated") { + return null; + } + + const userProfile = { + name: user.name ?? t("unnamed_user"), + country: "costa_rica", + memberSince: new Date(user.createdAt).getFullYear(), + walletAddress: user.walletAddress, + badges: ["lover", "contributor"] as Badge[], + }; return (
- router.back()} - showBlockie={false} - /> - +
+
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 fc855ab..9ae1e60 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 @@ -3,8 +3,11 @@ import { zodResolver } from "@hookform/resolvers/zod"; import Button from "@repo/ui/button"; import InputField from "@repo/ui/form/inputField"; -import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; import { useForm } from "react-hook-form"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { z } from "zod"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; @@ -21,10 +24,8 @@ type FormData = z.infer; function EditMyProfile() { const utils = api.useUtils(); const { t } = useTranslation(); - - const userId = "cm2wbxug00000kkm7tvhlujf2"; - - const { data: user, isLoading } = api.user.getUser.useQuery({ userId }); + const { data: session } = useSession(); + const router = useRouter(); const { register, handleSubmit, control, reset } = useForm({ resolver: zodResolver(schema), @@ -35,6 +36,33 @@ function EditMyProfile() { }, }); + const userId = session?.user?.id; + + const { + data: user, + isLoading: isLoadingUser, + error: userError, + } = api.user.getUser.useQuery( + { userId: userId ?? "" }, + { + enabled: !!userId, + retry: false, + }, + ); + + // If there's an error fetching the user, redirect to home + useEffect(() => { + if (!userId) { + router.push("/"); + return; + } + + if (userError) { + toast.error(t("error_fetching_profile")); + router.push("/"); + } + }, [userError, router, t, userId]); + useEffect(() => { if (user) { reset({ @@ -45,19 +73,35 @@ function EditMyProfile() { } }, [user, reset]); - const { mutate: updateProfile } = api.user.updateUserProfile.useMutation({ - onSuccess: async () => { - try { - await utils.user.getUser.invalidate({ userId }); - alert(t("profile_updated")); - } catch (error) { - console.error(t("invalidate_user_data_failed"), error); - } - }, - }); + const { mutate: updateProfile, isPending } = + api.user.updateUserProfile.useMutation({ + onSuccess: async () => { + try { + await utils.user.getUser.invalidate({ userId }); + toast.success(t("profile_updated")); + } catch (error) { + console.error(t("invalidate_user_data_failed"), error); + } + }, + onError: (error) => { + console.error("Error updating profile:", error); + if (error.message?.includes("Record to update not found")) { + toast.error(t("user_not_found")); + router.push("/"); + } else { + toast.error(t("error_updating_profile")); + } + }, + }); const onSubmit = (data: FormData) => { - void updateProfile({ + if (!userId) { + toast.error(t("session_expired")); + router.push("/"); + return; + } + + updateProfile({ userId, name: data.fullName, email: data.email, @@ -70,36 +114,43 @@ function EditMyProfile() { title={t("edit_my_profile")} backLink="/user/edit-profile" > - {isLoading ? ( -
{t("loading")}
- ) : ( - <> -
- - - - - - - )} +
+
+ + + + + +
); } diff --git a/apps/web/src/app/user/register-coffee/page.tsx b/apps/web/src/app/user/register-coffee/page.tsx index 425104e..e48ec71 100644 --- a/apps/web/src/app/user/register-coffee/page.tsx +++ b/apps/web/src/app/user/register-coffee/page.tsx @@ -127,10 +127,14 @@ export default function RegisterCoffee() {
-
-