diff --git a/.env.local.sample b/.env.local.sample index 1414025..babd5ea 100644 --- a/.env.local.sample +++ b/.env.local.sample @@ -1,5 +1,6 @@ NEXT_PUBLIC_CHATWOOT_API_KEY=obtain-from-engineering NEXT_PUBLIC_ONRAMPER_URL=https://buy.onramper.com/?apiKey=pk_prod_01HWMA66BYRB2271G08XDZVVCX&defaultCrypto=btc&wallets=btc%3A&onlyCryptos=btc&supportSell=false&isAddressEditable=false&language=en&defaultFiat=USD&themeName=dark&redirectURL=https%3A%2F%2Fapp.shapeshift.com%2F%23%2Fbuy-crypto&signature=cf9f78149db21574bbfe02dc72a1ab15ae15a0bd25d7b932ef0b453d8a922f30 -NEXT_PUBLIC_STRAPI_API_TOKEN=obtain-from-engineering NEXT_PUBLIC_STRAPI_URL=https://strapi.shapeshift.com NEXT_PUBLIC_WEGLOT_API_KEY=obtain-from-engineering + +STRAPI_API_TOKEN=obtain-from-engineering \ No newline at end of file diff --git a/app/[lang]/(core-products)/_components/ProductHero.tsx b/app/[lang]/(core-products)/_components/ProductHero.tsx index 9270a0f..7ea0624 100644 --- a/app/[lang]/(core-products)/_components/ProductHero.tsx +++ b/app/[lang]/(core-products)/_components/ProductHero.tsx @@ -20,7 +20,7 @@ import Image from 'next/image' import { Button } from '@/app/[lang]/_components/Button' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import type { TButton, TStrapiImage } from '@/app/[lang]/_components/strapi/types' import type { ReactNode } from 'react' diff --git a/app/[lang]/(core-products)/_components/fetchUtils.ts b/app/[lang]/(core-products)/_components/fetchUtils.ts index 6f643dd..de5901b 100644 --- a/app/[lang]/(core-products)/_components/fetchUtils.ts +++ b/app/[lang]/(core-products)/_components/fetchUtils.ts @@ -1,3 +1,7 @@ +import 'server-only' + +import { strapiServerFetch } from '@/app/[lang]/_utils/strapiServer' + /************************************************************************************************ * Generic fetch utility with consistent error handling * @@ -12,14 +16,7 @@ export async function fetchWithErrorHandling( error: string ): Promise { try { - const response = await fetch(`${process.env.NEXT_PUBLIC_STRAPI_URL}/api/${endpoint}?${queryParams}`, { - headers: { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRAPI_API_TOKEN}`, - }, - next: { - revalidate: 3600, // Cache for 1 hour - }, - }) + const response = await strapiServerFetch(`${endpoint}?${queryParams}`, { revalidate: 3600 }) if (!response.ok) { console.error(`Failed to fetch ${error}: Status ${response.status}`) diff --git a/app/[lang]/(core-products)/defi-wallet/page.tsx b/app/[lang]/(core-products)/defi-wallet/page.tsx index b1ec4bb..5fbe9de 100644 --- a/app/[lang]/(core-products)/defi-wallet/page.tsx +++ b/app/[lang]/(core-products)/defi-wallet/page.tsx @@ -20,7 +20,7 @@ import Script from 'next/script' import { Card } from '@/app/[lang]/_components/strapi/cards-row/Card' import CardsRow from '@/app/[lang]/_components/strapi/cards-row/CardsRow' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import { generateProductSchema } from '@/app/[lang]/_utils/schema' import { BackgroundImage } from '../_components/BackgroundImage' diff --git a/app/[lang]/(core-products)/earn/page.tsx b/app/[lang]/(core-products)/earn/page.tsx index eedc1dc..3b5c767 100644 --- a/app/[lang]/(core-products)/earn/page.tsx +++ b/app/[lang]/(core-products)/earn/page.tsx @@ -19,7 +19,7 @@ import { notFound } from 'next/navigation' import Script from 'next/script' import Grid from '@/app/[lang]/_components/strapi/products/Grid' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import { generateProductSchema } from '@/app/[lang]/_utils/schema' import { BackgroundImage } from '../_components/BackgroundImage' diff --git a/app/[lang]/(core-products)/mobile-app/page.tsx b/app/[lang]/(core-products)/mobile-app/page.tsx index c0268b6..3ed901f 100644 --- a/app/[lang]/(core-products)/mobile-app/page.tsx +++ b/app/[lang]/(core-products)/mobile-app/page.tsx @@ -19,7 +19,7 @@ import { notFound } from 'next/navigation' import Script from 'next/script' import GridLadder from '@/app/[lang]/_components/strapi/products/GridLadder' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import { generateProductSchema } from '@/app/[lang]/_utils/schema' import { BackgroundImage } from '../_components/BackgroundImage' diff --git a/app/[lang]/(core-products)/trade/page.tsx b/app/[lang]/(core-products)/trade/page.tsx index 0d3df1d..34803e3 100644 --- a/app/[lang]/(core-products)/trade/page.tsx +++ b/app/[lang]/(core-products)/trade/page.tsx @@ -23,7 +23,7 @@ import Script from 'next/script' import { Card } from '@/app/[lang]/_components/strapi/cards-row/Card' import CardsRow from '@/app/[lang]/_components/strapi/cards-row/CardsRow' import GridDisplaced from '@/app/[lang]/_components/strapi/products/GridDisplaced' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import { generateProductSchema } from '@/app/[lang]/_utils/schema' import { BackgroundImage } from '../_components/BackgroundImage' diff --git a/app/[lang]/(resources)/_components/DiscoverFeature.tsx b/app/[lang]/(resources)/_components/DiscoverFeature.tsx index 0b4a51a..2d35d15 100644 --- a/app/[lang]/(resources)/_components/DiscoverFeature.tsx +++ b/app/[lang]/(resources)/_components/DiscoverFeature.tsx @@ -21,7 +21,7 @@ import Image from 'next/image' import { LocalizedLink } from '@/app/[lang]/_components/LocalizedLink' import { cl } from '@/app/[lang]/_utils/cl' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import type { ReactNode } from 'react' diff --git a/app/[lang]/(resources)/_components/ResourceCard.tsx b/app/[lang]/(resources)/_components/ResourceCard.tsx index 3a1e0ef..a5a5495 100644 --- a/app/[lang]/(resources)/_components/ResourceCard.tsx +++ b/app/[lang]/(resources)/_components/ResourceCard.tsx @@ -23,7 +23,7 @@ import Image from 'next/image' import Link from 'next/link' import { cl } from '@/app/[lang]/_utils/cl' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import type { ReactNode } from 'react' diff --git a/app/[lang]/(resources)/_utils/fetchUtils.ts b/app/[lang]/(resources)/_utils/fetchUtils.ts index a49644a..299a21a 100644 --- a/app/[lang]/(resources)/_utils/fetchUtils.ts +++ b/app/[lang]/(resources)/_utils/fetchUtils.ts @@ -15,6 +15,10 @@ ** - Handle null returns with appropriate UI (usually notFound) ************************************************************************************************/ +import 'server-only' + +import { strapiServerFetch } from '@/app/[lang]/_utils/strapiServer' + import type { TDiscoverData, TFaqData, @@ -23,16 +27,6 @@ import type { TSupportedWalletData, } from '@/app/[lang]/_components/strapi/types' -// Common headers and cache configuration -const apiConfig = { - headers: { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRAPI_API_TOKEN}`, - }, - next: { - revalidate: 3600, // Cache for 1 hour - }, -} - /************************************************************************************************ * Fetch all Protocols * @@ -40,9 +34,9 @@ const apiConfig = { ************************************************************************************************/ export async function fetchAllProtocols(): Promise { try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/supported-protocols?populate=*&pagination[page]=1&pagination[pageSize]=500`, - apiConfig + const response = await strapiServerFetch( + `supported-protocols?populate=*&pagination[page]=1&pagination[pageSize]=500`, + { revalidate: 3600 } ) if (!response.ok) { @@ -65,9 +59,9 @@ export async function fetchAllProtocols(): Promise { try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/supported-chains?populate=*&pagination[page]=1&pagination[pageSize]=500`, - apiConfig + const response = await strapiServerFetch( + `supported-chains?populate=*&pagination[page]=1&pagination[pageSize]=500`, + { revalidate: 3600 } ) if (!response.ok) { @@ -90,9 +84,9 @@ export async function fetchAllChains(): Promise { ************************************************************************************************/ export async function fetchAllWallets(): Promise { try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/supported-wallets?populate=*&pagination[page]=1&pagination[pageSize]=500`, - apiConfig + const response = await strapiServerFetch( + `supported-wallets?populate=*&pagination[page]=1&pagination[pageSize]=500`, + { revalidate: 3600 } ) if (!response.ok) { @@ -116,9 +110,9 @@ export async function fetchAllWallets(): Promise ************************************************************************************************/ export async function fetchDiscoverBySlug(slug: string): Promise { try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/discovers?filters[slug][$eq]=${slug}&populate[0]=features&fields[1]=title&fields[2]=description&populate[3]=featuredImg&populate[4]=features.image&fields[5]=tag&populate[6]=features.buttonCta&pagination[page]=1&pagination[pageSize]=500`, - apiConfig + const response = await strapiServerFetch( + `discovers?filters[slug][$eq]=${encodeURIComponent(slug)}&populate[0]=features&fields[1]=title&fields[2]=description&populate[3]=featuredImg&populate[4]=features.image&fields[5]=tag&populate[6]=features.buttonCta&pagination[page]=1&pagination[pageSize]=500`, + { revalidate: 3600 } ) if (!response.ok) { @@ -144,9 +138,9 @@ export async function fetchDiscoverBySlug(slug: string): Promise { try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/faq?populate[faqSection][populate][faqSectionItem][populate]=*&pagination[page]=1&pagination[pageSize]=500`, - apiConfig + const response = await strapiServerFetch( + `faq?populate[faqSection][populate][faqSectionItem][populate]=*&pagination[page]=1&pagination[pageSize]=500`, + { revalidate: 3600 } ) if (!response.ok) { @@ -161,3 +155,86 @@ export async function fetchFaqData(): Promise { return null } } + +/************************************************************************************************ + * Fetch a single Protocol by slug + * + * @param slug - Unique identifier for the protocol + * @returns Promise resolving to protocol data or null if not found + ************************************************************************************************/ +export async function fetchSupportedProtocol(slug: string): Promise { + try { + const res = await strapiServerFetch(`supported-protocols?filters[slug][$eq]=${encodeURIComponent(slug)}&populate=*`) + + if (!res.ok) { + return null + } + const data = await res.json() + return data.data[0] ?? null + } catch (error) { + console.error('Error fetching protocol data:', error instanceof Error ? error.message : String(error)) + return null + } +} + +/************************************************************************************************ + * Fetch a single Chain by slug + * + * @param slug - Unique identifier for the chain + * @returns Promise resolving to chain data or null if not found + ************************************************************************************************/ +export async function fetchSupportedChain(slug: string): Promise { + try { + const res = await strapiServerFetch(`supported-chains?filters[slug][$eq]=${encodeURIComponent(slug)}&populate=*`) + + if (!res.ok) { + return null + } + const data = await res.json() + return data.data[0] ?? null + } catch (error) { + console.error('Error fetching chain data:', error instanceof Error ? error.message : String(error)) + return null + } +} + +/************************************************************************************************ + * Fetch a single Wallet by slug + * + * @param slug - Unique identifier for the wallet + * @returns Promise resolving to wallet data or null if not found + ************************************************************************************************/ +export async function fetchSupportedWallet(slug: string): Promise { + try { + const res = await strapiServerFetch(`supported-wallets?filters[slug][$eq]=${encodeURIComponent(slug)}&populate=*`) + + if (!res.ok) { + return null + } + const data = await res.json() + return data.data[0] ?? null + } catch (error) { + console.error('Error fetching wallet data:', error instanceof Error ? error.message : String(error)) + return null + } +} + +/************************************************************************************************ + * Fetch all Discover entries + * + * @returns Promise resolving to array of discover data or null if error + ************************************************************************************************/ +export async function fetchDiscovers(): Promise { + try { + const res = await strapiServerFetch(`discovers?populate=*`) + + if (!res.ok) { + return null + } + const data = await res.json() + return data.data + } catch (error) { + console.error('Error fetching discover data:', error instanceof Error ? error.message : String(error)) + return null + } +} diff --git a/app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/page.tsx b/app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/page.tsx index 679fb5f..a9d211b 100644 --- a/app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/page.tsx +++ b/app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/page.tsx @@ -1,19 +1,15 @@ import { notFound } from 'next/navigation' import { ListOfPosts } from '@/app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/ListOfPosts' +import { strapiServerFetch } from '@/app/[lang]/_utils/strapiServer' export default async function BlogCategoriesPage(props: { params: Promise<{ category: string }> }): Promise { const { category } = await props.params - const data = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/posts?filters[type][$contains]=${category}&fields[0]=title&fields[1]=slug&fields[2]=publishedAt&populate[0]=featuredImg&sort[0]=publishedAt:desc&pagination[pageSize]=1`, - { - headers: { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRAPI_API_TOKEN}`, - }, - } + const data = await strapiServerFetch( + `posts?filters[type][$contains]=${encodeURIComponent(category)}&fields[0]=title&fields[1]=slug&fields[2]=publishedAt&populate[0]=featuredImg&sort[0]=publishedAt:desc&pagination[pageSize]=1` ) const { data: posts } = await data.json() diff --git a/app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/page.tsx b/app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/page.tsx index f6c3cc4..2db10f9 100644 --- a/app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/page.tsx +++ b/app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/page.tsx @@ -1,16 +1,12 @@ import { notFound } from 'next/navigation' import { ListOfPosts } from '@/app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/ListOfPosts' +import { strapiServerFetch } from '@/app/[lang]/_utils/strapiServer' export default async function BlogTagsPage(props: { params: Promise<{ tag: string }> }): Promise { const { tag } = await props.params - const data = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/posts?filters[tags][$contains]=${tag}&fields[0]=title&fields[1]=slug&fields[2]=publishedAt&populate[0]=featuredImg&sort[0]=publishedAt:desc`, - { - headers: { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRAPI_API_TOKEN}`, - }, - } + const data = await strapiServerFetch( + `posts?filters[tags][$contains]=${encodeURIComponent(tag)}&fields[0]=title&fields[1]=slug&fields[2]=publishedAt&populate[0]=featuredImg&sort[0]=publishedAt:desc` ) const { data: posts } = await data.json() diff --git a/app/[lang]/(resources)/blog/[slug]/layout.tsx b/app/[lang]/(resources)/blog/[slug]/layout.tsx index d9f9601..e8bb558 100644 --- a/app/[lang]/(resources)/blog/[slug]/layout.tsx +++ b/app/[lang]/(resources)/blog/[slug]/layout.tsx @@ -1,6 +1,7 @@ import { notFound } from 'next/navigation' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' +import { strapiServerFetch } from '@/app/[lang]/_utils/strapiServer' import type { Metadata } from 'next' @@ -11,13 +12,8 @@ import type { Metadata } from 'next' ************************************************************************************************/ export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise { const { slug } = await params - const data = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/posts?filters[slug][$eq]=${slug}&fields[0]=summary&fields[2]=tags&fields[3]=title&fields[4]=publishedAt&populate[0]=featuredImg`, - { - headers: { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRAPI_API_TOKEN}`, - }, - } + const data = await strapiServerFetch( + `posts?filters[slug][$eq]=${encodeURIComponent(slug)}&fields[0]=summary&fields[2]=tags&fields[3]=title&fields[4]=publishedAt&populate[0]=featuredImg` ).then(async (res) => res.json()) const post = data.data[0] diff --git a/app/[lang]/(resources)/chains/[slug]/page.tsx b/app/[lang]/(resources)/chains/[slug]/page.tsx index 6b8cd18..5c13daa 100644 --- a/app/[lang]/(resources)/chains/[slug]/page.tsx +++ b/app/[lang]/(resources)/chains/[slug]/page.tsx @@ -1,13 +1,15 @@ import Image from 'next/image' import { notFound } from 'next/navigation' +import { fetchSupportedChain } from '@/app/[lang]/(resources)/_utils/fetchUtils' import { Banner } from '@/app/[lang]/_components/Banner' import { ChainActions } from '@/app/[lang]/_components/strapi/templates/ChainActions' import { ChainDescription } from '@/app/[lang]/_components/strapi/templates/ChainDescription' import { ChainFeatures } from '@/app/[lang]/_components/strapi/templates/ChainFeatures' import { ChainHeader } from '@/app/[lang]/_components/strapi/templates/ChainHeader' import { ChainHero } from '@/app/[lang]/_components/strapi/templates/ChainHero' -import { getStrapiImageUrl, getSupportedChain } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' +import { strapiServerFetch } from '@/app/[lang]/_utils/strapiServer' import type { TSupportedChainData } from '@/app/[lang]/_components/strapi/types' import type { Metadata } from 'next' @@ -19,14 +21,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str return notFound() } - const response = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/supported-chains?filters[slug][$eq]=${slug}&populate=*`, - { - headers: { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRAPI_API_TOKEN}`, - }, - } - ) + const response = await strapiServerFetch(`supported-chains?filters[slug][$eq]=${encodeURIComponent(slug)}&populate=*`) const data = await response.json() const chain = data.data[0] as TSupportedChainData if (!chain) { @@ -68,7 +63,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str export default async function ChainPage({ params }: { params: Promise<{ slug: string }> }): Promise { const { slug } = await params - const chain = await getSupportedChain(slug) + const chain = await fetchSupportedChain(slug) if (!chain) { return notFound() diff --git a/app/[lang]/(resources)/discover/[slug]/page.tsx b/app/[lang]/(resources)/discover/[slug]/page.tsx index 02ec8c5..4d359cd 100644 --- a/app/[lang]/(resources)/discover/[slug]/page.tsx +++ b/app/[lang]/(resources)/discover/[slug]/page.tsx @@ -24,7 +24,7 @@ import { ResourceHero } from '@/app/[lang]/(resources)/_components/ResourceHero' import { DEFAULT_FEATURES } from '@/app/[lang]/(resources)/_utils/constants' import { fetchDiscoverBySlug } from '@/app/[lang]/(resources)/_utils/fetchUtils' import { Banner } from '@/app/[lang]/_components/Banner' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import type { Metadata } from 'next' import type { ReactNode } from 'react' diff --git a/app/[lang]/(resources)/discover/page.tsx b/app/[lang]/(resources)/discover/page.tsx index ce8f21b..af15f69 100644 --- a/app/[lang]/(resources)/discover/page.tsx +++ b/app/[lang]/(resources)/discover/page.tsx @@ -1,14 +1,14 @@ +import { fetchDiscovers } from '@/app/[lang]/(resources)/_utils/fetchUtils' import { Banner } from '@/app/[lang]/_components/Banner' import { Button } from '@/app/[lang]/_components/Button' import { RESOURCES_DICT } from '@/app/[lang]/_utils/dictionary/resources' -import { getDiscovers } from '@/app/[lang]/_utils/query' import { DiscoverSearchWrapper } from './_components/DiscoverSearchWrapper' import type { ReactNode } from 'react' export default async function DiscoverPage(): Promise { - const discover = await getDiscovers() + const discover = await fetchDiscovers() return (
diff --git a/app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/page.tsx b/app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/page.tsx index 6a43939..21f4155 100644 --- a/app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/page.tsx +++ b/app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/page.tsx @@ -1,18 +1,14 @@ import { notFound } from 'next/navigation' import { ListOfPosts } from '@/app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/ListOfPosts' +import { strapiServerFetch } from '@/app/[lang]/_utils/strapiServer' export default async function BlogCategoriesPage(props: { params: Promise<{ category: string }> }): Promise { const { category } = await props.params - const data = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/newsrooms?filters[category][$contains]=${category}&fields[0]=title&fields[1]=slug&fields[2]=publishedAt&populate[0]=featuredImg&sort[0]=publishedAt:desc&pagination[pageSize]=1`, - { - headers: { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRAPI_API_TOKEN}`, - }, - } + const data = await strapiServerFetch( + `newsrooms?filters[category][$contains]=${encodeURIComponent(category)}&fields[0]=title&fields[1]=slug&fields[2]=publishedAt&populate[0]=featuredImg&sort[0]=publishedAt:desc&pagination[pageSize]=1` ) const { data: posts } = await data.json() diff --git a/app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/page.tsx b/app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/page.tsx index a809d31..8e7750d 100644 --- a/app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/page.tsx +++ b/app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/page.tsx @@ -1,16 +1,12 @@ import { notFound } from 'next/navigation' import { ListOfPosts } from '@/app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/ListOfPosts' +import { strapiServerFetch } from '@/app/[lang]/_utils/strapiServer' export default async function NewsroomTagsPage(props: { params: Promise<{ tag: string }> }): Promise { const { tag } = await props.params - const data = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/newsrooms?filters[tags][$contains]=${tag}&fields[0]=title&fields[1]=slug&fields[2]=publishedAt&populate[0]=featuredImg&sort[0]=publishedAt:desc`, - { - headers: { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRAPI_API_TOKEN}`, - }, - } + const data = await strapiServerFetch( + `newsrooms?filters[tags][$contains]=${encodeURIComponent(tag)}&fields[0]=title&fields[1]=slug&fields[2]=publishedAt&populate[0]=featuredImg&sort[0]=publishedAt:desc` ) const { data: posts } = await data.json() diff --git a/app/[lang]/(resources)/newsroom/[slug]/layout.tsx b/app/[lang]/(resources)/newsroom/[slug]/layout.tsx index 719ae4e..43feb94 100644 --- a/app/[lang]/(resources)/newsroom/[slug]/layout.tsx +++ b/app/[lang]/(resources)/newsroom/[slug]/layout.tsx @@ -1,6 +1,7 @@ import { notFound } from 'next/navigation' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' +import { strapiServerFetch } from '@/app/[lang]/_utils/strapiServer' import type { Metadata } from 'next' @@ -11,13 +12,8 @@ import type { Metadata } from 'next' ************************************************************************************************/ export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise { const { slug } = await params - const data = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/newsrooms?filters[slug][$eq]=${slug}&fields[0]=postSummary&fields[1]=tags&fields[2]=title&fields[3]=publishedAt&populate[0]=featuredImg`, - { - headers: { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRAPI_API_TOKEN}`, - }, - } + const data = await strapiServerFetch( + `newsrooms?filters[slug][$eq]=${encodeURIComponent(slug)}&fields[0]=postSummary&fields[1]=tags&fields[2]=title&fields[3]=publishedAt&populate[0]=featuredImg` ).then(async (res) => res.json()) const post = data.data[0] diff --git a/app/[lang]/(resources)/protocols/[slug]/page.tsx b/app/[lang]/(resources)/protocols/[slug]/page.tsx index 92c71c2..5f6b8c0 100644 --- a/app/[lang]/(resources)/protocols/[slug]/page.tsx +++ b/app/[lang]/(resources)/protocols/[slug]/page.tsx @@ -5,8 +5,10 @@ import { ProtocolAbout } from '@/app/[lang]/(resources)/_components/ProtocolAbou import { ProtocolEasier } from '@/app/[lang]/(resources)/_components/ProtocolEasier' import { ProtocolFeatures } from '@/app/[lang]/(resources)/_components/ProtocolFeatures' import { ProtocolHeader } from '@/app/[lang]/(resources)/_components/ProtocolHeader' +import { fetchSupportedProtocol } from '@/app/[lang]/(resources)/_utils/fetchUtils' import { Banner } from '@/app/[lang]/_components/Banner' -import { getStrapiImageUrl, getSupportedProtocol } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' +import { strapiServerFetch } from '@/app/[lang]/_utils/strapiServer' import type { TSupportedProtocolData } from '@/app/[lang]/_components/strapi/types' import type { Metadata } from 'next' @@ -18,13 +20,8 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str return notFound() } - const response = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/supported-protocols?filters[slug][$eq]=${slug}&populate=*`, - { - headers: { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRAPI_API_TOKEN}`, - }, - } + const response = await strapiServerFetch( + `supported-protocols?filters[slug][$eq]=${encodeURIComponent(slug)}&populate=*` ) const data = await response.json() const protocol = data.data[0] as TSupportedProtocolData @@ -68,7 +65,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str export default async function ProtocolPage({ params }: { params: Promise<{ slug: string }> }): Promise { const { slug } = await params - const protocol = await getSupportedProtocol(slug) + const protocol = await fetchSupportedProtocol(slug) if (!protocol) { return notFound() diff --git a/app/[lang]/(resources)/wallets/[slug]/page.tsx b/app/[lang]/(resources)/wallets/[slug]/page.tsx index 0da655b..9c601f6 100644 --- a/app/[lang]/(resources)/wallets/[slug]/page.tsx +++ b/app/[lang]/(resources)/wallets/[slug]/page.tsx @@ -3,9 +3,11 @@ import { notFound } from 'next/navigation' import { SupportedWalletAccelerate } from '@/app/[lang]/(resources)/_components/SupportedWalletAccelerate' import { SupportedWalletHeader } from '@/app/[lang]/(resources)/_components/SupportedWalletHeader' import { SupportedWalletHero } from '@/app/[lang]/(resources)/_components/SupportedWalletHero' +import { fetchSupportedWallet } from '@/app/[lang]/(resources)/_utils/fetchUtils' import { Banner } from '@/app/[lang]/_components/Banner' import { StrapiFAQ } from '@/app/[lang]/_components/StrapiFAQ' -import { getStrapiImageUrl, getSupportedWallet } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' +import { strapiServerFetch } from '@/app/[lang]/_utils/strapiServer' import type { TSupportedWalletData } from '@/app/[lang]/_components/strapi/types' import type { Metadata } from 'next' @@ -17,13 +19,8 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str return notFound() } - const response = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/supported-wallets?filters[slug][$eq]=${slug}&populate=*`, - { - headers: { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRAPI_API_TOKEN}`, - }, - } + const response = await strapiServerFetch( + `supported-wallets?filters[slug][$eq]=${encodeURIComponent(slug)}&populate=*` ) const data = await response.json() const wallet = data.data[0] as TSupportedWalletData @@ -66,7 +63,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str export default async function WalletPage({ params }: { params: Promise<{ slug: string }> }): Promise { const { slug } = await params - const wallet = await getSupportedWallet(slug) + const wallet = await fetchSupportedWallet(slug) if (!wallet) { return notFound() diff --git a/app/[lang]/(terms)/_components/utils.ts b/app/[lang]/(terms)/_components/utils.ts index 9404f5a..96965ed 100644 --- a/app/[lang]/(terms)/_components/utils.ts +++ b/app/[lang]/(terms)/_components/utils.ts @@ -6,42 +6,65 @@ ** Enforces consistent data structure between different terms pages **************************************************************************************************/ -import { getPrivacyPolicy, getTermsOfService } from '@/app/[lang]/_utils/query' +import 'server-only' + +import { strapiServerFetch } from '@/app/[lang]/_utils/strapiServer' import type { TTermsItemData } from '@/app/[lang]/(terms)/_components/TermsAccordion' +import type { TPrivacyPolicyData, TTermsOfServiceData } from '@/app/[lang]/_components/strapi/types' /************************************************************************************************** * Transforms privacy policy data from API format to component format **************************************************************************************************/ export async function getPrivacyPolicyItems(): Promise { - const data = await getPrivacyPolicy() + try { + const res = await strapiServerFetch(`privacy-policy?populate=*`) + + if (!res.ok) { + return [] + } + const { data } = (await res.json()) as { data: TPrivacyPolicyData | null } - if (!data) { + if (!data) { + return [] + } + + return data.policy.map((policy) => ({ + id: policy.id, + title: policy.title, + date: policy.date, + content: policy.policy, + })) + } catch (error) { + console.error('Error fetching privacy policy data:', error instanceof Error ? error.message : String(error)) return [] } - - return data.policy.map((policy) => ({ - id: policy.id, - title: policy.title, - date: policy.date, - content: policy.policy, - })) } /************************************************************************************************** * Transforms terms of service data from API format to component format **************************************************************************************************/ export async function getTermsOfServiceItems(): Promise { - const data = await getTermsOfService() + try { + const res = await strapiServerFetch(`terms-of-service?populate=*`) - if (!data) { + if (!res.ok) { + return [] + } + const { data } = (await res.json()) as { data: TTermsOfServiceData | null } + + if (!data) { + return [] + } + + return data.terms.map((term) => ({ + id: term.id, + title: term.title, + date: term.date, + content: term.policy, + })) + } catch (error) { + console.error('Error fetching terms of service data:', error instanceof Error ? error.message : String(error)) return [] } - - return data.terms.map((term) => ({ - id: term.id, - title: term.title, - date: term.date, - content: term.policy, - })) } diff --git a/app/[lang]/_components/BlogPost.tsx b/app/[lang]/_components/BlogPost.tsx index c0e5fd6..5815db9 100644 --- a/app/[lang]/_components/BlogPost.tsx +++ b/app/[lang]/_components/BlogPost.tsx @@ -1,7 +1,7 @@ import Image from 'next/image' import { useMemo } from 'react' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import { LocalizedLink } from './LocalizedLink' import { cl } from '../_utils/cl' diff --git a/app/[lang]/_components/ElementCard.tsx b/app/[lang]/_components/ElementCard.tsx index 686c70d..e05744a 100644 --- a/app/[lang]/_components/ElementCard.tsx +++ b/app/[lang]/_components/ElementCard.tsx @@ -5,7 +5,7 @@ import Image from 'next/image' import Link from 'next/link' import { cl } from '@/app/[lang]/_utils/cl' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import type { ReactNode } from 'react' diff --git a/app/[lang]/_components/NewsPost.tsx b/app/[lang]/_components/NewsPost.tsx index 444bfe4..ef877e0 100644 --- a/app/[lang]/_components/NewsPost.tsx +++ b/app/[lang]/_components/NewsPost.tsx @@ -17,7 +17,7 @@ import Image from 'next/image' import { useMemo } from 'react' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import { LocalizedLink } from './LocalizedLink' import { cl } from '../_utils/cl' diff --git a/app/[lang]/_components/Notification.tsx b/app/[lang]/_components/Notification.tsx index 4618d08..2592877 100644 --- a/app/[lang]/_components/Notification.tsx +++ b/app/[lang]/_components/Notification.tsx @@ -52,10 +52,7 @@ export function Notification(): ReactNode { const fetchData = async (): Promise => { try { setIsLoading(true) - const res = await fetch(`${process.env.NEXT_PUBLIC_STRAPI_URL}/api/notification?populate=*`, { - headers: { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRAPI_API_TOKEN}`, - }, + const res = await fetch('/api/strapi/notification?populate=*', { signal: abortController.signal, }) diff --git a/app/[lang]/_components/Popup.tsx b/app/[lang]/_components/Popup.tsx index 363d844..2039398 100644 --- a/app/[lang]/_components/Popup.tsx +++ b/app/[lang]/_components/Popup.tsx @@ -1,7 +1,7 @@ import { motion } from 'motion/react' import Link from 'next/link' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import { IconArrow } from '../_icons/IconArrow' import { IconClose } from '../_icons/IconClose' diff --git a/app/[lang]/_components/StrapiFAQ.tsx b/app/[lang]/_components/StrapiFAQ.tsx index dcceba3..aa8bf47 100644 --- a/app/[lang]/_components/StrapiFAQ.tsx +++ b/app/[lang]/_components/StrapiFAQ.tsx @@ -9,19 +9,21 @@ import { useCallback, useEffect, useState } from 'react' import { QuestionSection } from '@/app/[lang]/_components/QuestionSection' -import { getFaq } from '@/app/[lang]/_utils/query' -import type { TFaqSectionItem } from '@/app/[lang]/_components/strapi/types' +import type { TFaqData, TFaqSectionItem } from '@/app/[lang]/_components/strapi/types' import type { ReactNode } from 'react' export function StrapiFAQ(): ReactNode { const [faqItems, setFaqItems] = useState([]) - /* Callback: Fetches FAQ items from Strapi CMS - * No dependencies as it's a static fetch - */ const handleFAQItems = useCallback(async () => { - const data = await getFaq() + const res = await fetch( + `/api/strapi/faq?populate[0]=faqSection&populate[1]=faqSection.faqSectionItem&pagination[pageSize]=10&pagination[page]=1&status=published&locale=en` + ) + if (!res.ok) { + return + } + const { data } = (await res.json()) as { data: TFaqData | null } const allQuestions = data?.faqSection.flatMap((section) => section.faqSectionItem) ?? [] setFaqItems(allQuestions) }, []) diff --git a/app/[lang]/_components/strapi/cards-row/Card.tsx b/app/[lang]/_components/strapi/cards-row/Card.tsx index ef16c6a..db4f5cd 100644 --- a/app/[lang]/_components/strapi/cards-row/Card.tsx +++ b/app/[lang]/_components/strapi/cards-row/Card.tsx @@ -3,7 +3,7 @@ import Image from 'next/image' import { Button } from '@/app/[lang]/_components/Button' import { cl } from '@/app/[lang]/_utils/cl' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import type { TCard } from '@/app/[lang]/_components/strapi/types' import type { ReactNode } from 'react' diff --git a/app/[lang]/_components/strapi/products/CarouselCard.tsx b/app/[lang]/_components/strapi/products/CarouselCard.tsx index ed0ab04..dc6aaae 100644 --- a/app/[lang]/_components/strapi/products/CarouselCard.tsx +++ b/app/[lang]/_components/strapi/products/CarouselCard.tsx @@ -4,7 +4,7 @@ import Image from 'next/image' import { Carousel } from '@/app/[lang]/_components/Carousel' import { LocalizedLink } from '@/app/[lang]/_components/LocalizedLink' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import type { TStrapiImage } from '@/app/[lang]/_components/strapi/types' import type { ReactNode } from 'react' diff --git a/app/[lang]/_components/strapi/products/ChainBubblesCard.tsx b/app/[lang]/_components/strapi/products/ChainBubblesCard.tsx index 121b2e3..5d3fac4 100644 --- a/app/[lang]/_components/strapi/products/ChainBubblesCard.tsx +++ b/app/[lang]/_components/strapi/products/ChainBubblesCard.tsx @@ -5,7 +5,7 @@ import Image from 'next/image' import Link from 'next/link' import { PRODUCTS_DICT } from '@/app/[lang]/_utils/dictionary/products' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import type { TStrapiImage } from '@/app/[lang]/_components/strapi/types' import type { ReactNode } from 'react' diff --git a/app/[lang]/_components/strapi/products/Grid.tsx b/app/[lang]/_components/strapi/products/Grid.tsx index a4610a3..fb1a8cf 100644 --- a/app/[lang]/_components/strapi/products/Grid.tsx +++ b/app/[lang]/_components/strapi/products/Grid.tsx @@ -2,7 +2,7 @@ import Image from 'next/image' import { Button } from '@/app/[lang]/_components/Button' import { cl } from '@/app/[lang]/_utils/cl' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import type { TCard, TGridSection } from '@/app/[lang]/_components/strapi/types' import type { ReactNode } from 'react' diff --git a/app/[lang]/_components/strapi/products/GridDisplaced.tsx b/app/[lang]/_components/strapi/products/GridDisplaced.tsx index ede9662..060c7ea 100644 --- a/app/[lang]/_components/strapi/products/GridDisplaced.tsx +++ b/app/[lang]/_components/strapi/products/GridDisplaced.tsx @@ -3,7 +3,7 @@ import Image from 'next/image' import { useMemo } from 'react' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import { BuyCryptoCard } from './BuyCryptoCard' import { CarouselCard } from './CarouselCard' diff --git a/app/[lang]/_components/strapi/products/GridLadder.tsx b/app/[lang]/_components/strapi/products/GridLadder.tsx index af5003e..177ef65 100644 --- a/app/[lang]/_components/strapi/products/GridLadder.tsx +++ b/app/[lang]/_components/strapi/products/GridLadder.tsx @@ -3,7 +3,7 @@ import React from 'react' import { Button } from '@/app/[lang]/_components/Button' import { cl } from '@/app/[lang]/_utils/cl' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import type { TGridLadderSection, TGridLadderStep } from '@/app/[lang]/_components/strapi/types' import type { ReactNode } from 'react' diff --git a/app/[lang]/_components/strapi/templates/ChainFeatures.tsx b/app/[lang]/_components/strapi/templates/ChainFeatures.tsx index 478aa79..9cc5a52 100644 --- a/app/[lang]/_components/strapi/templates/ChainFeatures.tsx +++ b/app/[lang]/_components/strapi/templates/ChainFeatures.tsx @@ -4,7 +4,7 @@ import { useMemo } from 'react' import { Button } from '@/app/[lang]/_components/Button' import { LocalizedLink } from '@/app/[lang]/_components/LocalizedLink' import { cl } from '@/app/[lang]/_utils/cl' -import { getStrapiImageUrl } from '@/app/[lang]/_utils/query' +import { getStrapiImageUrl } from '@/app/[lang]/_utils/getStrapiImageUrl' import type { TStrapiImage } from '@/app/[lang]/_components/strapi/types' import type { ReactNode } from 'react' diff --git a/app/[lang]/_hooks/useFetchNewsroom.tsx b/app/[lang]/_hooks/useFetchNewsroom.tsx index b7c5268..009add3 100644 --- a/app/[lang]/_hooks/useFetchNewsroom.tsx +++ b/app/[lang]/_hooks/useFetchNewsroom.tsx @@ -76,12 +76,7 @@ export function useFetchNewsroom({ try { // Fetch posts with featured image population const res = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/newsrooms?populate[0]=featuredImg&fields[0]=slug&fields[1]=postSummary&fields[2]=title&fields[3]=category&fields[4]=tags&fields[5]=publishedAt&fields[7]=publishedOn&fields[14]=author&fields[8]=externalURL&sort[0]=publishedOn:${sort}&pagination[page]=${page}&pagination[pageSize]=${pageSize}&pagination[withCount]=true${populateContent ? '&fields[6]=content' : ''}${slug ? `&filters[slug][$eq]=${slug}` : ''}${category ? `&filters[category][$contains]=${category.replace(/ /g, '_')}` : ''}${tag ? `&filters[tags][$contains]=${tag.replace(/ /g, '_')}` : ''}`, - { - headers: { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRAPI_API_TOKEN}`, - }, - } + `/api/strapi/newsrooms?populate[0]=featuredImg&fields[0]=slug&fields[1]=postSummary&fields[2]=title&fields[3]=category&fields[4]=tags&fields[5]=publishedAt&fields[7]=publishedOn&fields[14]=author&fields[8]=externalURL&sort[0]=publishedOn:${sort}&pagination[page]=${page}&pagination[pageSize]=${pageSize}&pagination[withCount]=true${populateContent ? '&fields[6]=content' : ''}${slug ? `&filters[slug][$eq]=${encodeURIComponent(slug)}` : ''}${category ? `&filters[category][$contains]=${encodeURIComponent(category.replace(/ /g, '_'))}` : ''}${tag ? `&filters[tags][$contains]=${encodeURIComponent(tag.replace(/ /g, '_'))}` : ''}` ) if (!res.ok) { diff --git a/app/[lang]/_hooks/useFetchPosts.tsx b/app/[lang]/_hooks/useFetchPosts.tsx index 9015d96..67b87a7 100644 --- a/app/[lang]/_hooks/useFetchPosts.tsx +++ b/app/[lang]/_hooks/useFetchPosts.tsx @@ -96,12 +96,7 @@ export function useFetchPosts({ try { // Fetch posts with featured image population const res = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/posts?populate[0]=featuredImg&fields[0]=slug&fields[1]=summary&fields[2]=title&fields[3]=type&fields[4]=tags&fields[5]=publishedAt&fields[6]=isFeatured&sort[0]=isFeatured:desc&sort[1]=id:${sort}&pagination[page]=${page}&pagination[pageSize]=${pageSize}&pagination[withCount]=true${populateContent ? '&fields[7]=content' : ''}${slug ? `&filters[slug][$eq]=${slug}` : ''}${type ? `&filters[type][$contains]=${type.replace(/ /g, '_')}` : ''}${tag ? `&filters[tags][$contains]=${tag.replace(/ /g, '_')}` : ''}`, - { - headers: { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRAPI_API_TOKEN}`, - }, - } + `/api/strapi/posts?populate[0]=featuredImg&fields[0]=slug&fields[1]=summary&fields[2]=title&fields[3]=type&fields[4]=tags&fields[5]=publishedAt&fields[6]=isFeatured&sort[0]=isFeatured:desc&sort[1]=id:${sort}&pagination[page]=${page}&pagination[pageSize]=${pageSize}&pagination[withCount]=true${populateContent ? '&fields[7]=content' : ''}${slug ? `&filters[slug][$eq]=${encodeURIComponent(slug)}` : ''}${type ? `&filters[type][$contains]=${encodeURIComponent(type.replace(/ /g, '_'))}` : ''}${tag ? `&filters[tags][$contains]=${encodeURIComponent(tag.replace(/ /g, '_'))}` : ''}` ) if (!res.ok) { diff --git a/app/[lang]/_hooks/useFetchSupportArticles.ts b/app/[lang]/_hooks/useFetchSupportArticles.ts index dd2751b..0dd9b04 100644 --- a/app/[lang]/_hooks/useFetchSupportArticles.ts +++ b/app/[lang]/_hooks/useFetchSupportArticles.ts @@ -77,12 +77,7 @@ export function useFetchSupportArticles({ async function fetchArticles(): Promise { try { const res = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/support-articles?populate[0]=featuredImg&fields[0]=slug&fields[1]=summary&fields[2]=title&fields[3]=publishedAt&sort[0]=publishedAt:${sort}&pagination[page]=${page}&pagination[pageSize]=${pageSize}&pagination[withCount]=true${populateContent ? '&fields[4]=content' : ''}${slug ? `&filters[slug][$eq]=${slug}` : ''}`, - { - headers: { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRAPI_API_TOKEN}`, - }, - } + `/api/strapi/support-articles?populate[0]=featuredImg&fields[0]=slug&fields[1]=summary&fields[2]=title&fields[3]=publishedAt&sort[0]=publishedAt:${sort}&pagination[page]=${page}&pagination[pageSize]=${pageSize}&pagination[withCount]=true${populateContent ? '&fields[4]=content' : ''}${slug ? `&filters[slug][$eq]=${encodeURIComponent(slug)}` : ''}` ) if (!res.ok) { diff --git a/app/[lang]/_utils/getStrapiImageUrl.ts b/app/[lang]/_utils/getStrapiImageUrl.ts new file mode 100644 index 0000000..69d0dd4 --- /dev/null +++ b/app/[lang]/_utils/getStrapiImageUrl.ts @@ -0,0 +1,14 @@ +/** + * Builds a full image URL from a Strapi image url field. + * Strapi may return either a relative path (/uploads/...) or an absolute CDN URL. + * Prepend NEXT_PUBLIC_STRAPI_URL only when the url is relative. + */ +export function getStrapiImageUrl(url: string | null | undefined): string { + if (!url) { + return '' + } + if (url.startsWith('http')) { + return url + } + return `${process.env.NEXT_PUBLIC_STRAPI_URL}${url}` +} diff --git a/app/[lang]/_utils/query.ts b/app/[lang]/_utils/query.ts deleted file mode 100644 index 0138f51..0000000 --- a/app/[lang]/_utils/query.ts +++ /dev/null @@ -1,160 +0,0 @@ -import type { - TDiscoverData, - TFaqData, - TPrivacyPolicyData, - TSupportedChainData, - TSupportedProtocolData, - TSupportedWalletData, - TTermsOfServiceData, -} from '@/app/[lang]/_components/strapi/types' - -/** - * API utility functions to fetch data from Strapi CMS - * - * Each function fetches specific content types and handles error cases uniformly. - * All API calls include authentication via STRAPI_API_TOKEN environment variable. - */ - -/** - * Builds a full image URL from a Strapi image url field. - * Strapi may return either a relative path (/uploads/...) or an absolute CDN URL. - * Prepend NEXT_PUBLIC_STRAPI_URL only when the url is relative. - */ -export function getStrapiImageUrl(url: string | null | undefined): string { - if (!url) { - return '' - } - if (url.startsWith('http')) { - return url - } - return `${process.env.NEXT_PUBLIC_STRAPI_URL}${url}` -} - -/** - * Common headers used for all Strapi API requests - */ -const apiHeaders = { - headers: { - Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRAPI_API_TOKEN}`, - }, -} - -/** - * Fetches FAQ sections with nested items - * - * @returns Promise with FAQ data or null if request fails - */ -export async function getFaq(): Promise { - const res = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/faq?populate[0]=faqSection&populate[1]=faqSection.faqSectionItem&pagination[pageSize]=10&pagination[page]=1&status=published&locale=en`, - apiHeaders - ) - - if (!res.ok) { - return null - } - const data = await res.json() - return data.data -} - -/** - * Fetches a specific wallet by its slug - * - * @param slug - Unique identifier for the wallet - * @returns Promise with wallet data or null if request fails or wallet not found - */ -export async function getSupportedWallet(slug: string): Promise { - const res = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/supported-wallets?filters[slug][$eq]=${slug}&populate=*`, - apiHeaders - ) - - if (!res.ok) { - return null - } - const data = await res.json() - return data.data[0] -} - -/** - * Fetches a specific blockchain network by its slug - * - * @param slug - Unique identifier for the chain - * @returns Promise with chain data or null if request fails or chain not found - */ -export async function getSupportedChain(slug: string): Promise { - const res = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/supported-chains?filters[slug][$eq]=${slug}&populate=*`, - apiHeaders - ) - - if (!res.ok) { - return null - } - const data = await res.json() - return data.data[0] -} - -/** - * Fetches a specific protocol by its slug - * - * @param slug - Unique identifier for the protocol - * @returns Promise with protocol data or null if request fails or protocol not found - */ -export async function getSupportedProtocol(slug: string): Promise { - const res = await fetch( - `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/supported-protocols?filters[slug][$eq]=${slug}&populate=*`, - apiHeaders - ) - - if (!res.ok) { - return null - } - const data = await res.json() - return data.data[0] -} - -/** - * Fetches all discover section entries - * - * @returns Promise with array of discover data or null if request fails - */ -export async function getDiscovers(): Promise { - const res = await fetch(`${process.env.NEXT_PUBLIC_STRAPI_URL}/api/discovers?populate=*`, apiHeaders) - - if (!res.ok) { - return null - } - const data = await res.json() - return data.data -} - -/** - * Fetches privacy policy content - * - * @returns Promise with privacy policy data or null if request fails - */ -export async function getPrivacyPolicy(): Promise { - const res = await fetch(`${process.env.NEXT_PUBLIC_STRAPI_URL}/api/privacy-policy?populate=*`, apiHeaders) - - if (!res.ok) { - return null - } - const data = await res.json() - return data.data -} - -/** - * Fetches terms of service content - * - * @returns Promise with terms of service data or null if request fails - */ -export async function getTermsOfService(): Promise { - const res = await fetch(`${process.env.NEXT_PUBLIC_STRAPI_URL}/api/terms-of-service?populate=*`, apiHeaders) - - if (!res.ok) { - return null - } - const data = await res.json() - return data.data -} diff --git a/app/[lang]/_utils/strapiServer.ts b/app/[lang]/_utils/strapiServer.ts new file mode 100644 index 0000000..d2c74a4 --- /dev/null +++ b/app/[lang]/_utils/strapiServer.ts @@ -0,0 +1,20 @@ +import 'server-only' + +/** + * Server-only low-level fetch against the Strapi Content API. + * + * This is the single place (besides the /api/strapi proxy in route.ts) that attaches the API token, + * so no page, component, or util needs to inline it. The `server-only` import turns importing this + * module from a client component into a build error, so the token can never be bundled into client + * code (where process.env.STRAPI_API_TOKEN is undefined and the request would send `Bearer + * undefined`). Client-side data goes through the /api/strapi proxy instead. + * + * `path` is everything after `/api/`, e.g. `posts?filters[slug][$eq]=foo`. Pass `revalidate` + * (seconds) to opt into Next's data cache. + */ +export async function strapiServerFetch(path: string, options?: { revalidate?: number }): Promise { + return fetch(`${process.env.NEXT_PUBLIC_STRAPI_URL}/api/${path}`, { + headers: { Authorization: `Bearer ${process.env.STRAPI_API_TOKEN}` }, + ...(options?.revalidate !== undefined ? { next: { revalidate: options.revalidate } } : {}), + }) +} diff --git a/app/api/strapi/[...path]/route.ts b/app/api/strapi/[...path]/route.ts new file mode 100644 index 0000000..c1ef4df --- /dev/null +++ b/app/api/strapi/[...path]/route.ts @@ -0,0 +1,71 @@ +import { NextResponse } from 'next/server' + +import type { NextRequest } from 'next/server' + +/** + * Same-origin proxy for the Strapi Content API. + * + * Keeps the Strapi API token server-side: the browser calls /api/strapi/ with no + * credentials and this handler injects the bearer token before forwarding to Strapi. + * + * Forwarding is restricted to what the frontend actually uses: + * - only GET, and the upstream path is always prefixed with /api/ + * - only the collections in ALLOWED_COLLECTIONS; anything else 404s + * - only the query params in ALLOWED_QUERY_KEYS; anything else 400s + */ + +// Collections the browser is allowed to read through this proxy. +const ALLOWED_COLLECTIONS = new Set(['notification', 'posts', 'newsrooms', 'support-articles', 'faq']) + +// Top-level query params the frontend uses. +const ALLOWED_QUERY_KEYS = new Set(['fields', 'filters', 'populate', 'sort', 'pagination', 'status', 'locale']) + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ path: string[] }> } +): Promise { + const strapiUrl = process.env.NEXT_PUBLIC_STRAPI_URL + const strapiToken = process.env.STRAPI_API_TOKEN + + if (!strapiUrl || !strapiToken) { + return NextResponse.json({ error: 'Strapi proxy is not configured' }, { status: 500 }) + } + + const { path } = await params + + // Gate on the collection (first path segment) so the proxy can only reach allowlisted content. + if (path.length !== 1 || !ALLOWED_COLLECTIONS.has(path[0])) { + return NextResponse.json({ error: 'Not found' }, { status: 404 }) + } + + // Reject any query param outside the known-safe set. Keys look like `filters[slug][$eq]`, so we + // compare the base key before the first bracket. + for (const key of request.nextUrl.searchParams.keys()) { + const baseKey = key.split('[')[0] + if (!ALLOWED_QUERY_KEYS.has(baseKey)) { + return NextResponse.json({ error: `Unsupported query parameter: ${baseKey}` }, { status: 400 }) + } + } + + const target = new URL(`/api/${path.join('/')}`, strapiUrl) + target.search = request.nextUrl.search + + try { + const upstream = await fetch(target, { + headers: { Authorization: `Bearer ${strapiToken}` }, + next: { revalidate: 60 }, + }) + + const body = await upstream.text() + const responseHeaders = new Headers() + responseHeaders.set('content-type', upstream.headers.get('content-type') ?? 'application/json') + return new NextResponse(body, { + status: upstream.status, + headers: responseHeaders, + }) + } catch { + // Transport-level failure (Strapi unreachable, DNS, timeout) — surface a controlled 502 + // instead of letting the thrown fetch error bubble up as an opaque 500. + return NextResponse.json({ error: 'Upstream request failed' }, { status: 502 }) + } +} diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index 641cef8..0000000 Binary files a/public/favicon.ico and /dev/null differ