Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
package.json
package-lock.json
tsconfig.json
tsconfig.json
next-env.d.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -18,12 +12,16 @@ export interface OrderCheckoutIntegrationEvent<T = null> {
ConfigData: T
}

const OrderCalculateHandler: NextApiHandler<OrderCalculateResponse> = (req, res) => {
const OrderCalculateHandler = (req: any, res: any) => {
const event = req.body as OrderCheckoutIntegrationEvent

return res.status(200).send({
TaxTotal: event.OrderWorksheet.Order.BillingAddress ? 5 : 0,
})
}

export default withOcWebhookAuth(OrderCalculateHandler)
const authenticatedHandler = withOcWebhookAuth(OrderCalculateHandler)

export async function POST(request: Request) {
return handleAppRouterWebhook(request, authenticatedHandler)
}
46 changes: 46 additions & 0 deletions app/api/checkout/shippingrates/route.ts
Original file line number Diff line number Diff line change
@@ -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)
}
10 changes: 10 additions & 0 deletions app/api/checkout/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { OrderWorksheet } from 'ordercloud-javascript-sdk'

export type OrderCloudEnvironment = 'Production' | 'Staging' | 'Sandbox' | 'Qa'

export interface OrderCheckoutIntegrationEvent<T = null> {
OrderWorksheet: OrderWorksheet
Environment: OrderCloudEnvironment
OrderCloudAccessToken: string
ConfigData: T
}
63 changes: 63 additions & 0 deletions app/api/checkout/webhookHelper.ts
Original file line number Diff line number Diff line change
@@ -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<Response> {
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<void>((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 }
)
}
}
12 changes: 6 additions & 6 deletions pages/cart.tsx → app/cart/page.tsx
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -13,9 +15,7 @@ const CartPage: FunctionComponent = () => {
Clear Cart
</button>
<OcLineItemList emptyMessage="Your shopping cart is empty" editable />
<Link href="/checkout">
<a>Checkout</a>
</Link>
<Link href="/checkout">Checkout</Link>
</div>
)
}
Expand Down
12 changes: 7 additions & 5 deletions pages/checkout.tsx → app/checkout/page.tsx
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
18 changes: 18 additions & 0 deletions app/confirmation/[orderid]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <OcOrderConfirmation orderId={orderId} />
}

export default OrderConfirmationPage
43 changes: 43 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<html lang="en">
<body>
<OcProvider config={ocConfig}>
<Layout>{children}</Layout>
</OcProvider>
</body>
</html>
)
}
6 changes: 4 additions & 2 deletions pages/login.tsx → app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
2 changes: 2 additions & 0 deletions pages/index.tsx → app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client'

import { FunctionComponent } from 'react'
import { useOcSelector } from '../ordercloud/redux/ocStore'
import styles from '../styles/Home.module.css'
Expand Down
47 changes: 47 additions & 0 deletions app/products/[productid]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<OcProductDetail
onLineItemUpdated={handleLineItemUpdated}
productId={productId}
lineItemId={lineItemId}
/>
)
}

const ProductPage: FunctionComponent = () => {
return (
<Suspense fallback={<div>Loading product details...</div>}>
<ProductDetailContent />
</Suspense>
)
}

export default ProductPage
34 changes: 20 additions & 14 deletions pages/products/index.tsx → app/products/page.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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) => {
Expand All @@ -31,22 +33,26 @@ const ProductListPage: FunctionComponent = () => {
const handleRenderItem = (p: BuyerProduct) => {
return (
<Link href={`/products/${p.ID}`}>
<a>
<OcProductCard product={p} />
</a>
<OcProductCard product={p} />
</Link>
)
}

return (
isReady && (
<>
<h2>Facets</h2>
<OcProductFacetForm onChange={handleFacetChange} />
<h2>Products</h2>
<OcProductList options={options} renderItem={handleRenderItem} />
</>
)
<>
<h2>Facets</h2>
<OcProductFacetForm onChange={handleFacetChange} />
<h2>Products</h2>
<OcProductList options={options} renderItem={handleRenderItem} />
</>
)
}

const ProductListPage: FunctionComponent = () => {
return (
<Suspense fallback={<div>Loading products...</div>}>
<ProductListContent />
</Suspense>
)
}

Expand Down
Loading