Skip to content

Commit

Permalink
feat: api routes + gtm
Browse files Browse the repository at this point in the history
  • Loading branch information
gatteo committed Mar 13, 2024
1 parent 70a8472 commit 64241da
Show file tree
Hide file tree
Showing 20 changed files with 608 additions and 66 deletions.
3 changes: 1 addition & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ NEXT_PUBLIC_PAYPAL_CLIENT_ID = "replace me"
NEXT_PUBLIC_STRIPE_TCL_LINK = "replace me"
NEXT_PUBLIC_STRIPE_PL_LINK = "replace me"

SLACK_TOKEN = "replace me"
SLACK_PAYMENTS_CHANNEL = "replace me"
SLACK_PAYMENTS_WEBHOOK_URL = "replace me"

# Spotify Now Playing
# https://developer.spotify.com/documentation/web-api/reference/get-the-users-currently-playing-track
Expand Down
40 changes: 40 additions & 0 deletions app/api/paypal/orders/[orderId]/capture/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { NextRequest } from 'next/server'
import { z } from 'zod'

import { APIResponse } from '@/lib/api'
import { capturePayPalPayment } from '@/lib/paypal'
import { notifyOfPayPalPurchase } from '@/lib/slack'

const CapturePaymentSchema = z.object({
orderId: z.string(),
})

export const POST = async (req: NextRequest, { params }: { params: { orderId: string } }) => {
try {
const safeParams = CapturePaymentSchema.parse(params)

const order = await capturePayPalPayment(safeParams.orderId)

console.log('Captured PayPal Payment', order)

// send email to customer

// send slack message to admin
try {
await notifyOfPayPalPurchase(
order.payer.email_address,
order.purchase_units[0].payments.captures[0].seller_receivable_breakdown.net_amount.value,
)
} catch (e) {}

return APIResponse.success(order, 201)
} catch (error: any) {
console.error('[API] Error capturing PayPal payment', error)

if (error instanceof z.ZodError) {
return APIResponse.error('Invalid data', 422, error.errors)
}

return APIResponse.error(error.message)
}
}
31 changes: 31 additions & 0 deletions app/api/paypal/orders/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NextRequest } from 'next/server'
import { z } from 'zod'

import { ProductPrices, ProductSKU } from '@/config/products'
import { APIResponse } from '@/lib/api'
import { createPayPalOrder } from '@/lib/paypal'

const CreateOrderSchema = z.object({
sku: z.nativeEnum(ProductSKU),
})

export const POST = async (req: NextRequest) => {
try {
const json = await req.json()
const body = CreateOrderSchema.parse(json)

const order = await createPayPalOrder(ProductPrices[body.sku])

console.log('Created PayPal order', order)

return APIResponse.success(order, 201)
} catch (error: any) {
console.error('[API] Error creating PayPal order', error)

if (error instanceof z.ZodError) {
return APIResponse.error('Invalid data', 422, error.errors)
}

return APIResponse.error(error.message)
}
}
47 changes: 47 additions & 0 deletions app/api/spotify/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { z } from 'zod'

import { APIResponse } from '@/lib/api'
import { getNowPlaying } from '@/lib/spotify'

export const runtime = 'edge'
export const dynamic = 'force-dynamic'

export const GET = async () => {
try {
const response = await getNowPlaying()

if (response.status === 204 || response.status > 400 || response?.data?.item === null || !response.data) {
return APIResponse.success({ isPlaying: false })
}

const song = response.data

if (song.is_playing === false) {
return APIResponse.success({ isPlaying: false })
}

const isPlaying = song.is_playing
const name = song.item.name
const artist = song.item.artists.map((_artist) => _artist.name).join(', ')
const album = song.item.album.name
const albumImage = song.item.album.images[0].url
const songUrl = song.item.external_urls.spotify

return APIResponse.success({
isPlaying,
name,
artist,
album,
albumImage,
songUrl,
})
} catch (error: any) {
console.error('[API] Error creating PayPal order', error)

if (error instanceof z.ZodError) {
return APIResponse.error('Invalid data', 422, error.errors)
}

return APIResponse.error(error.message)
}
}
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Toaster } from 'sonner'

import { site, siteBaseMetadata } from '@/config/site'
import { cn } from '@/lib/utils'
import { GTM } from '@/components/gtm'
import { ProgressProvider } from '@/components/progress-provider'
import { TailwindIndicator } from '@/components/tailwind-indicator'
import { ThemeProvider } from '@/components/theme-provider'
Expand Down Expand Up @@ -74,6 +75,7 @@ export default function RootLayout({
<TailwindIndicator />
</ProgressProvider>
</ThemeProvider>
<GTM />
</body>
</html>
)
Expand Down
16 changes: 0 additions & 16 deletions components/blog/view-counter.tsx

This file was deleted.

15 changes: 3 additions & 12 deletions components/footer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,12 @@ export function Footer() {
<div className='col-span-2 mb-10 flex flex-col items-start gap-4 pr-4 md:hidden'>
<div className='grid w-full grid-cols-2 gap-2 sm:grid-cols-4'>
{products.map((p) => (
<Link key={p.title} href={p.url} target='_blank'>
<Image
src={p.image}
height={80}
width={100}
key={p.title}
alt={p.title}
className='hidden'
/>
<Link key={p.id} href={p.url} target='_blank'>
<Image src={p.image} height={80} width={100} alt={p.title} className='hidden' />
<Image
src={p.imageDark}
height={80}
width={100}
key={p.title}
alt={p.title}
className='hidden dark:hidden'
/>
Expand Down Expand Up @@ -77,13 +69,12 @@ export function Footer() {
</div>
<div className='mb-10 hidden flex-col items-start gap-4 pr-4 md:flex'>
{products.map((p) => (
<Link key={p.title} href={p.url} target='_blank'>
<Link key={p.id} href={p.url} target='_blank'>
<Image src={p.image} height={80} width={100} alt={p.title} className='dark:hidden' />
<Image
src={p.imageDark}
height={80}
width={100}
key={p.title}
alt={p.title}
className='hidden dark:block'
/>
Expand Down
12 changes: 6 additions & 6 deletions components/footer/now-playing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { motion } from 'framer-motion'
import useSWR from 'swr'

import { Song } from '@/types/api'
import { ApiResponseSuccess, Song } from '@/types/api'
import { ApiRoutes } from '@/config/routes'
import { fetcher } from '@/lib/fetcher'

Expand Down Expand Up @@ -48,7 +48,7 @@ function AnimatedBars() {
}

const NowPlaying = () => {
const { data } = useSWR<Song>(ApiRoutes.getSong, fetcher)
const { data: response } = useSWR<ApiResponseSuccess<Song>>(ApiRoutes.GetSong, fetcher)

return (
<div className='flex items-center gap-4'>
Expand All @@ -63,19 +63,19 @@ const NowPlaying = () => {
<path d='M248 8C111.1 8 0 119.1 0 256s111.1 248 248 248 248-111.1 248-248S384.9 8 248 8zm100.7 364.9c-4.2 0-6.8-1.3-10.7-3.6-62.4-37.6-135-39.2-206.7-24.5-3.9 1-9 2.6-11.9 2.6-9.7 0-15.8-7.7-15.8-15.8 0-10.3 6.1-15.2 13.6-16.8 81.9-18.1 165.6-16.5 237 26.2 6.1 3.9 9.7 7.4 9.7 16.5s-7.1 15.4-15.2 15.4zm26.9-65.6c-5.2 0-8.7-2.3-12.3-4.2-62.5-37-155.7-51.9-238.6-29.4-4.8 1.3-7.4 2.6-11.9 2.6-10.7 0-19.4-8.7-19.4-19.4s5.2-17.8 15.5-20.7c27.8-7.8 56.2-13.6 97.8-13.6 64.9 0 127.6 16.1 177 45.5 8.1 4.8 11.3 11 11.3 19.7-.1 10.8-8.5 19.5-19.4 19.5zm31-76.2c-5.2 0-8.4-1.3-12.9-3.9-71.2-42.5-198.5-52.7-280.9-29.7-3.6 1-8.1 2.6-12.9 2.6-13.2 0-23.3-10.3-23.3-23.6 0-13.6 8.4-21.3 17.4-23.9 35.2-10.3 74.6-15.2 117.5-15.2 73 0 149.5 15.2 205.4 47.8 7.8 4.5 12.9 10.7 12.9 22.6 0 13.6-11 23.3-23.2 23.3z'></path>
</svg>

{data?.isPlaying ? <AnimatedBars /> : null}
{response?.data?.isPlaying ? <AnimatedBars /> : null}

<div className='inline-flex w-full items-center justify-center gap-1 text-sm md:justify-start'>
<p>
{data?.isPlaying ? (
{response?.data?.isPlaying ? (
<>
<a
href='https://open.spotify.com/user/1189222906'
target='_blank'
rel='noopener noreferrer'>
<span className='text-muted-foreground'>In questo momento sto ascoltando </span>
{data.name}
<span className='text-muted-foreground'> - {data.artist}</span>
{response.data.name}
<span className='text-muted-foreground'> - {response.data.artist}</span>
</a>
</>
) : (
Expand Down
31 changes: 31 additions & 0 deletions components/gtm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client'

import Script from 'next/script'

import { env } from '@/env.mjs'

export function GTM() {
return (
<>
<noscript>
<iframe
title='google-tag-manager'
src={`https://www.googletagmanager.com/ns.html?id=${env.NEXT_PUBLIC_GTM_MEASUREMENT_ID}`}
height='0'
width='0'
style={{ display: 'none', visibility: 'hidden' }}
/>
</noscript>

<Script id='google-tag-manager' strategy='afterInteractive'>
{`
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','${env.NEXT_PUBLIC_GTM_MEASUREMENT_ID}');
`}
</Script>
</>
)
}
29 changes: 17 additions & 12 deletions components/marketing/paypal-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ import type { FUNDING_SOURCE } from '@paypal/paypal-js'
import { FUNDING, PayPalButtons, PayPalScriptProvider } from '@paypal/react-paypal-js'

import { env } from '@/env.mjs'
import { ApiResponseSuccess, CapturePayPalOrderResponse, CreatePayPalOrderResponse } from '@/types/api'
import { ProductSKU } from '@/config/products'
import { ApiRoutes } from '@/config/routes'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'

const initialOptions = {
'clientId': env.NEXT_PUBLIC_PAYPAL_CLIENT_ID as string,
'clientId': env.NEXT_PUBLIC_PAYPAL_CLIENT_ID,
'enable-funding': 'paylater,venmo',
'currency': 'EUR',
}

type Props = {
productSKU: string
productSKU: ProductSKU
fundingType: FUNDING_SOURCE
}

Expand Down Expand Up @@ -52,40 +55,42 @@ export function PayPalButton({ fundingType, productSKU }: Props) {
color: fundingType === FUNDING.PAYLATER ? 'gold' : 'blue',
height: 36,
}}
createOrder={async (data, actions) => {
createOrder={async (_data, _actions) => {
try {
const response = await fetch('/api/paypal/orders', {
const response = await fetch(ApiRoutes.CreatePayPalOrder, {
method: 'POST',
body: JSON.stringify({
sku: productSKU,
}),
})

const details = await response.json()
const json = (await response.json()) as ApiResponseSuccess<CreatePayPalOrderResponse>

return details.order.id
return json.data.id
} catch (error) {
console.error(error)
showErrorMessage(error as string)
throw error
}
}}
onApprove={async (data, actions) => {
try {
console.log('PayPal Approve', data)
console.log('PayPal onApprove data', data)

const response = await fetch(`/api/paypal/orders/${data.orderID}/capture`, {
const response = await fetch(ApiRoutes.CapturePayPalOrder(data.orderID), {
method: 'POST',
})

const details = (await response.json()).order
const json = (await response.json()) as ApiResponseSuccess<CapturePayPalOrderResponse>

// Three cases to handle:
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
// (2) Other non-recoverable errors -> Show a failure message
// (3) Successful transaction -> Show confirmation or thank you message

// This example reads a v2/checkout/orders capture response, propagated from the server
// You could use a different API or structure for your 'orderData'
const errorDetail = Array.isArray(details.details) && details.details[0]
const errorDetail = Array.isArray(json.data.details) && json.data.details[0]

if (errorDetail && errorDetail.issue === 'INSTRUMENT_DECLINED') {
return actions.restart()
Expand All @@ -95,11 +100,11 @@ export function PayPalButton({ fundingType, productSKU }: Props) {
if (errorDetail) {
let msg = ''
msg += errorDetail.description ? ' ' + errorDetail.description : ''
msg += details.debug_id ? ' - ' + details.debug_id + ' ' : ''
msg += json.data.debug_id ? ' - ' + json.data.debug_id + ' ' : ''
showErrorMessage(msg)
}

setDialogTitle(`${details.payer.name.given_name}, il pagamento è andato a buon fine 🎉`)
setDialogTitle(`${json.data.payer.name.given_name}, il pagamento è andato a buon fine 🎉`)
setDialogDescription(
'Grazie del tuo pagamento, ora non ti resta che aspettare di ricevere una mail con le indicazioni su come procedere!',
)
Expand Down
5 changes: 3 additions & 2 deletions components/pl/pricing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import NetflixLogo from 'public/images/brands/netflix.svg'
import WezardLogo from 'public/images/brands/wezard.svg'

import { env } from '@/env.mjs'
import { ProductSKU } from '@/config/products'
import { Button } from '@/components/ui/button'
import { PayPalButton } from '@/components/marketing/paypal-button'

Expand Down Expand Up @@ -94,8 +95,8 @@ export function Pricing() {
<Button asChild className='mb-2 w-full' size='sm'>
<Link href={env.NEXT_PUBLIC_STRIPE_PL_LINK as string}>Paga con carta</Link>
</Button>
<PayPalButton fundingType={'paypal'} productSKU='PL' />
<PayPalButton fundingType={'paylater'} productSKU='PL' />
<PayPalButton fundingType={'paypal'} productSKU={ProductSKU.PL} />
<PayPalButton fundingType={'paylater'} productSKU={ProductSKU.PL} />
</div>

<p className='mt-2 text-xs text-neutral-500 dark:text-neutral-400'>
Expand Down
12 changes: 12 additions & 0 deletions config/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,15 @@ export const products: Array<{
imageDark: '/images/projects/devv/logo-extended-white.webp',
},
]

export enum ProductSKU {
PL = 'programmatore-leggendario',
TCL = 'tech-career-launch',
TCB = 'tech-career-boost',
}

export const ProductPrices = {
[ProductSKU.PL]: '59.00',
[ProductSKU.TCL]: '249.00',
[ProductSKU.TCB]: '449.00',
}
Loading

0 comments on commit 64241da

Please sign in to comment.