diff --git a/src/app/product/[productId]/review/[reviewId]/page.tsx b/src/app/product/[productId]/review/[reviewId]/page.tsx
index 329a00c..d15748b 100644
--- a/src/app/product/[productId]/review/[reviewId]/page.tsx
+++ b/src/app/product/[productId]/review/[reviewId]/page.tsx
@@ -44,6 +44,7 @@ export default async function Page(props: PageProps) {
});
const customerReviews = reviews.filter((r) => r.email === review.email);
+ const customerReviewIdStrings = customerReviews.map((r) => `${r.id}`);
let sentimentAnalysis = await db.getReviewAnalysis({
productId,
@@ -70,6 +71,20 @@ export default async function Page(props: PageProps) {
}
}
+ const allAnalyses = await db.getAllReviewAnalyses({
+ storeHash: authorized.storeHash,
+ });
+
+ const allUserAnalyses =
+ allAnalyses?.filter((analysis) =>
+ customerReviewIdStrings.includes(analysis.reviewId)
+ ) ?? [];
+
+ const userAverageScore = allUserAnalyses.length
+ ? allUserAnalyses.reduce((acc, analysis) => acc + analysis.data.score, 0) /
+ allUserAnalyses.length
+ : undefined;
+
return (
);
}
diff --git a/src/app/productDescription/[productId]/form.tsx b/src/app/productDescription/[productId]/form.tsx
deleted file mode 100644
index 98d8658..0000000
--- a/src/app/productDescription/[productId]/form.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-'use client';
-
-import { usePromptAttributes } from '~/context/PromptAttributesContext';
-import { useDescriptionsHistory } from '~/hooks';
-import { useState } from 'react';
-import { type NewProduct, type Product } from 'types';
-import styled from 'styled-components';
-import { Box, Button, Flex, FlexItem } from '@bigcommerce/big-design';
-import AiResults from '~/components/AiResults/AiResults';
-import { CustomPromptForm } from '~/components/PromptForm/CustomPromptForm';
-import { GuidedPromptForm } from '~/components/PromptForm/GuidedPromptForm';
-import { StyledButton } from '~/components/PromptForm/styled';
-import { prepareAiPromptAttributes } from '~/utils/utils';
-import Loader from '~/components/Loader';
-
-const Hr = styled(Flex)`
- margin-left: -${({ theme }) => theme.spacing.xLarge};
- margin-right: -${({ theme }) => theme.spacing.xLarge};
-`;
-
-export default function Form({ product }: { product: Product | NewProduct }) {
- const { results, setResults, handleDescriptionChange } =
- useDescriptionsHistory(product.id);
- const [isPrompting, setIsPrompting] = useState(false);
- const [description, setDescription] = useState(
- results.at(0)?.description || ''
- );
-
- const {
- isFormGuided,
- setIsFormGuided,
- currentAttributes,
- guidedAttributes,
- customAttributes,
- setGuidedAttributes,
- setCustomAttributes,
- } = usePromptAttributes();
-
- const handleGenerateDescription = async () => {
- setIsPrompting(true);
- const res = await fetch('/api/generateDescription', {
- method: 'POST',
- body: JSON.stringify(
- prepareAiPromptAttributes(currentAttributes, product)
- ),
- });
-
- if (!res.ok) {
- setIsPrompting(false);
- throw new Error('Cannot generate description, try again later');
- }
-
- const { description } = (await res.json()) as { description: string };
- setResults({ promptAttributes: currentAttributes, description });
- setIsPrompting(false);
- };
-
- const descriptionChangeWrapper = (index: number, description: string) => {
- setDescription(description);
- handleDescriptionChange(index, description);
- };
-
- const handleCancelClick = () =>
- window.top?.postMessage(
- JSON.stringify({ namespace: 'APP_EXT', action: 'CLOSE' }),
- '*'
- );
- const handleUseThisClick = () =>
- window.top?.postMessage(
- JSON.stringify({
- namespace: 'APP_EXT',
- action: 'PRODUCT_DESCRIPTION',
- data: { description },
- }),
- '*'
- );
-
- return (
-
-
-
- setIsFormGuided(true)}
- >
- Guided
-
- setIsFormGuided(false)}
- >
- Custom
-
-
- {isFormGuided ? (
-
- ) : (
-
- )}
-
-
-
-
- {isPrompting && }
- {!isPrompting && (
- <>
-
-
-
-
-
-
- >
- )}
-
- );
-}
diff --git a/src/app/productDescription/[productId]/generator.tsx b/src/app/productDescription/[productId]/generator.tsx
deleted file mode 100644
index 4f1a12f..0000000
--- a/src/app/productDescription/[productId]/generator.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-'use client';
-
-import { PromptAttributesProvider } from '~/context/PromptAttributesContext';
-import { useDescriptionsHistory } from '~/hooks';
-import { useEffect, useState } from 'react';
-import { type NewProduct, type Product } from 'types';
-import { DEFAULT_GUIDED_ATTRIBUTES } from '~/constants';
-import Form from './form';
-
-export default function Generator({
- product,
- initialDescription,
-}: {
- product: Product | NewProduct;
- initialDescription: string;
-}) {
- const [isInitialLoad, setInitialLoad] = useState(false);
- const { setResults } = useDescriptionsHistory(product.id);
-
- useEffect(() => {
- setResults({
- description: initialDescription,
- promptAttributes: DEFAULT_GUIDED_ATTRIBUTES,
- });
- setInitialLoad(true);
- }, [initialDescription, setResults]);
-
- return (
-
- {isInitialLoad && }
-
- );
-}
diff --git a/src/app/productDescription/[productId]/loading.tsx b/src/app/productDescription/[productId]/loading.tsx
deleted file mode 100644
index e99db1f..0000000
--- a/src/app/productDescription/[productId]/loading.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-'use client';
-
-import Loader from '~/components/Loader';
-
-export default function Loading() {
- return ;
-}
diff --git a/src/app/productDescription/[productId]/page.tsx b/src/app/productDescription/[productId]/page.tsx
deleted file mode 100644
index dc5a166..0000000
--- a/src/app/productDescription/[productId]/page.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { Suspense } from 'react';
-import Loader from '~/components/Loader';
-import { fetchProductWithAttributes } from '~/server/bigcommerce-api';
-import generateDescription from '~/server/google-ai';
-import { authorize } from '~/lib/authorize';
-import * as db from '~/lib/db';
-import Generator from './generator';
-
-interface PageProps {
- params: { productId: string };
- searchParams: { product_name: string };
-}
-
-export default async function Page(props: PageProps) {
- const { productId } = props.params;
- const { product_name: name } = props.searchParams;
-
- const authorized = authorize();
-
- if (!authorized) {
- throw new Error('Token is not valid. Try to re-open the app.');
- }
-
- const accessToken = await db.getStoreToken(authorized.storeHash);
-
- if (!accessToken) {
- throw new Error('Access token not found. Try to re-install the app.');
- }
-
- const id = Number(productId);
-
- // cover case when product is not created yet
- const product =
- id === 0
- ? { id, name: name || '' }
- : await fetchProductWithAttributes(id, accessToken, authorized.storeHash);
-
- const description = await generateDescription({ product });
-
- return (
- }>
-
-
- );
-}
diff --git a/src/app/products/page.tsx b/src/app/products/page.tsx
index b60bc6d..7dff962 100644
--- a/src/app/products/page.tsx
+++ b/src/app/products/page.tsx
@@ -18,7 +18,10 @@ export default async function Page() {
throw new Error('Access token not found. Try to re-install the app.');
}
- const products = await fetchAllProducts(accessToken, authorized.storeHash);
+ const [products, allReviews] = await Promise.all([
+ fetchAllProducts(accessToken, authorized.storeHash),
+ db.getAllReviewAnalyses({ storeHash: authorized.storeHash }),
+ ]);
- return ;
+ return ;
}
diff --git a/src/components/ProductList.tsx b/src/components/ProductList.tsx
index dc9f717..260dbbe 100644
--- a/src/components/ProductList.tsx
+++ b/src/components/ProductList.tsx
@@ -12,9 +12,12 @@ import { NextLink } from '~/components/NextLink';
import { ProductFilters } from '~/components/ProductFilters';
import { StarRating } from '~/components/StarRating';
+import { ScoreCircle } from '~/components/ScoreCircle';
+import { type AllReviewAnalysesResponse } from '~/lib/db';
import { convertToUDS } from '~/utils/utils';
interface ProductListProps {
+ allAnalyses: AllReviewAnalysesResponse;
products: SimpleProduct[];
}
@@ -38,7 +41,7 @@ const ReviewsAverage = ({ product }: ReviewsAverageProps) => {
return ;
};
-const ProductList = ({ products }: ProductListProps) => {
+const ProductList = ({ allAnalyses, products }: ProductListProps) => {
const [filteredProducts, setFilteredProducts] = useState(products);
const sortedProductsByReviews = useMemo(
() =>
@@ -48,11 +51,35 @@ const ProductList = ({ products }: ProductListProps) => {
[filteredProducts]
);
+ const productScoresById = products?.reduce(
+ (acc, { id: productId }) => {
+ const reviewAnalyses =
+ allAnalyses?.filter((review) => review.productId === `${productId}`) ??
+ [];
+
+ const productScore = reviewAnalyses?.length
+ ? Math.floor(
+ reviewAnalyses.reduce(
+ (acc, analysis) => acc + analysis.data.score,
+ 0
+ ) / reviewAnalyses.length
+ )
+ : undefined;
+
+ return {
+ ...acc,
+ [productId]: productScore,
+ };
+ },
+ {} as Record
+ );
+
return (
All Products
+
{
hash: 'name',
render: (product) => product.name,
},
+ {
+ header: 'Score',
+ hash: 'score',
+ render: (product) => (
+
+ ),
+ },
{
header: 'Price',
hash: 'price',
diff --git a/src/components/ReviewDetail.tsx b/src/components/ReviewDetail.tsx
index c037079..a3851ac 100644
--- a/src/components/ReviewDetail.tsx
+++ b/src/components/ReviewDetail.tsx
@@ -32,6 +32,7 @@ interface ReviewDetailProps {
product: Product;
review: Review;
sentimentAnalysis?: AnalyzeReviewOutputValues;
+ userAverageScore?: number;
}
export const ReviewDetail = ({
@@ -40,6 +41,7 @@ export const ReviewDetail = ({
product,
review: reviewProp,
sentimentAnalysis,
+ userAverageScore,
}: ReviewDetailProps) => {
const [review, setReview] = useState(reviewProp);
@@ -125,7 +127,7 @@ export const ReviewDetail = ({
}
topRightContent={
}
diff --git a/src/lib/appExtensions.ts b/src/lib/appExtensions.ts
index abff370..df0bd64 100644
--- a/src/lib/appExtensions.ts
+++ b/src/lib/appExtensions.ts
@@ -37,27 +37,6 @@ export const createAppExtension = async ({
if (errors && errors.length > 0) {
throw new Error(errors[0]?.message);
}
-
- // Create the review app extension
-
- const response2 = await fetch(
- `${BIGCOMMERCE_API_URL}/stores/${storeHash}/graphql`,
- {
- method: 'POST',
- headers: {
- accept: 'application/json',
- 'content-type': 'application/json',
- 'x-auth-token': accessToken,
- },
- body: JSON.stringify(createReviewsAppExtensionMutation()),
- }
- );
-
- const { errors: errors2 } = await response2.json();
-
- if (errors2 && errors2.length > 0) {
- throw new Error(errors2[0]?.message);
- }
};
export const getAppExtensions = async ({
@@ -102,45 +81,6 @@ const getAppExtensionsQuery = () => ({
});
const createAppExtensionMutation = () => ({
- query: `
- mutation AppExtension($input: CreateAppExtensionInput!) {
- appExtension {
- createAppExtension(input: $input) {
- appExtension {
- id
- context
- model
- url
- label {
- defaultValue
- locales {
- value
- localeCode
- }
- }
- }
- }
- }
- }`,
- variables: {
- input: {
- context: 'PANEL',
- model: 'PRODUCT_DESCRIPTION',
- url: '/productDescription/${id}',
- label: {
- defaultValue: 'Generate text',
- locales: [
- {
- value: 'Generate text',
- localeCode: 'en-US',
- },
- ],
- },
- },
- },
-});
-
-const createReviewsAppExtensionMutation = () => ({
query: `
mutation AppExtension($input: CreateAppExtensionInput!) {
appExtension {
diff --git a/src/lib/db.ts b/src/lib/db.ts
index 84fd0ab..4ecddb6 100644
--- a/src/lib/db.ts
+++ b/src/lib/db.ts
@@ -7,7 +7,6 @@ import {
getDocs,
getFirestore,
setDoc,
- updateDoc,
} from 'firebase/firestore';
import { z } from 'zod';
import { env } from '~/env.mjs';
@@ -161,25 +160,31 @@ export async function setReviewAnalysis({
}) {
if (!storeHash) return null;
- const ref = doc(
+ const productRef = doc(
db,
'reviewAnalysis',
storeHash,
'products',
- `${productId}`,
- 'reviews',
- `${reviewId}`
+ `${productId}`
);
- await setDoc(ref, analysis);
+ const reviewRef = doc(productRef, 'reviews', `${reviewId}`);
+
+ await Promise.all([
+ // Empty product document so we can reference it as a collection in queries
+ setDoc(productRef, {}),
+
+ // @todo: include customerId in firestore
+ setDoc(reviewRef, analysis),
+ ]);
}
-const reviewAnalysesListSchema = z.array(
+const reviewAnalysesByProductIdSchema = z.array(
z.object({ id: z.string(), data: analyzeReviewOutputSchema })
);
export type ReviewAnalysesByProductIdResponse = Zod.infer<
- typeof reviewAnalysesListSchema
+ typeof reviewAnalysesByProductIdSchema
>;
export async function getReviewAnalysesByProductId({
@@ -202,7 +207,7 @@ export async function getReviewAnalysesByProductId({
const snapshot = await getDocs(ref);
- const parsedAnalyses = reviewAnalysesListSchema.safeParse(
+ const parsedAnalyses = reviewAnalysesByProductIdSchema.safeParse(
snapshot.docs.map((doc) => ({ id: doc.id, data: doc.data() }))
);
@@ -212,3 +217,44 @@ export async function getReviewAnalysesByProductId({
return parsedAnalyses.data;
}
+
+const allReviewAnalysesSchema = z.array(
+ z.object({
+ productId: z.string(),
+ reviewId: z.string(),
+ data: analyzeReviewOutputSchema,
+ })
+);
+
+export type AllReviewAnalysesResponse = Zod.infer<
+ typeof allReviewAnalysesSchema
+>;
+
+export async function getAllReviewAnalyses({
+ storeHash,
+}: {
+ storeHash: string;
+}): Promise {
+ if (!storeHash) return null;
+
+ const reviews: AllReviewAnalysesResponse = [];
+
+ const productsRef = collection(db, 'reviewAnalysis', storeHash, 'products');
+ const productsSnapshot = await getDocs(productsRef);
+
+ for (const product of productsSnapshot.docs) {
+ const reviewsRef = collection(product.ref, 'reviews');
+
+ const reviewsSnapshot = await getDocs(reviewsRef);
+
+ for (const review of reviewsSnapshot.docs) {
+ reviews.push({
+ data: review.data() as AllReviewAnalysesResponse[number]['data'],
+ productId: product.id,
+ reviewId: review.id,
+ });
+ }
+ }
+
+ return reviews;
+}