├── .expo-shared └── assets.json ├── .gitignore ├── .prettierrc ├── App.tsx ├── README.md ├── app.json ├── assets ├── icon.png └── splash.png ├── babel.config.js ├── package.json ├── src ├── components │ ├── ClipButton.tsx │ ├── ListItem.tsx │ └── Loading.tsx ├── navigation │ └── AppNavigator.tsx ├── screens │ ├── ArticleScreen.tsx │ ├── ClipScreen.tsx │ └── HomeScreen.tsx ├── store │ ├── actions │ │ └── user.ts │ ├── index.ts │ └── reducers │ │ └── user.ts └── types │ ├── article.ts │ ├── clip.ts │ ├── navigation.ts │ ├── state.ts │ └── user.ts ├── tsconfig.json └── yarn.lock /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | web-report/ 12 | 13 | # macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "jsxBracketSameLine": true, 4 | "singleQuote": false, 5 | "trailingComma": "all", 6 | "tabWidth": 2, 7 | "printWidth": 80, 8 | "semi": true 9 | } 10 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {PersistGate} from "redux-persist/integration/react"; 3 | import {Provider} from "react-redux"; 4 | import store, {persistor} from "./src/store"; 5 | import {AppNavigator} from "./src/navigation/AppNavigator"; 6 | export default function App() { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # news-app-ts 2 | Udemy「React Native入門」の**TypeScript版**サンプルです。 3 | TypeScriptで書いてみたい方はご参考にして下さい。 4 | 5 | React Navigation v5に対応した改訂版です。 6 | 7 | https://www.udemy.com/course/react-native-first-step/?referralCode=05B579F15272BFA71DE1 8 | 9 | ![udemy_banner](https://user-images.githubusercontent.com/7026785/75599698-aed0fb80-5aea-11ea-81d5-06b8d4f98a96.jpg) 10 | 11 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "news-app-ts", 4 | "slug": "news-app-ts", 5 | "platforms": ["ios", "android", "web"], 6 | "version": "1.0.0", 7 | "orientation": "portrait", 8 | "icon": "./assets/icon.png", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "updates": { 15 | "fallbackToCacheTimeout": 0 16 | }, 17 | "assetBundlePatterns": ["**/*"], 18 | "ios": { 19 | "supportsTablet": true 20 | }, 21 | "extra": { 22 | "newsApiKey": "49a9ea5cfe5c4377b90ee6083946d195" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takahi5/news-app-ts/f41562607bced89865ef4114fa565f38e8305eca/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takahi5/news-app-ts/f41562607bced89865ef4114fa565f38e8305eca/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "@react-native-community/masked-view": "0.1.6", 12 | "@react-navigation/bottom-tabs": "^5.2.6", 13 | "@react-navigation/native": "^5.1.5", 14 | "@react-navigation/stack": "^5.2.10", 15 | "@types/react-redux": "^7.1.7", 16 | "axios": "^0.19.2", 17 | "expo": "~37.0.3", 18 | "expo-constants": "^9.0.0", 19 | "react": "~16.9.0", 20 | "react-dom": "~16.9.0", 21 | "react-native": "https://github.com/expo/react-native/archive/sdk-37.0.1.tar.gz", 22 | "react-native-gesture-handler": "~1.6.0", 23 | "react-native-reanimated": "~1.7.0", 24 | "react-native-safe-area-context": "0.7.3", 25 | "react-native-screens": "~2.2.0", 26 | "react-native-web": "~0.11.7", 27 | "react-native-webview": "^9.1.4", 28 | "react-redux": "^7.2.0", 29 | "redux": "^4.0.5", 30 | "redux-devtools-extension": "^2.13.8", 31 | "redux-persist": "^6.0.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.8.6", 35 | "@types/react": "~16.9.23", 36 | "@types/react-native": "~0.61.17", 37 | "babel-preset-expo": "~8.1.0", 38 | "typescript": "~3.8.3" 39 | }, 40 | "private": true 41 | } 42 | -------------------------------------------------------------------------------- /src/components/ClipButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {StyleSheet, TouchableOpacity} from "react-native"; 3 | import {FontAwesome} from "@expo/vector-icons"; 4 | 5 | const styles = StyleSheet.create({ 6 | container: { 7 | padding: 5, 8 | }, 9 | }); 10 | 11 | type Props = { 12 | onPress: () => void; 13 | enabled: boolean; 14 | }; 15 | 16 | export const ClipButton = ({onPress, enabled}: Props) => { 17 | const name = enabled ? "bookmark" : "bookmark-o"; 18 | return ( 19 | 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/ListItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {StyleSheet, Text, View, Image, TouchableOpacity} from "react-native"; 3 | 4 | const styles = StyleSheet.create({ 5 | itemContainer: { 6 | height: 100, 7 | width: "100%", 8 | borderColor: "gray", 9 | borderWidth: 1, 10 | flexDirection: "row", 11 | }, 12 | leftContainer: { 13 | width: 100, 14 | alignItems: "center", 15 | justifyContent: "center", 16 | }, 17 | rightContainer: { 18 | flex: 1, 19 | padding: 16, 20 | justifyContent: "space-between", 21 | }, 22 | text: { 23 | fontSize: 16, 24 | }, 25 | subText: { 26 | fontSize: 12, 27 | color: "gray", 28 | }, 29 | image: { 30 | width: 95, 31 | height: 95, 32 | }, 33 | }); 34 | 35 | type Props = { 36 | imageUrl: string; 37 | title: string; 38 | author: string; 39 | onPress: () => void; 40 | }; 41 | 42 | export const ListItem = ({imageUrl, title, author, onPress}: Props) => { 43 | return ( 44 | 45 | 46 | {!!imageUrl && } 47 | 48 | 49 | 50 | {title} 51 | 52 | {author} 53 | 54 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {StyleSheet, View, ActivityIndicator, Dimensions} from "react-native"; 3 | 4 | const styles = StyleSheet.create({ 5 | container: { 6 | width: "100%", 7 | height: "100%", 8 | position: "absolute", 9 | top: 0, 10 | left: 0, 11 | backgroundColor: "rgba(255, 255, 255, 0.5)", 12 | alignItems: "center", 13 | justifyContent: "center", 14 | }, 15 | }); 16 | 17 | export const Loading = () => { 18 | return ( 19 | 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/navigation/AppNavigator.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {NavigationContainer} from "@react-navigation/native"; 3 | import {createStackNavigator} from "@react-navigation/stack"; 4 | import { 5 | createBottomTabNavigator, 6 | BottomTabNavigationOptions, 7 | } from "@react-navigation/bottom-tabs"; 8 | import {FontAwesome} from "@expo/vector-icons"; 9 | /* screens */ 10 | import {HomeScreen} from "../screens/HomeScreen"; 11 | import {ArticleScreen} from "../screens/ArticleScreen"; 12 | import {ClipScreen} from "../screens/ClipScreen"; 13 | /* types */ 14 | import {RootStackParamList} from "../types/navigation"; 15 | 16 | const Stack = createStackNavigator(); 17 | const Tab = createBottomTabNavigator(); 18 | 19 | const HomeStack = () => { 20 | return ( 21 | 22 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | const ClipStack = () => { 33 | return ( 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | const screenOption = ({route}): BottomTabNavigationOptions => ({ 42 | tabBarIcon: ({color, size}) => { 43 | let iconName; 44 | switch (route.name) { 45 | case "Home": 46 | iconName = "home"; 47 | break; 48 | case "Clip": 49 | iconName = "bookmark"; 50 | break; 51 | } 52 | return ; 53 | }, 54 | }); 55 | 56 | export const AppNavigator = () => { 57 | return ( 58 | 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /src/screens/ArticleScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {StyleSheet, SafeAreaView} from "react-native"; 3 | import {WebView} from "react-native-webview"; 4 | import {useDispatch, useSelector} from "react-redux"; 5 | import {addClip, deleteClip} from "../store/actions/user"; 6 | /* components */ 7 | import {ClipButton} from "../components/ClipButton"; 8 | import {Loading} from "../components/Loading"; 9 | /* types */ 10 | import {StackNavigationProp} from "@react-navigation/stack"; 11 | import {RootStackParamList} from "../types/navigation"; 12 | import {RouteProp} from "@react-navigation/native"; 13 | import {State} from "../types/state"; 14 | import {User} from "../types/user"; 15 | 16 | type Props = { 17 | navigation: StackNavigationProp; 18 | route: RouteProp; 19 | }; 20 | 21 | export const ArticleScreen: React.FC = ({navigation, route}: Props) => { 22 | const {article} = route.params; 23 | 24 | const user = useSelector((state: State) => state.user) as User; 25 | 26 | const dispatch = useDispatch(); 27 | 28 | const isClipped = () => { 29 | return user.clips.some((clip) => clip.url === article.url); 30 | }; 31 | 32 | const toggleClip = () => { 33 | if (isClipped()) { 34 | dispatch(deleteClip({clip: article})); 35 | } else { 36 | dispatch(addClip({clip: article})); 37 | } 38 | }; 39 | 40 | return ( 41 | 42 | 43 | { 47 | return ; 48 | }} 49 | /> 50 | 51 | ); 52 | }; 53 | 54 | const styles = StyleSheet.create({ 55 | container: { 56 | flex: 1, 57 | backgroundColor: "#fff", 58 | justifyContent: "flex-start", 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /src/screens/ClipScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from "react"; 2 | import {StyleSheet, SafeAreaView, FlatList} from "react-native"; 3 | import {useSelector} from "react-redux"; 4 | /* components */ 5 | import {ListItem} from "../components/ListItem"; 6 | /* types */ 7 | import {StackNavigationProp} from "@react-navigation/stack"; 8 | import {RootStackParamList} from "../types/navigation"; 9 | import {RouteProp} from "@react-navigation/native"; 10 | import {State} from "../types/state"; 11 | import {User} from "../types/user"; 12 | 13 | type Props = { 14 | navigation: StackNavigationProp; 15 | route: RouteProp; 16 | }; 17 | 18 | export const ClipScreen: React.FC = ({navigation, route}: Props) => { 19 | const user = useSelector((state: State) => state.user) as User; 20 | 21 | return ( 22 | 23 | ( 26 | navigation.navigate("Article", {article: item})} 31 | /> 32 | )} 33 | keyExtractor={(item, index) => index.toString()} 34 | /> 35 | 36 | ); 37 | }; 38 | 39 | const styles = StyleSheet.create({ 40 | container: { 41 | flex: 1, 42 | backgroundColor: "#fff", 43 | justifyContent: "flex-start", 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /src/screens/HomeScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from "react"; 2 | import {StyleSheet, SafeAreaView, FlatList} from "react-native"; 3 | import Constants from "expo-constants"; 4 | import axios from "axios"; 5 | /* components */ 6 | import {ListItem} from "../components/ListItem"; 7 | import {Loading} from "../components/Loading"; 8 | /* types */ 9 | import {StackNavigationProp} from "@react-navigation/stack"; 10 | import {RouteProp} from "@react-navigation/native"; 11 | import {RootStackParamList} from "../types/navigation"; 12 | import {Article} from "../types/article"; 13 | 14 | const URL = `https://newsapi.org/v2/top-headlines?country=jp&category=business&apiKey=${Constants.manifest.extra.newsApiKey}`; 15 | 16 | const styles = StyleSheet.create({ 17 | container: { 18 | flex: 1, 19 | backgroundColor: "#fff", 20 | justifyContent: "flex-start", 21 | }, 22 | }); 23 | 24 | type Props = { 25 | navigation: StackNavigationProp; 26 | route: RouteProp; 27 | }; 28 | 29 | export const HomeScreen: React.FC = ({navigation, route}: Props) => { 30 | const [articles, setArticles] = useState([]); 31 | const [loading, setLoading] = useState(false); 32 | 33 | useEffect(() => { 34 | fetchArticles(); 35 | }, []); 36 | 37 | const fetchArticles = async () => { 38 | setLoading(true); 39 | try { 40 | const response = await axios.get(URL); 41 | setArticles(response.data.articles); 42 | } catch (error) { 43 | console.error(error); 44 | } 45 | setLoading(false); 46 | }; 47 | 48 | return ( 49 | 50 | {loading && } 51 | ( 54 | navigation.navigate("Article", {article: item})} 59 | /> 60 | )} 61 | keyExtractor={(item, index) => index.toString()} 62 | /> 63 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /src/store/actions/user.ts: -------------------------------------------------------------------------------- 1 | import {Article} from "../../types/article"; 2 | 3 | export type AddClipAction = { 4 | type: "ADD_CLIP"; 5 | clip: Article; 6 | }; 7 | 8 | export const addClip = ({clip}: {clip: Article}) => { 9 | return { 10 | type: "ADD_CLIP", 11 | clip, 12 | }; 13 | }; 14 | 15 | export type DeleteClipAction = { 16 | type: "DELETE_CLIP"; 17 | clip: Article; 18 | }; 19 | 20 | export const deleteClip = ({clip}: {clip: Article}) => { 21 | return { 22 | type: "DELETE_CLIP", 23 | clip, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import {createStore, combineReducers} from 'redux'; 2 | import {composeWithDevTools} from 'redux-devtools-extension'; 3 | import {AsyncStorage} from 'react-native'; 4 | import {persistReducer, persistStore} from 'redux-persist'; 5 | import userReducer from './reducers/user'; 6 | 7 | const persistConfig = { 8 | key: 'root', 9 | storage: AsyncStorage, 10 | whitelist: ['user'], 11 | }; 12 | 13 | const rootReducer = combineReducers({ 14 | user: userReducer, 15 | }); 16 | 17 | const persistedReducer = persistReducer(persistConfig, rootReducer); 18 | 19 | const store = createStore(persistedReducer, composeWithDevTools()); 20 | 21 | export const persistor = persistStore(store); 22 | export default store; 23 | -------------------------------------------------------------------------------- /src/store/reducers/user.ts: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | clips: [], 3 | }; 4 | 5 | const reducer = (state = initialState, action) => { 6 | switch (action.type) { 7 | case 'ADD_CLIP': 8 | return { 9 | ...state, 10 | clips: [...state.clips, action.clip], 11 | }; 12 | case 'DELETE_CLIP': 13 | return { 14 | ...state, 15 | clips: state.clips.filter(clip => clip.url !== action.clip.url), 16 | }; 17 | default: 18 | return state; 19 | } 20 | }; 21 | 22 | export default reducer; 23 | -------------------------------------------------------------------------------- /src/types/article.ts: -------------------------------------------------------------------------------- 1 | export type Article = { 2 | title: string; 3 | author: string; 4 | url: string; 5 | urlToImage: string; 6 | }; 7 | -------------------------------------------------------------------------------- /src/types/clip.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takahi5/news-app-ts/f41562607bced89865ef4114fa565f38e8305eca/src/types/clip.ts -------------------------------------------------------------------------------- /src/types/navigation.ts: -------------------------------------------------------------------------------- 1 | import {Article} from "./article"; 2 | 3 | export type RootStackParamList = { 4 | Home: undefined; 5 | Clip: undefined; 6 | Article: {article: Article}; 7 | }; 8 | -------------------------------------------------------------------------------- /src/types/state.ts: -------------------------------------------------------------------------------- 1 | import {AddClipAction, DeleteClipAction} from "../store/actions/user"; 2 | import {User} from "./user"; 3 | 4 | export interface State { 5 | user: User; 6 | } 7 | 8 | export type Actions = 9 | // user 10 | AddClipAction | DeleteClipAction; 11 | -------------------------------------------------------------------------------- /src/types/user.ts: -------------------------------------------------------------------------------- 1 | import {Article} from "./article"; 2 | 3 | export type User = { 4 | clips: Article[]; 5 | }; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "jsx": "react-native", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "noEmit": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true, 10 | "strict": true 11 | } 12 | } 13 | --------------------------------------------------------------------------------