Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion app/item/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { motion } from 'framer-motion'
import {
ArrowLeftIcon,
ShoppingCartIcon,
BuildingStorefrontIcon
BuildingStorefrontIcon,
NoSymbolIcon
} from '@heroicons/react/24/outline'
import { CheckIcon } from '@heroicons/react/24/solid'
import { Sidebar } from '@/components/layout/sidebar'
Expand All @@ -20,6 +21,7 @@ import { useSettingsStore } from '@/lib/store'
import { storeService } from '@/lib/services/store-service'
import { storeItemService } from '@/lib/services/store-item-service'
import { cartService } from '@/lib/services/cart-service'
import { useBlock } from '@/hooks/use-block'
import type { Store, StoreItem } from '@/lib/types'

function LoadingFallback() {
Expand Down Expand Up @@ -61,6 +63,9 @@ function ItemDetailContent() {
const [cartItemCount, setCartItemCount] = useState(0)
const addedToCartTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)

// Check if store owner is blocked
const { isBlocked: isOwnerBlocked, isLoading: isBlockLoading, toggleBlock } = useBlock(store?.ownerId ?? '')

// Cleanup timeout on unmount
useEffect(() => {
return () => {
Expand Down Expand Up @@ -287,6 +292,32 @@ function ItemDetailContent() {
{/* Image Gallery */}
<ImageGallery images={images} alt={item.title} />

{/* Blocked Store Owner Banner */}
{isOwnerBlocked && (
<div className="mx-4 mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<div className="flex items-center gap-3">
<NoSymbolIcon className="h-6 w-6 text-red-500" />
<div className="flex-1">
<p className="font-medium text-red-700 dark:text-red-400">
You have blocked this store owner
</p>
<p className="text-sm text-red-600 dark:text-red-400/80">
Items from this store won&apos;t appear in browse listings.
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => toggleBlock()}
disabled={isBlockLoading}
className="border-red-300 dark:border-red-700 text-red-700 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/40"
>
Unblock
</Button>
</div>
</div>
)}

{/* Item Info */}
<div className="p-4 space-y-4">
{/* Store Link */}
Expand Down
48 changes: 43 additions & 5 deletions app/store/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useState, useEffect } from 'react'
import { useState, useEffect, useMemo } from 'react'
import { useRouter } from 'next/navigation'
import { motion } from 'framer-motion'
import {
Expand All @@ -20,6 +20,7 @@ import { useSdk } from '@/contexts/sdk-context'
import { useSettingsStore } from '@/lib/store'
import { storeService } from '@/lib/services/store-service'
import { storeReviewService } from '@/lib/services/store-review-service'
import { checkBlockedForAuthors } from '@/hooks/use-block'
import type { Store, StoreRatingSummary } from '@/lib/types'

export default function StoreBrowsePage() {
Expand All @@ -29,6 +30,7 @@ export default function StoreBrowsePage() {
const potatoMode = useSettingsStore((s) => s.potatoMode)
const [stores, setStores] = useState<Store[]>([])
const [storeRatings, setStoreRatings] = useState<Map<string, StoreRatingSummary>>(new Map())
const [blockedOwners, setBlockedOwners] = useState<Map<string, boolean>>(new Map())
const [isLoading, setIsLoading] = useState(true)
const [searchQuery, setSearchQuery] = useState('')
const [hasStore, setHasStore] = useState(false)
Expand Down Expand Up @@ -78,13 +80,49 @@ export default function StoreBrowsePage() {
loadStores().catch(console.error)
}, [sdkReady])

// Filter stores by search query
const filteredStores = searchQuery
? stores.filter(store =>
// Check which store owners are blocked
useEffect(() => {
if (!user?.identityId || stores.length === 0) {
setBlockedOwners(new Map())
return
}

let cancelled = false

const checkBlockedOwners = async () => {
const ownerIds = stores.map(store => store.ownerId)
const blocked = await checkBlockedForAuthors(user.identityId, ownerIds)
if (!cancelled) {
setBlockedOwners(blocked)
}
}

checkBlockedOwners().catch(console.error)

return () => {
cancelled = true
}
}, [user?.identityId, stores])

// Filter stores by search query and block status
const filteredStores = useMemo(() => {
let filtered = stores

// Filter out stores owned by blocked users
if (blockedOwners.size > 0) {
filtered = filtered.filter(store => !blockedOwners.get(store.ownerId))
}
Comment on lines +133 to +136

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Wait for block status before rendering stores

blockedOwners is initially empty, and the independent block-status effect does not participate in isLoading. Consequently, when ratings finish before the block query—or the authenticated identity changes after the list is visible—these lines treat unresolved status as if no owners were blocked and render every store as clickable. Keep the list loading or hidden until the block check for the current identity and store set completes.

source: ['codex']


// Apply search filter
if (searchQuery) {
filtered = filtered.filter(store =>
store.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
store.description?.toLowerCase().includes(searchQuery.toLowerCase())
)
: stores
}

return filtered
}, [stores, blockedOwners, searchQuery])

const handleStoreClick = (storeId: string) => {
router.push(`/store/view?id=${storeId}`)
Expand Down
33 changes: 32 additions & 1 deletion app/store/view/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
ChatBubbleLeftIcon,
MapPinIcon,
ShoppingCartIcon,
StarIcon
StarIcon,
NoSymbolIcon
} from '@heroicons/react/24/outline'
import { StarIcon as StarIconSolid } from '@heroicons/react/24/solid'
import { Sidebar } from '@/components/layout/sidebar'
Expand All @@ -25,6 +26,7 @@ import { storeItemService } from '@/lib/services/store-item-service'
import { storeReviewService } from '@/lib/services/store-review-service'
import { cartService } from '@/lib/services/cart-service'
import { parseStorePolicies } from '@/lib/utils/policies'
import { useBlock } from '@/hooks/use-block'
import type { Store, StoreItem, StoreReview, StoreRatingSummary, StorePolicy } from '@/lib/types'

function LoadingFallback() {
Expand Down Expand Up @@ -68,6 +70,9 @@ function StoreDetailContent() {
const [ownerDisplayName, setOwnerDisplayName] = useState<string | null>(null)
const [ownerUsername, setOwnerUsername] = useState<string | null>(null)

// Check if store owner is blocked
const { isBlocked: isOwnerBlocked, isLoading: isBlockLoading, toggleBlock } = useBlock(store?.ownerId ?? '')

// Subscribe to cart changes
useEffect(() => {
const unsubscribe = cartService.subscribe(() => {
Expand Down Expand Up @@ -301,6 +306,32 @@ function StoreDetailContent() {
</div>
</div>

{/* Blocked Store Banner */}
{isOwnerBlocked && (
<div className="mx-4 mb-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<div className="flex items-center gap-3">
<NoSymbolIcon className="h-6 w-6 text-red-500" />
<div className="flex-1">
<p className="font-medium text-red-700 dark:text-red-400">
You have blocked this store owner
</p>
<p className="text-sm text-red-600 dark:text-red-400/80">
This store&apos;s products won&apos;t appear in browse listings.
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => toggleBlock()}
disabled={isBlockLoading}
className="border-red-300 dark:border-red-700 text-red-700 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/40"
>
Unblock

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Do not treat inherited blocks as directly unblockable

useBlock derives its boolean from blockService.isBlocked, which includes blocks inherited from followed block lists. However, toggleBlock handles every true value with unblockUser(viewerId, ownerId), which only deletes the viewer's own block and returns success when no such document exists. For an inherited block, the UI therefore shows a success toast and optimistically hides the banner even though the owner remains blocked and browse filtering will rediscover the block. The same control exists in app/item/page.tsx; expose block provenance and recompute the combined status after removing any direct block, while routing inherited-only cases to the relevant block-list settings.

source: ['codex']

</Button>
</div>
</div>
)}

{/* Tabs */}
<div className="flex border-b border-gray-200 dark:border-gray-800">
<button
Expand Down
18 changes: 17 additions & 1 deletion components/store/cart-store-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import { forwardRef } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { useRouter } from 'next/navigation'
import { BuildingStorefrontIcon } from '@heroicons/react/24/outline'
import { BuildingStorefrontIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline'
import { CartItemRow } from './cart-item-row'
import { Button } from '@/components/ui/button'
import { formatPrice } from '@/lib/utils/format'
import { cartService } from '@/lib/services/cart-service'
import { useBlock } from '@/hooks/use-block'
import type { CartItem, Store } from '@/lib/types'

interface CartStoreSectionProps {
Expand All @@ -21,6 +22,9 @@ export const CartStoreSection = forwardRef<HTMLDivElement, CartStoreSectionProps
function CartStoreSection({ storeId, store, items, onRemoveAll }, ref) {
const router = useRouter()

// Check if store owner is blocked
const { isBlocked: isOwnerBlocked } = useBlock(store?.ownerId ?? '')

const subtotal = items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0)
const currency = items[0]?.currency || 'USD'

Expand Down Expand Up @@ -71,6 +75,18 @@ export const CartStoreSection = forwardRef<HTMLDivElement, CartStoreSectionProps
</button>
</div>

{/* Blocked Store Owner Warning */}
{isOwnerBlocked && (
<div className="mx-4 mt-2 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<div className="flex items-center gap-2">
<ExclamationTriangleIcon className="h-5 w-5 text-amber-500 flex-shrink-0" />
<p className="text-sm text-amber-700 dark:text-amber-400">
You have blocked this store owner. Consider removing these items.
</p>
</div>
</div>
)}

{/* Items */}
<div className="divide-y divide-gray-100 dark:divide-gray-900">
<AnimatePresence mode="popLayout">
Expand Down