Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add dynamic swap page #1575

Open
wants to merge 30 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bfb94fd
chore: setup dynamic swap page
dennyscode Dec 9, 2024
d15891f
fix: clean up img urls
dennyscode Dec 9, 2024
ccd1048
Merge branch 'develop' into LF-11128-jumper-create-how-to-swap-on-x-s…
dennyscode Dec 9, 2024
7b0573f
fix: meta title, dark-theme issues, welcomeScreenClosed,..
dennyscode Dec 9, 2024
11703f3
Merge branch 'develop' into LF-11128-jumper-create-how-to-swap-on-x-s…
dennyscode Dec 10, 2024
75bd05d
fix: use chain-name as segment param
dennyscode Dec 10, 2024
40f56c8
refactor: cleanup
dennyscode Dec 11, 2024
ce99aaf
refactor: cleanup
dennyscode Dec 11, 2024
575952d
Merge branch 'develop' into LF-11128-jumper-create-how-to-swap-on-x-s…
dennyscode Dec 11, 2024
fbc23a2
Merge branch 'develop' into LF-11128-jumper-create-how-to-swap-on-x-s…
dennyscode Dec 12, 2024
3e4425a
chore: add swap-quote-images
dennyscode Dec 17, 2024
adf04ce
fix: update yarn.lock
dennyscode Dec 17, 2024
5a21272
Merge branch 'develop' into LF-11128-jumper-create-how-to-swap-on-x-s…
dennyscode Dec 17, 2024
e6b65ac
chore: rename styled component
dennyscode Dec 17, 2024
079376d
refactor: remove duplicate
dennyscode Dec 17, 2024
fe6cb7d
fix: sitemap and segments with empty space
dennyscode Dec 17, 2024
4e8a64f
chore: cleanup and fix token explorer links
dennyscode Dec 17, 2024
9cdc859
refactor: remove comment
dennyscode Dec 17, 2024
74d1de0
Merge branch 'develop' into LF-11128-jumper-create-how-to-swap-on-x-s…
dennyscode Dec 18, 2024
0fddf0a
fix: yarn.lock
dennyscode Dec 18, 2024
074e91c
Merge branch 'develop' into LF-11128-jumper-create-how-to-swap-on-x-s…
dennyscode Jan 7, 2025
a5aee59
fix: Label
dennyscode Jan 7, 2025
195e9e8
style: fix table cell bottom border color
dennyscode Jan 7, 2025
b5c5cc1
chore: replace next/image in favor of img tag
dennyscode Jan 7, 2025
e455783
refactor: create re-usable util function
dennyscode Jan 7, 2025
f6f3140
refactor: remove commented code
dennyscode Jan 7, 2025
40b845a
Merge branch 'develop' into LF-11128-jumper-create-how-to-swap-on-x-s…
dennyscode Jan 7, 2025
8e0e803
Merge branch 'develop' into LF-11128-jumper-create-how-to-swap-on-x-s…
dennyscode Jan 8, 2025
b4b90e6
Merge branch 'develop' into LF-11128-jumper-create-how-to-swap-on-x-s…
dennyscode Jan 14, 2025
a8c284d
chore: add faq questions
Jan 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,3 @@ Register on Crowdin and you can start translating the project into your preferre
Your contributions will help make our project accessible to a wider audience around the world.

Thank you for your support!


20,229 changes: 9,065 additions & 11,164 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

Binary file added public/widget/widget-connect-wallet-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/widget/widget-connect-wallet-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/widget/widget-quotes-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/widget/widget-quotes-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/widget/widget-swap-amounts-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/widget/widget-swap-amounts-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/widget/widget-swap-quotes-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/widget/widget-swap-quotes-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 82 additions & 0 deletions src/app/[lng]/swap/[segments]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { siteName } from '@/app/lib/metadata';
import { getSiteUrl } from '@/const/urls';
import { getChainsQuery } from '@/hooks/useChains';
import { getTokensQuery } from '@/hooks/useTokens';
import { getChainByName } from '@/utils/tokenAndChain';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import SwapPage from 'src/app/ui/swap/SwapPage';

export async function generateMetadata({
params,
}: {
params: { segments: string };
}): Promise<Metadata> {
const { chains } = await getChainsQuery();
const sourceChain = getChainByName(chains, params.segments);
const title = `Jumper | How To Swap on ${sourceChain?.name} | A Complete Guide`;

const openGraph: Metadata['openGraph'] = {
title: title,
description: `Jumper offers the best way to swap tokens on ${sourceChain?.name} with the fastest speeds, lowest costs, and most secure swap providers available.`,
siteName: siteName,
url: `${getSiteUrl()}/swap/${params.segments.replace('-', ' ').toLowerCase()}`,
type: 'article',
};

return {
title,
description: title,
twitter: openGraph,
openGraph,
alternates: {
canonical: `${getSiteUrl()}/swap/${params.segments}`,
},
};
}

export const revalidate = 86400;
export const dynamicParams = true; // or false, to 404 on unknown paths
export const dynamic = 'force-dynamic';

export async function generateStaticParams() {
return [];
}

export default async function Page({
params: { segments },
}: {
params: { segments: string };
}) {
try {
const chainName = decodeURIComponent(
segments.replace('-', ' ').toLowerCase(),
);
const { chains } = await getChainsQuery();
const { tokens } = await getTokensQuery();
const sourceChain = getChainByName(chains, chainName);
if (!sourceChain) {
return notFound();
}

const chainTokens = tokens[sourceChain.id];
let sourceToken, destinationToken;
if (chainTokens) {
sourceToken = chainTokens[0];
destinationToken = chainTokens[1];
}

return (
<SwapPage
sourceChain={sourceChain}
sourceToken={sourceToken}
destinationChain={sourceChain}
chainName={chainName}
destinationToken={destinationToken}
tokens={tokens}
/>
);
} catch (e) {
notFound();
}
}
13 changes: 13 additions & 0 deletions src/app/[lng]/swap/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Metadata } from 'next';
import type { PropsWithChildren } from 'react';
import { Layout } from 'src/Layout';

export const metadata: Metadata = {
other: {
'partner-theme': 'default',
},
};

export default async function InfosLayout({ children }: PropsWithChildren) {
return <Layout>{children}</Layout>;
}
90 changes: 90 additions & 0 deletions src/app/api/widget-amounts/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* eslint-disable @next/next/no-img-element */

/**
* Image Generation of Widget for SEO pages
* Step 4 - Route execution
*
* Example:
* ```
* http://localhost:3000/api/widget-amounts?chainName=arbitrum&amount=1&theme=light
* ```
*
* @typedef {Object} SearchParams
* @property {number} chainId - The chain ID to send from.
* @property {number} amount - The amount of tokens.
* @property {'light'|'dark'} [theme] - The theme for the widget (optional).
*
*/

import { ImageResponse } from 'next/og';
import type { CSSProperties } from 'react';
import type { HighlightedAreas } from 'src/components/ImageGeneration/ImageGeneration.types';
import { imageResponseOptions } from 'src/components/ImageGeneration/imageResponseOptions';
import { imageFrameStyles } from 'src/components/ImageGeneration/style';
import WidgetAmountsImage from 'src/components/ImageGeneration/WidgetAmountImage';
import { getChainsQuery } from 'src/hooks/useChains';
import { getTokensQuery } from 'src/hooks/useTokens';
import { parseSearchParams } from 'src/utils/image-generation/parseSearchParams';
import { sortChainsBySpecificName } from 'src/utils/image-generation/sortChains';

const WIDGET_IMAGE_WIDTH = 416;
const WIDGET_IMAGE_HEIGHT = 536;
const WIDGET_IMAGE_SCALING_FACTOR = 2;

export async function GET(request: Request) {
const { chainName, theme, amount, highlighted } = parseSearchParams(
request.url,
);

if (!chainName) {
return;
}

// Fetch data asynchronously before rendering
const { chains } = await getChainsQuery();
const sortedChains = sortChainsBySpecificName(chains, chainName);
const { tokens } = await getTokensQuery();
const sortedTokensByChainId =
sortedChains[0]?.id && tokens[sortedChains[0]?.id].slice(0, 4);
const options = await imageResponseOptions({
width: WIDGET_IMAGE_WIDTH,
height: WIDGET_IMAGE_HEIGHT,
scalingFactor: WIDGET_IMAGE_SCALING_FACTOR,
});

const imageFrameStyle = imageFrameStyles({
width: WIDGET_IMAGE_WIDTH,
height: WIDGET_IMAGE_HEIGHT,
scalingFactor: WIDGET_IMAGE_SCALING_FACTOR,
}) as CSSProperties;

const imageStyle = imageFrameStyles({
width: WIDGET_IMAGE_WIDTH,
height: WIDGET_IMAGE_HEIGHT,
scalingFactor: WIDGET_IMAGE_SCALING_FACTOR,
}) as CSSProperties;

return new ImageResponse(
(
<div style={imageFrameStyle}>
<img
alt="Widget Amount Example"
width={'100%'}
height={'100%'}
style={imageStyle}
src={`${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL ? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}` : process.env.NEXT_PUBLIC_SITE_URL}/widget/widget-swap-amounts-${theme === 'dark' ? 'dark' : 'light'}.png`} //${theme === 'dark' ? 'dark' : 'light'}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to use getSiteUrl() instead

/>
<WidgetAmountsImage
height={WIDGET_IMAGE_WIDTH}
width={WIDGET_IMAGE_HEIGHT}
theme={theme as 'light' | 'dark'}
chains={sortedChains}
amount={amount}
tokens={sortedTokensByChainId || undefined}
highlighted={highlighted as HighlightedAreas}
/>
</div>
),
options,
);
}
2 changes: 1 addition & 1 deletion src/app/api/widget-quotes/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export async function GET(request: Request) {
width={'100%'}
height={'100%'}
style={imageStyle}
src={`${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL ? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}` : process.env.NEXT_PUBLIC_SITE_URL}/widget/widget-quotes-${theme === 'dark' ? 'dark' : 'light'}.png`}
src={`${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL ? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}` : process.env.NEXT_PUBLIC_SITE_URL}/widget/widget${isSwap ? '-swap' : ''}-quotes-${theme === 'dark' ? 'dark' : 'light'}.png`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to use getSiteUrl() instead

/>
<WidgetQuoteImage
theme={theme as 'light' | 'dark'}
Expand Down
34 changes: 25 additions & 9 deletions src/app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { getSiteUrl, JUMPER_LEARN_PATH, pages } from '@/const/urls';
import {
getSiteUrl,
JUMPER_LEARN_PATH,
JUMPER_SWAP_PATH,
pages,
} from '@/const/urls';
import type { ChangeFrequency, SitemapPage } from '@/types/sitemap';
import type { BlogArticleData, StrapiResponse } from '@/types/strapi';
import type { MetadataRoute } from 'next';
import { getChainsQuery } from 'src/hooks/useChains';
import { removeTrailingSlash } from 'src/utils/removeTrailingSlash';
import { getArticles } from './lib/getArticles';
import { locales } from 'src/i18n';

function withoutTrailingSlash(url: string) {
return url.endsWith('/') ? url.slice(0, -1) : url;
}

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// paths
const routes = pages.flatMap((route: SitemapPage) => {
return {
url: withoutTrailingSlash(`${getSiteUrl()}${route.path}`),
url: removeTrailingSlash(`${getSiteUrl()}${route.path}`),
lastModified: new Date().toISOString().split('T')[0],
changeFrequency: 'weekly' as ChangeFrequency,
priority: route.priority,
Expand All @@ -25,7 +27,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
(article: StrapiResponse<BlogArticleData>) => {
return article.data.map((el) => {
return {
url: withoutTrailingSlash(
url: removeTrailingSlash(
`${getSiteUrl()}${JUMPER_LEARN_PATH}/${el.attributes.Slug}`,
),
lastModified: new Date(
Expand All @@ -40,5 +42,19 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
},
);

return [...routes, ...articles];
// swap pages
const { chains } = await getChainsQuery();
const swapPages = chains.map((chain) => {
return {
url: removeTrailingSlash(
`${getSiteUrl()}${JUMPER_SWAP_PATH}/${chain.name}`
.replace(' ', '-')
.toLowerCase(),
),
lastModified: new Date().toISOString().split('T')[0],
priority: 0.4,
};
});

return [...routes, ...articles, ...swapPages];
}
6 changes: 3 additions & 3 deletions src/app/ui/bridge/BridgeExplanation.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use client';
import { BridgePageContainer } from '@/app/ui/bridge/BridgePage.style';
import { Typography } from '@mui/material';
import { DynamicPagesContainer } from 'src/components/DynamicPagesContainer';

const BridgeExplanationSection = () => {
return (
<BridgePageContainer>
<DynamicPagesContainer>
<Typography variant="h3" marginY={2} sx={{ fontSize: '32px' }}>
What is a Blockchain / Crypto Bridge?
</Typography>
Expand Down Expand Up @@ -130,7 +130,7 @@ const BridgeExplanationSection = () => {
This convenience not only improves user satisfaction but also encourages
broader adoption of blockchain technology.
</Typography>
</BridgePageContainer>
</DynamicPagesContainer>
);
};

Expand Down
2 changes: 1 addition & 1 deletion src/app/ui/bridge/BridgePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getChainInfoData, getTokenInfoData } from '@/app/ui/bridge/utils';
import { Widget } from '@/components/Widgets/Widget';
import type { ExtendedChain, Token, TokensResponse } from '@lifi/sdk';
import { Container, Stack, Typography } from '@mui/material';
import InformationCard from 'src/app/ui/bridge/InformationCard';
import InformationCard from 'src/components/InformationCard/InformationCard';
import BridgeExplanationSection from './BridgeExplanation';
import PopularBridgeSection from './PopularBridgeSection';
import StepsExplainerSection from './StepsExplainer';
Expand Down
13 changes: 0 additions & 13 deletions src/app/ui/bridge/InformationCard.style.ts

This file was deleted.

12 changes: 6 additions & 6 deletions src/app/ui/bridge/PopularBridgeSection.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use client';
import { BridgePageContainer } from '@/app/ui/bridge/BridgePage.style';
import generateKey from '@/app/lib/generateKey';
import { getChainById } from '@/utils/tokenAndChain';
import type { ExtendedChain, Token, TokensResponse } from '@lifi/sdk';
import { Link as MuiLink, Stack, Typography } from '@mui/material';
import Link from 'next/link';
import type { ExtendedChain, Token, TokensResponse } from '@lifi/sdk';
import { getChainById, getTokenByName } from '@/utils/tokenAndChain';
import generateKey from '@/app/lib/generateKey';
import { DynamicPagesContainer } from 'src/components/DynamicPagesContainer';

interface PopularBridgeProps {
sourceChain: ExtendedChain;
Expand Down Expand Up @@ -53,7 +53,7 @@ const PopularBridgeSection = ({
);

return (
<BridgePageContainer width="100%">
<DynamicPagesContainer width="100%">
<Typography variant="h3" marginY={2}>
Popular bridges
</Typography>
Expand All @@ -72,7 +72,7 @@ const PopularBridgeSection = ({
</MuiLink>
))}
</Stack>
</BridgePageContainer>
</DynamicPagesContainer>
);
};

Expand Down
14 changes: 7 additions & 7 deletions src/app/ui/bridge/StepsExplainer.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
'use client';
import { BridgePageContainer } from '@/app/ui/bridge/BridgePage.style';
import type { ExtendedChain, Token } from '@lifi/sdk';
import { Link as MuiLink, Typography, useTheme } from '@mui/material';
import Link from 'next/link';
import React from 'react';
import { Fragment } from 'react';
import { Divider } from 'src/components/Blog';
import { DynamicPagesContainer } from 'src/components/DynamicPagesContainer';
import StepDetail from 'src/components/StepDetail/StepDetail';
import { getWidgetImageProps } from 'src/utils/image-generation/getWidgetImage';
import StepDetail from './StepDetail';

interface StepsExplainerProps {
sourceChain: ExtendedChain;
Expand Down Expand Up @@ -157,7 +157,7 @@ const StepsExplainerSection = ({
];

return (
<BridgePageContainer sx={(theme) => ({ marginTop: theme.spacing(4) })}>
<DynamicPagesContainer sx={(theme) => ({ marginTop: theme.spacing(4) })}>
<Typography
variant="h2"
color="text.primary"
Expand All @@ -176,17 +176,17 @@ const StepsExplainerSection = ({
</Typography>

{steps.map((step, index) => (
<React.Fragment key={index}>
<Fragment key={index}>
<Divider />
<StepDetail
title={step.title}
description={step.description}
content={step.content}
img={step.img}
/>
</React.Fragment>
</Fragment>
))}
</BridgePageContainer>
</DynamicPagesContainer>
);
};

Expand Down
Loading
Loading