Skip to content

Commit

Permalink
refactor: lib search
Browse files Browse the repository at this point in the history
  • Loading branch information
ddaoxuan committed Oct 31, 2024
1 parent 763a488 commit 9ce23bb
Show file tree
Hide file tree
Showing 28 changed files with 834 additions and 489 deletions.
93 changes: 93 additions & 0 deletions starters/shopify-algolia/components/modals/login-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
import { getCurrentUser, loginUser } from "app/actions/user.actions"
import { Button } from "components/ui/button-old"
import { DialogFooter } from "components/ui/dialog"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "components/ui/form"
import { GenericModal } from "components/generic-modal"
import { Input } from "components/ui/input"
import { Logo } from "components/logo"
import { useModalStore } from "stores/modal-store"
import { useUserStore } from "stores/user-store"

const passwordRegexp = new RegExp(/(?=.*\d)(?=.*\W)(?=.*[a-z])(?=.*[A-Z]).{8,20}$/)

const formSchema = z.object({
email: z.string().email().min(3).max(64),
password: z.string().min(8).max(20).regex(passwordRegexp, "Password must have at least one number, one symbol, one uppercase letter, and be at least 8 characters"),
})

const formFields = [
{ label: "Email", name: "email", type: "text", placeholder: "Enter email..." },
{ label: "Password", name: "password", type: "password", placeholder: "Enter password..." },
] as const

export function LoginModal() {
const setUser = useUserStore((s) => s.setUser)
const modals = useModalStore((s) => s.modals)
const closeModal = useModalStore((s) => s.closeModal)
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
})

async function onSubmit(payload: z.infer<typeof formSchema>) {
const { email, password } = payload
const user = await loginUser({ email, password })

if (user) {
const currentUser = await getCurrentUser()
currentUser && setUser(currentUser)

toast.success("Successfully logged in")
closeModal("login")

return
}

form.setError("root", { message: "Couldn't log in. The email address or password is incorrect." })
}

return (
<GenericModal title="Login" open={!!modals["login"]} onOpenChange={() => closeModal("login")}>
<Form {...form}>
<Logo className="mt-6 flex size-24 w-full justify-center" />
{form.formState.errors.root?.message && <p className="mt-6 w-full text-[14px] leading-tight tracking-tight text-red-400">{form.formState.errors.root?.message}</p>}
<form name="loginForm" id="loginForm" onSubmit={form.handleSubmit(onSubmit)} className="space-y-1">
{formFields.map((singleField) => (
<FormField
key={singleField.name}
control={form.control}
name={singleField.name}
render={({ field }) => (
<FormItem>
<FormLabel>{singleField.label}</FormLabel>
<FormControl>
<Input type={singleField.type} className="text-sm" placeholder={singleField.placeholder} {...field} />
</FormControl>
<FormMessage className="text-xs font-normal text-red-400" />
</FormItem>
)}
/>
))}
</form>
</Form>

<DialogFooter>
<Button
size="lg"
form="loginForm"
className="hover:text-white"
variant="secondary"
isAnimated={false}
type="submit"
disabled={form.formState.isSubmitting}
isLoading={form.formState.isSubmitting}
>
Submit
</Button>
</DialogFooter>
</GenericModal>
)
}
6 changes: 6 additions & 0 deletions starters/shopify-algolia/components/modals/modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import React from "react"
import { type Modal, useModalStore } from "stores/modal-store"
import { ReviewModal } from "./review-modal"

const LoginModal = dynamic(() => import("./login-modal").then((m) => m.LoginModal), { loading: Placeholder })
const SignupModal = dynamic(() => import("./signup-modal").then((m) => m.SignupModal), { loading: Placeholder })
const SearchModal = dynamic(() => import("./search-modal").then((m) => m.SearchModal), { loading: Placeholder })

export function Modals() {
Expand All @@ -21,6 +23,10 @@ export function Modals() {

function ModalsFactory({ type }: { type: Modal }) {
switch (type) {
case "login":
return <LoginModal />
case "signup":
return <SignupModal />
case "search":
return <SearchModal />
case "review":
Expand Down
92 changes: 92 additions & 0 deletions starters/shopify-algolia/components/modals/signup-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
import { getCurrentUser, signupUser } from "app/actions/user.actions"
import { Button } from "components/ui/button-old"
import { DialogFooter } from "components/ui/dialog"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "components/ui/form"
import { GenericModal } from "components/generic-modal"
import { Input } from "components/ui/input"
import { Logo } from "components/logo"
import { useModalStore } from "stores/modal-store"
import { useUserStore } from "stores/user-store"

const passwordRegexp = new RegExp(/(?=.*\d)(?=.*\W)(?=.*[a-z])(?=.*[A-Z]).{8,20}$/)

const formSchema = z.object({
email: z.string().email().min(3).max(64),
password: z.string().min(8).max(20).regex(passwordRegexp, "Password must have at least one number, one symbol, one uppercase letter, and be at least 8 characters"),
})

const formFields = [
{ label: "Email", name: "email", type: "text", placeholder: "Enter email..." },
{ label: "Password", name: "password", type: "password", placeholder: "Enter password..." },
] as const

export function SignupModal() {
const modals = useModalStore((s) => s.modals)
const setUser = useUserStore((s) => s.setUser)
const closeModal = useModalStore((s) => s.closeModal)
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
})

async function onSubmit(payload: z.infer<typeof formSchema>) {
const { email, password } = payload
const user = await signupUser({ email, password })

if (user) {
const currentUser = await getCurrentUser()
currentUser && setUser(currentUser)

closeModal("signup")
toast.success("You have successfully signed up! You can now log in.")
return
}

toast.error("Couldn't create user. The email address may be already in use.")
}

return (
<GenericModal title="Signup" open={!!modals["signup"]} onOpenChange={() => closeModal("signup")}>
<Form {...form}>
<Logo className="mt-6 flex size-24 w-full justify-center" />
{form.formState.errors.root?.message && <p className="mt-6 w-full text-[14px] leading-tight tracking-tight text-red-400">{form.formState.errors.root?.message}</p>}
<form name="loginForm" id="loginForm" onSubmit={form.handleSubmit(onSubmit)} className="space-y-1">
{formFields.map((singleField) => (
<FormField
key={singleField.name}
control={form.control}
name={singleField.name}
render={({ field }) => (
<FormItem>
<FormLabel>{singleField.label}</FormLabel>
<FormControl>
<Input type={singleField.type} className="text-sm" placeholder={singleField.placeholder} {...field} />
</FormControl>
<FormMessage className="text-xs font-normal text-red-400" />
</FormItem>
)}
/>
))}
</form>
</Form>

<DialogFooter>
<Button
size="lg"
form="loginForm"
className="hover:text-white"
variant="secondary"
isAnimated={false}
type="submit"
disabled={form.formState.isSubmitting}
isLoading={form.formState.isSubmitting}
>
Submit
</Button>
</DialogFooter>
</GenericModal>
)
}
2 changes: 1 addition & 1 deletion starters/shopify-algolia/stores/modal-store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { create } from "zustand"

export type Modal = "search" | "facets-mobile" | "review"
export type Modal = "login" | "signup" | "search" | "facets-mobile" | "review"

interface ModalStore {
modals: Partial<Record<Modal, boolean>>
Expand Down
4 changes: 2 additions & 2 deletions starters/shopify-meilisearch/app/actions/product.actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use server"

import { meilisearch } from "clients/search"
import { searchClient } from "lib/meilisearch/client"
import { env } from "env.mjs"
import { unstable_cache } from "next/cache"
import type { CommerceProduct } from "types"
Expand All @@ -14,7 +14,7 @@ export const searchProducts = unstable_cache(
hasMore: false,
}

const { hits, estimatedTotalHits } = await meilisearch.searchDocuments<CommerceProduct>({
const { hits, estimatedTotalHits } = await searchClient.searchDocuments<CommerceProduct>({
indexName: env.MEILISEARCH_PRODUCTS_INDEX,
query,
options: {
Expand Down
30 changes: 7 additions & 23 deletions starters/shopify-meilisearch/app/api/feed/sync/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { PlatformProduct } from "lib/shopify/types"
import { meilisearch } from "clients/search"
import { storefrontClient } from "clients/storefrontClient"
import { env } from "env.mjs"
import { compareHmac } from "utils/compare-hmac"
import { enrichProduct } from "utils/enrich-product"
import { deleteCategories, deleteProducts, updateCategories, updateProducts } from "lib/meilisearch"

type SupportedTopic = "products/update" | "products/delete" | "products/create" | "collections/update" | "collections/delete" | "collections/create"

Expand Down Expand Up @@ -54,21 +54,13 @@ async function handleCollectionTopics(topic: SupportedTopic, { id }: Record<stri
console.error(`Collection ${id} not found`)
return new Response(JSON.stringify({ message: "Collection not found" }), { status: 404, headers: { "Content-Type": "application/json" } })
}
await meilisearch.updateDocuments({
indexName: env.MEILISEARCH_CATEGORIES_INDEX,
documents: [{ ...collection, id: `${id}` }],
options: {
primaryKey: "id",
},
})

await updateCategories([{ ...collection, id: `${id}` }])

break

case "collections/delete":
await meilisearch.deleteDocuments({
indexName: env.MEILISEARCH_CATEGORIES_INDEX,
params: [id],
})
await deleteCategories([id])
break

default:
Expand All @@ -91,20 +83,12 @@ async function handleProductTopics(topic: SupportedTopic, { id }: Record<string,
}

const enrichedProduct = await enrichProduct(product, items)
await meilisearch.updateDocuments({
indexName: env.MEILISEARCH_PRODUCTS_INDEX,
documents: [normalizeProduct(enrichedProduct, id)],
options: {
primaryKey: "id",
},
})

await updateProducts([normalizeProduct(enrichedProduct, id)])

break
case "products/delete":
await meilisearch.deleteDocuments({
indexName: env.MEILISEARCH_PRODUCTS_INDEX,
params: [id],
})
await deleteProducts([id])
break

default:
Expand Down
27 changes: 9 additions & 18 deletions starters/shopify-meilisearch/app/api/reviews/ai-summary/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import { generateObject } from "ai"
import z from "zod"
import { openai } from "@ai-sdk/openai"
import type { Review } from "lib/reviews/types"
import type { CommerceProduct } from "types"
import { meilisearch } from "clients/search"
import { env } from "env.mjs"
import { authenticate } from "utils/authenticate-api-route"
import { isOptIn, notifyOptIn } from "utils/opt-in"
import { unstable_noStore } from "next/cache"
import { isDemoMode } from "utils/demo-utils"
import { getAllProducts, getAllReviews, updateProducts } from "lib/meilisearch"

const summarySchema = z.object({
products: z.array(
Expand Down Expand Up @@ -46,25 +45,17 @@ export async function GET(req: Request) {
return new Response(JSON.stringify({ message: "Sorry, something went wrong" }), { status: 500 })
}

const [allReviews, allProducts] = await Promise.all([
meilisearch.getDocuments<Review>({
indexName: env.MEILISEARCH_REVIEWS_INDEX,
options: {
limit: 10000,
fields: ["body", "title", "product_handle", "rating"],
filter: "published=true AND hidden=false",
},
const [{ reviews }, allProducts] = await Promise.all([
getAllReviews({
fields: ["body", "title", "product_handle", "rating"],
filter: "published=true AND hidden=false",
}),
meilisearch.getDocuments<CommerceProduct>({
indexName: env.MEILISEARCH_PRODUCTS_INDEX,
options: {
limit: 10000,
fields: ["handle", "title", "id", "totalReviews"],
},
getAllProducts({
fields: ["handle", "title", "id", "totalReviews"],
}),
])

const mappedReviews: Record<string, Review[]> = allReviews?.results.reduce(
const mappedReviews: Record<string, Review[]> = reviews.reduce(
(acc, review) => {
const productHandle = review.product_handle
if (acc[productHandle]) {
Expand Down Expand Up @@ -126,7 +117,7 @@ export async function GET(req: Request) {
})
.filter(Boolean)

await meilisearch.updateDocuments<CommerceProduct>({ indexName: env.MEILISEARCH_PRODUCTS_INDEX, documents: updatedProducts, options: { primaryKey: "id" } })
await updateProducts(updatedProducts)

return new Response(JSON.stringify({ message: "Reviews synced" }), { status: 200 })
}
Expand Down
Loading

0 comments on commit 9ce23bb

Please sign in to comment.