├── .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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### Would you like to support me?
2 |
3 |
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 |
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 |
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 |
--------------------------------------------------------------------------------