diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index e7f3863..494b8fb 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -1,26 +1,26 @@
name: Lint
on:
- pull_request:
- branches: [main, develop]
- push:
- branches: [main, develop]
+ pull_request:
+ branches: [main, develop]
+ push:
+ branches: [main, develop]
jobs:
- lint:
- runs-on: ubuntu-latest
+ lint:
+ runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
- - name: Setup Bun
- uses: oven-sh/setup-bun@v2
- with:
- bun-version: latest
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
- - name: Install dependencies
- run: bun install --frozen-lockfile
+ - name: Install dependencies
+ run: bun install --frozen-lockfile
- - name: Run ESLint
- run: bun run lint
+ - name: Run ESLint
+ run: bun run lint
diff --git a/.gitignore b/.gitignore
index 588db45..303815c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,7 @@
# misc
.DS_Store
+.eslintcache
*.pem
# debug
diff --git a/.prettierrc b/.prettierrc
deleted file mode 100644
index 19a28d9..0000000
--- a/.prettierrc
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "singleQuote": true,
- "semi": false,
- "useTabs": true,
- "tabWidth": 4,
- "trailingComma": "none",
- "bracketSpacing": false,
- "arrowParens": "avoid",
- "bracketSameLine": true,
- "singleAttributePerLine": true,
- "printWidth": 120
-}
\ No newline at end of file
diff --git a/README.md b/README.md
index 9da4bae..e0f83cc 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,3 @@ bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-
-## Deploy on Vercel
-
-The website will redeploy on Vercel on merges to `main`.
diff --git a/app/[lang]/(core-products)/README.md b/app/[lang]/(core-products)/README.md
index df358e7..ccb7bae 100644
--- a/app/[lang]/(core-products)/README.md
+++ b/app/[lang]/(core-products)/README.md
@@ -4,17 +4,18 @@ This directory contains all product-related pages and components for ShapeShift'
## Directory Structure
-- **_components/**: Shared components used across all core product pages
- - Product heroes, stats displays, background images, etc.
- - Utility functions and constants for product data fetching
+- **\_components/**: Shared components used across all core product pages
+ - Product heroes, stats displays, background images, etc.
+ - Utility functions and constants for product data fetching
- **defi-wallet/**: DeFi wallet product page
- **earn/**: Yield-earning opportunities product page
-- **mobile-app/**: Mobile application product page
+- **mobile-app/**: Mobile application product page
- **trade/**: Trading platform product page
## Route Convention
The parentheses in the directory name `(core-products)` indicate a route group in Next.js. This means:
+
- The folder name itself doesn't affect URL paths
- All pages inside share UI layouts and features
- It keeps related features organized without affecting the URL structure
@@ -22,6 +23,7 @@ The parentheses in the directory name `(core-products)` indicate a route group i
## Component Integration
The components in this directory are designed to:
+
- Integrate with the main layout defined in the app root
- Share consistent styling and branding
- Leverage common utilities for product data fetching
@@ -32,4 +34,4 @@ The components in this directory are designed to:
- Follow the design patterns established in the shared components
- Maintain consistent UI/UX across all product pages
- Use the ProductFetcher utility for data retrieval
-- Ensure responsive layouts for all device sizes
\ No newline at end of file
+- Ensure responsive layouts for all device sizes
diff --git a/app/[lang]/(core-products)/_components/BackgroundImage.tsx b/app/[lang]/(core-products)/_components/BackgroundImage.tsx
index 30032d1..ae5c281 100644
--- a/app/[lang]/(core-products)/_components/BackgroundImage.tsx
+++ b/app/[lang]/(core-products)/_components/BackgroundImage.tsx
@@ -18,19 +18,12 @@
import Image from 'next/image'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
export function BackgroundImage(): ReactNode {
- return (
-
-
-
- )
+ return (
+
+
+
+ )
}
diff --git a/app/[lang]/(core-products)/_components/DownloadButtons.tsx b/app/[lang]/(core-products)/_components/DownloadButtons.tsx
index 4e4c94e..689c73a 100644
--- a/app/[lang]/(core-products)/_components/DownloadButtons.tsx
+++ b/app/[lang]/(core-products)/_components/DownloadButtons.tsx
@@ -17,36 +17,32 @@
import Image from 'next/image'
-import {LocalizedLink} from '@/app/[lang]/_components/LocalizedLink'
+import { LocalizedLink } from '@/app/[lang]/_components/LocalizedLink'
-import type {TDownloadButton} from '@/app/[lang]/_components/strapi/types'
-import type {ReactNode} from 'react'
+import type { TDownloadButton } from '@/app/[lang]/_components/strapi/types'
+import type { ReactNode } from 'react'
type TDownloadButtonsProps = {
- buttons: TDownloadButton[];
-};
+ buttons: TDownloadButton[]
+}
-export function DownloadButtons({buttons}: TDownloadButtonsProps): ReactNode {
- if (!buttons || buttons.length === 0) {
- return null
- }
+export function DownloadButtons({ buttons }: TDownloadButtonsProps): ReactNode {
+ if (!buttons || buttons.length === 0) {
+ return null
+ }
- return (
-
- {buttons.map(button => (
-
-
-
- ))}
-
- )
+ return (
+
+ {buttons.map((button) => (
+
+
+
+ ))}
+
+ )
}
diff --git a/app/[lang]/(core-products)/_components/ProductFetcher.ts b/app/[lang]/(core-products)/_components/ProductFetcher.ts
index e7e46af..8381484 100644
--- a/app/[lang]/(core-products)/_components/ProductFetcher.ts
+++ b/app/[lang]/(core-products)/_components/ProductFetcher.ts
@@ -27,18 +27,18 @@
** // Use typed page data safely in your component
************************************************************************************************/
-import {fetchWithErrorHandling} from './fetchUtils'
+import { fetchWithErrorHandling } from './fetchUtils'
import type {
- TButton,
- TCardsRowSection,
- TDownloadButton,
- TFooterSection,
- TGridDisplacedSection,
- TGridLadderSection,
- TGridSection,
- TStat,
- TStrapiImage
+ TButton,
+ TCardsRowSection,
+ TDownloadButton,
+ TFooterSection,
+ TGridDisplacedSection,
+ TGridLadderSection,
+ TGridSection,
+ TStat,
+ TStrapiImage,
} from '@/app/[lang]/_components/strapi/types'
/************************************************************************************************
@@ -53,52 +53,52 @@ import type {
* Base type containing properties common to all product pages
************************************************************************************************/
type TBaseProductPage = {
- title: string;
- description: string;
- featuredImg: TStrapiImage;
- footer: TFooterSection;
-};
+ title: string
+ description: string
+ featuredImg: TStrapiImage
+ footer: TFooterSection
+}
/************************************************************************************************
* DeFi Wallet page data structure
* Features card row layout highlighting wallet capabilities
************************************************************************************************/
type TDeFiWalletPage = TBaseProductPage & {
- buttonCta: TButton;
- buttonDownload: TButton[];
- cardsRow: TCardsRowSection;
-};
+ buttonCta: TButton
+ buttonDownload: TButton[]
+ cardsRow: TCardsRowSection
+}
/************************************************************************************************
* Earn page data structure
* Features grid layout showcasing earning opportunities
************************************************************************************************/
type TEarnPage = TBaseProductPage & {
- buttonCta: TButton;
- buttonDownload: TButton[];
- grid: TGridSection;
-};
+ buttonCta: TButton
+ buttonDownload: TButton[]
+ grid: TGridSection
+}
/************************************************************************************************
* Mobile App page data structure
* Features step-by-step ladder grid and download buttons
************************************************************************************************/
type TMobileAppPage = TBaseProductPage & {
- buttonCta: TButton;
- buttonDownload: TDownloadButton[];
- gridLadder: TGridLadderSection;
-};
+ buttonCta: TButton
+ buttonDownload: TDownloadButton[]
+ gridLadder: TGridLadderSection
+}
/************************************************************************************************
* Trade page data structure
* Features statistics, card row, and displaced grid layout
************************************************************************************************/
type TTradePage = TBaseProductPage & {
- buttonCta: TButton;
- stats: TStat[];
- cardsRow: TCardsRowSection;
- gridDisplaced: TGridDisplacedSection;
-};
+ buttonCta: TButton
+ stats: TStat[]
+ cardsRow: TCardsRowSection
+ gridDisplaced: TGridDisplacedSection
+}
/************************************************************************************************
* Fetches DeFi Wallet page data from Strapi API
@@ -112,10 +112,10 @@ type TTradePage = TBaseProductPage & {
* @returns Promise resolving to page data or null if not found/error
************************************************************************************************/
export async function fetchDeFiWalletPage(): Promise {
- const queryParams =
- 'fields[0]=title&populate[1]=buttonCta&fields[2]=description&populate[3]=featuredImg&populate[4]=cardsRow&populate[5]=cardsRow.cards&populate[6]=cardsRow.cards.image&populate[7]=cardsRow.ctaBlock&populate[8]=cardsRow.ctaBlock.icon&pagination[pageSize]=1&pagination[page]=1&status=published'
+ const queryParams =
+ 'fields[0]=title&populate[1]=buttonCta&fields[2]=description&populate[3]=featuredImg&populate[4]=cardsRow&populate[5]=cardsRow.cards&populate[6]=cardsRow.cards.image&populate[7]=cardsRow.ctaBlock&populate[8]=cardsRow.ctaBlock.icon&pagination[pageSize]=1&pagination[page]=1&status=published'
- return fetchWithErrorHandling('defi-wallet', queryParams, 'DeFi Wallet page')
+ return fetchWithErrorHandling('defi-wallet', queryParams, 'DeFi Wallet page')
}
/************************************************************************************************
@@ -130,10 +130,10 @@ export async function fetchDeFiWalletPage(): Promise {
* @returns Promise resolving to page data or null if not found/error
************************************************************************************************/
export async function fetchEarnPage(): Promise {
- const queryParams =
- 'fields[0]=title&populate[1]=buttonCta&populate[3]=featuredImg&fields[4]=description&populate[10]=grid&populate[11]=grid.cardCta&populate[12]=grid.cardCta.buttonCta&populate[13]=grid.cardCta.imageBg&populate[14]=grid.card&populate[15]=grid.card.image&pagination[pageSize]=10&pagination[page]=1&status=published&locale=en'
+ const queryParams =
+ 'fields[0]=title&populate[1]=buttonCta&populate[3]=featuredImg&fields[4]=description&populate[10]=grid&populate[11]=grid.cardCta&populate[12]=grid.cardCta.buttonCta&populate[13]=grid.cardCta.imageBg&populate[14]=grid.card&populate[15]=grid.card.image&pagination[pageSize]=10&pagination[page]=1&status=published&locale=en'
- return fetchWithErrorHandling('earn', queryParams, 'Earn page')
+ return fetchWithErrorHandling('earn', queryParams, 'Earn page')
}
/************************************************************************************************
@@ -148,10 +148,10 @@ export async function fetchEarnPage(): Promise {
* @returns Promise resolving to page data or null if not found/error
************************************************************************************************/
export async function fetchMobileAppPage(): Promise {
- const queryParams =
- 'fields[0]=title&populate[1]=buttonDownload&fields[2]=description&populate[3]=featuredImg&populate[4]=gridLadder&populate[5]=gridLadder.steps&populate[6]=gridLadder.steps.buttonCta&populate[7]=gridLadder.steps.image&pagination[pageSize]=1&pagination[page]=1&status=published'
+ const queryParams =
+ 'fields[0]=title&populate[1]=buttonDownload&fields[2]=description&populate[3]=featuredImg&populate[4]=gridLadder&populate[5]=gridLadder.steps&populate[6]=gridLadder.steps.buttonCta&populate[7]=gridLadder.steps.image&pagination[pageSize]=1&pagination[page]=1&status=published'
- return fetchWithErrorHandling('mobile-app', queryParams, 'Mobile App page')
+ return fetchWithErrorHandling('mobile-app', queryParams, 'Mobile App page')
}
/************************************************************************************************
@@ -168,8 +168,8 @@ export async function fetchMobileAppPage(): Promise {
* @returns Promise resolving to page data or null if not found/error
************************************************************************************************/
export async function fetchTradePage(): Promise {
- const queryParams =
- 'fields[0]=title&populate[1]=buttonCta&fields[2]=description&populate[3]=featuredImg&populate[4]=stats&populate[5]=cardsRow&populate[6]=cardsRow.cards&populate[7]=cardsRow.cards.image&populate[8]=cardsRow.ctaBlock&populate[9]=cardsRow.ctaBlock.icon&populate[20]=gridDisplaced&populate[21]=gridDisplaced.cards&populate[22]=gridDisplaced.cards.image&pagination[pageSize]=10&populate[23]=gridDisplaced.cards.items&populate[24]=gridDisplaced.cards.items.image&pagination[page]=1&status=published&locale=en'
+ const queryParams =
+ 'fields[0]=title&populate[1]=buttonCta&fields[2]=description&populate[3]=featuredImg&populate[4]=stats&populate[5]=cardsRow&populate[6]=cardsRow.cards&populate[7]=cardsRow.cards.image&populate[8]=cardsRow.ctaBlock&populate[9]=cardsRow.ctaBlock.icon&populate[20]=gridDisplaced&populate[21]=gridDisplaced.cards&populate[22]=gridDisplaced.cards.image&pagination[pageSize]=10&populate[23]=gridDisplaced.cards.items&populate[24]=gridDisplaced.cards.items.image&pagination[page]=1&status=published&locale=en'
- return fetchWithErrorHandling('trade', queryParams, 'Trade page')
+ return fetchWithErrorHandling('trade', queryParams, 'Trade page')
}
diff --git a/app/[lang]/(core-products)/_components/ProductFooterBanner.tsx b/app/[lang]/(core-products)/_components/ProductFooterBanner.tsx
index 6ed0ff4..ba44fbd 100644
--- a/app/[lang]/(core-products)/_components/ProductFooterBanner.tsx
+++ b/app/[lang]/(core-products)/_components/ProductFooterBanner.tsx
@@ -1,4 +1,3 @@
-
/************************************************************************************************
** ProductFooterBanner Component:
**
@@ -18,45 +17,33 @@
** - Maintains consistent branding and messaging
************************************************************************************************/
-import {FooterBanner, FooterBannerMobileApp} from '@/app/[lang]/_components/FooterBanner'
+import { FooterBanner, FooterBannerMobileApp } from '@/app/[lang]/_components/FooterBanner'
-import {PRODUCT_FOOTER_CONFIGS} from './constants'
+import { PRODUCT_FOOTER_CONFIGS } from './constants'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
type TProductFooterBannerProps = {
- productName: keyof typeof PRODUCT_FOOTER_CONFIGS;
-};
-
-export function ProductFooterBanner({productName}: TProductFooterBannerProps): ReactNode {
- // Get configuration for the specified product
- const config = PRODUCT_FOOTER_CONFIGS[productName]
-
- // Verify config exists to prevent runtime errors
- if (!config) {
- console.error(`No footer configuration found for product: ${productName}`)
- return null
- }
-
- // Use mobile-app specific banner for the mobile app product
- if (productName === 'mobile-app') {
- return (
-
- )
- }
-
- // Standard footer banner for other products
- return (
-
- )
+ productName: keyof typeof PRODUCT_FOOTER_CONFIGS
+}
+
+export function ProductFooterBanner({ productName }: TProductFooterBannerProps): ReactNode {
+ // Get configuration for the specified product
+ const config = PRODUCT_FOOTER_CONFIGS[productName]
+
+ // Verify config exists to prevent runtime errors
+ if (!config) {
+ console.error(`No footer configuration found for product: ${productName}`)
+ return null
+ }
+
+ // Use mobile-app specific banner for the mobile app product
+ if (productName === 'mobile-app') {
+ return (
+
+ )
+ }
+
+ // Standard footer banner for other products
+ return
}
diff --git a/app/[lang]/(core-products)/_components/ProductHero.tsx b/app/[lang]/(core-products)/_components/ProductHero.tsx
index e19592b..9270a0f 100644
--- a/app/[lang]/(core-products)/_components/ProductHero.tsx
+++ b/app/[lang]/(core-products)/_components/ProductHero.tsx
@@ -19,59 +19,60 @@
import Image from 'next/image'
-import {Button} from '@/app/[lang]/_components/Button'
+import { Button } from '@/app/[lang]/_components/Button'
+import { getStrapiImageUrl } from '@/app/[lang]/_utils/query'
-import type {TButton, TStrapiImage} from '@/app/[lang]/_components/strapi/types'
-import type {ReactNode} from 'react'
+import type { TButton, TStrapiImage } from '@/app/[lang]/_components/strapi/types'
+import type { ReactNode } from 'react'
type TProductHeroProps = {
- title: string;
- description: string;
- buttonCta?: TButton; // Optional because mobile-app doesn't use it
- featuredImg: TStrapiImage;
- children?: ReactNode; // Additional content like stats or download buttons
- buttonClassName?: string;
-};
+ title: string
+ description: string
+ buttonCta?: TButton // Optional because mobile-app doesn't use it
+ featuredImg: TStrapiImage
+ children?: ReactNode // Additional content like stats or download buttons
+ buttonClassName?: string
+}
export function ProductHero({
- title,
- description,
- buttonCta,
- featuredImg,
- children,
- buttonClassName = '!w-full lg:!w-[232px]'
+ title,
+ description,
+ buttonCta,
+ featuredImg,
+ children,
+ buttonClassName = '!w-full lg:!w-[232px]',
}: TProductHeroProps): ReactNode {
- return (
-
-
-
-
{title}
-
-
{description}
- {buttonCta ? (
-
- ) : null}
- {children}
-
-
+ return (
+
+
+
+
{title}
+
+
{description}
+ {buttonCta ? (
+
+ ) : null}
+ {children}
+
+
-
-
-
-
-
- )
+
+
+
+
+
+ )
}
diff --git a/app/[lang]/(core-products)/_components/ProductStats.tsx b/app/[lang]/(core-products)/_components/ProductStats.tsx
index 3fe71aa..3584ddf 100644
--- a/app/[lang]/(core-products)/_components/ProductStats.tsx
+++ b/app/[lang]/(core-products)/_components/ProductStats.tsx
@@ -16,35 +16,37 @@
** - Stats will be arranged in a row (desktop) or column (mobile)
************************************************************************************************/
-import type {TStat} from '@/app/[lang]/_components/strapi/types'
-import type {ReactNode} from 'react'
+import type { TStat } from '@/app/[lang]/_components/strapi/types'
+import type { ReactNode } from 'react'
type TProductStatsProps = {
- stats: TStat[];
-};
+ stats: TStat[]
+}
-export function ProductStats({stats = []}: TProductStatsProps): ReactNode {
- // Handle case where stats is null, undefined, or empty
- if (!stats || stats.length === 0) {
- return {'No statistics available'}
- }
+export function ProductStats({ stats = [] }: TProductStatsProps): ReactNode {
+ // Handle case where stats is null, undefined, or empty
+ if (!stats || stats.length === 0) {
+ return {'No statistics available'}
+ }
- return (
-
- {stats.map(stat => (
-
-
- {stat.value}
-
-
{stat.title}
-
- ))}
-
- )
+ return (
+
+ {stats.map((stat) => (
+
+
+ {stat.value}
+
+
{stat.title}
+
+ ))}
+
+ )
}
diff --git a/app/[lang]/(core-products)/_components/README.md b/app/[lang]/(core-products)/_components/README.md
index 9581873..c992c35 100644
--- a/app/[lang]/(core-products)/_components/README.md
+++ b/app/[lang]/(core-products)/_components/README.md
@@ -48,4 +48,4 @@ When using or adding to this directory:
- **Product Data**: The ProductFetcher handles data retrieval for product information
- **Hero Components**: Multiple hero variants exist for different product types
- **Stats Display**: ProductStats presents metrics in a standardized format
-- **Image Handling**: BackgroundImage provides optimized image loading
\ No newline at end of file
+- **Image Handling**: BackgroundImage provides optimized image loading
diff --git a/app/[lang]/(core-products)/_components/TradeHero.tsx b/app/[lang]/(core-products)/_components/TradeHero.tsx
index cca8253..78ad47b 100644
--- a/app/[lang]/(core-products)/_components/TradeHero.tsx
+++ b/app/[lang]/(core-products)/_components/TradeHero.tsx
@@ -1,6 +1,6 @@
'use client'
-import {Button} from '@/app/[lang]/_components/Button'
+import { Button } from '@/app/[lang]/_components/Button'
/************************************************************************************************
** TradeHero Component:
**
@@ -19,68 +19,64 @@ import {Button} from '@/app/[lang]/_components/Button'
** - Requires 'use client' directive for client-side interactivity
************************************************************************************************/
-import type {TButton} from '@/app/[lang]/_components/strapi/types'
-import type {ReactNode} from 'react'
+import type { TButton } from '@/app/[lang]/_components/strapi/types'
+import type { ReactNode } from 'react'
type TTradeHeroProps = {
- title: string;
- description: string;
- buttonCta: TButton;
- imageUrl: string;
-};
+ title: string
+ description: string
+ buttonCta: TButton
+ imageUrl: string
+}
-export function TradeHero({title, description, buttonCta, imageUrl}: TTradeHeroProps): ReactNode {
- // Handle button click to open URL in new tab with security precautions
- const handleButtonClick = (): void => {
- if (buttonCta?.url) {
- window.open(buttonCta.url, '_blank', 'noopener,noreferrer')
- }
- }
+export function TradeHero({ title, description, buttonCta, imageUrl }: TTradeHeroProps): ReactNode {
+ // Handle button click to open URL in new tab with security precautions
+ const handleButtonClick = (): void => {
+ if (buttonCta?.url) {
+ window.open(buttonCta.url, '_blank', 'noopener,noreferrer')
+ }
+ }
- return (
-
-
- {/* Title, description and CTA button */}
-
-
- {title}
-
-
-
{description}
- {buttonCta && (
-
- )}
-
-
+ return (
+
+
+ {/* Title, description and CTA button */}
+
+
+ {title}
+
+
+
{description}
+ {buttonCta && (
+
+ )}
+
+
- {/* Featured image with responsive display */}
-
-
-
-
-
-
-
-
- )
+ {/* Featured image with responsive display */}
+
+
+
+
+
+
+
+
+ )
}
diff --git a/app/[lang]/(core-products)/_components/constants.ts b/app/[lang]/(core-products)/_components/constants.ts
index 7850432..bca2029 100644
--- a/app/[lang]/(core-products)/_components/constants.ts
+++ b/app/[lang]/(core-products)/_components/constants.ts
@@ -19,28 +19,28 @@
* Contains display text, button text, and target URLs
*/
export const PRODUCT_FOOTER_CONFIGS = {
- 'defi-wallet': {
- tag: 'ShapeShift DeFi wallet',
- title: 'Everything you need in one place.',
- buttonText: 'Get started',
- href: 'https://app.shapeshift.com'
- },
- earn: {
- tag: 'Earn with ShapeShift',
- title: 'Everything you need in one place.',
- buttonText: 'Start Earning',
- href: 'https://app.shapeshift.com/#/earn'
- },
- trade: {
- tag: 'Trade with ShapeShift',
- title: 'Everything you need in one place.',
- buttonText: 'Start Trading',
- href: 'https://app.shapeshift.com/'
- },
- 'mobile-app': {
- tag: 'ShapeShift mobile app',
- title: 'Everything you need in one place.',
- buttonText: 'Start Earning',
- href: 'https://app.shapeshift.com/#/earn'
- }
+ 'defi-wallet': {
+ tag: 'ShapeShift DeFi wallet',
+ title: 'Everything you need in one place.',
+ buttonText: 'Get started',
+ href: 'https://app.shapeshift.com',
+ },
+ earn: {
+ tag: 'Earn with ShapeShift',
+ title: 'Everything you need in one place.',
+ buttonText: 'Start Earning',
+ href: 'https://app.shapeshift.com/#/earn',
+ },
+ trade: {
+ tag: 'Trade with ShapeShift',
+ title: 'Everything you need in one place.',
+ buttonText: 'Start Trading',
+ href: 'https://app.shapeshift.com/',
+ },
+ 'mobile-app': {
+ tag: 'ShapeShift mobile app',
+ title: 'Everything you need in one place.',
+ buttonText: 'Start Earning',
+ href: 'https://app.shapeshift.com/#/earn',
+ },
}
diff --git a/app/[lang]/(core-products)/_components/fetchUtils.ts b/app/[lang]/(core-products)/_components/fetchUtils.ts
index 2381ea6..6f643dd 100644
--- a/app/[lang]/(core-products)/_components/fetchUtils.ts
+++ b/app/[lang]/(core-products)/_components/fetchUtils.ts
@@ -7,29 +7,29 @@
* @returns Properly typed response data or null on error
************************************************************************************************/
export async function fetchWithErrorHandling(
- endpoint: string,
- queryParams: string,
- error: string
+ endpoint: string,
+ queryParams: string,
+ 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
- }
- })
+ 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
+ },
+ })
- if (!response.ok) {
- console.error(`Failed to fetch ${error}: Status ${response.status}`)
- return null
- }
+ if (!response.ok) {
+ console.error(`Failed to fetch ${error}: Status ${response.status}`)
+ return null
+ }
- const data = await response.json()
- return data.data
- } catch (error) {
- console.error(`Error fetching ${error}:`, error instanceof Error ? error.message : String(error))
- return null
- }
+ const data = await response.json()
+ return data.data
+ } catch (error) {
+ console.error(`Error fetching ${error}:`, error instanceof Error ? error.message : String(error))
+ return null
+ }
}
diff --git a/app/[lang]/(core-products)/defi-wallet/page.tsx b/app/[lang]/(core-products)/defi-wallet/page.tsx
index 440b91d..b1ec4bb 100644
--- a/app/[lang]/(core-products)/defi-wallet/page.tsx
+++ b/app/[lang]/(core-products)/defi-wallet/page.tsx
@@ -15,113 +15,111 @@
** - Includes text content, button configurations, and images
************************************************************************************************/
-import {notFound} from 'next/navigation'
+import { notFound } from 'next/navigation'
import Script from 'next/script'
-import {Card} from '@/app/[lang]/_components/strapi/cards-row/Card'
+import { Card } from '@/app/[lang]/_components/strapi/cards-row/Card'
import CardsRow from '@/app/[lang]/_components/strapi/cards-row/CardsRow'
-import {generateProductSchema} from '@/app/[lang]/_utils/schema'
+import { getStrapiImageUrl } from '@/app/[lang]/_utils/query'
+import { generateProductSchema } from '@/app/[lang]/_utils/schema'
-import {BackgroundImage} from '../_components/BackgroundImage'
-import {fetchDeFiWalletPage} from '../_components/ProductFetcher'
-import {ProductFooterBanner} from '../_components/ProductFooterBanner'
-import {ProductHero} from '../_components/ProductHero'
+import { BackgroundImage } from '../_components/BackgroundImage'
+import { fetchDeFiWalletPage } from '../_components/ProductFetcher'
+import { ProductFooterBanner } from '../_components/ProductFooterBanner'
+import { ProductHero } from '../_components/ProductHero'
-import type {TCard} from '@/app/[lang]/_components/strapi/types'
-import type {Metadata} from 'next'
-import type {ReactNode} from 'react'
+import type { TCard } from '@/app/[lang]/_components/strapi/types'
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
// Generate metadata for SEO
export async function generateMetadata(): Promise {
- const page = await fetchDeFiWalletPage()
-
- if (!page) {
- return {}
- }
-
- return {
- title: page.title,
- description: page.description,
- openGraph: {
- title: page.title,
- description: page.description,
- type: 'website',
- images: [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}${page.featuredImg.url}`,
- width: 1200,
- height: 630,
- alt: page.title
- }
- ]
- },
- twitter: {
- card: 'summary_large_image',
- title: page.title,
- description: page.description,
- images: [`${process.env.NEXT_PUBLIC_STRAPI_URL}${page.featuredImg.url}`]
- }
- }
+ const page = await fetchDeFiWalletPage()
+
+ if (!page) {
+ return {}
+ }
+
+ return {
+ title: page.title,
+ description: page.description,
+ openGraph: {
+ title: page.title,
+ description: page.description,
+ type: 'website',
+ images: [
+ {
+ url: getStrapiImageUrl(page.featuredImg.url),
+ width: 1200,
+ height: 630,
+ alt: page.title,
+ },
+ ],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: page.title,
+ description: page.description,
+ images: [getStrapiImageUrl(page.featuredImg.url)],
+ },
+ }
}
export default async function DeFiWalletPage(): Promise {
- // Fetch page data from Strapi CMS
- const page = await fetchDeFiWalletPage()
-
- // Handle case where page data is not found
- if (!page) {
- console.error('DeFi Wallet page data not found')
- return notFound()
- }
-
- // Generate structured data for product
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://shapeshift.com'
- const pageURL = `${baseUrl}/defi-wallet`
-
- // Map card data to features format for schema
- const features = page.cardsRow.cards.map(card => ({
- title: card.title,
- description: card.description
- }))
-
- // Generate product schema
- const productSchema = generateProductSchema({
- title: page.title,
- description: page.description,
- featuredImage: `${process.env.NEXT_PUBLIC_STRAPI_URL}${page.featuredImg.url}`,
- pageURL,
- features
- })
-
- return (
-
- {/* Add structured data */}
-
-
- {/* Background image (desktop only) */}
-
-
- {/* Hero section with title, description and CTA */}
-
-
- {/* Feature cards section */}
- }
- />
-
- {/* Footer banner with CTA */}
-
-
- )
+ // Fetch page data from Strapi CMS
+ const page = await fetchDeFiWalletPage()
+
+ // Handle case where page data is not found
+ if (!page) {
+ console.error('DeFi Wallet page data not found')
+ return notFound()
+ }
+
+ // Generate structured data for product
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://shapeshift.com'
+ const pageURL = `${baseUrl}/defi-wallet`
+
+ // Map card data to features format for schema
+ const features = page.cardsRow.cards.map((card) => ({
+ title: card.title,
+ description: card.description,
+ }))
+
+ // Generate product schema
+ const productSchema = generateProductSchema({
+ title: page.title,
+ description: page.description,
+ featuredImage: getStrapiImageUrl(page.featuredImg.url),
+ pageURL,
+ features,
+ })
+
+ return (
+
+ {/* Add structured data */}
+
+
+ {/* Background image (desktop only) */}
+
+
+ {/* Hero section with title, description and CTA */}
+
+
+ {/* Feature cards section */}
+ } />
+
+ {/* Footer banner with CTA */}
+
+
+ )
}
diff --git a/app/[lang]/(core-products)/earn/page.tsx b/app/[lang]/(core-products)/earn/page.tsx
index f2df6f2..eedc1dc 100644
--- a/app/[lang]/(core-products)/earn/page.tsx
+++ b/app/[lang]/(core-products)/earn/page.tsx
@@ -15,108 +15,109 @@
** - Includes text content, button configurations, and images
************************************************************************************************/
-import {notFound} from 'next/navigation'
+import { notFound } from 'next/navigation'
import Script from 'next/script'
import Grid from '@/app/[lang]/_components/strapi/products/Grid'
-import {generateProductSchema} from '@/app/[lang]/_utils/schema'
+import { getStrapiImageUrl } from '@/app/[lang]/_utils/query'
+import { generateProductSchema } from '@/app/[lang]/_utils/schema'
-import {BackgroundImage} from '../_components/BackgroundImage'
-import {fetchEarnPage} from '../_components/ProductFetcher'
-import {ProductFooterBanner} from '../_components/ProductFooterBanner'
-import {ProductHero} from '../_components/ProductHero'
+import { BackgroundImage } from '../_components/BackgroundImage'
+import { fetchEarnPage } from '../_components/ProductFetcher'
+import { ProductFooterBanner } from '../_components/ProductFooterBanner'
+import { ProductHero } from '../_components/ProductHero'
-import type {Metadata} from 'next'
-import type {ReactNode} from 'react'
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
// Generate metadata for SEO
export async function generateMetadata(): Promise {
- const page = await fetchEarnPage()
-
- if (!page) {
- return {}
- }
-
- return {
- title: page.title,
- description: page.description,
- openGraph: {
- title: page.title,
- description: page.description,
- type: 'website',
- images: [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}${page.featuredImg.url}`,
- width: 1200,
- height: 630,
- alt: page.title
- }
- ]
- },
- twitter: {
- card: 'summary_large_image',
- title: page.title,
- description: page.description,
- images: [`${process.env.NEXT_PUBLIC_STRAPI_URL}${page.featuredImg.url}`]
- }
- }
+ const page = await fetchEarnPage()
+
+ if (!page) {
+ return {}
+ }
+
+ return {
+ title: page.title,
+ description: page.description,
+ openGraph: {
+ title: page.title,
+ description: page.description,
+ type: 'website',
+ images: [
+ {
+ url: getStrapiImageUrl(page.featuredImg.url),
+ width: 1200,
+ height: 630,
+ alt: page.title,
+ },
+ ],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: page.title,
+ description: page.description,
+ images: [getStrapiImageUrl(page.featuredImg.url)],
+ },
+ }
}
export default async function EarnPage(): Promise {
- // Fetch page data from Strapi CMS
- const page = await fetchEarnPage()
-
- // Handle case where page data is not found
- if (!page) {
- console.error('Earn page data not found')
- return notFound()
- }
-
- // Generate structured data for product
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://shapeshift.com'
- const pageURL = `${baseUrl}/earn`
-
- // Map grid card data to features format for schema
- const features = page.grid.card.map(card => ({
- title: card.title,
- description: card.description
- }))
-
- // Generate product schema
- const productSchema = generateProductSchema({
- title: page.title,
- description: page.description,
- featuredImage: `${process.env.NEXT_PUBLIC_STRAPI_URL}${page.featuredImg.url}`,
- pageURL,
- features
- })
-
- return (
-
- {/* Add structured data */}
-
-
- {/* Background image (desktop only) */}
-
-
- {/* Hero section with title, description and CTA */}
-
-
- {/* Grid of earning opportunities */}
-
-
- {/* Footer banner with CTA */}
-
-
- )
+ // Fetch page data from Strapi CMS
+ const page = await fetchEarnPage()
+
+ // Handle case where page data is not found
+ if (!page) {
+ console.error('Earn page data not found')
+ return notFound()
+ }
+
+ // Generate structured data for product
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://shapeshift.com'
+ const pageURL = `${baseUrl}/earn`
+
+ // Map grid card data to features format for schema
+ const features = page.grid.card.map((card) => ({
+ title: card.title,
+ description: card.description,
+ }))
+
+ // Generate product schema
+ const productSchema = generateProductSchema({
+ title: page.title,
+ description: page.description,
+ featuredImage: getStrapiImageUrl(page.featuredImg.url),
+ pageURL,
+ features,
+ })
+
+ return (
+
+ {/* Add structured data */}
+
+
+ {/* Background image (desktop only) */}
+
+
+ {/* Hero section with title, description and CTA */}
+
+
+ {/* Grid of earning opportunities */}
+
+
+ {/* Footer banner with CTA */}
+
+
+ )
}
diff --git a/app/[lang]/(core-products)/mobile-app/page.tsx b/app/[lang]/(core-products)/mobile-app/page.tsx
index 2625306..c0268b6 100644
--- a/app/[lang]/(core-products)/mobile-app/page.tsx
+++ b/app/[lang]/(core-products)/mobile-app/page.tsx
@@ -15,109 +15,107 @@
** - Includes text content, download buttons, and images
************************************************************************************************/
-import {notFound} from 'next/navigation'
+import { notFound } from 'next/navigation'
import Script from 'next/script'
import GridLadder from '@/app/[lang]/_components/strapi/products/GridLadder'
-import {generateProductSchema} from '@/app/[lang]/_utils/schema'
+import { getStrapiImageUrl } from '@/app/[lang]/_utils/query'
+import { generateProductSchema } from '@/app/[lang]/_utils/schema'
-import {BackgroundImage} from '../_components/BackgroundImage'
-import {DownloadButtons} from '../_components/DownloadButtons'
-import {fetchMobileAppPage} from '../_components/ProductFetcher'
-import {ProductFooterBanner} from '../_components/ProductFooterBanner'
-import {ProductHero} from '../_components/ProductHero'
+import { BackgroundImage } from '../_components/BackgroundImage'
+import { DownloadButtons } from '../_components/DownloadButtons'
+import { fetchMobileAppPage } from '../_components/ProductFetcher'
+import { ProductFooterBanner } from '../_components/ProductFooterBanner'
+import { ProductHero } from '../_components/ProductHero'
-import type {Metadata} from 'next'
-import type {ReactNode} from 'react'
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
// Generate metadata for SEO
export async function generateMetadata(): Promise {
- const page = await fetchMobileAppPage()
-
- if (!page) {
- return {}
- }
-
- return {
- title: page.title,
- description: page.description,
- openGraph: {
- title: page.title,
- description: page.description,
- type: 'website',
- images: [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}${page.featuredImg.url}`,
- width: 1200,
- height: 630,
- alt: page.title
- }
- ]
- },
- twitter: {
- card: 'summary_large_image',
- title: page.title,
- description: page.description,
- images: [`${process.env.NEXT_PUBLIC_STRAPI_URL}${page.featuredImg.url}`]
- }
- }
+ const page = await fetchMobileAppPage()
+
+ if (!page) {
+ return {}
+ }
+
+ return {
+ title: page.title,
+ description: page.description,
+ openGraph: {
+ title: page.title,
+ description: page.description,
+ type: 'website',
+ images: [
+ {
+ url: getStrapiImageUrl(page.featuredImg.url),
+ width: 1200,
+ height: 630,
+ alt: page.title,
+ },
+ ],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: page.title,
+ description: page.description,
+ images: [getStrapiImageUrl(page.featuredImg.url)],
+ },
+ }
}
export default async function MobileAppPage(): Promise {
- // Fetch page data from Strapi CMS
- const page = await fetchMobileAppPage()
-
- // Handle case where page data is not found
- if (!page) {
- console.error('Mobile App page data not found')
- return notFound()
- }
-
- // Generate structured data for product
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://shapeshift.com'
- const pageURL = `${baseUrl}/mobile-app`
-
- // Map ladder step data to features format for schema
- const features = page.gridLadder.steps.map(step => ({
- title: step.title,
- description: step.description
- }))
-
- // Generate product schema with app-specific properties
- const productSchema = generateProductSchema({
- title: page.title,
- description: page.description,
- featuredImage: `${process.env.NEXT_PUBLIC_STRAPI_URL}${page.featuredImg.url}`,
- pageURL,
- features
- })
-
- return (
-
- {/* Add structured data */}
-
-
- {/* Background image (desktop only) */}
-
-
- {/* Hero section with title, description and download buttons */}
-
-
-
-
- {/* Step-by-step ladder grid */}
-
-
- {/* Footer banner with app store links */}
-
-
- )
+ // Fetch page data from Strapi CMS
+ const page = await fetchMobileAppPage()
+
+ // Handle case where page data is not found
+ if (!page) {
+ console.error('Mobile App page data not found')
+ return notFound()
+ }
+
+ // Generate structured data for product
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://shapeshift.com'
+ const pageURL = `${baseUrl}/mobile-app`
+
+ // Map ladder step data to features format for schema
+ const features = page.gridLadder.steps.map((step) => ({
+ title: step.title,
+ description: step.description,
+ }))
+
+ // Generate product schema with app-specific properties
+ const productSchema = generateProductSchema({
+ title: page.title,
+ description: page.description,
+ featuredImage: getStrapiImageUrl(page.featuredImg.url),
+ pageURL,
+ features,
+ })
+
+ return (
+
+ {/* Add structured data */}
+
+
+ {/* Background image (desktop only) */}
+
+
+ {/* Hero section with title, description and download buttons */}
+
+
+
+
+ {/* Step-by-step ladder grid */}
+
+
+ {/* Footer banner with app store links */}
+
+
+ )
}
diff --git a/app/[lang]/(core-products)/trade/page.tsx b/app/[lang]/(core-products)/trade/page.tsx
index 5d30ec6..0d3df1d 100644
--- a/app/[lang]/(core-products)/trade/page.tsx
+++ b/app/[lang]/(core-products)/trade/page.tsx
@@ -17,121 +17,119 @@
** - Includes text content, button configurations, statistics, and images
************************************************************************************************/
-import {notFound} from 'next/navigation'
+import { notFound } from 'next/navigation'
import Script from 'next/script'
-import {Card} from '@/app/[lang]/_components/strapi/cards-row/Card'
+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 {generateProductSchema} from '@/app/[lang]/_utils/schema'
+import { getStrapiImageUrl } from '@/app/[lang]/_utils/query'
+import { generateProductSchema } from '@/app/[lang]/_utils/schema'
-import {BackgroundImage} from '../_components/BackgroundImage'
-import {fetchTradePage} from '../_components/ProductFetcher'
-import {ProductFooterBanner} from '../_components/ProductFooterBanner'
-import {ProductStats} from '../_components/ProductStats'
-import {TradeHero} from '../_components/TradeHero'
+import { BackgroundImage } from '../_components/BackgroundImage'
+import { fetchTradePage } from '../_components/ProductFetcher'
+import { ProductFooterBanner } from '../_components/ProductFooterBanner'
+import { ProductStats } from '../_components/ProductStats'
+import { TradeHero } from '../_components/TradeHero'
-import type {TCard} from '@/app/[lang]/_components/strapi/types'
-import type {Metadata} from 'next'
-import type {ReactNode} from 'react'
+import type { TCard } from '@/app/[lang]/_components/strapi/types'
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
// Generate metadata for SEO
export async function generateMetadata(): Promise {
- const page = await fetchTradePage()
-
- if (!page) {
- return {}
- }
-
- return {
- title: page.title,
- description: page.description,
- openGraph: {
- title: page.title,
- description: page.description,
- type: 'website',
- images: [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}${page.featuredImg.url}`,
- width: 1200,
- height: 630,
- alt: page.title
- }
- ]
- },
- twitter: {
- card: 'summary_large_image',
- title: page.title,
- description: page.description,
- images: [`${process.env.NEXT_PUBLIC_STRAPI_URL}${page.featuredImg.url}`]
- }
- }
+ const page = await fetchTradePage()
+
+ if (!page) {
+ return {}
+ }
+
+ return {
+ title: page.title,
+ description: page.description,
+ openGraph: {
+ title: page.title,
+ description: page.description,
+ type: 'website',
+ images: [
+ {
+ url: getStrapiImageUrl(page.featuredImg.url),
+ width: 1200,
+ height: 630,
+ alt: page.title,
+ },
+ ],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: page.title,
+ description: page.description,
+ images: [getStrapiImageUrl(page.featuredImg.url)],
+ },
+ }
}
export default async function TradePage(): Promise {
- // Fetch page data from Strapi CMS
- const page = await fetchTradePage()
-
- // Handle case where page data is not found
- if (!page) {
- console.error('Trade page data not found')
- return notFound()
- }
-
- // Generate structured data for product
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://shapeshift.com'
- const pageURL = `${baseUrl}/trade`
-
- // Map card data to features format for schema
- const features = page.cardsRow.cards.map(card => ({
- title: card.title,
- description: card.description
- }))
-
- // Generate product schema
- const productSchema = generateProductSchema({
- title: page.title,
- description: page.description,
- featuredImage: `${process.env.NEXT_PUBLIC_STRAPI_URL}${page.featuredImg.url}`,
- pageURL,
- features
- })
-
- return (
-
- {/* Add structured data */}
-
-
- {/* Background image (desktop only) */}
-
-
- {/* Hero section with client-side interactivity for button */}
-
-
- {/* Feature cards section */}
- }
- />
-
- {/* Displaced grid layout highlighting additional features */}
-
-
- {/* Stats section */}
-
-
- {/* Footer banner with CTA */}
-
-
- )
+ // Fetch page data from Strapi CMS
+ const page = await fetchTradePage()
+
+ // Handle case where page data is not found
+ if (!page) {
+ console.error('Trade page data not found')
+ return notFound()
+ }
+
+ // Generate structured data for product
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://shapeshift.com'
+ const pageURL = `${baseUrl}/trade`
+
+ // Map card data to features format for schema
+ const features = page.cardsRow.cards.map((card) => ({
+ title: card.title,
+ description: card.description,
+ }))
+
+ // Generate product schema
+ const productSchema = generateProductSchema({
+ title: page.title,
+ description: page.description,
+ featuredImage: getStrapiImageUrl(page.featuredImg.url),
+ pageURL,
+ features,
+ })
+
+ return (
+
+ {/* Add structured data */}
+
+
+ {/* Background image (desktop only) */}
+
+
+ {/* Hero section with client-side interactivity for button */}
+
+
+ {/* Feature cards section */}
+ } />
+
+ {/* Displaced grid layout highlighting additional features */}
+
+
+ {/* Stats section */}
+
+
+ {/* Footer banner with CTA */}
+
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/ChainList.tsx b/app/[lang]/(resources)/_components/ChainList.tsx
index f4a8904..2b911d2 100644
--- a/app/[lang]/(resources)/_components/ChainList.tsx
+++ b/app/[lang]/(resources)/_components/ChainList.tsx
@@ -14,44 +14,42 @@
** - Pass chains data and optional loading state
************************************************************************************************/
-import {ResourceCard} from '@/app/[lang]/(resources)/_components/ResourceCard'
-import {ResourceGrid} from '@/app/[lang]/(resources)/_components/ResourceGrid'
+import { ResourceCard } from '@/app/[lang]/(resources)/_components/ResourceCard'
+import { ResourceGrid } from '@/app/[lang]/(resources)/_components/ResourceGrid'
-import type {TSupportedChainData} from '@/app/[lang]/_components/strapi/types'
-import type {ReactNode} from 'react'
+import type { TSupportedChainData } from '@/app/[lang]/_components/strapi/types'
+import type { ReactNode } from 'react'
type TChainListProps = {
- chains: TSupportedChainData[] | null;
- isLoading?: boolean;
- className?: string;
- isSearchQuery?: boolean;
-};
+ chains: TSupportedChainData[] | null
+ isLoading?: boolean
+ className?: string
+ isSearchQuery?: boolean
+}
-export function ChainList({chains, isLoading, className, isSearchQuery}: TChainListProps): ReactNode {
- return (
- (
-
- )}
- />
- )
+export function ChainList({ chains, isLoading, className, isSearchQuery }: TChainListProps): ReactNode {
+ return (
+ (
+
+ )}
+ />
+ )
}
diff --git a/app/[lang]/(resources)/_components/DiscoverFeature.tsx b/app/[lang]/(resources)/_components/DiscoverFeature.tsx
index a3a92a2..0b4a51a 100644
--- a/app/[lang]/(resources)/_components/DiscoverFeature.tsx
+++ b/app/[lang]/(resources)/_components/DiscoverFeature.tsx
@@ -19,122 +19,115 @@
import Image from 'next/image'
-import {LocalizedLink} from '@/app/[lang]/_components/LocalizedLink'
-import {cl} from '@/app/[lang]/_utils/cl'
+import { LocalizedLink } from '@/app/[lang]/_components/LocalizedLink'
+import { cl } from '@/app/[lang]/_utils/cl'
+import { getStrapiImageUrl } from '@/app/[lang]/_utils/query'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
type TFeatureItem = {
- id?: number
- title: string
- description: string
- image?: {
- url: string
- width: number
- height: number
- alt?: string
- }
- buttonCta?: {
- title: string
- url: string
- }
+ id?: number
+ title: string
+ description: string
+ image?: {
+ url: string
+ width: number
+ height: number
+ alt?: string
+ }
+ buttonCta?: {
+ title: string
+ url: string
+ }
}
type TDiscoverFeatureProps = {
- features: TFeatureItem[]
- title?: string
- description?: string
- columns?: 2 | 3 | 4
- className?: string
+ features: TFeatureItem[]
+ title?: string
+ description?: string
+ columns?: 2 | 3 | 4
+ className?: string
}
export function DiscoverFeature({
- features,
- title,
- description,
- columns = 3,
- className
+ features,
+ title,
+ description,
+ columns = 3,
+ className,
}: TDiscoverFeatureProps): ReactNode {
- if (!features || features.length === 0) {
- return null
- }
+ if (!features || features.length === 0) {
+ return null
+ }
- // Get the grid columns class based on the columns prop
- const gridColumnsClass = {
- 2: 'md:grid-cols-2',
- 3: 'md:grid-cols-2 lg:grid-cols-3',
- 4: 'md:grid-cols-2 lg:grid-cols-4'
- }[columns]
+ // Get the grid columns class based on the columns prop
+ const gridColumnsClass = {
+ 2: 'md:grid-cols-2',
+ 3: 'md:grid-cols-2 lg:grid-cols-3',
+ 4: 'md:grid-cols-2 lg:grid-cols-4',
+ }[columns]
- return (
-
- {/* Optional title and description */}
- {title && (
-
-
- {title}
-
- {description &&
{description}
}
-
- )}
+ return (
+
+ {/* Optional title and description */}
+ {title && (
+
+
+ {title}
+
+ {description &&
{description}
}
+
+ )}
- {/* Features grid */}
-
- {features.map((feature, index) => (
-
- {/* Feature image if provided */}
- {feature.image?.url && (
-
-
-
- )}
+ {/* Features grid */}
+
+ {features.map((feature, index) => (
+
+ {/* Feature image if provided */}
+ {feature.image?.url && (
+
+
+
+ )}
- {/* Feature title and description */}
-
{feature.title}
-
{feature.description}
- {/* CTA under description (only if provided) */}
- {feature.buttonCta?.url && feature.buttonCta?.title ? (
-
- {feature.buttonCta.url.startsWith('/') ? (
-
- {feature.buttonCta.title}
-
- ) : /^(https?:)\/\//i.test(feature.buttonCta.url) ? (
-
- {feature.buttonCta.title}
-
- ) : null}
-
- ) : null}
-
- ))}
-
-
- )
+ {/* Feature title and description */}
+
{feature.title}
+
{feature.description}
+ {/* CTA under description (only if provided) */}
+ {feature.buttonCta?.url && feature.buttonCta?.title ? (
+
+ {feature.buttonCta.url.startsWith('/') ? (
+
+ {feature.buttonCta.title}
+
+ ) : /^(https?:)\/\//i.test(feature.buttonCta.url) ? (
+
+ {feature.buttonCta.title}
+
+ ) : null}
+
+ ) : null}
+
+ ))}
+
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/FAQContent.tsx b/app/[lang]/(resources)/_components/FAQContent.tsx
index 86ddd05..5d814ec 100644
--- a/app/[lang]/(resources)/_components/FAQContent.tsx
+++ b/app/[lang]/(resources)/_components/FAQContent.tsx
@@ -19,170 +19,162 @@
'use client'
-import {useEffect, useRef, useState} from 'react'
+import { useEffect, useRef, useState } from 'react'
-import {Banner} from '@/app/[lang]/_components/Banner'
-import {QuestionSection} from '@/app/[lang]/_components/QuestionSection'
-import {RESOURCES_DICT} from '@/app/[lang]/_utils/dictionary/resources'
+import { Banner } from '@/app/[lang]/_components/Banner'
+import { QuestionSection } from '@/app/[lang]/_components/QuestionSection'
+import { RESOURCES_DICT } from '@/app/[lang]/_utils/dictionary/resources'
-import {FAQNavigation} from './FAQNavigation'
+import { FAQNavigation } from './FAQNavigation'
-import type {TFaqData, TFaqSection} from '@/app/[lang]/_components/strapi/types'
-import type {ReactNode} from 'react'
+import type { TFaqData, TFaqSection } from '@/app/[lang]/_components/strapi/types'
+import type { ReactNode } from 'react'
type TFAQContentProps = {
- faqData: TFaqData;
-};
-
-export function FAQContent({faqData}: TFAQContentProps): ReactNode {
- const [activeSection, setActiveSection] = useState('')
- const sectionRefs = useRef>({})
- const isManualScrolling = useRef(false)
- const rafId = useRef()
-
- /************************************************************************************************
- ** Scroll Handling
- **
- ** Detects which section is currently in view during scrolling
- ** Uses requestAnimationFrame for performance and prevents interference during manual scrolling
- ************************************************************************************************/
- useEffect(() => {
- const handleScroll = (): void => {
- // Skip scroll handling when user has manually clicked a section
- if (isManualScrolling.current) {
- return
- }
-
- // Cancel any pending animation frame to avoid rapid updates
- if (rafId.current) {
- cancelAnimationFrame(rafId.current)
- }
-
- // Use requestAnimationFrame for better performance
- rafId.current = requestAnimationFrame(() => {
- // Calculate positions of all sections
- const sectionPositions = Object.entries(sectionRefs.current).map(([title, element]) => ({
- title,
- top: element?.getBoundingClientRect().top ?? 0
- }))
-
- // Find which section is closest to our target position (160px from top)
- // This accounts for the header height and provides a good UX
- const closestSection = sectionPositions.reduce(
- (closest, current) => {
- const currentDistance = Math.abs(current.top - 160)
- const closestDistance = Math.abs(closest.top - 160)
- return currentDistance < closestDistance ? current : closest
- },
- sectionPositions[0] || {title: '', top: 0}
- )
-
- // Update the active section
- setActiveSection(closestSection.title)
- })
- }
-
- // Add scroll event listener
- window.addEventListener('scroll', handleScroll)
-
- // Initialize by triggering the scroll handler
- handleScroll()
-
- // Clean up event listener and any pending animation frames
- return () => {
- window.removeEventListener('scroll', handleScroll)
- if (rafId.current) {
- cancelAnimationFrame(rafId.current)
- }
- }
- }, [])
-
- /************************************************************************************************
- ** Section Navigation
- **
- ** Handles user clicking on a section in the navigation
- ** Implements smooth scrolling and temporary disables scroll tracking
- ************************************************************************************************/
- const scrollToSection = (sectionTitle: string): void => {
- const element = sectionRefs.current[sectionTitle]
- if (!element) {
- return
- }
-
- // Flag that we're manually scrolling to prevent interference
- isManualScrolling.current = true
-
- // Update active section immediately for better UX
- setActiveSection(sectionTitle)
-
- // Calculate scroll position with offset for header height
- const elementPosition = element.getBoundingClientRect().top
- const offsetPosition = elementPosition + window.scrollY - 160
-
- // Smooth scroll to the target position
- window.scrollTo({
- top: offsetPosition,
- behavior: 'smooth'
- })
-
- // Reset the manual scrolling flag after animation completes
- setTimeout(() => {
- isManualScrolling.current = false
- }, 500)
- }
-
- // Early return if data is missing
- if (!faqData?.faqSection) {
- return {RESOURCES_DICT.faq.noDataMessage}
- }
-
- return (
-
-
- {/* FAQ Title */}
-
- {faqData.title}
-
-
-
- {/* FAQ Sections */}
-
- {faqData.faqSection.map((section: TFaqSection) => (
-
{
- sectionRefs.current[section.sectionTitle] = el
- }}
- aria-labelledby={`section-${section.id}`}>
-
- {section.sectionTitle}
-
-
-
- {section.faqSectionItem.map(item => (
-
- ))}
-
-
- ))}
-
-
- {/* FAQ Navigation Sidebar */}
-
-
-
- {/* Footer Banner */}
-
-
-
- )
+ faqData: TFaqData
+}
+
+export function FAQContent({ faqData }: TFAQContentProps): ReactNode {
+ const [activeSection, setActiveSection] = useState('')
+ const sectionRefs = useRef>({})
+ const isManualScrolling = useRef(false)
+ const rafId = useRef()
+
+ /************************************************************************************************
+ ** Scroll Handling
+ **
+ ** Detects which section is currently in view during scrolling
+ ** Uses requestAnimationFrame for performance and prevents interference during manual scrolling
+ ************************************************************************************************/
+ useEffect(() => {
+ const handleScroll = (): void => {
+ // Skip scroll handling when user has manually clicked a section
+ if (isManualScrolling.current) {
+ return
+ }
+
+ // Cancel any pending animation frame to avoid rapid updates
+ if (rafId.current) {
+ cancelAnimationFrame(rafId.current)
+ }
+
+ // Use requestAnimationFrame for better performance
+ rafId.current = requestAnimationFrame(() => {
+ // Calculate positions of all sections
+ const sectionPositions = Object.entries(sectionRefs.current).map(([title, element]) => ({
+ title,
+ top: element?.getBoundingClientRect().top ?? 0,
+ }))
+
+ // Find which section is closest to our target position (160px from top)
+ // This accounts for the header height and provides a good UX
+ const closestSection = sectionPositions.reduce(
+ (closest, current) => {
+ const currentDistance = Math.abs(current.top - 160)
+ const closestDistance = Math.abs(closest.top - 160)
+ return currentDistance < closestDistance ? current : closest
+ },
+ sectionPositions[0] || { title: '', top: 0 }
+ )
+
+ // Update the active section
+ setActiveSection(closestSection.title)
+ })
+ }
+
+ // Add scroll event listener
+ window.addEventListener('scroll', handleScroll)
+
+ // Initialize by triggering the scroll handler
+ handleScroll()
+
+ // Clean up event listener and any pending animation frames
+ return () => {
+ window.removeEventListener('scroll', handleScroll)
+ if (rafId.current) {
+ cancelAnimationFrame(rafId.current)
+ }
+ }
+ }, [])
+
+ /************************************************************************************************
+ ** Section Navigation
+ **
+ ** Handles user clicking on a section in the navigation
+ ** Implements smooth scrolling and temporary disables scroll tracking
+ ************************************************************************************************/
+ const scrollToSection = (sectionTitle: string): void => {
+ const element = sectionRefs.current[sectionTitle]
+ if (!element) {
+ return
+ }
+
+ // Flag that we're manually scrolling to prevent interference
+ isManualScrolling.current = true
+
+ // Update active section immediately for better UX
+ setActiveSection(sectionTitle)
+
+ // Calculate scroll position with offset for header height
+ const elementPosition = element.getBoundingClientRect().top
+ const offsetPosition = elementPosition + window.scrollY - 160
+
+ // Smooth scroll to the target position
+ window.scrollTo({
+ top: offsetPosition,
+ behavior: 'smooth',
+ })
+
+ // Reset the manual scrolling flag after animation completes
+ setTimeout(() => {
+ isManualScrolling.current = false
+ }, 500)
+ }
+
+ // Early return if data is missing
+ if (!faqData?.faqSection) {
+ return {RESOURCES_DICT.faq.noDataMessage}
+ }
+
+ return (
+
+
+ {/* FAQ Title */}
+
+ {faqData.title}
+
+
+
+ {/* FAQ Sections */}
+
+ {faqData.faqSection.map((section: TFaqSection) => (
+
{
+ sectionRefs.current[section.sectionTitle] = el
+ }}
+ aria-labelledby={`section-${section.id}`}
+ >
+
+ {section.sectionTitle}
+
+
+
+ {section.faqSectionItem.map((item) => (
+
+ ))}
+
+
+ ))}
+
+
+ {/* FAQ Navigation Sidebar */}
+
+
+
+ {/* Footer Banner */}
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/FAQNavigation.tsx b/app/[lang]/(resources)/_components/FAQNavigation.tsx
index de1352a..b0d14a5 100644
--- a/app/[lang]/(resources)/_components/FAQNavigation.tsx
+++ b/app/[lang]/(resources)/_components/FAQNavigation.tsx
@@ -18,43 +18,42 @@
'use client'
-import {cl} from '@/app/[lang]/_utils/cl'
+import { cl } from '@/app/[lang]/_utils/cl'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
type TFAQNavigationProps = {
- sections: {
- id: number;
- sectionTitle: string;
- }[];
- activeSection: string;
- onSectionClick: (sectionTitle: string) => void;
-};
+ sections: {
+ id: number
+ sectionTitle: string
+ }[]
+ activeSection: string
+ onSectionClick: (sectionTitle: string) => void
+}
-export function FAQNavigation({sections, activeSection, onSectionClick}: TFAQNavigationProps): ReactNode {
- if (!sections || sections.length === 0) {
- return null
- }
+export function FAQNavigation({ sections, activeSection, onSectionClick }: TFAQNavigationProps): ReactNode {
+ if (!sections || sections.length === 0) {
+ return null
+ }
- return (
-
-
- {sections.map(section => (
-
- onSectionClick(section.sectionTitle)}
- className={cl(
- 'text-left text-lg transition-all hover:text-blue',
- activeSection === section.sectionTitle ? 'text-white' : 'text-gray-500'
- )}
- aria-current={activeSection === section.sectionTitle ? 'true' : 'false'}>
- {section.sectionTitle}
-
-
- ))}
-
-
- )
+ return (
+
+
+ {sections.map((section) => (
+
+ onSectionClick(section.sectionTitle)}
+ className={cl(
+ 'text-left text-lg transition-all hover:text-blue',
+ activeSection === section.sectionTitle ? 'text-white' : 'text-gray-500'
+ )}
+ aria-current={activeSection === section.sectionTitle ? 'true' : 'false'}
+ >
+ {section.sectionTitle}
+
+
+ ))}
+
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/NewsroomBreadcrumb.tsx b/app/[lang]/(resources)/_components/NewsroomBreadcrumb.tsx
index 8f3375d..4f3d2d9 100644
--- a/app/[lang]/(resources)/_components/NewsroomBreadcrumb.tsx
+++ b/app/[lang]/(resources)/_components/NewsroomBreadcrumb.tsx
@@ -1,23 +1,24 @@
'use client'
-import {usePathname} from 'next/navigation'
+import { usePathname } from 'next/navigation'
-import {LocalizedLink} from '@/app/[lang]/_components/LocalizedLink'
-import {IconBack} from '@/app/[lang]/_icons/IconBack'
-import {cl} from '@/app/[lang]/_utils/cl'
+import { LocalizedLink } from '@/app/[lang]/_components/LocalizedLink'
+import { IconBack } from '@/app/[lang]/_icons/IconBack'
+import { cl } from '@/app/[lang]/_utils/cl'
export function NewsroomBreadcrumb(): React.ReactNode {
- const pathname = usePathname()
+ const pathname = usePathname()
- return (
-
-
- {'Back to Newsroom'}
-
- )
+ return (
+
+
+ {'Back to Newsroom'}
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/NewsroomNav.tsx b/app/[lang]/(resources)/_components/NewsroomNav.tsx
index 9c76540..6895898 100644
--- a/app/[lang]/(resources)/_components/NewsroomNav.tsx
+++ b/app/[lang]/(resources)/_components/NewsroomNav.tsx
@@ -1,40 +1,40 @@
'use client'
-import {useParams, usePathname} from 'next/navigation'
+import { useParams, usePathname } from 'next/navigation'
-import {TabItem} from '@/app/[lang]/_components/TabItem'
-import {newsroomCategories, newsroomTags} from '@/app/[lang]/_utils/constants'
+import { TabItem } from '@/app/[lang]/_components/TabItem'
+import { newsroomCategories, newsroomTags } from '@/app/[lang]/_utils/constants'
export function NewsroomNav(): React.ReactNode {
- const pathname = usePathname()
- const params = useParams()
- const category = (params.category as string) || ''
- const tag = (params.tag as string) || ''
+ const pathname = usePathname()
+ const params = useParams()
+ const category = (params.category as string) || ''
+ const tag = (params.tag as string) || ''
- if (tag || pathname.includes('/tags')) {
- return (
-
- {newsroomTags.map(tab => (
-
- ))}
-
- )
- }
- return (
-
- {newsroomCategories.map(tab => (
-
- ))}
-
- )
+ if (tag || pathname.includes('/tags')) {
+ return (
+
+ {newsroomTags.map((tab) => (
+
+ ))}
+
+ )
+ }
+ return (
+
+ {newsroomCategories.map((tab) => (
+
+ ))}
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/NewsroomTitle.tsx b/app/[lang]/(resources)/_components/NewsroomTitle.tsx
index 9b373eb..4f9f838 100644
--- a/app/[lang]/(resources)/_components/NewsroomTitle.tsx
+++ b/app/[lang]/(resources)/_components/NewsroomTitle.tsx
@@ -1,46 +1,46 @@
'use client'
-import {useParams, usePathname} from 'next/navigation'
+import { useParams, usePathname } from 'next/navigation'
-import {newsroomCategoriesSlugToCategory} from '@/app/[lang]/_utils/constants'
+import { newsroomCategoriesSlugToCategory } from '@/app/[lang]/_utils/constants'
export function NewsroomTitle(): React.ReactNode {
- const pathname = usePathname()
- const params = useParams()
- const category = (params.category as string) || ''
- const tag = (params.tag as string) || ''
+ const pathname = usePathname()
+ const params = useParams()
+ const category = (params.category as string) || ''
+ const tag = (params.tag as string) || ''
- if (tag || pathname.includes('/tags')) {
- if (tag) {
- return (
-
- {'Tagged: '}
- {tag}
-
- )
- }
- return (
-
- {'All '}
- {'tags'}
-
- )
- }
+ if (tag || pathname.includes('/tags')) {
+ if (tag) {
+ return (
+
+ {'Tagged: '}
+ {tag}
+
+ )
+ }
+ return (
+
+ {'All '}
+ {'tags'}
+
+ )
+ }
- if (category) {
- return (
-
- {'Category: '}
- {newsroomCategoriesSlugToCategory(category)}
-
- )
- }
- return (
-
- {'ShapeShift'}
-
- {'Newsroom'}
- {'.'}
-
- )
+ if (category) {
+ return (
+
+ {'Category: '}
+ {newsroomCategoriesSlugToCategory(category)}
+
+ )
+ }
+ return (
+
+ {'ShapeShift'}
+
+ {'Newsroom'}
+ {'.'}
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/PostList.tsx b/app/[lang]/(resources)/_components/PostList.tsx
index 8a88090..b3b9ef7 100644
--- a/app/[lang]/(resources)/_components/PostList.tsx
+++ b/app/[lang]/(resources)/_components/PostList.tsx
@@ -18,131 +18,111 @@
** - Add custom empty state message if needed
************************************************************************************************/
-import {Fragment, useState} from 'react'
+import { Fragment, useState } from 'react'
import ReactPaginate from 'react-paginate'
-import {Banner} from '@/app/[lang]/_components/Banner'
-import {BlogPost} from '@/app/[lang]/_components/BlogPost'
-import {useFetchPosts} from '@/app/[lang]/_hooks/useFetchPosts'
-import {IconChevron} from '@/app/[lang]/_icons/IconChevron'
-import {cl} from '@/app/[lang]/_utils/cl'
+import { Banner } from '@/app/[lang]/_components/Banner'
+import { BlogPost } from '@/app/[lang]/_components/BlogPost'
+import { useFetchPosts } from '@/app/[lang]/_hooks/useFetchPosts'
+import { IconChevron } from '@/app/[lang]/_icons/IconChevron'
+import { cl } from '@/app/[lang]/_utils/cl'
-import {DEFAULT_PAGINATION} from '../_utils/constants'
+import { DEFAULT_PAGINATION } from '../_utils/constants'
-import type {TBlogPost} from '@/app/[lang]/_components/strapi/types'
-import type {ReactNode} from 'react'
+import type { TBlogPost } from '@/app/[lang]/_components/strapi/types'
+import type { ReactNode } from 'react'
type TPostListProps = {
- pageSize?: number;
- sort?: 'asc' | 'desc';
- initialPage?: number;
- populateContent?: boolean;
- cachePosts?: boolean;
- emptyMessage?: string;
- category?: string;
- tag?: string;
- gridClassName?: string;
-};
+ pageSize?: number
+ sort?: 'asc' | 'desc'
+ initialPage?: number
+ populateContent?: boolean
+ cachePosts?: boolean
+ emptyMessage?: string
+ category?: string
+ tag?: string
+ gridClassName?: string
+}
export function PostList({
- pageSize = DEFAULT_PAGINATION.PAGE_SIZE,
- sort = DEFAULT_PAGINATION.SORT,
- initialPage = DEFAULT_PAGINATION.INITIAL_PAGE,
- populateContent = true,
- cachePosts = true,
- emptyMessage = "We couldn't find any posts matching your criteria.",
- category,
- tag,
- gridClassName
+ pageSize = DEFAULT_PAGINATION.PAGE_SIZE,
+ sort = DEFAULT_PAGINATION.SORT,
+ initialPage = DEFAULT_PAGINATION.INITIAL_PAGE,
+ populateContent = true,
+ cachePosts = true,
+ emptyMessage = "We couldn't find any posts matching your criteria.",
+ category,
+ tag,
+ gridClassName,
}: TPostListProps): ReactNode {
- const [page, setPage] = useState(initialPage)
- const {posts, pagination, isLoading} = useFetchPosts({
- page,
- pageSize,
- sort,
- populateContent,
- cachePosts,
- type: category,
- tag
- })
+ const [page, setPage] = useState(initialPage)
+ const { posts, pagination, isLoading } = useFetchPosts({
+ page,
+ pageSize,
+ sort,
+ populateContent,
+ cachePosts,
+ type: category,
+ tag,
+ })
- // Loading skeleton
- if (isLoading) {
- return (
-
- {Array.from({length: pageSize}).map((_, i) => (
-
- ))}
-
- )
- }
+ // Loading skeleton
+ if (isLoading) {
+ return (
+
+ {Array.from({ length: pageSize }).map((_, i) => (
+
+ ))}
+
+ )
+ }
- return (
-
- {/* Empty state */}
- {!posts || posts.length === 0 ? (
-
- {emptyMessage}
-
- ) : (
- /* Posts grid */
-
- {posts.map((post: TBlogPost) => (
-
- ))}
-
- )}
+ return (
+
+ {/* Empty state */}
+ {!posts || posts.length === 0 ? (
+
+ {emptyMessage}
+
+ ) : (
+ /* Posts grid */
+
+ {posts.map((post: TBlogPost) => (
+
+ ))}
+
+ )}
- {/* Pagination controls */}
- {pagination && pagination.pageCount > 1 && (
- setPage(selected + 1)}
- containerClassName={'flex gap-2 items-center justify-center mb-12'}
- pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
- pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
- activeClassName={'!opacity-100'}
- previousClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === 1 ? 'opacity-20' : 'opacity-100'
- )}
- previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
- )}
- disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
- disabledLinkClassName={'cursor-not-allowed'}
- previousLabel={ }
- nextLabel={
-
- }
- aria-label={'Pagination'}
- />
- )}
+ {/* Pagination controls */}
+ {pagination && pagination.pageCount > 1 && (
+ setPage(selected + 1)}
+ containerClassName={'flex gap-2 items-center justify-center mb-12'}
+ pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
+ pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ activeClassName={'!opacity-100'}
+ previousClassName={cl('hover:opacity-100 transition-opacity', page === 1 ? 'opacity-20' : 'opacity-100')}
+ previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextClassName={cl(
+ 'hover:opacity-100 transition-opacity',
+ page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
+ )}
+ disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
+ disabledLinkClassName={'cursor-not-allowed'}
+ previousLabel={ }
+ nextLabel={ }
+ aria-label={'Pagination'}
+ />
+ )}
- {/* Banner */}
-
-
-
-
- )
+ {/* Banner */}
+
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/ProtocolAbout.tsx b/app/[lang]/(resources)/_components/ProtocolAbout.tsx
index 18bf94b..07ee5e7 100644
--- a/app/[lang]/(resources)/_components/ProtocolAbout.tsx
+++ b/app/[lang]/(resources)/_components/ProtocolAbout.tsx
@@ -1,23 +1,22 @@
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
type THeaderData = {
- name: string;
- description: string;
-};
+ name: string
+ description: string
+}
export function ProtocolAbout(data: THeaderData): ReactNode {
- return (
-
-
-
- {`What is ${data.name}`}
-
-
-
-
- )
+ return (
+
+
+ {`What is ${data.name}`}
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/ProtocolEasier.tsx b/app/[lang]/(resources)/_components/ProtocolEasier.tsx
index 69c848d..ec142eb 100644
--- a/app/[lang]/(resources)/_components/ProtocolEasier.tsx
+++ b/app/[lang]/(resources)/_components/ProtocolEasier.tsx
@@ -1,41 +1,42 @@
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
-export function ProtocolEasier({cards}: {cards: {title: string; description?: string}[]}): ReactNode {
- if (cards.length === 0) {
- return null
- }
+export function ProtocolEasier({ cards }: { cards: { title: string; description?: string }[] }): ReactNode {
+ if (cards.length === 0) {
+ return null
+ }
- return (
-
- {cards[0].title ? (
-
-
-
{cards[0]?.title}
-
{cards[0]?.description}
-
-
- ) : null}
- {cards[1].title ? (
-
-
-
{cards[1]?.title}
-
{cards[1]?.description}
-
-
- ) : null}
- {cards[2]?.title ? (
-
- ) : null}
-
- )
+ return (
+
+ {cards[0].title ? (
+
+
+
{cards[0]?.title}
+
{cards[0]?.description}
+
+
+ ) : null}
+ {cards[1].title ? (
+
+
+
{cards[1]?.title}
+
{cards[1]?.description}
+
+
+ ) : null}
+ {cards[2]?.title ? (
+
+ ) : null}
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/ProtocolFeatures.tsx b/app/[lang]/(resources)/_components/ProtocolFeatures.tsx
index b106739..2f4bca0 100644
--- a/app/[lang]/(resources)/_components/ProtocolFeatures.tsx
+++ b/app/[lang]/(resources)/_components/ProtocolFeatures.tsx
@@ -1,260 +1,215 @@
'use client'
import Image from 'next/image'
-import {useMemo} from 'react'
+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 { Button } from '@/app/[lang]/_components/Button'
+import { LocalizedLink } from '@/app/[lang]/_components/LocalizedLink'
+import { cl } from '@/app/[lang]/_utils/cl'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
-function OgPlatformFeature({description, index}: {description: string; index: number}): ReactNode {
- return (
-
-
-
-
{'OG DeFi Platforms'}
-
{description}
-
-
-
-
-
-
-
- )
+function OgPlatformFeature({ description, index }: { description: string; index: number }): ReactNode {
+ return (
+
+
+
+
{'OG DeFi Platforms'}
+
{description}
+
+
+
+
+
+
+
+ )
}
-function MobileAppFeature({index}: {index: number}): ReactNode {
- return (
-
-
-
-
{'Shift to DeFi on the go'}
-
- {'Permissionlessly track, trade, shift, and earn with your favorite chains such as '}
-
- {'Bitcoin'}
-
- {', '}
-
- {'Dogecoin'}
-
- {', '}
-
- {'Ethereum'}
-
- {', '}
-
- {'Arbitrum'}
-
- {', '}
-
- {'AVAX'}
-
- {' and more.'}
-
-
- {'Do more with your crypto when you ShapeShift.'}
-
-
-
-
-
+function MobileAppFeature({ index }: { index: number }): ReactNode {
+ return (
+
+
+
+
{'Shift to DeFi on the go'}
+
+ {'Permissionlessly track, trade, shift, and earn with your favorite chains such as '}
+
+ {'Bitcoin'}
+
+ {', '}
+
+ {'Dogecoin'}
+
+ {', '}
+
+ {'Ethereum'}
+
+ {', '}
+
+ {'Arbitrum'}
+
+ {', '}
+
+ {'AVAX'}
+
+ {' and more.'}
+
+
+ {'Do more with your crypto when you ShapeShift.'}
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
- )
+
+
+
+
+
+
+
+
+
+
+ )
}
-function GetPaidToTradeFeature({index}: {index: number}): ReactNode {
- return (
-
-
-
-
{'Get Paid to Trade'}
-
- {'Stake FOX into rFOX, get rewarded, and save when you trade.'}
-
-
{'The more you trade, the more you earn.'}
-
-
-
-
-
-
-
- )
+function GetPaidToTradeFeature({ index }: { index: number }): ReactNode {
+ return (
+
+
+
+
{'Get Paid to Trade'}
+
{'Stake FOX into rFOX, get rewarded, and save when you trade.'}
+
{'The more you trade, the more you earn.'}
+
+
+
+
+
+
+
+ )
}
-function PortalsFeature({index}: {index: number}): ReactNode {
- return (
-
-
-
-
{'Powered by Portals'}
-
{'Shifts are Powered by Portals!'}
-
- {
- 'When you Shift, you take advantage of Portals bespoke routing and optimizations to execute the best trades.'
- }
-
-
-
-
-
-
-
-
- )
+function PortalsFeature({ index }: { index: number }): ReactNode {
+ return (
+
+
+
+
{'Powered by Portals'}
+
{'Shifts are Powered by Portals!'}
+
+ {
+ 'When you Shift, you take advantage of Portals bespoke routing and optimizations to execute the best trades.'
+ }
+
+
+
+
+
+
+
+
+ )
}
-export function ProtocolFeatures({description, features}: {description: string; features: string[]}): ReactNode {
- const featuresComponents = useMemo(() => {
- const allFeatures = []
- if (features?.includes('OG DeFi Platforms')) {
- allFeatures.push(
-
- )
- }
- if (features?.includes('Shift to DeFi on the go')) {
- allFeatures.push( )
- }
- if (features?.includes('Get Paid to Trade')) {
- allFeatures.push( )
- }
- if (features?.includes('Powered by Portals')) {
- allFeatures.push( )
- }
- if (allFeatures.length === 0) {
- return null
- }
- return allFeatures
- }, [description, features])
- return (
-
- {featuresComponents}
-
- )
+export function ProtocolFeatures({ description, features }: { description: string; features: string[] }): ReactNode {
+ const featuresComponents = useMemo(() => {
+ const allFeatures = []
+ if (features?.includes('OG DeFi Platforms')) {
+ allFeatures.push( )
+ }
+ if (features?.includes('Shift to DeFi on the go')) {
+ allFeatures.push( )
+ }
+ if (features?.includes('Get Paid to Trade')) {
+ allFeatures.push( )
+ }
+ if (features?.includes('Powered by Portals')) {
+ allFeatures.push( )
+ }
+ if (allFeatures.length === 0) {
+ return null
+ }
+ return allFeatures
+ }, [description, features])
+ return (
+
+ {featuresComponents}
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/ProtocolHeader.tsx b/app/[lang]/(resources)/_components/ProtocolHeader.tsx
index 05c8fb8..53064ee 100644
--- a/app/[lang]/(resources)/_components/ProtocolHeader.tsx
+++ b/app/[lang]/(resources)/_components/ProtocolHeader.tsx
@@ -1,70 +1,53 @@
import Image from 'next/image'
-import {Fragment} from 'react'
+import { Fragment } from 'react'
-import {Button} from '@/app/[lang]/_components/Button'
-import {IconCheck} from '@/app/[lang]/_icons/IconCheck'
+import { Button } from '@/app/[lang]/_components/Button'
+import { IconCheck } from '@/app/[lang]/_icons/IconCheck'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
type THeaderData = {
- description: string;
- items: string[];
- url: string;
- width: number;
- height: number;
- name: string;
-};
+ description: string
+ items: string[]
+ url: string
+ width: number
+ height: number
+ name: string
+}
export function ProtocolHeader(data: THeaderData): ReactNode {
- return (
-
-
-
- {data.items.map(item => (
-
-
- {item}
-
- ))}
-
-
-
- {`Shift into ${data.name}`}
-
-
- {
- 'Say goodbye to multiple interfaces and hello to ShapeShift.'
- }
-
-
-
-
-
-
- )
+ return (
+
+
+
+ {data.items.map((item) => (
+
+
+ {item}
+
+ ))}
+
+
+
{`Shift into ${data.name}`}
+
+ {'Say goodbye to multiple interfaces and hello to ShapeShift.'}
+
+
+
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/ProtocolList.tsx b/app/[lang]/(resources)/_components/ProtocolList.tsx
index c25f6a2..21f4035 100644
--- a/app/[lang]/(resources)/_components/ProtocolList.tsx
+++ b/app/[lang]/(resources)/_components/ProtocolList.tsx
@@ -16,42 +16,40 @@
** - Pass protocols data and optional loading state
************************************************************************************************/
-import {ResourceCard} from '@/app/[lang]/(resources)/_components/ResourceCard'
-import {ResourceGrid} from '@/app/[lang]/(resources)/_components/ResourceGrid'
+import { ResourceCard } from '@/app/[lang]/(resources)/_components/ResourceCard'
+import { ResourceGrid } from '@/app/[lang]/(resources)/_components/ResourceGrid'
-import type {TSupportedProtocolData} from '@/app/[lang]/_components/strapi/types'
-import type {ReactNode} from 'react'
+import type { TSupportedProtocolData } from '@/app/[lang]/_components/strapi/types'
+import type { ReactNode } from 'react'
type TProtocolListProps = {
- protocols: TSupportedProtocolData[] | null;
- isLoading?: boolean;
- className?: string;
- isSearchQuery?: boolean;
-};
+ protocols: TSupportedProtocolData[] | null
+ isLoading?: boolean
+ className?: string
+ isSearchQuery?: boolean
+}
-export function ProtocolList({protocols, isLoading, className, isSearchQuery}: TProtocolListProps): ReactNode {
- return (
- (
-
- )}
- />
- )
+export function ProtocolList({ protocols, isLoading, className, isSearchQuery }: TProtocolListProps): ReactNode {
+ return (
+ (
+
+ )}
+ />
+ )
}
diff --git a/app/[lang]/(resources)/_components/README.md b/app/[lang]/(resources)/_components/README.md
index 556ecef..e99767a 100644
--- a/app/[lang]/(resources)/_components/README.md
+++ b/app/[lang]/(resources)/_components/README.md
@@ -5,12 +5,14 @@ This directory contains shared components used across the resources section of t
## Component Files
### Resource Structure Components
+
- **ResourceCard.tsx**: Card component for displaying resource items
- **ResourceGrid.tsx**: Grid layout for arranging resource cards
- **ResourceHeader.tsx**: Header component for resource pages
- **ResourceHero.tsx**: Hero component for resource section landing pages
### Blog & Newsroom Components
+
- **BlogBreadcrumb.tsx**: Breadcrumb navigation for blog pages
- **NewsroomBreadcrumb.tsx**: Breadcrumb navigation for newsroom pages
- **NewsroomNav.tsx**: Navigation component for newsroom
@@ -18,6 +20,7 @@ This directory contains shared components used across the resources section of t
- **PostList.tsx**: Component for listing blog or news posts
### Chain & Protocol Components
+
- **ChainList.tsx**: Component for displaying supported blockchain networks
- **ProtocolAbout.tsx**: About section for protocol pages
- **ProtocolEasier.tsx**: Component highlighting protocol ease of use
@@ -26,22 +29,26 @@ This directory contains shared components used across the resources section of t
- **ProtocolList.tsx**: List component for displaying protocols
### Wallet Components
+
- **SupportedWalletAccelerate.tsx**: Component for wallet acceleration features
- **SupportedWalletHeader.tsx**: Header component for wallet pages
- **SupportedWalletHero.tsx**: Hero component for wallet section
- **WalletList.tsx**: Component for displaying supported wallets
### Feature & Discovery Components
+
- **DiscoverFeature.tsx**: Component for highlighting discover features
- **SupportedChainTable.tsx**: Table component for displaying chain support details
### FAQ Components
+
- **FAQContent.tsx**: Component for rendering FAQ content
- **FAQNavigation.tsx**: Navigation component for FAQ sections
## Purpose
These components provide:
+
- Consistent UI across all resource pages
- Reusable layouts and design patterns
- Standardized presentation of various content types
@@ -50,6 +57,7 @@ These components provide:
## Usage Guidelines
When using or adding to this directory:
+
- Group related components by resource type
- Maintain consistent naming conventions
- Ensure proper props typing and documentation
@@ -62,4 +70,4 @@ When using or adding to this directory:
- Most components expect data from CMS or API sources
- Component naming reflects its primary purpose and content type
- Navigation components handle both desktop and mobile layouts
-- List and grid components handle both loading and error states
\ No newline at end of file
+- List and grid components handle both loading and error states
diff --git a/app/[lang]/(resources)/_components/ResourceCard.tsx b/app/[lang]/(resources)/_components/ResourceCard.tsx
index 8c66c67..3a1e0ef 100644
--- a/app/[lang]/(resources)/_components/ResourceCard.tsx
+++ b/app/[lang]/(resources)/_components/ResourceCard.tsx
@@ -18,97 +18,99 @@
'use client'
-import {motion} from 'framer-motion'
+import { motion } from 'framer-motion'
import Image from 'next/image'
import Link from 'next/link'
-import {cl} from '@/app/[lang]/_utils/cl'
+import { cl } from '@/app/[lang]/_utils/cl'
+import { getStrapiImageUrl } from '@/app/[lang]/_utils/query'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
const MotionLink = motion(Link)
type TResourceCardProps = {
- slug: string;
- title: string;
- description: string;
- imageUrl: string;
- imageWidth?: number;
- imageHeight?: number;
- baseURL: string;
- imagePosition?: 'center' | 'bottom';
- altText?: string;
- className?: string;
-};
+ slug: string
+ title: string
+ description: string
+ imageUrl: string
+ imageWidth?: number
+ imageHeight?: number
+ baseURL: string
+ imagePosition?: 'center' | 'bottom'
+ altText?: string
+ className?: string
+}
export function ResourceCard({
- slug,
- title,
- description,
- imageUrl,
- imageWidth = 0,
- imageHeight = 0,
- baseURL,
- imagePosition = 'center',
- altText,
- className
+ slug,
+ title,
+ description,
+ imageUrl,
+ imageWidth = 0,
+ imageHeight = 0,
+ baseURL,
+ imagePosition = 'center',
+ altText,
+ className,
}: TResourceCardProps): ReactNode {
- return (
-
- {/* Card Image */}
-
- {/* Background Image */}
-
+ return (
+
+ {/* Card Image */}
+
+ {/* Background Image */}
+
- {/* Resource Image */}
- {imageUrl ? (
-
-
-
- ) : (
-
- )}
-
+ {/* Resource Image */}
+ {imageUrl ? (
+
+
+
+ ) : (
+
+ )}
+
- {/* Card Content */}
-
-
-
{title}
-
- {description}
-
-
-
-
- )
+ {/* Card Content */}
+
+
+
{title}
+
{description}
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/ResourceGrid.tsx b/app/[lang]/(resources)/_components/ResourceGrid.tsx
index 5be8cb5..3c64455 100644
--- a/app/[lang]/(resources)/_components/ResourceGrid.tsx
+++ b/app/[lang]/(resources)/_components/ResourceGrid.tsx
@@ -16,53 +16,47 @@
** - Pass items array and renderItem function to customize display
************************************************************************************************/
-import type {ReactElement, ReactNode} from 'react'
+import type { ReactElement, ReactNode } from 'react'
type TResourceGridProps = {
- items: T[] | null | undefined;
- renderItem: (item: T, index: number) => ReactNode;
- isLoading?: boolean;
- emptyMessage?: string;
- className?: string;
-};
+ items: T[] | null | undefined
+ renderItem: (item: T, index: number) => ReactNode
+ isLoading?: boolean
+ emptyMessage?: string
+ className?: string
+}
export function ResourceGrid({
- items,
- renderItem,
- isLoading = false,
- emptyMessage = 'No items available yet.',
- className = ''
+ items,
+ renderItem,
+ isLoading = false,
+ emptyMessage = 'No items available yet.',
+ className = '',
}: TResourceGridProps): ReactNode {
- // Handle loading state
- if (isLoading) {
- return (
-
- )
- }
+ // Handle loading state
+ if (isLoading) {
+ return
+ }
- // Handle empty state
- if (!items || items.length === 0) {
- return (
-
- )
- }
+ // Handle empty state
+ if (!items || items.length === 0) {
+ return (
+
+ )
+ }
- // Render grid with items
- return (
-
-
-
- {items.map((item, index) => renderItem(item, index) as ReactElement)}
-
-
-
- )
+ // Render grid with items
+ return (
+
+
+
+ {items.map((item, index) => renderItem(item, index) as ReactElement)}
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/ResourceHeader.tsx b/app/[lang]/(resources)/_components/ResourceHeader.tsx
index 70be717..8258568 100644
--- a/app/[lang]/(resources)/_components/ResourceHeader.tsx
+++ b/app/[lang]/(resources)/_components/ResourceHeader.tsx
@@ -18,89 +18,88 @@
import Image from 'next/image'
-import {Button} from '@/app/[lang]/_components/Button'
-import {IconCheck} from '@/app/[lang]/_icons/IconCheck'
-import {cl} from '@/app/[lang]/_utils/cl'
+import { Button } from '@/app/[lang]/_components/Button'
+import { IconCheck } from '@/app/[lang]/_icons/IconCheck'
+import { cl } from '@/app/[lang]/_utils/cl'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
type TResourceHeaderProps = {
- title: string;
- description: string;
- items: string[];
- ctaButton?: {
- text: string;
- url: string;
- };
- className?: string;
- titlePrefix?: string;
- hasLogo?: boolean;
- logoUrl?: string;
- logoWidth?: number;
- logoHeight?: number;
- logoAlt?: string;
-};
+ title: string
+ description: string
+ items: string[]
+ ctaButton?: {
+ text: string
+ url: string
+ }
+ className?: string
+ titlePrefix?: string
+ hasLogo?: boolean
+ logoUrl?: string
+ logoWidth?: number
+ logoHeight?: number
+ logoAlt?: string
+}
export function ResourceHeader({
- title,
- description,
- items,
- ctaButton,
- className,
- titlePrefix,
- hasLogo = false,
- logoUrl,
- logoWidth,
- logoHeight,
- logoAlt
+ title,
+ description,
+ items,
+ ctaButton,
+ className,
+ titlePrefix,
+ hasLogo = false,
+ logoUrl,
+ logoWidth,
+ logoHeight,
+ logoAlt,
}: TResourceHeaderProps): ReactNode {
- return (
-
- {/* Feature badges - desktop only */}
-
- {items.map(item => (
-
-
- {item}
-
- ))}
-
+ return (
+
+ {/* Feature badges - desktop only */}
+
+ {items.map((item) => (
+
+
+ {item}
+
+ ))}
+
- {/* Title and description */}
-
-
- {titlePrefix ? `${titlePrefix} ${title}` : title}
-
-
- {description}
-
-
+ {/* Title and description */}
+
+
+ {titlePrefix ? `${titlePrefix} ${title}` : title}
+
+
{description}
+
- {/* Optional CTA Button */}
- {ctaButton && (
-
- )}
+ {/* Optional CTA Button */}
+ {ctaButton && (
+
+ )}
- {/* Optional Logo display */}
- {hasLogo && logoUrl && (
-
-
-
- )}
-
- )
+ {/* Optional Logo display */}
+ {hasLogo && logoUrl && (
+
+
+
+ )}
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/ResourceHero.tsx b/app/[lang]/(resources)/_components/ResourceHero.tsx
index bd45721..af9829b 100644
--- a/app/[lang]/(resources)/_components/ResourceHero.tsx
+++ b/app/[lang]/(resources)/_components/ResourceHero.tsx
@@ -18,65 +18,65 @@
import Image from 'next/image'
-import {cl} from '@/app/[lang]/_utils/cl'
+import { cl } from '@/app/[lang]/_utils/cl'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
type TResourceHeroProps = {
- imageSrc: string;
- imageAlt: string;
- className?: string;
- logoSrc?: string;
- logoAlt?: string;
- logoWidth?: number;
- logoHeight?: number;
- logoPosition?: 'left' | 'center' | 'right';
- priority?: boolean;
-};
+ imageSrc: string
+ imageAlt: string
+ className?: string
+ logoSrc?: string
+ logoAlt?: string
+ logoWidth?: number
+ logoHeight?: number
+ logoPosition?: 'left' | 'center' | 'right'
+ priority?: boolean
+}
export function ResourceHero({
- imageSrc,
- imageAlt,
- className,
- logoSrc,
- logoAlt,
- logoWidth = 256,
- logoHeight = 256,
- logoPosition = 'right',
- priority = true
+ imageSrc,
+ imageAlt,
+ className,
+ logoSrc,
+ logoAlt,
+ logoWidth = 256,
+ logoHeight = 256,
+ logoPosition = 'right',
+ priority = true,
}: TResourceHeroProps): ReactNode {
- // Calculate position classes for logo
- const logoPositionClasses = {
- left: 'justify-start pl-16',
- center: 'justify-center',
- right: 'justify-end pr-16'
- }
+ // Calculate position classes for logo
+ const logoPositionClasses = {
+ left: 'justify-start pl-16',
+ center: 'justify-center',
+ right: 'justify-end pr-16',
+ }
- return (
-
- {/* Hero background image */}
-
+ return (
+
+ {/* Hero background image */}
+
- {/* Optional logo overlay */}
- {logoSrc && (
-
-
-
- )}
-
- )
+ {/* Optional logo overlay */}
+ {logoSrc && (
+
+
+
+ )}
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/SupportArticleList.tsx b/app/[lang]/(resources)/_components/SupportArticleList.tsx
index ce9a4c9..8ca0300 100644
--- a/app/[lang]/(resources)/_components/SupportArticleList.tsx
+++ b/app/[lang]/(resources)/_components/SupportArticleList.tsx
@@ -18,129 +18,118 @@
** - Add custom empty state message if needed
************************************************************************************************/
-import {Fragment, useState} from 'react'
+import { Fragment, useState } from 'react'
import ReactPaginate from 'react-paginate'
-import {Banner} from '@/app/[lang]/_components/Banner'
-import {LocalizedLink} from '@/app/[lang]/_components/LocalizedLink'
-import {useFetchSupportArticles} from '@/app/[lang]/_hooks/useFetchSupportArticles'
-import {IconChevron} from '@/app/[lang]/_icons/IconChevron'
-import {IconDocs} from '@/app/[lang]/_icons/IconDocs'
-import {cl} from '@/app/[lang]/_utils/cl'
-import {RESOURCES_DICT} from '@/app/[lang]/_utils/dictionary/resources'
+import { Banner } from '@/app/[lang]/_components/Banner'
+import { LocalizedLink } from '@/app/[lang]/_components/LocalizedLink'
+import { useFetchSupportArticles } from '@/app/[lang]/_hooks/useFetchSupportArticles'
+import { IconChevron } from '@/app/[lang]/_icons/IconChevron'
+import { IconDocs } from '@/app/[lang]/_icons/IconDocs'
+import { cl } from '@/app/[lang]/_utils/cl'
+import { RESOURCES_DICT } from '@/app/[lang]/_utils/dictionary/resources'
-import {SupportArticleListSkeleton} from './SupportArticleListSkeleton'
-import {DEFAULT_PAGINATION} from '../_utils/constants'
+import { SupportArticleListSkeleton } from './SupportArticleListSkeleton'
+import { DEFAULT_PAGINATION } from '../_utils/constants'
-import type {TSupportArticle} from '@/app/[lang]/_components/strapi/types'
-import type {ReactNode} from 'react'
+import type { TSupportArticle } from '@/app/[lang]/_components/strapi/types'
+import type { ReactNode } from 'react'
type TSupportArticleListProps = {
- pageSize?: number;
- sort?: 'asc' | 'desc';
- initialPage?: number;
- populateContent?: boolean;
- cacheArticles?: boolean;
- emptyMessage?: string;
- gridClassName?: string;
-};
+ pageSize?: number
+ sort?: 'asc' | 'desc'
+ initialPage?: number
+ populateContent?: boolean
+ cacheArticles?: boolean
+ emptyMessage?: string
+ gridClassName?: string
+}
export function SupportArticleList({
- pageSize = DEFAULT_PAGINATION.PAGE_SIZE,
- sort = DEFAULT_PAGINATION.SORT,
- initialPage = DEFAULT_PAGINATION.INITIAL_PAGE,
- populateContent = true,
- cacheArticles = true,
- emptyMessage = RESOURCES_DICT.support.emptyMessage,
- gridClassName
+ pageSize = DEFAULT_PAGINATION.PAGE_SIZE,
+ sort = DEFAULT_PAGINATION.SORT,
+ initialPage = DEFAULT_PAGINATION.INITIAL_PAGE,
+ populateContent = true,
+ cacheArticles = true,
+ emptyMessage = RESOURCES_DICT.support.emptyMessage,
+ gridClassName,
}: TSupportArticleListProps): ReactNode {
- const [page, setPage] = useState(initialPage)
- const {articles, pagination, isLoading} = useFetchSupportArticles({
- page,
- pageSize,
- sort,
- populateContent,
- cacheArticles
- })
+ const [page, setPage] = useState(initialPage)
+ const { articles, pagination, isLoading } = useFetchSupportArticles({
+ page,
+ pageSize,
+ sort,
+ populateContent,
+ cacheArticles,
+ })
- // Loading skeleton
- if (isLoading) {
- return
- }
+ // Loading skeleton
+ if (isLoading) {
+ return
+ }
- return (
-
-
-
-
{RESOURCES_DICT.support.title}
-
- {RESOURCES_DICT.support.description}
-
-
+ return (
+
+
+
+
{RESOURCES_DICT.support.title}
+
{RESOURCES_DICT.support.description}
+
- {/* Empty state */}
- {!articles || articles.length === 0 ? (
-
- {emptyMessage}
-
- ) : (
- /* Articles grid */
-
- {articles.map((article: TSupportArticle) => (
-
-
-
-
- {article.title}
-
- ))}
-
- )}
+ {/* Empty state */}
+ {!articles || articles.length === 0 ? (
+
+ {emptyMessage}
+
+ ) : (
+ /* Articles grid */
+
+ {articles.map((article: TSupportArticle) => (
+
+
+
+
+ {article.title}
+
+ ))}
+
+ )}
- {/* Pagination controls */}
- {pagination && pagination.pageCount > 1 && (
-
setPage(selected + 1)}
- containerClassName={'flex gap-2 items-center justify-center mb-12'}
- pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
- pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
- activeClassName={'!opacity-100'}
- previousClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === 1 ? 'opacity-20' : 'opacity-100'
- )}
- previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
- )}
- disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
- disabledLinkClassName={'cursor-not-allowed'}
- previousLabel={ }
- nextLabel={
-
- }
- aria-label={'Pagination'}
- />
- )}
+ {/* Pagination controls */}
+ {pagination && pagination.pageCount > 1 && (
+ setPage(selected + 1)}
+ containerClassName={'flex gap-2 items-center justify-center mb-12'}
+ pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
+ pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ activeClassName={'!opacity-100'}
+ previousClassName={cl('hover:opacity-100 transition-opacity', page === 1 ? 'opacity-20' : 'opacity-100')}
+ previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextClassName={cl(
+ 'hover:opacity-100 transition-opacity',
+ page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
+ )}
+ disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
+ disabledLinkClassName={'cursor-not-allowed'}
+ previousLabel={ }
+ nextLabel={ }
+ aria-label={'Pagination'}
+ />
+ )}
- {/* Banner */}
-
-
-
-
-
- )
+ {/* Banner */}
+
+
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/SupportArticleListSkeleton.tsx b/app/[lang]/(resources)/_components/SupportArticleListSkeleton.tsx
index e8dbae5..650892a 100644
--- a/app/[lang]/(resources)/_components/SupportArticleListSkeleton.tsx
+++ b/app/[lang]/(resources)/_components/SupportArticleListSkeleton.tsx
@@ -1,51 +1,49 @@
-import {Fragment} from 'react'
+import { Fragment } from 'react'
-import {Banner} from '@/app/[lang]/_components/Banner'
-import {RESOURCES_DICT} from '@/app/[lang]/_utils/dictionary/resources'
+import { Banner } from '@/app/[lang]/_components/Banner'
+import { RESOURCES_DICT } from '@/app/[lang]/_utils/dictionary/resources'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
type TSupportArticleListSkeletonProps = {
- pageSize?: number;
-};
+ pageSize?: number
+}
-export function SupportArticleListSkeleton({pageSize = 6}: TSupportArticleListSkeletonProps): ReactNode {
- return (
-
-
- {/* Header */}
-
-
{RESOURCES_DICT.support.title}
-
{RESOURCES_DICT.support.description}
-
+export function SupportArticleListSkeleton({ pageSize = 6 }: TSupportArticleListSkeletonProps): ReactNode {
+ return (
+
+
+ {/* Header */}
+
+
{RESOURCES_DICT.support.title}
+
{RESOURCES_DICT.support.description}
+
- {/* Articles grid skeleton */}
-
- {Array.from({length: pageSize}).map((_, i) => (
-
- ))}
-
+ {/* Articles grid skeleton */}
+
+ {Array.from({ length: pageSize }).map((_, i) => (
+
+ ))}
+
- {/* Pagination skeleton */}
-
+ {/* Pagination skeleton */}
+
- {/* Banner */}
-
-
-
-
-
- )
+ {/* Banner */}
+
+
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/SupportedWalletAccelerate.tsx b/app/[lang]/(resources)/_components/SupportedWalletAccelerate.tsx
index 179b528..5f9f68e 100644
--- a/app/[lang]/(resources)/_components/SupportedWalletAccelerate.tsx
+++ b/app/[lang]/(resources)/_components/SupportedWalletAccelerate.tsx
@@ -1,74 +1,69 @@
-import {IconBorrow} from '@/app/[lang]/_icons/IconBorrow'
-import {IconEarn} from '@/app/[lang]/_icons/IconEarn'
-import {IconGlobe} from '@/app/[lang]/_icons/IconGlobe'
-import {IconKey} from '@/app/[lang]/_icons/IconKey'
-import {IconShield} from '@/app/[lang]/_icons/IconShield'
-import {IconTrade} from '@/app/[lang]/_icons/IconTrade'
+import { IconBorrow } from '@/app/[lang]/_icons/IconBorrow'
+import { IconEarn } from '@/app/[lang]/_icons/IconEarn'
+import { IconGlobe } from '@/app/[lang]/_icons/IconGlobe'
+import { IconKey } from '@/app/[lang]/_icons/IconKey'
+import { IconShield } from '@/app/[lang]/_icons/IconShield'
+import { IconTrade } from '@/app/[lang]/_icons/IconTrade'
-import type {ReactNode, SVGProps} from 'react'
+import type { ReactNode, SVGProps } from 'react'
const ACCELERATE_WITH_SHAPESHIFT_DATA = [
- {
- title: 'Trade',
- description:
- 'Swap 10,000+ EVM assets like ETH, stablecoins, and tokens on Arbitrum, Base, Polygon, and BNB Chain with the best rates through ShapeShift’s aggregator.',
- icon: (props: SVGProps) =>
- },
- {
- title: 'Save',
- description: 'Compare top liquidity sources in real time and capture the most competitive swap routes.',
- icon: (props: SVGProps) =>
- },
- {
- title: 'Earn',
- description:
- 'Access DeFi in one click: stake, farm, or provide liquidity across supported EVM chains without leaving ShapeShift.',
- icon: (props: SVGProps) =>
- },
- {
- title: 'Control your crypto',
- description:
- 'Stay non-custodial. Whether you connect Base App (Coinbase Wallet) or ShapeShift Wallet, you always hold your keys and stay in charge of your funds.',
- icon: (props: SVGProps) =>
- },
- {
- title: 'Multichain',
- description:
- 'Unlock seamless EVM trading with Base App, or import your seed into ShapeShift Wallet to access full multichain support including Bitcoin, Dogecoin, and more',
- icon: (props: SVGProps) =>
- },
- {
- title: 'Privacy',
- description: 'No KYC. No sign-ups. ShapeShift protects your privacy while you trade and explore DeFi.',
- icon: (props: SVGProps) =>
- }
+ {
+ title: 'Trade',
+ description:
+ 'Swap 10,000+ EVM assets like ETH, stablecoins, and tokens on Arbitrum, Base, Polygon, and BNB Chain with the best rates through ShapeShift’s aggregator.',
+ icon: (props: SVGProps) => ,
+ },
+ {
+ title: 'Save',
+ description: 'Compare top liquidity sources in real time and capture the most competitive swap routes.',
+ icon: (props: SVGProps) => ,
+ },
+ {
+ title: 'Earn',
+ description:
+ 'Access DeFi in one click: stake, farm, or provide liquidity across supported EVM chains without leaving ShapeShift.',
+ icon: (props: SVGProps) => ,
+ },
+ {
+ title: 'Control your crypto',
+ description:
+ 'Stay non-custodial. Whether you connect Base App (Coinbase Wallet) or ShapeShift Wallet, you always hold your keys and stay in charge of your funds.',
+ icon: (props: SVGProps) => ,
+ },
+ {
+ title: 'Multichain',
+ description:
+ 'Unlock seamless EVM trading with Base App, or import your seed into ShapeShift Wallet to access full multichain support including Bitcoin, Dogecoin, and more',
+ icon: (props: SVGProps) => ,
+ },
+ {
+ title: 'Privacy',
+ description: 'No KYC. No sign-ups. ShapeShift protects your privacy while you trade and explore DeFi.',
+ icon: (props: SVGProps) => ,
+ },
]
export function SupportedWalletAccelerate(): ReactNode {
- return (
-
-
- {'Accelerate with ShapeShift'}
-
- {ACCELERATE_WITH_SHAPESHIFT_DATA.map(action => (
-
-
- {action.icon({className: 'size-5'})}
-
+ return (
+
+
+ {'Accelerate with ShapeShift'}
+
+ {ACCELERATE_WITH_SHAPESHIFT_DATA.map((action) => (
+
+
+ {action.icon({ className: 'size-5' })}
+
-
-
{action.title}
-
{action.description}
-
-
- ))}
-
-
-
- )
+
+
{action.title}
+
{action.description}
+
+
+ ))}
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/SupportedWalletHeader.tsx b/app/[lang]/(resources)/_components/SupportedWalletHeader.tsx
index 8120956..c4e614f 100644
--- a/app/[lang]/(resources)/_components/SupportedWalletHeader.tsx
+++ b/app/[lang]/(resources)/_components/SupportedWalletHeader.tsx
@@ -1,42 +1,35 @@
-import {Button} from '@/app/[lang]/_components/Button'
-import {IconCheck} from '@/app/[lang]/_icons/IconCheck'
+import { Button } from '@/app/[lang]/_components/Button'
+import { IconCheck } from '@/app/[lang]/_icons/IconCheck'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
type THeaderData = {
- title: string;
- description: string;
- items: string[];
-};
+ title: string
+ description: string
+ items: string[]
+}
export function SupportedWalletHeader(data: THeaderData): ReactNode {
- return (
-
-
- {data.items.map(item => (
-
-
- {item}
-
- ))}
-
-
-
- {`ShapeShift supports ${data.title}`}
-
-
- {data.description}
-
-
-
-
- )
+ return (
+
+
+ {data.items.map((item) => (
+
+
+ {item}
+
+ ))}
+
+
+
{`ShapeShift supports ${data.title}`}
+
+ {data.description}
+
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/SupportedWalletHero.tsx b/app/[lang]/(resources)/_components/SupportedWalletHero.tsx
index 247a90e..994a0f7 100644
--- a/app/[lang]/(resources)/_components/SupportedWalletHero.tsx
+++ b/app/[lang]/(resources)/_components/SupportedWalletHero.tsx
@@ -1,27 +1,20 @@
import Image from 'next/image'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
-export function SupportedWalletHero(props: {url: string; width: number; height: number; name: string}): ReactNode {
- return (
-
- )
+export function SupportedWalletHero(props: { url: string; width: number; height: number; name: string }): ReactNode {
+ return (
+
+ )
}
diff --git a/app/[lang]/(resources)/_components/WalletList.tsx b/app/[lang]/(resources)/_components/WalletList.tsx
index 677fd32..dad1fca 100644
--- a/app/[lang]/(resources)/_components/WalletList.tsx
+++ b/app/[lang]/(resources)/_components/WalletList.tsx
@@ -16,41 +16,39 @@
** - Pass wallets data and optional loading state
************************************************************************************************/
-import {ResourceCard} from './ResourceCard'
-import {ResourceGrid} from './ResourceGrid'
+import { ResourceCard } from './ResourceCard'
+import { ResourceGrid } from './ResourceGrid'
-import type {TSupportedWalletData} from '@/app/[lang]/_components/strapi/types'
+import type { TSupportedWalletData } from '@/app/[lang]/_components/strapi/types'
type TWalletListProps = {
- wallets: TSupportedWalletData[] | null;
- isLoading?: boolean;
- className?: string;
- isSearchQuery?: boolean;
-};
+ wallets: TSupportedWalletData[] | null
+ isLoading?: boolean
+ className?: string
+ isSearchQuery?: boolean
+}
-export function WalletList({wallets, isLoading, className, isSearchQuery}: TWalletListProps): JSX.Element {
- return (
- (
-
- )}
- />
- )
+export function WalletList({ wallets, isLoading, className, isSearchQuery }: TWalletListProps): JSX.Element {
+ return (
+ (
+
+ )}
+ />
+ )
}
diff --git a/app/[lang]/(resources)/_utils/constants.ts b/app/[lang]/(resources)/_utils/constants.ts
index 1bd2207..58e79bf 100644
--- a/app/[lang]/(resources)/_utils/constants.ts
+++ b/app/[lang]/(resources)/_utils/constants.ts
@@ -20,9 +20,9 @@
* Default pagination settings used across resource list pages
************************************************************************************************/
export const DEFAULT_PAGINATION = {
- PAGE_SIZE: 12,
- SORT: 'desc' as const,
- INITIAL_PAGE: 1
+ PAGE_SIZE: 12,
+ SORT: 'desc' as const,
+ INITIAL_PAGE: 1,
}
/************************************************************************************************
diff --git a/app/[lang]/(resources)/_utils/fetchUtils.ts b/app/[lang]/(resources)/_utils/fetchUtils.ts
index 76ac282..a49644a 100644
--- a/app/[lang]/(resources)/_utils/fetchUtils.ts
+++ b/app/[lang]/(resources)/_utils/fetchUtils.ts
@@ -16,21 +16,21 @@
************************************************************************************************/
import type {
- TDiscoverData,
- TFaqData,
- TSupportedChainData,
- TSupportedProtocolData,
- TSupportedWalletData
+ TDiscoverData,
+ TFaqData,
+ TSupportedChainData,
+ TSupportedProtocolData,
+ 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
- }
+ headers: {
+ Authorization: `Bearer ${process.env.NEXT_PUBLIC_STRAPI_API_TOKEN}`,
+ },
+ next: {
+ revalidate: 3600, // Cache for 1 hour
+ },
}
/************************************************************************************************
@@ -39,23 +39,23 @@ const apiConfig = {
* @returns Promise resolving to array of protocol data or null if error
************************************************************************************************/
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
- )
-
- if (!response.ok) {
- console.error(`Failed to fetch protocols: ${response.status}`)
- return null
- }
-
- const data = await response.json()
- return data.data || null
- } catch (error) {
- console.error('Error fetching protocols:', error instanceof Error ? error.message : String(error))
- return null
- }
+ try {
+ const response = await fetch(
+ `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/supported-protocols?populate=*&pagination[page]=1&pagination[pageSize]=500`,
+ apiConfig
+ )
+
+ if (!response.ok) {
+ console.error(`Failed to fetch protocols: ${response.status}`)
+ return null
+ }
+
+ const data = await response.json()
+ return data.data || null
+ } catch (error) {
+ console.error('Error fetching protocols:', error instanceof Error ? error.message : String(error))
+ return null
+ }
}
/************************************************************************************************
@@ -64,23 +64,23 @@ 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
- )
-
- if (!response.ok) {
- console.error(`Failed to fetch chains: ${response.status}`)
- return null
- }
-
- const data = await response.json()
- return data.data || null
- } catch (error) {
- console.error('Error fetching chains:', error instanceof Error ? error.message : String(error))
- return null
- }
+ try {
+ const response = await fetch(
+ `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/supported-chains?populate=*&pagination[page]=1&pagination[pageSize]=500`,
+ apiConfig
+ )
+
+ if (!response.ok) {
+ console.error(`Failed to fetch chains: ${response.status}`)
+ return null
+ }
+
+ const data = await response.json()
+ return data.data || null
+ } catch (error) {
+ console.error('Error fetching chains:', error instanceof Error ? error.message : String(error))
+ return null
+ }
}
/************************************************************************************************
@@ -89,23 +89,23 @@ export async function fetchAllChains(): Promise {
* @returns Promise resolving to array of wallet data or null if error
************************************************************************************************/
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
- )
-
- if (!response.ok) {
- console.error(`Failed to fetch wallets: ${response.status}`)
- return null
- }
-
- const data = await response.json()
- return data.data || null
- } catch (error) {
- console.error('Error fetching wallets:', error instanceof Error ? error.message : String(error))
- return null
- }
+ try {
+ const response = await fetch(
+ `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/supported-wallets?populate=*&pagination[page]=1&pagination[pageSize]=500`,
+ apiConfig
+ )
+
+ if (!response.ok) {
+ console.error(`Failed to fetch wallets: ${response.status}`)
+ return null
+ }
+
+ const data = await response.json()
+ return data.data || null
+ } catch (error) {
+ console.error('Error fetching wallets:', error instanceof Error ? error.message : String(error))
+ return null
+ }
}
/************************************************************************************************
@@ -115,26 +115,26 @@ export async function fetchAllWallets(): Promise
* @returns Promise resolving to discover data or null if not found
************************************************************************************************/
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
- )
-
- if (!response.ok) {
- console.error(`Failed to fetch discover data for slug "${slug}": ${response.status}`)
- return null
- }
-
- const data = await response.json()
- return data.data?.[0] || null
- } catch (error) {
- console.error(
- `Error fetching discover data for slug "${slug}":`,
- error instanceof Error ? error.message : String(error)
- )
- return null
- }
+ 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
+ )
+
+ if (!response.ok) {
+ console.error(`Failed to fetch discover data for slug "${slug}": ${response.status}`)
+ return null
+ }
+
+ const data = await response.json()
+ return data.data?.[0] || null
+ } catch (error) {
+ console.error(
+ `Error fetching discover data for slug "${slug}":`,
+ error instanceof Error ? error.message : String(error)
+ )
+ return null
+ }
}
/************************************************************************************************
@@ -143,21 +143,21 @@ 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
- )
-
- if (!response.ok) {
- console.error(`Failed to fetch FAQ data: ${response.status}`)
- return null
- }
-
- const data = await response.json()
- return data.data || null
- } catch (error) {
- console.error('Error fetching FAQ data:', error instanceof Error ? error.message : String(error))
- return null
- }
+ 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
+ )
+
+ if (!response.ok) {
+ console.error(`Failed to fetch FAQ data: ${response.status}`)
+ return null
+ }
+
+ const data = await response.json()
+ return data.data || null
+ } catch (error) {
+ console.error('Error fetching FAQ data:', error instanceof Error ? error.message : String(error))
+ return null
+ }
}
diff --git a/app/[lang]/(resources)/blog/(withNavigation)/BlogBreadcrumb.tsx b/app/[lang]/(resources)/blog/(withNavigation)/BlogBreadcrumb.tsx
index bb072b7..d656ab8 100644
--- a/app/[lang]/(resources)/blog/(withNavigation)/BlogBreadcrumb.tsx
+++ b/app/[lang]/(resources)/blog/(withNavigation)/BlogBreadcrumb.tsx
@@ -1,10 +1,10 @@
'use client'
-import {usePathname} from 'next/navigation'
+import { usePathname } from 'next/navigation'
-import {LocalizedLink} from '@/app/[lang]/_components/LocalizedLink'
-import {IconBack} from '@/app/[lang]/_icons/IconBack'
-import {cl} from '@/app/[lang]/_utils/cl'
+import { LocalizedLink } from '@/app/[lang]/_components/LocalizedLink'
+import { IconBack } from '@/app/[lang]/_icons/IconBack'
+import { cl } from '@/app/[lang]/_utils/cl'
/********************************************************************************************
* Blog Breadcrumb Navigation Component
@@ -13,17 +13,18 @@ import {cl} from '@/app/[lang]/_utils/cl'
* Automatically hides when on the main blog page.
********************************************************************************************/
export function BlogBreadcrumb(): React.ReactNode {
- const pathname = usePathname()
+ const pathname = usePathname()
- return (
-
-
- {'Back to blog'}
-
- )
+ return (
+
+
+ {'Back to blog'}
+
+ )
}
diff --git a/app/[lang]/(resources)/blog/(withNavigation)/BlogNav.tsx b/app/[lang]/(resources)/blog/(withNavigation)/BlogNav.tsx
index b3d695e..55c3d77 100644
--- a/app/[lang]/(resources)/blog/(withNavigation)/BlogNav.tsx
+++ b/app/[lang]/(resources)/blog/(withNavigation)/BlogNav.tsx
@@ -1,40 +1,40 @@
'use client'
-import {useParams, usePathname} from 'next/navigation'
+import { useParams, usePathname } from 'next/navigation'
-import {TabItem} from '@/app/[lang]/_components/TabItem'
-import {blogTags, blogTypes} from '@/app/[lang]/_utils/constants'
+import { TabItem } from '@/app/[lang]/_components/TabItem'
+import { blogTags, blogTypes } from '@/app/[lang]/_utils/constants'
export function BlogNav(): React.ReactNode {
- const pathname = usePathname()
- const params = useParams()
- const category = (params.category as string) || ''
- const tag = (params.tag as string) || ''
+ const pathname = usePathname()
+ const params = useParams()
+ const category = (params.category as string) || ''
+ const tag = (params.tag as string) || ''
- if (tag || pathname.includes('/tags')) {
- return (
-
- {blogTags.map(tab => (
-
- ))}
-
- )
- }
- return (
-
- {blogTypes.map(tab => (
-
- ))}
-
- )
+ if (tag || pathname.includes('/tags')) {
+ return (
+
+ {blogTags.map((tab) => (
+
+ ))}
+
+ )
+ }
+ return (
+
+ {blogTypes.map((tab) => (
+
+ ))}
+
+ )
}
diff --git a/app/[lang]/(resources)/blog/(withNavigation)/BlogTitle.tsx b/app/[lang]/(resources)/blog/(withNavigation)/BlogTitle.tsx
index 9b14707..3129225 100644
--- a/app/[lang]/(resources)/blog/(withNavigation)/BlogTitle.tsx
+++ b/app/[lang]/(resources)/blog/(withNavigation)/BlogTitle.tsx
@@ -1,46 +1,46 @@
'use client'
-import {useParams, usePathname} from 'next/navigation'
+import { useParams, usePathname } from 'next/navigation'
-import {blogTypesSlugToCategory} from '@/app/[lang]/_utils/constants'
+import { blogTypesSlugToCategory } from '@/app/[lang]/_utils/constants'
export function BlogTitle(): React.ReactNode {
- const pathname = usePathname()
- const params = useParams()
- const category = (params.category as string) || ''
- const tag = (params.tag as string) || ''
+ const pathname = usePathname()
+ const params = useParams()
+ const category = (params.category as string) || ''
+ const tag = (params.tag as string) || ''
- if (tag || pathname.includes('/tags')) {
- if (tag) {
- return (
-
- {'Tagged: '}
- {tag}
-
- )
- }
- return (
-
- {'All '}
- {'tags'}
-
- )
- }
+ if (tag || pathname.includes('/tags')) {
+ if (tag) {
+ return (
+
+ {'Tagged: '}
+ {tag}
+
+ )
+ }
+ return (
+
+ {'All '}
+ {'tags'}
+
+ )
+ }
- if (category) {
- return (
-
- {'Category: '}
- {blogTypesSlugToCategory(category)}
-
- )
- }
- return (
-
- {'ShapeShift'}
-
- {'Blog'}
- {'.'}
-
- )
+ if (category) {
+ return (
+
+ {'Category: '}
+ {blogTypesSlugToCategory(category)}
+
+ )
+ }
+ return (
+
+ {'ShapeShift'}
+
+ {'Blog'}
+ {'.'}
+
+ )
}
diff --git a/app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/ListOfPosts.tsx b/app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/ListOfPosts.tsx
index 045d044..a363d0d 100644
--- a/app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/ListOfPosts.tsx
+++ b/app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/ListOfPosts.tsx
@@ -18,22 +18,16 @@
'use client'
-import {PostList} from '@/app/[lang]/(resources)/_components/PostList'
-import {blogTypesSlugToCategory} from '@/app/[lang]/_utils/constants'
+import { PostList } from '@/app/[lang]/(resources)/_components/PostList'
+import { blogTypesSlugToCategory } from '@/app/[lang]/_utils/constants'
-import type {ReactElement} from 'react'
+import type { ReactElement } from 'react'
-export function ListOfPosts({category}: {category: string}): ReactElement {
- // Convert URL slug to proper category type for the API
- const categoryType = blogTypesSlugToCategory(category)
+export function ListOfPosts({ category }: { category: string }): ReactElement {
+ // Convert URL slug to proper category type for the API
+ const categoryType = blogTypesSlugToCategory(category)
- // Format category name for display (capitalize first letter)
- const formattedCategory = category.charAt(0).toUpperCase() + category.slice(1)
-
- return (
-
- )
+ return (
+
+ )
}
diff --git a/app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/page.tsx b/app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/page.tsx
index 059006d..679fb5f 100644
--- a/app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/page.tsx
+++ b/app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/page.tsx
@@ -1,24 +1,24 @@
-import {notFound} from 'next/navigation'
+import { notFound } from 'next/navigation'
-import {ListOfPosts} from '@/app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/ListOfPosts'
+import { ListOfPosts } from '@/app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/ListOfPosts'
export default async function BlogCategoriesPage(props: {
- params: Promise<{category: string}>;
+ 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: posts} = await data.json()
+ const { category } = await props.params
- if (!posts) {
- return notFound()
- }
+ 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}`,
+ },
+ }
+ )
- return
+ const { data: posts } = await data.json()
+
+ if (!posts) return notFound()
+
+ return
}
diff --git a/app/[lang]/(resources)/blog/(withNavigation)/layout.tsx b/app/[lang]/(resources)/blog/(withNavigation)/layout.tsx
index 24bb280..8d374a4 100644
--- a/app/[lang]/(resources)/blog/(withNavigation)/layout.tsx
+++ b/app/[lang]/(resources)/blog/(withNavigation)/layout.tsx
@@ -1,19 +1,19 @@
-import {BlogBreadcrumb} from '@/app/[lang]/(resources)/blog/(withNavigation)/BlogBreadcrumb'
-import {BlogNav} from '@/app/[lang]/(resources)/blog/(withNavigation)/BlogNav'
-import {BlogTitle} from '@/app/[lang]/(resources)/blog/(withNavigation)/BlogTitle'
+import { BlogBreadcrumb } from '@/app/[lang]/(resources)/blog/(withNavigation)/BlogBreadcrumb'
+import { BlogNav } from '@/app/[lang]/(resources)/blog/(withNavigation)/BlogNav'
+import { BlogTitle } from '@/app/[lang]/(resources)/blog/(withNavigation)/BlogTitle'
export default async function BlogPageLayout(props: {
- children: React.ReactNode;
- params: Promise<{category: string}>;
+ children: React.ReactNode
+ params: Promise<{ category: string }>
}): Promise {
- await props.params
+ await props.params
- return (
-
-
-
-
- {props.children}
-
- )
+ return (
+
+
+
+
+ {props.children}
+
+ )
}
diff --git a/app/[lang]/(resources)/blog/(withNavigation)/page.tsx b/app/[lang]/(resources)/blog/(withNavigation)/page.tsx
index 28891a3..dc62c87 100644
--- a/app/[lang]/(resources)/blog/(withNavigation)/page.tsx
+++ b/app/[lang]/(resources)/blog/(withNavigation)/page.tsx
@@ -18,10 +18,10 @@
'use client'
-import {PostList} from '@/app/[lang]/(resources)/_components/PostList'
+import { PostList } from '@/app/[lang]/(resources)/_components/PostList'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
export default function BlogListPage(): ReactNode {
- return
+ return
}
diff --git a/app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/ListOfPosts.tsx b/app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/ListOfPosts.tsx
index 2107c7d..70898ed 100644
--- a/app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/ListOfPosts.tsx
+++ b/app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/ListOfPosts.tsx
@@ -1,88 +1,78 @@
'use client'
-import {Fragment, useState} from 'react'
+import { Fragment, useState } from 'react'
import ReactPaginate from 'react-paginate'
-import {Banner} from '@/app/[lang]/_components/Banner'
-import {BlogPost} from '@/app/[lang]/_components/BlogPost'
-import {useFetchPosts} from '@/app/[lang]/_hooks/useFetchPosts'
-import {IconChevron} from '@/app/[lang]/_icons/IconChevron'
-import {cl} from '@/app/[lang]/_utils/cl'
+import { Banner } from '@/app/[lang]/_components/Banner'
+import { BlogPost } from '@/app/[lang]/_components/BlogPost'
+import { useFetchPosts } from '@/app/[lang]/_hooks/useFetchPosts'
+import { IconChevron } from '@/app/[lang]/_icons/IconChevron'
+import { cl } from '@/app/[lang]/_utils/cl'
-import type {TBlogPost} from '@/app/[lang]/_components/strapi/types'
-import type {ReactElement} from 'react'
+import type { TBlogPost } from '@/app/[lang]/_components/strapi/types'
+import type { ReactElement } from 'react'
const PAGE_SIZE = 12
const SORT = 'desc'
-export function ListOfPosts(props: {tag: string}): ReactElement {
- const {tag} = props
- const [page, setPage] = useState(1)
- const {posts, pagination, isLoading} = useFetchPosts({
- page,
- pageSize: PAGE_SIZE,
- sort: SORT,
- tag,
- populateContent: true,
- cachePosts: true
- })
+export function ListOfPosts(props: { tag: string }): ReactElement {
+ const { tag } = props
+ const [page, setPage] = useState(1)
+ const { posts, pagination, isLoading } = useFetchPosts({
+ page,
+ pageSize: PAGE_SIZE,
+ sort: SORT,
+ tag,
+ populateContent: true,
+ cachePosts: true,
+ })
- if (isLoading) {
- return (
-
- {[...Array(PAGE_SIZE)].map((_, i) => (
-
- ))}
-
- )
- }
+ if (isLoading) {
+ return (
+
+ {[...Array(PAGE_SIZE)].map((_, i) => (
+
+ ))}
+
+ )
+ }
- return (
-
- {posts.length === 0 ? (
-
- {"We couldn't find any blog posts matching your criteria."}
-
- ) : (
-
- {posts.map((post: TBlogPost) => (
-
- ))}
-
- )}
+ return (
+
+ {posts.length === 0 ? (
+
+ {"We couldn't find any blog posts matching your criteria."}
+
+ ) : (
+
+ {posts.map((post: TBlogPost) => (
+
+ ))}
+
+ )}
- setPage(selected + 1)}
- containerClassName={'flex gap-2 items-center justify-center mb-16'}
- pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
- pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
- activeClassName={'!opacity-100'}
- previousClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === 1 ? 'opacity-20' : 'opacity-100'
- )}
- previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
- )}
- disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
- disabledLinkClassName={'cursor-not-allowed'}
- previousLabel={ }
- nextLabel={ }
- />
+ setPage(selected + 1)}
+ containerClassName={'flex gap-2 items-center justify-center mb-16'}
+ pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
+ pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ activeClassName={'!opacity-100'}
+ previousClassName={cl('hover:opacity-100 transition-opacity', page === 1 ? 'opacity-20' : 'opacity-100')}
+ previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextClassName={cl(
+ 'hover:opacity-100 transition-opacity',
+ page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
+ )}
+ disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
+ disabledLinkClassName={'cursor-not-allowed'}
+ previousLabel={ }
+ nextLabel={ }
+ />
-
-
- )
+
+
+ )
}
diff --git a/app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/page.tsx b/app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/page.tsx
index 771a761..f6c3cc4 100644
--- a/app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/page.tsx
+++ b/app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/page.tsx
@@ -1,22 +1,22 @@
-import {notFound} from 'next/navigation'
+import { notFound } from 'next/navigation'
-import {ListOfPosts} from '@/app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/ListOfPosts'
+import { ListOfPosts } from '@/app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/ListOfPosts'
-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: posts} = await data.json()
+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: posts } = await data.json()
- if (!posts) {
- return notFound()
- }
+ if (!posts) {
+ return notFound()
+ }
- return
+ return
}
diff --git a/app/[lang]/(resources)/blog/(withNavigation)/tags/page.tsx b/app/[lang]/(resources)/blog/(withNavigation)/tags/page.tsx
index f639073..da0010c 100644
--- a/app/[lang]/(resources)/blog/(withNavigation)/tags/page.tsx
+++ b/app/[lang]/(resources)/blog/(withNavigation)/tags/page.tsx
@@ -1,84 +1,74 @@
'use client'
-import {Fragment, useState} from 'react'
+import { Fragment, useState } from 'react'
import ReactPaginate from 'react-paginate'
-import {Banner} from '@/app/[lang]/_components/Banner'
-import {BlogPost} from '@/app/[lang]/_components/BlogPost'
-import {useFetchPosts} from '@/app/[lang]/_hooks/useFetchPosts'
-import {IconChevron} from '@/app/[lang]/_icons/IconChevron'
-import {cl} from '@/app/[lang]/_utils/cl'
+import { Banner } from '@/app/[lang]/_components/Banner'
+import { BlogPost } from '@/app/[lang]/_components/BlogPost'
+import { useFetchPosts } from '@/app/[lang]/_hooks/useFetchPosts'
+import { IconChevron } from '@/app/[lang]/_icons/IconChevron'
+import { cl } from '@/app/[lang]/_utils/cl'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
const PAGE_SIZE = 12
const SORT = 'desc'
export default function BlogList(): ReactNode {
- const [page, setPage] = useState(1)
- const {posts, pagination, isLoading} = useFetchPosts({
- page,
- pageSize: PAGE_SIZE,
- sort: SORT,
- populateContent: true,
- cachePosts: true
- })
+ const [page, setPage] = useState(1)
+ const { posts, pagination, isLoading } = useFetchPosts({
+ page,
+ pageSize: PAGE_SIZE,
+ sort: SORT,
+ populateContent: true,
+ cachePosts: true,
+ })
- if (isLoading) {
- return (
-
- {[...Array(PAGE_SIZE)].map((_, i) => (
-
- ))}
-
- )
- }
+ if (isLoading) {
+ return (
+
+ {[...Array(PAGE_SIZE)].map((_, i) => (
+
+ ))}
+
+ )
+ }
- return (
-
- {posts.length === 0 ? (
-
- {"We couldn't find any blog posts matching your criteria."}
-
- ) : (
-
- {posts.map(post => (
-
- ))}
-
- )}
- setPage(selected + 1)}
- containerClassName={'flex gap-2 items-center justify-center'}
- pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
- pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
- activeClassName={'!opacity-100'}
- previousClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === 1 ? 'opacity-20' : 'opacity-100'
- )}
- previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
- )}
- disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
- disabledLinkClassName={'cursor-not-allowed'}
- previousLabel={ }
- nextLabel={ }
- />
-
-
- )
+ return (
+
+ {posts.length === 0 ? (
+
+ {"We couldn't find any blog posts matching your criteria."}
+
+ ) : (
+
+ {posts.map((post) => (
+
+ ))}
+
+ )}
+ setPage(selected + 1)}
+ containerClassName={'flex gap-2 items-center justify-center'}
+ pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
+ pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ activeClassName={'!opacity-100'}
+ previousClassName={cl('hover:opacity-100 transition-opacity', page === 1 ? 'opacity-20' : 'opacity-100')}
+ previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextClassName={cl(
+ 'hover:opacity-100 transition-opacity',
+ page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
+ )}
+ disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
+ disabledLinkClassName={'cursor-not-allowed'}
+ previousLabel={ }
+ nextLabel={ }
+ />
+
+
+ )
}
diff --git a/app/[lang]/(resources)/blog/README.md b/app/[lang]/(resources)/blog/README.md
index 3607abc..9eb9600 100644
--- a/app/[lang]/(resources)/blog/README.md
+++ b/app/[lang]/(resources)/blog/README.md
@@ -5,20 +5,20 @@ This directory contains the blog section of the ShapeShift website, featuring bl
## Directory Structure
- **(withNavigation)/**: Blog pages that include navigation elements
- - **BlogBreadcrumb.tsx**: Breadcrumb navigation for blog
- - **BlogNav.tsx**: Main navigation for blog section
- - **BlogTitle.tsx**: Title component for blog section
- - **categories/**: Category-based blog post filtering
- - **[category]/**: Dynamic routes for individual categories
- - **tags/**: Tag-based blog post filtering
- - **[tag]/**: Dynamic routes for individual tags
- - **layout.tsx**: Layout for blog pages with navigation
- - **page.tsx**: Main blog landing page
+ - **BlogBreadcrumb.tsx**: Breadcrumb navigation for blog
+ - **BlogNav.tsx**: Main navigation for blog section
+ - **BlogTitle.tsx**: Title component for blog section
+ - **categories/**: Category-based blog post filtering
+ - **[category]/**: Dynamic routes for individual categories
+ - **tags/**: Tag-based blog post filtering
+ - **[tag]/**: Dynamic routes for individual tags
+ - **layout.tsx**: Layout for blog pages with navigation
+ - **page.tsx**: Main blog landing page
- **[slug]/**: Individual blog post pages
- - **BlogContent.tsx**: Component for rendering blog post content
- - **BlogSkeleton.tsx**: Loading skeleton for blog posts
- - **layout.tsx**: Layout for individual blog posts
- - **page.tsx**: Page component for individual blog posts
+ - **BlogContent.tsx**: Component for rendering blog post content
+ - **BlogSkeleton.tsx**: Loading skeleton for blog posts
+ - **layout.tsx**: Layout for individual blog posts
+ - **page.tsx**: Page component for individual blog posts
## Features
@@ -51,4 +51,4 @@ This directory contains the blog section of the ShapeShift website, featuring bl
- Test loading states and error handling
- Consider performance optimizations for image-heavy blog posts
- Ensure responsive design works across all device sizes
-- Preserve consistent typography and styling
\ No newline at end of file
+- Preserve consistent typography and styling
diff --git a/app/[lang]/(resources)/blog/[slug]/BlogContent.tsx b/app/[lang]/(resources)/blog/[slug]/BlogContent.tsx
index 17d52a3..1dc6c7e 100644
--- a/app/[lang]/(resources)/blog/[slug]/BlogContent.tsx
+++ b/app/[lang]/(resources)/blog/[slug]/BlogContent.tsx
@@ -5,215 +5,167 @@ import rehypeHighlight from 'rehype-highlight'
import remarkEmoji from 'remark-emoji' // For emoji support
import remarkGfm from 'remark-gfm'
-import {isHtml} from '@/app/[lang]/_utils/isHtml'
-
-import type {ReactNode} from 'react'
-
-export function BlogContent({content}: {content: string}): ReactNode {
- return (
-
- {isHtml(content) ? (
-
- ) : (
-
(
-
- ),
- h2: ({...props}) => (
-
- ),
- h3: ({...props}) => (
-
- ),
-
- // Code blocks
- code: ({className, children, ...props}) => {
- const match = /language-(\w+)/.exec(className || '')
- return match ? (
-
-
{match[1]}
-
-
- {children}
-
-
-
- ) : (
-
- {children}
-
- )
- },
-
- // Tables
- table: ({...props}) => (
-
- ),
- th: ({...props}) => (
-
- ),
- td: ({...props}) => (
-
- ),
-
- // Images
- img: ({...props}) => (
-
- ),
-
- // Blockquotes
- blockquote: ({...props}) => (
-
- ),
-
- // Lists
- ul: ({...props}) => (
-
- ),
- ol: ({...props}) => (
-
- ),
-
- // Links
- a: ({...props}) => (
-
- ),
- p: ({...props}) => (
-
- )
- }}>
- {content}
-
- )}
-
-
-
- )
+import { isHtml } from '@/app/[lang]/_utils/isHtml'
+
+import type { ReactNode } from 'react'
+
+export function BlogContent({ content }: { content: string }): ReactNode {
+ return (
+
+ {isHtml(content) ? (
+
+ ) : (
+
,
+ h2: ({ ...props }) => ,
+ h3: ({ ...props }) => ,
+
+ // Code blocks
+ code: ({ className, children, ...props }) => {
+ const match = /language-(\w+)/.exec(className || '')
+ return match ? (
+
+
{match[1]}
+
+
+ {children}
+
+
+
+ ) : (
+
+ {children}
+
+ )
+ },
+
+ // Tables
+ table: ({ ...props }) => (
+
+ ),
+ th: ({ ...props }) => ,
+ td: ({ ...props }) => ,
+
+ // Images
+ img: ({ ...props }) => (
+
+ ),
+
+ // Blockquotes
+ blockquote: ({ ...props }) => (
+
+ ),
+
+ // Lists
+ ul: ({ ...props }) => ,
+ ol: ({ ...props }) => ,
+
+ // Links
+ a: ({ ...props }) => (
+
+ ),
+ p: ({ ...props }) =>
,
+ }}
+ >
+ {content}
+
+ )}
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/blog/[slug]/BlogSkeleton.tsx b/app/[lang]/(resources)/blog/[slug]/BlogSkeleton.tsx
index 75d9fca..fec8755 100644
--- a/app/[lang]/(resources)/blog/[slug]/BlogSkeleton.tsx
+++ b/app/[lang]/(resources)/blog/[slug]/BlogSkeleton.tsx
@@ -1,34 +1,34 @@
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
export function BlogSkeleton(): ReactNode {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/blog/[slug]/layout.tsx b/app/[lang]/(resources)/blog/[slug]/layout.tsx
index 6cfdc20..d9f9601 100644
--- a/app/[lang]/(resources)/blog/[slug]/layout.tsx
+++ b/app/[lang]/(resources)/blog/[slug]/layout.tsx
@@ -1,65 +1,67 @@
-import {notFound} from 'next/navigation'
+import { notFound } from 'next/navigation'
-import type {Metadata} from 'next'
+import { getStrapiImageUrl } from '@/app/[lang]/_utils/query'
+
+import type { Metadata } from 'next'
/************************************************************************************************
* Layout component for blog post pages
* Handles metadata generation for SEO and social sharing
* Uses Next.js 13+ Metadata API
************************************************************************************************/
-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}`
- }
- }
- ).then(async res => res.json())
+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}`,
+ },
+ }
+ ).then(async (res) => res.json())
- const post = data.data[0]
+ const post = data.data[0]
- if (!post) {
- return notFound()
- }
+ if (!post) {
+ return notFound()
+ }
- const imageUrl = post.featuredImg?.formats?.thumbnail?.url || post.featuredImg?.url
- const metadata: Metadata = {
- title: `${post.title} | ShapeShift Blog`,
- description: post.summary || `Read ${post.title} on ShapeShift Blog`,
- keywords: Array.isArray(post.tags) ? post.tags.join(', ') : post.tags,
- openGraph: {
- title: post.title,
- description: post.summary,
- type: 'article',
- publishedTime: post.publishedAt,
- authors: ['ShapeShift'],
- tags: Array.isArray(post.tags) ? post.tags : [post.tags]
- },
- twitter: {
- card: 'summary_large_image',
- title: post.title,
- description: post.summary || `Read ${post.title} on ShapeShift Blog`
- }
- }
+ const imageUrl = post.featuredImg?.formats?.thumbnail?.url || post.featuredImg?.url
+ const metadata: Metadata = {
+ title: `${post.title} | ShapeShift Blog`,
+ description: post.summary || `Read ${post.title} on ShapeShift Blog`,
+ keywords: Array.isArray(post.tags) ? post.tags.join(', ') : post.tags,
+ openGraph: {
+ title: post.title,
+ description: post.summary,
+ type: 'article',
+ publishedTime: post.publishedAt,
+ authors: ['ShapeShift'],
+ tags: Array.isArray(post.tags) ? post.tags : [post.tags],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: post.title,
+ description: post.summary || `Read ${post.title} on ShapeShift Blog`,
+ },
+ }
- if (imageUrl) {
- metadata.openGraph!.images = [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}${imageUrl}`
- }
- ]
- metadata.twitter!.images = [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}${imageUrl}`
- }
- ]
- }
+ if (imageUrl) {
+ metadata.openGraph!.images = [
+ {
+ url: getStrapiImageUrl(imageUrl),
+ },
+ ]
+ metadata.twitter!.images = [
+ {
+ url: getStrapiImageUrl(imageUrl),
+ },
+ ]
+ }
- return metadata
+ return metadata
}
-export default function BlogPostLayout({children}: {children: React.ReactNode}): React.ReactNode {
- return children
+export default function BlogPostLayout({ children }: { children: React.ReactNode }): React.ReactNode {
+ return children
}
diff --git a/app/[lang]/(resources)/blog/[slug]/page.tsx b/app/[lang]/(resources)/blog/[slug]/page.tsx
index 588a90d..c5ca821 100644
--- a/app/[lang]/(resources)/blog/[slug]/page.tsx
+++ b/app/[lang]/(resources)/blog/[slug]/page.tsx
@@ -1,92 +1,88 @@
'use client'
import 'highlight.js/styles/github-dark.css'
-import {notFound, useParams, useRouter} from 'next/navigation'
+import { notFound, useParams, useRouter } from 'next/navigation'
import Script from 'next/script'
-import {BlogContent} from '@/app/[lang]/(resources)/blog/[slug]/BlogContent'
-import {BlogSkeleton} from '@/app/[lang]/(resources)/blog/[slug]/BlogSkeleton'
-import {Banner} from '@/app/[lang]/_components/Banner'
-import {LocalizedLink} from '@/app/[lang]/_components/LocalizedLink'
-import {useCachedPosts} from '@/app/[lang]/_contexts/CachedPostsContext'
-import {useFetchPosts} from '@/app/[lang]/_hooks/useFetchPosts'
-import {IconBack} from '@/app/[lang]/_icons/IconBack'
-import {generateBlogPostSchema} from '@/app/[lang]/_utils/schema'
+import { BlogContent } from '@/app/[lang]/(resources)/blog/[slug]/BlogContent'
+import { BlogSkeleton } from '@/app/[lang]/(resources)/blog/[slug]/BlogSkeleton'
+import { Banner } from '@/app/[lang]/_components/Banner'
+import { LocalizedLink } from '@/app/[lang]/_components/LocalizedLink'
+import { useCachedPosts } from '@/app/[lang]/_contexts/CachedPostsContext'
+import { useFetchPosts } from '@/app/[lang]/_hooks/useFetchPosts'
+import { IconBack } from '@/app/[lang]/_icons/IconBack'
+import { generateBlogPostSchema } from '@/app/[lang]/_utils/schema'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
export default function BlogPost(): ReactNode {
- const {slug} = useParams()
+ const { slug } = useParams()
- const {
- cachedResponse: {data: cachedPosts}
- } = useCachedPosts()
- const {posts, isLoading} = useFetchPosts({
- page: 1,
- pageSize: 1,
- sort: 'desc',
- populateContent: true,
- cachePosts: true,
- slug: slug as string,
- skip: !!cachedPosts.find(p => p.slug === slug)
- })
+ const {
+ cachedResponse: { data: cachedPosts },
+ } = useCachedPosts()
+ const { posts, isLoading } = useFetchPosts({
+ page: 1,
+ pageSize: 1,
+ sort: 'desc',
+ populateContent: true,
+ cachePosts: true,
+ slug: slug as string,
+ skip: !!cachedPosts.find((p) => p.slug === slug),
+ })
- const post = [...cachedPosts, ...posts].find(p => p.slug === slug)
- const router = useRouter()
+ const post = [...cachedPosts, ...posts].find((p) => p.slug === slug)
+ const router = useRouter()
- if (isLoading) {
- return
- }
+ if (isLoading) {
+ return
+ }
- if (!post) {
- notFound()
- }
+ if (!post) {
+ notFound()
+ }
- // Generate structured data for the blog post
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://shapeshift.com'
- const blogPostSchema = generateBlogPostSchema(post, baseUrl)
+ // Generate structured data for the blog post
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://shapeshift.com'
+ const blogPostSchema = generateBlogPostSchema(post, baseUrl)
- return (
- <>
- {/* Add structured data */}
-
+ return (
+ <>
+ {/* Add structured data */}
+
-
- router.back()}>
-
- {'Back'}
-
-
- {new Date(post.publishedAt).toLocaleDateString()}
-
+
+ router.back()}
+ >
+
+ {'Back'}
+
+ {new Date(post.publishedAt).toLocaleDateString()}
-
- {post.tags.map((tag: string, index: number) => (
-
- {`#${tag}`}
-
- ))}
-
-
- {post.title || post.slug.replace(/-/g, ' ')}
-
-
-
- {!isLoading && (
-
-
-
- )}
- >
- )
+
+ {post.tags.map((tag: string, index: number) => (
+
+ {`#${tag}`}
+
+ ))}
+
+
+ {post.title || post.slug.replace(/-/g, ' ')}
+
+
+
+ {!isLoading && (
+
+
+
+ )}
+ >
+ )
}
diff --git a/app/[lang]/(resources)/chains/README.md b/app/[lang]/(resources)/chains/README.md
index 66be42a..82bdb86 100644
--- a/app/[lang]/(resources)/chains/README.md
+++ b/app/[lang]/(resources)/chains/README.md
@@ -5,7 +5,7 @@ This directory contains pages that showcase and document the blockchain networks
## Directory Structure
- **[slug]/**: Dynamic routes for individual chain pages
- - **page.tsx**: Page component for individual chain details
+ - **page.tsx**: Page component for individual chain details
- **layout.tsx**: Layout component for all supported chains pages
- **loading.tsx**: Loading state component for supported chains pages
- **page.tsx**: Main supported chains landing page
@@ -52,4 +52,4 @@ The chain pages utilize specialized components from the `_components` directory:
- Optimize for both technical and non-technical audiences
- Consider performance when loading chain-specific resources
- Ensure all chain information is properly categorized and searchable
-- Update content when chain support details change
\ No newline at end of file
+- Update content when chain support details change
diff --git a/app/[lang]/(resources)/chains/[slug]/page.tsx b/app/[lang]/(resources)/chains/[slug]/page.tsx
index 5feb84d..6b8cd18 100644
--- a/app/[lang]/(resources)/chains/[slug]/page.tsx
+++ b/app/[lang]/(resources)/chains/[slug]/page.tsx
@@ -1,125 +1,109 @@
import Image from 'next/image'
-import {notFound} from 'next/navigation'
+import { notFound } from 'next/navigation'
-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 {getSupportedChain} from '@/app/[lang]/_utils/query'
+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 type {TSupportedChainData} from '@/app/[lang]/_components/strapi/types'
-import type {Metadata} from 'next'
-import type {ReactNode} from 'react'
+import type { TSupportedChainData } from '@/app/[lang]/_components/strapi/types'
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
-export async function generateMetadata({params}: {params: Promise<{slug: string}>}): Promise {
- const {slug} = await params
- if (!slug) {
- return notFound()
- }
+export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise {
+ const { slug } = await params
+ if (!slug) {
+ 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 data = await response.json()
- const chain = data.data[0] as TSupportedChainData
- if (!chain) {
- 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 data = await response.json()
+ const chain = data.data[0] as TSupportedChainData
+ if (!chain) {
+ return notFound()
+ }
- const imageUrl = chain.featuredImg?.formats?.thumbnail?.url || chain.featuredImg?.url
- const metadata: Metadata = {
- title: `${chain.name} | ShapeShift Chains`,
- description: `ShapeShift supports ${chain.name}! Use it now to buy, sell, and swap crypto.`,
- keywords: `${chain.name}, ShapeShift`,
- openGraph: {
- title: chain.name,
- description: `ShapeShift supports ${chain.name}! Use it now to buy, sell, and swap crypto.`,
- type: 'website'
- },
- twitter: {
- card: 'summary_large_image',
- title: chain.name,
- description: `ShapeShift supports ${chain.name}! Use it now to buy, sell, and swap crypto.`
- }
- }
+ const imageUrl = chain.featuredImg?.formats?.thumbnail?.url || chain.featuredImg?.url
+ const metadata: Metadata = {
+ title: `${chain.name} | ShapeShift Chains`,
+ description: `ShapeShift supports ${chain.name}! Use it now to buy, sell, and swap crypto.`,
+ keywords: `${chain.name}, ShapeShift`,
+ openGraph: {
+ title: chain.name,
+ description: `ShapeShift supports ${chain.name}! Use it now to buy, sell, and swap crypto.`,
+ type: 'website',
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: chain.name,
+ description: `ShapeShift supports ${chain.name}! Use it now to buy, sell, and swap crypto.`,
+ },
+ }
- if (imageUrl) {
- metadata.openGraph!.images = [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}${imageUrl}`
- }
- ]
- metadata.twitter!.images = [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}${imageUrl}`
- }
- ]
- }
+ if (imageUrl) {
+ metadata.openGraph!.images = [
+ {
+ url: getStrapiImageUrl(imageUrl),
+ },
+ ]
+ metadata.twitter!.images = [
+ {
+ url: getStrapiImageUrl(imageUrl),
+ },
+ ]
+ }
- return metadata
+ return metadata
}
-export default async function ChainPage({params}: {params: Promise<{slug: string}>}): Promise {
- const {slug} = await params
- const chain = await getSupportedChain(slug)
+export default async function ChainPage({ params }: { params: Promise<{ slug: string }> }): Promise {
+ const { slug } = await params
+ const chain = await getSupportedChain(slug)
- if (!chain) {
- return notFound()
- }
+ if (!chain) {
+ return notFound()
+ }
- return (
-
-
-
-
-
-
+ return (
+
+
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
-
- )
+
+
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/chains/_components/ChainSearchWrapper.tsx b/app/[lang]/(resources)/chains/_components/ChainSearchWrapper.tsx
index 592fc9a..99aef49 100644
--- a/app/[lang]/(resources)/chains/_components/ChainSearchWrapper.tsx
+++ b/app/[lang]/(resources)/chains/_components/ChainSearchWrapper.tsx
@@ -1,19 +1,19 @@
'use client'
-import {useState} from 'react'
+import { useState } from 'react'
-import {Dropdown} from '@/app/[lang]/_components/Dropdown'
-import {SearchBar} from '@/app/[lang]/_components/SearchBar'
-import {CHAIN_TYPES} from '@/app/[lang]/_utils/constants'
+import { Dropdown } from '@/app/[lang]/_components/Dropdown'
+import { SearchBar } from '@/app/[lang]/_components/SearchBar'
+import { CHAIN_TYPES } from '@/app/[lang]/_utils/constants'
-import {ChainList} from '../../_components/ChainList'
+import { ChainList } from '../../_components/ChainList'
-import type {TSupportedChainData, TSupportedChainTypes} from '@/app/[lang]/_components/strapi/types'
-import type {ReactNode} from 'react'
+import type { TSupportedChainData, TSupportedChainTypes } from '@/app/[lang]/_components/strapi/types'
+import type { ReactNode } from 'react'
type TChainSearchWrapperProps = {
- chains: TSupportedChainData[];
-};
+ chains: TSupportedChainData[]
+}
/**************************************************************************************************
** ChainSearchWrapper Component
@@ -24,61 +24,53 @@ type TChainSearchWrapperProps = {
** @param {TChainSearchWrapperProps} props - Component props containing array of chain data
** @returns {ReactNode} Rendered component with search, filter, and chain list
*************************************************************************************************/
-export function ChainSearchWrapper({chains}: TChainSearchWrapperProps): ReactNode {
- /** State for search query and selected chain type filter **/
- const [searchQuery, setSearchQuery] = useState('')
- const [selectedType, setSelectedType] = useState<'All chains' | TSupportedChainTypes>('All chains')
+export function ChainSearchWrapper({ chains }: TChainSearchWrapperProps): ReactNode {
+ /** State for search query and selected chain type filter **/
+ const [searchQuery, setSearchQuery] = useState('')
+ const [selectedType, setSelectedType] = useState<'All chains' | TSupportedChainTypes>('All chains')
- /**********************************************************************************************
- ** Filter chains based on selected chain type
- ** Returns all chains if 'All chains' is selected, otherwise filters by typeOfChain
- *********************************************************************************************/
- const chainsByType = chains.filter(chain => {
- if (selectedType === 'All chains') {
- return true
- }
- return chain.typeOfChain === selectedType
- })
+ /**********************************************************************************************
+ ** Filter chains based on selected chain type
+ ** Returns all chains if 'All chains' is selected, otherwise filters by typeOfChain
+ *********************************************************************************************/
+ const chainsByType = chains.filter((chain) => {
+ if (selectedType === 'All chains') {
+ return true
+ }
+ return chain.typeOfChain === selectedType
+ })
- /**********************************************************************************************
- ** Further filter chains by search query
- ** Filters the type-filtered chains by matching chain names with search query
- *********************************************************************************************/
- const filteredChains = chainsByType.filter(chain => chain.name.toLowerCase().includes(searchQuery.toLowerCase()))
+ /**********************************************************************************************
+ ** Further filter chains by search query
+ ** Filters the type-filtered chains by matching chain names with search query
+ *********************************************************************************************/
+ const filteredChains = chainsByType.filter((chain) => chain.name.toLowerCase().includes(searchQuery.toLowerCase()))
- /**********************************************************************************************
- ** Handle search input changes
- ** Updates search query state when user types in search bar
- *********************************************************************************************/
- const handleSearch = (query: string): void => {
- setSearchQuery(query)
- }
+ /**********************************************************************************************
+ ** Handle search input changes
+ ** Updates search query state when user types in search bar
+ *********************************************************************************************/
+ const handleSearch = (query: string): void => {
+ setSearchQuery(query)
+ }
- return (
- <>
-
+ return (
+ <>
+
- {/** Chains grid section **/}
-
- >
- )
+ {/** Chains grid section **/}
+
+ >
+ )
}
diff --git a/app/[lang]/(resources)/chains/layout.tsx b/app/[lang]/(resources)/chains/layout.tsx
index d2e5f9f..9ceaa03 100644
--- a/app/[lang]/(resources)/chains/layout.tsx
+++ b/app/[lang]/(resources)/chains/layout.tsx
@@ -1,34 +1,34 @@
-import type {Metadata} from 'next'
-import type {ReactNode} from 'react'
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
export async function generateMetadata(): Promise {
- return {
- title: 'Explore multiple chains with ShapeShift',
- description: 'Discover all the chains ShapeShift supports. Buy, sell, and swap crypto with ease.',
- keywords: 'ShapeShift, Supported Chains',
- openGraph: {
- title: 'Explore multiple chains with ShapeShift',
- description: 'Discover all the chains ShapeShift supports. Buy, sell, and swap crypto with ease.',
- type: 'website',
- images: [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}/og.png`
- }
- ]
- },
- twitter: {
- card: 'summary_large_image',
- title: 'Explore multiple chains with ShapeShift',
- description: 'Discover all the chains ShapeShift supports. Buy, sell, and swap crypto with ease.',
- images: [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}/og.png`
- }
- ]
- }
- }
+ return {
+ title: 'Explore multiple chains with ShapeShift',
+ description: 'Discover all the chains ShapeShift supports. Buy, sell, and swap crypto with ease.',
+ keywords: 'ShapeShift, Supported Chains',
+ openGraph: {
+ title: 'Explore multiple chains with ShapeShift',
+ description: 'Discover all the chains ShapeShift supports. Buy, sell, and swap crypto with ease.',
+ type: 'website',
+ images: [
+ {
+ url: `${process.env.NEXT_PUBLIC_STRAPI_URL}/og.png`,
+ },
+ ],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: 'Explore multiple chains with ShapeShift',
+ description: 'Discover all the chains ShapeShift supports. Buy, sell, and swap crypto with ease.',
+ images: [
+ {
+ url: `${process.env.NEXT_PUBLIC_STRAPI_URL}/og.png`,
+ },
+ ],
+ },
+ }
}
-export default function Layout({children}: {children: ReactNode}): ReactNode {
- return children
+export default function Layout({ children }: { children: ReactNode }): ReactNode {
+ return children
}
diff --git a/app/[lang]/(resources)/chains/page.tsx b/app/[lang]/(resources)/chains/page.tsx
index bc3805c..41bc730 100644
--- a/app/[lang]/(resources)/chains/page.tsx
+++ b/app/[lang]/(resources)/chains/page.tsx
@@ -18,95 +18,92 @@
import Image from 'next/image'
-import {Button} from '@/app/[lang]/_components/Button'
-import {ChainsBanner} from '@/app/[lang]/_components/ChainsBanner'
-import {requestUrl} from '@/app/[lang]/_utils/constants'
+import { Button } from '@/app/[lang]/_components/Button'
+import { ChainsBanner } from '@/app/[lang]/_components/ChainsBanner'
+import { requestUrl } from '@/app/[lang]/_utils/constants'
-import {ResourceHeader} from '../_components/ResourceHeader'
-import {fetchAllChains} from '../_utils/fetchUtils'
-import {ChainSearchWrapper} from './_components/ChainSearchWrapper'
+import { ResourceHeader } from '../_components/ResourceHeader'
+import { fetchAllChains } from '../_utils/fetchUtils'
+import { ChainSearchWrapper } from './_components/ChainSearchWrapper'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
// Static content for the page
const pageContent = {
- title: 'Supported Chains',
- description:
- 'Explore all the blockchain networks integrated into the ShapeShift platform for seamless multi-chain access.',
- features: ['Multi-Chain Support', 'Self-Custody', 'Cross-Chain Trading'],
- ctaButton: {
- text: 'Get Started',
- url: 'https://app.shapeshift.com/'
- }
+ title: 'Supported Chains',
+ description:
+ 'Explore all the blockchain networks integrated into the ShapeShift platform for seamless multi-chain access.',
+ features: ['Multi-Chain Support', 'Self-Custody', 'Cross-Chain Trading'],
+ ctaButton: {
+ text: 'Get Started',
+ url: 'https://app.shapeshift.com/',
+ },
}
export default async function SupportedChainsPage(): Promise {
- // Fetch chains data
- const chains = await fetchAllChains()
+ // Fetch chains data
+ const chains = await fetchAllChains()
- // Handle loading and error states
- if (!chains) {
- return (
-
-
{'Unable to load chain data. Please try again later.'}
-
- )
- }
+ // Handle loading and error states
+ if (!chains) {
+ return (
+
+
{'Unable to load chain data. Please try again later.'}
+
+ )
+ }
- return (
-
-
- {/* Reusable header component */}
-
+ return (
+
+
+ {/* Reusable header component */}
+
-
-
-
-
-
-
-
- {"Don't see your chain? Request it here"}
-
-
-
-
-
-
+
+
+
+
+
+
+
+ {"Don't see your chain? Request it here"}
+
+
+
+
+
+
- {/* Footer banner */}
-
-
-
-
-
- )
+ {/* Footer banner */}
+
+
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/discover/README.md b/app/[lang]/(resources)/discover/README.md
index b3ecf6c..1a19e57 100644
--- a/app/[lang]/(resources)/discover/README.md
+++ b/app/[lang]/(resources)/discover/README.md
@@ -5,7 +5,7 @@ This directory contains pages that showcase ShapeShift's features, products, and
## Directory Structure
- **[slug]/**: Dynamic routes for individual discover feature pages
- - **page.tsx**: Page component for individual feature details
+ - **page.tsx**: Page component for individual feature details
- **layout.tsx**: Layout component shared across all discover pages
- **loading.tsx**: Loading state component for discover pages
- **page.tsx**: Main discover landing/index page
@@ -55,4 +55,4 @@ The discover pages utilize specialized components:
- Provide clear calls-to-action for users to try featured functionality
- Consider performance when implementing interactive features
- Maintain a logical progression between related discover pages
-- Update content regularly to reflect new platform capabilities
\ No newline at end of file
+- Update content regularly to reflect new platform capabilities
diff --git a/app/[lang]/(resources)/discover/[slug]/page.tsx b/app/[lang]/(resources)/discover/[slug]/page.tsx
index 19e95b7..02ec8c5 100644
--- a/app/[lang]/(resources)/discover/[slug]/page.tsx
+++ b/app/[lang]/(resources)/discover/[slug]/page.tsx
@@ -16,142 +16,145 @@
** - Generates metadata for SEO and social sharing
************************************************************************************************/
-import {notFound} from 'next/navigation'
+import { notFound } from 'next/navigation'
-import {DiscoverFeature} from '@/app/[lang]/(resources)/_components/DiscoverFeature'
-import {ResourceHeader} from '@/app/[lang]/(resources)/_components/ResourceHeader'
-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 { DiscoverFeature } from '@/app/[lang]/(resources)/_components/DiscoverFeature'
+import { ResourceHeader } from '@/app/[lang]/(resources)/_components/ResourceHeader'
+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 type {Metadata} from 'next'
-import type {ReactNode} from 'react'
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
/************************************************************************************************
* Generates metadata for the discover page
* Provides SEO-optimized title, description, and social sharing tags
************************************************************************************************/
-export async function generateMetadata({params}: {params: Promise<{slug: string}>}): Promise {
- const {slug} = await params
- if (!slug) {
- return notFound()
- }
-
- // Fetch discover data
- const discover = await fetchDiscoverBySlug(slug)
- if (!discover) {
- return notFound()
- }
-
- // Get image URL for metadata
- const imageUrl = discover.featuredImg?.formats?.thumbnail?.url || discover.featuredImg?.url
-
- // Metadata with SEO optimization
- const metadata: Metadata = {
- title: `${discover.title} | Discover with ShapeShift`,
- description: `Discover ${discover.title} with ShapeShift!`,
- keywords: `${discover.title}, ShapeShift, discover, cryptocurrency`,
- openGraph: {
- title: discover.title,
- description: `Discover ${discover.title} with ShapeShift!`,
- type: 'website'
- },
- twitter: {
- card: 'summary_large_image',
- title: discover.title,
- description: `Discover ${discover.title} with ShapeShift!`
- }
- }
-
- if (imageUrl) {
- metadata.openGraph!.images = [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}${imageUrl}`
- }
- ]
- metadata.twitter!.images = [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}${imageUrl}`
- }
- ]
- }
-
- return metadata
+export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise {
+ const { slug } = await params
+ if (!slug) {
+ return notFound()
+ }
+
+ // Fetch discover data
+ const discover = await fetchDiscoverBySlug(slug)
+ if (!discover) {
+ return notFound()
+ }
+
+ // Get image URL for metadata
+ const imageUrl = discover.featuredImg?.formats?.thumbnail?.url || discover.featuredImg?.url
+
+ // Metadata with SEO optimization
+ const metadata: Metadata = {
+ title: `${discover.title} | Discover with ShapeShift`,
+ description: `Discover ${discover.title} with ShapeShift!`,
+ keywords: `${discover.title}, ShapeShift, discover, cryptocurrency`,
+ openGraph: {
+ title: discover.title,
+ description: `Discover ${discover.title} with ShapeShift!`,
+ type: 'website',
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: discover.title,
+ description: `Discover ${discover.title} with ShapeShift!`,
+ },
+ }
+
+ if (imageUrl) {
+ metadata.openGraph!.images = [
+ {
+ url: getStrapiImageUrl(imageUrl),
+ },
+ ]
+ metadata.twitter!.images = [
+ {
+ url: getStrapiImageUrl(imageUrl),
+ },
+ ]
+ }
+
+ return metadata
}
-export default async function DiscoverDetailPage({params}: {params: Promise<{slug: string}>}): Promise {
- // Extract slug from params
- const {slug} = await params
-
- // Fetch discover data using utility function
- const discover = await fetchDiscoverBySlug(slug)
-
- // Handle case when discover data is not found
- if (!discover) {
- console.error(`Discover page not found for slug: ${slug}`)
- return notFound()
- }
-
- // Map discover features to feature section format
- const features = discover.features.map(feature => ({
- id: feature.id,
- title: feature.title,
- description: feature.description,
- image: feature.image
- ? {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}${feature.image.url}`,
- width: feature.image.width,
- height: feature.image.height,
- alt: feature.title
- }
- : undefined,
- buttonCta:
- feature.buttonCta &&
- typeof feature.buttonCta.url === 'string' &&
- /^(\/|https?:\/\/)/i.test(feature.buttonCta.url)
- ? {title: feature.buttonCta.title, url: feature.buttonCta.url}
- : undefined
- }))
-
- return (
-
-
- {/* Header section with feature badges */}
-
-
- {/* Hero banner with discover image */}
-
-
- {/* Features section */}
-
-
- {/* Footer banner */}
-
-
-
-
-
- )
+export default async function DiscoverDetailPage({
+ params,
+}: {
+ params: Promise<{ slug: string }>
+}): Promise {
+ // Extract slug from params
+ const { slug } = await params
+
+ // Fetch discover data using utility function
+ const discover = await fetchDiscoverBySlug(slug)
+
+ // Handle case when discover data is not found
+ if (!discover) {
+ console.error(`Discover page not found for slug: ${slug}`)
+ return notFound()
+ }
+
+ // Map discover features to feature section format
+ const features = discover.features.map((feature) => ({
+ id: feature.id,
+ title: feature.title,
+ description: feature.description,
+ image: feature.image
+ ? {
+ url: getStrapiImageUrl(feature.image.url),
+ width: feature.image.width,
+ height: feature.image.height,
+ alt: feature.title,
+ }
+ : undefined,
+ buttonCta:
+ feature.buttonCta && typeof feature.buttonCta.url === 'string' && /^(\/|https?:\/\/)/i.test(feature.buttonCta.url)
+ ? { title: feature.buttonCta.title, url: feature.buttonCta.url }
+ : undefined,
+ }))
+
+ return (
+
+
+ {/* Header section with feature badges */}
+
+
+ {/* Hero banner with discover image */}
+
+
+ {/* Features section */}
+
+
+ {/* Footer banner */}
+
+
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/discover/_components/DiscoverSearchWrapper.tsx b/app/[lang]/(resources)/discover/_components/DiscoverSearchWrapper.tsx
index 7f09174..dfee397 100644
--- a/app/[lang]/(resources)/discover/_components/DiscoverSearchWrapper.tsx
+++ b/app/[lang]/(resources)/discover/_components/DiscoverSearchWrapper.tsx
@@ -1,16 +1,16 @@
'use client'
-import {useState} from 'react'
+import { useState } from 'react'
-import {SearchBar} from '@/app/[lang]/_components/SearchBar'
-import {StrapiDiscover} from '@/app/[lang]/_components/StrapiDiscover'
+import { SearchBar } from '@/app/[lang]/_components/SearchBar'
+import { StrapiDiscover } from '@/app/[lang]/_components/StrapiDiscover'
-import type {TDiscoverData} from '@/app/[lang]/_components/strapi/types'
-import type {ReactNode} from 'react'
+import type { TDiscoverData } from '@/app/[lang]/_components/strapi/types'
+import type { ReactNode } from 'react'
type TDiscoverSearchWrapperProps = {
- discover: TDiscoverData[] | null;
-};
+ discover: TDiscoverData[] | null
+}
/**************************************************************************************************
** DiscoverSearchWrapper Component
@@ -21,53 +21,48 @@ type TDiscoverSearchWrapperProps = {
** @param {TDiscoverSearchWrapperProps} props - Component props containing array of discover data
** @returns {ReactNode} Rendered component with search and discover list
*************************************************************************************************/
-export function DiscoverSearchWrapper({discover}: TDiscoverSearchWrapperProps): ReactNode {
- const [searchQuery, setSearchQuery] = useState('')
+export function DiscoverSearchWrapper({ discover }: TDiscoverSearchWrapperProps): ReactNode {
+ const [searchQuery, setSearchQuery] = useState('')
- /**********************************************************************************************
- ** Filter discover items by search query
- ** Filters items by matching names with search query
- *********************************************************************************************/
- const filteredDiscover = discover?.filter(
- item =>
- item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
- item.tag.toLowerCase().includes(searchQuery.toLowerCase())
- )
+ /**********************************************************************************************
+ ** Filter discover items by search query
+ ** Filters items by matching names with search query
+ *********************************************************************************************/
+ const filteredDiscover = discover?.filter(
+ (item) =>
+ item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ item.tag.toLowerCase().includes(searchQuery.toLowerCase())
+ )
- /**********************************************************************************************
- ** Group discover items by type for organized display
- ** This reduces the array of discover items into an object where:
- ** - Keys are the different types (categories) of discover items
- ** - Values are arrays of items belonging to each type
- *********************************************************************************************/
- const groupedDiscover = filteredDiscover?.reduce(
- (acc, item) => {
- const type = item.type
- if (!acc[type]) {
- acc[type] = []
- }
- acc[type].push(item)
- return acc
- },
- {} as Record
- )
+ /**********************************************************************************************
+ ** Group discover items by type for organized display
+ ** This reduces the array of discover items into an object where:
+ ** - Keys are the different types (categories) of discover items
+ ** - Values are arrays of items belonging to each type
+ *********************************************************************************************/
+ const groupedDiscover = filteredDiscover?.reduce(
+ (acc, item) => {
+ const type = item.type
+ if (!acc[type]) {
+ acc[type] = []
+ }
+ acc[type].push(item)
+ return acc
+ },
+ {} as Record
+ )
- return (
-
-
+ return (
+
+
- {groupedDiscover &&
- Object.entries(groupedDiscover).map(([type, items]) => (
-
- ))}
-
- )
+ {groupedDiscover &&
+ Object.entries(groupedDiscover).map(([type, items]) => (
+
+ ))}
+
+ )
}
diff --git a/app/[lang]/(resources)/discover/layout.tsx b/app/[lang]/(resources)/discover/layout.tsx
index f6b298e..55dd750 100644
--- a/app/[lang]/(resources)/discover/layout.tsx
+++ b/app/[lang]/(resources)/discover/layout.tsx
@@ -1,34 +1,34 @@
-import type {Metadata} from 'next'
-import type {ReactNode} from 'react'
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
export async function generateMetadata(): Promise {
- return {
- title: 'Explore Web3 with ShapeShift',
- description: 'Discover the world of Web3 with ShapeShift.',
- keywords: 'ShapeShift, Explore Web3',
- openGraph: {
- title: 'Explore Web3 with ShapeShift',
- description: 'Discover the world of Web3 with ShapeShift.',
- type: 'website',
- images: [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}/og.png`
- }
- ]
- },
- twitter: {
- card: 'summary_large_image',
- title: 'Explore Web3 with ShapeShift',
- description: 'Discover the world of Web3 with ShapeShift.',
- images: [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}/og.png`
- }
- ]
- }
- }
+ return {
+ title: 'Explore Web3 with ShapeShift',
+ description: 'Discover the world of Web3 with ShapeShift.',
+ keywords: 'ShapeShift, Explore Web3',
+ openGraph: {
+ title: 'Explore Web3 with ShapeShift',
+ description: 'Discover the world of Web3 with ShapeShift.',
+ type: 'website',
+ images: [
+ {
+ url: `${process.env.NEXT_PUBLIC_STRAPI_URL}/og.png`,
+ },
+ ],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: 'Explore Web3 with ShapeShift',
+ description: 'Discover the world of Web3 with ShapeShift.',
+ images: [
+ {
+ url: `${process.env.NEXT_PUBLIC_STRAPI_URL}/og.png`,
+ },
+ ],
+ },
+ }
}
-export default function Layout({children}: {children: ReactNode}): ReactNode {
- return children
+export default function Layout({ children }: { children: ReactNode }): ReactNode {
+ return children
}
diff --git a/app/[lang]/(resources)/discover/page.tsx b/app/[lang]/(resources)/discover/page.tsx
index c10214d..ce8f21b 100644
--- a/app/[lang]/(resources)/discover/page.tsx
+++ b/app/[lang]/(resources)/discover/page.tsx
@@ -1,35 +1,31 @@
-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 { 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 { DiscoverSearchWrapper } from './_components/DiscoverSearchWrapper'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
export default async function DiscoverPage(): Promise {
- const discover = await getDiscovers()
+ const discover = await getDiscovers()
- return (
-
-
-
-
-
{RESOURCES_DICT.discover.title}
-
-
-
+ return (
+
+
+
+
+
{RESOURCES_DICT.discover.title}
+
+
+
-
+
-
-
-
-
-
- )
+
+
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/faq/README.md b/app/[lang]/(resources)/faq/README.md
index 0da2fb9..faad493 100644
--- a/app/[lang]/(resources)/faq/README.md
+++ b/app/[lang]/(resources)/faq/README.md
@@ -29,9 +29,9 @@ The FAQ section likely organizes content into categories such as:
## Technical Implementation
-- Utilizes specialized components from the _components directory:
- - **FAQContent.tsx**: For rendering the actual FAQ content
- - **FAQNavigation.tsx**: For category navigation
+- Utilizes specialized components from the \_components directory:
+ - **FAQContent.tsx**: For rendering the actual FAQ content
+ - **FAQNavigation.tsx**: For category navigation
- Likely implements collapsible/expandable question sections
- May integrate with a CMS for content management
- Implements responsive design for all device sizes
@@ -54,4 +54,4 @@ The FAQ section likely organizes content into categories such as:
- Ensure responsive behavior for all device sizes
- Optimize for readability and easy scanning
- Provide links to more detailed resources where appropriate
-- Consider implementing user feedback mechanism for FAQ usefulness
\ No newline at end of file
+- Consider implementing user feedback mechanism for FAQ usefulness
diff --git a/app/[lang]/(resources)/faq/page.tsx b/app/[lang]/(resources)/faq/page.tsx
index ccd6cae..803f870 100644
--- a/app/[lang]/(resources)/faq/page.tsx
+++ b/app/[lang]/(resources)/faq/page.tsx
@@ -16,35 +16,35 @@
** - Handles data loading errors gracefully
************************************************************************************************/
-import {notFound} from 'next/navigation'
+import { notFound } from 'next/navigation'
-import {FAQContent} from '@/app/[lang]/(resources)/_components/FAQContent'
-import {fetchFaqData} from '@/app/[lang]/(resources)/_utils/fetchUtils'
-import {RESOURCES_DICT} from '@/app/[lang]/_utils/dictionary/resources'
+import { FAQContent } from '@/app/[lang]/(resources)/_components/FAQContent'
+import { fetchFaqData } from '@/app/[lang]/(resources)/_utils/fetchUtils'
+import { RESOURCES_DICT } from '@/app/[lang]/_utils/dictionary/resources'
-import type {Metadata} from 'next'
-import type {ReactNode} from 'react'
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
export const metadata: Metadata = {
- title: RESOURCES_DICT.faq.metadata.title,
- description: RESOURCES_DICT.faq.metadata.description,
- keywords: RESOURCES_DICT.faq.metadata.keywords
+ title: RESOURCES_DICT.faq.metadata.title,
+ description: RESOURCES_DICT.faq.metadata.description,
+ keywords: RESOURCES_DICT.faq.metadata.keywords,
}
export default async function FAQPage(): Promise {
- // Fetch FAQ data using centralized utility
- const faqData = await fetchFaqData()
+ // Fetch FAQ data using centralized utility
+ const faqData = await fetchFaqData()
- // Handle missing data case
- if (!faqData) {
- console.error('Failed to load FAQ data')
- return notFound()
- }
+ // Handle missing data case
+ if (!faqData) {
+ console.error('Failed to load FAQ data')
+ return notFound()
+ }
- return (
- <>
- {/* FAQ content with navigation */}
-
- >
- )
+ return (
+ <>
+ {/* FAQ content with navigation */}
+
+ >
+ )
}
diff --git a/app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/ListOfPosts.tsx b/app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/ListOfPosts.tsx
index 4790198..da1bd3c 100644
--- a/app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/ListOfPosts.tsx
+++ b/app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/ListOfPosts.tsx
@@ -1,88 +1,78 @@
'use client'
-import {Fragment, useState} from 'react'
+import { Fragment, useState } from 'react'
import ReactPaginate from 'react-paginate'
-import {Banner} from '@/app/[lang]/_components/Banner'
-import {NewsPost} from '@/app/[lang]/_components/NewsPost'
-import {useFetchNewsroom} from '@/app/[lang]/_hooks/useFetchNewsroom'
-import {IconChevron} from '@/app/[lang]/_icons/IconChevron'
-import {cl} from '@/app/[lang]/_utils/cl'
-import {newsroomCategoriesSlugToCategory} from '@/app/[lang]/_utils/constants'
+import { Banner } from '@/app/[lang]/_components/Banner'
+import { NewsPost } from '@/app/[lang]/_components/NewsPost'
+import { useFetchNewsroom } from '@/app/[lang]/_hooks/useFetchNewsroom'
+import { IconChevron } from '@/app/[lang]/_icons/IconChevron'
+import { cl } from '@/app/[lang]/_utils/cl'
+import { newsroomCategoriesSlugToCategory } from '@/app/[lang]/_utils/constants'
-import type {ReactElement} from 'react'
+import type { ReactElement } from 'react'
const PAGE_SIZE = 12
const SORT = 'desc'
-export function ListOfPosts(props: {category: string}): ReactElement {
- const {category} = props
- const [page, setPage] = useState(1)
- const {posts, pagination, isLoading} = useFetchNewsroom({
- page,
- pageSize: PAGE_SIZE,
- sort: SORT,
- category: newsroomCategoriesSlugToCategory(category),
- populateContent: true,
- cachePosts: true
- })
+export function ListOfPosts(props: { category: string }): ReactElement {
+ const { category } = props
+ const [page, setPage] = useState(1)
+ const { posts, pagination, isLoading } = useFetchNewsroom({
+ page,
+ pageSize: PAGE_SIZE,
+ sort: SORT,
+ category: newsroomCategoriesSlugToCategory(category),
+ populateContent: true,
+ cachePosts: true,
+ })
- if (isLoading) {
- return (
-
- {[...Array(PAGE_SIZE)].map((_, i) => (
-
- ))}
-
- )
- }
+ if (isLoading) {
+ return (
+
+ {[...Array(PAGE_SIZE)].map((_, i) => (
+
+ ))}
+
+ )
+ }
- return (
-
- {posts.length === 0 ? (
-
- {"We couldn't find any blog posts matching your criteria."}
-
- ) : (
-
- {posts.map(post => (
-
- ))}
-
- )}
+ return (
+
+ {posts.length === 0 ? (
+
+ {"We couldn't find any blog posts matching your criteria."}
+
+ ) : (
+
+ {posts.map((post) => (
+
+ ))}
+
+ )}
- setPage(selected + 1)}
- containerClassName={'flex gap-2 items-center justify-center mb-16'}
- pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
- pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
- activeClassName={'!opacity-100'}
- previousClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === 1 ? 'opacity-20' : 'opacity-100'
- )}
- previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
- )}
- disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
- disabledLinkClassName={'cursor-not-allowed'}
- previousLabel={ }
- nextLabel={ }
- />
+ setPage(selected + 1)}
+ containerClassName={'flex gap-2 items-center justify-center mb-16'}
+ pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
+ pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ activeClassName={'!opacity-100'}
+ previousClassName={cl('hover:opacity-100 transition-opacity', page === 1 ? 'opacity-20' : 'opacity-100')}
+ previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextClassName={cl(
+ 'hover:opacity-100 transition-opacity',
+ page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
+ )}
+ disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
+ disabledLinkClassName={'cursor-not-allowed'}
+ previousLabel={ }
+ nextLabel={ }
+ />
-
-
- )
+
+
+ )
}
diff --git a/app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/page.tsx b/app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/page.tsx
index c5981b9..6a43939 100644
--- a/app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/page.tsx
+++ b/app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/page.tsx
@@ -1,24 +1,24 @@
-import {notFound} from 'next/navigation'
+import { notFound } from 'next/navigation'
-import {ListOfPosts} from '@/app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/ListOfPosts'
+import { ListOfPosts } from '@/app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/ListOfPosts'
export default async function BlogCategoriesPage(props: {
- params: Promise<{category: string}>;
+ 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: posts} = await data.json()
+ 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: posts } = await data.json()
- if (!posts) {
- return notFound()
- }
+ if (!posts) {
+ return notFound()
+ }
- return
+ return
}
diff --git a/app/[lang]/(resources)/newsroom/(withNavigation)/categories/page.tsx b/app/[lang]/(resources)/newsroom/(withNavigation)/categories/page.tsx
index fe86dd7..7d9e6e9 100644
--- a/app/[lang]/(resources)/newsroom/(withNavigation)/categories/page.tsx
+++ b/app/[lang]/(resources)/newsroom/(withNavigation)/categories/page.tsx
@@ -1,84 +1,74 @@
'use client'
-import {Fragment, useState} from 'react'
+import { Fragment, useState } from 'react'
import ReactPaginate from 'react-paginate'
-import {Banner} from '@/app/[lang]/_components/Banner'
-import {NewsPost} from '@/app/[lang]/_components/NewsPost'
-import {useFetchNewsroom} from '@/app/[lang]/_hooks/useFetchNewsroom'
-import {IconChevron} from '@/app/[lang]/_icons/IconChevron'
-import {cl} from '@/app/[lang]/_utils/cl'
+import { Banner } from '@/app/[lang]/_components/Banner'
+import { NewsPost } from '@/app/[lang]/_components/NewsPost'
+import { useFetchNewsroom } from '@/app/[lang]/_hooks/useFetchNewsroom'
+import { IconChevron } from '@/app/[lang]/_icons/IconChevron'
+import { cl } from '@/app/[lang]/_utils/cl'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
const PAGE_SIZE = 12
const SORT = 'desc'
export default function BlogList(): ReactNode {
- const [page, setPage] = useState(1)
- const {posts, pagination, isLoading} = useFetchNewsroom({
- page,
- pageSize: PAGE_SIZE,
- sort: SORT,
- populateContent: true,
- cachePosts: true
- })
+ const [page, setPage] = useState(1)
+ const { posts, pagination, isLoading } = useFetchNewsroom({
+ page,
+ pageSize: PAGE_SIZE,
+ sort: SORT,
+ populateContent: true,
+ cachePosts: true,
+ })
- if (isLoading) {
- return (
-
- {[...Array(PAGE_SIZE)].map((_, i) => (
-
- ))}
-
- )
- }
+ if (isLoading) {
+ return (
+
+ {[...Array(PAGE_SIZE)].map((_, i) => (
+
+ ))}
+
+ )
+ }
- return (
-
- {posts.length === 0 ? (
-
- {"We couldn't find any blog posts matching your criteria."}
-
- ) : (
-
- {posts.map(post => (
-
- ))}
-
- )}
- setPage(selected + 1)}
- containerClassName={'flex gap-2 items-center justify-center'}
- pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
- pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
- activeClassName={'!opacity-100'}
- previousClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === 1 ? 'opacity-20' : 'opacity-100'
- )}
- previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
- )}
- disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
- disabledLinkClassName={'cursor-not-allowed'}
- previousLabel={ }
- nextLabel={ }
- />
-
-
- )
+ return (
+
+ {posts.length === 0 ? (
+
+ {"We couldn't find any blog posts matching your criteria."}
+
+ ) : (
+
+ {posts.map((post) => (
+
+ ))}
+
+ )}
+ setPage(selected + 1)}
+ containerClassName={'flex gap-2 items-center justify-center'}
+ pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
+ pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ activeClassName={'!opacity-100'}
+ previousClassName={cl('hover:opacity-100 transition-opacity', page === 1 ? 'opacity-20' : 'opacity-100')}
+ previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextClassName={cl(
+ 'hover:opacity-100 transition-opacity',
+ page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
+ )}
+ disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
+ disabledLinkClassName={'cursor-not-allowed'}
+ previousLabel={ }
+ nextLabel={ }
+ />
+
+
+ )
}
diff --git a/app/[lang]/(resources)/newsroom/(withNavigation)/layout.tsx b/app/[lang]/(resources)/newsroom/(withNavigation)/layout.tsx
index 2491bbd..08e74c3 100644
--- a/app/[lang]/(resources)/newsroom/(withNavigation)/layout.tsx
+++ b/app/[lang]/(resources)/newsroom/(withNavigation)/layout.tsx
@@ -1,19 +1,19 @@
-import {NewsroomBreadcrumb} from '@/app/[lang]/(resources)/_components/NewsroomBreadcrumb'
-import {NewsroomNav} from '@/app/[lang]/(resources)/_components/NewsroomNav'
-import {NewsroomTitle} from '@/app/[lang]/(resources)/_components/NewsroomTitle'
+import { NewsroomBreadcrumb } from '@/app/[lang]/(resources)/_components/NewsroomBreadcrumb'
+import { NewsroomNav } from '@/app/[lang]/(resources)/_components/NewsroomNav'
+import { NewsroomTitle } from '@/app/[lang]/(resources)/_components/NewsroomTitle'
export default async function BlogPageLayout(props: {
- children: React.ReactNode;
- params: Promise<{category: string}>;
+ children: React.ReactNode
+ params: Promise<{ category: string }>
}): Promise {
- await props.params
+ await props.params
- return (
-
-
-
-
- {props.children}
-
- )
+ return (
+
+
+
+
+ {props.children}
+
+ )
}
diff --git a/app/[lang]/(resources)/newsroom/(withNavigation)/page.tsx b/app/[lang]/(resources)/newsroom/(withNavigation)/page.tsx
index fe86dd7..7d9e6e9 100644
--- a/app/[lang]/(resources)/newsroom/(withNavigation)/page.tsx
+++ b/app/[lang]/(resources)/newsroom/(withNavigation)/page.tsx
@@ -1,84 +1,74 @@
'use client'
-import {Fragment, useState} from 'react'
+import { Fragment, useState } from 'react'
import ReactPaginate from 'react-paginate'
-import {Banner} from '@/app/[lang]/_components/Banner'
-import {NewsPost} from '@/app/[lang]/_components/NewsPost'
-import {useFetchNewsroom} from '@/app/[lang]/_hooks/useFetchNewsroom'
-import {IconChevron} from '@/app/[lang]/_icons/IconChevron'
-import {cl} from '@/app/[lang]/_utils/cl'
+import { Banner } from '@/app/[lang]/_components/Banner'
+import { NewsPost } from '@/app/[lang]/_components/NewsPost'
+import { useFetchNewsroom } from '@/app/[lang]/_hooks/useFetchNewsroom'
+import { IconChevron } from '@/app/[lang]/_icons/IconChevron'
+import { cl } from '@/app/[lang]/_utils/cl'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
const PAGE_SIZE = 12
const SORT = 'desc'
export default function BlogList(): ReactNode {
- const [page, setPage] = useState(1)
- const {posts, pagination, isLoading} = useFetchNewsroom({
- page,
- pageSize: PAGE_SIZE,
- sort: SORT,
- populateContent: true,
- cachePosts: true
- })
+ const [page, setPage] = useState(1)
+ const { posts, pagination, isLoading } = useFetchNewsroom({
+ page,
+ pageSize: PAGE_SIZE,
+ sort: SORT,
+ populateContent: true,
+ cachePosts: true,
+ })
- if (isLoading) {
- return (
-
- {[...Array(PAGE_SIZE)].map((_, i) => (
-
- ))}
-
- )
- }
+ if (isLoading) {
+ return (
+
+ {[...Array(PAGE_SIZE)].map((_, i) => (
+
+ ))}
+
+ )
+ }
- return (
-
- {posts.length === 0 ? (
-
- {"We couldn't find any blog posts matching your criteria."}
-
- ) : (
-
- {posts.map(post => (
-
- ))}
-
- )}
- setPage(selected + 1)}
- containerClassName={'flex gap-2 items-center justify-center'}
- pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
- pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
- activeClassName={'!opacity-100'}
- previousClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === 1 ? 'opacity-20' : 'opacity-100'
- )}
- previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
- )}
- disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
- disabledLinkClassName={'cursor-not-allowed'}
- previousLabel={ }
- nextLabel={ }
- />
-
-
- )
+ return (
+
+ {posts.length === 0 ? (
+
+ {"We couldn't find any blog posts matching your criteria."}
+
+ ) : (
+
+ {posts.map((post) => (
+
+ ))}
+
+ )}
+ setPage(selected + 1)}
+ containerClassName={'flex gap-2 items-center justify-center'}
+ pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
+ pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ activeClassName={'!opacity-100'}
+ previousClassName={cl('hover:opacity-100 transition-opacity', page === 1 ? 'opacity-20' : 'opacity-100')}
+ previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextClassName={cl(
+ 'hover:opacity-100 transition-opacity',
+ page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
+ )}
+ disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
+ disabledLinkClassName={'cursor-not-allowed'}
+ previousLabel={ }
+ nextLabel={ }
+ />
+
+
+ )
}
diff --git a/app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/ListOfPosts.tsx b/app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/ListOfPosts.tsx
index e383aea..5b6ac30 100644
--- a/app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/ListOfPosts.tsx
+++ b/app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/ListOfPosts.tsx
@@ -1,87 +1,77 @@
'use client'
-import {Fragment, useState} from 'react'
+import { Fragment, useState } from 'react'
import ReactPaginate from 'react-paginate'
-import {Banner} from '@/app/[lang]/_components/Banner'
-import {NewsPost} from '@/app/[lang]/_components/NewsPost'
-import {useFetchNewsroom} from '@/app/[lang]/_hooks/useFetchNewsroom'
-import {IconChevron} from '@/app/[lang]/_icons/IconChevron'
-import {cl} from '@/app/[lang]/_utils/cl'
+import { Banner } from '@/app/[lang]/_components/Banner'
+import { NewsPost } from '@/app/[lang]/_components/NewsPost'
+import { useFetchNewsroom } from '@/app/[lang]/_hooks/useFetchNewsroom'
+import { IconChevron } from '@/app/[lang]/_icons/IconChevron'
+import { cl } from '@/app/[lang]/_utils/cl'
-import type {ReactElement} from 'react'
+import type { ReactElement } from 'react'
const PAGE_SIZE = 12
const SORT = 'desc'
-export function ListOfPosts(props: {tag: string}): ReactElement {
- const {tag} = props
- const [page, setPage] = useState(1)
- const {posts, pagination, isLoading} = useFetchNewsroom({
- page,
- pageSize: PAGE_SIZE,
- sort: SORT,
- tag,
- populateContent: true,
- cachePosts: true
- })
+export function ListOfPosts(props: { tag: string }): ReactElement {
+ const { tag } = props
+ const [page, setPage] = useState(1)
+ const { posts, pagination, isLoading } = useFetchNewsroom({
+ page,
+ pageSize: PAGE_SIZE,
+ sort: SORT,
+ tag,
+ populateContent: true,
+ cachePosts: true,
+ })
- if (isLoading) {
- return (
-
- {[...Array(PAGE_SIZE)].map((_, i) => (
-
- ))}
-
- )
- }
+ if (isLoading) {
+ return (
+
+ {[...Array(PAGE_SIZE)].map((_, i) => (
+
+ ))}
+
+ )
+ }
- return (
-
- {posts.length === 0 ? (
-
- {"We couldn't find any blog posts matching your criteria."}
-
- ) : (
-
- {posts.map(post => (
-
- ))}
-
- )}
+ return (
+
+ {posts.length === 0 ? (
+
+ {"We couldn't find any blog posts matching your criteria."}
+
+ ) : (
+
+ {posts.map((post) => (
+
+ ))}
+
+ )}
- setPage(selected + 1)}
- containerClassName={'flex gap-2 items-center justify-center mb-16'}
- pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
- pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
- activeClassName={'!opacity-100'}
- previousClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === 1 ? 'opacity-20' : 'opacity-100'
- )}
- previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
- )}
- disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
- disabledLinkClassName={'cursor-not-allowed'}
- previousLabel={ }
- nextLabel={ }
- />
+ setPage(selected + 1)}
+ containerClassName={'flex gap-2 items-center justify-center mb-16'}
+ pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
+ pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ activeClassName={'!opacity-100'}
+ previousClassName={cl('hover:opacity-100 transition-opacity', page === 1 ? 'opacity-20' : 'opacity-100')}
+ previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextClassName={cl(
+ 'hover:opacity-100 transition-opacity',
+ page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
+ )}
+ disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
+ disabledLinkClassName={'cursor-not-allowed'}
+ previousLabel={ }
+ nextLabel={ }
+ />
-
-
- )
+
+
+ )
}
diff --git a/app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/page.tsx b/app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/page.tsx
index 9166f48..a809d31 100644
--- a/app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/page.tsx
+++ b/app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/page.tsx
@@ -1,22 +1,22 @@
-import {notFound} from 'next/navigation'
+import { notFound } from 'next/navigation'
-import {ListOfPosts} from '@/app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/ListOfPosts'
+import { ListOfPosts } from '@/app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/ListOfPosts'
-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: posts} = await data.json()
+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: posts } = await data.json()
- if (!posts) {
- return notFound()
- }
+ if (!posts) {
+ return notFound()
+ }
- return
+ return
}
diff --git a/app/[lang]/(resources)/newsroom/(withNavigation)/tags/page.tsx b/app/[lang]/(resources)/newsroom/(withNavigation)/tags/page.tsx
index fe86dd7..7d9e6e9 100644
--- a/app/[lang]/(resources)/newsroom/(withNavigation)/tags/page.tsx
+++ b/app/[lang]/(resources)/newsroom/(withNavigation)/tags/page.tsx
@@ -1,84 +1,74 @@
'use client'
-import {Fragment, useState} from 'react'
+import { Fragment, useState } from 'react'
import ReactPaginate from 'react-paginate'
-import {Banner} from '@/app/[lang]/_components/Banner'
-import {NewsPost} from '@/app/[lang]/_components/NewsPost'
-import {useFetchNewsroom} from '@/app/[lang]/_hooks/useFetchNewsroom'
-import {IconChevron} from '@/app/[lang]/_icons/IconChevron'
-import {cl} from '@/app/[lang]/_utils/cl'
+import { Banner } from '@/app/[lang]/_components/Banner'
+import { NewsPost } from '@/app/[lang]/_components/NewsPost'
+import { useFetchNewsroom } from '@/app/[lang]/_hooks/useFetchNewsroom'
+import { IconChevron } from '@/app/[lang]/_icons/IconChevron'
+import { cl } from '@/app/[lang]/_utils/cl'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
const PAGE_SIZE = 12
const SORT = 'desc'
export default function BlogList(): ReactNode {
- const [page, setPage] = useState(1)
- const {posts, pagination, isLoading} = useFetchNewsroom({
- page,
- pageSize: PAGE_SIZE,
- sort: SORT,
- populateContent: true,
- cachePosts: true
- })
+ const [page, setPage] = useState(1)
+ const { posts, pagination, isLoading } = useFetchNewsroom({
+ page,
+ pageSize: PAGE_SIZE,
+ sort: SORT,
+ populateContent: true,
+ cachePosts: true,
+ })
- if (isLoading) {
- return (
-
- {[...Array(PAGE_SIZE)].map((_, i) => (
-
- ))}
-
- )
- }
+ if (isLoading) {
+ return (
+
+ {[...Array(PAGE_SIZE)].map((_, i) => (
+
+ ))}
+
+ )
+ }
- return (
-
- {posts.length === 0 ? (
-
- {"We couldn't find any blog posts matching your criteria."}
-
- ) : (
-
- {posts.map(post => (
-
- ))}
-
- )}
- setPage(selected + 1)}
- containerClassName={'flex gap-2 items-center justify-center'}
- pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
- pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
- activeClassName={'!opacity-100'}
- previousClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === 1 ? 'opacity-20' : 'opacity-100'
- )}
- previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
- nextClassName={cl(
- 'hover:opacity-100 transition-opacity',
- page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
- )}
- disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
- disabledLinkClassName={'cursor-not-allowed'}
- previousLabel={ }
- nextLabel={ }
- />
-
-
- )
+ return (
+
+ {posts.length === 0 ? (
+
+ {"We couldn't find any blog posts matching your criteria."}
+
+ ) : (
+
+ {posts.map((post) => (
+
+ ))}
+
+ )}
+ setPage(selected + 1)}
+ containerClassName={'flex gap-2 items-center justify-center'}
+ pageClassName={'opacity-20 hover:opacity-100 transition-opacity'}
+ pageLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ activeClassName={'!opacity-100'}
+ previousClassName={cl('hover:opacity-100 transition-opacity', page === 1 ? 'opacity-20' : 'opacity-100')}
+ previousLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextLinkClassName={'px-6 py-4 flex items-center justify-center'}
+ nextClassName={cl(
+ 'hover:opacity-100 transition-opacity',
+ page === pagination?.pageCount ? 'opacity-20' : 'opacity-100'
+ )}
+ disabledClassName={'hover:opacity-20 opacity-20 transition-opacity'}
+ disabledLinkClassName={'cursor-not-allowed'}
+ previousLabel={ }
+ nextLabel={ }
+ />
+
+
+ )
}
diff --git a/app/[lang]/(resources)/newsroom/README.md b/app/[lang]/(resources)/newsroom/README.md
index 4e6220f..35b0228 100644
--- a/app/[lang]/(resources)/newsroom/README.md
+++ b/app/[lang]/(resources)/newsroom/README.md
@@ -5,15 +5,15 @@ This directory contains the newsroom section of the ShapeShift website, featurin
## Directory Structure
- **(withNavigation)/**: Newsroom pages that include navigation elements
- - **categories/**: Category-based news filtering
- - **[category]/**: Dynamic routes for news categories
- - **tags/**: Tag-based news filtering
- - **[tag]/**: Dynamic routes for news tags
- - **layout.tsx**: Layout for newsroom pages with navigation
- - **page.tsx**: Main newsroom landing page
+ - **categories/**: Category-based news filtering
+ - **[category]/**: Dynamic routes for news categories
+ - **tags/**: Tag-based news filtering
+ - **[tag]/**: Dynamic routes for news tags
+ - **layout.tsx**: Layout for newsroom pages with navigation
+ - **page.tsx**: Main newsroom landing page
- **[slug]/**: Individual news post pages
- - **layout.tsx**: Layout for individual news posts
- - **page.tsx**: Page component for individual news posts
+ - **layout.tsx**: Layout for individual news posts
+ - **page.tsx**: Page component for individual news posts
## Features
@@ -28,10 +28,10 @@ This directory contains the newsroom section of the ShapeShift website, featurin
- Uses Next.js dynamic routing for categories, tags, and post slugs
- Implements dedicated layout for consistent newsroom section UI
- Shares structure similar to the blog section but with news-specific content
-- Utilizes components from the _components directory:
- - **NewsroomBreadcrumb.tsx**: Breadcrumb navigation for newsroom
- - **NewsroomNav.tsx**: Main navigation for newsroom section
- - **NewsroomTitle.tsx**: Title component for newsroom content
+- Utilizes components from the \_components directory:
+ - **NewsroomBreadcrumb.tsx**: Breadcrumb navigation for newsroom
+ - **NewsroomNav.tsx**: Main navigation for newsroom section
+ - **NewsroomTitle.tsx**: Title component for newsroom content
## Content Types
@@ -61,4 +61,4 @@ The newsroom likely contains various types of content:
- Implement proper loading states for content fetching
- Optimize images for faster loading
- Maintain mobile-responsive design
-- Keep navigation intuitive for browsing news archives
\ No newline at end of file
+- Keep navigation intuitive for browsing news archives
diff --git a/app/[lang]/(resources)/newsroom/[slug]/layout.tsx b/app/[lang]/(resources)/newsroom/[slug]/layout.tsx
index 7ff220e..719ae4e 100644
--- a/app/[lang]/(resources)/newsroom/[slug]/layout.tsx
+++ b/app/[lang]/(resources)/newsroom/[slug]/layout.tsx
@@ -1,65 +1,67 @@
-import {notFound} from 'next/navigation'
+import { notFound } from 'next/navigation'
-import type {Metadata} from 'next'
+import { getStrapiImageUrl } from '@/app/[lang]/_utils/query'
+
+import type { Metadata } from 'next'
/************************************************************************************************
* Layout component for blog post pages
* Handles metadata generation for SEO and social sharing
* Uses Next.js 13+ Metadata API
************************************************************************************************/
-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}`
- }
- }
- ).then(async res => res.json())
+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}`,
+ },
+ }
+ ).then(async (res) => res.json())
- const post = data.data[0]
+ const post = data.data[0]
- if (!post) {
- return notFound()
- }
+ if (!post) {
+ return notFound()
+ }
- const imageUrl = post.featuredImg?.formats?.thumbnail?.url || post.featuredImg?.url
- const metadata: Metadata = {
- title: `${post.title} | ShapeShift Newsroom`,
- description: post.postSummary || `Read ${post.title} on ShapeShift Newsroom`,
- keywords: Array.isArray(post.tags) ? post.tags.join(', ') : post.tags,
- openGraph: {
- title: post.title,
- description: post.postSummary,
- type: 'article',
- publishedTime: post.publishedAt,
- authors: ['ShapeShift'],
- tags: Array.isArray(post.tags) ? post.tags : [post.tags]
- },
- twitter: {
- card: 'summary_large_image',
- title: post.title,
- description: post.postSummary || `Read ${post.title} on ShapeShift Newsroom`
- }
- }
+ const imageUrl = post.featuredImg?.formats?.thumbnail?.url || post.featuredImg?.url
+ const metadata: Metadata = {
+ title: `${post.title} | ShapeShift Newsroom`,
+ description: post.postSummary || `Read ${post.title} on ShapeShift Newsroom`,
+ keywords: Array.isArray(post.tags) ? post.tags.join(', ') : post.tags,
+ openGraph: {
+ title: post.title,
+ description: post.postSummary,
+ type: 'article',
+ publishedTime: post.publishedAt,
+ authors: ['ShapeShift'],
+ tags: Array.isArray(post.tags) ? post.tags : [post.tags],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: post.title,
+ description: post.postSummary || `Read ${post.title} on ShapeShift Newsroom`,
+ },
+ }
- if (imageUrl) {
- metadata.openGraph!.images = [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}${imageUrl}`
- }
- ]
- metadata.twitter!.images = [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}${imageUrl}`
- }
- ]
- }
+ if (imageUrl) {
+ metadata.openGraph!.images = [
+ {
+ url: getStrapiImageUrl(imageUrl),
+ },
+ ]
+ metadata.twitter!.images = [
+ {
+ url: getStrapiImageUrl(imageUrl),
+ },
+ ]
+ }
- return metadata
+ return metadata
}
-export default function NewsroomPostLayout({children}: {children: React.ReactNode}): React.ReactNode {
- return children
+export default function NewsroomPostLayout({ children }: { children: React.ReactNode }): React.ReactNode {
+ return children
}
diff --git a/app/[lang]/(resources)/newsroom/[slug]/page.tsx b/app/[lang]/(resources)/newsroom/[slug]/page.tsx
index eda246d..8d7949d 100644
--- a/app/[lang]/(resources)/newsroom/[slug]/page.tsx
+++ b/app/[lang]/(resources)/newsroom/[slug]/page.tsx
@@ -2,314 +2,264 @@
import 'highlight.js/styles/github-dark.css'
import Image from 'next/image'
-import {notFound, useParams, useRouter} from 'next/navigation'
+import { notFound, useParams, useRouter } from 'next/navigation'
import ReactMarkdown from 'react-markdown'
import rehypeHighlight from 'rehype-highlight'
import remarkEmoji from 'remark-emoji' // For emoji support
import remarkGfm from 'remark-gfm'
-import {Banner} from '@/app/[lang]/_components/Banner'
-import {LocalizedLink} from '@/app/[lang]/_components/LocalizedLink'
-import {useCachedNews} from '@/app/[lang]/_contexts/CachedNewsContext'
-import {useFetchNewsroom} from '@/app/[lang]/_hooks/useFetchNewsroom'
-import {IconBack} from '@/app/[lang]/_icons/IconBack'
+import { Banner } from '@/app/[lang]/_components/Banner'
+import { LocalizedLink } from '@/app/[lang]/_components/LocalizedLink'
+import { useCachedNews } from '@/app/[lang]/_contexts/CachedNewsContext'
+import { useFetchNewsroom } from '@/app/[lang]/_hooks/useFetchNewsroom'
+import { IconBack } from '@/app/[lang]/_icons/IconBack'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
function LoadingSkeleton(): ReactNode {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
}
-function BlogContent({content}: {content: string}): ReactNode {
- // Check if content looks like HTML (contains HTML tags)
- const isHtml = /<\/?(?:div|span|p|a|img|h[1-6]|ul|ol|li|table|tr|td|th|br|hr|em|strong)[^>]*>/i.test(content)
+function BlogContent({ content }: { content: string }): ReactNode {
+ // Check if content looks like HTML (contains HTML tags)
+ const isHtml = /<\/?(?:div|span|p|a|img|h[1-6]|ul|ol|li|table|tr|td|th|br|hr|em|strong)[^>]*>/i.test(content)
- return (
-
- {isHtml ? (
- // eslint-disable-next-line @typescript-eslint/naming-convention
-
- ) : (
-
(
-
- ),
- h2: ({...props}) => (
-
- ),
- h3: ({...props}) => (
-
- ),
+ return (
+
+ {isHtml ? (
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+
+ ) : (
+
,
+ h2: ({ ...props }) => ,
+ h3: ({ ...props }) => ,
- // Code blocks
- code: ({className, children, ...props}) => {
- const match = /language-(\w+)/.exec(className || '')
- return match ? (
-
-
{match[1]}
-
-
- {children}
-
-
-
- ) : (
-
- {children}
-
- )
- },
+ // Code blocks
+ code: ({ className, children, ...props }) => {
+ const match = /language-(\w+)/.exec(className || '')
+ return match ? (
+
+
{match[1]}
+
+
+ {children}
+
+
+
+ ) : (
+
+ {children}
+
+ )
+ },
- // Tables
- table: ({...props}) => (
-
- ),
- th: ({...props}) => (
-
- ),
- td: ({...props}) => (
-
- ),
+ // Tables
+ table: ({ ...props }) => (
+
+ ),
+ th: ({ ...props }) => ,
+ td: ({ ...props }) => ,
- // Images
- img: ({...props}) => (
-
- ),
+ // Images
+ img: ({ ...props }) => (
+
+ ),
- // Blockquotes
- blockquote: ({...props}) => (
-
- ),
+ // Blockquotes
+ blockquote: ({ ...props }) => (
+
+ ),
- // Lists
- ul: ({...props}) => (
-
- ),
- ol: ({...props}) => (
-
- ),
+ // Lists
+ ul: ({ ...props }) => ,
+ ol: ({ ...props }) => ,
- // Links
- a: ({...props}) => (
-
- )
- }}>
- {content}
-
- )}
+ // Links
+ a: ({ ...props }) => (
+
+ ),
+ }}
+ >
+ {content}
+
+ )}
-
-
- )
+ .blog-content strong {
+ margin-top: 20px;
+ display: inline-block;
+ }
+ .blog-content img {
+ margin-top: 20px;
+ margin-bottom: 20px;
+ }
+ `}
+
+
+ )
}
export default function BlogPost(): ReactNode {
- const {slug} = useParams()
+ const { slug } = useParams()
- const {
- cachedResponse: {data: cachedPosts}
- } = useCachedNews()
- const {posts, isLoading} = useFetchNewsroom({
- page: 1,
- pageSize: 1,
- sort: 'desc',
- populateContent: true,
- cachePosts: true,
- slug: slug as string,
- skip: !!cachedPosts.find(p => p.slug === slug)
- })
+ const {
+ cachedResponse: { data: cachedPosts },
+ } = useCachedNews()
+ const { posts, isLoading } = useFetchNewsroom({
+ page: 1,
+ pageSize: 1,
+ sort: 'desc',
+ populateContent: true,
+ cachePosts: true,
+ slug: slug as string,
+ skip: !!cachedPosts.find((p) => p.slug === slug),
+ })
- const post = [...cachedPosts, ...posts].find(p => p.slug === slug)
- const router = useRouter()
+ const post = [...cachedPosts, ...posts].find((p) => p.slug === slug)
+ const router = useRouter()
- if (isLoading) {
- return
- }
+ if (isLoading) {
+ return
+ }
- if (!post) {
- notFound()
- }
+ if (!post) {
+ notFound()
+ }
- return (
- <>
-
- router.back()}>
-
- {'Back'}
-
-
- {new Date(post.publishedAt).toLocaleDateString()}
-
+ return (
+ <>
+
+ router.back()}
+ >
+
+ {'Back'}
+
+ {new Date(post.publishedAt).toLocaleDateString()}
-
- {post.tags.map((tag: string, index: number) => (
-
- {`#${tag}`}
-
- ))}
-
- {post.slug.replace(/-/g, ' ')}
-
-
- {!isLoading && (
-
-
-
- )}
- >
- )
+
+ {post.tags.map((tag: string, index: number) => (
+
+ {`#${tag}`}
+
+ ))}
+
+ {post.slug.replace(/-/g, ' ')}
+
+
+ {!isLoading && (
+
+
+
+ )}
+ >
+ )
}
diff --git a/app/[lang]/(resources)/protocols/README.md b/app/[lang]/(resources)/protocols/README.md
index 9f98a10..005173f 100644
--- a/app/[lang]/(resources)/protocols/README.md
+++ b/app/[lang]/(resources)/protocols/README.md
@@ -5,7 +5,7 @@ This directory contains pages that showcase and document the DeFi protocols inte
## Directory Structure
- **[slug]/**: Dynamic routes for individual protocol pages
- - **page.tsx**: Page component for individual protocol details
+ - **page.tsx**: Page component for individual protocol details
- **layout.tsx**: Layout component for all supported protocols pages
- **loading.tsx**: Loading state component for supported protocols pages
- **page.tsx**: Main supported protocols landing page
@@ -59,4 +59,4 @@ The protocol pages utilize specialized components from the `_components` directo
- Ensure all protocol information is properly categorized and searchable
- Update content when protocol integrations change or are enhanced
- Coordinate with protocol teams for accurate representation
-- Include relevant risk disclosures where appropriate
\ No newline at end of file
+- Include relevant risk disclosures where appropriate
diff --git a/app/[lang]/(resources)/protocols/[slug]/page.tsx b/app/[lang]/(resources)/protocols/[slug]/page.tsx
index 9bfb712..92c71c2 100644
--- a/app/[lang]/(resources)/protocols/[slug]/page.tsx
+++ b/app/[lang]/(resources)/protocols/[slug]/page.tsx
@@ -1,113 +1,101 @@
import Image from 'next/image'
-import {notFound} from 'next/navigation'
+import { notFound } from 'next/navigation'
-import {ProtocolAbout} from '@/app/[lang]/(resources)/_components/ProtocolAbout'
-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 {Banner} from '@/app/[lang]/_components/Banner'
-import {getSupportedProtocol} from '@/app/[lang]/_utils/query'
+import { ProtocolAbout } from '@/app/[lang]/(resources)/_components/ProtocolAbout'
+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 { Banner } from '@/app/[lang]/_components/Banner'
+import { getStrapiImageUrl, getSupportedProtocol } from '@/app/[lang]/_utils/query'
-import type {TSupportedProtocolData} from '@/app/[lang]/_components/strapi/types'
-import type {Metadata} from 'next'
-import type {ReactNode} from 'react'
+import type { TSupportedProtocolData } from '@/app/[lang]/_components/strapi/types'
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
-export async function generateMetadata({params}: {params: Promise<{slug: string}>}): Promise {
- const {slug} = await params
- if (!slug) {
- return notFound()
- }
+export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise {
+ const { slug } = await params
+ if (!slug) {
+ 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 data = await response.json()
- const protocol = data.data[0] as TSupportedProtocolData
- if (!protocol) {
- 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 data = await response.json()
+ const protocol = data.data[0] as TSupportedProtocolData
+ if (!protocol) {
+ return notFound()
+ }
- const imageUrl = protocol.featuredImg?.formats?.thumbnail?.url || protocol.featuredImg?.url
+ const imageUrl = protocol.featuredImg?.formats?.thumbnail?.url || protocol.featuredImg?.url
- const metadata: Metadata = {
- title: `${protocol.name} | ShapeShift`,
- description: `Shift into ${protocol.name} with ShapeShift!`,
- keywords: `${protocol.name}, ShapeShift`,
- openGraph: {
- title: protocol.name,
- description: `Shift into ${protocol.name} with ShapeShift!`,
- type: 'website'
- },
- twitter: {
- card: 'summary_large_image',
- title: protocol.name,
- description: `Shift into ${protocol.name} with ShapeShift!`
- }
- }
+ const metadata: Metadata = {
+ title: `${protocol.name} | ShapeShift`,
+ description: `Shift into ${protocol.name} with ShapeShift!`,
+ keywords: `${protocol.name}, ShapeShift`,
+ openGraph: {
+ title: protocol.name,
+ description: `Shift into ${protocol.name} with ShapeShift!`,
+ type: 'website',
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: protocol.name,
+ description: `Shift into ${protocol.name} with ShapeShift!`,
+ },
+ }
- if (imageUrl) {
- metadata.openGraph!.images = [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}${imageUrl}`
- }
- ]
- metadata.twitter!.images = [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}${imageUrl}`
- }
- ]
- }
+ if (imageUrl) {
+ metadata.openGraph!.images = [
+ {
+ url: getStrapiImageUrl(imageUrl),
+ },
+ ]
+ metadata.twitter!.images = [
+ {
+ url: getStrapiImageUrl(imageUrl),
+ },
+ ]
+ }
- return metadata
+ return metadata
}
-export default async function ProtocolPage({params}: {params: Promise<{slug: string}>}): Promise {
- const {slug} = await params
- const protocol = await getSupportedProtocol(slug)
+export default async function ProtocolPage({ params }: { params: Promise<{ slug: string }> }): Promise {
+ const { slug } = await params
+ const protocol = await getSupportedProtocol(slug)
- if (!protocol) {
- return notFound()
- }
+ if (!protocol) {
+ return notFound()
+ }
- return (
-
-
-
-
-
-
-
-
-
+ return (
+
- )
+
+
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/protocols/_components/ProtocolSearchWrapper.tsx b/app/[lang]/(resources)/protocols/_components/ProtocolSearchWrapper.tsx
index 804b105..0553049 100644
--- a/app/[lang]/(resources)/protocols/_components/ProtocolSearchWrapper.tsx
+++ b/app/[lang]/(resources)/protocols/_components/ProtocolSearchWrapper.tsx
@@ -8,14 +8,14 @@
'use client'
-import {useState} from 'react'
+import { useState } from 'react'
-import {SearchBar} from '@/app/[lang]/_components/SearchBar'
+import { SearchBar } from '@/app/[lang]/_components/SearchBar'
-import {ProtocolList} from '../../_components/ProtocolList'
+import { ProtocolList } from '../../_components/ProtocolList'
-import type {TSupportedProtocolData} from '@/app/[lang]/_components/strapi/types'
-import type {ReactNode} from 'react'
+import type { TSupportedProtocolData } from '@/app/[lang]/_components/strapi/types'
+import type { ReactNode } from 'react'
/**************************************************************************************************
** ProtocolSearchWrapper Component provides search functionality for protocols
@@ -27,8 +27,8 @@ import type {ReactNode} from 'react'
** @property {TSupportedProtocolData[]} protocols - Array of protocol data to be displayed and filtered
*************************************************************************************************/
type TProtocolSearchWrapperProps = {
- protocols: TSupportedProtocolData[];
-};
+ protocols: TSupportedProtocolData[]
+}
/**************************************************************************************************
** ProtocolSearchWrapper Component
@@ -36,60 +36,49 @@ type TProtocolSearchWrapperProps = {
** @param {TProtocolSearchWrapperProps} props - Component props
** @returns {ReactNode} Rendered component
*************************************************************************************************/
-export function ProtocolSearchWrapper({protocols}: TProtocolSearchWrapperProps): ReactNode {
- /**********************************************************************************************
- ** State Management
- ** - filteredProtocols: protocols filtered by search query
- ** - searchQuery: current search input value
- *********************************************************************************************/
- const [filteredProtocols, setFilteredProtocols] = useState(protocols)
- const [searchQuery, setSearchQuery] = useState('')
+export function ProtocolSearchWrapper({ protocols }: TProtocolSearchWrapperProps): ReactNode {
+ /**********************************************************************************************
+ ** State Management
+ ** - filteredProtocols: protocols filtered by search query
+ ** - searchQuery: current search input value
+ *********************************************************************************************/
+ const [filteredProtocols, setFilteredProtocols] = useState(protocols)
+ const [searchQuery, setSearchQuery] = useState('')
- /**********************************************************************************************
- ** Search Handler
- ** Filters protocols based on name matches (case-insensitive)
- *********************************************************************************************/
- const handleSearch = (query: string): void => {
- setSearchQuery(query)
- const filtered = protocols.filter(protocol => protocol.name.toLowerCase().includes(query.toLowerCase()))
- setFilteredProtocols(filtered)
- }
+ /**********************************************************************************************
+ ** Search Handler
+ ** Filters protocols based on name matches (case-insensitive)
+ *********************************************************************************************/
+ const handleSearch = (query: string): void => {
+ setSearchQuery(query)
+ const filtered = protocols.filter((protocol) => protocol.name.toLowerCase().includes(query.toLowerCase()))
+ setFilteredProtocols(filtered)
+ }
- /**********************************************************************************************
- ** Protocol Filtering
- ** Split protocols into featured and non-featured categories
- *********************************************************************************************/
- const featuredProtocols = filteredProtocols?.filter(protocol => protocol.isFeatured)
- const nonFeaturedProtocols = filteredProtocols?.filter(protocol => !protocol.isFeatured)
+ /**********************************************************************************************
+ ** Protocol Filtering
+ ** Split protocols into featured and non-featured categories
+ *********************************************************************************************/
+ const featuredProtocols = filteredProtocols?.filter((protocol) => protocol.isFeatured)
+ const nonFeaturedProtocols = filteredProtocols?.filter((protocol) => !protocol.isFeatured)
- return (
- <>
-
+ return (
+ <>
+
- {/* Featured Protocols */}
- {featuredProtocols && featuredProtocols.length > 0 && (
-
-
{'Featured Protocols'}
-
-
- )}
+ {/* Featured Protocols */}
+ {featuredProtocols && featuredProtocols.length > 0 && (
+
+
{'Featured Protocols'}
+
+
+ )}
- {/* Protocols grid section */}
-
- {'Choose your preferred protocol'}
-
-
- >
- )
+ {/* Protocols grid section */}
+
+ {'Choose your preferred protocol'}
+
+
+ >
+ )
}
diff --git a/app/[lang]/(resources)/protocols/layout.tsx b/app/[lang]/(resources)/protocols/layout.tsx
index 80815d7..de80644 100644
--- a/app/[lang]/(resources)/protocols/layout.tsx
+++ b/app/[lang]/(resources)/protocols/layout.tsx
@@ -1,34 +1,34 @@
-import type {Metadata} from 'next'
-import type {ReactNode} from 'react'
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
export async function generateMetadata(): Promise {
- return {
- title: 'Shift into DeFi with ShapeShift',
- description: 'Discover all the protocols ShapeShift supports. Buy, sell, and swap crypto with ease.',
- keywords: 'ShapeShift, Supported Protocols',
- openGraph: {
- title: 'Shift into DeFi with ShapeShift',
- description: 'Discover all the wallets ShapeShift supports. Buy, sell, and swap crypto with ease.',
- type: 'website',
- images: [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}/og.png`
- }
- ]
- },
- twitter: {
- card: 'summary_large_image',
- title: 'Shift into DeFi with ShapeShift',
- description: 'Discover all the protocols ShapeShift supports. Buy, sell, and swap crypto with ease.',
- images: [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}/og.png`
- }
- ]
- }
- }
+ return {
+ title: 'Shift into DeFi with ShapeShift',
+ description: 'Discover all the protocols ShapeShift supports. Buy, sell, and swap crypto with ease.',
+ keywords: 'ShapeShift, Supported Protocols',
+ openGraph: {
+ title: 'Shift into DeFi with ShapeShift',
+ description: 'Discover all the wallets ShapeShift supports. Buy, sell, and swap crypto with ease.',
+ type: 'website',
+ images: [
+ {
+ url: `${process.env.NEXT_PUBLIC_STRAPI_URL}/og.png`,
+ },
+ ],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: 'Shift into DeFi with ShapeShift',
+ description: 'Discover all the protocols ShapeShift supports. Buy, sell, and swap crypto with ease.',
+ images: [
+ {
+ url: `${process.env.NEXT_PUBLIC_STRAPI_URL}/og.png`,
+ },
+ ],
+ },
+ }
}
-export default function Layout({children}: {children: ReactNode}): ReactNode {
- return children
+export default function Layout({ children }: { children: ReactNode }): ReactNode {
+ return children
}
diff --git a/app/[lang]/(resources)/protocols/page.tsx b/app/[lang]/(resources)/protocols/page.tsx
index 010c82b..4262dfe 100644
--- a/app/[lang]/(resources)/protocols/page.tsx
+++ b/app/[lang]/(resources)/protocols/page.tsx
@@ -15,57 +15,57 @@
** - Implements responsive design with Tailwind CSS
************************************************************************************************/
-import {Banner} from '@/app/[lang]/_components/Banner'
+import { Banner } from '@/app/[lang]/_components/Banner'
-import {ResourceHeader} from '../_components/ResourceHeader'
-import {fetchAllProtocols} from '../_utils/fetchUtils'
-import {ProtocolSearchWrapper} from './_components/ProtocolSearchWrapper'
+import { ResourceHeader } from '../_components/ResourceHeader'
+import { fetchAllProtocols } from '../_utils/fetchUtils'
+import { ProtocolSearchWrapper } from './_components/ProtocolSearchWrapper'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
// Static content for the page
const pageContent = {
- title: 'Shift into DeFi with ShapeShift',
- description: 'Enter and exit top DeFi protocols in one-click with ShapeShift',
- features: ['Multi-Chain Support', 'Non-Custodial', 'User-Friendly Interface'],
- ctaButton: {
- text: 'Get Started',
- url: 'https://app.shapeshift.com/'
- }
+ title: 'Shift into DeFi with ShapeShift',
+ description: 'Enter and exit top DeFi protocols in one-click with ShapeShift',
+ features: ['Multi-Chain Support', 'Non-Custodial', 'User-Friendly Interface'],
+ ctaButton: {
+ text: 'Get Started',
+ url: 'https://app.shapeshift.com/',
+ },
}
export default async function ProtocolsPage(): Promise {
- // Fetch protocols data
- const protocols = await fetchAllProtocols()
+ // Fetch protocols data
+ const protocols = await fetchAllProtocols()
- // Handle loading and error states
- if (!protocols) {
- return (
-
-
{'Unable to load protocols. Please try again later.'}
-
- )
- }
+ // Handle loading and error states
+ if (!protocols) {
+ return (
+
+
{'Unable to load protocols. Please try again later.'}
+
+ )
+ }
- return (
-
-
- {/* Reusable header component */}
-
+ return (
+
+
+ {/* Reusable header component */}
+
-
+
- {/* Footer banner */}
-
-
-
-
-
- )
+ {/* Footer banner */}
+
+
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/support/(withNavigation)/page.tsx b/app/[lang]/(resources)/support/(withNavigation)/page.tsx
index 9c46de7..3146a9f 100644
--- a/app/[lang]/(resources)/support/(withNavigation)/page.tsx
+++ b/app/[lang]/(resources)/support/(withNavigation)/page.tsx
@@ -18,14 +18,14 @@
'use client'
-import {SupportArticleList} from '../../_components/SupportArticleList'
+import { SupportArticleList } from '../../_components/SupportArticleList'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
export default function SupportListPage(): ReactNode {
- return (
- <>
-
- >
- )
+ return (
+ <>
+
+ >
+ )
}
diff --git a/app/[lang]/(resources)/support/[slug]/SupportArticleContent.tsx b/app/[lang]/(resources)/support/[slug]/SupportArticleContent.tsx
index 271de8e..1d87b15 100644
--- a/app/[lang]/(resources)/support/[slug]/SupportArticleContent.tsx
+++ b/app/[lang]/(resources)/support/[slug]/SupportArticleContent.tsx
@@ -5,212 +5,161 @@ import rehypeHighlight from 'rehype-highlight'
import remarkEmoji from 'remark-emoji'
import remarkGfm from 'remark-gfm'
-import {isHtml} from '@/app/[lang]/_utils/isHtml'
-
-import type {ReactNode} from 'react'
-
-export function SupportArticleContent({content}: {content: string}): ReactNode {
- return (
-
- {isHtml(content) ? (
- // eslint-disable-next-line @typescript-eslint/naming-convention
-
- ) : (
-
(
-
- ),
- h2: ({...props}) => (
-
- ),
- h3: ({...props}) => (
-
- ),
-
- // Code blocks
- code: ({className, children, ...props}) => {
- const match = /language-(\w+)/.exec(className || '')
- return match ? (
-
-
{match[1]}
-
-
- {children}
-
-
-
- ) : (
-
- {children}
-
- )
- },
-
- // Tables
- table: ({...props}) => (
-
- ),
- th: ({...props}) => (
-
- ),
- td: ({...props}) => (
-
- ),
-
- // Images
- img: ({...props}) => (
-
- ),
-
- // Blockquotes
- blockquote: ({...props}) => (
-
- ),
-
- // Lists
- ul: ({...props}) => (
-
- ),
- ol: ({...props}) => (
-
- ),
-
- // Links
- a: ({...props}) => (
-
- ),
- p: ({...props}) => (
-
- )
- }}>
- {content}
-
- )}
-
-
-
- )
+import { isHtml } from '@/app/[lang]/_utils/isHtml'
+
+import type { ReactNode } from 'react'
+
+export function SupportArticleContent({ content }: { content: string }): ReactNode {
+ return (
+
+ {isHtml(content) ? (
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+
+ ) : (
+
,
+ h2: ({ ...props }) => ,
+ h3: ({ ...props }) => ,
+
+ // Code blocks
+ code: ({ className, children, ...props }) => {
+ const match = /language-(\w+)/.exec(className || '')
+ return match ? (
+
+
{match[1]}
+
+
+ {children}
+
+
+
+ ) : (
+
+ {children}
+
+ )
+ },
+
+ // Tables
+ table: ({ ...props }) => (
+
+ ),
+ th: ({ ...props }) => ,
+ td: ({ ...props }) => ,
+
+ // Images
+ img: ({ ...props }) => (
+
+ ),
+
+ // Blockquotes
+ blockquote: ({ ...props }) => (
+
+ ),
+
+ // Lists
+ ul: ({ ...props }) => ,
+ ol: ({ ...props }) => ,
+
+ // Links
+ a: ({ ...props }) => (
+
+ ),
+ p: ({ ...props }) =>
,
+ }}
+ >
+ {content}
+
+ )}
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/support/[slug]/page.tsx b/app/[lang]/(resources)/support/[slug]/page.tsx
index 3f007ad..f2e7f68 100644
--- a/app/[lang]/(resources)/support/[slug]/page.tsx
+++ b/app/[lang]/(resources)/support/[slug]/page.tsx
@@ -1,90 +1,89 @@
'use client'
import 'highlight.js/styles/github-dark.css'
-import {notFound, useParams, useRouter} from 'next/navigation'
+import { notFound, useParams, useRouter } from 'next/navigation'
import Script from 'next/script'
-import {SupportArticleContent} from '@/app/[lang]/(resources)/support/[slug]/SupportArticleContent'
-import {Banner} from '@/app/[lang]/_components/Banner'
-import {useCachedArticles} from '@/app/[lang]/_contexts/CachedArticlesContext'
-import {useFetchSupportArticles} from '@/app/[lang]/_hooks/useFetchSupportArticles'
-import {IconBack} from '@/app/[lang]/_icons/IconBack'
-import {generateSupportArticleSchema} from '@/app/[lang]/_utils/schema'
+import { SupportArticleContent } from '@/app/[lang]/(resources)/support/[slug]/SupportArticleContent'
+import { Banner } from '@/app/[lang]/_components/Banner'
+import { useCachedArticles } from '@/app/[lang]/_contexts/CachedArticlesContext'
+import { useFetchSupportArticles } from '@/app/[lang]/_hooks/useFetchSupportArticles'
+import { IconBack } from '@/app/[lang]/_icons/IconBack'
+import { generateSupportArticleSchema } from '@/app/[lang]/_utils/schema'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
function LoadingSkeleton(): ReactNode {
- return (
-
- )
+ return (
+
+ )
}
export default function SupportArticle(): ReactNode {
- const {slug} = useParams()
- const {
- cachedResponse: {data: cachedArticles}
- } = useCachedArticles()
- const {articles, isLoading} = useFetchSupportArticles({
- page: 1,
- pageSize: 1,
- sort: 'desc',
- populateContent: true,
- cacheArticles: true,
- slug: slug as string
- })
+ const { slug } = useParams()
+ const {
+ cachedResponse: { data: cachedArticles },
+ } = useCachedArticles()
+ const { articles, isLoading } = useFetchSupportArticles({
+ page: 1,
+ pageSize: 1,
+ sort: 'desc',
+ populateContent: true,
+ cacheArticles: true,
+ slug: slug as string,
+ })
- const article = [...cachedArticles, ...articles].find(a => a.slug === slug)
- const router = useRouter()
+ const article = [...cachedArticles, ...articles].find((a) => a.slug === slug)
+ const router = useRouter()
- if (isLoading) {
- return
- }
+ if (isLoading) {
+ return
+ }
- if (!article) {
- notFound()
- }
+ if (!article) {
+ notFound()
+ }
- // Generate structured data for the support article
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://shapeshift.com'
- const articleSchema = generateSupportArticleSchema(article, baseUrl)
+ // Generate structured data for the support article
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://shapeshift.com'
+ const articleSchema = generateSupportArticleSchema(article, baseUrl)
- return (
- <>
- {/* Add structured data */}
-
+ return (
+ <>
+ {/* Add structured data */}
+
-
- router.back()}>
-
- {'Back'}
-
-
- {new Date(article.publishedAt).toLocaleDateString()}
-
+
+ router.back()}
+ >
+
+ {'Back'}
+
+ {new Date(article.publishedAt).toLocaleDateString()}
- {article.title}
-
-
- {!isLoading && (
-
-
-
- )}
- >
- )
+ {article.title}
+
+
+ {!isLoading && (
+
+
+
+ )}
+ >
+ )
}
diff --git a/app/[lang]/(resources)/wallets/README.md b/app/[lang]/(resources)/wallets/README.md
index 31f3473..b7496ed 100644
--- a/app/[lang]/(resources)/wallets/README.md
+++ b/app/[lang]/(resources)/wallets/README.md
@@ -5,7 +5,7 @@ This directory contains pages that showcase and document the cryptocurrency wall
## Directory Structure
- **[slug]/**: Dynamic routes for individual wallet pages
- - **page.tsx**: Page component for individual wallet details
+ - **page.tsx**: Page component for individual wallet details
- **layout.tsx**: Layout component for all supported wallets pages
- **loading.tsx**: Loading state component for supported wallets pages
- **page.tsx**: Main supported wallets landing page
@@ -59,4 +59,4 @@ The wallet pages utilize specialized components from the `_components` directory
- Ensure all wallet information is properly categorized and searchable
- Update content when wallet support changes or is enhanced
- Coordinate with wallet development teams for accurate representation
-- Include troubleshooting tips for common connection issues
\ No newline at end of file
+- Include troubleshooting tips for common connection issues
diff --git a/app/[lang]/(resources)/wallets/[slug]/page.tsx b/app/[lang]/(resources)/wallets/[slug]/page.tsx
index 23e1d9f..0da655b 100644
--- a/app/[lang]/(resources)/wallets/[slug]/page.tsx
+++ b/app/[lang]/(resources)/wallets/[slug]/page.tsx
@@ -1,102 +1,102 @@
-import {notFound} from 'next/navigation'
+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 {Banner} from '@/app/[lang]/_components/Banner'
-import {StrapiFAQ} from '@/app/[lang]/_components/StrapiFAQ'
-import {getSupportedWallet} from '@/app/[lang]/_utils/query'
+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 { Banner } from '@/app/[lang]/_components/Banner'
+import { StrapiFAQ } from '@/app/[lang]/_components/StrapiFAQ'
+import { getStrapiImageUrl, getSupportedWallet } from '@/app/[lang]/_utils/query'
-import type {TSupportedWalletData} from '@/app/[lang]/_components/strapi/types'
-import type {Metadata} from 'next'
-import type {ReactNode} from 'react'
+import type { TSupportedWalletData } from '@/app/[lang]/_components/strapi/types'
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
-export async function generateMetadata({params}: {params: Promise<{slug: string}>}): Promise {
- const {slug} = await params
- if (!slug) {
- return notFound()
- }
+export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise {
+ const { slug } = await params
+ if (!slug) {
+ 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 data = await response.json()
- const wallet = data.data[0] as TSupportedWalletData
- if (!wallet) {
- 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 data = await response.json()
+ const wallet = data.data[0] as TSupportedWalletData
+ if (!wallet) {
+ return notFound()
+ }
- const imageUrl = wallet.featuredImg?.formats?.thumbnail?.url || wallet.featuredImg?.url
- const metadata: Metadata = {
- title: `${wallet.name} | ShapeShift Wallets`,
- description: `ShapeShift supports ${wallet.name}! Use it now to buy, sell, and swap crypto.`,
- keywords: `${wallet.name}, ShapeShift`,
- openGraph: {
- title: wallet.name,
- description: `ShapeShift supports ${wallet.name}! Use it now to buy, sell, and swap crypto.`,
- type: 'website'
- },
- twitter: {
- card: 'summary_large_image',
- title: wallet.name,
- description: `ShapeShift supports ${wallet.name}! Use it now to buy, sell, and swap crypto.`
- }
- }
+ const imageUrl = wallet.featuredImg?.formats?.thumbnail?.url || wallet.featuredImg?.url
+ const metadata: Metadata = {
+ title: `${wallet.name} | ShapeShift Wallets`,
+ description: `ShapeShift supports ${wallet.name}! Use it now to buy, sell, and swap crypto.`,
+ keywords: `${wallet.name}, ShapeShift`,
+ openGraph: {
+ title: wallet.name,
+ description: `ShapeShift supports ${wallet.name}! Use it now to buy, sell, and swap crypto.`,
+ type: 'website',
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: wallet.name,
+ description: `ShapeShift supports ${wallet.name}! Use it now to buy, sell, and swap crypto.`,
+ },
+ }
- if (imageUrl) {
- metadata.openGraph!.images = [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}${imageUrl}`
- }
- ]
- metadata.twitter!.images = [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}${imageUrl}`
- }
- ]
- }
+ if (imageUrl) {
+ metadata.openGraph!.images = [
+ {
+ url: getStrapiImageUrl(imageUrl),
+ },
+ ]
+ metadata.twitter!.images = [
+ {
+ url: getStrapiImageUrl(imageUrl),
+ },
+ ]
+ }
- return metadata
+ return metadata
}
-export default async function WalletPage({params}: {params: Promise<{slug: string}>}): Promise {
- const {slug} = await params
- const wallet = await getSupportedWallet(slug)
+export default async function WalletPage({ params }: { params: Promise<{ slug: string }> }): Promise {
+ const { slug } = await params
+ const wallet = await getSupportedWallet(slug)
- if (!wallet) {
- return notFound()
- }
+ if (!wallet) {
+ return notFound()
+ }
- return (
-
-
-
-
-
-
+ return (
+
- )
+
+
+
+
+
+ )
}
diff --git a/app/[lang]/(resources)/wallets/_components/WalletSearchWrapper.tsx b/app/[lang]/(resources)/wallets/_components/WalletSearchWrapper.tsx
index e55f7de..a8906ab 100644
--- a/app/[lang]/(resources)/wallets/_components/WalletSearchWrapper.tsx
+++ b/app/[lang]/(resources)/wallets/_components/WalletSearchWrapper.tsx
@@ -1,59 +1,48 @@
'use client'
-import {useState} from 'react'
+import { useState } from 'react'
-import {SearchBar} from '@/app/[lang]/_components/SearchBar'
+import { SearchBar } from '@/app/[lang]/_components/SearchBar'
-import {WalletList} from '../../_components/WalletList'
+import { WalletList } from '../../_components/WalletList'
-import type {TSupportedWalletData} from '@/app/[lang]/_components/strapi/types'
-import type {ReactNode} from 'react'
+import type { TSupportedWalletData } from '@/app/[lang]/_components/strapi/types'
+import type { ReactNode } from 'react'
type TWalletSearchWrapperProps = {
- wallets: TSupportedWalletData[];
-};
-
-export function WalletSearchWrapper({wallets}: TWalletSearchWrapperProps): ReactNode {
- const [filteredWallets, setFilteredWallets] = useState(wallets)
- const [searchQuery, setSearchQuery] = useState('')
-
- const handleSearch = (query: string): void => {
- setSearchQuery(query)
- const filtered = wallets.filter(wallet => wallet.name.toLowerCase().includes(query.toLowerCase()))
- setFilteredWallets(filtered)
- }
-
- const featuredWallets = filteredWallets?.filter(wallet => wallet.isFeatured)
- const nonFeaturedWallets = filteredWallets?.filter(wallet => !wallet.isFeatured)
-
- return (
- <>
-
-
- {/* Featured Wallets */}
- {featuredWallets && featuredWallets.length > 0 && (
-
-
{'Featured Wallets'}
-
-
- )}
-
- {/* Wallets grid section */}
-
- {'Choose your preferred wallet'}
-
-
- >
- )
+ wallets: TSupportedWalletData[]
+}
+
+export function WalletSearchWrapper({ wallets }: TWalletSearchWrapperProps): ReactNode {
+ const [filteredWallets, setFilteredWallets] = useState(wallets)
+ const [searchQuery, setSearchQuery] = useState('')
+
+ const handleSearch = (query: string): void => {
+ setSearchQuery(query)
+ const filtered = wallets.filter((wallet) => wallet.name.toLowerCase().includes(query.toLowerCase()))
+ setFilteredWallets(filtered)
+ }
+
+ const featuredWallets = filteredWallets?.filter((wallet) => wallet.isFeatured)
+ const nonFeaturedWallets = filteredWallets?.filter((wallet) => !wallet.isFeatured)
+
+ return (
+ <>
+
+
+ {/* Featured Wallets */}
+ {featuredWallets && featuredWallets.length > 0 && (
+
+
{'Featured Wallets'}
+
+
+ )}
+
+ {/* Wallets grid section */}
+
+ {'Choose your preferred wallet'}
+
+
+ >
+ )
}
diff --git a/app/[lang]/(resources)/wallets/layout.tsx b/app/[lang]/(resources)/wallets/layout.tsx
index 557bbdd..6ec5802 100644
--- a/app/[lang]/(resources)/wallets/layout.tsx
+++ b/app/[lang]/(resources)/wallets/layout.tsx
@@ -1,34 +1,34 @@
-import type {Metadata} from 'next'
-import type {ReactNode} from 'react'
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
export async function generateMetadata(): Promise {
- return {
- title: 'ShapeShift Supported Wallets',
- description: 'Discover all the wallets ShapeShift supports. Buy, sell, and swap crypto with ease.',
- keywords: 'ShapeShift, Supported Wallets',
- openGraph: {
- title: 'ShapeShift Supported Wallets',
- description: 'Discover all the wallets ShapeShift supports. Buy, sell, and swap crypto with ease.',
- type: 'website',
- images: [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}/og.png`
- }
- ]
- },
- twitter: {
- card: 'summary_large_image',
- title: 'ShapeShift Supported Wallets',
- description: 'Discover all the wallets ShapeShift supports. Buy, sell, and swap crypto with ease.',
- images: [
- {
- url: `${process.env.NEXT_PUBLIC_STRAPI_URL}/og.png`
- }
- ]
- }
- }
+ return {
+ title: 'ShapeShift Supported Wallets',
+ description: 'Discover all the wallets ShapeShift supports. Buy, sell, and swap crypto with ease.',
+ keywords: 'ShapeShift, Supported Wallets',
+ openGraph: {
+ title: 'ShapeShift Supported Wallets',
+ description: 'Discover all the wallets ShapeShift supports. Buy, sell, and swap crypto with ease.',
+ type: 'website',
+ images: [
+ {
+ url: `${process.env.NEXT_PUBLIC_STRAPI_URL}/og.png`,
+ },
+ ],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: 'ShapeShift Supported Wallets',
+ description: 'Discover all the wallets ShapeShift supports. Buy, sell, and swap crypto with ease.',
+ images: [
+ {
+ url: `${process.env.NEXT_PUBLIC_STRAPI_URL}/og.png`,
+ },
+ ],
+ },
+ }
}
-export default function Layout({children}: {children: ReactNode}): ReactNode {
- return children
+export default function Layout({ children }: { children: ReactNode }): ReactNode {
+ return children
}
diff --git a/app/[lang]/(resources)/wallets/page.tsx b/app/[lang]/(resources)/wallets/page.tsx
index 9762ca8..d2de75c 100644
--- a/app/[lang]/(resources)/wallets/page.tsx
+++ b/app/[lang]/(resources)/wallets/page.tsx
@@ -16,79 +16,79 @@
** - Implements descriptive section headings for better accessibility
************************************************************************************************/
-import {Banner} from '@/app/[lang]/_components/Banner'
-import {WalletRequestCard} from '@/app/[lang]/_components/WalletRequestCard'
-import {requestUrl} from '@/app/[lang]/_utils/constants'
+import { Banner } from '@/app/[lang]/_components/Banner'
+import { WalletRequestCard } from '@/app/[lang]/_components/WalletRequestCard'
+import { requestUrl } from '@/app/[lang]/_utils/constants'
-import {ResourceHeader} from '../_components/ResourceHeader'
-import {fetchAllWallets} from '../_utils/fetchUtils'
-import {WalletSearchWrapper} from './_components/WalletSearchWrapper'
+import { ResourceHeader } from '../_components/ResourceHeader'
+import { fetchAllWallets } from '../_utils/fetchUtils'
+import { WalletSearchWrapper } from './_components/WalletSearchWrapper'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
// Static content for the page
const pageContent = {
- title: 'Bring your own wallet',
- description:
- 'Connect your favorite self-custody wallet to access the full ShapeShift platform and all supported chains.',
- features: ['Non-Custodial', 'Multi-Provider Support', 'Enhanced Privacy'],
- ctaButton: {
- text: 'Get Started',
- url: 'https://app.shapeshift.com/'
- }
+ title: 'Bring your own wallet',
+ description:
+ 'Connect your favorite self-custody wallet to access the full ShapeShift platform and all supported chains.',
+ features: ['Non-Custodial', 'Multi-Provider Support', 'Enhanced Privacy'],
+ ctaButton: {
+ text: 'Get Started',
+ url: 'https://app.shapeshift.com/',
+ },
}
export default async function WalletPage(): Promise {
- // Fetch wallets data
- const wallets = await fetchAllWallets()
+ // Fetch wallets data
+ const wallets = await fetchAllWallets()
- // Handle loading and error states
- if (!wallets) {
- return (
-
-
{'Unable to load wallet data. Please try again later.'}
-
- )
- }
+ // Handle loading and error states
+ if (!wallets) {
+ return (
+
+
{'Unable to load wallet data. Please try again later.'}
+
+ )
+ }
- return (
-
-
- {/* Reusable header component */}
-
+ return (
+
+
+ {/* Reusable header component */}
+
-
+
-
- {/* Footer banner */}
-
-
-
-
-
- )
+ {/* Footer banner */}
+
+
+
+
+
+ )
}
diff --git a/app/[lang]/(terms)/README.md b/app/[lang]/(terms)/README.md
index 0bf5d02..319ae7f 100644
--- a/app/[lang]/(terms)/README.md
+++ b/app/[lang]/(terms)/README.md
@@ -4,12 +4,12 @@ This directory contains legal documents and terms-related pages for the ShapeShi
## Directory Structure
-- **_components/**: Shared components used for displaying legal content
- - `TermsAccordion.tsx`: Collapsible sections for terms content
- - `TermsMarkdown.tsx`: Component for rendering markdown legal content
- - `TermsPage.tsx`: Layout template for all terms pages
- - `terms.module.css`: Specific styling for terms content
- - `utils.ts`: Helper functions for terms pages
+- **\_components/**: Shared components used for displaying legal content
+ - `TermsAccordion.tsx`: Collapsible sections for terms content
+ - `TermsMarkdown.tsx`: Component for rendering markdown legal content
+ - `TermsPage.tsx`: Layout template for all terms pages
+ - `terms.module.css`: Specific styling for terms content
+ - `utils.ts`: Helper functions for terms pages
- **privacy-policy/**: Privacy policy page
- **terms-of-service/**: Terms of service page
- **error.tsx**: Error handling for terms pages
@@ -19,6 +19,7 @@ This directory contains legal documents and terms-related pages for the ShapeShi
## Route Convention
The parentheses in the directory name `(terms)` indicate a route group in Next.js. This means:
+
- The folder name itself doesn't affect URL paths
- All pages inside share UI layouts and features
- It keeps related legal content organized without affecting the URL structure
@@ -26,6 +27,7 @@ The parentheses in the directory name `(terms)` indicate a route group in Next.j
## Content Rendering
The components in this directory are designed to:
+
- Present legal content in a clean, readable format
- Support markdown formatting for easy content updates
- Provide consistent styling across all legal documents
@@ -44,4 +46,4 @@ The components in this directory are designed to:
- Ensure all legal pages share consistent structure
- Test thoroughly after any changes to legal document components
- Coordinate with legal team before updating content
-- Preserve proper heading hierarchy for accessibility
\ No newline at end of file
+- Preserve proper heading hierarchy for accessibility
diff --git a/app/[lang]/(terms)/_components/README.md b/app/[lang]/(terms)/_components/README.md
index b628579..b355bc7 100644
--- a/app/[lang]/(terms)/_components/README.md
+++ b/app/[lang]/(terms)/_components/README.md
@@ -57,4 +57,4 @@ The `terms.module.css` file contains specialized styles that:
- Provide proper spacing for dense content
- Style the accordion components appropriately
- Ensure responsive behavior
-- Maintain consistent typography throughout legal documents
\ No newline at end of file
+- Maintain consistent typography throughout legal documents
diff --git a/app/[lang]/(terms)/_components/TermsAccordion.tsx b/app/[lang]/(terms)/_components/TermsAccordion.tsx
index 6ca36fe..b8156e4 100644
--- a/app/[lang]/(terms)/_components/TermsAccordion.tsx
+++ b/app/[lang]/(terms)/_components/TermsAccordion.tsx
@@ -8,94 +8,97 @@
**************************************************************************************************/
'use client'
-import {AnimatePresence, motion} from 'framer-motion'
+import { AnimatePresence, motion } from 'framer-motion'
import 'highlight.js/styles/github-dark.css'
-import {memo, useCallback, useState} from 'react'
+import { memo, useCallback, useState } from 'react'
import TermsMarkdown from '@/app/[lang]/(terms)/_components/TermsMarkdown'
-import {AnimatedPlusMinusIcon} from '@/app/[lang]/_components/QuestionSection'
+import { AnimatedPlusMinusIcon } from '@/app/[lang]/_components/QuestionSection'
-import type {KeyboardEvent, ReactNode} from 'react'
+import type { KeyboardEvent, ReactNode } from 'react'
export type TTermsItemData = {
- id: number
- title: string
- date: string
- content: string
+ id: number
+ title: string
+ date: string
+ content: string
}
type TTermsAccordionProps = {
- item: TTermsItemData
+ item: TTermsItemData
}
/**************************************************************************************************
* TermsAccordion component
**************************************************************************************************/
-function TermsAccordion({item}: TTermsAccordionProps): ReactNode {
- const [isOpen, setIsOpen] = useState(false)
+function TermsAccordion({ item }: TTermsAccordionProps): ReactNode {
+ const [isOpen, setIsOpen] = useState(false)
- const handleToggle = useCallback(() => {
- setIsOpen(prevState => !prevState)
- }, [])
+ const handleToggle = useCallback(() => {
+ setIsOpen((prevState) => !prevState)
+ }, [])
- const handleKeyDown = useCallback(
- (event: KeyboardEvent) => {
- if (event.key === 'Enter' || event.key === ' ') {
- event.preventDefault()
- handleToggle()
- }
- },
- [handleToggle]
- )
+ const handleKeyDown = useCallback(
+ (event: KeyboardEvent) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault()
+ handleToggle()
+ }
+ },
+ [handleToggle]
+ )
- return (
-
-
-
- {item.title}
-
- {new Date(item.date).toLocaleDateString(undefined, {
- year: 'numeric',
- month: 'long',
- day: 'numeric'
- })}
-
-
-
-
-
- {isOpen && (
-
-
-
-
-
- )}
-
-
- )
+ return (
+
+
+
+ {item.title}
+
+ {new Date(item.date).toLocaleDateString(undefined, {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })}
+
+
+
+
+
+ {isOpen && (
+
+
+
+
+
+ )}
+
+
+ )
}
export default memo(TermsAccordion)
diff --git a/app/[lang]/(terms)/_components/TermsMarkdown.tsx b/app/[lang]/(terms)/_components/TermsMarkdown.tsx
index 723a4f6..0b8e0e8 100644
--- a/app/[lang]/(terms)/_components/TermsMarkdown.tsx
+++ b/app/[lang]/(terms)/_components/TermsMarkdown.tsx
@@ -10,185 +10,129 @@
import 'highlight.js/styles/github-dark.css'
import Image from 'next/image'
-import {memo} from 'react'
+import { memo } from 'react'
import ReactMarkdown from 'react-markdown'
import rehypeHighlight from 'rehype-highlight'
import remarkEmoji from 'remark-emoji'
import remarkGfm from 'remark-gfm'
-import type {ComponentProps, ReactNode} from 'react'
+import type { ComponentProps, ReactNode } from 'react'
type TTermsMarkdownProps = {
- content: string
+ content: string
}
/**************************************************************************************************
* Helper function to check if content is HTML
**************************************************************************************************/
function isHtmlContent(content: string): boolean {
- return /<\/?(?:div|span|p|a|img|h[1-6]|ul|ol|li|table|tr|td|th|br|hr|em|strong)[^>]*>/i.test(content)
+ return /<\/?(?:div|span|p|a|img|h[1-6]|ul|ol|li|table|tr|td|th|br|hr|em|strong)[^>]*>/i.test(content)
}
/**************************************************************************************************
* TermsMarkdown component for rendering markdown content
**************************************************************************************************/
-function TermsMarkdown({content}: TTermsMarkdownProps): ReactNode {
- if (isHtmlContent(content)) {
- // eslint-disable-next-line @typescript-eslint/naming-convention
- return
- }
-
- return (
- (
-
- ),
- h2: ({...props}) => (
-
- ),
- h3: ({...props}) => (
-
- ),
-
- // Code blocks
- code: CodeBlock,
-
- // Tables
- table: ({...props}) => (
-
- ),
- th: ({...props}) => (
-
- ),
- td: ({...props}) => (
-
- ),
-
- // Images - properly typed with next/image
- img: ({src, alt}) => {
- // If src is missing, render nothing
- if (!src) {
- return null
- }
-
- return (
-
- )
- },
-
- // Blockquotes
- blockquote: ({...props}) => (
-
- ),
-
- // Lists
- ul: ({...props}) => (
-
- ),
- ol: ({...props}) => (
-
- ),
- li: ({...props}) => (
-
- ),
-
- // Links
- a: ({...props}) => (
-
- ),
-
- p: ({...props}) => (
-
- )
- }}>
- {content}
-
- )
+function TermsMarkdown({ content }: TTermsMarkdownProps): ReactNode {
+ if (isHtmlContent(content)) {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ return
+ }
+
+ return (
+ ,
+ h2: ({ ...props }) => ,
+ h3: ({ ...props }) => ,
+
+ // Code blocks
+ code: CodeBlock,
+
+ // Tables
+ table: ({ ...props }) => (
+
+ ),
+ th: ({ ...props }) => ,
+ td: ({ ...props }) => ,
+
+ // Images - properly typed with next/image
+ img: ({ src, alt }) => {
+ // If src is missing, render nothing
+ if (!src) {
+ return null
+ }
+
+ return (
+
+ )
+ },
+
+ // Blockquotes
+ blockquote: ({ ...props }) => (
+
+ ),
+
+ // Lists
+ ul: ({ ...props }) => ,
+ ol: ({ ...props }) => ,
+ li: ({ ...props }) => ,
+
+ // Links
+ a: ({ ...props }) => (
+
+ ),
+
+ p: ({ ...props }) =>
,
+ }}
+ >
+ {content}
+
+ )
}
/**************************************************************************************************
* CodeBlock component for rendering code with syntax highlighting
**************************************************************************************************/
-function CodeBlock({className, children, ...props}: ComponentProps<'code'>): ReactNode {
- const match = /language-(\w+)/.exec(className || '')
-
- if (match) {
- return (
-
-
- {match[1]}
-
-
-
- {children}
-
-
-
- )
- }
-
- return (
-
- {children}
-
- )
+function CodeBlock({ className, children, ...props }: ComponentProps<'code'>): ReactNode {
+ const match = /language-(\w+)/.exec(className || '')
+
+ if (match) {
+ return (
+
+
+ {match[1]}
+
+
+
+ {children}
+
+
+
+ )
+ }
+
+ return (
+
+ {children}
+
+ )
}
export default memo(TermsMarkdown)
diff --git a/app/[lang]/(terms)/_components/TermsPage.tsx b/app/[lang]/(terms)/_components/TermsPage.tsx
index 6deab7a..136aede 100644
--- a/app/[lang]/(terms)/_components/TermsPage.tsx
+++ b/app/[lang]/(terms)/_components/TermsPage.tsx
@@ -7,92 +7,79 @@
** Implements loading state and error handling for better UX
**************************************************************************************************/
-import {Suspense} from 'react'
+import { Suspense } from 'react'
import styles from '@/app/[lang]/(terms)/_components/terms.module.css'
import TermsAccordion from '@/app/[lang]/(terms)/_components/TermsAccordion'
-import {Button} from '@/app/[lang]/_components/Button'
+import { Button } from '@/app/[lang]/_components/Button'
-import type {TTermsItemData} from '@/app/[lang]/(terms)/_components/TermsAccordion'
-import type {Metadata} from 'next'
-import type {ReactNode} from 'react'
+import type { TTermsItemData } from '@/app/[lang]/(terms)/_components/TermsAccordion'
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
type TTermsPageProps = {
- title: string;
- items: TTermsItemData[];
-};
+ title: string
+ items: TTermsItemData[]
+}
/**************************************************************************************************
* Helper function to create dynamic metadata based on title
**************************************************************************************************/
-export function generateMetadata({title}: {title: string}): Metadata {
- return {
- title: `${title} | ShapeShift`,
- description: `ShapeShift ${title.toLowerCase()} - legal information for users.`,
- openGraph: {
- title: `ShapeShift ${title}`,
- description: `View ShapeShift's ${title.toLowerCase()} and legal information.`
- }
- }
+export function generateMetadata({ title }: { title: string }): Metadata {
+ return {
+ title: `${title} | ShapeShift`,
+ description: `ShapeShift ${title.toLowerCase()} - legal information for users.`,
+ openGraph: {
+ title: `ShapeShift ${title}`,
+ description: `View ShapeShift's ${title.toLowerCase()} and legal information.`,
+ },
+ }
}
/**************************************************************************************************
* Terms content component
**************************************************************************************************/
-function TermsContent({items}: TTermsPageProps): ReactNode {
- return (
-
-
- {items.map(item => (
-
- ))}
-
-
- )
+function TermsContent({ items }: TTermsPageProps): ReactNode {
+ return (
+
+
+ {items.map((item) => (
+
+ ))}
+
+
+ )
}
/**************************************************************************************************
* Loading fallback component
**************************************************************************************************/
function TermsLoadingSkeleton(): ReactNode {
- return (
-
- {Array.from({length: 3}).map((_, index) => (
-
- ))}
-
- )
+ return (
+
+ {Array.from({ length: 3 }).map((_, index) => (
+
+ ))}
+
+ )
}
/**************************************************************************************************
* Terms page component
**************************************************************************************************/
-export function TermsPage({title, items}: TTermsPageProps): ReactNode {
- return (
-
-
-
-
+export function TermsPage({ title, items }: TTermsPageProps): ReactNode {
+ return (
+
+
+
+
- }>
-
-
-
- )
+ }>
+
+
+
+ )
}
diff --git a/app/[lang]/(terms)/_components/utils.ts b/app/[lang]/(terms)/_components/utils.ts
index 9798a8a..9404f5a 100644
--- a/app/[lang]/(terms)/_components/utils.ts
+++ b/app/[lang]/(terms)/_components/utils.ts
@@ -6,42 +6,42 @@
** Enforces consistent data structure between different terms pages
**************************************************************************************************/
-import {getPrivacyPolicy, getTermsOfService} from '@/app/[lang]/_utils/query'
+import { getPrivacyPolicy, getTermsOfService } from '@/app/[lang]/_utils/query'
-import type {TTermsItemData} from '@/app/[lang]/(terms)/_components/TermsAccordion'
+import type { TTermsItemData } from '@/app/[lang]/(terms)/_components/TermsAccordion'
/**************************************************************************************************
* Transforms privacy policy data from API format to component format
**************************************************************************************************/
export async function getPrivacyPolicyItems(): Promise {
- const data = await getPrivacyPolicy()
-
- if (!data) {
- return []
- }
-
- return data.policy.map(policy => ({
- id: policy.id,
- title: policy.title,
- date: policy.date,
- content: policy.policy
- }))
+ const data = await getPrivacyPolicy()
+
+ if (!data) {
+ 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()
-
- if (!data) {
- return []
- }
-
- return data.terms.map(term => ({
- id: term.id,
- title: term.title,
- date: term.date,
- content: term.policy
- }))
+ const data = await getTermsOfService()
+
+ if (!data) {
+ return []
+ }
+
+ return data.terms.map((term) => ({
+ id: term.id,
+ title: term.title,
+ date: term.date,
+ content: term.policy,
+ }))
}
diff --git a/app/[lang]/(terms)/error.tsx b/app/[lang]/(terms)/error.tsx
index 682a734..c689fc3 100644
--- a/app/[lang]/(terms)/error.tsx
+++ b/app/[lang]/(terms)/error.tsx
@@ -7,41 +7,33 @@
**************************************************************************************************/
'use client'
-import {useEffect} from 'react'
+import { useEffect } from 'react'
-import {Button} from '@/app/[lang]/_components/Button'
+import { Button } from '@/app/[lang]/_components/Button'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
type TErrorProps = {
- error: Error & {digest?: string};
- reset: () => void;
-};
+ error: Error & { digest?: string }
+ reset: () => void
+}
-export default function TermsError({error, reset}: TErrorProps): ReactNode {
- useEffect(() => {
- // Log the error to console for debugging
- console.error('Terms page error:', error)
- }, [error])
+export default function TermsError({ error, reset }: TErrorProps): ReactNode {
+ useEffect(() => {
+ // Log the error to console for debugging
+ console.error('Terms page error:', error)
+ }, [error])
- return (
-
- {'Something went wrong'}
-
- {'We encountered an error while loading this page. Please try again later.'}
-
-
-
-
-
-
- )
+ return (
+
+ {'Something went wrong'}
+
+ {'We encountered an error while loading this page. Please try again later.'}
+
+
+
+
+
+
+ )
}
diff --git a/app/[lang]/(terms)/layout.tsx b/app/[lang]/(terms)/layout.tsx
index 1fe61e2..b30f6c9 100644
--- a/app/[lang]/(terms)/layout.tsx
+++ b/app/[lang]/(terms)/layout.tsx
@@ -6,35 +6,35 @@
** Sets up responsive container layout with consistent spacing
**************************************************************************************************/
-import {Banner} from '@/app/[lang]/_components/Banner'
+import { Banner } from '@/app/[lang]/_components/Banner'
-import type {Metadata} from 'next'
-import type {ReactNode} from 'react'
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
export const metadata: Metadata = {
- title: 'Legal Documents | ShapeShift',
- description: 'ShapeShift privacy policy and terms of service documents.',
- openGraph: {
- title: 'ShapeShift Legal Documents',
- description: 'Privacy Policy and Terms of Service for ShapeShift',
- type: 'website',
- locale: 'en_US',
- url: 'https://shapeshift.com/terms',
- siteName: 'ShapeShift'
- }
+ title: 'Legal Documents | ShapeShift',
+ description: 'ShapeShift privacy policy and terms of service documents.',
+ openGraph: {
+ title: 'ShapeShift Legal Documents',
+ description: 'Privacy Policy and Terms of Service for ShapeShift',
+ type: 'website',
+ locale: 'en_US',
+ url: 'https://shapeshift.com/terms',
+ siteName: 'ShapeShift',
+ },
}
type TTermsLayoutProps = {
- children: ReactNode;
-};
+ children: ReactNode
+}
-export default function TermsLayout({children}: TTermsLayoutProps): ReactNode {
- return (
- <>
- {children}
-
-
-
- >
- )
+export default function TermsLayout({ children }: TTermsLayoutProps): ReactNode {
+ return (
+ <>
+ {children}
+
+
+
+ >
+ )
}
diff --git a/app/[lang]/(terms)/privacy-policy/page.tsx b/app/[lang]/(terms)/privacy-policy/page.tsx
index 97a4496..74b8048 100644
--- a/app/[lang]/(terms)/privacy-policy/page.tsx
+++ b/app/[lang]/(terms)/privacy-policy/page.tsx
@@ -6,27 +6,22 @@
** Handles loading states and error cases gracefully
**************************************************************************************************/
-import {notFound} from 'next/navigation'
+import { notFound } from 'next/navigation'
-import {TermsPage, generateMetadata} from '@/app/[lang]/(terms)/_components/TermsPage'
-import {getPrivacyPolicyItems} from '@/app/[lang]/(terms)/_components/utils'
+import { TermsPage, generateMetadata } from '@/app/[lang]/(terms)/_components/TermsPage'
+import { getPrivacyPolicyItems } from '@/app/[lang]/(terms)/_components/utils'
-import type {Metadata} from 'next'
-import type {ReactNode} from 'react'
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
-export const metadata: Metadata = generateMetadata({title: 'Privacy Policy'})
+export const metadata: Metadata = generateMetadata({ title: 'Privacy Policy' })
export default async function PrivacyPolicyPage(): Promise {
- const items = await getPrivacyPolicyItems()
+ const items = await getPrivacyPolicyItems()
- if (!items.length) {
- return notFound()
- }
+ if (!items.length) {
+ return notFound()
+ }
- return (
-
- )
+ return
}
diff --git a/app/[lang]/(terms)/terms-of-service/page.tsx b/app/[lang]/(terms)/terms-of-service/page.tsx
index f95f84e..b8c3d85 100644
--- a/app/[lang]/(terms)/terms-of-service/page.tsx
+++ b/app/[lang]/(terms)/terms-of-service/page.tsx
@@ -6,29 +6,24 @@
** Handles loading states and error cases gracefully
*************************************************************************************************/
-import {notFound} from 'next/navigation'
+import { notFound } from 'next/navigation'
-import {TermsPage, generateMetadata} from '@/app/[lang]/(terms)/_components/TermsPage'
-import {getTermsOfServiceItems} from '@/app/[lang]/(terms)/_components/utils'
+import { TermsPage, generateMetadata } from '@/app/[lang]/(terms)/_components/TermsPage'
+import { getTermsOfServiceItems } from '@/app/[lang]/(terms)/_components/utils'
-import type {Metadata} from 'next'
-import type {ReactNode} from 'react'
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
export const metadata: Metadata = generateMetadata({
- title: 'Terms of Service'
+ title: 'Terms of Service',
})
export default async function TermsOfServicePage(): Promise {
- const items = await getTermsOfServiceItems()
+ const items = await getTermsOfServiceItems()
- if (!items.length) {
- return notFound()
- }
+ if (!items.length) {
+ return notFound()
+ }
- return (
-
- )
+ return
}
diff --git a/app/[lang]/_components/AnimatedHeight.tsx b/app/[lang]/_components/AnimatedHeight.tsx
index 33fb18b..0e18314 100644
--- a/app/[lang]/_components/AnimatedHeight.tsx
+++ b/app/[lang]/_components/AnimatedHeight.tsx
@@ -1,14 +1,14 @@
-import {motion} from 'framer-motion'
-import {useEffect, useRef, useState} from 'react'
+import { motion } from 'framer-motion'
+import { useEffect, useRef, useState } from 'react'
-import {cl} from '@/app/[lang]/_utils/cl'
+import { cl } from '@/app/[lang]/_utils/cl'
import type React from 'react'
type TAnimateChangeInHeightProps = {
- children: React.ReactNode;
- className?: string;
-};
+ children: React.ReactNode
+ className?: string
+}
/********************************************************************************************
* Animated Height Component
@@ -17,38 +17,39 @@ type TAnimateChangeInHeightProps = {
* Automatically adjusts height based on content changes.
********************************************************************************************/
-export const AnimateChangeInHeight: React.FC = ({children, className}) => {
- const containerRef = useRef(null)
- const [height, setHeight] = useState('auto')
-
- /********************************************************************************************
- * Effect: Sets up ResizeObserver to track content height changes
- * Deps: None - Observer is set up once on mount
- ********************************************************************************************/
- useEffect(() => {
- if (containerRef.current) {
- const resizeObserver = new ResizeObserver(entries => {
- // We only have one entry, so we can use entries[0].
- const observedHeight = entries[0].contentRect.height
- setHeight(observedHeight)
- })
-
- resizeObserver.observe(containerRef.current)
-
- return () => {
- // Cleanup the observer when the component is unmounted
- resizeObserver.disconnect()
- }
- }
- }, [])
-
- return (
-
- {children}
-
- )
+export const AnimateChangeInHeight: React.FC = ({ children, className }) => {
+ const containerRef = useRef(null)
+ const [height, setHeight] = useState('auto')
+
+ /********************************************************************************************
+ * Effect: Sets up ResizeObserver to track content height changes
+ * Deps: None - Observer is set up once on mount
+ ********************************************************************************************/
+ useEffect(() => {
+ if (containerRef.current) {
+ const resizeObserver = new ResizeObserver((entries) => {
+ // We only have one entry, so we can use entries[0].
+ const observedHeight = entries[0].contentRect.height
+ setHeight(observedHeight)
+ })
+
+ resizeObserver.observe(containerRef.current)
+
+ return () => {
+ // Cleanup the observer when the component is unmounted
+ resizeObserver.disconnect()
+ }
+ }
+ }, [])
+
+ return (
+
+ {children}
+
+ )
}
diff --git a/app/[lang]/_components/Banner.tsx b/app/[lang]/_components/Banner.tsx
index a3c8fca..6431333 100644
--- a/app/[lang]/_components/Banner.tsx
+++ b/app/[lang]/_components/Banner.tsx
@@ -1,84 +1,64 @@
import Image from 'next/image'
-import {Button} from '@/app/[lang]/_components/Button'
+import { Button } from '@/app/[lang]/_components/Button'
-import {LocalizedLink} from './LocalizedLink'
+import { LocalizedLink } from './LocalizedLink'
import {
- bannerLeftButtonTitle,
- bannerLeftTitle,
- bannerMobileSubtitle,
- bannerRightTitle,
- dAppUrl
+ bannerLeftButtonTitle,
+ bannerLeftTitle,
+ bannerMobileSubtitle,
+ bannerRightTitle,
+ dAppUrl,
} from '../_utils/constants'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
export function Banner(): ReactNode {
- return (
-
-
-
-
{bannerLeftTitle}
+ return (
+
+
+
+
{bannerLeftTitle}
-
-
-
-
-
{bannerMobileSubtitle}
+
+
+
+
+
{bannerMobileSubtitle}
-
+
+
+ )
}
diff --git a/app/[lang]/_components/BlogList.tsx b/app/[lang]/_components/BlogList.tsx
index a862812..606af64 100644
--- a/app/[lang]/_components/BlogList.tsx
+++ b/app/[lang]/_components/BlogList.tsx
@@ -1,33 +1,29 @@
'use client'
-import {Button} from '@/app/[lang]/_components/Button'
-import {useFetchPosts} from '@/app/[lang]/_hooks/useFetchPosts'
+import { Button } from '@/app/[lang]/_components/Button'
+import { useFetchPosts } from '@/app/[lang]/_hooks/useFetchPosts'
-import {BlogPost} from './BlogPost'
-import {LocalizedLink} from './LocalizedLink'
+import { BlogPost } from './BlogPost'
+import { LocalizedLink } from './LocalizedLink'
-import type {TBlogPost} from '@/app/[lang]/_components/strapi/types'
-import type {ReactNode} from 'react'
+import type { TBlogPost } from '@/app/[lang]/_components/strapi/types'
+import type { ReactNode } from 'react'
-export function LatestBlogPosts({limit, isWithTitle = true}: {limit: number; isWithTitle?: boolean}): ReactNode {
- const {posts} = useFetchPosts({page: 1, pageSize: limit, sort: 'desc'})
- return (
-
- {isWithTitle && (
-
-
{'Read more about ShapeShift.'}
-
-
-
-
- )}
- {posts.map((post: TBlogPost) => (
-
- ))}
-
- )
+export function LatestBlogPosts({ limit, isWithTitle = true }: { limit: number; isWithTitle?: boolean }): ReactNode {
+ const { posts } = useFetchPosts({ page: 1, pageSize: limit, sort: 'desc' })
+ return (
+
+ {isWithTitle && (
+
+
{'Read more about ShapeShift.'}
+
+
+
+
+ )}
+ {posts.map((post: TBlogPost) => (
+
+ ))}
+
+ )
}
diff --git a/app/[lang]/_components/BlogPost.tsx b/app/[lang]/_components/BlogPost.tsx
index 413f11e..c0e5fd6 100644
--- a/app/[lang]/_components/BlogPost.tsx
+++ b/app/[lang]/_components/BlogPost.tsx
@@ -1,11 +1,13 @@
import Image from 'next/image'
-import {useMemo} from 'react'
+import { useMemo } from 'react'
-import {LocalizedLink} from './LocalizedLink'
-import {cl} from '../_utils/cl'
+import { getStrapiImageUrl } from '@/app/[lang]/_utils/query'
-import type {TBlogPost} from '@/app/[lang]/_components/strapi/types'
-import type {ReactNode} from 'react'
+import { LocalizedLink } from './LocalizedLink'
+import { cl } from '../_utils/cl'
+
+import type { TBlogPost } from '@/app/[lang]/_components/strapi/types'
+import type { ReactNode } from 'react'
/********************************************************************************************
* Blog Post Card Component
@@ -15,133 +17,119 @@ import type {ReactNode} from 'react'
********************************************************************************************/
export function BlogPost({
- post,
- className,
- isClassic = false
+ post,
+ className,
+ isClassic = false,
}: {
- post: TBlogPost
- className?: string
- isClassic?: boolean
+ post: TBlogPost
+ className?: string
+ isClassic?: boolean
}): ReactNode {
- if (post.isFeatured && !isClassic) {
- return (
- <>
-
-
- >
- )
- }
+ if (post.isFeatured && !isClassic) {
+ return (
+ <>
+
+
+ >
+ )
+ }
- return (
-
- )
+ return
}
-function FeaturedPost({post, className}: {post: TBlogPost; className?: string}): ReactNode {
- const formatDate = useMemo(
- () =>
- (date: string): string => {
- return new Date(date).toLocaleDateString(undefined, {
- year: 'numeric',
- month: 'long',
- day: 'numeric'
- })
- },
- []
- )
+function FeaturedPost({ post, className }: { post: TBlogPost; className?: string }): ReactNode {
+ const formatDate = useMemo(
+ () =>
+ (date: string): string => {
+ return new Date(date).toLocaleDateString(undefined, {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })
+ },
+ []
+ )
- return (
-
-
- {post?.featuredImg?.url ? (
-
- ) : (
-
- )}
-
+ return (
+
+
+ {post?.featuredImg?.url ? (
+
+ ) : (
+
+ )}
+
-
-
- {post.type?.length > 1 && (
-
{post?.type.join(', ')}
- )}
-
{formatDate(post.publishedAt)}
-
-
-
-
- )
+
+
+ {post.type?.length > 1 &&
{post?.type.join(', ')}
}
+
{formatDate(post.publishedAt)}
+
+
+
+
+ )
}
-function PostCard({post, className}: {post: TBlogPost; className?: string}): ReactNode {
- /********************************************************************************************
- * Memo: Creates a date formatting function
- * No dependencies as it's a static function
- ********************************************************************************************/
- const formatDate = useMemo(
- () =>
- (date: string): string => {
- return new Date(date).toLocaleDateString(undefined, {
- year: 'numeric',
- month: 'long',
- day: 'numeric'
- })
- },
- []
- )
+function PostCard({ post, className }: { post: TBlogPost; className?: string }): ReactNode {
+ /********************************************************************************************
+ * Memo: Creates a date formatting function
+ * No dependencies as it's a static function
+ ********************************************************************************************/
+ const formatDate = useMemo(
+ () =>
+ (date: string): string => {
+ return new Date(date).toLocaleDateString(undefined, {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })
+ },
+ []
+ )
- return (
-
-
- {post?.featuredImg?.url ? (
-
- ) : (
-
- )}
-
+ return (
+
+
+ {post?.featuredImg?.url ? (
+
+ ) : (
+
+ )}
+
-
-
- {post.type?.length > 1 && (
-
{post?.type.join(', ')}
- )}
-
{formatDate(post.publishedAt)}
-
-
-
-
- )
+
+
+ {post.type?.length > 1 &&
{post?.type.join(', ')}
}
+
{formatDate(post.publishedAt)}
+
+
+
+
+ )
}
diff --git a/app/[lang]/_components/Button.tsx b/app/[lang]/_components/Button.tsx
index e33e39d..131f882 100644
--- a/app/[lang]/_components/Button.tsx
+++ b/app/[lang]/_components/Button.tsx
@@ -1,55 +1,53 @@
-import {LocalizedLink} from './LocalizedLink'
-import {IconNext} from '../_icons/IconNext'
-import {cl} from '../_utils/cl'
+import { LocalizedLink } from './LocalizedLink'
+import { IconNext } from '../_icons/IconNext'
+import { cl } from '../_utils/cl'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
-type TButtonVariant = 'blue' | 'white';
+type TButtonVariant = 'blue' | 'white'
type TButtonProps = {
- title?: string;
- variant?: TButtonVariant;
- className?: string;
- onClick?: () => void;
- href?: string;
- hasArrow?: boolean;
-};
+ title?: string
+ variant?: TButtonVariant
+ className?: string
+ onClick?: () => void
+ href?: string
+ hasArrow?: boolean
+}
export function Button(props: TButtonProps): ReactNode {
- const {variant = 'blue', hasArrow = false, ...rest} = props
- return (
- <>
- {props.href ? (
-
- {props.title}
- {hasArrow ? : null}
-
- ) : (
-
- {props.title}
- {hasArrow ? : null}
-
- )}
- >
- )
+ const { variant = 'blue', hasArrow = false, ...rest } = props
+ return (
+ <>
+ {props.href ? (
+
+ {props.title}
+ {hasArrow ? : null}
+
+ ) : (
+
+ {props.title}
+ {hasArrow ? : null}
+
+ )}
+ >
+ )
}
diff --git a/app/[lang]/_components/Carousel.tsx b/app/[lang]/_components/Carousel.tsx
index 8709ee5..cd15d0a 100644
--- a/app/[lang]/_components/Carousel.tsx
+++ b/app/[lang]/_components/Carousel.tsx
@@ -1,6 +1,6 @@
-import {cl} from '../_utils/cl'
+import { cl } from '../_utils/cl'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
/**************************************************************************************************
* Carousel Component
@@ -32,42 +32,41 @@ import type {ReactNode} from 'react'
*************************************************************************************************/
type TCarouselProps = React.HTMLAttributes & {
- /** The content to be displayed in the carousel */
- children: React.ReactNode;
- /** Whether to pause the animation when hovering over the carousel */
- pauseOnHover?: boolean;
- /** The direction in which the carousel should scroll */
- direction?: 'left' | 'right';
- /** The speed of the carousel animation in seconds (lower = faster) */
- speed?: number;
-};
+ /** The content to be displayed in the carousel */
+ children: React.ReactNode
+ /** Whether to pause the animation when hovering over the carousel */
+ pauseOnHover?: boolean
+ /** The direction in which the carousel should scroll */
+ direction?: 'left' | 'right'
+ /** The speed of the carousel animation in seconds (lower = faster) */
+ speed?: number
+}
export function Carousel({
- children,
- pauseOnHover = false,
- direction = 'left',
- //The lower the number, the faster the carousel
- speed = 20,
- className,
- ...props
+ children,
+ pauseOnHover = false,
+ direction = 'left',
+ //The lower the number, the faster the carousel
+ speed = 20,
+ className,
+ ...props
}: TCarouselProps): ReactNode {
- return (
-
-
-
- {children}
- {children}
-
-
-
- )
+ return (
+
+
+
+ {children}
+ {children}
+
+
+
+ )
}
diff --git a/app/[lang]/_components/ChainsBanner.tsx b/app/[lang]/_components/ChainsBanner.tsx
index a4f3d30..34c9c2e 100644
--- a/app/[lang]/_components/ChainsBanner.tsx
+++ b/app/[lang]/_components/ChainsBanner.tsx
@@ -1,48 +1,49 @@
import Image from 'next/image'
-import {Button} from '@/app/[lang]/_components/Button'
+import { Button } from '@/app/[lang]/_components/Button'
-import {cl} from '../_utils/cl'
+import { cl } from '../_utils/cl'
-import type {ReactNode} from 'react'
+import type { ReactNode } from 'react'
type TChainsBanner = {
- tag: string;
- title: string;
- href: string;
- buttonText: string;
-};
+ tag: string
+ title: string
+ href: string
+ buttonText: string
+}
export function ChainsBanner(data: TChainsBanner): ReactNode {
- return (
-
-
-
+ return (
+
+
+
-
-
{data.tag}
-
- {data.title}
-
-
-
-
-
- )
+
+
{data.tag}
+
+ {data.title}
+
+
+
+
+
+ )
}
diff --git a/app/[lang]/_components/ChatwootWidget.tsx b/app/[lang]/_components/ChatwootWidget.tsx
index de27468..b6bb4fc 100644
--- a/app/[lang]/_components/ChatwootWidget.tsx
+++ b/app/[lang]/_components/ChatwootWidget.tsx
@@ -2,30 +2,30 @@
'use client'
import Script from 'next/script'
-import {useEffect, useState} from 'react'
+import { useEffect, useState } from 'react'
-import type {ReactElement} from 'react'
+import type { ReactElement } from 'react'
type SecureMessageEvent = {
- data: {
- type?: string;
- eventName?: string;
- config?: Record;
- payload?: Record;
- };
-} & MessageEvent;
+ data: {
+ type?: string
+ eventName?: string
+ config?: Record
+ payload?: Record
+ }
+} & MessageEvent
declare global {
- // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
- interface Window {
- chatwootSDK?: {
- run: (config: {websiteToken: string; baseUrl: string}) => void;
- isLoaded?: boolean;
- };
- $chatwoot?: {
- reset?: () => void;
- };
- }
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+ interface Window {
+ chatwootSDK?: {
+ run: (config: { websiteToken: string; baseUrl: string }) => void
+ isLoaded?: boolean
+ }
+ $chatwoot?: {
+ reset?: () => void
+ }
+ }
}
const ALLOWED_CHATWOOT_ORIGINS = ['https://app.chatwoot.com', 'https://widget.chatwoot.com']
@@ -37,241 +37,237 @@ const BLOCKED_MESSAGE_TYPES = ['popoutChatWindow', 'chatwoot:popout']
const ALLOWED_CHATWOOT_DOMAINS = ['app.chatwoot.com', 'widget.chatwoot.com']
function isValidUrl(urlString: string): boolean {
- try {
- const url = new URL(urlString)
-
- if (url.protocol !== 'https:') {
- return false
- }
-
- if (!ALLOWED_CHATWOOT_DOMAINS.includes(url.hostname)) {
- return false
- }
-
- if (
- url.pathname.includes('..') ||
- url.pathname.includes('%') ||
- url.search.includes('javascript') ||
- url.hash.includes('javascript')
- ) {
- return false
- }
-
- return true
- } catch {
- return false
- }
+ try {
+ const url = new URL(urlString)
+
+ if (url.protocol !== 'https:') {
+ return false
+ }
+
+ if (!ALLOWED_CHATWOOT_DOMAINS.includes(url.hostname)) {
+ return false
+ }
+
+ if (
+ url.pathname.includes('..') ||
+ url.pathname.includes('%') ||
+ url.search.includes('javascript') ||
+ url.hash.includes('javascript')
+ ) {
+ return false
+ }
+
+ return true
+ } catch {
+ return false
+ }
}
function sanitizeValue(value: unknown, isUrl = false): string | number | boolean | null {
- if (typeof value === 'string') {
- if (isUrl) {
- return isValidUrl(value) ? value : null
- }
-
- const sanitized = value
- .replace(/
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
-
-
-