test
This commit is contained in:
		| @@ -0,0 +1,25 @@ | ||||
| import { cache } from "react" | ||||
| import { getPokemon } from "@/registry/new-york/complex-component/lib/pokemon" | ||||
| import { Card, CardContent } from "@/components/ui/card" | ||||
| import { PokemonImage } from "@/registry/new-york/complex-component/components/pokemon-image" | ||||
|  | ||||
| const cachedGetPokemon = cache(getPokemon) | ||||
|  | ||||
| export async function PokemonCard({ name }: { name: string }) { | ||||
|   const pokemon = await cachedGetPokemon(name) | ||||
|  | ||||
|   if (!pokemon) { | ||||
|     return null | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Card> | ||||
|       <CardContent className="flex flex-col items-center p-2"> | ||||
|         <div> | ||||
|           <PokemonImage name={pokemon.name} number={pokemon.id} /> | ||||
|         </div> | ||||
|         <div className="text-center font-medium">{pokemon.name}</div> | ||||
|       </CardContent> | ||||
|     </Card> | ||||
|   ) | ||||
| } | ||||
| @@ -0,0 +1,20 @@ | ||||
| "use client" | ||||
|  | ||||
| /* eslint-disable @next/next/no-img-element */ | ||||
| import { usePokemonImage } from "@/registry/new-york/complex-component/hooks/use-pokemon" | ||||
|  | ||||
| export function PokemonImage({ | ||||
|   name, | ||||
|   number, | ||||
| }: { | ||||
|   name: string | ||||
|   number: number | ||||
| }) { | ||||
|   const imageUrl = usePokemonImage(number) | ||||
|  | ||||
|   if (!imageUrl) { | ||||
|     return null | ||||
|   } | ||||
|  | ||||
|   return <img src={imageUrl} alt={name} /> | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| "use client" | ||||
|  | ||||
| // Totally unnecessary hook, but it's a good example of how to use a hook in a custom registry. | ||||
|  | ||||
| export function usePokemonImage(number: number) { | ||||
|   return `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${number}.png` | ||||
| } | ||||
| @@ -0,0 +1,48 @@ | ||||
| import { z } from "zod" | ||||
|  | ||||
| export async function getPokemonList({ limit = 10 }: { limit?: number }) { | ||||
|   try { | ||||
|     const response = await fetch( | ||||
|       `https://pokeapi.co/api/v2/pokemon?limit=${limit}` | ||||
|     ) | ||||
|     return z | ||||
|       .object({ | ||||
|         results: z.array(z.object({ name: z.string() })), | ||||
|       }) | ||||
|       .parse(await response.json()) | ||||
|   } catch (error) { | ||||
|     console.error(error) | ||||
|     return null | ||||
|   } | ||||
| } | ||||
|  | ||||
| export async function getPokemon(name: string) { | ||||
|   try { | ||||
|     const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`) | ||||
|  | ||||
|     if (!response.ok) { | ||||
|       throw new Error("Failed to fetch pokemon") | ||||
|     } | ||||
|  | ||||
|     return z | ||||
|       .object({ | ||||
|         name: z.string(), | ||||
|         id: z.number(), | ||||
|         sprites: z.object({ | ||||
|           front_default: z.string(), | ||||
|         }), | ||||
|         stats: z.array( | ||||
|           z.object({ | ||||
|             base_stat: z.number(), | ||||
|             stat: z.object({ | ||||
|               name: z.string(), | ||||
|             }), | ||||
|           }) | ||||
|         ), | ||||
|       }) | ||||
|       .parse(await response.json()) | ||||
|   } catch (error) { | ||||
|     console.error(error) | ||||
|     return null | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,23 @@ | ||||
| import { cache } from "react" | ||||
| import { PokemonCard } from "@/registry/new-york/complex-component/components/pokemon-card" | ||||
| import { getPokemonList } from "@/registry/new-york/complex-component/lib/pokemon" | ||||
|  | ||||
| const getCachedPokemonList = cache(getPokemonList) | ||||
|  | ||||
| export default async function Page() { | ||||
|   const pokemons = await getCachedPokemonList({ limit: 12 }) | ||||
|  | ||||
|   if (!pokemons) { | ||||
|     return null | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div className="mx-auto w-full max-w-2xl px-4"> | ||||
|       <div className="grid grid-cols-2 gap-4 py-10 sm:grid-cols-3 md:grid-cols-4"> | ||||
|         {pokemons.results.map((p) => ( | ||||
|           <PokemonCard key={p.name} name={p.name} /> | ||||
|         ))} | ||||
|       </div> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
| @@ -0,0 +1,164 @@ | ||||
| "use client" | ||||
|  | ||||
| import * as React from "react" | ||||
| import { | ||||
|   Card, | ||||
|   CardTitle, | ||||
|   CardHeader, | ||||
|   CardDescription, | ||||
|   CardContent, | ||||
|   CardFooter, | ||||
| } from "@/components/ui/card" | ||||
| import { Input } from "@/components/ui/input" | ||||
| import { Label } from "@/components/ui/label" | ||||
| import { Button } from "@/components/ui/button" | ||||
| import { Textarea } from "@/components/ui/textarea" | ||||
| import { z } from "zod" | ||||
|  | ||||
| const exampleFormSchema = z.object({ | ||||
|   name: z.string().min(1), | ||||
|   email: z.string().email(), | ||||
|   message: z.string().min(1), | ||||
| }) | ||||
|  | ||||
| export function ExampleForm() { | ||||
|   const [pending, setPending] = React.useState(false) | ||||
|   const [state, setState] = React.useState({ | ||||
|     defaultValues: { | ||||
|       name: "", | ||||
|       email: "", | ||||
|       message: "", | ||||
|     }, | ||||
|     success: false, | ||||
|     errors: { | ||||
|       name: "", | ||||
|       email: "", | ||||
|       message: "", | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   const handleSubmit = React.useCallback( | ||||
|     (e: React.FormEvent<HTMLFormElement>) => { | ||||
|       e.preventDefault() | ||||
|       setPending(true) | ||||
|  | ||||
|       const formData = new FormData(e.target as HTMLFormElement) | ||||
|       const data = Object.fromEntries(formData.entries()) | ||||
|       const result = exampleFormSchema.safeParse(data) | ||||
|  | ||||
|       if (!result.success) { | ||||
|         setState({ | ||||
|           ...state, | ||||
|           errors: Object.fromEntries( | ||||
|             Object.entries(result.error.flatten().fieldErrors).map( | ||||
|               ([key, value]) => [key, value?.[0] ?? ""] | ||||
|             ) | ||||
|           ) as Record<keyof typeof state.errors, string>, | ||||
|         }) | ||||
|         setPending(false) | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       setPending(false) | ||||
|     }, | ||||
|     [state] | ||||
|   ) | ||||
|  | ||||
|   return ( | ||||
|     <Card className="w-full max-w-sm"> | ||||
|       <CardHeader> | ||||
|         <CardTitle>How can we help?</CardTitle> | ||||
|         <CardDescription> | ||||
|           Need help with your project? We're here to assist you. | ||||
|         </CardDescription> | ||||
|       </CardHeader> | ||||
|       <form onSubmit={handleSubmit}> | ||||
|         <CardContent className="flex flex-col gap-6"> | ||||
|           <div | ||||
|             className="group/field grid gap-2" | ||||
|             data-invalid={!!state.errors?.name} | ||||
|           > | ||||
|             <Label | ||||
|               htmlFor="name" | ||||
|               className="group-data-[invalid=true]/field:text-destructive" | ||||
|             > | ||||
|               Name <span aria-hidden="true">*</span> | ||||
|             </Label> | ||||
|             <Input | ||||
|               id="name" | ||||
|               name="name" | ||||
|               placeholder="Lee Robinson" | ||||
|               className="group-data-[invalid=true]/field:border-destructive focus-visible:group-data-[invalid=true]/field:ring-destructive" | ||||
|               disabled={pending} | ||||
|               aria-invalid={!!state.errors?.name} | ||||
|               aria-errormessage="error-name" | ||||
|               defaultValue={state.defaultValues.name} | ||||
|             /> | ||||
|             {state.errors?.name && ( | ||||
|               <p id="error-name" className="text-destructive text-sm"> | ||||
|                 {state.errors.name} | ||||
|               </p> | ||||
|             )} | ||||
|           </div> | ||||
|           <div | ||||
|             className="group/field grid gap-2" | ||||
|             data-invalid={!!state.errors?.email} | ||||
|           > | ||||
|             <Label | ||||
|               htmlFor="email" | ||||
|               className="group-data-[invalid=true]/field:text-destructive" | ||||
|             > | ||||
|               Email <span aria-hidden="true">*</span> | ||||
|             </Label> | ||||
|             <Input | ||||
|               id="email" | ||||
|               name="email" | ||||
|               placeholder="leerob@acme.com" | ||||
|               className="group-data-[invalid=true]/field:border-destructive focus-visible:group-data-[invalid=true]/field:ring-destructive" | ||||
|               disabled={pending} | ||||
|               aria-invalid={!!state.errors?.email} | ||||
|               aria-errormessage="error-email" | ||||
|               defaultValue={state.defaultValues.email} | ||||
|             /> | ||||
|             {state.errors?.email && ( | ||||
|               <p id="error-email" className="text-destructive text-sm"> | ||||
|                 {state.errors.email} | ||||
|               </p> | ||||
|             )} | ||||
|           </div> | ||||
|           <div | ||||
|             className="group/field grid gap-2" | ||||
|             data-invalid={!!state.errors?.message} | ||||
|           > | ||||
|             <Label | ||||
|               htmlFor="message" | ||||
|               className="group-data-[invalid=true]/field:text-destructive" | ||||
|             > | ||||
|               Message <span aria-hidden="true">*</span> | ||||
|             </Label> | ||||
|             <Textarea | ||||
|               id="message" | ||||
|               name="message" | ||||
|               placeholder="Type your message here..." | ||||
|               className="group-data-[invalid=true]/field:border-destructive focus-visible:group-data-[invalid=true]/field:ring-destructive" | ||||
|               disabled={pending} | ||||
|               aria-invalid={!!state.errors?.message} | ||||
|               aria-errormessage="error-message" | ||||
|               defaultValue={state.defaultValues.message} | ||||
|             /> | ||||
|             {state.errors?.message && ( | ||||
|               <p id="error-message" className="text-destructive text-sm"> | ||||
|                 {state.errors.message} | ||||
|               </p> | ||||
|             )} | ||||
|           </div> | ||||
|         </CardContent> | ||||
|         <CardFooter> | ||||
|           <Button type="submit" size="sm" disabled={pending}> | ||||
|             {pending ? "Sending..." : "Send Message"} | ||||
|           </Button> | ||||
|         </CardFooter> | ||||
|       </form> | ||||
|     </Card> | ||||
|   ) | ||||
| } | ||||
| @@ -0,0 +1,3 @@ | ||||
| export function HelloWorld() { | ||||
|   return <h1 className="text-2xl font-bold">Hello World</h1> | ||||
| } | ||||
		Reference in New Issue
	
	Block a user