Skip to content

Commit

Permalink
Merge pull request #5 from ildecimo/develop
Browse files Browse the repository at this point in the history
merge dev
  • Loading branch information
maxdyy committed Aug 10, 2023
2 parents 2f9c305 + 55f98fc commit 013907d
Show file tree
Hide file tree
Showing 14 changed files with 868 additions and 17 deletions.
523 changes: 513 additions & 10 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,17 @@
"@types/react-dom": "^18.2.4",
"@typescript-eslint/eslint-plugin": "^5.59.6",
"@typescript-eslint/parser": "^5.59.6",
"autoprefixer": "^10.4.14",
"eslint": "^8.40.0",
"eslint-config-next": "^13.4.2",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^5.0.0",
"husky": "^8.0.3",
"lint-staged": "^13.2.3",
"postcss": "^8.4.27",
"prettier": "^3.0.1",
"request": "^2.88.2",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.4"
},
"eslintIgnore": [
Expand Down
6 changes: 6 additions & 0 deletions postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
3 changes: 2 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Source_Sans_3 } from 'next/font/google';
import { type Metadata } from 'next/types';
import StyledComponentsRegistry from '~/lib/registry';
import ThemeProvider from '../components/ThemeProvider';
import ThemeProvider from '~/components/ThemeProvider';
import '~/styles/main.css';

const sourceSans = Source_Sans_3({
subsets: ['latin'],
Expand Down
42 changes: 38 additions & 4 deletions src/app/productReview/[productId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,48 @@
import { authorize } from '~/lib/authorize';
import * as db from '~/lib/db';

import { fetchProductWithAttributes, fetchReviews } from '~/server/bigcommerce-api';

import ProductReviewList from '~/components/ProductReviewList';

interface PageProps {
params: { productId: string };
searchParams: { product_name: string };
}

export default function Page(props: PageProps) {
console.log('props', props);
export default async function Page(props: PageProps) {
const { productId } = props.params;

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);

const product = await fetchProductWithAttributes(
id,
accessToken,
authorized.storeHash
);

const reviews = await fetchReviews(
id,
accessToken,
authorized.storeHash
);

console.log(reviews);

return (
<div>
<h1>Product Review Page from the APP</h1>
<ProductReviewList productName={product.name} reviews={reviews} />
</div>
);
}
144 changes: 144 additions & 0 deletions src/components/ProductReviewList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
'use client';
import { useMemo } from "react";
import { H1, Table, Badge, Link, Box } from '@bigcommerce/big-design';
import {
StarBorderIcon,
StarHalfIcon,
StarIcon,
} from '@bigcommerce/big-design-icons';
import { type Review } from 'types';
import { convertToDateString } from '~/utils/utils';

interface ProductReviewListProps {
productName: string;
reviews: Review[];
}

interface StatusBadgeProps {
status: 'approved' | 'pending' | 'disapproved';
}

interface ReviewRatingProps {
rating: number;
}

const StatusBadge = ({ status }: StatusBadgeProps) => {
const variant =
status === 'approved'
? 'success'
: status === 'pending'
? 'warning'
: 'danger';
const label = status.charAt(0).toUpperCase() + status.slice(1);

return (
<Badge label={label} variant={variant}>
{status}
</Badge>
);
};

const ReviewRating = ({ rating }: ReviewRatingProps) => {
const stars: React.JSX.Element[] = [];
const fullStars = Math.floor(rating);
const halfStars = Math.ceil(rating % 1);
const emptyStars = 5 - fullStars - halfStars;

for (let i = 0; i < fullStars; i++) {
stars.push(<StarIcon color="warning50" size={'medium'} key={i} />);
}

if (halfStars) {
stars.push(
<StarHalfIcon color="warning50" size={'medium'} key={fullStars} />
);
}

for (let i = 0; i < emptyStars; i++) {
stars.push(
<StarBorderIcon size={'medium'} key={fullStars + halfStars + i} />
);
}

return <div className="flex">{stars}</div>;
};

const ProductReviewList = ({
productName,
reviews,
}: ProductReviewListProps) => {

const averageRating = useMemo(() => {
return reviews.reduce((acc, review) => acc + review.rating, 0) / reviews.length;
}
, [reviews]);

const approvedReviewsCount = useMemo(() => {
return reviews.filter(review => review.status === 'approved').length;
}
, [reviews]);

return (
<div>
<div>
<div className="text-center">
<H1>Reviews for - <strong>{productName}</strong></H1>
</div>
<div className="my-12">
<Box border="box" padding="small" borderRadius="normal">
<div>
<strong>Reviews count:</strong><span className="pl-2">{reviews.length}</span>
</div>
<div>
<strong>Approved:</strong><span className="pl-2">{approvedReviewsCount}</span>
</div>
<div className="flex">
<strong>Average Rating:</strong><span className="pl-2"><ReviewRating rating={averageRating} /></span>
</div>
</Box>
</div>
<Table
columns={[
{
header: 'Rating',
hash: 'rating',
render: ({ rating }) => <ReviewRating rating={rating} />,
},
{
header: 'Status',
hash: 'status',
render: ({ status }) => <StatusBadge status={status} />,
},
{
header: 'Posted by',
hash: 'name',
render: ({ name, email }) => (
<span>
{name}
<br />
<small>{email}</small>
</span>
),
},
{
header: 'Date',
hash: 'date',
render: ({ date_created }) => (
<span>{convertToDateString(date_created)}</span>
),
},
{
header: 'Action',
hash: 'action',
render: ({id}) => <Link href={`/review/${id}`}>AI Explore</Link>,
}
]}
items={reviews}
stickyHeader
/>
</div>
</div>
);
};

export default ProductReviewList;
59 changes: 59 additions & 0 deletions src/server/bigcommerce-api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,37 @@ const brandSchema = z.object({
data: z.object({ name: z.string().optional() }),
});

const reviewSchema = z.object({
data: z.array(
z.object({
title: z.string(),
text: z.string(),
status: z.enum(['approved', 'pending', 'disapproved']),
rating: z.number(),
email: z.string(),
name: z.string(),
date_reviewed: z.string(),
id: z.number(),
date_created: z.string(),
date_modified: z.string(),
})
),
meta: z.object({
pagination: z.object({
total: z.number(),
count: z.number(),
per_page: z.number(),
current_page: z.number(),
total_pages: z.number(),
links: z.object({
previous: z.string().optional(),
current: z.string(),
next: z.string().optional(),
}),
}),
}),
});

const fetchFromBigCommerceApi = (
path: string,
accessToken: string,
Expand Down Expand Up @@ -124,3 +155,31 @@ export async function fetchBrand(

return parsedBrand.data.data.name;
}

export async function fetchProductReviews(
productId: number,
accessToken: string,
storeHash: string
) {
const response = await fetchFromBigCommerceApi(
`/catalog/products/${productId}/reviews`,
accessToken,
storeHash
);

if (!response.ok) {
throw new Error('Failed to fetch reviews');
}

const parsedReviews = reviewSchema.safeParse(await response.json());

console.log(parsedReviews);

if (!parsedReviews.success) {
throw new Error('Failed to parse reviews');
}

console.log(parsedReviews);

return parsedReviews.data.data;
}
19 changes: 17 additions & 2 deletions src/server/bigcommerce-api/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { type Product } from 'types';
import { fetchProduct, fetchCategories, fetchBrand } from './client';
import { type Product, type Review } from 'types';
import {
fetchProduct,
fetchCategories,
fetchBrand,
fetchProductReviews,
} from './client';

export const fetchProductWithAttributes = async (
id: number,
Expand All @@ -23,3 +28,13 @@ export const fetchProductWithAttributes = async (

return { ...product, id, brand, categoriesNames } as Product;
};

export const fetchReviews = async (
id: number,
accessToken: string,
storeHash: string
): Promise<Review[]> => {
const reviews = await fetchProductReviews(id, accessToken, storeHash);

return reviews;
};
3 changes: 3 additions & 0 deletions src/styles/main.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
11 changes: 11 additions & 0 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,14 @@ export const prepareAiPromptAttributes = (
product: includeProductAttributes ? product : null,
};
};

export const convertToDateString = (date: string) => {
const dateObject = new Date(date);

return dateObject.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
});
}
8 changes: 8 additions & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
Loading

0 comments on commit 013907d

Please sign in to comment.