├── .gitignore ├── README.md ├── app.json ├── app ├── _layout.tsx ├── index.tsx └── pokemon │ └── [id].tsx ├── assets ├── fonts │ └── SpaceMono-Regular.ttf └── images │ ├── adaptive-icon.png │ ├── arrow_back.png │ ├── chevron_left.png │ ├── chevron_right.png │ ├── close.png │ ├── favicon.png │ ├── icon.png │ ├── partial-react-logo.png │ ├── pokeball-big.png │ ├── pokeball.png │ ├── react-logo.png │ ├── react-logo@2x.png │ ├── react-logo@3x.png │ ├── rule.png │ ├── search.png │ ├── splash.png │ ├── tag.png │ ├── text_format.png │ └── weight.png ├── babel.config.js ├── bun.lockb ├── components ├── Card.tsx ├── FadingImage.tsx ├── Placeholder.tsx ├── SearchBar.tsx ├── SortButton.tsx ├── ThemedText.tsx ├── __tests__ │ ├── ThemedText-test.tsx │ └── __snapshots__ │ │ └── ThemedText-test.tsx.snap ├── animation │ └── AppearFromBottom.tsx ├── form │ └── Radio.tsx ├── layout │ ├── ColoredView.tsx │ ├── RootView.tsx │ └── Row.tsx ├── navigation │ └── TabBarIcon.tsx └── pokemon │ ├── PokemonCard.tsx │ ├── PokemonSpec.tsx │ ├── PokemonStat.tsx │ └── TypePill.tsx ├── constants ├── Colors.ts └── Shadow.ts ├── expo-env.d.ts ├── functions ├── pokemon.ts └── style.ts ├── hooks ├── useColorScheme.ts ├── useColorScheme.web.ts ├── useFetchQuery.ts ├── useRefSync.ts ├── useThemeColor.ts └── useThemeColors.ts ├── package.json ├── scripts └── reset-project.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 17 | # The following patterns were generated by expo-cli 18 | 19 | expo-env.d.ts 20 | # @end expo-cli -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native 2 | 3 | L'objectif de ce dépôt est de préparer un tutoriel sur React Native en créant une application "pokedex" en utilisant React Native 4 | 5 | - [Maquette](https://www.figma.com/community/file/979132880663340794) 6 | - [API](https://pokeapi.co/docs/v2) 7 | 8 | ## Découverte 9 | 10 | - View attention au flex ! 11 | - Un text doit être dans un Text 12 | - Reanimate & Moti pour les animations 13 | - Stack de base 14 | 15 | ```tsx 16 | 26 | 27 | 28 | 29 | ``` 30 | 31 | ## TODO 32 | 33 | - Bouton pokemon suivant / pokemon précédent sur la single 34 | - *Slide pour passer au pokemon suivant / précédent 35 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "PokeMobile", 4 | "slug": "PokeMobile", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "splash": { 11 | "image": "./assets/images/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "ios": { 16 | "supportsTablet": true 17 | }, 18 | "android": { 19 | "adaptiveIcon": { 20 | "foregroundImage": "./assets/images/adaptive-icon.png", 21 | "backgroundColor": "#ffffff" 22 | } 23 | }, 24 | "web": { 25 | "bundler": "metro", 26 | "output": "static", 27 | "favicon": "./assets/images/favicon.png" 28 | }, 29 | "plugins": [ 30 | "expo-router" 31 | ], 32 | "experiments": { 33 | "typedRoutes": true 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "expo-router"; 2 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 3 | 4 | const queryClient = new QueryClient(); 5 | 6 | export default function RootLayout() { 7 | return ( 8 | 9 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ActivityIndicator, 3 | FlatList, 4 | Image, 5 | StyleSheet, 6 | View, 7 | } from "react-native"; 8 | import { useInfiniteFetchQuery } from "@/hooks/useFetchQuery"; 9 | import { useThemeColors } from "@/hooks/useThemeColors"; 10 | import { PokemonCard } from "@/components/pokemon/PokemonCard"; 11 | import React, { useState } from "react"; 12 | import { ThemedText } from "@/components/ThemedText"; 13 | import { Card } from "@/components/Card"; 14 | import { RootView } from "@/components/layout/RootView"; 15 | import { getPokemonId } from "@/functions/pokemon"; 16 | import { SearchBar } from "@/components/SearchBar"; 17 | import { Row } from "@/components/layout/Row"; 18 | import { SortButton } from "@/components/SortButton"; 19 | 20 | export default function HomeScreen() { 21 | const [sortKey, setSortKey] = useState<"id" | "name">("id"); 22 | const [search, setSearch] = useState(""); 23 | const { data, fetchNextPage, isFetching } = 24 | useInfiniteFetchQuery("/pokemon?limit=21"); 25 | const colors = useThemeColors(); 26 | if (!data) { 27 | return ( 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | const pokemons = data.pages.flatMap((page) => 35 | page.results.map((r) => ({ ...r, id: getPokemonId(r.url) })), 36 | ); 37 | const filteredPokemons = [ 38 | ...(search 39 | ? pokemons.filter( 40 | (pokemon) => 41 | pokemon.name.toLowerCase().includes(search.toLowerCase()) || 42 | pokemon.id.toString() === search, 43 | ) 44 | : pokemons), 45 | ].sort((a, b) => (a[sortKey] < b[sortKey] ? -1 : 1)); 46 | 47 | const onEnd = () => { 48 | fetchNextPage(); 49 | }; 50 | 51 | return ( 52 | 53 | 54 | 59 | 60 | Pokédex 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | ( 75 | 76 | )} 77 | ListFooterComponent={ 78 | isFetching ? : null 79 | } 80 | keyExtractor={(pokemon) => pokemon.id.toString()} 81 | /> 82 | 83 | 84 | ); 85 | } 86 | 87 | const styles = StyleSheet.create({ 88 | header: { 89 | margin: 12, 90 | marginTop: 0, 91 | marginBottom: 8, 92 | flexDirection: "row", 93 | alignItems: "center", 94 | gap: 12, 95 | }, 96 | gap: { 97 | gap: 8, 98 | }, 99 | wrapper: { 100 | flex: 1, 101 | alignItems: "stretch", 102 | overflow: "hidden", 103 | }, 104 | list: { 105 | backgroundColor: "#FFF", 106 | padding: 12, 107 | }, 108 | searchBar: { 109 | gap: 16, 110 | marginHorizontal: 12, 111 | marginBottom: 24, 112 | }, 113 | }); 114 | -------------------------------------------------------------------------------- /app/pokemon/[id].tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ActivityIndicator, 3 | Image, 4 | Pressable, 5 | StyleSheet, 6 | useWindowDimensions, 7 | View, 8 | } from "react-native"; 9 | import { router, useLocalSearchParams } from "expo-router"; 10 | import { useThemeColors } from "@/hooks/useThemeColors"; 11 | import { useFetchQuery } from "@/hooks/useFetchQuery"; 12 | import { ThemedText } from "@/components/ThemedText"; 13 | import React, { memo, type PropsWithChildren, useMemo, useState } from "react"; 14 | import { 15 | formatHeight, 16 | formatWeight, 17 | getPokemonArtwork, 18 | getPokemonNumber, 19 | } from "@/functions/pokemon"; 20 | import { RootView } from "@/components/layout/RootView"; 21 | import { Card } from "@/components/Card"; 22 | import { TypePill } from "@/components/pokemon/TypePill"; 23 | import { PokemonSpec } from "@/components/pokemon/PokemonSpec"; 24 | import { Row } from "@/components/layout/Row"; 25 | import { PokemonStat } from "@/components/pokemon/PokemonStat"; 26 | import { AnimatePresence } from "moti"; 27 | import { AppearFromBottom } from "@/components/animation/AppearFromBottom"; 28 | import { Audio } from "expo-av"; 29 | import { FadingImage } from "@/components/FadingImage"; 30 | import { type Route, TabView } from "react-native-tab-view"; 31 | 32 | type PokemonRoute = Route & { 33 | id: number; 34 | onNext: () => void; 35 | onPrevious: () => void; 36 | }; 37 | 38 | const renderScene = ({ route }: { route: PokemonRoute }) => { 39 | return ( 40 | 45 | ); 46 | }; 47 | 48 | const lastPokemon = 151; 49 | const firstPokemon = 1; 50 | 51 | export default function PokemonScreen() { 52 | const [id, setId] = useState( 53 | parseInt((useLocalSearchParams() as { id: string }).id, 10), 54 | ); 55 | 56 | const routeFor = (id: number) => { 57 | return { 58 | key: id.toString(), 59 | id: id, 60 | title: id.toString(), 61 | onNext: () => setIndex((i) => i + 1), 62 | onPrevious: () => setIndex((i) => i - 1), 63 | } satisfies PokemonRoute; 64 | }; 65 | const layout = useWindowDimensions(); 66 | const [index, setIndex] = useState(1); 67 | const routes = useMemo( 68 | () => [routeFor(id - 1), routeFor(id), routeFor(id + 1)], 69 | [id], 70 | ); 71 | const onAnimationEnd = () => { 72 | if ( 73 | index === 1 || 74 | (index === 0 && id === firstPokemon + 1) || 75 | (index === 2 && id === lastPokemon - 1) 76 | ) { 77 | return; 78 | } 79 | setId(id + (index - 1)); 80 | }; 81 | return ( 82 | 83 | renderTabBar={() => null} 84 | onIndexChange={setIndex} 85 | navigationState={{ index, routes }} 86 | renderScene={renderScene} 87 | initialLayout={{ width: layout.width }} 88 | onSwipeEnd={onAnimationEnd} 89 | /> 90 | ); 91 | } 92 | 93 | type Props = { id: number; onPrevious: () => void; onNext: () => void }; 94 | 95 | const PokemonView = memo(function ({ id, onPrevious, onNext }: Props) { 96 | const colors = useThemeColors(); 97 | 98 | if (Array.isArray(id)) { 99 | return Error; 100 | } 101 | 102 | const { data: pokemon } = useFetchQuery("/pokemon/:id", { id: id }); 103 | const { data: species } = useFetchQuery("/pokemon-species/:id", { id: id }); 104 | const mainType = pokemon?.types?.[0]["type"]["name"]; 105 | const colorType = mainType ? (colors.type as any)[mainType] : colors.tint; 106 | const statsName = [ 107 | "hp", 108 | "attack", 109 | "defense", 110 | "special-attack", 111 | "special-defense", 112 | "speed", 113 | ]; 114 | const bio = species?.flavor_text_entries 115 | ?.find(({ language }) => language.name === "en") 116 | ?.flavor_text.replaceAll("\n", " "); 117 | const specs = 118 | pokemon?.weight && pokemon?.moves && pokemon?.height 119 | ? [ 120 | { 121 | name: "Weight", 122 | image: require("@/assets/images/weight.png"), 123 | spec: formatWeight(pokemon.weight), 124 | }, 125 | { 126 | name: "Height", 127 | image: require("@/assets/images/rule.png"), 128 | spec: formatHeight(pokemon.height), 129 | }, 130 | { 131 | name: "Moves", 132 | spec: pokemon?.moves 133 | .slice(0, 2) 134 | .map((m) => m.move.name) 135 | .join("\n"), 136 | }, 137 | ] 138 | : []; 139 | 140 | const onCry = async () => { 141 | const cry = pokemon?.cries.latest; 142 | if (!cry) { 143 | return; 144 | } 145 | const { sound } = await Audio.Sound.createAsync( 146 | { 147 | uri: pokemon?.cries.latest, 148 | }, 149 | { shouldPlay: true }, 150 | ); 151 | sound.playAsync(); 152 | }; 153 | 154 | return ( 155 | 156 | {/* Header */} 157 | 158 | 159 | 164 | 165 | 170 | {pokemon?.name} 171 | 172 | {pokemon?.id && ( 173 | 178 | {getPokemonNumber(pokemon?.id)} 179 | 180 | )} 181 | 182 | 183 | 189 | 190 | {/* Pokemon image and nav */} 191 | 192 | {id > firstPokemon ? ( 193 | 194 | 199 | 200 | ) : ( 201 | 202 | )} 203 | 204 | 209 | 210 | {id < lastPokemon ? ( 211 | 212 | 217 | 218 | ) : ( 219 | 220 | )} 221 | 222 | 223 | 224 | {/* Types */} 225 | 226 | {pokemon?.types.map((type) => ( 227 | 228 | ))} 229 | 230 | 231 | {/* Specs */} 232 | About 233 | 234 | {specs.length === 0 ? ( 235 | 240 | ) : ( 241 | 242 | {specs.map((spec, k) => ( 243 | 0 246 | ? { 247 | borderLeftColor: colors.gray.light, 248 | borderLeftWidth: 1, 249 | } 250 | : undefined 251 | } 252 | key={spec.name} 253 | index={k} 254 | > 255 | 256 | 257 | ))} 258 | 259 | )} 260 | 261 | 262 | {bio} 263 | 264 | Base stats 265 | 266 | {pokemon 267 | ? pokemon?.stats.map((stat) => ( 268 | 274 | )) 275 | : statsName.map((name) => ( 276 | 282 | ))} 283 | 284 | 285 | 286 | 287 | 288 | ); 289 | }); 290 | 291 | function TitleSection({ 292 | color, 293 | children, 294 | }: PropsWithChildren<{ color: string }>) { 295 | return ( 296 | 300 | {children} 301 | 302 | ); 303 | } 304 | 305 | const styles = StyleSheet.create({ 306 | container: { 307 | flex: 1, 308 | justifyContent: "flex-start", 309 | alignItems: "stretch", 310 | }, 311 | header: { 312 | flex: 0, 313 | padding: 20, 314 | flexDirection: "row", 315 | alignItems: "center", 316 | justifyContent: "flex-start", 317 | gap: 8, 318 | }, 319 | headerActions: { 320 | flex: 1, 321 | flexDirection: "row", 322 | alignItems: "center", 323 | justifyContent: "flex-start", 324 | gap: 8, 325 | }, 326 | body: { 327 | flex: 0, 328 | justifyContent: "flex-start", 329 | alignItems: "stretch", 330 | }, 331 | pokeball: { 332 | position: "absolute", 333 | top: -80, 334 | right: 8, 335 | }, 336 | card: { 337 | marginTop: 140, 338 | paddingTop: 60, 339 | padding: 20, 340 | zIndex: 2, 341 | alignItems: "stretch", 342 | }, 343 | imageRow: { 344 | position: "absolute", 345 | top: -140, 346 | left: 0, 347 | right: 0, 348 | justifyContent: "space-between", 349 | paddingHorizontal: 20, 350 | }, 351 | image: { 352 | justifyContent: "center", 353 | alignSelf: "center", 354 | }, 355 | pills: { 356 | flex: 0, 357 | height: 20, 358 | flexDirection: "row", 359 | justifyContent: "center", 360 | gap: 16, 361 | }, 362 | specs: { 363 | alignSelf: "stretch", 364 | height: 48, 365 | }, 366 | stack: { 367 | flex: 0, 368 | gap: 16, 369 | }, 370 | bio: { 371 | flex: 0, 372 | height: 60, 373 | justifyContent: "center", 374 | }, 375 | stats: { 376 | alignSelf: "stretch", 377 | }, 378 | }); 379 | -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/images/arrow_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/arrow_back.png -------------------------------------------------------------------------------- /assets/images/chevron_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/chevron_left.png -------------------------------------------------------------------------------- /assets/images/chevron_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/chevron_right.png -------------------------------------------------------------------------------- /assets/images/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/close.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/favicon.png -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/partial-react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/partial-react-logo.png -------------------------------------------------------------------------------- /assets/images/pokeball-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/pokeball-big.png -------------------------------------------------------------------------------- /assets/images/pokeball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/pokeball.png -------------------------------------------------------------------------------- /assets/images/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/react-logo.png -------------------------------------------------------------------------------- /assets/images/react-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/react-logo@2x.png -------------------------------------------------------------------------------- /assets/images/react-logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/react-logo@3x.png -------------------------------------------------------------------------------- /assets/images/rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/rule.png -------------------------------------------------------------------------------- /assets/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/search.png -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/splash.png -------------------------------------------------------------------------------- /assets/images/tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/tag.png -------------------------------------------------------------------------------- /assets/images/text_format.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/text_format.png -------------------------------------------------------------------------------- /assets/images/weight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/assets/images/weight.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"], 5 | plugins: ["react-native-reanimated/plugin"], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grafikart/PokeNative/ddcfc1dfa1ab0bb1463508666dbd79a2752d953c/bun.lockb -------------------------------------------------------------------------------- /components/Card.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, View, type ViewProps } from "react-native"; 2 | import { useThemeColors } from "@/hooks/useThemeColors"; 3 | import { Shadows } from "@/constants/Shadow"; 4 | 5 | type Props = ViewProps & { 6 | elevation?: number; 7 | }; 8 | 9 | export function Card({ elevation = 2, style, ...rest }: Props) { 10 | const colors = useThemeColors(); 11 | return ( 12 | 16 | ); 17 | } 18 | 19 | const styles = StyleSheet.create({ 20 | card: { 21 | alignItems: "center", 22 | borderRadius: 8, 23 | ...Shadows.dp2, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /components/FadingImage.tsx: -------------------------------------------------------------------------------- 1 | import { type ImageProps, Image } from "react-native"; 2 | import Animated, { useSharedValue, withTiming } from "react-native-reanimated"; 3 | 4 | type Props = ImageProps & { 5 | url: string; 6 | }; 7 | 8 | export function FadingImage({ style, url, ...rest }: Props) { 9 | const opacity = useSharedValue(0); 10 | const onLoad = () => { 11 | opacity.value = withTiming(1, { duration: 300 }); 12 | }; 13 | 14 | return ( 15 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /components/Placeholder.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, View, type ViewProps } from "react-native"; 2 | 3 | type Props = ViewProps & {}; 4 | 5 | export function TypePill({ style, ...rest }: Props) { 6 | return ; 7 | } 8 | 9 | const styles = StyleSheet.create({}); 10 | -------------------------------------------------------------------------------- /components/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import { Image, Pressable, StyleSheet, TextInput } from "react-native"; 2 | import { useThemeColors } from "@/hooks/useThemeColors"; 3 | import { Row } from "@/components/layout/Row"; 4 | 5 | type Props = { 6 | search: string; 7 | onChange: (s: string) => void; 8 | }; 9 | 10 | export function SearchBar({ search, onChange }: Props) { 11 | const hasSearch = search !== ""; 12 | const emptySearch = () => { 13 | onChange(""); 14 | }; 15 | const colors = useThemeColors(); 16 | return ( 17 | 18 | 24 | 30 | {hasSearch && ( 31 | 32 | 38 | 39 | )} 40 | 41 | ); 42 | } 43 | 44 | const styles = StyleSheet.create({ 45 | wrapper: { 46 | flex: 1, 47 | borderRadius: 16, 48 | height: 32, 49 | gap: 8, 50 | paddingHorizontal: 12, 51 | }, 52 | input: { 53 | flex: 1, 54 | height: 16, 55 | fontSize: 10, 56 | lineHeight: 16, 57 | }, 58 | icon: { 59 | flex: 0, 60 | }, 61 | }); 62 | -------------------------------------------------------------------------------- /components/SortButton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dimensions, 3 | Image, 4 | Modal, 5 | Pressable, 6 | StyleSheet, 7 | View, 8 | } from "react-native"; 9 | import { useThemeColors } from "@/hooks/useThemeColors"; 10 | import { ThemedText } from "@/components/ThemedText"; 11 | import { Card } from "@/components/Card"; 12 | import { Radio } from "@/components/form/Radio"; 13 | import { Row } from "@/components/layout/Row"; 14 | import { useRef, useState } from "react"; 15 | 16 | type Props = { 17 | sortKey: "id" | "name"; 18 | onChange: (v: "id" | "name") => void; 19 | }; 20 | const options = [ 21 | { 22 | label: "Number", 23 | value: "id", 24 | }, 25 | { 26 | label: "Name", 27 | value: "name", 28 | }, 29 | ] as const; 30 | 31 | export function SortButton({ sortKey, onChange }: Props) { 32 | const buttonRef = useRef(null); 33 | const colors = useThemeColors(); 34 | const [showPopup, setShowPopup] = useState(false); 35 | const [position, setPosition] = useState(null); 39 | const onOpen = () => { 40 | buttonRef.current?.measureInWindow((x, y, width, height) => { 41 | setPosition({ 42 | top: y + height, 43 | right: Dimensions.get("window").width - x - width, 44 | }); 45 | }); 46 | setShowPopup(true); 47 | }; 48 | const onClose = () => { 49 | setShowPopup(false); 50 | }; 51 | const isPopupVisible = Boolean(showPopup && position); 52 | 53 | return ( 54 | <> 55 | 56 | 60 | 69 | 70 | 71 | 72 | 78 | 79 | 82 | 83 | Sort by: 84 | 85 | 86 | {options.map((option) => ( 87 | onChange(option.value)} 89 | key={option.label} 90 | > 91 | 92 | 93 | {option.label} 94 | 95 | 96 | ))} 97 | 98 | 99 | 100 | 101 | ); 102 | } 103 | 104 | const styles = StyleSheet.create({ 105 | button: { 106 | position: "relative", 107 | alignItems: "center", 108 | justifyContent: "center", 109 | width: 32, 110 | height: 32, 111 | borderRadius: 32, 112 | flex: 0, 113 | zIndex: 6, 114 | }, 115 | backdrop: { 116 | flex: 1, 117 | backgroundColor: "rgba(0, 0, 0, 0.3)", 118 | }, 119 | popup: { 120 | position: "absolute", 121 | elevation: 2, 122 | gap: 16, 123 | padding: 4, 124 | paddingTop: 16, 125 | top: 0, 126 | right: 0, 127 | width: 113, 128 | borderRadius: 12, 129 | }, 130 | title: { 131 | marginLeft: 20, 132 | }, 133 | card: { 134 | paddingVertical: 16, 135 | paddingLeft: 20, 136 | gap: 16, 137 | alignItems: "flex-start", 138 | }, 139 | }); 140 | -------------------------------------------------------------------------------- /components/ThemedText.tsx: -------------------------------------------------------------------------------- 1 | import { Text, type TextProps, StyleSheet } from "react-native"; 2 | 3 | import { useThemeColors } from "@/hooks/useThemeColors"; 4 | import type { Colors } from "@/constants/Colors"; 5 | 6 | export type ThemedTextProps = TextProps & { 7 | color?: keyof (typeof Colors)["light"]["gray"]; 8 | variant?: 9 | | "body3" 10 | | "caption" 11 | | "headline" 12 | | "subtitle1" 13 | | "subtitle2" 14 | | "subtitle3"; 15 | }; 16 | 17 | export function ThemedText({ 18 | color = "dark", 19 | variant = "body3", 20 | style, 21 | ...rest 22 | }: ThemedTextProps) { 23 | const colors = useThemeColors(); 24 | 25 | return ( 26 | 30 | ); 31 | } 32 | 33 | const styles = StyleSheet.create({ 34 | body3: { 35 | fontSize: 10, 36 | lineHeight: 16, 37 | }, 38 | headline: { 39 | fontSize: 24, 40 | lineHeight: 32, 41 | fontWeight: "bold", 42 | }, 43 | caption: { 44 | fontSize: 8, 45 | lineHeight: 12, 46 | }, 47 | subtitle1: { 48 | fontSize: 14, 49 | lineHeight: 16, 50 | fontWeight: "bold", 51 | }, 52 | subtitle2: { 53 | fontSize: 12, 54 | lineHeight: 16, 55 | fontWeight: "bold", 56 | }, 57 | subtitle3: { 58 | fontSize: 10, 59 | lineHeight: 16, 60 | fontWeight: "bold", 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /components/__tests__/ThemedText-test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import { ThemedText } from '../ThemedText'; 5 | 6 | it(`renders correctly`, () => { 7 | const tree = renderer.create(Snapshot test!).toJSON(); 8 | 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /components/__tests__/__snapshots__/ThemedText-test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | 22 | Snapshot test! 23 | 24 | `; 25 | -------------------------------------------------------------------------------- /components/animation/AppearFromBottom.tsx: -------------------------------------------------------------------------------- 1 | import { MotiView } from "moti"; 2 | import type { PropsWithChildren } from "react"; 3 | import type { ViewProps } from "react-native"; 4 | 5 | const from = { 6 | opacity: 0, 7 | translateY: 50, 8 | }; 9 | 10 | const to = { 11 | opacity: 1, 12 | translateY: 0, 13 | }; 14 | 15 | export function AppearFromBottom({ 16 | index, 17 | style, 18 | ...props 19 | }: PropsWithChildren) { 20 | return ( 21 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /components/form/Radio.tsx: -------------------------------------------------------------------------------- 1 | import { Pressable, StyleSheet, View, type ViewProps } from "react-native"; 2 | import { useThemeColors } from "@/hooks/useThemeColors"; 3 | 4 | type Props = { 5 | checked: boolean; 6 | onChange?: (v: boolean) => void; 7 | }; 8 | 9 | export function Radio({ checked, onChange }: Props) { 10 | const colors = useThemeColors(); 11 | return ( 12 | onChange(!checked) : undefined}> 13 | 14 | {checked && ( 15 | 16 | )} 17 | 18 | 19 | ); 20 | } 21 | 22 | const styles = StyleSheet.create({ 23 | radio: { 24 | width: 14, 25 | height: 14, 26 | borderStyle: "solid", 27 | borderWidth: 1, 28 | borderRadius: 14, 29 | alignItems: "center", 30 | justifyContent: "center", 31 | }, 32 | radioInner: { 33 | borderRadius: 6, 34 | width: 6, 35 | height: 6, 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /components/layout/ColoredView.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | type PropsWithChildren, 4 | useCallback, 5 | useContext, 6 | useState, 7 | } from "react"; 8 | import { useThemeColors } from "@/hooks/useThemeColors"; 9 | import { useFocusEffect } from "expo-router"; 10 | import { MotiView } from "moti"; 11 | 12 | const ColorContext = createContext({ 13 | setBackground: (color: string) => {}, 14 | }); 15 | 16 | export function ColoredView(props: PropsWithChildren) { 17 | const colors = useThemeColors(); 18 | const [background, setBackground] = useState(colors.tint); 19 | 20 | return ( 21 | 22 | 27 | 28 | ); 29 | } 30 | 31 | export function useBackgroundColor(newColor: string | undefined) { 32 | const { setBackground } = useContext(ColorContext); 33 | useFocusEffect( 34 | useCallback(() => { 35 | if (newColor) { 36 | setBackground(newColor); 37 | } 38 | }, [newColor, setBackground]), 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /components/layout/RootView.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from "react"; 2 | import { SafeAreaView } from "react-native-safe-area-context"; 3 | import { StyleSheet, type ViewProps } from "react-native"; 4 | import { MotiView } from "moti"; 5 | import { useThemeColors } from "@/hooks/useThemeColors"; 6 | 7 | export function RootView({ 8 | children, 9 | style, 10 | color, 11 | ...rest 12 | }: PropsWithChildren) { 13 | const colors = useThemeColors(); 14 | return ( 15 | 19 | 20 | {children} 21 | 22 | 23 | ); 24 | } 25 | 26 | const styles = StyleSheet.create({ 27 | container: { 28 | flex: 1, 29 | padding: 4, 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /components/layout/Row.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, View, type ViewProps } from "react-native"; 2 | import { Children } from "react"; 3 | import { Colors } from "@/constants/Colors"; 4 | 5 | type Props = ViewProps & { 6 | separator?: boolean; 7 | gap?: number; 8 | }; 9 | 10 | export function Row({ style, separator, gap, ...rest }: Props) { 11 | return ( 12 | 16 | ); 17 | } 18 | 19 | const styles = StyleSheet.create({ 20 | row: { 21 | flex: 0, 22 | flexDirection: "row", 23 | alignItems: "center", 24 | justifyContent: "center", 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /components/navigation/TabBarIcon.tsx: -------------------------------------------------------------------------------- 1 | // You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ 2 | 3 | import Ionicons from '@expo/vector-icons/Ionicons'; 4 | import { type IconProps } from '@expo/vector-icons/build/createIconSet'; 5 | import { type ComponentProps } from 'react'; 6 | 7 | export function TabBarIcon({ style, ...rest }: IconProps['name']>) { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /components/pokemon/PokemonCard.tsx: -------------------------------------------------------------------------------- 1 | import type { APIResult } from "@/hooks/useFetchQuery"; 2 | import { View, Text, StyleSheet, Image, Pressable } from "react-native"; 3 | import React from "react"; 4 | import { getPokemonArtwork, getPokemonId } from "@/functions/pokemon"; 5 | import { ThemedText } from "@/components/ThemedText"; 6 | import { Card } from "@/components/Card"; 7 | import { Link } from "expo-router"; 8 | import { Colors } from "@/constants/Colors"; 9 | 10 | type Props = { 11 | id: string; 12 | name: string; 13 | }; 14 | 15 | export function PokemonCard({ id, name }: Props) { 16 | return ( 17 | 18 | 19 | 20 | 21 | #{id.toString().padStart(3, "0")} 22 | 23 | 28 | {name} 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | const styles = StyleSheet.create({ 37 | card: { 38 | position: "relative", 39 | paddingVertical: 4, 40 | paddingHorizontal: 8, 41 | }, 42 | id: { 43 | alignSelf: "flex-end", 44 | }, 45 | shadow: { 46 | flex: 1, 47 | position: "absolute", 48 | bottom: 0, 49 | left: 0, 50 | right: 0, 51 | borderRadius: 7, 52 | height: 44, 53 | zIndex: -1, 54 | backgroundColor: Colors.light.gray.background, 55 | }, 56 | }); 57 | -------------------------------------------------------------------------------- /components/pokemon/PokemonSpec.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Image, 3 | type ImageSourcePropType, 4 | StyleSheet, 5 | View, 6 | type ViewProps, 7 | } from "react-native"; 8 | import React from "react"; 9 | import { ThemedText } from "@/components/ThemedText"; 10 | 11 | type Props = ViewProps & { 12 | spec?: string; 13 | name: string; 14 | image?: ImageSourcePropType; 15 | }; 16 | 17 | export function PokemonSpec({ 18 | style, 19 | spec, 20 | name, 21 | image, 22 | children, 23 | ...rest 24 | }: Props) { 25 | return ( 26 | 27 | {children ?? ( 28 | 29 | {image && } 30 | {spec} 31 | 32 | )} 33 | 38 | {name} 39 | 40 | 41 | ); 42 | } 43 | const styles = StyleSheet.create({ 44 | container: { 45 | flex: 1, 46 | gap: 4, 47 | }, 48 | row: { 49 | height: 32, 50 | flexDirection: "row", 51 | alignItems: "center", 52 | justifyContent: "center", 53 | gap: 8, 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /components/pokemon/PokemonStat.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, View } from "react-native"; 2 | import { Row } from "@/components/layout/Row"; 3 | import { ThemedText } from "@/components/ThemedText"; 4 | import { MotiView } from "moti"; 5 | 6 | type Props = { 7 | name: string; 8 | value: number; 9 | color: string; 10 | }; 11 | 12 | export function PokemonStat({ name, value, color }: Props) { 13 | return ( 14 | 15 | 16 | 17 | {statShortName(name)} 18 | 19 | 20 | 21 | 22 | {value.toString().padStart(3, "0")} 23 | 24 | 25 | 35 | 43 | 44 | 45 | 46 | ); 47 | } 48 | 49 | function statShortName(name: string): string { 50 | return name 51 | .replaceAll("special", "S") 52 | .replaceAll("-", "") 53 | .replaceAll("attack", "ATK") 54 | .replaceAll("defense", "DEF") 55 | .replaceAll("speed", "SPD") 56 | .toUpperCase(); 57 | } 58 | 59 | const styles = StyleSheet.create({ 60 | row: { 61 | gap: 8, 62 | }, 63 | name: { 64 | flex: 0, 65 | width: 32, 66 | }, 67 | stat: { 68 | flex: 1, 69 | marginLeft: 4, 70 | gap: 8, 71 | }, 72 | bar: { 73 | flexDirection: "row", 74 | height: 4, 75 | flex: 1, 76 | borderRadius: 20, 77 | overflow: "hidden", 78 | }, 79 | barInner: { 80 | flex: 1, 81 | top: 0, 82 | left: 0, 83 | height: "100%", 84 | }, 85 | barBackground: { 86 | flex: 1, 87 | width: "100%", 88 | opacity: 0.24, 89 | }, 90 | }); 91 | -------------------------------------------------------------------------------- /components/pokemon/TypePill.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, View, type ViewProps } from "react-native"; 2 | import { useThemeColors } from "@/hooks/useThemeColors"; 3 | import { ThemedText } from "@/components/ThemedText"; 4 | 5 | type Props = ViewProps & { 6 | name: string; 7 | }; 8 | 9 | export function TypePill({ name, style, ...rest }: Props) { 10 | const colors = useThemeColors(); 11 | return ( 12 | 16 | 21 | {name} 22 | 23 | 24 | ); 25 | } 26 | 27 | const styles = StyleSheet.create({ 28 | pill: { 29 | flex: 0, 30 | height: 20, 31 | justifyContent: "center", 32 | alignItems: "center", 33 | paddingHorizontal: 8, 34 | borderRadius: 8, 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /constants/Colors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Below are the colors that are used in the app. The colors are defined in the light and dark mode. 3 | * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. 4 | */ 5 | 6 | const tintColorLight = "#DC0A2D"; 7 | const tintColorDark = "#fff"; 8 | 9 | export const Colors = { 10 | light: { 11 | tint: tintColorLight, 12 | gray: { 13 | dark: "#212121", 14 | medium: "#666666", 15 | light: "#E0E0E0", 16 | background: "#EFEFEF", 17 | white: "#FFFFFF", 18 | }, 19 | type: { 20 | bug: "#A7B723", 21 | dark: "#75574C", 22 | dragon: "#7037FF", 23 | electric: "#F9CF30", 24 | fairy: "#E69EAC", 25 | fighting: "#C12239", 26 | fire: "#F57D31", 27 | flying: "#A891EC", 28 | ghost: "#70559B", 29 | normal: "#AAA67F", 30 | grass: "#74CB48", 31 | ground: "#DEC16B", 32 | ice: "#9AD6DF", 33 | poison: "#A43E9E", 34 | psychic: "#FB5584", 35 | rock: "#B69E31", 36 | steel: "#B7B9D0", 37 | water: "#6493EB", 38 | } as Record, 39 | }, 40 | dark: {}, 41 | }; 42 | -------------------------------------------------------------------------------- /constants/Shadow.ts: -------------------------------------------------------------------------------- 1 | import type { ViewStyle } from "react-native"; 2 | 3 | export const Shadows = { 4 | dp2: { 5 | shadowOpacity: 0.2, 6 | shadowColor: "#000", 7 | shadowOffset: { width: 0, height: 1 }, 8 | elevation: 2, 9 | shadowRadius: 3, 10 | }, 11 | } satisfies Record; 12 | -------------------------------------------------------------------------------- /expo-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // NOTE: This file should not be edited and should be in your git ignore -------------------------------------------------------------------------------- /functions/pokemon.ts: -------------------------------------------------------------------------------- 1 | export function getPokemonArtwork(url: string | number) { 2 | return `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${getPokemonId(url)}.png`; 3 | } 4 | 5 | export function getPokemonId(url: string | number): number { 6 | if (typeof url === "number") { 7 | return url; 8 | } 9 | if (!url.includes("/")) { 10 | return parseInt(url, 10); 11 | } 12 | return parseInt(url.split("/").at(-2)!, 10); 13 | } 14 | 15 | export function getPokemonNumber(id: string | number) { 16 | return `#${id.toString().padStart(3, "0")}`; 17 | } 18 | 19 | export function formatWeight(weight?: number): string { 20 | if (weight === undefined) { 21 | return ""; 22 | } 23 | return (weight / 10).toString().replace(".", ",") + " kg"; 24 | } 25 | 26 | export function formatHeight(weight?: number): string { 27 | if (weight === undefined) { 28 | return ""; 29 | } 30 | return (weight / 10).toString().replace(".", ",") + " m"; 31 | } 32 | -------------------------------------------------------------------------------- /functions/style.ts: -------------------------------------------------------------------------------- 1 | import type { DimensionValue } from "react-native"; 2 | 3 | export function ratioToPercent(value: number, max: number): DimensionValue { 4 | return `${(value / max) * 100}%`; 5 | } 6 | -------------------------------------------------------------------------------- /hooks/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | export { useColorScheme } from 'react-native'; 2 | -------------------------------------------------------------------------------- /hooks/useColorScheme.web.ts: -------------------------------------------------------------------------------- 1 | // NOTE: The default React Native styling doesn't support server rendering. 2 | // Server rendered styles should not change between the first render of the HTML 3 | // and the first render on the client. Typically, web developers will use CSS media queries 4 | // to render different styles on the client and server, these aren't directly supported in React Native 5 | // but can be achieved using a styling library like Nativewind. 6 | export function useColorScheme() { 7 | return 'light'; 8 | } 9 | -------------------------------------------------------------------------------- /hooks/useFetchQuery.ts: -------------------------------------------------------------------------------- 1 | import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; 2 | 3 | type PaginatedResults = { 4 | count: number; 5 | next: string | null; 6 | previous: string | null; 7 | results: T[]; 8 | }; 9 | 10 | type API = { 11 | "/pokemon?limit=21": PaginatedResults<{ 12 | name: string; 13 | url: string; 14 | }>; 15 | "/pokemon-species/:id": { 16 | flavor_text_entries: { 17 | flavor_text: string; 18 | language: { 19 | name: string; 20 | }; 21 | }[]; 22 | }; 23 | "/pokemon/:id": { 24 | id: number; 25 | name: string; 26 | url: string; 27 | weight: number; 28 | height: number; 29 | moves: { move: { name: string } }[]; 30 | stats: { 31 | base_stat: number; 32 | stat: { 33 | name: string; 34 | }; 35 | }[]; 36 | cries: { 37 | latest: string; 38 | }; 39 | types: { 40 | type: { 41 | name: string; 42 | }; 43 | }[]; 44 | }; 45 | }; 46 | 47 | type ItemOf = T extends (infer I)[] ? I : never; 48 | 49 | export type APIResult = API[T] extends { results: infer R } 50 | ? ItemOf 51 | : never; 52 | 53 | export function useFetchQuery( 54 | url: T, 55 | params?: Record, 56 | ) { 57 | const localUrl = Object.entries(params ?? {}).reduce( 58 | (acc, [key, value]) => acc.replace(":" + key, value.toString()), 59 | url as string, 60 | ); 61 | return useQuery({ 62 | queryKey: [localUrl], 63 | queryFn: async () => { 64 | // await wait(1000); 65 | return fetch("https://pokeapi.co/api/v2" + localUrl, { 66 | headers: { 67 | Accept: "application/json", 68 | }, 69 | }).then((r) => r.json()) as Promise; 70 | }, 71 | }); 72 | } 73 | 74 | export function useInfiniteFetchQuery( 75 | url: T, 76 | params?: Record, 77 | ) { 78 | const localUrl = Object.entries(params ?? {}).reduce( 79 | (acc, [key, value]) => acc.replace(":" + key, value.toString()), 80 | url as string, 81 | ); 82 | return useInfiniteQuery({ 83 | queryKey: [localUrl], 84 | initialPageParam: "https://pokeapi.co/api/v2" + localUrl, 85 | queryFn: async ({ pageParam }) => { 86 | await wait(1000); 87 | return fetch(pageParam, { 88 | headers: { 89 | Accept: "application/json", 90 | }, 91 | }).then((r) => r.json()) as Promise; 92 | }, 93 | getNextPageParam: (lastPage) => { 94 | if (!("next" in lastPage)) { 95 | throw new Error("Unpaginated result"); 96 | } 97 | return lastPage.next; 98 | }, 99 | }); 100 | } 101 | const wait = (duration: number) => { 102 | return new Promise((resolve) => setTimeout(resolve, duration)); 103 | }; 104 | -------------------------------------------------------------------------------- /hooks/useRefSync.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | 3 | export function useRefSync(value: T) { 4 | const ref = useRef(value); 5 | ref.current = value; 6 | return ref; 7 | } 8 | -------------------------------------------------------------------------------- /hooks/useThemeColor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about light and dark modes: 3 | * https://docs.expo.dev/guides/color-schemes/ 4 | */ 5 | 6 | import { useColorScheme } from "react-native"; 7 | 8 | import { Colors } from "@/constants/Colors"; 9 | 10 | export function useThemeColor( 11 | props: { light?: string; dark?: string }, 12 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark, 13 | ) { 14 | //const theme = useColorScheme() ?? 'light'; 15 | const theme = "light"; 16 | const colorFromProps = props[theme]; 17 | 18 | if (colorFromProps) { 19 | return colorFromProps; 20 | } else { 21 | return Colors[theme][colorName]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /hooks/useThemeColors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about light and dark modes: 3 | * https://docs.expo.dev/guides/color-schemes/ 4 | */ 5 | 6 | import { useColorScheme } from "react-native"; 7 | 8 | import { Colors } from "@/constants/Colors"; 9 | 10 | export function useThemeColors() { 11 | //const theme = useColorScheme() ?? 'light'; 12 | const theme = "light"; 13 | return Colors[theme]; 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pokemobile", 3 | "main": "expo-router/entry", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "expo start", 7 | "check": "tsc --noemit", 8 | "reset-project": "node ./scripts/reset-project.js", 9 | "android": "expo start --android", 10 | "ios": "expo start --ios", 11 | "web": "expo start --web", 12 | "test": "jest --watchAll", 13 | "lint": "expo lint" 14 | }, 15 | "jest": { 16 | "preset": "jest-expo" 17 | }, 18 | "dependencies": { 19 | "@expo/vector-icons": "^14.0.2", 20 | "@react-navigation/native": "^6.0.2", 21 | "@tanstack/react-query": "^5.51.21", 22 | "expo": "~51.0.24", 23 | "expo-av": "~14.0.6", 24 | "expo-constants": "~16.0.2", 25 | "expo-font": "~12.0.9", 26 | "expo-linking": "~6.3.1", 27 | "expo-router": "~3.5.20", 28 | "expo-splash-screen": "~0.27.5", 29 | "expo-status-bar": "~1.12.1", 30 | "expo-system-ui": "~3.0.7", 31 | "expo-web-browser": "~13.0.3", 32 | "moti": "^0.29.0", 33 | "react": "18.2.0", 34 | "react-dom": "18.2.0", 35 | "react-native": "0.74.3", 36 | "react-native-gesture-handler": "~2.16.1", 37 | "react-native-reanimated": "~3.10.1", 38 | "react-native-safe-area-context": "4.10.5", 39 | "react-native-screens": "3.31.1", 40 | "react-native-tab-view": "^3.5.2", 41 | "react-native-web": "~0.19.10" 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.20.0", 45 | "@types/jest": "^29.5.12", 46 | "@types/react": "~18.2.45", 47 | "@types/react-test-renderer": "^18.0.7", 48 | "jest": "^29.2.1", 49 | "jest-expo": "~51.0.3", 50 | "prettier": "^3.3.3", 51 | "react-test-renderer": "18.2.0", 52 | "typescript": "~5.3.3" 53 | }, 54 | "private": true 55 | } 56 | -------------------------------------------------------------------------------- /scripts/reset-project.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script is used to reset the project to a blank state. 5 | * It moves the /app directory to /app-example and creates a new /app directory with an index.tsx and _layout.tsx file. 6 | * You can remove the `reset-project` script from package.json and safely delete this file after running it. 7 | */ 8 | 9 | const fs = require('fs'); 10 | const path = require('path'); 11 | 12 | const root = process.cwd(); 13 | const oldDirPath = path.join(root, 'app'); 14 | const newDirPath = path.join(root, 'app-example'); 15 | const newAppDirPath = path.join(root, 'app'); 16 | 17 | const indexContent = `import { Text, View } from "react-native"; 18 | 19 | export default function Index() { 20 | return ( 21 | 28 | Edit app/index.tsx to edit this screen. 29 | 30 | ); 31 | } 32 | `; 33 | 34 | const layoutContent = `import { Stack } from "expo-router"; 35 | 36 | export default function RootLayout() { 37 | return ( 38 | 39 | 40 | 41 | ); 42 | } 43 | `; 44 | 45 | fs.rename(oldDirPath, newDirPath, (error) => { 46 | if (error) { 47 | return console.error(`Error renaming directory: ${error}`); 48 | } 49 | console.log('/app moved to /app-example.'); 50 | 51 | fs.mkdir(newAppDirPath, { recursive: true }, (error) => { 52 | if (error) { 53 | return console.error(`Error creating new app directory: ${error}`); 54 | } 55 | console.log('New /app directory created.'); 56 | 57 | const indexPath = path.join(newAppDirPath, 'index.tsx'); 58 | fs.writeFile(indexPath, indexContent, (error) => { 59 | if (error) { 60 | return console.error(`Error creating index.tsx: ${error}`); 61 | } 62 | console.log('app/index.tsx created.'); 63 | 64 | const layoutPath = path.join(newAppDirPath, '_layout.tsx'); 65 | fs.writeFile(layoutPath, layoutContent, (error) => { 66 | if (error) { 67 | return console.error(`Error creating _layout.tsx: ${error}`); 68 | } 69 | console.log('app/_layout.tsx created.'); 70 | }); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "@/*": [ 7 | "./*" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "**/*.tsx", 14 | ".expo/types/**/*.ts", 15 | "expo-env.d.ts" 16 | ] 17 | } 18 | --------------------------------------------------------------------------------