Skip to content

Commit

Permalink
fix: restore cart functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
brolag committed Feb 18, 2025
1 parent 41e00db commit fef9342
Show file tree
Hide file tree
Showing 16 changed files with 648 additions and 144 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,28 @@
- Database seeding script for initial data population
- Starknet provider for blockchain connectivity
- TRPC setup for type-safe API routes
- Implemented Starknet payment processing in checkout flow
- Added loading states and error handling for payment transactions
- Integrated cart management with blockchain transactions
- Added i18n translations for checkout and payment flows

### Changed
- Updated project structure to use Turborepo for monorepo management
- Configured custom themes for DaisyUI
- Enhanced OrderReview component with Starknet payment functionality
- Improved error handling and user feedback during payment process

### Fixed
- Resolved potential issues with environment variable validation
- Fixed IPFS image loading in shopping cart
- Fixed cart counter synchronization with server state

### Removed
- Removed unused boilerplate code from initial setup

### Security
- Implemented secure practices for handling wallet addresses and user data
- Added transaction validation for Starknet payments

### Development
- Added scripts for database management and development server startup
Expand Down
33 changes: 28 additions & 5 deletions apps/web/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,7 @@
"seller1_fullname": "Seller 1",
"seller2_fullname": "Seller 2",
"delivery_method_label": "Delivery method",
"delivery_method": {
"address": "Address",
"meetup": "Meetup"
},
"delivery_method": "Delivery Method",
"quantity_with_unit": "{{count}} {{unit}}",
"total_balance_receivable": "Total Balance Receivable",
"recieve": "Receive",
Expand Down Expand Up @@ -279,5 +276,31 @@
"en": "English",
"es": "Spanish",
"pt": "Portuguese"
}
},

"send_to_my_home": "Send to my home",
"plus_20_usd": "+20 USD",
"pick_up_at_meetup": "Pick up at meetup",
"free": "Free",
"check_meetup_calendar": "Check meetup calendar",
"im_in": "I'm in",
"im_in_gam": "I'm in GAM",
"im_not_in_gam": "I'm not in GAM",
"next": "Next",

"proceed_to_payment": "Proceed to Payment",
"processing_payment": "Processing Payment...",
"review_your_order": "Review Your Order",
"quantity": "Quantity",
"product_price": "Product Price",
"delivery_address": "Delivery Address",
"my_home": "My Home",
"delivery_price": "Delivery Price",
"total_price": "Total Price",
"change_currency": "Change Currency",
"congrats": "Congratulations!",
"order_confirmation_message": "Your order has been confirmed and is being processed.",
"track_in_my_orders": "Track in My Orders",

"marketplace": "Marketplace"
}
81 changes: 81 additions & 0 deletions apps/web/src/app/_components/features/CartContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use client";

import { TrashIcon } from "@heroicons/react/24/outline";
import { Text } from "@repo/ui/typography";
import Image from "next/image";
import { useTranslation } from "react-i18next";
import { api } from "~/trpc/react";

export default function CartContent() {
const { t } = useTranslation();
const { data: cart, refetch: refetchCart } = api.cart.getUserCart.useQuery();
const { mutate: removeFromCart } = api.cart.removeFromCart.useMutation({
onSuccess: () => {
void refetchCart();
},
});

const handleRemove = (cartItemId: string) => {
removeFromCart({ cartItemId });
};

const getImageUrl = (nftMetadata: unknown): string => {
if (typeof nftMetadata !== "string") return "/images/default.webp";
try {
const metadata = JSON.parse(nftMetadata) as { imageUrl: string };
return metadata.imageUrl;
} catch {
return "/images/default.webp";
}
};

if (!cart?.items || cart.items.length === 0) {
return (
<div className="py-8 text-center text-gray-500">
{t("cart_empty_message")}
</div>
);
}

return (
<div className="space-y-4">
{cart.items.map((item) => (
<div
key={item.id}
className="py-4 flex items-center justify-between border-b"
>
<div className="flex items-center gap-3">
<Image
src={getImageUrl(item.product.nftMetadata)}
alt={item.product.name}
width={48}
height={48}
className="rounded-lg object-cover bg-gray-100"
/>
<div>
<Text className="font-medium text-gray-900">
{t(item.product.name)}
</Text>
<Text className="text-gray-400 text-sm">
{t("quantity_label")}: {item.quantity}
</Text>
</div>
</div>
<div className="flex items-center gap-4">
<span className="text-gray-900">
{item.product.price * item.quantity} USD
</span>
<button
type="button"
onClick={() => handleRemove(item.id)}
className="text-red-500 hover:text-red-600"
aria-label={`Remove ${item.product.name} from cart`}
>
<TrashIcon className="h-5 w-5" />
</button>
</div>
</div>
))}
</div>
);
}
174 changes: 77 additions & 97 deletions apps/web/src/app/_components/features/ProfileOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import LogoutModal from "~/app/_components/features/LogoutModal";
import { UserWalletsModal } from "~/app/_components/features/UserWalletsModal";
import { api } from "~/trpc/react";

interface ProfileOptionsProps {
address?: string;
Expand All @@ -33,11 +32,6 @@ type ProfileOption = {

export function ProfileOptions({ address: _ }: ProfileOptionsProps) {
const { t } = useTranslation();
const { data: user, isLoading } = api.user.getMe.useQuery(undefined, {
staleTime: 30_000, // Keep data fresh for 30 seconds
refetchOnWindowFocus: false,
});

const [isLogoutModalOpen, setIsLogoutModalOpen] = useState(false);
const [isWalletModalOpen, setIsWalletModalOpen] = useState(false);

Expand All @@ -57,102 +51,88 @@ export function ProfileOptions({ address: _ }: ProfileOptionsProps) {
setIsWalletModalOpen(false);
};

const getOptionsForRole = (userRole?: Role): ProfileOption[] => {
const commonOptions: ProfileOption[] = [
{ icon: UserIcon, label: t("edit_profile"), href: "/user/edit-profile" },
{
icon: HeartIcon,
label: t("favorite_products"),
href: "/user/favorites",
},
{
icon: CubeIcon,
label: t("my_collectibles"),
href: "/user/collectibles",
},
{
icon: ShoppingCartIcon,
label: t("my_orders"),
href: "/user/my-orders",
},
{ icon: WalletIcon, label: t("wallet"), onClick: openWalletModal },
{
icon: AdjustmentsHorizontalIcon,
label: t("settings"),
href: "/user/settings",
},
{
icon: NoSymbolIcon,
label: t("log_out"),
customClass: "text-error-default",
iconColor: "text-error-default",
onClick: openLogoutModal,
},
];
// Common options that are always shown
const commonOptions: ProfileOption[] = [
{ icon: UserIcon, label: t("edit_profile"), href: "/user/edit-profile" },
{
icon: HeartIcon,
label: t("favorite_products"),
href: "/user/favorites",
},
{
icon: CubeIcon,
label: t("my_collectibles"),
href: "/user/collectibles",
},
{
icon: ShoppingCartIcon,
label: t("my_orders"),
href: "/user/my-orders",
},
{ icon: WalletIcon, label: t("wallet"), onClick: openWalletModal },
{
icon: AdjustmentsHorizontalIcon,
label: t("settings"),
href: "/user/settings",
},
{
icon: NoSymbolIcon,
label: t("log_out"),
customClass: "text-error-default",
iconColor: "text-error-default",
onClick: openLogoutModal,
},
];

if (userRole === "COFFEE_PRODUCER") {
return [
...commonOptions.slice(0, 4),
{ 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",
},
...commonOptions.slice(4),
];
}
// Producer-specific options that are loaded after user data
const producerOptions: ProfileOption[] = [
{ 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",
},
];

return commonOptions;
};
const renderOption = (option: ProfileOption) => (
<div
key={`${option.label}-${option.href ?? "action"}`}
className="relative"
>
{option.href ? (
<Link
href={option.href}
className={`flex items-center p-2 hover:bg-gray-100 rounded transition-colors ${option.customClass ?? ""}`}
>
<option.icon className={`w-5 h-5 mr-3 ${option.iconColor ?? ""}`} />
<span>{option.label}</span>
</Link>
) : option.onClick ? (
<button
type="button"
onClick={option.onClick}
className={`flex items-center p-2 hover:bg-gray-100 rounded transition-colors w-full text-left ${option.customClass ?? ""}`}
>
<option.icon className={`w-5 h-5 mr-3 ${option.iconColor ?? ""}`} />
<span>{option.label}</span>
</button>
) : null}
<div className="h-px bg-gray-100 mx-2" />
</div>
);

const profileOptions = getOptionsForRole(user?.role);
return (
<div id="profile-options" className="bg-white rounded-lg overflow-hidden">
{/* Always render common options first */}
{commonOptions.slice(0, 4).map(renderOption)}

if (isLoading) {
return (
<div className="bg-white rounded-lg overflow-hidden p-4">
<div className="animate-pulse space-y-4">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="h-10 bg-gray-200 rounded" />
))}
</div>
</div>
);
}
{/* Show all producer options for now */}
{producerOptions.map(renderOption)}

{/* Always render remaining common options */}
{commonOptions.slice(4).map(renderOption)}

return (
<div className="bg-white rounded-lg overflow-hidden">
{profileOptions.map((option) => (
<div
key={`${option.label}-${option.href ?? "action"}`}
className="relative"
>
{option.href ? (
<Link
href={option.href}
className={`flex items-center p-2 hover:bg-gray-100 rounded transition-colors ${option.customClass ?? ""}`}
>
<option.icon
className={`w-5 h-5 mr-3 ${option.iconColor ?? ""}`}
/>
<span>{option.label}</span>
</Link>
) : option.onClick ? (
<button
type="button"
onClick={option.onClick}
className={`flex items-center p-2 hover:bg-gray-100 rounded transition-colors w-full text-left ${option.customClass ?? ""}`}
>
<option.icon
className={`w-5 h-5 mr-3 ${option.iconColor ?? ""}`}
/>
<span>{option.label}</span>
</button>
) : null}
<div className="h-px bg-gray-100 mx-2" />
</div>
))}
<LogoutModal isOpen={isLogoutModalOpen} onClose={closeLogoutModal} />
<UserWalletsModal isOpen={isWalletModalOpen} onClose={closeWalletModal} />
</div>
Expand Down
Loading

0 comments on commit fef9342

Please sign in to comment.