diff --git a/.env.example b/.env.example index 63a9f65..a1a7bed 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/app/api/paypal/orders/[orderId]/capture/route.ts b/app/api/paypal/orders/[orderId]/capture/route.ts new file mode 100644 index 0000000..f46cb84 --- /dev/null +++ b/app/api/paypal/orders/[orderId]/capture/route.ts @@ -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) + } +} diff --git a/app/api/paypal/orders/route.ts b/app/api/paypal/orders/route.ts new file mode 100644 index 0000000..86f8452 --- /dev/null +++ b/app/api/paypal/orders/route.ts @@ -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) + } +} diff --git a/app/api/spotify/route.ts b/app/api/spotify/route.ts new file mode 100644 index 0000000..537f4a3 --- /dev/null +++ b/app/api/spotify/route.ts @@ -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) + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 965a246..928d223 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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' @@ -74,6 +75,7 @@ export default function RootLayout({ + ) diff --git a/components/blog/view-counter.tsx b/components/blog/view-counter.tsx deleted file mode 100644 index 6026c92..0000000 --- a/components/blog/view-counter.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import useSWR from 'swr' - -import { fetcher } from '@/lib/fetcher' - -import { Skeleton } from '../ui/skeleton' - -type Views = { - views: number -} - -const ViewCounter = ({ slug }: { slug: string }) => { - const { data, isLoading } = useSWR(`/api/views?slug=${slug}`, fetcher) - return <>{isLoading ? :
{`๐Ÿ‘€ ${data?.views} visualizzazioni`}
} -} - -export default ViewCounter diff --git a/components/footer/index.tsx b/components/footer/index.tsx index e7ca4ed..5e5aede 100644 --- a/components/footer/index.tsx +++ b/components/footer/index.tsx @@ -28,20 +28,12 @@ export function Footer() {
{products.map((p) => ( - - {p.title} + + {p.title} {p.title} @@ -77,13 +69,12 @@ export function Footer() {
{products.map((p) => ( - + {p.title} {p.title} diff --git a/components/footer/now-playing.tsx b/components/footer/now-playing.tsx index feb412f..9114915 100644 --- a/components/footer/now-playing.tsx +++ b/components/footer/now-playing.tsx @@ -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' @@ -48,7 +48,7 @@ function AnimatedBars() { } const NowPlaying = () => { - const { data } = useSWR(ApiRoutes.getSong, fetcher) + const { data: response } = useSWR>(ApiRoutes.GetSong, fetcher) return (
@@ -63,19 +63,19 @@ const NowPlaying = () => { - {data?.isPlaying ? : null} + {response?.data?.isPlaying ? : null}

- {data?.isPlaying ? ( + {response?.data?.isPlaying ? ( <> In questo momento sto ascoltando - {data.name} - - {data.artist} + {response.data.name} + - {response.data.artist} ) : ( diff --git a/components/gtm.tsx b/components/gtm.tsx new file mode 100644 index 0000000..863c3d0 --- /dev/null +++ b/components/gtm.tsx @@ -0,0 +1,31 @@ +'use client' + +import Script from 'next/script' + +import { env } from '@/env.mjs' + +export function GTM() { + return ( + <> +