这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,6 @@ Here are a few videos / talks that introduce Ignite and show off some of its fea
</a>
</figure>
</td>
<td>
<figure>
<a href="https://www.youtube.com/watch?v=n_VjjJxyd8Q">
<img src="https://img.youtube.com/vi/n_VjjJxyd8Q/sddefault.jpg" alt="Jamon's Code Quest on MobX-State-Tree" width="100%" /><br />
<figcaption><strong>Intro to MobX-State-Tree</strong></figcaption>
</a>
</figure>
</td>
</tr>
</table>

Expand All @@ -70,8 +62,6 @@ Nothing makes it into Ignite unless it's been proven on projects that Infinite R
| React | UI Framework | v19 | The most popular UI framework in the world |
| TypeScript | Language | v5 | Static typechecking |
| React Navigation | Navigation | v7 | Performant and consistent navigation framework |
| MobX-State-Tree | State Management | v7 | Observable state tree |
| MobX-React-Lite | React Integration | v4 | Re-render React performantly |
| Expo | SDK | v53 | Allows (optional) Expo modules |
| Expo Font | Custom Fonts | v13 | Import custom fonts |
| Expo Localization | Internationalization | v16 | i18n support (including RTL!) |
Expand Down
34 changes: 9 additions & 25 deletions boilerplate/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ import { useEffect, useState } from "react"
import { initialWindowMetrics, SafeAreaProvider } from "react-native-safe-area-context"
import { useFonts } from "expo-font"
import * as Linking from "expo-linking"
import * as SplashScreen from "expo-splash-screen"
import { KeyboardProvider } from "react-native-keyboard-controller"

import { useInitialRootStore } from "./models" // @mst remove-current-line
import { AuthProvider } from "./context/AuthContext"
import { AppNavigator, useNavigationPersistence } from "./navigators"
import * as storage from "./utils/storage"
import { customFontsToLoad } from "./theme"
Expand Down Expand Up @@ -75,30 +74,13 @@ export function App() {
.then(() => loadDateFnsLocale())
}, [])

// @mst replace-next-line useEffect(() => {
const { rehydrated } = useInitialRootStore(() => {
// @mst replace-next-line
// This runs after the root store has been initialized and rehydrated.

// If your initialization scripts run very fast, it's good to show the splash screen for just a bit longer to prevent flicker.
// Slightly delaying splash screen hiding for better UX; can be customized or removed as needed,
setTimeout(SplashScreen.hideAsync, 500)

// @mst replace-next-line }, [])
})

// Before we show the app, we have to wait for our state to be ready.
// In the meantime, don't render anything. This will be the background
// color set in native by rootView's background color.
// In iOS: application:didFinishLaunchingWithOptions:
// In Android: https://stackoverflow.com/a/45838109/204044
// You can replace with your own loading component if you wish.
if (
!rehydrated || // @mst remove-current-line
!isNavigationStateRestored ||
!isI18nInitialized ||
(!areFontsLoaded && !fontLoadError)
) {
if (!isNavigationStateRestored || !isI18nInitialized || (!areFontsLoaded && !fontLoadError)) {
return null
}

Expand All @@ -111,11 +93,13 @@ export function App() {
return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<KeyboardProvider>
<AppNavigator
linking={linking}
initialState={initialNavigationState}
onStateChange={onNavigationStateChange}
/>
<AuthProvider>
<AppNavigator
linking={linking}
initialState={initialNavigationState}
onStateChange={onNavigationStateChange}
/>
</AuthProvider>
</KeyboardProvider>
</SafeAreaProvider>
)
Expand Down
53 changes: 53 additions & 0 deletions boilerplate/app/context/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createContext, FC, PropsWithChildren, useCallback, useContext, useMemo } from "react"
import { useMMKVString } from "react-native-mmkv"

export type AuthContextType = {
isAuthenticated: boolean
authToken?: string
authEmail?: string
setAuthToken: (token?: string) => void
setAuthEmail: (email: string) => void
logout: () => void
validationError: string
}

export const AuthContext = createContext<AuthContextType | null>(null)

export interface AuthProviderProps {}

export const AuthProvider: FC<PropsWithChildren<AuthProviderProps>> = ({ children }) => {
const [authToken, setAuthToken] = useMMKVString("AuthProvider.authToken")
Copy link
Contributor

Choose a reason for hiding this comment

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

I love this as a simple way to get persistence

const [authEmail, setAuthEmail] = useMMKVString("AuthProvider.authEmail")

const logout = useCallback(() => {
setAuthToken(undefined)
setAuthEmail("")
}, [setAuthEmail, setAuthToken])

const validationError = useMemo(() => {
if (!authEmail || authEmail.length === 0) return "can't be blank"
if (authEmail.length < 6) return "must be at least 6 characters"
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(authEmail)) return "must be a valid email address"
return ""
}, [authEmail])

const value = {
isAuthenticated: !!authToken,
authToken,
authEmail,
setAuthToken,
setAuthEmail,
logout,
validationError,
}

return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}

export const useAuth = () => {
const context = useContext(AuthContext)
if (!context) throw new Error("useAuth must be used within an AuthProvider")
return context
}

// @demo remove-file
154 changes: 154 additions & 0 deletions boilerplate/app/context/EpisodeContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import {
createContext,
FC,
PropsWithChildren,
useCallback,
useContext,
useMemo,
useState,
} from "react"

import { api } from "@/services/api"
import { translate } from "@/i18n/translate"
import { formatDate } from "@/utils/formatDate"

export interface Episode {
guid: string
title: string
pubDate: string
link: string
author: string
thumbnail: string
description: string
content: string
enclosure: {
link: string
type: string
length: number
duration: number
rating: { scheme: string; value: string }
}
categories: string[]
}

export type EpisodeContextType = {
totalEpisodes: number
totalFavorites: number
episodesForList: Episode[]
fetchEpisodes: () => Promise<void>
favoritesOnly: boolean
toggleFavoritesOnly: () => void
hasFavorite: (episode: Episode) => boolean
toggleFavorite: (episode: Episode) => void
}

export const EpisodeContext = createContext<EpisodeContextType | null>(null)

export interface EpisodeProviderProps {}

export const EpisodeProvider: FC<PropsWithChildren<EpisodeProviderProps>> = ({ children }) => {
const [episodes, setEpisodes] = useState<Episode[]>([])
const [favorites, setFavorites] = useState<string[]>([])
const [favoritesOnly, setFavoritesOnly] = useState<boolean>(false)

const fetchEpisodes = useCallback(async () => {
const response = await api.getEpisodes()
if (response.kind === "ok") {
setEpisodes(response.episodes)
} else {
console.error(`Error fetching episodes: ${JSON.stringify(response)}`)
}
}, [])

const toggleFavoritesOnly = useCallback(() => {
setFavoritesOnly((prev) => !prev)
}, [])

const toggleFavorite = useCallback(
(episode: Episode) => {
if (favorites.some((fav) => fav === episode.guid)) {
setFavorites((prev) => prev.filter((fav) => fav !== episode.guid))
} else {
setFavorites((prev) => [...prev, episode.guid])
}
},
[favorites],
)

const hasFavorite = useCallback(
(episode: Episode) => favorites.some((fav) => fav === episode.guid),
[favorites],
)

const episodesForList = useMemo(() => {
return favoritesOnly ? episodes.filter((episode) => favorites.includes(episode.guid)) : episodes
}, [episodes, favorites, favoritesOnly])

const value = {
totalEpisodes: episodes.length,
totalFavorites: favorites.length,
episodesForList,
fetchEpisodes,
favoritesOnly,
toggleFavoritesOnly,
hasFavorite,
toggleFavorite,
}

return <EpisodeContext.Provider value={value}>{children}</EpisodeContext.Provider>
}

export const useEpisodes = () => {
const context = useContext(EpisodeContext)
if (!context) throw new Error("useEpisodes must be used within an EpisodeProvider")
return context
}

// A helper hook to extract and format episode details
export const useEpisode = (episode: Episode) => {
const { hasFavorite } = useEpisodes()

const isFavorite = hasFavorite(episode)

let datePublished
try {
const formatted = formatDate(episode.pubDate)
datePublished = {
textLabel: formatted,
accessibilityLabel: translate("demoPodcastListScreen:accessibility.publishLabel", {
date: formatted,
}),
}
} catch {
datePublished = { textLabel: "", accessibilityLabel: "" }
}

const seconds = Number(episode.enclosure?.duration ?? 0)
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
const s = Math.floor((seconds % 3600) % 60)
const duration = {
textLabel: `${h > 0 ? `${h}:` : ""}${m > 0 ? `${m}:` : ""}${s}`,
accessibilityLabel: translate("demoPodcastListScreen:accessibility.durationLabel", {
hours: h,
minutes: m,
seconds: s,
}),
}

const trimmedTitle = episode.title?.trim()
const titleMatches = trimmedTitle?.match(/^(RNR.*\d)(?: - )(.*$)/)
const parsedTitleAndSubtitle =
titleMatches && titleMatches.length === 3
? { title: titleMatches[1], subtitle: titleMatches[2] }
: { title: trimmedTitle, subtitle: "" }

return {
isFavorite,
datePublished,
duration,
parsedTitleAndSubtitle,
}
}

// @demo remove-file
27 changes: 1 addition & 26 deletions boilerplate/app/devtools/ReactotronConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,10 @@
*/
import { Platform, NativeModules } from "react-native"
import { ArgType } from "reactotron-core-client"
import { mst } from "reactotron-mst" // @mst remove-current-line
import mmkvPlugin from "reactotron-react-native-mmkv"
import { ReactotronReactNative } from "reactotron-react-native"

import {
storage,
clear, // @mst remove-current-line
} from "@/utils/storage"
import { storage } from "@/utils/storage"
import { goBack, resetRoot, navigate } from "@/navigators/navigationUtilities"

import { Reactotron } from "./ReactotronClient"
Expand All @@ -25,15 +21,6 @@ const reactotron = Reactotron.configure({
},
})

// @mst remove-block-start
reactotron.use(
mst({
/* ignore some chatty `mobx-state-tree` actions */
filter: (event) => /postProcessSnapshot|@APPLY_SNAPSHOT/.test(event.name) === false,
}),
)
// @mst remove-block-end

reactotron.use(mmkvPlugin<ReactotronReactNative>({ storage }))

if (Platform.OS !== "web") {
Expand Down Expand Up @@ -65,18 +52,6 @@ reactotron.onCustomCommand({
},
})

// @mst remove-block-start
reactotron.onCustomCommand({
title: "Reset Root Store",
description: "Resets the MST store",
command: "resetStore",
handler: () => {
Reactotron.log("resetting store")
clear()
},
})
// @mst remove-block-end

reactotron.onCustomCommand({
title: "Reset Navigation State",
description: "Resets the navigation state",
Expand Down
37 changes: 0 additions & 37 deletions boilerplate/app/models/AuthenticationStore.ts

This file was deleted.

Loading