Browse Source

Initial commit

master
Agastya 1 year ago
commit
72355cd8e2
22 changed files with 11896 additions and 0 deletions
  1. +4
    -0
      .expo-shared/assets.json
  2. +14
    -0
      .gitignore
  3. +15
    -0
      App.js
  4. +30
    -0
      app.json
  5. +4
    -0
      assets/constants/API.js
  6. BIN
      assets/icon.png
  7. BIN
      assets/splash.png
  8. +83
    -0
      assets/styles/index.js
  9. +6
    -0
      babel.config.js
  10. +27
    -0
      package.json
  11. +81
    -0
      src/components/AppContainer/index.js
  12. +93
    -0
      src/components/comment/index.js
  13. +9
    -0
      src/components/renderSeparator/index.js
  14. +70
    -0
      src/components/screens/AskStories.js
  15. +70
    -0
      src/components/screens/BestStories.js
  16. +70
    -0
      src/components/screens/NewStories.js
  17. +70
    -0
      src/components/screens/TopStories.js
  18. +128
    -0
      src/components/story/index.js
  19. +93
    -0
      src/components/storyModal/index.js
  20. +71
    -0
      src/components/userModal/index.js
  21. +5357
    -0
      yarn-error.log
  22. +5601
    -0
      yarn.lock

+ 4
- 0
.expo-shared/assets.json View File

@ -0,0 +1,4 @@
{
"f9155ac790fd02fadcdeca367b02581c04a353aa6d5aa84409a59f6804c87acd": true,
"89ed26367cdb9b771858e026f2eb95bfdb90e5ae943e716575327ec325f39c44": true
}

+ 14
- 0
.gitignore View File

@ -0,0 +1,14 @@
node_modules/**/*
.expo/*
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
web-report/
# macOS
.DS_Store

+ 15
- 0
App.js View File

@ -0,0 +1,15 @@
import React from 'react';
import { SafeAreaView } from 'react-native';
import AppContainer from './src/components/AppContainer';
import styles from './assets/styles';
function App() {
return (
<SafeAreaView style={styles.container}>
<AppContainer />
</SafeAreaView>
);
}
export default App;

+ 30
- 0
app.json View File

@ -0,0 +1,30 @@
{
"expo": {
"name": "ex",
"slug": "ex",
"privacy": "public",
"sdkVersion": "35.0.0",
"platforms": [
"ios",
"android",
"web"
],
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true
}
}
}

+ 4
- 0
assets/constants/API.js View File

@ -0,0 +1,4 @@
const APIurl = 'https://hacker-news.firebaseio.com';
const version = 'v0';
module.exports = { APIurl, version };

BIN
assets/icon.png View File

Before After
Width: 192  |  Height: 192  |  Size: 1.1 KiB

BIN
assets/splash.png View File

Before After
Width: 1242  |  Height: 2436  |  Size: 7.0 KiB

+ 83
- 0
assets/styles/index.js View File

@ -0,0 +1,83 @@
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 20,
justifyContent: 'center'
},
headingContainer: {
paddingTop: 20,
paddingBottom: 10
},
heading: {
fontSize: 32,
fontWeight: '800'
},
seperator: {
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#777'
},
commentSeperator: {
marginTop: 20,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#777'
},
storyComment: {
marginBottom: 5
},
storyContainer: {
marginTop: 10,
marginBottom: 10
},
storyTitle: {
fontSize: 22,
fontWeight: '700',
marginBottom: 10
},
storyUrl: {
marginBottom: 5
},
storyUser: {
color: 'rgb(147,147,149)',
marginBottom: 5
},
userModalName: {
fontSize: 20,
fontWeight: '600',
marginBottom: 5,
color: 'white'
},
userModalAbout: {
color: 'white',
marginBottom: 5
},
userModalContainer: {
backgroundColor: '#777',
borderRadius: 5,
padding: 20
},
userModalKarma: {
marginTop: 5,
color: 'white'
},
modelContainer: {
flex: 1,
padding: 20,
marginHorizontal: 20,
justifyContent: 'center'
},
storyModalContainer: {
flex: 1,
paddingHorizontal: 20,
marginHorizontal: 20,
marginBottom: 20
},
commentStarter: {
marginTop: 20,
fontWeight: '700',
fontSize: 20
}
});
export default styles;

+ 6
- 0
babel.config.js View File

@ -0,0 +1,6 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
};
};

+ 27
- 0
package.json View File

@ -0,0 +1,27 @@
{
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"eject": "expo eject"
},
"dependencies": {
"@expo/vector-icons": "^10.0.0",
"expo": "^35.0.0",
"moment": "^2.24.0",
"node-fetch": "^2.6.0",
"react": "16.8.3",
"react-dom": "16.8.3",
"react-native": "https://github.com/expo/react-native/archive/sdk-35.0.0.tar.gz",
"react-native-render-html": "^4.1.2",
"react-native-web": "^0.11.7",
"react-navigation": "2.0.1",
"react-navigation-material-bottom-tabs": "0.1.2"
},
"devDependencies": {
"babel-preset-expo": "^7.1.0"
},
"private": true
}

+ 81
- 0
src/components/AppContainer/index.js View File

@ -0,0 +1,81 @@
// import { createMaterialBottomTabNavigator } from 'react-navigation-material-bottom-tabs';
import React from 'react';
import { createBottomTabNavigator } from 'react-navigation';
import { Ionicons } from '@expo/vector-icons';
import AskStories from '../screens/AskStories';
import BestStories from '../screens/BestStories';
import NewStories from '../screens/NewStories';
import TopStories from '../screens/TopStories';
const AppContainer = createBottomTabNavigator(
{
Top: {
screen: TopStories,
navigationOptions: {
tabBarOptions: {
activeTintColor: 'rgb(255, 25, 85)'
},
tabBarIcon: ({ tintColor }) => (
<Ionicons
name="ios-arrow-dropup-circle"
color={tintColor}
size={22}
/>
)
}
},
Best: {
screen: BestStories,
navigationOptions: {
tabBarOptions: {
activeTintColor: 'rgb(255, 204, 0)'
},
tabBarIcon: ({ tintColor }) => (
<Ionicons
name="ios-star"
color={tintColor}
size={22}
/>
)
}
},
New: {
screen: NewStories,
navigationOptions: {
tabBarOptions: {
activeTintColor: 'rgb(88, 86, 214)'
},
tabBarIcon: ({ tintColor }) => (
<Ionicons
name="ios-hourglass"
color={tintColor}
size={22}
/>
)
}
},
Ask: {
screen: AskStories,
navigationOptions: {
tabBarOptions: {
activeTintColor: 'rgb(175, 82, 222)'
},
tabBarIcon: ({ tintColor }) => (
<Ionicons
name="ios-help-circle-outline"
color={tintColor}
size={22}
/>
)
}
}
},
{
tabBarOptions: {
inactiveTintColor: 'rgb(142,142,147)',
activeTintColor: 'rgb(255, 204, 0)'
}
}
);
export default AppContainer;

+ 93
- 0
src/components/comment/index.js View File

@ -0,0 +1,93 @@
import React, { useState, useEffect } from 'react';
import {
Linking,
Text,
View,
Modal,
TouchableOpacity
} from 'react-native';
import moment from 'moment';
import HTML from 'react-native-render-html';
import fetch from 'node-fetch';
import {
APIurl,
version
} from '../../../assets/constants/API';
import styles from '../../../assets/styles';
import UserModal from '../userModal';
function Comment({ commentID }) {
const [commentData, setCommentData] = useState();
const [showUserModal, setShowUserModal] = useState(false);
useEffect(() => {
let isMounted = true;
async function getCommentData() {
if (isMounted) {
const data = await fetch(
`${APIurl}/${version}/item/${commentID}.json`
);
const res = await data.json();
setCommentData(res);
}
}
getCommentData();
return () => {
setCommentData(undefined);
isMounted = false;
};
}, []);
return commentData && !commentData.deleted ? (
<View style={styles.storyContainer}>
<Modal
animationType="slide"
transparent={false}
visible={showUserModal}
onRequestClose={() => {
Alert.alert('Modal has been closed.');
}}
>
<UserModal
userID={commentData.by}
setShowUserModal={setShowUserModal}
/>
</Modal>
<TouchableOpacity
onPress={() => setShowUserModal(true)}
>
<Text style={styles.storyUser}>
{commentData.by} {parseTime(commentData.time)}
</Text>
</TouchableOpacity>
{commentData.text ? (
<HTML
html={commentData.text}
onLinkPress={(_, href) => Linking.openURL(href)}
/>
) : (
<></>
)}
{commentData.url ? (
<Text
style={styles.storyUrl}
onPress={() => Linking.openURL(commentData.url)}
>
Open URL 🔗
</Text>
) : (
<></>
)}
</View>
) : (
<Null />
);
}
function Null() {
return <></>;
}
function parseTime(UNIXtime) {
return moment(UNIXtime * 1000).fromNow();
}
export default Comment;

+ 9
- 0
src/components/renderSeparator/index.js View File

@ -0,0 +1,9 @@
import React from 'react';
import { View } from 'react-native';
import styles from '../../../assets/styles';
function renderSeparator() {
return <View style={styles.seperator}></View>;
}
export default renderSeparator;

+ 70
- 0
src/components/screens/AskStories.js View File

@ -0,0 +1,70 @@
import React, { useState, useEffect } from 'react';
import {
FlatList,
SafeAreaView,
Platform,
Text,
View
} from 'react-native';
import fetch from 'node-fetch';
import styles from '../../../assets/styles';
import Story from '../story';
import {
APIurl,
version
} from '../../../assets/constants/API';
import renderSeparator from '../renderSeparator';
function AskStories() {
const [askStories, setAskStories] = useState([]);
useEffect(() => {
let isMounted = true;
async function loadAskStories() {
if (isMounted) {
const data = await fetch(
`${APIurl}/${version}/askstories.json`
);
const res = await data.json();
setAskStories(res);
}
}
loadAskStories();
return () => {
setAskStories([]);
isMounted = false;
};
}, []);
return (
<SafeAreaView style={styles.container}>
<View style={styles.headingContainer}>
<Text style={styles.heading}>Ask stories</Text>
</View>
{askStories ? (
<FlatList
showsVerticalScrollIndicator={false}
data={askStories}
renderItem={story => (
<Story storyID={story.item} />
)}
keyExtractor={story => String(story)}
ItemSeparatorComponent={
Platform.OS == 'ios' && renderSeparator
}
/>
) : (
<Null />
)}
</SafeAreaView>
);
}
function Null() {
return <></>;
}
export default AskStories;

+ 70
- 0
src/components/screens/BestStories.js View File

@ -0,0 +1,70 @@
import React, { useState, useEffect } from 'react';
import {
FlatList,
SafeAreaView,
Platform,
Text,
View
} from 'react-native';
import fetch from 'node-fetch';
import styles from '../../../assets/styles';
import Story from '../story';
import {
APIurl,
version
} from '../../../assets/constants/API';
import renderSeparator from '../renderSeparator';
function BestStories() {
const [bestStories, setBestStories] = useState([]);
useEffect(() => {
let isMounted = true;
async function loadBestStories() {
if (isMounted) {
const data = await fetch(
`${APIurl}/${version}/beststories.json`
);
const res = await data.json();
setBestStories(res);
}
}
loadBestStories();
return () => {
setBestStories([]);
isMounted = false;
};
}, []);
return (
<SafeAreaView style={styles.container}>
<View style={styles.headingContainer}>
<Text style={styles.heading}>Best stories</Text>
</View>
{bestStories ? (
<FlatList
showsVerticalScrollIndicator={false}
data={bestStories}
renderItem={story => (
<Story storyID={story.item} />
)}
keyExtractor={story => String(story)}
ItemSeparatorComponent={
Platform.OS == 'ios' && renderSeparator
}
/>
) : (
<Null />
)}
</SafeAreaView>
);
}
function Null() {
return <Text>Loading...</Text>;
}
export default BestStories;

+ 70
- 0
src/components/screens/NewStories.js View File

@ -0,0 +1,70 @@
import React, { useState, useEffect } from 'react';
import {
FlatList,
SafeAreaView,
Platform,
Text,
View
} from 'react-native';
import fetch from 'node-fetch';
import styles from '../../../assets/styles';
import Story from '../story';
import {
APIurl,
version
} from '../../../assets/constants/API';
import renderSeparator from '../renderSeparator';
function NewStories() {
const [newStories, setNewStories] = useState([]);
useEffect(() => {
let isMounted = true;
async function loadNewStories() {
if (isMounted) {
const data = await fetch(
`${APIurl}/${version}/newstories.json`
);
const res = await data.json();
setNewStories(res);
}
}
loadNewStories();
return () => {
setNewStories([]);
isMounted = false;
};
}, []);
return (
<SafeAreaView style={styles.container}>
<View style={styles.headingContainer}>
<Text style={styles.heading}>New stories</Text>
</View>
{newStories ? (
<FlatList
showsVerticalScrollIndicator={false}
data={newStories}
renderItem={story => (
<Story storyID={story.item} />
)}
keyExtractor={story => String(story)}
ItemSeparatorComponent={
Platform.OS == 'ios' && renderSeparator
}
/>
) : (
<Null />
)}
</SafeAreaView>
);
}
function Null() {
return <></>;
}
export default NewStories;

+ 70
- 0
src/components/screens/TopStories.js View File

@ -0,0 +1,70 @@
import React, { useState, useEffect } from 'react';
import {
FlatList,
SafeAreaView,
Platform,
Text,
View
} from 'react-native';
import fetch from 'node-fetch';
import styles from '../../../assets/styles';
import Story from '../story';
import {
APIurl,
version
} from '../../../assets/constants/API';
import renderSeparator from '../renderSeparator';
function TopStories() {
const [topStories, setTopStories] = useState([]);
useEffect(() => {
let isMounted = true;
async function loadTopStories() {
if (isMounted) {
const data = await fetch(
`${APIurl}/${version}/topstories.json`
);
const res = await data.json();
setTopStories(res);
}
}
loadTopStories();
return () => {
setTopStories([]);
isMounted = false;
};
}, []);
return (
<SafeAreaView style={styles.container}>
<View style={styles.headingContainer}>
<Text style={styles.heading}>Top stories</Text>
</View>
{topStories ? (
<FlatList
showsVerticalScrollIndicator={false}
data={topStories}
renderItem={story => (
<Story storyID={story.item} />
)}
keyExtractor={story => String(story)}
ItemSeparatorComponent={
Platform.OS == 'ios' && renderSeparator
}
/>
) : (
<Null />
)}
</SafeAreaView>
);
}
function Null() {
return <></>;
}
export default TopStories;

+ 128
- 0
src/components/story/index.js View File

@ -0,0 +1,128 @@
import React, { useEffect, useState } from 'react';
import {
Alert,
TouchableOpacity,
Text,
View,
Linking,
Modal
} from 'react-native';
import moment from 'moment';
import HTML from 'react-native-render-html';
import fetch from 'node-fetch';
import {
APIurl,
version
} from '../../../assets/constants/API';
import styles from '../../../assets/styles';
import UserModal from '../userModal';
import StoryModal from '../storyModal';
function Story({ storyID }) {
const [storyData, setStoryData] = useState(undefined);
const [showUserModal, setShowUserModal] = useState(false);
const [showStoryModal, setShowStoryModal] = useState(
false
);
useEffect(() => {
let isMounted = true;
async function getStoryData() {
if (isMounted) {
const data = await fetch(
`${APIurl}/${version}/item/${storyID}.json`
);
const res = await data.json();
setStoryData(res);
}
}
getStoryData();
return () => {
setStoryData(undefined);
isMounted = false;
};
}, []);
{
return storyData && !storyData.deleted ? (
<View style={styles.storyContainer}>
<Modal
animationType="slide"
transparent={false}
visible={showUserModal}
onRequestClose={() => {
Alert.alert('Modal has been closed.');
}}
>
<UserModal
userID={storyData.by}
setShowUserModal={setShowUserModal}
/>
</Modal>
<Modal
animationType="slide"
transparent={false}
visible={showStoryModal}
>
<StoryModal
setShowStoryModal={setShowStoryModal}
storyData={storyData}
/>
</Modal>
<TouchableOpacity
onPress={() => setShowUserModal(true)}
>
<Text style={styles.storyUser}>
{storyData.by} {parseTime(storyData.time)}
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setShowStoryModal(true)}
>
<Text style={styles.storyTitle}>
{storyData.title}
</Text>
{storyData.text ? (
<HTML
html={storyData.text}
onLinkPress={(_, href) =>
Linking.openURL(href)
}
/>
) : (
<></>
)}
</TouchableOpacity>
{storyData.url ? (
<Text
style={styles.storyUrl}
onPress={() => Linking.openURL(storyData.url)}
>
Open URL 🔗
</Text>
) : (
<></>
)}
<Text>
{storyData.score} upvote
{storyData.score == 1 ? '' : 's'} {' '}
{storyData.descendants} comment
{storyData.descendants == 1 ? '' : 's'}
</Text>
</View>
) : (
<Null />
);
}
}
function Null() {
return <></>;
}
function parseTime(UNIXtime) {
return moment(UNIXtime * 1000).fromNow();
}
export default Story;

+ 93
- 0
src/components/storyModal/index.js View File

@ -0,0 +1,93 @@
import React, { useEffect, useState } from 'react';
import {
SafeAreaView,
Text,
Linking,
FlatList,
Platform,
View,
TouchableOpacity
} from 'react-native';
import moment from 'moment';
import HTML from 'react-native-render-html';
import renderSeparator from '../renderSeparator';
import Comment from '../comment';
import {
APIurl,
version
} from '../../../assets/constants/API';
import styles from '../../../assets/styles';
function StoryModal({ storyData, setShowStoryModal }) {
return (
<SafeAreaView style={styles.storyModalContainer}>
<Text style={styles.storyUser}>
{storyData.by} {parseTime(storyData.time)}
</Text>
<TouchableOpacity
onPress={() => setShowStoryModal(false)}
>
<Text style={styles.storyTitle}>
{storyData.title}
</Text>
</TouchableOpacity>
{storyData.text ? (
<HTML html={storyData.text} />
) : (
<></>
)}
{storyData.url ? (
<Text
style={styles.storyUrl}
onPress={() => Linking.openURL(storyData.url)}
>
Open URL 🔗
</Text>
) : (
<></>
)}
<Text>
{storyData.score} upvote
{storyData.score == 1 ? '' : 's'} {' '}
{storyData.descendants} comment
{storyData.descendants == 1 ? '' : 's'}
</Text>
<View style={styles.commentSeperator} />
<Text style={styles.commentStarter}>Comments</Text>
{storyData.kids ? (
<FlatList
showsVerticalScrollIndicator={false}
data={storyData.kids}
renderItem={comment => (
<Comment commentID={comment.item} />
)}
keyExtractor={comment => {
return String(comment);
}}
ItemSeparatorComponent={
Platform.OS == 'ios' && renderSeparator
}
/>
) : (
<Null />
)}
</SafeAreaView>
);
}
function Null() {
return (
<View>
<Text>No comments so far.</Text>
</View>
);
}
function parseTime(UNIXtime) {
return moment(UNIXtime * 1000).fromNow();
}
export default StoryModal;

+ 71
- 0
src/components/userModal/index.js View File

@ -0,0 +1,71 @@
import React, { useEffect, useState } from 'react';
import {
SafeAreaView,
View,
Text,
Linking,
TouchableOpacity
} from 'react-native';
import HTML from 'react-native-render-html';
import {
APIurl,
version
} from '../../../assets/constants/API';
import styles from '../../../assets/styles';
function UserModal({ userID, setShowUserModal }) {
const [userData, setUserData] = useState(undefined);
useEffect(() => {
let isMounted = true;
async function getUserData() {
if (isMounted) {
const data = await fetch(
`${APIurl}/${version}/user/${userID}.json`
);
const res = await data.json();
setUserData(res);
}
}
getUserData();
return () => {
setUserData(undefined);
isMounted = false;
};
}, []);
return userData ? (
<SafeAreaView style={styles.modelContainer}>
<TouchableOpacity
onPress={() => setShowUserModal(false)}
>
<View style={styles.userModalContainer}>
<Text style={styles.userModalName}>
{userData.id}
</Text>
{userData.about ? (
<HTML
html={userData.about}
style={styles.userModalAbout}
onLinkPress={(_, href) =>
Linking.openURL(href)
}
/>
) : (
<Null />
)}
<Text style={styles.userModalKarma}>
{userData.karma} karma
</Text>
</View>
</TouchableOpacity>
</SafeAreaView>
) : (
<Null />
);
}
function Null() {
return <></>;
}
export default UserModal;

+ 5357
- 0
yarn-error.log
File diff suppressed because it is too large
View File


+ 5601
- 0
yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save