Skip to content

Commit

Permalink
fix sorting and filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
ddaoxuan committed Oct 2, 2024
1 parent 3d4767a commit 57787eb
Show file tree
Hide file tree
Showing 20 changed files with 147 additions and 92 deletions.
10 changes: 4 additions & 6 deletions starters/shopify-algolia/app/api/reviews/ai-summary/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,16 @@ export async function GET(req: Request) {
}

const [{ hits: allReviews = [] }, { hits: allProducts = [] }] = await Promise.all([
algolia.search<Review>({
algolia.getAllResults<Review>({
indexName: env.ALGOLIA_REVIEWS_INDEX,
searchParams: {
hitsPerPage: 10000,
browseParams: {
attributesToRetrieve: ["body", "title", "product_handle", "rating"],
filters: "published:true AND hidden:false",
},
}),
algolia.search<CommerceProduct>({
algolia.getAllResults<CommerceProduct>({
indexName: env.ALGOLIA_PRODUCTS_INDEX,
searchParams: {
hitsPerPage: 10000,
browseParams: {
attributesToRetrieve: ["handle", "title", "id", "totalReviews"],
},
}),
Expand Down
18 changes: 10 additions & 8 deletions starters/shopify-algolia/app/api/reviews/sync/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,15 @@ export async function GET(req: Request) {

const [allReviews = [], { hits: allProducts = [] }, { hits: allIndexReviews = [] }] = await Promise.all([
reviewsClient.getAllProductReviews(),
algolia.search<CommerceProduct>({
algolia.getAllResults<CommerceProduct>({
indexName: env.ALGOLIA_PRODUCTS_INDEX,
searchParams: {
hitsPerPage: 10000,
browseParams: {
attributesToRetrieve: ["handle", "totalReviews", "avgRating", "id"],
},
}),
algolia.search<Review>({
algolia.getAllResults<Review>({
indexName: env.ALGOLIA_REVIEWS_INDEX,
searchParams: {
hitsPerPage: 10000,
browseParams: {
attributesToRetrieve: ["updated_at", "id"],
},
}),
Expand All @@ -67,7 +65,9 @@ export async function GET(req: Request) {
.filter(Boolean)

if (!reviewsDelta.length && !productTotalReviewsDelta.length) {
return new Response(JSON.stringify({ message: "Nothing to sync" }), { status: 200 })
return new Response(JSON.stringify({ message: "Nothing to sync" }), {
status: 200,
})
}

!!reviewsDelta.length &&
Expand All @@ -87,5 +87,7 @@ export async function GET(req: Request) {
console.log("API/sync:Products synced", productTotalReviewsDelta.length)
})()

return new Response(JSON.stringify({ message: "All synced" }), { status: 200 })
return new Response(JSON.stringify({ message: "All synced" }), {
status: 200,
})
}
17 changes: 4 additions & 13 deletions starters/shopify-algolia/app/product/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { getProduct, getProductReviews } from "app/actions/product.actions"
import { getProduct } from "app/actions/product.actions"
import { Breadcrumbs } from "components/Breadcrumbs/Breadcrumbs"

import { getCombination, getOptionsFromUrl, hasValidOption, removeOptionsFromUrl } from "utils/productOptionsUtils"
Expand Down Expand Up @@ -50,7 +50,7 @@ export async function generateStaticParams() {
}

export default async function Product({ params: { slug } }: ProductProps) {
const [product, { reviews, total: totalReviews }] = await Promise.all([getProduct(removeOptionsFromUrl(slug)), getProductReviews(removeOptionsFromUrl(slug), { limit: 16 })])
const product = await getProduct(removeOptionsFromUrl(slug))

const { color } = getOptionsFromUrl(slug)
const hasInvalidOptions = !hasValidOption(product?.variants, "color", color)
Expand Down Expand Up @@ -94,18 +94,9 @@ export default async function Product({ params: { slug } }: ProductProps) {
<FaqSection />
</RightSection>
</div>
<Suspense>
<ReviewsSection
avgRating={product.avgRating}
productHandle={product.handle}
productId={product.id}
reviews={reviews?.map((review) => ({ ...review, author: review.reviewer.name })) || []}
total={totalReviews}
summary={product.reviewsSummary}
/>
</Suspense>
<ReviewsSection avgRating={product.avgRating} productHandle={product.handle} productId={product.id} summary={product.reviewsSummary} slug={slug} />
<Suspense fallback={<SimilarProductsSectionSkeleton />}>
<SimilarProductsSection collectionHandle={lastCollection?.handle} slug={slug} />
<SimilarProductsSection collectionHandle={lastCollection?.handle} objectID={product.objectID} />
</Suspense>
</main>
</div>
Expand Down
5 changes: 2 additions & 3 deletions starters/shopify-algolia/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getDemoCategories, getDemoProducts, isDemoMode } from "utils/demoUtils"
import type { PlatformCollection } from "lib/shopify/types"
import type { CommerceProduct } from "types"

export const revalidate = 604800
export const revalidate = 604800 // once a week
export const runtime = "nodejs"

const BASE_URL = env.LIVE_URL
Expand Down Expand Up @@ -80,12 +80,11 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
return [...staticRoutes, ...paginationRoutes, ...productRoutes, ...collectionsRoutes]
}

// Pull only 100 results for the case of the demo
async function getResults<T extends Record<string, any>>(indexName: string) {
const { hits } = await algolia.search<T>({
indexName,
searchParams: {
hitsPerPage: 100,
hitsPerPage: 50,
},
})

Expand Down
5 changes: 4 additions & 1 deletion starters/shopify-algolia/clients/search.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import "server-only"

import { env } from "env.mjs"
import { algolia as searchClient } from "lib/algolia"

export const algolia: ReturnType<typeof searchClient> = searchClient({
applicationId: env.ALGOLIA_APP_ID || "",
apiKey: env.ALGOLIA_API_KEY || "",
// Make sure write api key never leaks to the client
apiKey: env.ALGOLIA_WRITE_API_KEY || "",
})
4 changes: 2 additions & 2 deletions starters/shopify-algolia/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export const env = createEnv({
ALGOLIA_PRODUCTS_INDEX: z.string(),
ALGOLIA_CATEGORIES_INDEX: z.string(),
ALGOLIA_REVIEWS_INDEX: z.string().optional(),
ALGOLIA_API_KEY: z.string().optional(),
ALGOLIA_APP_ID: z.string().optional(),
ALGOLIA_WRITE_API_KEY: z.string().optional(),
REPLICATE_API_KEY: z.string().optional(),
OPENAI_API_KEY: z.string().optional(),
LIVE_URL: z.string().optional(),
Expand All @@ -40,8 +40,8 @@ export const env = createEnv({
SHOPIFY_HIERARCHICAL_NAV_HANDLE: process.env.SHOPIFY_HIERARCHICAL_NAV_HANDLE,
ALGOLIA_PRODUCTS_INDEX: process.env.ALGOLIA_PRODUCTS_INDEX || "products",
ALGOLIA_CATEGORIES_INDEX: process.env.ALGOLIA_CATEGORIES_INDEX || "categories",
ALGOLIA_API_KEY: process.env.ALGOLIA_API_KEY || "demo",
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || "demo",
ALGOLIA_WRITE_API_KEY: process.env.ALGOLIA_WRITE_API_KEY || "demo",
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
ALGOLIA_REVIEWS_INDEX: process.env.ALGOLIA_REVIEWS_INDEX,
LIVE_URL: process.env.LIVE_URL || "https://commerce.blazity.com",
Expand Down
9 changes: 5 additions & 4 deletions starters/shopify-algolia/lib/algolia/filterBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// For numeric attributes only
export enum ComparisonOperators {
Equal = ":",
Equal = "=",
NotEqual = "!=",
GreaterThan = ">",
GreaterThanOrEqual = ">=",
Expand All @@ -19,11 +20,11 @@ type Value = string | number | boolean | (string | number | boolean)[]
export class FilterBuilder {
private expression: string[] = []

where(attribute: string, value: Value, operator: ComparisonOperators = ComparisonOperators.Equal): FilterBuilder {
where(attribute: string, value: Value, operator?: ComparisonOperators): FilterBuilder {
if (Array.isArray(value)) {
this.in(attribute, value)
} else {
this.expression.push(`${attribute}${operator}${this.formatValue(value)}`)
this.expression.push(`${attribute}${operator || ":"}${this.formatValue(value)}`)
}
return this
}
Expand Down Expand Up @@ -67,7 +68,7 @@ export class FilterBuilder {
const subBuilder = new FilterBuilder()
fn(subBuilder)
const subExpression = subBuilder.build()
this.expression.push(`(${subExpression})`)
this.expression.push(`${subExpression}`)
return this
}

Expand Down
74 changes: 64 additions & 10 deletions starters/shopify-algolia/lib/algolia/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import {
algoliasearch,
type BrowseProps,
type DeleteObjectsOptions,
type GetRecommendationsParams,
type PartialUpdateObjectsOptions,
type SearchMethodParams,
type SearchResponse,
type SearchSingleIndexProps,
} from "algoliasearch"

import { FilterBuilder } from "./filterBuilder"

const algoliaClient = (args: { applicationId: string; apiKey: string }) => {
Expand All @@ -14,36 +17,87 @@ const algoliaClient = (args: { applicationId: string; apiKey: string }) => {

export const algolia = (args: { applicationId: string; apiKey: string }) => {
const client = algoliaClient(args)
const recommendationClient = client.initRecommend()

return {
search: <T extends Record<string, any>>(args: SearchSingleIndexProps) => search<T>(args, client),
update: (args: PartialUpdateObjectsOptions) => updateObjects(args, client),
delete: (args: DeleteObjectsOptions) => deleteObjects(args, client),
create: (args: PartialUpdateObjectsOptions) => createObjects(args, client),
multiSearch: <T extends Record<string, any>>(args: SearchMethodParams) => multiSearch<T>(args, client),
search: async <T extends Record<string, any>>(args: SearchSingleIndexProps) => search<T>(args, client),
getAllResults: async <T extends Record<string, any>>(args: BrowseProps) => getAllResults<T>(client, args),
update: async (args: PartialUpdateObjectsOptions) => updateObjects(args, client),
delete: async (args: DeleteObjectsOptions) => deleteObjects(args, client),
create: async (args: PartialUpdateObjectsOptions) => createObjects(args, client),
multiSearch: async <T extends Record<string, any>>(args: SearchMethodParams) => multiSearch<T>(args, client),
getRecommendations: async (args: GetRecommendationsParams) => getRecommendations(recommendationClient, args),
filterBuilder: () => new FilterBuilder(),
mapIndexToSort,
}
}

const search = <T extends Record<string, any>>(args: SearchSingleIndexProps, client: ReturnType<typeof algoliaClient>) => {
const search = async <T extends Record<string, any>>(args: SearchSingleIndexProps, client: ReturnType<typeof algoliaClient>) => {
return client.searchSingleIndex<T>(args)
}

const updateObjects = (args: PartialUpdateObjectsOptions, client: ReturnType<typeof algoliaClient>) => {
// agregator as temp fix for now
const getAllResults = async <T extends Record<string, any>>(client: ReturnType<typeof algoliaClient>, args: BrowseProps) => {
const allHits: T[] = []
let totalPages: number
let currentPage = 0

do {
const { hits, nbPages } = await client.browseObjects<T>({
...args,
browseParams: {
...args.browseParams,
hitsPerPage: 1000,
},
aggregator: () => null,
})
allHits.push(...hits)
totalPages = nbPages || 0
currentPage++
} while (currentPage < totalPages)

return { hits: allHits, totalPages }
}

const updateObjects = async (args: PartialUpdateObjectsOptions, client: ReturnType<typeof algoliaClient>) => {
return client.partialUpdateObjects(args)
}

const deleteObjects = (args: DeleteObjectsOptions, client: ReturnType<typeof algoliaClient>) => {
const deleteObjects = async (args: DeleteObjectsOptions, client: ReturnType<typeof algoliaClient>) => {
return client.deleteObjects(args)
}

const createObjects = (args: PartialUpdateObjectsOptions, client: ReturnType<typeof algoliaClient>) => {
const createObjects = async (args: PartialUpdateObjectsOptions, client: ReturnType<typeof algoliaClient>) => {
return client.partialUpdateObjects({
...args,
createIfNotExists: true,
})
}

const multiSearch = <T extends Record<string, any>>(args: SearchMethodParams, client: ReturnType<typeof algoliaClient>) => {
const multiSearch = async <T extends Record<string, any>>(args: SearchMethodParams, client: ReturnType<typeof algoliaClient>) => {
return client.search<T>(args) as Promise<{ results: SearchResponse<T>[] }>
}

const getRecommendations = async (client: ReturnType<ReturnType<typeof algoliaClient>["initRecommend"]>, args: GetRecommendationsParams) => {
return client.getRecommendations(args)
}

export type SortType = "minPrice:desc" | "minPrice:asc" | "avgRating:desc" | "updatedAtTimestamp:asc" | "updatedAtTimestamp:desc"

const mapIndexToSort = (index: string, sortOption: SortType) => {
switch (sortOption) {
case "minPrice:desc":
return `${index}_price_desc`
case "minPrice:asc":
return `${index}_price_asc`
case "avgRating:desc":
return `${index}_rating_desc`
case "updatedAtTimestamp:asc":
return `${index}_updated_asc`
case "updatedAtTimestamp:desc":
return `${index}_updated_desc`

default:
return index
}
}
4 changes: 2 additions & 2 deletions starters/shopify-algolia/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@
"schema-dts": "^1.1.2",
"sharp": "0.32.6",
"sonner": "^1.4.41",
"zustand": "^4.5.2",
"zod": "^3.22.4"
"zod": "^3.22.4",
"zustand": "^4.5.2"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.2",
Expand Down
1 change: 1 addition & 0 deletions starters/shopify-algolia/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export type CommerceProduct = PlatformProduct & {
avgRating?: number
totalReviews?: number
reviewsSummary?: string
objectID: string // algolia necessary attribute
}
2 changes: 1 addition & 1 deletion starters/shopify-algolia/utils/demoUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function isDemoMode(): boolean {
isDemoValue(process.env.SHOPIFY_APP_API_SECRET_KEY) ||
isDemoValue(process.env.SHOPIFY_STORE_DOMAIN) ||
isDemoValue(process.env.ALGOLIA_APP_ID) ||
isDemoValue(process.env.ALGOLIA_API_KEY) ||
isDemoValue(process.env.ALGOLIA_WRITE_API_KEY) ||
isDemoValue(process.env.ALGOLIA_CATEGORIES_INDEX) ||
isDemoValue(process.env.ALGOLIA_PRODUCTS_INDEX) ||
!process.env.LIVE_URL ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const getBestOffers = unstable_cache(

const { hits } = await algolia.search<CommerceProduct>({
indexName: env.ALGOLIA_PRODUCTS_INDEX,
//@TODO REIMPLEMENT SORT
searchParams: {
hitsPerPage: 8,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const getPriceRangedProducts = unstable_cache(

const { hits } = await algolia.search<CommerceProduct>({
indexName: env.ALGOLIA_PRODUCTS_INDEX,
//@TODO REIMPLEMENT SORT
searchParams: {
hitsPerPage: 8,
filters: algolia.filterBuilder().where("minPrice", 50, ComparisonOperators.LessThanOrEqual).build(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ const getNewestProducts = unstable_cache(
async () => {
if (isDemoMode()) return getDemoProducts().hits.slice(0, 8)
const { hits } = await algolia.search<CommerceProduct>({
indexName: env.ALGOLIA_PRODUCTS_INDEX,
//@TODO REIMPLEMENT SORT
indexName: algolia.mapIndexToSort(env.ALGOLIA_PRODUCTS_INDEX, "updatedAtTimestamp:asc"),
searchParams: {
hitsPerPage: 8,
},
Expand Down
4 changes: 2 additions & 2 deletions starters/shopify-algolia/views/Listing/Facet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export function Facet({ id, title, distribution, isChecked, onCheckedChange }: F
<p className="text-[14px] text-neutral-500">No {title.toLowerCase()} found</p>
) : (
<div className="grid gap-2">
{distributionsEntries.map(([value], index) => (
{distributionsEntries.map(([value, count], index) => (
<Label key={value + index} className="flex items-center gap-2 font-normal">
<Checkbox name={value} checked={isChecked(value)} onCheckedChange={(checked) => onCheckedChange(!!checked, value)} />
{value}
{value} ({count})
</Label>
))}
</div>
Expand Down
15 changes: 9 additions & 6 deletions starters/shopify-algolia/views/Listing/RatingFacet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ export function RatingFacet({ id, title, distribution, isChecked, onCheckedChang
<p className="text-[14px] text-neutral-500">No {title.toLowerCase()} found</p>
) : (
<div className="grid gap-2">
{distributionsEntries.map(([value], index) => (
<Label key={value + index} className="flex items-center gap-2 font-normal">
<Checkbox name={value} checked={isChecked(value)} onCheckedChange={(checked) => onCheckedChange(!!checked, value)} />
<StarRating rating={parseInt(value)} /> {parseInt(value) !== 5 && `& up`}{" "}
</Label>
))}
{distributionsEntries.map(
([value, count], index) =>
+value >= 3 && (
<Label key={value + index} className="flex items-center gap-2 font-normal">
<Checkbox name={value} checked={isChecked(value)} onCheckedChange={(checked) => onCheckedChange(!!checked, value)} />
<StarRating rating={parseInt(value)} /> {parseInt(value) !== 5 && `& up`} ({count})
</Label>
)
)}
</div>
)}
</AccordionContent>
Expand Down
1 change: 0 additions & 1 deletion starters/shopify-algolia/views/Product/FavoriteMarker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export function FavoriteMarker({ handle }: { handle: string }) {
Favorite
<HeartIcon
onAnimationEnd={() => {
console.log("end")
setIsAnimating(false)
}}
className={cn("ml-2 size-5 transition-all", isActive ? "text-red-500 " : "text-black", isAnimating && "animate-single-bounce")}
Expand Down
Loading

0 comments on commit 57787eb

Please sign in to comment.