diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..2399e7e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "bracketSpacing": false, + "singleQuote": true, + "trailingComma": "all", + "arrowParens": "avoid" +} diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index c26961c..0000000 --- a/.prettierrc.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - bracketSpacing: false, - singleQuote: true, - trailingComma: 'all', - arrowParens: 'avoid', -}; diff --git a/README.md b/README.md index da06a7c..780474d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # SociQuote -A open source quotes app made possible by [Airtable](https://airtable.com/) and [React Native](https://reactnative.dev/). +A open source quotes app made possible by [React Native](https://reactnative.dev/). -![Airtable](https://img.shields.io/badge/Airtable-18BFFF?style=for-the-badge&logo=Airtable&logoColor=white) ![React Native](https://img.shields.io/badge/React_Native-20232A?style=for-the-badge&logo=react&logoColor=61DAFB) ## Status @@ -14,7 +13,12 @@ A open source quotes app made possible by [Airtable](https://airtable.com/) and Head over to [releases](https://github.com/siddsarkar/SociQuote/releases) to grab the latest apk from assets. -## Design +## Features + +- [x] Pull to refresh +- [x] Longpress for sharing + +## Design ![cover image](resources/cover.jpeg) diff --git a/package.json b/package.json index af2dcaf..dd29fd9 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "lint": "eslint . --fix" }, "dependencies": { - "@react-native-clipboard/clipboard": "^1.8.4", "appcenter": "^4.3.0", "appcenter-analytics": "^4.3.0", "appcenter-crashes": "^4.3.0", diff --git a/src/api/index.js b/src/api/index.js index 08574f8..d9f36c1 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -1,3 +1,3 @@ -import airtable from './providers/airtable.js'; +import quotable from './providers/quotable'; -export default {airtable}; +export default {quotable}; diff --git a/src/api/providers/airtable.js b/src/api/providers/airtable.js deleted file mode 100644 index f0fc073..0000000 --- a/src/api/providers/airtable.js +++ /dev/null @@ -1,116 +0,0 @@ -import requesthandler from '../requestHandler'; - -const AIRTABLE_BASE = 'appikp0oubt89KWGW'; // quoted app base -const AIRTABLE_BASE_URL = `https://api.airtable.com/v0/${AIRTABLE_BASE}`; -const AIRTABLE_API_KEY = 'keyZXKPvGAfYaTQKY'; - -/** - * ?AIRTABLE API INTERFACE - */ -export default { - /** - * Get a table within base - * @param {{airtable_api_key: string, table: string}} options api key and record data - * @returns table records - */ - getTable: async function ({ - airtable_api_key = AIRTABLE_API_KEY, - table, - params, - }) { - let queryStr = ''; - if (params) { - queryStr = Object.keys(params) - .map((key, idx) => { - if (idx === 0) { - return `?${key}=${params[key]}`; - } - return `&${key}=${params[key]}`; - }) - .join(''); - } - const url = `${AIRTABLE_BASE_URL}/${table}${queryStr}`; - return requesthandler(url, { - method: 'GET', - headers: {Authorization: `Bearer ${airtable_api_key}`}, - }); - }, - - /** - * Creates a record on specified base and table - * @param {{airtable_api_key: string, table: string, record: object}} options apikey, name of table, record - * @returns saved record - */ - createRecord: async function ({ - airtable_api_key = AIRTABLE_API_KEY, - table, - record, - }) { - const url = `${AIRTABLE_BASE_URL}/${table}`; - return requesthandler(url, { - method: 'POST', - headers: { - Authorization: `Bearer ${airtable_api_key}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(record), - }); - }, - - /** - * Updates a record - * @param {{airtable_api_key: string, table: string, record: object}} options apikey, table name and updated record object - * @returns updated record - */ - updateRecord: async function ({ - airtable_api_key = AIRTABLE_API_KEY, - table, - record, - }) { - const url = `${AIRTABLE_BASE_URL}/${table}`; - return requesthandler(url, { - method: 'PATCH', - headers: { - Authorization: `Bearer ${airtable_api_key}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({records: [record]}), - }); - }, - - /** - * Retrive user record if any - * @param {{airtable_api_key: string, user: object}} options apikey and firebase user object - * @returns users with the firebase user id - */ - getUser: async function ({airtable_api_key = AIRTABLE_API_KEY, user}) { - const url = `${AIRTABLE_BASE_URL}/users?filterByFormula=id=${user.id}`; - return requesthandler(url, { - method: 'GET', - headers: { - Authorization: `Bearer ${airtable_api_key}`, - 'Content-Type': 'application/json', - }, - }); - }, - - /** - * Sign Up a User - * @param {{airtable_api_key: string, user: object}} options api key and user object - * @returns user from airtable - */ - signupUser: async function ({airtable_api_key = AIRTABLE_API_KEY, user}) { - const url = `${AIRTABLE_BASE_URL}/users`; - const body = user.photo - ? JSON.stringify({fields: {...user, photo: [{url: user.photo}]}}) - : JSON.stringify({fields: {...user}}); - return requesthandler(url, { - method: 'POST', - headers: { - Authorization: `Bearer ${airtable_api_key}`, - 'Content-Type': 'application/json', - }, - body, - }); - }, -}; diff --git a/src/api/providers/quotable.js b/src/api/providers/quotable.js new file mode 100644 index 0000000..43e18fe --- /dev/null +++ b/src/api/providers/quotable.js @@ -0,0 +1,22 @@ +import {generateQueryStrFromObject, processFetchRequest} from '../../utils'; + +const QUOTABLE_BASE_URL = 'https://quotable.io'; + +/** + * Get list of quotes + * @param {object=} params query params as object + * @returns json response + */ +const getQuotes = async function (params) { + let url = `${QUOTABLE_BASE_URL}/quotes`; + if (params) { + url += generateQueryStrFromObject(params); + } + + return processFetchRequest(url); +}; + +export default { + getQuotes, + // other methods here +}; diff --git a/src/libs/Caraousel.js b/src/components/common/Caraousel/Caraousel.js similarity index 77% rename from src/libs/Caraousel.js rename to src/components/common/Caraousel/Caraousel.js index eaec888..23b4da7 100644 --- a/src/libs/Caraousel.js +++ b/src/components/common/Caraousel/Caraousel.js @@ -1,13 +1,14 @@ /** - * @source https://lloyds-digital.com/blog/lets-create-a-carousel-in-react-native - * @modified + * This caraousel was made by following the below article link + * https://lloyds-digital.com/blog/lets-create-a-carousel-in-react-native */ +import Analytics from 'appcenter-analytics'; import React, {memo, useCallback, useEffect, useRef, useState} from 'react'; import { - Clipboard, Dimensions, FlatList, + Share, StyleSheet, Text, ToastAndroid, @@ -17,19 +18,35 @@ import { const {width: windowWidth, height: windowHeight} = Dimensions.get('window'); const Slide = memo(function Slide({data}) { - const copyToClipboard = () => { - Clipboard.setString(data.fields.content + ' — ' + data.fields.author); - ToastAndroid.show('Copied', ToastAndroid.SHORT); + const onShare = async () => { + try { + const result = await Share.share({ + message: data.content + '\n — ' + data.author, + }); + if (result.action === Share.sharedAction) { + if (result.activityType) { + // shared with activity type of result.activityType + } else { + // shared + Analytics.trackEvent('Quote Shared', {id: data._id}); + } + } else if (result.action === Share.dismissedAction) { + // dismissed + } + } catch (error) { + ToastAndroid.show(error.message, ToastAndroid.SHORT); + } }; return ( - - {data.fields.content} + + + {data.content} - - — {data.fields.author} + + — {data.author} ); @@ -69,7 +86,7 @@ const Caraousel = ({slideList = []}) => { removeClippedSubviews: true, scrollEventThrottle: 16, windowSize: 2, - keyExtractor: useCallback(s => String(s.id), []), + keyExtractor: useCallback(s => s._id, []), getItemLayout: useCallback( (_, idx) => ({ index: idx, diff --git a/src/components/common/index.js b/src/components/common/index.js new file mode 100644 index 0000000..68a9139 --- /dev/null +++ b/src/components/common/index.js @@ -0,0 +1,5 @@ +/** + * Components that can be used anywhere in the project + */ + +export {default as Caraousel} from './Caraousel/Caraousel'; diff --git a/src/components/ui/index.js b/src/components/ui/index.js new file mode 100644 index 0000000..fffa08a --- /dev/null +++ b/src/components/ui/index.js @@ -0,0 +1,3 @@ +/** + * UI Elements goes here + */ diff --git a/src/utils/generateQueryStrFromObject.js b/src/utils/generateQueryStrFromObject.js new file mode 100644 index 0000000..d596523 --- /dev/null +++ b/src/utils/generateQueryStrFromObject.js @@ -0,0 +1,16 @@ +/** + * Returns query string from object + * @param {{ [param: string]: string|number|boolean }} obj query params as object + * @example generateQueryStrFromObject({"q":"any","page":1}) returns "?q=any&page=1" + */ +const generateQueryStrFromObject = obj => + Object.keys(obj) + .map((key, idx) => { + if (idx === 0) { + return `?${key}=${obj[key]}`; + } + return `&${key}=${obj[key]}`; + }) + .join(''); + +export default generateQueryStrFromObject; diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..6dafb5e --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,6 @@ +/** + * Utlities to be used anywhere in the project + */ + +export {default as generateQueryStrFromObject} from './generateQueryStrFromObject'; +export {default as processFetchRequest} from './processFetchRequest'; diff --git a/src/api/requestHandler.js b/src/utils/processFetchRequest.js similarity index 70% rename from src/api/requestHandler.js rename to src/utils/processFetchRequest.js index bb15718..2d8439e 100644 --- a/src/api/requestHandler.js +++ b/src/utils/processFetchRequest.js @@ -1,10 +1,10 @@ /** * Global network request handler - * @param {string} url url to fetch - * @param {Request} options fetch options + * @param {RequestInfo} url url to fetch + * @param {RequestInit=} options fetch options * @returns json response */ -async function requestHandler(url, options) { +const processFetchRequest = async (url, options) => { const ts = Date.now(); const method = options?.method || 'GET'; const endpoint = url.match( @@ -18,6 +18,6 @@ async function requestHandler(url, options) { return response.json(); } throw response.json(); -} +}; -export default requestHandler; +export default processFetchRequest; diff --git a/src/views/Home/HomeScreen.js b/src/views/Home/HomeScreen.js index 1818b46..7c31c81 100644 --- a/src/views/Home/HomeScreen.js +++ b/src/views/Home/HomeScreen.js @@ -7,24 +7,24 @@ import { View, } from 'react-native'; import api from '../../api'; -import Caraousel from '../../libs/Caraousel'; +import {Caraousel} from '../../components/common'; const HomeScreen = () => { const [data, setData] = useState([]); - const [pageOffset, setPageOffset] = useState(''); + const [page, setPage] = useState(1); const [isLoading, setIsLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const onRefresh = () => { setRefreshing(true); async function fetchData() { - const response = await api.airtable.getTable({ - table: 'quotes', - params: {limit: 10, offset: pageOffset}, + const response = await api.quotable.getQuotes({ + limit: 10, + page, }); - setPageOffset(response.offset); - setData(response.records); + setPage(response.page + 1); + setData(response.results); setRefreshing(false); } @@ -33,12 +33,10 @@ const HomeScreen = () => { useEffect(() => { async function fetchData() { - const response = await api.airtable.getTable({ - table: 'quotes', - params: {limit: 10}, - }); - setPageOffset(response.offset); - setData(response.records); + const response = await api.quotable.getQuotes({limit: 10}); + setPage(response.page + 1); + setData(response.results); + setIsLoading(false); } diff --git a/src/views/index.js b/src/views/index.js index 4eec777..7108b55 100644 --- a/src/views/index.js +++ b/src/views/index.js @@ -1 +1,5 @@ +/** + * All App Screens are Exported here + */ + export {default as HomeScreen} from './Home/HomeScreen'; diff --git a/yarn.lock b/yarn.lock index c44af88..9db9353 100644 --- a/yarn.lock +++ b/yarn.lock @@ -979,11 +979,6 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" -"@react-native-clipboard/clipboard@^1.8.4": - version "1.8.4" - resolved "https://registry.yarnpkg.com/@react-native-clipboard/clipboard/-/clipboard-1.8.4.tgz#4bc1fb00643688e489d8220cd635844ab5c066f9" - integrity sha512-poFq3RvXzkbXcqoQNssbZ+aNbCRzBFAWkR9QL7u9xNMgsyWZtk7d16JQoaBo8D2E+kKi+/9JOiVQzA5w+9N67w== - "@react-native-community/cli-debugger-ui@^6.0.0-rc.0": version "6.0.0-rc.0" resolved "https://registry.yarnpkg.com/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-6.0.0-rc.0.tgz#774378626e4b70f5e1e2e54910472dcbaffa1536"