Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 76 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,77 @@
module.exports = {
"extends": "airbnb"
};
extends: 'airbnb',
env: {
es6: true,
node: true,
commonjs: true,
browser: true,
},
};

module.exports = {
extends: ['airbnb'],
env: {
es6: true,
node: true,
commonjs: true,
browser: true,
},
settings: {
'import/resolver': {
node: {
moduleDirectory: ['node_modules', 'app/javascript/packs/twitter'],
},
},
},
plugins: ['react', 'import'],
rules: {
'function-paren-newline': 0,
'object-curly-newline': 0,
'key-spacing': 0,
indent: 0,
'no-multi-spaces': 0,
'no-debugger': 0,
'no-continue': 0,
quotes: [2, 'single'],
strict: [2, 'never'],
'template-curly-spacing': [0, 'always'],
'comma-dangle': [2, 'always-multiline'],
'max-len': 0,
'no-shadow': 0,
'arrow-body-style': 0,
'arrow-parens': [2, 'as-needed'],
'global-require': 0,
'no-unused-expressions': 0,
'no-confusing-arrow': 0,
'no-unused-vars': [2, { ignoreRestSiblings: true }],
'no-constant-condition': 0,
'import/no-dynamic-require': 0,
'import/no-extraneous-dependencies': 0,
'import/prefer-default-export': 0,
'import/no-named-as-default': 0,
'import/no-unresolved': ['error', { caseSensitive: false }],
'react/require-default-props': [
'error',
{ forbidDefaultForRequired: true },
],
'react/forbid-prop-types': 0,
'react/no-unused-prop-types': 0,
'react/no-typos': 0,
'react/default-props-match-prop-types': 0,
'react/prefer-stateless-function': 0,
'react/no-array-index-key': 0,
'react/jsx-uses-react': 2,
'react/jsx-uses-vars': 2,
'react/jsx-no-bind': 0,
'react/react-in-jsx-scope': 2,
'react/jsx-filename-extension': [2, { extensions: ['.js', '.jsx'] }],
'jsx-a11y/anchor-is-valid': [
2,
{
components: ['Link'],
specialLink: ['hrefLeft', 'hrefRight', 'to'],
aspects: ['noHref', 'invalidHref', 'preferButton'],
},
],
},
};
16 changes: 16 additions & 0 deletions app/controllers/api/v1/replies_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class Api::V1::RepliesController < ApplicationController
def index
end

def create
end

def show
end

def update
end

def destroycreate
end
end
18 changes: 18 additions & 0 deletions app/controllers/api/v1/tweets_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class Api::V1::TweetsController < ApplicationController
def index
tweets = Tweet.where(user_id: current_user.id)
render json: tweets
end

def create
end

def show
end

def update
end

def destroy
end
end
2 changes: 1 addition & 1 deletion app/javascript/packs/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
//
// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate
// layout file, like app/views/layouts/application.html.erb
import "bootstrap/dist/js/bootstrap";
import 'bootstrap/dist/js/bootstrap';
10 changes: 10 additions & 0 deletions app/javascript/packs/twitter/api/tweets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { get } from '../utils/fetch';
import endpoints from '../utils/apiEndpoints';

export const tweets = () => {
return new Promise((resolve, reject) => {
return get(endpoints.tweets(), {})
.then(response => resolve(response))
.catch(error => reject(error));
});
};
12 changes: 0 additions & 12 deletions app/javascript/packs/twitter/components/landingPage.jsx

This file was deleted.

27 changes: 27 additions & 0 deletions app/javascript/packs/twitter/components/tweetsList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { Component } from 'react';
import { tweets } from '../api/tweets';

class TweetsList extends Component {
constructor(props) {
super(props);
this.state = {
tweets: [],
};
}

componentDidMount() {
tweets().then(response => {
this.setState({ tweets: response });
});
}

render() {
const { tweets } = this.state;
return (
<div>
<h1>Tweets</h1>
</div>
);
}
}
export default TweetsList;
10 changes: 5 additions & 5 deletions app/javascript/packs/twitter/routes.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
import LandingPage from "./components/landingPage";
import React from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import TweetsList from './components/tweetsList';

const App = props => (
const App = () => (
<Router>
<div>
<Route exact path="/" component={LandingPage} />
<Route exact path="/" component={TweetsList} />
</div>
</Router>
);
Expand Down
8 changes: 8 additions & 0 deletions app/javascript/packs/twitter/utils/apiEndpoints.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const apiBaseRoute = process.env.API_BASE_URL || 'http://localhost:3000/api/v1';

export default {
//---------------------------------------------------------------------------

// /tweets GET
tweets: () => `${apiBaseRoute}/tweets`,
};
94 changes: 94 additions & 0 deletions app/javascript/packs/twitter/utils/fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { stringify } from 'qs';
import { curry, isEmpty } from 'lodash';
import { getToken } from './token';

const API_VERSION = 1;
const ACCEPT_HEADER = { Accept: `application/json; version=${API_VERSION}` };
const CONTENT_TYPE_HEADER = { 'Content-type': 'application/json' };

/**
* Wrapper for built-in `fetch` which adds the expected Authorization header to the
* request. If no access token is available, a rejected Promise will be returned.
*
* @param {string} url - The URL to fetch from
* @param {object} options - Standard options for fetch
* @returns {Promise}
*/
const authenticatedFetch = (url, options) => {
const accessToken = getToken();

if (!accessToken) {
return Promise.reject(
new Error(
`Attempting to access an API that requires authentication, but no auth token is available. URL=${url}`
)
);
}

const authedOpts = {
...options,
headers: {
Authorization: `Bearer ${accessToken}`,
...options.headers,
},
};

return fetch(url, authedOpts);
};

const makeApiUrl = (endpoint, query) => {
const queryString = stringify(query);
return `${endpoint}${queryString ? '?' : ''}${queryString}`;
};

const parseJSON = response => new Promise(resolve => response.json().then(json => resolve({
status: response.status,
ok: response.ok,
json,
})
)
);

const buildFetchArgs = ({ method, headers = {}, requestBody = {} }) => {
const fetchOpts = {
method,
headers: {
...ACCEPT_HEADER,
...CONTENT_TYPE_HEADER,
...headers,
},
};
return isEmpty(requestBody)
? fetchOpts
: { ...fetchOpts, body: JSON.stringify(requestBody) };
};

function execute(
method,
endpoint,
{ query, auth = false, headers = {}, requestBody = {} } = {}
) {
const url = makeApiUrl(endpoint, query);
const options = buildFetchArgs({ method, headers, requestBody });
const fetchFn = auth ? authenticatedFetch : fetch;
return new Promise((resolve, reject) => {
fetchFn(url, options)
.then(parseJSON)
.then(response => {
if (response.ok) {
return resolve(response.json);
}
// extract the error from the server's json
return reject(response.json);
})
.catch(error => reject(new Error(error)));
});
}

// These are the functions that are exported for use:
export { makeApiUrl };
export const get = curry(execute)('GET');
export const post = curry(execute)('POST');
export const patch = curry(execute)('PATCH');
export const put = curry(execute)('PUT');
// Any other functions exported above are only exported for test purposes.
37 changes: 37 additions & 0 deletions app/javascript/packs/twitter/utils/token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export const getToken = () => JSON.parse(localStorage.getItem('token'));

export const getRefreshToken = () => JSON.parse(localStorage.getItem('refreshToken'));

export const setAuthToken = ({ token, refreshToken }) => {
localStorage.setItem('token', JSON.stringify(token));
localStorage.setItem('refreshToken', JSON.stringify(refreshToken));
};

export const decodeToken = (token) => {
if (token) {
try {
const jwtDecode = require('jwt-decode');
return jwtDecode(token);
} catch (error) {
throw new Error('Invalid token');
}
}
return null;
};

export const isNotExpired = (token) => {
try {
const decodedToken = decodeToken(token);
if (!decodedToken) return false;
const date = decodedToken.exp ? decodedToken.exp : null;
// convert to millis
return new Date(date * 1000) > Date.now();
} catch (error) {
throw new Error(error);
}
};

export const removeAuthToken = () => {
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
};
7 changes: 7 additions & 0 deletions app/models/tweet.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Tweet < ApplicationRecord
belongs_to :user, class_name: "User", foreign_key: "user_id"
has_many :replies, class_name: "Tweet", foreign_key: "to_id"

validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 }
end
4 changes: 3 additions & 1 deletion app/views/pages/root.html.erb
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
<div id='root'></div>
<div class="container">
<div id='root'></div>
</div>
7 changes: 7 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,11 @@
devise_for :users, path: '', path_names: { sign_in: 'login', sign_out: 'logout', sign_up: 'register', edit: 'settings' }
get '/users', to: 'users#index'
root to: "pages#root"
namespace :api, defaults: { format: :json } do
namespace :v1 do
resources :tweets do
resources :replies
end
end
end
end
6 changes: 3 additions & 3 deletions config/webpack/development.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
process.env.NODE_ENV = process.env.NODE_ENV || 'development'
process.env.NODE_ENV = process.env.NODE_ENV || 'development';

const environment = require('./environment')
const environment = require('./environment');

module.exports = environment.toWebpackConfig()
module.exports = environment.toWebpackConfig();
Loading