diff --git a/.eslintrc.js b/.eslintrc.js
index 4ee202f..28608f0 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,3 +1,77 @@
module.exports = {
- "extends": "airbnb"
-};
\ No newline at end of file
+ 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'],
+ },
+ ],
+ },
+};
diff --git a/app/controllers/api/v1/replies_controller.rb b/app/controllers/api/v1/replies_controller.rb
new file mode 100644
index 0000000..e5ed7d4
--- /dev/null
+++ b/app/controllers/api/v1/replies_controller.rb
@@ -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
\ No newline at end of file
diff --git a/app/controllers/api/v1/tweets_controller.rb b/app/controllers/api/v1/tweets_controller.rb
new file mode 100644
index 0000000..620a2d4
--- /dev/null
+++ b/app/controllers/api/v1/tweets_controller.rb
@@ -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
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index c69edb7..0450155 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -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';
diff --git a/app/javascript/packs/twitter/api/tweets.js b/app/javascript/packs/twitter/api/tweets.js
new file mode 100644
index 0000000..2bc8d3a
--- /dev/null
+++ b/app/javascript/packs/twitter/api/tweets.js
@@ -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));
+ });
+};
diff --git a/app/javascript/packs/twitter/components/landingPage.jsx b/app/javascript/packs/twitter/components/landingPage.jsx
deleted file mode 100644
index 734e332..0000000
--- a/app/javascript/packs/twitter/components/landingPage.jsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import React, { Component } from "react";
-
-class LandingPage extends Component {
- render() {
- return (
-
-
Hello World
-
- );
- }
-}
-export default LandingPage;
diff --git a/app/javascript/packs/twitter/components/tweetsList.jsx b/app/javascript/packs/twitter/components/tweetsList.jsx
new file mode 100644
index 0000000..a294f4d
--- /dev/null
+++ b/app/javascript/packs/twitter/components/tweetsList.jsx
@@ -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 (
+
+
Tweets
+
+ );
+ }
+}
+export default TweetsList;
diff --git a/app/javascript/packs/twitter/routes.js b/app/javascript/packs/twitter/routes.js
index 188f5cd..16d76a0 100644
--- a/app/javascript/packs/twitter/routes.js
+++ b/app/javascript/packs/twitter/routes.js
@@ -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 = () => (
-
+
);
diff --git a/app/javascript/packs/twitter/utils/apiEndpoints.js b/app/javascript/packs/twitter/utils/apiEndpoints.js
new file mode 100644
index 0000000..5bda9c4
--- /dev/null
+++ b/app/javascript/packs/twitter/utils/apiEndpoints.js
@@ -0,0 +1,8 @@
+export const apiBaseRoute = process.env.API_BASE_URL || 'http://localhost:3000/api/v1';
+
+export default {
+ //---------------------------------------------------------------------------
+
+ // /tweets GET
+ tweets: () => `${apiBaseRoute}/tweets`,
+};
diff --git a/app/javascript/packs/twitter/utils/fetch.js b/app/javascript/packs/twitter/utils/fetch.js
new file mode 100644
index 0000000..7bf8ffa
--- /dev/null
+++ b/app/javascript/packs/twitter/utils/fetch.js
@@ -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.
diff --git a/app/javascript/packs/twitter/utils/token.js b/app/javascript/packs/twitter/utils/token.js
new file mode 100644
index 0000000..371b258
--- /dev/null
+++ b/app/javascript/packs/twitter/utils/token.js
@@ -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');
+};
diff --git a/app/models/tweet.rb b/app/models/tweet.rb
new file mode 100644
index 0000000..9bc77ea
--- /dev/null
+++ b/app/models/tweet.rb
@@ -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
diff --git a/app/views/pages/root.html.erb b/app/views/pages/root.html.erb
index a097762..042bb9c 100644
--- a/app/views/pages/root.html.erb
+++ b/app/views/pages/root.html.erb
@@ -1 +1,3 @@
-
+
diff --git a/config/routes.rb b/config/routes.rb
index 204b2da..4ef392e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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
diff --git a/config/webpack/development.js b/config/webpack/development.js
index c5edff9..89d35d9 100644
--- a/config/webpack/development.js
+++ b/config/webpack/development.js
@@ -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();
diff --git a/config/webpack/environment.js b/config/webpack/environment.js
index be7b30c..1ee35bb 100644
--- a/config/webpack/environment.js
+++ b/config/webpack/environment.js
@@ -1,19 +1,6 @@
const { environment } = require('@rails/webpacker');
const webpack = require('webpack');
-{
- [
- {
- test: /\.js(\.erb)?$/,
- exclude: /node_modules/,
- loader: 'babel-loader',
- options: {
- presets: [['env', { modules: false }]],
- },
- },
- ];
-}
-
// Add an additional plugin of your choosing : ProvidePlugin
environment.plugins.prepend(
'Provide',
@@ -26,11 +13,11 @@ environment.plugins.prepend(
}),
);
-environment.loaders.get('sass').use.splice(-1, 0, {
- loader: 'resolve-url-loader',
- options: {
- attempts: 1,
- },
-});
+// environment.loaders.get('sass').use.splice(-1, 0, {
+// loader: 'resolve-url-loader',
+// options: {
+// attempts: 1,
+// },
+// });
module.exports = environment;
diff --git a/config/webpack/production.js b/config/webpack/production.js
index be0f53a..7684a10 100644
--- a/config/webpack/production.js
+++ b/config/webpack/production.js
@@ -1,5 +1,5 @@
-process.env.NODE_ENV = process.env.NODE_ENV || 'production'
+process.env.NODE_ENV = process.env.NODE_ENV || 'production';
-const environment = require('./environment')
+const environment = require('./environment');
-module.exports = environment.toWebpackConfig()
+module.exports = environment.toWebpackConfig();
diff --git a/db/migrate/20180828000002_create_tweets.rb b/db/migrate/20180828000002_create_tweets.rb
new file mode 100644
index 0000000..08ad166
--- /dev/null
+++ b/db/migrate/20180828000002_create_tweets.rb
@@ -0,0 +1,11 @@
+class CreateTweets < ActiveRecord::Migration[5.2]
+ def change
+ create_table :tweets do |t|
+ t.integer :user_id
+ t.text :content
+ t.integer :to_id
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 16ceb5b..cf100c8 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,15 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2018_08_20_232436) do
+ActiveRecord::Schema.define(version: 2018_08_28_000002) do
+
+ create_table "tweets", force: :cascade do |t|
+ t.integer "user_id"
+ t.text "content"
+ t.integer "to_id"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
diff --git a/package.json b/package.json
index e1e688b..94856ec 100644
--- a/package.json
+++ b/package.json
@@ -6,8 +6,12 @@
"babel-preset-react": "^6.24.1",
"bootstrap": "^4.1.3",
"jquery": "^3.3.1",
+ "jwt-decode": "^2.2.0",
+ "lodash": "^4.17.10",
+ "lodash.curry": "^4.1.1",
"popper.js": "^1.14.4",
"prop-types": "^15.6.2",
+ "qs": "^6.5.2",
"react": "^16.4.2",
"react-dom": "^16.4.2",
"react-router": "^4.3.1",
diff --git a/spec/factories/tweets.rb b/spec/factories/tweets.rb
new file mode 100644
index 0000000..4f853bf
--- /dev/null
+++ b/spec/factories/tweets.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :tweet do
+ user_id 1
+ content "MyText"
+ to_id 1
+ end
+end
diff --git a/spec/models/tweet_spec.rb b/spec/models/tweet_spec.rb
new file mode 100644
index 0000000..9a94ce1
--- /dev/null
+++ b/spec/models/tweet_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe Tweet, type: :model do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/yarn.lock b/yarn.lock
index ad98bdf..b5b5e7b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3578,6 +3578,10 @@ jsx-ast-utils@^2.0.1:
dependencies:
array-includes "^3.0.3"
+jwt-decode@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
+
killable@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b"
@@ -3732,6 +3736,10 @@ lodash.clonedeep@^4.3.2:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+lodash.curry@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170"
+
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
@@ -5284,7 +5292,7 @@ qs@6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
-qs@~6.5.1, qs@~6.5.2:
+qs@^6.5.2, qs@~6.5.1, qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"