Skip to content

Commit

Permalink
feat: alchemy enhanced api integration
Browse files Browse the repository at this point in the history
  • Loading branch information
denniswon committed Mar 5, 2024
1 parent 64b5bb6 commit b9ba8ba
Show file tree
Hide file tree
Showing 19 changed files with 632 additions and 14 deletions.
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @dphilipson @avasisht23 @denniswon
2 changes: 1 addition & 1 deletion app/api/rpc/[...routes]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const runtime = 'edge';
export const preferredRegion = 'iad1';

export async function POST(req: Request, { params }: { params: { routes: string[] } }) {
const body = await req.json();
const body = await req.json().catch(console.error);

const res = await fetch(env.ALCHEMY_API_URL + `/${params.routes.join('/')}`, {
method: 'POST',
Expand Down
2 changes: 1 addition & 1 deletion app/api/rpc/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const runtime = 'edge';
export const preferredRegion = 'iad1';

export async function POST(req: Request) {
const body = await req.json();
const body = await req.json().catch(console.error);

const res = await fetch(env.ALCHEMY_RPC_URL, {
method: 'POST',
Expand Down
34 changes: 34 additions & 0 deletions app/nfts/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Image } from '@/components/images/individual';
import { ImageRecord, TagRecord, getXataClient } from '@/utils/xata';
import { JSONData } from '@xata.io/client';
import { compact } from 'lodash';
import { notFound } from 'next/navigation';

const xata = getXataClient();

const getImage = async (id: string) => {
const image = (await xata.db.image.read(id)) as ImageRecord;
if (!image?.image) {
return undefined;
}
return image.toSerializable();
};

export default async function Page({ params: { id } }: { params: { id: string } }) {
const image = await getImage(id);
if (!image) {
notFound();
}
const tagsFromImage = await xata.db['tag-to-image']
.filter({
'image.id': id
})
.select(['*', 'tag.*'])
.getMany();

const tags = compact(tagsFromImage.map((tag) => tag.tag?.toSerializable())) as JSONData<TagRecord>[];

const readOnly = process.env.READ_ONLY === 'true';

return <Image image={image} tags={tags} readOnly={readOnly} />;
}
77 changes: 77 additions & 0 deletions app/nfts/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Images, TagWithImageCount } from '@/components/images';
import { useAccountContext } from '@/context/account';
import useNftsForOwner from '@/hooks/assets.ts/useNftsForOwner';
import { IMAGE_SIZE } from '@/utils/constants';
import { compact, pick } from 'lodash';

export default async function Page({ searchParams }: { searchParams: { page: string } }) {
const pageNumber = parseInt(searchParams.page) || 1;

const { client } = useAccountContext();

// TODO replace with react-query infinite query for pagination

const { items, count } = useNftsForOwner({ address: client.account });

// This page object is needed for building the buttons in the pagination component
const page = {
pageNumber,
hasNextPage: imagesPage.hasNextPage(),
hasPreviousPage: pageNumber > 1,
totalNumberOfPages
};

// transform helper to create a thumbnail for each image and apply it to the image object
console.time('Fetching images transforms');
const images = compact(
await Promise.all(
imagesPage.records.map(async (record) => {
if (!record.image) {
return undefined;
}

const { url } = record.image.transform({
width: IMAGE_SIZE,
height: IMAGE_SIZE,
format: 'auto',
fit: 'cover',
gravity: 'top'
});

// Since the resulting image will be a square, we don't really need to fetch the metadata in this case.
// The meta data provides both the original and transformed dimensions of the image.
// The metadataUrl you get from the transform() call.
// const metadata = await fetchMetadata(metadataUrl);

if (!url) {
return undefined;
}

const thumb = {
url,
attributes: {
width: IMAGE_SIZE, // Post transform width
height: IMAGE_SIZE // Post transform height
}
};

return { ...record.toSerializable(), thumb };
})
)
);
console.timeEnd('Fetching images transforms');

// Find the top 10 tags
const tags = topTags.summaries.map((tagSummary) => {
const tag = tagSummary.tag;
const serializableTag = pick(tag, ['id', 'name', 'slug']);
return {
...serializableTag,
imageCount: tagSummary.imageCount
};
}) as TagWithImageCount[];

const readOnly = process.env.READ_ONLY === 'true';

return <Images images={images} tags={tags} page={page} readOnly={readOnly} />;
}
2 changes: 1 addition & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function Home() {
return (
<Center flex={1}>
{isLoadingUser ? (
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="primary.500" size="xl" />
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="primary.500" size="3xl" />
) : account == null ? (
<LoginSignupCard signer={signer} onLogin={refetchUserDetails} />
) : (
Expand Down
2 changes: 1 addition & 1 deletion app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { SignerContextProvider } from '@/context/signer';
import { GlobalStyle } from '@/theme/globalstyles';
import { default as theme } from '@/theme/theme';
import { getRpcUrl } from '@/utils/rpc';
import { getRpcUrl } from '@/utils/alchemy';
import { CacheProvider } from '@chakra-ui/next-js';
import { ChakraProvider, ColorModeScript, extendTheme } from '@chakra-ui/react';
import { Global } from '@emotion/react';
Expand Down
4 changes: 2 additions & 2 deletions components/auth/EmailForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const EmailForm = ({ onSubmit, buttonDisabled }: EmailFormProps) => {
return (
<form onSubmit={handleSubmit(_onSubmit)} style={{ width: '70%' }}>
<FormControl isRequired isInvalid={!!errors.email}>
<FormLabel htmlFor="email" ms={1}>
<FormLabel htmlFor="email" ms={1} fontSize="0.8rem">
Email
</FormLabel>
<Input
Expand All @@ -43,7 +43,7 @@ const EmailForm = ({ onSubmit, buttonDisabled }: EmailFormProps) => {
message: 'Please enter a valid email'
}
})}
mt={1}
my={2}
/>
<Button
mt={3}
Expand Down
6 changes: 3 additions & 3 deletions components/auth/LoginSignupCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const LoginSignupCard = ({ signer, onLogin }: Props) => {

<TabPanels>
<TabPanel>
<Box h="36vh" alignContent="center" py="6vh">
<Box h="36vh" alignContent="center" py="4vh">
{email && isPending ? (
<Alert
status="success"
Expand All @@ -90,10 +90,10 @@ export const LoginSignupCard = ({ signer, onLogin }: Props) => {
height="200px"
>
<AlertIcon boxSize="40px" mr={0} />
<AlertTitle mt={4} mb={1} fontSize="lg">
<AlertTitle mt={4} mb={1} fontSize="md">
Check your email
</AlertTitle>
<AlertDescription maxWidth="xs">
<AlertDescription fontSize="xs">
We sent an email to you at {email}. It has a magic link that&apos;ll log you in.
</AlertDescription>
</Alert>
Expand Down
144 changes: 144 additions & 0 deletions components/nfts/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
'use client';

import { ImageRecord, TagRecord } from '@/utils/xata';
import { Link } from '@chakra-ui/next-js';
import { Box, Flex, Heading, Select, SimpleGrid, Tag, Text } from '@chakra-ui/react';
import { JSONData } from '@xata.io/client';
import { range } from 'lodash';
import Image from 'next/image';
import NextLink from 'next/link';
import { useRouter } from 'next/navigation';
import { FC } from 'react';
import { Search } from '../search';
import { ImageUpload } from './upload';

// cast it back to the original type as JSON Data from Serializable
export type ImageRecordWithThumb = JSONData<ImageRecord> & {
thumb: {
url: string;
attributes: {
width: number;
height: number;
};
};
};

// A similar strategy is used for tags
export type TagWithImageCount = JSONData<TagRecord> & {
imageCount: number;
};

export type Page = {
pageNumber: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
totalNumberOfPages: number;
};

type ImagesProps = {
images: ImageRecordWithThumb[];
tags: TagWithImageCount[];
page: Page;
readOnly: boolean;
};

export const Images: FC<ImagesProps> = ({ images, tags, page, readOnly }) => {
const currentPage = page.pageNumber;
const router = useRouter();

// render the tags in different ways depending on how many there are
const renderTags = (tags: TagWithImageCount[]) => {
if (tags.length === 0) {
return null;
}

if (tags.length > 1) {
return (
<>
<Heading as="h1" size="md" mb={8}>
All images
</Heading>
{tags && (
<Flex mb={8} gap={2} wrap="wrap">
{tags.map((tag) => (
<Tag as={NextLink} key={tag.id} href={`/tags/${tag.id}`} gap={2}>
{tag.name}
<Flex
alignItems="center"
justifyContent="center"
fontSize="xs"
bg="contrastLowest"
boxSize={4}
borderRadius="md"
color="contrastMedium"
>
{tag.imageCount}
</Flex>
</Tag>
))}
</Flex>
)}
</>
);
}

return (
<>
<Heading as="h1" size="md" mb={8}>
{tags[0].imageCount} images tagged with <Tag>{tags[0].name}</Tag>
</Heading>
<Flex mb={8} gap={2} wrap="wrap">
<Link href="/">&laquo; Back to all images</Link>
</Flex>
</>
);
};

return (
<>
<Flex alignItems="start" justifyContent="space-between" mb={8}>
<ImageUpload readOnly={readOnly} />
<Search />
</Flex>
{renderTags(tags)}
{images.length === 0 && <Text>No images yet added</Text>}
<SimpleGrid columns={{ base: 2, md: 3, lg: 4 }} spacing={4}>
{/* thumbnails generated on server side based for images */}
{images.map(({ id, name, thumb }) => {
return (
<Box borderRadius="md" overflow="hidden" key={id}>
<NextLink href={`/images/${id}`}>
<Image
src={thumb.url}
width={thumb.attributes.width}
height={thumb.attributes.height}
alt={name ?? 'unknown image'}
/>
</NextLink>
</Box>
);
})}
</SimpleGrid>
{/*
with server side created page object that contains information about the current page,
find the current page from the router query.
*/}
{page.totalNumberOfPages > 1 && (
<Flex justifyContent="center" mt={8}>
<Flex gap={4} alignItems="center">
{page.hasPreviousPage && <Link href={`?page=${currentPage - 1}`}>Previous</Link>}
<Select onChange={(event) => router.push(`?page=${event.target.value}`)} value={currentPage}>
{range(1, page.totalNumberOfPages + 1).map((pageNumber) => (
<option key={pageNumber} value={pageNumber}>
{pageNumber}
</option>
))}
</Select>

{page.hasNextPage && <Link href={`?page=${currentPage + 1}`}>Next</Link>}
</Flex>
</Flex>
)}
</>
);
};
Loading

0 comments on commit b9ba8ba

Please sign in to comment.