diff --git a/.eslintignore b/.eslintignore index 356908a..e7066b0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ package.json package-lock.json -tsconfig.json \ No newline at end of file +tsconfig.json +next-env.d.ts \ No newline at end of file diff --git a/pages/api/checkout/ordercalculate.ts b/app/api/checkout/ordercalculate/route.ts similarity index 53% rename from pages/api/checkout/ordercalculate.ts rename to app/api/checkout/ordercalculate/route.ts index 9af2219..45bfec1 100644 --- a/pages/api/checkout/ordercalculate.ts +++ b/app/api/checkout/ordercalculate/route.ts @@ -1,13 +1,7 @@ -import { NextApiHandler } from 'next' -import { OrderCalculateResponse, OrderWorksheet } from 'ordercloud-javascript-sdk' +/* eslint-disable @typescript-eslint/no-explicit-any */ import { withOcWebhookAuth } from '@ordercloud/catalyst' - -// withOCWebhookAuth needs the raw body in order to validate the payload is coming from ordercloud -export const config = { - api: { - bodyParser: false, - }, -}; +import { OrderWorksheet } from 'ordercloud-javascript-sdk' +import { handleAppRouterWebhook } from '../webhookHelper' export type OrderCloudEnvironment = 'Production' | 'Staging' | 'Sandbox' | 'Qa' @@ -18,7 +12,7 @@ export interface OrderCheckoutIntegrationEvent { ConfigData: T } -const OrderCalculateHandler: NextApiHandler = (req, res) => { +const OrderCalculateHandler = (req: any, res: any) => { const event = req.body as OrderCheckoutIntegrationEvent return res.status(200).send({ @@ -26,4 +20,8 @@ const OrderCalculateHandler: NextApiHandler = (req, res) }) } -export default withOcWebhookAuth(OrderCalculateHandler) +const authenticatedHandler = withOcWebhookAuth(OrderCalculateHandler) + +export async function POST(request: Request) { + return handleAppRouterWebhook(request, authenticatedHandler) +} diff --git a/app/api/checkout/shippingrates/route.ts b/app/api/checkout/shippingrates/route.ts new file mode 100644 index 0000000..e165c6c --- /dev/null +++ b/app/api/checkout/shippingrates/route.ts @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { withOcWebhookAuth } from '@ordercloud/catalyst' +import { OrderCheckoutIntegrationEvent } from '../ordercalculate/route' +import { handleAppRouterWebhook } from '../webhookHelper' + +const ShippingRatesHandler = (req: any, res: any) => { + const event = req.body as OrderCheckoutIntegrationEvent + + return res.status(200).send({ + ShipEstimates: [ + { + ID: 'test', + ShipEstimateItems: event.OrderWorksheet.LineItems.map((li) => ({ + LineItemID: li.ID, + Quantity: li.Quantity, + })), + ShipMethods: [ + { + ID: '1day', + Name: 'Next Day Shipping', + Cost: 50, + EstimatedTransitDays: 1, + }, + { + ID: '2day', + Name: 'Two Day Shipping', + Cost: 25, + EstimatedTransitDays: 2, + }, + { + ID: 'standard', + Name: 'Standard Shipping', + Cost: 12, + EstimatedTransitDays: 5, + }, + ], + }, + ], + }) +} + +const authenticatedHandler = withOcWebhookAuth(ShippingRatesHandler) + +export async function POST(request: Request) { + return handleAppRouterWebhook(request, authenticatedHandler) +} diff --git a/app/api/checkout/types.ts b/app/api/checkout/types.ts new file mode 100644 index 0000000..7a7c83b --- /dev/null +++ b/app/api/checkout/types.ts @@ -0,0 +1,10 @@ +import { OrderWorksheet } from 'ordercloud-javascript-sdk' + +export type OrderCloudEnvironment = 'Production' | 'Staging' | 'Sandbox' | 'Qa' + +export interface OrderCheckoutIntegrationEvent { + OrderWorksheet: OrderWorksheet + Environment: OrderCloudEnvironment + OrderCloudAccessToken: string + ConfigData: T +} diff --git a/app/api/checkout/webhookHelper.ts b/app/api/checkout/webhookHelper.ts new file mode 100644 index 0000000..be459bf --- /dev/null +++ b/app/api/checkout/webhookHelper.ts @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { NextResponse } from 'next/server' + +export async function handleAppRouterWebhook( + request: Request, + handler: any +): Promise { + try { + const rawBody = await request.text() + + const req: any = { + headers: {}, + rawBody, + body: rawBody ? JSON.parse(rawBody) : {}, + method: request.method, + url: request.url, + } + + request.headers.forEach((value, key) => { + req.headers[key.toLowerCase()] = value + }) + + let status = 200 + let sentData: any = null + + const res: any = { + status: (code: number) => { + status = code + return res + }, + send: (data: any) => { + sentData = data + return res + }, + json: (data: any) => { + sentData = data + return res + } + } + + await new Promise((resolve, reject) => { + handler(req, res, (err?: any) => { + if (err) { + reject(err) + } else { + resolve() + } + }).catch((err: any) => { + reject(err) + }) + }) + + return NextResponse.json(sentData, { status }) + } catch (error: any) { + console.error('Webhook error:', error) + const statusCode = error.status || 500 + const message = error.message || 'Internal Server Error' + return NextResponse.json( + { Errors: [{ ErrorCode: error.name || 'Unauthorized', Message: message }] }, + { status: statusCode } + ) + } +} diff --git a/pages/cart.tsx b/app/cart/page.tsx similarity index 59% rename from pages/cart.tsx rename to app/cart/page.tsx index 5fab81c..473e81e 100644 --- a/pages/cart.tsx +++ b/app/cart/page.tsx @@ -1,8 +1,10 @@ +'use client' + import Link from 'next/link' import { FunctionComponent } from 'react' -import OcLineItemList from '../ordercloud/components/OcLineItemList' -import { deleteCurrentOrder } from '../ordercloud/redux/ocCurrentOrder' -import { useOcDispatch } from '../ordercloud/redux/ocStore' +import OcLineItemList from '../../ordercloud/components/OcLineItemList' +import { deleteCurrentOrder } from '../../ordercloud/redux/ocCurrentOrder' +import { useOcDispatch } from '../../ordercloud/redux/ocStore' const CartPage: FunctionComponent = () => { const dispatch = useOcDispatch() @@ -13,9 +15,7 @@ const CartPage: FunctionComponent = () => { Clear Cart - - Checkout - + Checkout ) } diff --git a/pages/checkout.tsx b/app/checkout/page.tsx similarity index 66% rename from pages/checkout.tsx rename to app/checkout/page.tsx index b2c99fd..f224140 100644 --- a/pages/checkout.tsx +++ b/app/checkout/page.tsx @@ -1,9 +1,11 @@ -import { useRouter } from 'next/router' +'use client' + +import { useRouter } from 'next/navigation' import { FunctionComponent, useEffect } from 'react' -import OcCheckout from '../ordercloud/components/OcCheckout' -import OcCheckoutSummary from '../ordercloud/components/OcCheckout/OcCheckoutSummary' -import OcLineItemList from '../ordercloud/components/OcLineItemList' -import { useOcSelector } from '../ordercloud/redux/ocStore' +import OcCheckout from '../../ordercloud/components/OcCheckout' +import OcCheckoutSummary from '../../ordercloud/components/OcCheckout/OcCheckoutSummary' +import OcLineItemList from '../../ordercloud/components/OcLineItemList' +import { useOcSelector } from '../../ordercloud/redux/ocStore' const CheckoutPage: FunctionComponent = () => { const { push } = useRouter() diff --git a/app/confirmation/[orderid]/page.tsx b/app/confirmation/[orderid]/page.tsx new file mode 100644 index 0000000..2932446 --- /dev/null +++ b/app/confirmation/[orderid]/page.tsx @@ -0,0 +1,18 @@ +'use client' + +import { useParams } from 'next/navigation' +import { FunctionComponent, useEffect } from 'react' +import OcOrderConfirmation from '../../../ordercloud/components/OcOrderConfirmation' + +const OrderConfirmationPage: FunctionComponent = () => { + const params = useParams() + const orderId = params.orderid as string + + useEffect(() => { + document.title = 'Order Confirmed' + }, []) + + return +} + +export default OrderConfirmationPage diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..e7c184a --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,43 @@ +import type { Metadata } from 'next' +import '../styles/globals.css' +import OcProvider from '../ordercloud/redux/ocProvider' +import Layout from '../components/Layout' +import { ApiRole } from 'ordercloud-javascript-sdk' + +const clientId = process.env.NEXT_PUBLIC_OC_CLIENT_ID || '' +const scope = process.env.NEXT_PUBLIC_OC_SCOPE + ? (process.env.NEXT_PUBLIC_OC_SCOPE.split(',') as ApiRole[]) + : [] +const baseApiUrl = process.env.NEXT_PUBLIC_OC_BASE_API_URL +const allowAnonymous = Boolean(process.env.NEXT_PUBLIC_OC_ALLOW_ANONYMOUS) + +const ocConfig = { + clientId, + scope, + baseApiUrl, + allowAnonymous, + cookieOptions: { + prefix: 'hds-nextjs', + path: '/', + }, +} + +export const metadata: Metadata = { + title: 'Headstart Next.js', + description: 'OrderCloud Headstart Next.js Storefront', + icons: { + icon: '/favicon.ico', + }, +} + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ) +} diff --git a/pages/login.tsx b/app/login/page.tsx similarity index 70% rename from pages/login.tsx rename to app/login/page.tsx index 319dcab..c122bc4 100644 --- a/pages/login.tsx +++ b/app/login/page.tsx @@ -1,6 +1,8 @@ -import { useRouter } from 'next/router' +'use client' + +import { useRouter } from 'next/navigation' import { FunctionComponent } from 'react' -import OcLoginForm from '../ordercloud/components/OcLoginForm' +import OcLoginForm from '../../ordercloud/components/OcLoginForm' const LoginPage: FunctionComponent = () => { const { push } = useRouter() diff --git a/pages/index.tsx b/app/page.tsx similarity index 97% rename from pages/index.tsx rename to app/page.tsx index a00f8b9..b23132c 100644 --- a/pages/index.tsx +++ b/app/page.tsx @@ -1,3 +1,5 @@ +'use client' + import { FunctionComponent } from 'react' import { useOcSelector } from '../ordercloud/redux/ocStore' import styles from '../styles/Home.module.css' diff --git a/app/products/[productid]/page.tsx b/app/products/[productid]/page.tsx new file mode 100644 index 0000000..a9da009 --- /dev/null +++ b/app/products/[productid]/page.tsx @@ -0,0 +1,47 @@ +'use client' + +import { useRouter, useParams, useSearchParams } from 'next/navigation' +import { FunctionComponent, useEffect, Suspense } from 'react' +import OcProductDetail from '../../../ordercloud/components/OcProductDetail' +import { useOcSelector } from '../../../ordercloud/redux/ocStore' + +const ProductDetailContent: FunctionComponent = () => { + const { push } = useRouter() + const params = useParams() + const searchParams = useSearchParams() + + const productId = params.productid as string + const lineItemId = searchParams.get('lineitem') as string + + const productName = useOcSelector( + (s) => s.ocProductDetail.product && s.ocProductDetail.product.Name + ) + + useEffect(() => { + if (productName) { + document.title = productName + } + }, [productName]) + + const handleLineItemUpdated = () => { + push('/cart') + } + + return ( + + ) +} + +const ProductPage: FunctionComponent = () => { + return ( + Loading product details...}> + + + ) +} + +export default ProductPage diff --git a/pages/products/index.tsx b/app/products/page.tsx similarity index 65% rename from pages/products/index.tsx rename to app/products/page.tsx index b7ff3cb..103558d 100644 --- a/pages/products/index.tsx +++ b/app/products/page.tsx @@ -1,6 +1,8 @@ +'use client' + import Link from 'next/link' import { BuyerProduct, Filters } from 'ordercloud-javascript-sdk' -import { FunctionComponent, useCallback } from 'react' +import { FunctionComponent, useCallback, Suspense } from 'react' import OcProductCard from '../../ordercloud/components/OcProductCard' import OcProductFacetForm from '../../ordercloud/components/OcProductFacetsForm' import OcProductList from '../../ordercloud/components/OcProductList' @@ -18,8 +20,8 @@ const queryMap: NextQueryMap = { 'xp.test_number': 'num', } -const ProductListPage: FunctionComponent = () => { - const { isReady, options, updateQuery } = useNextRouterMapping(queryMap) +const ProductListContent: FunctionComponent = () => { + const { options, updateQuery } = useNextRouterMapping(queryMap) const handleFacetChange = useCallback( (updatedFilters: Filters) => { @@ -31,22 +33,26 @@ const ProductListPage: FunctionComponent = () => { const handleRenderItem = (p: BuyerProduct) => { return ( - - - + ) } return ( - isReady && ( - <> -

Facets

- -

Products

- - - ) + <> +

Facets

+ +

Products

+ + + ) +} + +const ProductListPage: FunctionComponent = () => { + return ( + Loading products...}> + + ) } diff --git a/components/Layout.tsx b/components/Layout.tsx index 765eff8..eae8d7b 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -1,10 +1,11 @@ -import Head from 'next/head' +'use client' + import Link from 'next/link' import { FunctionComponent } from 'react' import logout from '../ordercloud/redux/ocAuth/logout' import { useOcDispatch, useOcSelector } from '../ordercloud/redux/ocStore' -const Layout: FunctionComponent = ({ children }) => { +const Layout: FunctionComponent<{ children: React.ReactNode }> = ({ children }) => { const dispatch = useOcDispatch() const { user, isAnonymous, loading, lineItemCount } = useOcSelector((s) => ({ @@ -16,27 +17,15 @@ const Layout: FunctionComponent = ({ children }) => { return ( <> - - React Headstart - -

React Headstart

{`Cart Count ${lineItemCount}`}