├── .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 |
--------------------------------------------------------------------------------