├── .watchmanconfig ├── constants ├── auth.js ├── vars.js ├── Layout.js └── Colors.js ├── assets ├── images │ ├── brain.png │ ├── icon.png │ ├── splash.png │ ├── youtube-logo.png │ └── buymeacoffee-logo.png └── fonts │ ├── Menlo-Regular.ttf │ └── SpaceMono-Regular.ttf ├── .gitignore ├── babel.config.js ├── screens ├── settings │ ├── queries.gql.js │ ├── Container.js │ └── Settings.js ├── login │ ├── mutation.gql.js │ ├── Container.js │ └── Login.js ├── mood │ ├── mutations.gql.js │ ├── queries.gql.js │ ├── Container.js │ └── Mood.js ├── register │ ├── Container.js │ ├── mutation.gql.js │ └── Register.js ├── habits │ ├── queries.gql.js │ ├── mutations.gql.js │ ├── Container.js │ └── Habits.js └── createHabit │ ├── Container.js │ ├── mutations.gql.js │ └── CreateHabit.js ├── components ├── FullLoading.js ├── __tests__ │ └── StyledText-test.js ├── DayHabitSquare.js ├── DoubleTap.js ├── Habit.js ├── NavHeader.js ├── DayHabits.js ├── DayMood.js ├── styled.js └── MoodGraph.js ├── .expo-shared └── assets.json ├── utils ├── icons.js ├── dayjs.js └── reminders.js ├── navigation ├── TabBarIcon.js ├── ThemedTabBar.js └── AppNavigator.js ├── __tests__ └── App-test.js ├── app.json ├── dribbble-ball-icon.svg ├── dribbble.svg ├── package.json ├── dribbbble_logo.svg ├── config ├── theme.js └── setup.js ├── App.js ├── dddddd.svg └── README.md /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /constants/auth.js: -------------------------------------------------------------------------------- 1 | export const USER_ACCESS_TOKEN = 'USER_ACCESS_TOKEN'; -------------------------------------------------------------------------------- /assets/images/brain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalinmiron/uzual-mobile/HEAD/assets/images/brain.png -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalinmiron/uzual-mobile/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /constants/vars.js: -------------------------------------------------------------------------------- 1 | export const POLL_INTERVAL = 10000; 2 | export const MOOD_REMINDER = 'MOOD_REMINDER'; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p12 6 | *.key 7 | *.mobileprovision 8 | -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalinmiron/uzual-mobile/HEAD/assets/images/splash.png -------------------------------------------------------------------------------- /assets/fonts/Menlo-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalinmiron/uzual-mobile/HEAD/assets/fonts/Menlo-Regular.ttf -------------------------------------------------------------------------------- /assets/images/youtube-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalinmiron/uzual-mobile/HEAD/assets/images/youtube-logo.png -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalinmiron/uzual-mobile/HEAD/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /assets/images/buymeacoffee-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalinmiron/uzual-mobile/HEAD/assets/images/buymeacoffee-logo.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /screens/settings/queries.gql.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const me = gql` 4 | query me { 5 | me { 6 | id 7 | name 8 | email 9 | isPro 10 | pushToken 11 | } 12 | } 13 | `; 14 | 15 | export default { me }; 16 | -------------------------------------------------------------------------------- /screens/login/mutation.gql.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | const login = gql` 4 | mutation login($email: String!, $password: String!) { 5 | login(email: $email, password: $password) { 6 | token 7 | } 8 | } 9 | `; 10 | 11 | export default { login }; -------------------------------------------------------------------------------- /constants/Layout.js: -------------------------------------------------------------------------------- 1 | import { Dimensions } from 'react-native'; 2 | 3 | const width = Dimensions.get('window').width; 4 | const height = Dimensions.get('window').height; 5 | 6 | export default { 7 | window: { 8 | width, 9 | height, 10 | }, 11 | isSmallDevice: width < 375, 12 | }; 13 | -------------------------------------------------------------------------------- /screens/mood/mutations.gql.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const setMood = gql` 4 | mutation setMood($date: String, $type: MoodTypes!) { 5 | setMood(date: $date, type: $type) { 6 | id 7 | type 8 | date 9 | } 10 | } 11 | `; 12 | 13 | export default { setMood }; 14 | -------------------------------------------------------------------------------- /screens/register/Container.js: -------------------------------------------------------------------------------- 1 | import { graphql, compose, withApollo } from 'react-apollo'; 2 | import Register from './Register'; 3 | import mutations from './mutation.gql'; 4 | 5 | export default compose( 6 | graphql(mutations.register, { 7 | name: 'register' 8 | }), 9 | withApollo 10 | )(Register); 11 | -------------------------------------------------------------------------------- /components/FullLoading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FullScreenWrapper, Body } from './styled'; 3 | 4 | export default () => ( 5 | 6 | {/* */} 7 | Loading... 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /screens/mood/queries.gql.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const moods = gql` 4 | query moods($start: DateTime!, $end: DateTime!) { 5 | moods(where: { date_gte: $start, date_lte: $end }, orderBy: date_ASC) { 6 | id 7 | type 8 | date 9 | } 10 | } 11 | `; 12 | 13 | export default { moods }; 14 | -------------------------------------------------------------------------------- /screens/register/mutation.gql.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const register = gql` 4 | mutation register($name: String, $email: String!, $password: String!) { 5 | register: signup(name: $name, email: $email, password: $password) { 6 | token 7 | } 8 | } 9 | `; 10 | 11 | export default { register }; 12 | -------------------------------------------------------------------------------- /components/__tests__/StyledText-test.js: -------------------------------------------------------------------------------- 1 | import 'react-native'; 2 | import React from 'react'; 3 | import { MonoText } from '../StyledText'; 4 | import renderer from 'react-test-renderer'; 5 | 6 | it('renders correctly', () => { 7 | const tree = renderer.create(Snapshot test!).toJSON(); 8 | 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /screens/login/Container.js: -------------------------------------------------------------------------------- 1 | import { graphql, compose, withApollo } from 'react-apollo'; 2 | import Login from './Login'; 3 | import mutations from './mutation.gql'; 4 | import { withTheme } from 'styled-components'; 5 | 6 | export default compose( 7 | graphql(mutations.login, { 8 | name: 'login' 9 | }), 10 | withApollo, 11 | withTheme 12 | )(Login); 13 | -------------------------------------------------------------------------------- /components/DayHabitSquare.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Body, HabitSquare } from './styled'; 3 | 4 | export default ({ day }) => { 5 | return ( 6 | 7 | 8 | {new Date(day.date).getDate()} 9 | 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "d6f3e8beadc6be5334ca557735ce52114bc871adc0ce2a9786fcd23156557e8d": true, 3 | "6a1ffaaab698159ba170e19de48a5d933ebbbadc547ec25a2ded202338b22d11": true, 4 | "15c318a72e0018c94470773bfa610d4e5e47c46deb319c80169d59fc8035c517": true, 5 | "0fba456c71b106597df5d65cd1311cbc328b394f4bd5f381572cf7b78177965e": true, 6 | "ac37bab86acad540b2352b7e7b70bc9243ef44dcc7b772f5f47f9ecca830b96e": true 7 | } -------------------------------------------------------------------------------- /screens/habits/queries.gql.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const habits = gql` 4 | query habits($start: DateTime, $end: DateTime) { 5 | habits { 6 | id 7 | title 8 | description 9 | starred 10 | habits(orderBy: date_ASC, where: { date_gte: $start, date_lte: $end }) { 11 | id 12 | date 13 | done 14 | } 15 | } 16 | } 17 | `; 18 | 19 | export default { habits }; 20 | -------------------------------------------------------------------------------- /screens/createHabit/Container.js: -------------------------------------------------------------------------------- 1 | import { graphql, compose, withApollo } from 'react-apollo'; 2 | import CreateHabit from './CreateHabit'; 3 | import mutations from './mutations.gql'; 4 | import { withTheme } from 'styled-components'; 5 | 6 | export default compose( 7 | graphql(mutations.createHabit, { 8 | name: 'createHabit' 9 | }), 10 | graphql(mutations.deleteHabit, { 11 | name: 'deleteHabit' 12 | }), 13 | withApollo, 14 | withTheme 15 | )(CreateHabit); 16 | -------------------------------------------------------------------------------- /screens/settings/Container.js: -------------------------------------------------------------------------------- 1 | import { graphql, compose, withApollo } from 'react-apollo'; 2 | import Settings from './Settings'; 3 | import queries from './queries.gql'; 4 | import { withTheme } from 'styled-components'; 5 | 6 | export default compose( 7 | graphql(queries.me, { 8 | options: { 9 | pollInterval: 20000, 10 | fetchPolicy: 'cache-and-network', 11 | notifyOnNetworkStatusChange: true 12 | } 13 | }), 14 | withApollo, 15 | withTheme 16 | )(Settings); 17 | -------------------------------------------------------------------------------- /constants/Colors.js: -------------------------------------------------------------------------------- 1 | const tintColor = 'turquoise'; 2 | 3 | export default { 4 | tintColor, 5 | tabIconDefault: '#d3d3d3', 6 | tabIconSelected: tintColor, 7 | tabBar: 'red', 8 | errorBackground: 'red', 9 | errorText: 'rgba(242, 49, 76, 1)', 10 | warningBackground: '#EAEB5E', 11 | warningText: '#666804', 12 | noticeBackground: tintColor, 13 | noticeText: '#fff', 14 | grey: '#d3d3d3', 15 | primary: tintColor, 16 | white: '#fff', 17 | darkGrey: '#333', 18 | midGrey: 'rgba(0,0,0,0.4)' 19 | }; 20 | -------------------------------------------------------------------------------- /screens/habits/mutations.gql.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const setDailyHabit = gql` 4 | mutation setDailyHabit($habitId: ID!, $date: String) { 5 | setDailyHabit(habitId: $habitId, date: $date) { 6 | id 7 | done 8 | date 9 | } 10 | } 11 | `; 12 | 13 | const setPushToken = gql` 14 | mutation setPushToken($pushToken: String!) { 15 | setPushToken(pushToken: $pushToken) { 16 | pushToken 17 | } 18 | } 19 | `; 20 | 21 | export default { setDailyHabit, setPushToken }; 22 | -------------------------------------------------------------------------------- /utils/icons.js: -------------------------------------------------------------------------------- 1 | import { 2 | faDizzy, 3 | faFrown, 4 | faFrownOpen, 5 | faMeh, 6 | faSmile, 7 | faLaugh, 8 | faGrinHearts 9 | } from '@fortawesome/free-solid-svg-icons'; 10 | //dizzy, frown, frown-open, meh,smile, laught, grid-hearts 11 | export const icns = [ 12 | 'Dizzy', 13 | 'Frown', 14 | 'FrownOpen', 15 | 'Meh', 16 | 'Smile', 17 | 'Laugh', 18 | 'GrinHearts' 19 | ]; 20 | export const icons = { 21 | Dizzy: faDizzy, 22 | Frown: faFrown, 23 | FrownOpen: faFrownOpen, 24 | Meh: faMeh, 25 | Smile: faSmile, 26 | Laugh: faLaugh, 27 | GrinHearts: faGrinHearts 28 | }; 29 | -------------------------------------------------------------------------------- /navigation/TabBarIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as Icon from '@expo/vector-icons'; 3 | import { withTheme } from 'styled-components'; 4 | 5 | class TabBarIcon extends React.Component { 6 | render() { 7 | return ( 8 | 18 | ); 19 | } 20 | } 21 | 22 | export default withTheme(TabBarIcon); 23 | -------------------------------------------------------------------------------- /components/DoubleTap.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TouchableWithoutFeedback } from 'react-native'; 3 | 4 | export default class DoubleTap extends React.Component { 5 | lastTap = null; 6 | _handleDoubleTap = () => { 7 | const now = Date.now(); 8 | if (this.lastTap && now - this.lastTap < this.props.delay) { 9 | this.props.onDoubleTap(); 10 | } else { 11 | this.lastTap = now; 12 | this.props.onPress(); 13 | } 14 | }; 15 | render() { 16 | return ( 17 | 18 | {this.props.children} 19 | 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /components/Habit.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TouchableWithoutFeedback } from 'react-native'; 3 | import { Body, Block } from './styled'; 4 | import DayHabits from './DayHabits'; 5 | 6 | export default ({ habit, onSetDailyHabit, onPress }) => ( 7 | onSetDailyHabit(habit)} 9 | onLongPress={() => onPress(habit)} 10 | > 11 | 12 | 13 | {habit.title} {habit.starred && '🌟'} 14 | 15 | 16 | {habit.description} 17 | 18 | 19 | 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /__tests__/App-test.js: -------------------------------------------------------------------------------- 1 | import 'react-native'; 2 | import React from 'react'; 3 | import App from '../App'; 4 | import renderer from 'react-test-renderer'; 5 | import NavigationTestUtils from 'react-navigation/NavigationTestUtils'; 6 | 7 | describe('App snapshot', () => { 8 | jest.useFakeTimers(); 9 | beforeEach(() => { 10 | NavigationTestUtils.resetInternalState(); 11 | }); 12 | 13 | it('renders the loading screen', async () => { 14 | const tree = renderer.create().toJSON(); 15 | expect(tree).toMatchSnapshot(); 16 | }); 17 | 18 | it('renders the root without loading screen', async () => { 19 | const tree = renderer.create().toJSON(); 20 | expect(tree).toMatchSnapshot(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /components/NavHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform, TouchableOpacity } from 'react-native'; 3 | import * as Icon from '@expo/vector-icons'; 4 | import { withTheme } from 'styled-components'; 5 | import { Header } from './styled'; 6 | 7 | export const BackButton = withTheme(({ onPress, theme }) => ( 8 | 9 | 17 | 18 | )); 19 | 20 | export default ({ onBackPress }) => ( 21 |
22 | 23 |
24 | ); 25 | -------------------------------------------------------------------------------- /screens/mood/Container.js: -------------------------------------------------------------------------------- 1 | import { graphql, compose, withApollo } from 'react-apollo'; 2 | import Mood from './Mood'; 3 | import queries from './queries.gql'; 4 | import mutations from './mutations.gql'; 5 | import { withTheme } from 'styled-components'; 6 | import { start, end, TIME_FORMAT } from '../../utils/dayjs'; 7 | import { POLL_INTERVAL } from '../../constants/vars'; 8 | 9 | export default compose( 10 | graphql(queries.moods, { 11 | options: { 12 | pollInterval: POLL_INTERVAL, 13 | fetchPolicy: 'cache-and-network', 14 | notifyOnNetworkStatusChange: true, 15 | variables: { 16 | start, 17 | end 18 | } 19 | } 20 | }), 21 | graphql(mutations.setMood, { 22 | name: 'setMood' 23 | }), 24 | withApollo, 25 | withTheme 26 | )(Mood); 27 | -------------------------------------------------------------------------------- /screens/habits/Container.js: -------------------------------------------------------------------------------- 1 | import { graphql, compose, withApollo } from 'react-apollo'; 2 | import Habits from './Habits'; 3 | import queries from './queries.gql'; 4 | import mutations from './mutations.gql'; 5 | import { withTheme } from 'styled-components'; 6 | import { start, end } from '../../utils/dayjs'; 7 | import { POLL_INTERVAL } from '../../constants/vars'; 8 | 9 | export default compose( 10 | graphql(queries.habits, { 11 | options: { 12 | pollInterval: POLL_INTERVAL, 13 | fetchPolicy: 'cache-and-network', 14 | variables: { 15 | start, 16 | end 17 | } 18 | } 19 | }), 20 | graphql(mutations.setDailyHabit, { 21 | name: 'setDailyHabit' 22 | }), 23 | graphql(mutations.setPushToken, { 24 | name: 'setPushToken' 25 | }), 26 | withApollo, 27 | withTheme 28 | )(Habits); 29 | -------------------------------------------------------------------------------- /screens/createHabit/mutations.gql.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const createHabit = gql` 4 | mutation createHabit( 5 | $id: ID 6 | $title: String! 7 | $description: String! 8 | $starred: Boolean 9 | $start: DateTime 10 | $end: DateTime 11 | ) { 12 | createHabit( 13 | id: $id 14 | title: $title 15 | description: $description 16 | starred: $starred 17 | ) { 18 | id 19 | title 20 | description 21 | starred 22 | habits(orderBy: date_ASC, where: { date_gte: $start, date_lte: $end }) { 23 | id 24 | date 25 | done 26 | } 27 | } 28 | } 29 | `; 30 | 31 | const deleteHabit = gql` 32 | mutation deleteHabit($id: ID!) { 33 | deleteHabit(id: $id) { 34 | id 35 | } 36 | } 37 | `; 38 | 39 | export default { createHabit, deleteHabit }; 40 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Uzual", 4 | "slug": "Uzual", 5 | "description": "Mood & Habits tracker.", 6 | "privacy": "public", 7 | "sdkVersion": "33.0.0", 8 | "platforms": ["ios", "android"], 9 | "version": "1.0.0", 10 | "orientation": "portrait", 11 | "icon": "./assets/images/icon.png", 12 | "primaryColor": "#40E0D0", 13 | "splash": { 14 | "image": "./assets/images/splash.png", 15 | "resizeMode": "contain", 16 | "backgroundColor": "#40E0D0" 17 | }, 18 | "updates": { 19 | "fallbackToCacheTimeout": 0 20 | }, 21 | "assetBundlePatterns": ["**/*"], 22 | "android": { 23 | "permissions": ["CAMERA_ROLL", "CAMERA", "NOTIFICATIONS"], 24 | "package": "com.BatmanCodes.Android.Uzual", 25 | "versionCode": 1 26 | }, 27 | "ios": { 28 | "bundleIdentifier": "com.BatmanCodes.Uzual", 29 | "supportsTablet": true 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /navigation/ThemedTabBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withTheme } from 'styled-components'; 3 | import { BottomTabBar } from 'react-navigation'; 4 | 5 | class ThemedTabBar extends React.Component { 6 | render() { 7 | return ( 8 | 25 | ); 26 | } 27 | } 28 | 29 | export default withTheme(ThemedTabBar); 30 | -------------------------------------------------------------------------------- /utils/dayjs.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | const dayInstance = dayjs(); 3 | 4 | export const start = dayInstance.startOf('month'); 5 | export const end = dayInstance.endOf('month'); 6 | export const current = dayInstance; 7 | export const daysInMonth = dayInstance.daysInMonth(); 8 | export const TIME_FORMAT = 'YYYY-MM-DD'; 9 | 10 | // Note 11 | // Open an Issue/Bug day.js repository (dayjs().startOf('month')) is 12 | // returning 2019-04-30T22:00:00.000Z but dayjs().startOf('month').format('YYYY-MM-DD') returns 13 | // 2019-05-01 14 | 15 | // Memoize 16 | // Expose a method that will accept an argument (Month) 17 | // and it will return the list of days as Array[] 18 | // This is going to be useful when we'll have a month 19 | // picker on Habit screen but also for the Mood screen 20 | // because there you can also filter the current month. 21 | export const days = [...Array(daysInMonth).keys()].map(u => { 22 | const date = start.add(u, 'day').format('YYYY-MM-DD'); 23 | 24 | return { 25 | date, 26 | disabled: dayjs(date).isAfter(current) 27 | }; 28 | }); 29 | -------------------------------------------------------------------------------- /components/DayHabits.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Body, HabitSquare, Row } from './styled'; 3 | import { days } from '../utils/dayjs'; 4 | import DayHabitSquare from './DayHabitSquare'; 5 | 6 | export default class DayHabits extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | dayHabits: this._getHabitList() 12 | }; 13 | } 14 | 15 | componentWillReceiveProps() { 16 | this.setState({ 17 | dayHabits: this._getHabitList() 18 | }); 19 | } 20 | 21 | _getHabitList = () => { 22 | const { habitId, habits } = this.props; 23 | 24 | return days.map(({ date, disabled }) => { 25 | const index = habits.findIndex(habit => habit.date.startsWith(date)); 26 | return index !== -1 27 | ? { 28 | ...habits[index], 29 | disabled 30 | } 31 | : { 32 | id: `${date}-${habitId}`, 33 | done: false, 34 | date, 35 | disabled 36 | }; 37 | }); 38 | }; 39 | 40 | render() { 41 | return ( 42 | 43 | {this.state.dayHabits.map(day => ( 44 | 45 | ))} 46 | 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /dribbble-ball-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /dribbble.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /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 | "eject": "expo eject", 8 | "test": "node ./node_modules/jest/bin/jest.js --watchAll", 9 | "debugger": "rndebugger-open --expo", 10 | "build-android": "expo build:android -t app-bundle", 11 | "build-ios": "expo build:ios" 12 | }, 13 | "jest": { 14 | "preset": "jest-expo" 15 | }, 16 | "dependencies": { 17 | "@expo/vector-icons": "^9.0.0", 18 | "@fortawesome/free-solid-svg-icons": "^5.8.2", 19 | "apollo-cache-inmemory": "^1.5.1", 20 | "apollo-cache-persist": "^0.1.1", 21 | "apollo-client": "^2.5.1", 22 | "apollo-client-preset": "^1.0.8", 23 | "apollo-link-context": "^1.0.17", 24 | "apollo-link-retry": "^2.2.13", 25 | "apollo-link-ws": "^1.0.17", 26 | "apollo-utilities": "^1.2.1", 27 | "d3-scale": "^3.0.0", 28 | "d3-shape": "^1.3.5", 29 | "dayjs": "^1.8.14", 30 | "expo": "^33.0.0", 31 | "expo-asset": "^5.0.1", 32 | "expo-constants": "~5.0.1", 33 | "expo-font": "~5.0.1", 34 | "expo-permissions": "~5.0.1", 35 | "graphql": "^14.2.1", 36 | "graphql-tag": "^2.10.1", 37 | "react": "16.8.3", 38 | "react-apollo": "^2.5.5", 39 | "react-native": "https://github.com/expo/react-native/archive/sdk-33.0.0.tar.gz", 40 | "react-native-safe-area-view": "^0.13.1", 41 | "react-native-svg": "~9.4.0", 42 | "react-native-view-shot": "~2.6.0", 43 | "react-navigation": "^3.11.0", 44 | "recompose": "^0.30.0", 45 | "styled-components": "^4.2.0", 46 | "styled-map": "^3.2.0-rc.1", 47 | "subscriptions-transport-ws": "^0.9.16", 48 | "uuid": "^3.3.2" 49 | }, 50 | "devDependencies": { 51 | "babel-preset-expo": "^5.0.0", 52 | "expo-codemod": "^0.0.1-beta.8", 53 | "jest-expo": "^33.0.0", 54 | "react-native-debugger-open": "^0.3.19" 55 | }, 56 | "private": true 57 | } 58 | -------------------------------------------------------------------------------- /utils/reminders.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import { Notifications } from 'expo'; 3 | import { AsyncStorage } from 'react-native'; 4 | import { MOOD_REMINDER } from '../constants/vars'; 5 | 6 | /* 7 | mood reminders (8PM daily) 8 | habits reminders 9 | daily goals 10 | random mantras (positive quotes) 11 | */ 12 | 13 | // https://docs.expo.io/versions/latest/sdk/notifications/#notificationsschedulelocalnotificationasynclocalnotification-schedulingoptions 14 | 15 | // An object that describes when the notification should fire. 16 | // time (date or number) -- A Date object representing when to fire the notification or a number in Unix epoch time. Example: (new Date()).getTime() + 1000 is one second from now. 17 | // repeat (optional) (string) -- 'minute', 'hour', 'day', 'week', 'month', or 'year'. 18 | // (Android only) intervalMs (optional) (number) -- Repeat interval in number of milliseconds 19 | const scheduleMoodReminders = async () => { 20 | // const hasNotificationId = AsyncStorage.getItem(MOOD_REMINDER); 21 | await Notifications.cancelAllScheduledNotificationsAsync(); 22 | 23 | // if (hasNotificationId) { 24 | // console.log('Already have a notification.'); 25 | // return; 26 | // } 27 | 28 | const time = dayjs() 29 | .add(dayjs().hour() > 20 ? 1 : 0, 'day') 30 | .set('hour', 20) 31 | .set('minutes', 0); 32 | 33 | console.log('Scheduled notification for: ', time); 34 | 35 | const notificationId = await Notifications.scheduleLocalNotificationAsync( 36 | { 37 | title: 'Mood reminder', 38 | body: `It's time to set your current mood. Always stay positive!` //Come with a better text 39 | }, 40 | { 41 | time: time.valueOf(), 42 | repeat: 'day' 43 | } 44 | ); 45 | // Check how to schedule local notifications in newer Expo SDKs. 46 | console.log('Mood Reminders NotificationID: ', notificationId); 47 | await AsyncStorage.setItem(MOOD_REMINDER, notificationId); 48 | }; 49 | 50 | export { scheduleMoodReminders }; 51 | -------------------------------------------------------------------------------- /dribbbble_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/DayMood.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Dimensions } from 'react-native'; 3 | import { withTheme } from 'styled-components'; 4 | import Svg, { Circle, G, Path, Symbol, Use } from 'react-native-svg'; 5 | 6 | import { start, end, TIME_FORMAT } from '../utils/dayjs'; 7 | import { icns, icons } from '../utils/icons'; 8 | import { 9 | Body, 10 | FullScreenWrapper, 11 | Row, 12 | Heading, 13 | Wrapper, 14 | Button 15 | } from './styled'; 16 | const { width, height } = Dimensions.get('screen'); 17 | 18 | const iconSize = (width * 0.8) / icns.length; 19 | const iconSpacing = (width * 0.2) / icns.length; 20 | const svgSize = (iconSize + iconSpacing) * (icns.length - 1); 21 | 22 | class DayMood extends React.Component { 23 | _renderMoodIcons = () => { 24 | const { theme } = this.props; 25 | 26 | return icns.map((iconType, index) => { 27 | const icon = icons[iconType]; 28 | const [w, h, _, __, path] = icon.icon; 29 | return [ 30 | 35 | 41 | , 42 | this.props.setMood(iconType)} 45 | width={iconSize} 46 | height={iconSize} 47 | x={index * (iconSize + iconSpacing)} 48 | > 49 | 55 | 60 | 61 | ]; 62 | }); 63 | }; 64 | 65 | _renderIconList = () => { 66 | return ( 67 | 72 | {this._renderMoodIcons()} 73 | 74 | ); 75 | }; 76 | 77 | render() { 78 | return ( 79 | 80 | 81 | Actually, what's your mood today? 82 | 83 | {this._renderIconList()} 84 | 85 | ); 86 | } 87 | } 88 | 89 | export default withTheme(DayMood); 90 | -------------------------------------------------------------------------------- /config/theme.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const ThemeContext = React.createContext(null); 4 | 5 | const moodColors = { 6 | dizzy: '#dc143c', 7 | frown: '#ce533b', 8 | 'frown-open': '#be723a', 9 | meh: '#ab8c39', 10 | smile: '#93a337', 11 | laugh: '#72b935', 12 | 'grin-hearts': '#32cd32' 13 | }; 14 | 15 | const colors = { 16 | white: '#FFFFFF', 17 | bg: '#FAFAFA', 18 | primary: '#40E0D0', 19 | default: '#333333', 20 | placeholder: '#9B9B9B', 21 | shadow: '#dddddd', 22 | subheading: '#6D6D6D', 23 | error: 'rgba(242, 49, 76, 1)', 24 | loadingBg: 'rgba(255, 255, 255, 0.85)', 25 | tabBar: 'white', 26 | noBg: 'transparent', 27 | moodGraphColor: '#333333', 28 | moodGraphColorNegative: moodColors.dizzy, 29 | moodGraphColorPositive: moodColors['grin-hearts'], 30 | moodColors 31 | }; 32 | 33 | const colorsDark = { 34 | ...colors, 35 | bg: '#222222', 36 | default: '#d3d3d3', 37 | tabBar: '#333333', 38 | shadow: 'rgba(255, 255, 255, 0.25)', 39 | loadingBg: 'rgba(0, 0, 0, 0.85)', 40 | moodGraphColor: '#d3d3d3' 41 | }; 42 | 43 | const spacing = { 44 | small: 10, 45 | normal: 14, 46 | big: 20, 47 | default: 10, 48 | huge: 30, 49 | xHuge: 60, 50 | noMargin: 0 51 | }; 52 | 53 | const flexAlign = { 54 | center: 'center', 55 | left: 'flex-start', 56 | right: 'flex-end', 57 | default: 'flex-start' 58 | }; 59 | const textAlign = { 60 | center: 'center', 61 | left: 'left', 62 | right: 'right', 63 | default: 'left' 64 | }; 65 | 66 | const size = { 67 | stiny: 10, 68 | tiny: 12, 69 | default: 14, 70 | medium: 16, 71 | large: 24, 72 | xlarge: 32, 73 | xxlarge: 46, 74 | xxxlarge: 52 75 | }; 76 | 77 | const height = { 78 | input: 40, 79 | tabBar: 60, 80 | header: 60 81 | }; 82 | 83 | const button = { 84 | tiny: 26, 85 | default: 40, 86 | medium: 60, 87 | big: 80 88 | }; 89 | 90 | const fontWeight = { 91 | light: '300', 92 | regular: '400', 93 | normal: '500', 94 | bold: '700', 95 | extrabold: '900', 96 | default: '500', 97 | consi: '400', 98 | // 99 | big: '700', 100 | large: '700', 101 | xlarge: '700' 102 | }; 103 | 104 | const avatar = { 105 | small: 50, 106 | default: 50, 107 | big: 60, 108 | huge: 140, 109 | light: 40 110 | }; 111 | const layout = { 112 | radius: 10, 113 | smallRadius: 7, 114 | badgeRadius: 4, 115 | habitSquareSize: 22, 116 | width: 260, 117 | fabButtonSize: 50, 118 | moodGraphStrokeDashArray: '13, 5' 119 | }; 120 | 121 | export default (theme = 'light') => ({ 122 | colors: theme === 'light' ? colors : colorsDark, 123 | spacing, 124 | layout, 125 | size, 126 | fontWeight, 127 | avatar, 128 | height, 129 | flexAlign, 130 | textAlign, 131 | button 132 | }); 133 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ApolloProvider } from 'react-apollo'; 3 | import { ThemeProvider } from 'styled-components'; 4 | import { SafeAreaView, Platform, StatusBar, StyleSheet } from 'react-native'; 5 | import { AppLoading } from 'expo'; 6 | import * as Icon from '@expo/vector-icons'; 7 | import * as Font from 'expo-font'; 8 | import { Asset } from 'expo-asset'; 9 | import AppNavigator from './navigation/AppNavigator'; 10 | import setupApolloClient from './config/setup'; 11 | import Colors from './constants/Colors'; 12 | import { Bg } from './components/styled'; 13 | import theme, { ThemeContext } from './config/theme'; 14 | 15 | const _makeTheme = (type = 'light') => ({ 16 | color: Colors, 17 | ...theme(type) 18 | }); 19 | const dark = _makeTheme('dark'); 20 | const light = _makeTheme('light'); 21 | 22 | export default class App extends React.PureComponent { 23 | apolloClient = null; 24 | state = { 25 | isLoadingComplete: false, 26 | hasHydrated: false, 27 | theme: 'light' 28 | }; 29 | 30 | toggleTheme = () => { 31 | this.setState( 32 | ({ theme }) => ({ 33 | theme: theme === 'light' ? 'dark' : 'light' 34 | }), 35 | this._changeStatusBarStyle 36 | ); 37 | }; 38 | 39 | _changeStatusBarStyle = () => { 40 | StatusBar.setBarStyle( 41 | this.state.theme === 'light' ? 'default' : 'light-content' 42 | ); 43 | }; 44 | 45 | componentDidMount() { 46 | return Platform.OS === 'ios' && this._changeStatusBarStyle(); 47 | } 48 | 49 | render() { 50 | const { isLoadingComplete, hasHydrated } = this.state; 51 | 52 | if (!isLoadingComplete && !hasHydrated) { 53 | return ( 54 | 59 | ); 60 | } else { 61 | return ( 62 | 63 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | ); 74 | } 75 | } 76 | 77 | _loadResourcesAsync = async () => { 78 | this.apolloClient = await setupApolloClient(); 79 | 80 | return Promise.all([ 81 | Asset.loadAsync([ 82 | require('./assets/images/buymeacoffee-logo.png'), 83 | require('./assets/images/youtube-logo.png'), 84 | require('./assets/images/brain.png') 85 | ]), 86 | Font.loadAsync({ 87 | // This is the font that we are using for our tab bar 88 | ...Icon.Ionicons.font, 89 | // We include SpaceMono because we use it in HomeScreen.js. Feel free 90 | // to remove this if you are not using it in your app 91 | 'space-mono': require('./assets/fonts/SpaceMono-Regular.ttf'), 92 | Menlo: require('./assets/fonts/Menlo-Regular.ttf') 93 | }) 94 | ]); 95 | }; 96 | 97 | _handleLoadingError = error => { 98 | // In this case, you might want to report the error to your error 99 | // reporting service, for example Sentry 100 | console.warn(error); 101 | }; 102 | 103 | _handleFinishLoading = () => { 104 | this.setState({ isLoadingComplete: true, hasHydrated: true }); 105 | }; 106 | } 107 | -------------------------------------------------------------------------------- /screens/settings/Settings.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | TouchableOpacity, 4 | StyleSheet, 5 | AsyncStorage, 6 | Switch, 7 | Image, 8 | Linking 9 | } from 'react-native'; 10 | import FullLoading from '../../components/FullLoading'; 11 | import { 12 | Body, 13 | Heading, 14 | Wrapper, 15 | RowAligned, 16 | Button, 17 | Badge, 18 | Scroll, 19 | Row, 20 | Spacer, 21 | Line 22 | } from '../../components/styled'; 23 | 24 | import { ThemeContext } from '../../config/theme'; 25 | 26 | const ProBadge = ({ isPro }) => ( 27 | 28 | 29 | {isPro ? 'PRO' : 'NORMAL'} 30 | 31 | 32 | ); 33 | 34 | export default class Settings extends React.PureComponent { 35 | static navigationOptions = { 36 | header: null 37 | }; 38 | 39 | _renderX = () => { 40 | const { me } = this.props.data; 41 | const { name, email, isPro } = me; 42 | return ( 43 | 44 | 45 | 46 | {name} 47 | 48 | 49 | 50 | {email} 51 | 56 | 57 | {({ toggleTheme, theme }) => ( 58 | 59 | 60 | Dark theme? 61 | 66 | 67 | 68 | )} 69 | 70 | 71 | 72 | 74 | this._openLink( 75 | 'https://www.youtube.com/playlist?list=PLQocKVqyqZDQrUU7zUfFogbAO0ynvQK2j' 76 | ) 77 | } 78 | > 79 | Watch the coding process 80 | 84 | 85 | 86 | 87 | ); 88 | }; 89 | 90 | _openLink = async url => { 91 | const supported = await Linking.canOpenURL(url); 92 | if (supported) { 93 | Linking.openURL(url); 94 | } else { 95 | console.log("Don't know how to open URI: " + url); 96 | } 97 | }; 98 | 99 | componentDidUpdate() { 100 | if (!this.props.data.loading && !this.props.data.me) { 101 | this._logout(); 102 | } 103 | } 104 | 105 | _logout = () => { 106 | new Promise.all([ 107 | AsyncStorage.clear(), 108 | this.props.client.cache.reset() 109 | ]).then(() => { 110 | this.props.navigation.navigate('Auth'); 111 | }); 112 | }; 113 | 114 | render() { 115 | if (this.props.data.loading && !this.props.data.me) { 116 | return ; 117 | } 118 | return {this._renderX()}; 119 | } 120 | } 121 | 122 | const styles = StyleSheet.create({ 123 | image: { 124 | width: 100, 125 | height: 60, 126 | resizeMode: 'contain', 127 | alignSelf: 'center' 128 | }, 129 | row: { 130 | alignItems: 'center', 131 | justifyContent: 'center', 132 | marginBottom: 20 133 | } 134 | }); 135 | -------------------------------------------------------------------------------- /config/setup.js: -------------------------------------------------------------------------------- 1 | import { AsyncStorage } from 'react-native'; 2 | import { ApolloClient, HttpLink, split, concat } from 'apollo-client-preset'; 3 | import { WebSocketLink } from 'apollo-link-ws'; 4 | import { setContext } from 'apollo-link-context'; 5 | import { InMemoryCache } from 'apollo-cache-inmemory'; 6 | import { persistCache } from 'apollo-cache-persist'; 7 | import { getMainDefinition } from 'apollo-utilities'; 8 | import { RetryLink } from 'apollo-link-retry'; 9 | import { USER_ACCESS_TOKEN } from '../constants/auth'; 10 | 11 | const officeURI = '192.168.0.115:4000'; 12 | // const officeURI = '10.105.99.218:4000'; 13 | // const officeURI = '10.5.5.161:4000'; 14 | // const officeURI = '127.0.0.1:4000'; 15 | const isDev = __DEV__; 16 | const URI = isDev ? officeURI : 'api.uzual.app'; 17 | 18 | let prodUri = { 19 | socket: `wss://api.uzual.app`, 20 | link: `https://api.uzual.app` 21 | }; 22 | 23 | let uri = !isDev 24 | ? { 25 | socket: `wss://api.uzual.app`, 26 | link: `https://api.uzual.app` 27 | } 28 | : { 29 | socket: `ws://${URI}`, 30 | link: `http://${URI}` 31 | }; 32 | 33 | const tok = 34 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjanYzbDN2angwMXZ0MDczN2xtaG1xMG5oIiwiaWF0IjoxNTU2NjE5ODU5fQ.jMXCbuT3-WcheHpH0aXr8pUtAyd_yrZUOwvMXbEq8_M'; 35 | 36 | export default async function setupApolloClient() { 37 | // await AsyncStorage.setItem(USER_ACCESS_TOKEN, tok); 38 | const wsLink = new WebSocketLink({ 39 | uri: !isDev ? prodUri.socket : uri.socket, 40 | options: { 41 | reconnect: true, 42 | connectionParams: () => { 43 | return new Promise(async resolve => { 44 | const token = await AsyncStorage.getItem(USER_ACCESS_TOKEN); 45 | resolve({ 46 | Authorization: token ? `Bearer ${token}` : null 47 | }); 48 | }); 49 | } 50 | } 51 | }); 52 | 53 | const httpLink = new HttpLink({ 54 | uri: !isDev ? prodUri.link : uri.link 55 | }); 56 | 57 | const authMiddleware = setContext( 58 | (_, { headers }) => 59 | new Promise(async resolve => { 60 | // get the authentication token from local storage if it exists 61 | const token = await AsyncStorage.getItem(USER_ACCESS_TOKEN); 62 | resolve({ 63 | headers: { 64 | ...headers, 65 | authorization: token ? `Bearer ${token}` : null 66 | } 67 | }); 68 | }) 69 | ); 70 | 71 | const retryLink = new RetryLink({ 72 | delay: { 73 | initial: 500, 74 | max: Infinity 75 | }, 76 | attempts: { 77 | max: Infinity, 78 | retryIf: (error, _operation) => { 79 | if (error.message === 'Network request failed') { 80 | // if (_operation.operationName === 'createPost') { 81 | return true; 82 | // } 83 | } 84 | return false; 85 | } 86 | } 87 | }); 88 | 89 | const httpLinkWithAuth = authMiddleware.concat(httpLink); 90 | const wsLinkWithAuth = authMiddleware.concat(wsLink); 91 | 92 | const link = concat( 93 | retryLink, 94 | split( 95 | ({ query }) => { 96 | const { kind, operation } = getMainDefinition(query); 97 | return kind === 'OperationDefinition' && operation === 'subscription'; 98 | }, 99 | wsLinkWithAuth, 100 | httpLinkWithAuth 101 | ) 102 | ); 103 | 104 | const defaultOptions = { 105 | watchQuery: { 106 | fetchPolicy: 'cache-and-network', 107 | errorPolicy: 'all' 108 | }, 109 | query: { 110 | fetchPolicy: 'cache-and-network', 111 | errorPolicy: 'all' 112 | } 113 | // mutate: { 114 | // errorPolicy: 'all' 115 | // } 116 | }; 117 | 118 | // const cache = new InMemoryCache({ dataIdFromObject: o => o.id }); 119 | const cache = new InMemoryCache(); 120 | const client = new ApolloClient({ 121 | link, 122 | cache, 123 | connectToDevTools: isDev, 124 | defaultOptions 125 | }); 126 | 127 | await persistCache({ 128 | cache, 129 | storage: AsyncStorage, 130 | debug: isDev 131 | }); 132 | 133 | return client; 134 | } 135 | -------------------------------------------------------------------------------- /screens/login/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image, AsyncStorage } from 'react-native'; 3 | import { USER_ACCESS_TOKEN } from '../../constants/auth'; 4 | import FullLoading from '../../components/FullLoading'; 5 | import { 6 | Body, 7 | Heading, 8 | Wrapper, 9 | LoginWrapper, 10 | Input, 11 | Button, 12 | Scroll, 13 | Spacer 14 | } from '../../components/styled'; 15 | 16 | export default class Login extends React.Component { 17 | state = { 18 | email: '', 19 | password: '', 20 | isLoading: false, 21 | error: null 22 | }; 23 | 24 | _change = (type, value) => { 25 | this.setState({ 26 | [type]: value, 27 | error: null 28 | }); 29 | }; 30 | 31 | _getErrorMessage = () => { 32 | const { email, password } = this.state; 33 | if (!email && !password) { 34 | return 'Fields are mandatory'; 35 | } 36 | if (!email) { 37 | return 'Email is missing'; 38 | } 39 | if (!password) { 40 | return 'Password is missing'; 41 | } 42 | }; 43 | 44 | _goToRegister = () => { 45 | this.props.navigation.navigate('Register'); 46 | }; 47 | 48 | _login = async () => { 49 | console.log('login'); 50 | const { email, password } = this.state; 51 | if (!email || !password) { 52 | const error = this._getErrorMessage(); 53 | return this.setState({ 54 | error 55 | }); 56 | } 57 | await this.setState({ 58 | isLoading: true 59 | }); 60 | 61 | try { 62 | const { data } = await this.props.login({ 63 | variables: { 64 | email, 65 | password 66 | } 67 | }); 68 | if (data && data.login && data.login.token) { 69 | await AsyncStorage.setItem(USER_ACCESS_TOKEN, data.login.token); 70 | return this.props.navigation.navigate('App'); 71 | } 72 | } catch (err) { 73 | this.setState({ 74 | isLoading: false, 75 | error: 76 | err.graphQLErrors.length > 0 77 | ? err.graphQLErrors[0].message 78 | : 'Something went wrong.' 79 | }); 80 | } 81 | }; 82 | 83 | render() { 84 | return ( 85 | 93 | 94 | 95 | 104 | 105 | UZUAL 106 | 107 | 108 | Feed your brain with habits for a better mood 109 | 110 | 111 | Email 112 | 113 | this._change('email', e.toLowerCase())} 117 | /> 118 | 119 | Password 120 | 121 | this._change('password', e)} 125 | /> 126 | {this.state.error && {this.state.error}} 127 | 132 | 133 | 134 | OR 135 | 136 | 137 | 142 | 143 | {this.state.isLoading && } 144 | 145 | 146 | ); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /screens/register/Register.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image, AsyncStorage } from 'react-native'; 3 | import { USER_ACCESS_TOKEN } from '../../constants/auth'; 4 | import FullLoading from '../../components/FullLoading'; 5 | import { 6 | Body, 7 | Heading, 8 | Wrapper, 9 | LoginWrapper, 10 | Input, 11 | Button, 12 | Scroll, 13 | Spacer 14 | } from '../../components/styled'; 15 | 16 | export default class Register extends React.Component { 17 | state = { 18 | email: '', 19 | password: '', 20 | name: '', 21 | isLoading: false, 22 | error: null 23 | }; 24 | 25 | _change = (type, value) => { 26 | this.setState({ 27 | [type]: value, 28 | error: null 29 | }); 30 | }; 31 | 32 | _getErrorMessage = () => { 33 | const { email, password } = this.state; 34 | if (!email && !password) { 35 | return 'Fields are mandatory'; 36 | } 37 | if (!email) { 38 | return 'Email is missing'; 39 | } 40 | if (!password) { 41 | return 'Password is missing'; 42 | } 43 | }; 44 | 45 | _register = async () => { 46 | console.log('register'); 47 | const { email, password, name } = this.state; 48 | if (!email || !password) { 49 | const error = this._getErrorMessage(); 50 | return this.setState({ 51 | error 52 | }); 53 | } 54 | await this.setState({ 55 | isLoading: true 56 | }); 57 | 58 | try { 59 | const { data } = await this.props.register({ 60 | variables: { 61 | email, 62 | password, 63 | name 64 | } 65 | }); 66 | if (data && data.register && data.register.token) { 67 | await AsyncStorage.setItem(USER_ACCESS_TOKEN, data.register.token); 68 | return this.props.navigation.navigate('App'); 69 | } 70 | } catch (err) { 71 | this.setState({ 72 | isLoading: false, 73 | error: 74 | err.graphQLErrors.length > 0 75 | ? err.graphQLErrors[0].message 76 | : 'Something went wrong.' 77 | }); 78 | } 79 | }; 80 | 81 | _goToLogin = () => { 82 | this.props.navigation.goBack(); 83 | }; 84 | 85 | render() { 86 | return ( 87 | 95 | 96 | 97 | 106 | 107 | UZUAL 108 | 109 | 110 | Feed your brain with habits for a better mood 111 | 112 | 113 | Name 114 | 115 | this._change('name', e)} 118 | /> 119 | 120 | Email 121 | 122 | this._change('email', e.toLowerCase())} 126 | /> 127 | 128 | Password 129 | 130 | this._change('password', e)} 134 | /> 135 | {this.state.error && {this.state.error}} 136 | 141 | 142 | 143 | OR 144 | 145 | 146 | 151 | 152 | {this.state.isLoading && } 153 | 154 | 155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /components/styled.js: -------------------------------------------------------------------------------- 1 | import { 2 | TouchableOpacity, 3 | KeyboardAvoidingView, 4 | Platform, 5 | StatusBar, 6 | FlatList 7 | } from 'react-native'; 8 | import styled from 'styled-components/native'; 9 | import styledMap, { mapToTheme as theme } from 'styled-map'; 10 | import SafeAreaView from 'react-native-safe-area-view'; 11 | 12 | const topSpacing = Platform.OS === 'android' ? StatusBar.currentHeight : 0; 13 | 14 | export const Row = styled.View` 15 | flex-direction: row; 16 | flex-wrap: ${props => (props.wrap ? 'wrap' : 'nowrap')}; 17 | `; 18 | 19 | export const Line = styled.View` 20 | height: 2px; 21 | background-color: ${props => props.theme.colors.shadow}; 22 | width: 200px; 23 | margin: 20px 0; 24 | `; 25 | 26 | export const RowAligned = styled(Row)` 27 | align-items: ${theme('flexAlign')} 28 | justify-content: ${theme('flexAlign')} 29 | `; 30 | 31 | export const Block = styled.View` 32 | margin-bottom: ${theme('spacing')}; 33 | `; 34 | 35 | export const Spacer = styled(Block)` 36 | margin-top: ${theme('spacing')}; 37 | `; 38 | 39 | export const Wrapper = styled.View` 40 | flex: 1; 41 | align-items: ${theme('flexAlign')}; 42 | justify-content: ${theme('flexAlign')}; 43 | padding-horizontal: ${theme('spacing')}; 44 | background-color: ${props => props.theme.colors.bg}; 45 | `; 46 | 47 | export const Bg = styled.View` 48 | flex: 1; 49 | background-color: ${props => props.theme.colors.bg}; 50 | `.withComponent(SafeAreaView); 51 | 52 | export const LoginWrapper = styled(Wrapper)` 53 | padding-horizontal: 0; 54 | width: 260px; 55 | `.withComponent(KeyboardAvoidingView); 56 | 57 | export const FullScreenWrapper = styled(Wrapper)` 58 | position: absolute; 59 | top: 0 60 | left: 0 61 | right: 0 62 | bottom: 0 63 | background-color: ${props => props.theme.colors.loadingBg} 64 | `; 65 | 66 | export const Scroll = styled.ScrollView` 67 | flex: 1 68 | background-color: ${props => props.theme.colors.bg}; 69 | `; 70 | 71 | export const Flat = styled(Scroll)` 72 | flex: 1 73 | background-color: ${props => props.theme.colors.bg}; 74 | padding-horizontal: ${theme('spacing')}; 75 | `.withComponent(FlatList); 76 | 77 | export const Body = styled.Text` 78 | font-size: ${theme('size')}; 79 | color: ${theme('colors')}; 80 | margin-bottom: ${theme('spacing')} 81 | margin-top: ${props => props.marginTop || 0} 82 | opacity: ${props => (props.faded ? 0.6 : 1)}; 83 | font-family: 'space-mono'; 84 | text-align: ${theme('textAlign')}; 85 | align-self: ${theme('flexAlign')} 86 | margin-right: ${props => 87 | props.marginRight ? props.theme.spacing.default : 0} 88 | `; 89 | 90 | export const Input = styled.TextInput` 91 | height: ${props => props.theme.height.input}; 92 | border-bottom-color: ${props => props.theme.colors.shadow}; 93 | border-bottom-width: 1px; 94 | margin-bottom: ${theme('spacing')}; 95 | font-family: 'space-mono'; 96 | color: ${theme('colors')} 97 | width: 260px; 98 | text-align-vertical: top; 99 | `; 100 | 101 | export const Badge = styled.View` 102 | background-color: ${theme('colors')} 103 | border-radius: ${props => props.theme.layout.badgeRadius} 104 | padding: 0 4px; 105 | `; 106 | 107 | export const Heading = styled(Body)` 108 | font-family: 'Menlo'; 109 | `; 110 | export const Button = styled.TouchableOpacity` 111 | align-items: center 112 | justify-content: center 113 | background-color: ${theme('colors')} 114 | height: ${theme('button')}; 115 | width: ${props => props.width || props.theme.layout.width} 116 | margin-top: ${theme('spacing')}; 117 | margin-right: ${props => 118 | props.marginRight ? props.theme.spacing.default : 0} 119 | `; 120 | 121 | export const HabitSquare = styled.View` 122 | height: ${props => props.theme.layout.habitSquareSize} 123 | width: ${props => props.theme.layout.habitSquareSize} 124 | margin-right: 1 125 | margin-bottom: 1 126 | align-items: center 127 | justify-content: center 128 | backgroundColor: ${props => 129 | props.done ? props.theme.colors.primary : props.theme.colors.shadow} 130 | opacity: ${props => (props.disabled ? 0.3 : 1)} 131 | `; 132 | 133 | const Shadow = styled.View` 134 | shadow-color: ${props => props.theme.colors.shadow}; 135 | shadow-offset: 0px 0px; 136 | shadow-radius: 4px; 137 | `; 138 | export const FabButton = styled(Shadow)` 139 | width: ${props => props.theme.layout.fabButtonSize} 140 | height: ${props => props.theme.layout.fabButtonSize} 141 | border-radius: ${props => props.theme.layout.fabButtonSize / 2} 142 | background-color: ${props => props.theme.colors.primary} 143 | position: absolute 144 | right: ${theme('spacing')} 145 | bottom: ${theme('spacing')} 146 | align-items: center; 147 | justify-content: center; 148 | `.withComponent(TouchableOpacity); 149 | 150 | export const Header = styled.View` 151 | height: ${props => props.theme.height.header}; 152 | align-items: center; 153 | flex-direction: row; 154 | justify-content: space-between; 155 | padding-horizontal: ${theme('spacing')}; 156 | `; 157 | -------------------------------------------------------------------------------- /dddddd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | background 5 | 6 | 7 | 8 | Layer 1 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Would you like to support me? 2 | 3 | Buy Me A Coffee 4 | 5 | 6 | 7 | # UZUAL 8 | 9 | Feed your brains with habits for a better mood 10 | 11 | ## Light Theme 12 | 13 |

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |

23 | 24 | ## Dark Theme 25 | 26 |

27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |

36 | 37 | ```graphql 38 | mutation createUser { 39 | signup( 40 | email: "mironcatalin@gmail.com" 41 | name: "Catalin Miron" 42 | password: "password" 43 | ) { 44 | token 45 | } 46 | } 47 | 48 | mutation signin { 49 | login(email: "mironcatalin@gmail.com", password: "password") { 50 | token 51 | } 52 | } 53 | 54 | # mutation addHabit{ 55 | # addHabit(title:"1 Coffee / day", description:"Just one coffee and see how it feels"){ 56 | # id 57 | # } 58 | # } 59 | 60 | # mutation addDailyHabit{ 61 | # addDailyHabit(habitId:"cjutuwgbu000t0765vjdje6n5", done: true, date: "2019-04-09"){ 62 | # id 63 | # } 64 | # } 65 | 66 | query me { 67 | me { 68 | name 69 | email 70 | pushToken 71 | id 72 | isPro 73 | } 74 | 75 | moods(first: 5, orderBy: date_DESC) { 76 | id 77 | type 78 | date 79 | } 80 | habits(first: 5) { 81 | title 82 | description 83 | starred 84 | habits(first: 5, orderBy: date_DESC) { 85 | id 86 | date 87 | done 88 | } 89 | } 90 | } 91 | 92 | query getHabits { 93 | habits { 94 | id 95 | title 96 | description 97 | habits { 98 | id 99 | done 100 | date 101 | } 102 | } 103 | } 104 | 105 | query myMoods { 106 | moods( 107 | where: { date_gte: "2019-03-01", date_lte: "2019-03-30" } 108 | orderBy: date_ASC 109 | ) { 110 | id 111 | type 112 | date 113 | } 114 | } 115 | 116 | mutation setMood { 117 | setMood(date: "2019-04-23", type: Frown) { 118 | id 119 | } 120 | } 121 | 122 | mutation setDailyHabit { 123 | setDailyHabit( 124 | id: "cjuxtixuk0066073847pvhos9" 125 | done: false 126 | date: "2019-04-01" 127 | ) { 128 | done 129 | } 130 | } 131 | 132 | //For changing the date for the current month + refreshing 133 | 134 | this.props.data.stopPolling(); 135 | await this.props.data.refetch({ 136 | start, 137 | end 138 | }); 139 | this.props.data.startPolling(5000); 140 | ``` 141 | 142 | ### Assets + Copyrights for them 143 | 144 | 145 | 146 | --- 147 | 148 |
Icons made by Skyclick from www.flaticon.com is licensed by CC 3.0 BY
149 | -------------------------------------------------------------------------------- /navigation/AppNavigator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform, StyleSheet, AsyncStorage } from 'react-native'; 3 | 4 | import { 5 | createStackNavigator, 6 | createBottomTabNavigator, 7 | createSwitchNavigator, 8 | createAppContainer 9 | } from 'react-navigation'; 10 | 11 | import FullLoading from '../components/FullLoading'; 12 | 13 | // Auth 14 | import Login from '../screens/login/Container'; 15 | import Register from '../screens/register/Container'; 16 | import Habits from '../screens/habits/Container'; 17 | import CreateHabit from '../screens/createHabit/Container'; 18 | import Mood from '../screens/mood/Container'; 19 | import Settings from '../screens/settings/Container'; 20 | import { USER_ACCESS_TOKEN } from '../constants/auth'; 21 | import TabBarIcon from './TabBarIcon'; 22 | import Colors from '../constants/Colors'; 23 | import ThemedTabBar from './ThemedTabBar'; 24 | 25 | const fade = props => { 26 | const { position, scene } = props; 27 | 28 | const index = scene.index; 29 | 30 | const translateX = 0; 31 | const translateY = 0; 32 | 33 | const opacity = position.interpolate({ 34 | inputRange: [index - 0.7, index, index + 0.7], 35 | outputRange: [0.3, 1, 0.3] 36 | }); 37 | 38 | return { 39 | opacity, 40 | transform: [{ translateX }, { translateY }] 41 | }; 42 | }; 43 | 44 | const HabitsStack = createStackNavigator( 45 | { 46 | Habits: { 47 | screen: Habits 48 | } 49 | }, 50 | { 51 | headerMode: 'none', 52 | navigationOptions: { 53 | tabBarLabel: 'Habits', 54 | tabBarIcon: ({ focused }) => ( 55 | 59 | ) 60 | } 61 | } 62 | ); 63 | 64 | const MoodStack = createStackNavigator( 65 | { 66 | Mood: { screen: Mood } 67 | }, 68 | { 69 | headerMode: 'none', 70 | navigationOptions: ({ navigation }) => { 71 | const navParams = navigation.state.routes[0].params; 72 | const tabBarVisible = navParams ? navParams.tabBarVisible : true; 73 | 74 | return { 75 | tabBarVisible, 76 | tabBarLabel: 'Mood', 77 | tabBarIcon: ({ focused }) => ( 78 | 82 | ) 83 | }; 84 | } 85 | } 86 | ); 87 | 88 | const SettingsStack = createStackNavigator( 89 | { 90 | Settings: { screen: Settings } 91 | }, 92 | { 93 | headerMode: 'none', 94 | navigationOptions: { 95 | tabBarLabel: 'Settings', 96 | tabBarIcon: ({ focused }) => ( 97 | 101 | ) 102 | } 103 | } 104 | ); 105 | 106 | const AppTabs = createBottomTabNavigator( 107 | { 108 | HabitsTab: { screen: HabitsStack }, 109 | MoodTab: { screen: MoodStack }, 110 | SettingsTab: { screen: SettingsStack } 111 | }, 112 | { 113 | // initialRouteName: "MyTripsTab", 114 | initialRouteName: 'HabitsTab', 115 | order: ['HabitsTab', 'MoodTab', 'SettingsTab'], 116 | tabBarComponent: ThemedTabBar 117 | } 118 | ); 119 | 120 | const AppRoutes = createStackNavigator( 121 | { 122 | AppTabs: { screen: AppTabs }, 123 | CreateHabit: { screen: CreateHabit } 124 | }, 125 | { 126 | initialRouteName: 'AppTabs', 127 | headerMode: 'none' 128 | } 129 | ); 130 | 131 | const AuthStack = createStackNavigator( 132 | { 133 | Login: { screen: Login }, 134 | Register: { screen: Register } 135 | }, 136 | { 137 | headerMode: 'none' 138 | } 139 | ); 140 | 141 | class Switch extends React.PureComponent { 142 | constructor() { 143 | super(); 144 | 145 | this._bootstrapAsync(); 146 | } 147 | 148 | componentDidMount() { 149 | // LoginManager.logOut(); 150 | // Alert.alert("XXX", JSON.stringify(this.props.client.clearStore, null, 2)); 151 | // this.props.client.resetStore(); 152 | // this.props.navigation.navigate("Auth"); 153 | // this.props.client.clearStore(); 154 | } 155 | 156 | // Fetch the token from storage then navigate to our appropriate place 157 | _bootstrapAsync = async () => { 158 | const token = await AsyncStorage.getItem(USER_ACCESS_TOKEN); 159 | 160 | // This will switch to the App screen or Auth screen and this loading 161 | // screen will be unmounted and thrown away. 162 | this.props.navigation.navigate(token ? 'App' : 'Auth'); 163 | }; 164 | 165 | // Render any loading content that you like here 166 | render() { 167 | return ; 168 | } 169 | } 170 | 171 | const RootSwitch = createAppContainer( 172 | createSwitchNavigator( 173 | { 174 | Switch: Switch, 175 | Auth: AuthStack, 176 | App: AppRoutes 177 | }, 178 | { 179 | initialRouteName: 'Switch' 180 | } 181 | ) 182 | ); 183 | 184 | export default RootSwitch; 185 | 186 | // import React from 'react'; 187 | // import { createAppContainer, createSwitchNavigator } from 'react-navigation'; 188 | 189 | // import MainTabNavigator from './MainTabNavigator'; 190 | 191 | // export default createAppContainer(createSwitchNavigator({ 192 | // // You could add another route here for authentication. 193 | // // Read more at https://reactnavigation.org/docs/en/auth-flow.html 194 | // Main: MainTabNavigator, 195 | // })); 196 | -------------------------------------------------------------------------------- /screens/habits/Habits.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform, AsyncStorage } from 'react-native'; 3 | import * as Icon from '@expo/vector-icons'; 4 | import FullLoading from '../../components/FullLoading'; 5 | import { Body, Block, Flat, Scroll, FabButton } from '../../components/styled'; 6 | import Habit from '../../components/Habit'; 7 | import queries from './queries.gql'; 8 | import { start, end, current, TIME_FORMAT } from '../../utils/dayjs'; 9 | import { POLL_INTERVAL } from '../../constants/vars'; 10 | import { v4 as uuid } from 'uuid'; 11 | import { Notifications } from 'expo'; 12 | import Constants from 'expo-constants'; 13 | import * as Permissions from 'expo-permissions'; 14 | import { scheduleMoodReminders } from '../../utils/reminders'; 15 | import gql from 'graphql-tag'; 16 | 17 | export default class Home extends React.PureComponent { 18 | static navigationOptions = { 19 | header: null 20 | }; 21 | 22 | registerForPushNotificationsAsync = async () => { 23 | if (Constants.isDevice) { 24 | const { status: existingStatus } = await Permissions.getAsync( 25 | Permissions.NOTIFICATIONS 26 | ); 27 | let finalStatus = existingStatus; 28 | if (existingStatus !== 'granted') { 29 | const { status } = await Permissions.askAsync( 30 | Permissions.NOTIFICATIONS 31 | ); 32 | finalStatus = status; 33 | } 34 | if (finalStatus !== 'granted') { 35 | alert('Failed to get push token for push notification!'); 36 | return; 37 | } 38 | let token = await Notifications.getExpoPushTokenAsync(); 39 | this._onSetPushToken(token); 40 | } else { 41 | // alert('Must use physical device for Push Notifications'); 42 | } 43 | 44 | await scheduleMoodReminders(); 45 | }; 46 | 47 | _onFabPress = () => { 48 | this.props.navigation.navigate('CreateHabit'); 49 | }; 50 | 51 | _renderEmptyState = () => { 52 | return ( 53 | 61 | 62 | You have no habits :-( 63 | 64 | 65 | Hit the + button to add one! 66 | 67 | 68 | ); 69 | }; 70 | 71 | componentWillMount() { 72 | // this._logout(); 73 | } 74 | 75 | componentDidMount() { 76 | this.registerForPushNotificationsAsync(); 77 | } 78 | 79 | _logout = () => { 80 | new Promise.all([ 81 | AsyncStorage.clear(), 82 | this.props.client.cache.reset() 83 | ]).then(() => { 84 | this.props.navigation.navigate('Auth'); 85 | }); 86 | }; 87 | 88 | _onSetDailyHabit = async habit => { 89 | const date = current.format(TIME_FORMAT); 90 | const dayHabit = habit.habits.find(h => h.date.startsWith(date)); 91 | await this.props.data.stopPolling(); 92 | const { id: habitId } = habit; 93 | this.props.setDailyHabit({ 94 | variables: { 95 | habitId, 96 | date 97 | }, 98 | optimisticResponse: { 99 | __typename: 'Mutation', 100 | setDailyHabit: { 101 | __typename: 'DayHabit', 102 | id: dayHabit ? dayHabit.id : uuid(), 103 | date: dayHabit ? dayHabit.date : date, 104 | done: dayHabit ? !dayHabit.done : true 105 | } 106 | }, 107 | update: async (proxy, { data: { setDailyHabit } }) => { 108 | console.log('setDailyHabit: ', setDailyHabit); 109 | try { 110 | const data = await proxy.readQuery({ 111 | query: queries.habits, 112 | variables: { start, end } 113 | }); 114 | 115 | const clonedData = JSON.parse(JSON.stringify(data.habits)); 116 | const currentHabit = clonedData.find(habit => habit.id === habitId); 117 | const currentHabitIndex = clonedData.findIndex( 118 | habit => habit.id === habitId 119 | ); 120 | 121 | if (dayHabit) { 122 | currentHabit.habits.splice(-1, 1, setDailyHabit); 123 | } else { 124 | currentHabit.habits.push(setDailyHabit); 125 | } 126 | 127 | const newData = { 128 | ...data, 129 | habits: [ 130 | ...clonedData.slice(0, currentHabitIndex), 131 | currentHabit, 132 | ...clonedData.slice(currentHabitIndex + 1, data.length) 133 | ] 134 | }; 135 | 136 | await proxy.writeQuery({ 137 | query: queries.habits, 138 | variables: { start, end }, 139 | data: newData 140 | }); 141 | this.forceUpdate(); 142 | this.props.data.startPolling(POLL_INTERVAL); 143 | } catch (err) { 144 | console.error(err); 145 | } 146 | } 147 | }); 148 | }; 149 | 150 | _onSetPushToken = pushToken => { 151 | this.props.setPushToken({ 152 | variables: { 153 | pushToken 154 | } 155 | }); 156 | }; 157 | 158 | _onHabitPress = habit => { 159 | this.props.navigation.navigate('CreateHabit', { habit }); 160 | }; 161 | 162 | render() { 163 | const { loading, habits } = this.props.data; 164 | if (loading && !habits) { 165 | return ; 166 | } 167 | 168 | // if (!loading && !habits) { 169 | // return this._renderEmptyState() 170 | // } 171 | 172 | return ( 173 | 174 | {habits.length > 0 ? ( 175 | ( 178 | 183 | )} 184 | keyExtractor={item => item.id} 185 | /> 186 | ) : ( 187 | this._renderEmptyState() 188 | )} 189 | 190 | 196 | 197 | 198 | ); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /components/MoodGraph.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform, Dimensions } from 'react-native'; 3 | import { withTheme } from 'styled-components'; 4 | import * as d3Shape from 'd3-shape'; 5 | import * as d3Scale from 'd3-scale'; 6 | import dayjs from 'dayjs'; 7 | import Svg, { 8 | Defs, 9 | Stop, 10 | LinearGradient, 11 | Path, 12 | G, 13 | Line, 14 | Circle, 15 | Text, 16 | Symbol, 17 | Use, 18 | Rect, 19 | Pattern 20 | } from 'react-native-svg'; 21 | 22 | import { start, end, days, daysInMonth, current } from '../utils/dayjs'; 23 | import { icns, icons } from '../utils/icons'; 24 | const { width: screenWidth, height: screenHeight } = Dimensions.get('screen'); 25 | 26 | const createMoodData = (data = realData) => 27 | days.map(({ date: day }, i) => { 28 | // console.log(d.findIndex(a => a.startsWith(totalDays[i])) === i) 29 | const index = data.findIndex(a => a.date.startsWith(day)); 30 | const item = index !== -1 ? data[index] : null; 31 | return { 32 | x: item ? icns.indexOf(item.type) : 0, 33 | y: i + 1, 34 | mood: item ? icons[item.type] : icons['Dizzy'], 35 | time: item ? dayjs(item.date) : null, 36 | itemDay: dayjs(day) 37 | }; 38 | }); 39 | 40 | const daySpacer = 50; 41 | const height = Platform.select({ 42 | android: screenHeight * 0.75, 43 | ios: screenHeight * 0.8 44 | }); 45 | const ITEMS_PER_ROW = icns.length; 46 | // const cellSize = height / daysInMonth; 47 | const width = screenWidth * 0.8 + daySpacer; 48 | const cellSizeWidth = (width - daySpacer) / ITEMS_PER_ROW; 49 | const cellSizeHeight = height / daysInMonth; 50 | const xRange = icns.length * cellSizeWidth + cellSizeWidth / 2; 51 | 52 | const xScale = d3Scale 53 | .scaleLinear() 54 | .domain([0, icns.length]) 55 | .range([cellSizeWidth / 2, xRange]); 56 | 57 | const yScale = d3Scale 58 | .scaleTime() 59 | .domain([start, end]) 60 | .range([cellSizeHeight * 1.4, height + cellSizeHeight * 1.4]); 61 | 62 | var line = d3Shape 63 | .line() 64 | .defined(function(d) { 65 | return d.time !== null; 66 | }) 67 | .x(function(d, i) { 68 | return xScale(d.x); 69 | }) // set the x values for the line generator 70 | .y(function(d, i) { 71 | return yScale(d.time); 72 | // return i * cellSize; 73 | }); // set the y values for the line generator 74 | 75 | export default withTheme(({ moods, theme }) => { 76 | const moodData = createMoodData(moods); 77 | // TODO: Add a prettier UI! 78 | if (!moodData.length === 0) { 79 | return There's nothing to display.; 80 | } 81 | 82 | return ( 83 | 88 | 89 | 96 | 97 | 98 | 99 | 108 | 112 | 113 | 114 | {icns.map((n, index) => { 115 | const icon = icons[n]; 116 | const [w, h, _, __, path] = icon.icon; 117 | return [ 118 | 123 | 129 | , 130 | // 0.35 => cellSize / 2 (0.5) - 0.7/2 131 | // subtract the cellSize / 2 to align it to the middle 132 | // subtract half of the actual size 133 | // (cellSize * .7) / 2 => cellSize * .3 or 30% :p 134 | 141 | ]; 142 | })} 143 | {moodData.map((mood, index) => { 144 | const { iconName, time, x, y, itemDay } = mood; 145 | return ( 146 | 147 | 155 | {dayjs(itemDay).format('ddd DD')} 156 | 157 | {time && ( 158 | <> 159 | 165 | 171 | 172 | )} 173 | 183 | 184 | ); 185 | })} 186 | 192 | 199 | {icns.map((icon, index) => { 200 | return ( 201 | 211 | ); 212 | })} 213 | 214 | ); 215 | }); 216 | -------------------------------------------------------------------------------- /screens/createHabit/CreateHabit.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Alert, Switch } from 'react-native'; 3 | import FullLoading from '../../components/FullLoading'; 4 | import { 5 | Body, 6 | Heading, 7 | Wrapper, 8 | Row, 9 | Spacer, 10 | Scroll, 11 | Input, 12 | Button 13 | } from '../../components/styled'; 14 | import NavHeader from '../../components/NavHeader'; 15 | import { start, end } from '../../utils/dayjs'; 16 | import queries from '../habits/queries.gql'; 17 | import { v4 as uuid } from 'uuid'; 18 | 19 | export default class CreateHabit extends React.Component { 20 | // static navigationOptions = { 21 | // header: null 22 | // }; 23 | constructor(props) { 24 | super(props); 25 | 26 | const { navigation } = this.props; 27 | const habit = navigation.getParam('habit', { 28 | title: '', 29 | description: '', 30 | starred: false, 31 | id: null, 32 | habits: [] 33 | }); 34 | 35 | this.state = { 36 | ...habit, 37 | error: null, 38 | type: habit.id ? 'EDIT' : 'CREATE' 39 | }; 40 | } 41 | 42 | _change = (type, value) => { 43 | this.setState({ 44 | [type]: value, 45 | error: null 46 | }); 47 | }; 48 | 49 | _getErrorMessage = () => { 50 | const { title, description } = this.state; 51 | if (!title && !description) { 52 | return 'Fields are mandatory'; 53 | } 54 | if (!title) { 55 | return 'Title is missing'; 56 | } 57 | if (!description) { 58 | return 'Description is missing'; 59 | } 60 | }; 61 | 62 | _createHabit = () => { 63 | const { title, description, starred, id, habits } = this.state; 64 | const isEditMode = this._isEditMode(); 65 | console.log(isEditMode ? 'edit' : 'create', ' habit'); 66 | if (!title || !description) { 67 | const error = this._getErrorMessage(); 68 | return this.setState({ 69 | error 70 | }); 71 | } 72 | 73 | try { 74 | this.props.createHabit({ 75 | variables: { 76 | id: isEditMode ? id : '', 77 | title, 78 | description, 79 | starred, 80 | start, 81 | end 82 | }, 83 | optimisticResponse: { 84 | __typename: 'Mutation', 85 | createHabit: { 86 | __typename: 'Habit', 87 | id: isEditMode ? id : uuid(), 88 | title, 89 | description, 90 | starred, 91 | habits 92 | } 93 | }, 94 | update: (proxy, { data: { createHabit } }) => { 95 | try { 96 | const data = proxy.readQuery({ 97 | query: queries.habits, 98 | variables: { start, end } 99 | }); 100 | 101 | if (isEditMode) { 102 | const habitIndex = data.habits.findIndex( 103 | habit => habit.id === id 104 | ); 105 | data.habits.splice(habitIndex, 1, createHabit); 106 | } else { 107 | data.habits.push(createHabit); 108 | } 109 | proxy.writeQuery({ 110 | query: queries.habits, 111 | variables: { start, end }, 112 | data 113 | }); 114 | // Update the cache and return. This is because maybe the 115 | // user is offline and so the promise will never be resolved. 116 | this._goBack(); 117 | } catch (err) { 118 | console.error(err); 119 | } 120 | } 121 | }); 122 | } catch (err) { 123 | this.setState({ 124 | error: 125 | err.graphQLErrors.length > 0 126 | ? err.graphQLErrors[0].message 127 | : 'Something went wrong.' 128 | }); 129 | } 130 | }; 131 | 132 | _isEditMode = () => this.state.type === 'EDIT'; 133 | 134 | _goBack = () => this.props.navigation.goBack(); 135 | _onDeleteHabit = () => { 136 | Alert.alert( 137 | `Confirm habit deletion`, 138 | `You're about to delete the habit. There's no way to undo it. Think twice before acting.`, 139 | [ 140 | { 141 | text: 'Cancel', 142 | onPress: () => {}, 143 | style: 'cancel' 144 | }, 145 | { 146 | text: 'Delete habit', 147 | onPress: this._deleteHabit, 148 | style: 'destructive' 149 | } 150 | ] 151 | ); 152 | }; 153 | 154 | _deleteHabit = () => { 155 | const { id } = this.state; 156 | try { 157 | this.props.deleteHabit({ 158 | variables: { 159 | id 160 | }, 161 | optimisticResponse: { 162 | __typename: 'Mutation', 163 | deleteHabit: { 164 | __typename: 'Habit', 165 | id 166 | } 167 | }, 168 | update: (proxy, { data: { deleteHabit } }) => { 169 | try { 170 | const data = proxy.readQuery({ 171 | query: queries.habits, 172 | variables: { start, end } 173 | }); 174 | 175 | const habitIndex = data.habits.findIndex(habit => habit.id === id); 176 | 177 | const newData = [ 178 | ...data.habits.slice(0, habitIndex), 179 | ...data.habits.slice(habitIndex + 1, data.length) 180 | ]; 181 | 182 | proxy.writeQuery({ 183 | query: queries.habits, 184 | variables: { start, end }, 185 | data: { ...data, habits: newData } 186 | }); 187 | // Update the cache and return. This is because maybe the 188 | // user is offline and so the promise will never be resolved. 189 | this._goBack(); 190 | } catch (err) { 191 | console.error(err); 192 | } 193 | } 194 | }); 195 | } catch (err) { 196 | console.log(err); 197 | } 198 | }; 199 | 200 | _renderCreateHabitForm = () => { 201 | const isEditMode = this._isEditMode(); 202 | const heading = `${isEditMode ? 'EDIT' : 'CREATE'} HABIT`; 203 | const buttonText = `${isEditMode ? 'SAVE' : 'CREATE'} HABIT`; 204 | return ( 205 | 206 | 207 | {heading} 208 | 209 | 210 | Title 211 | 212 | this._change('title', e)} 215 | /> 216 | 217 | Description 218 | 219 | this._change('description', e)} 225 | /> 226 | 227 | Favorite Habit? 228 | 229 | this._change('starred', !this.state.starred)} 232 | trackColor={this.props.theme.colors.primary} 233 | /> 234 | 235 | {this.state.error && ( 236 | 237 | {this.state.error} 238 | 239 | )} 240 | 241 | 246 | {isEditMode && ( 247 | 252 | )} 253 | 254 | ); 255 | }; 256 | 257 | render() { 258 | if (this.props.createHabit.loading) { 259 | return ; 260 | } 261 | return ( 262 | 263 | 264 | {this._renderCreateHabitForm()} 265 | 266 | ); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /screens/mood/Mood.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Platform, 4 | Dimensions, 5 | Image, 6 | CameraRoll, 7 | Share, 8 | TouchableOpacity 9 | } from 'react-native'; 10 | import { v4 as uuid } from 'uuid'; 11 | import { captureRef as takeSnapshotAsync } from 'react-native-view-shot'; 12 | import * as Icon from '@expo/vector-icons'; 13 | import queries from './queries.gql'; 14 | import FullLoading from '../../components/FullLoading'; 15 | import * as Permissions from 'expo-permissions'; 16 | import { 17 | RowAligned, 18 | Body, 19 | Heading, 20 | Wrapper, 21 | Scroll, 22 | Button, 23 | Spacer 24 | } from '../../components/styled'; 25 | import MoodGraph from '../../components/MoodGraph'; 26 | import DayMood from '../../components/DayMood'; 27 | import { start, current, end, TIME_FORMAT } from '../../utils/dayjs'; 28 | import { POLL_INTERVAL } from '../../constants/vars'; 29 | const { width, height } = Dimensions.get('screen'); 30 | const hitSlop = { top: 10, left: 10, bottom: 10, right: 10 }; 31 | 32 | export default class Mood extends React.Component { 33 | static navigationOptions = { 34 | header: null 35 | }; 36 | moodGraphRef = React.createRef(); 37 | 38 | componentWillReceiveProps(nextProps) { 39 | if (nextProps.navigation.getParam('isLoading')) { 40 | setTimeout(() => { 41 | takeSnapshotAsync(this.moodGraphRef, { 42 | format: 'png', 43 | result: 'tmpfile', 44 | quality: 1 45 | }).then(result => { 46 | this.props.navigation.setParams({ 47 | isLoading: false, 48 | imageUri: result 49 | }); 50 | }); 51 | }, 0); 52 | // let saveResult = await CameraRoll.saveToCameraRoll(result, 'photo'); 53 | } 54 | } 55 | 56 | _renderHeader = () => { 57 | return ( 58 | 59 | 60 | MOODS 61 | 62 | 63 | 64 | 70 | 71 | 72 | 73 | ); 74 | }; 75 | 76 | _renderContent = () => { 77 | const { moods } = this.props.data; 78 | return ( 79 | <> 80 | 81 | {!this._hasDayMood() && ( 82 | 83 | )} 84 | 85 | ); 86 | }; 87 | 88 | _onSetMood = async type => { 89 | const date = current.format(TIME_FORMAT); 90 | await this.props.data.stopPolling(); 91 | this.props.setMood({ 92 | variables: { 93 | date, 94 | type 95 | }, 96 | optimisticResponse: { 97 | __typename: 'Mutation', 98 | setMood: { 99 | __typename: 'Mood', 100 | date, 101 | type, 102 | id: uuid() 103 | } 104 | }, 105 | update: (proxy, { data: { setMood } }) => { 106 | try { 107 | const data = proxy.readQuery({ 108 | query: queries.moods, 109 | variables: { start, end } 110 | }); 111 | 112 | const lastMood = data.moods.slice(-1); 113 | if (lastMood[0] && lastMood[0].date.startsWith(date)) { 114 | data.moods.splice(-1, 1, setMood); 115 | } else { 116 | data.moods.push(setMood); 117 | } 118 | 119 | proxy.writeQuery({ 120 | query: queries.moods, 121 | variables: { start, end }, 122 | data 123 | }); 124 | this.props.data.startPolling(POLL_INTERVAL); 125 | } catch (err) { 126 | console.error(err); 127 | } 128 | } 129 | }); 130 | }; 131 | 132 | _takeSnapshot = async () => { 133 | this.props.navigation.setParams({ tabBarVisible: false, isLoading: true }); 134 | }; 135 | 136 | _cancelSharing = () => { 137 | this.props.navigation.setParams({ 138 | isLoading: false, 139 | imageUri: null, 140 | tabBarVisible: true 141 | }); 142 | }; 143 | 144 | _onSaveMoodGraph = async () => { 145 | const { isLoading, imageUri } = this._getProps(); 146 | const permission = await Permissions.getAsync(Permissions.CAMERA_ROLL); 147 | if (permission.status !== 'granted') { 148 | const newPermission = await Permissions.askAsync(Permissions.CAMERA_ROLL); 149 | if (newPermission.status === 'granted') { 150 | await CameraRoll.saveToCameraRoll(imageUri, 'photo'); 151 | } 152 | } else { 153 | await CameraRoll.saveToCameraRoll(imageUri, 'photo'); 154 | } 155 | 156 | this._cancelSharing(); 157 | }; 158 | 159 | _onShareMoodGraph = async () => { 160 | try { 161 | const { imageUri } = this._getProps(); 162 | const result = await Share.share({ 163 | message: 164 | 'My Mood graph from @Uzualapp (https://uzual.app) make me feel better.', 165 | url: imageUri 166 | }); 167 | 168 | if (result.action === Share.sharedAction) { 169 | if (result.activityType) { 170 | // shared with activity type of result.activityType 171 | } else { 172 | this._cancelSharing(); 173 | } 174 | } else if (result.action === Share.dismissedAction) { 175 | // dismissed 176 | } 177 | } catch (error) { 178 | alert(error.message); 179 | } 180 | }; 181 | 182 | _getProps = () => { 183 | return { 184 | isLoading: this.props.navigation.getParam('isLoading', false), 185 | imageUri: this.props.navigation.getParam('imageUri', null) 186 | }; 187 | }; 188 | 189 | _renderX = () => { 190 | const { isLoading } = this._getProps(); 191 | 192 | return ( 193 | 201 | {!isLoading && this._renderHeader()} 202 | {/* 203 | From: {start.format(TIME_FORMAT)} - To:{current.format(TIME_FORMAT)} 204 | */} 205 | {this._renderContent()} 206 | {isLoading && this._renderCopyright()} 207 | 208 | ); 209 | }; 210 | 211 | _renderSnapshot = () => { 212 | const { imageUri } = this._getProps(); 213 | return ( 214 | 215 | 220 | 233 | 244 | 255 | 264 | 265 | 266 | ); 267 | }; 268 | 269 | _renderCopyright = () => { 270 | return ( 271 | 272 | Go get Uzual.app -> https://uzual.app 273 | 274 | ); 275 | }; 276 | 277 | render() { 278 | if (this.props.data.loading && !this.props.data.moods) { 279 | return ; 280 | } 281 | 282 | const { imageUri } = this._getProps(); 283 | if (imageUri) { 284 | return this._renderSnapshot(); 285 | } 286 | return {this._renderX()}; 287 | } 288 | 289 | _hasDayMood = () => { 290 | const { moods } = this.props.data; 291 | const lastMood = moods.slice(-1); 292 | 293 | if (lastMood.length === 0) { 294 | return false; 295 | } 296 | if (lastMood[0].date.startsWith(current.format(TIME_FORMAT))) { 297 | return true; 298 | } 299 | 300 | return false; 301 | }; 302 | } 303 | --------------------------------------------------------------------------------