actions.hideAlert()}
31 | anchorOrigin={{
32 | vertical: "bottom",
33 | horizontal: "left",
34 | }}
35 | action={[
36 | action,
37 | actions.hideAlert()}
40 | >
41 |
42 | ,
43 | ]}
44 | />
45 | )
46 | }
47 |
48 | Alert.propTypes = {
49 | actions: PropTypes.object.isRequired,
50 | alert: PropTypes.object.isRequired,
51 | }
52 |
53 | export default connect(
54 | (state) => state,
55 | (dispatch) => ({ actions: bindActionCreators(alertActions, dispatch) }),
56 | )(Alert)
57 |
--------------------------------------------------------------------------------
/db/migrate/20170724021850_devise_create_users.rb:
--------------------------------------------------------------------------------
1 | class DeviseCreateUsers < ActiveRecord::Migration[5.1]
2 | def change
3 | create_table :users do |t|
4 | ## Database authenticatable
5 | t.string :email, null: false, default: ""
6 | t.string :encrypted_password, null: false, default: ""
7 |
8 | ## Recoverable
9 | t.string :reset_password_token
10 | t.datetime :reset_password_sent_at
11 |
12 | ## Rememberable
13 | t.datetime :remember_created_at
14 |
15 | ## Trackable
16 | t.integer :sign_in_count, default: 0, null: false
17 | t.datetime :current_sign_in_at
18 | t.datetime :last_sign_in_at
19 | t.string :current_sign_in_ip
20 | t.string :last_sign_in_ip
21 |
22 | ## Confirmable
23 | # t.string :confirmation_token
24 | # t.datetime :confirmed_at
25 | # t.datetime :confirmation_sent_at
26 | # t.string :unconfirmed_email # Only if using reconfirmable
27 |
28 | ## Lockable
29 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
30 | # t.string :unlock_token # Only if unlock strategy is :email or :both
31 | # t.datetime :locked_at
32 |
33 |
34 | t.timestamps null: false
35 | end
36 |
37 | add_index :users, :email, unique: true
38 | add_index :users, :reset_password_token, unique: true
39 | # add_index :users, :confirmation_token, unique: true
40 | # add_index :users, :unlock_token, unique: true
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | ruby "2.3.3"
4 |
5 | git_source(:github) do |repo_name|
6 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
7 | "https://github.com/#{repo_name}.git"
8 | end
9 |
10 | gem "puma", "~> 3.7"
11 | gem "rails", "~> 5.1.1"
12 | gem "pg"
13 |
14 | gem "coffee-rails", "~> 4.2"
15 | gem "sass-rails", "~> 5.0"
16 | gem "uglifier", ">= 1.3.0"
17 |
18 | gem "webpacker"
19 |
20 | group :development, :test do
21 | gem "byebug", platforms: [:mri, :mingw, :x64_mingw]
22 | gem "pry-rails"
23 |
24 | gem "capybara", "~> 2.13"
25 | gem "selenium-webdriver"
26 |
27 | gem "factory_girl_rails"
28 | gem "faker"
29 | gem "figaro"
30 | end
31 |
32 | group :test do
33 | gem "database_cleaner"
34 | gem "rspec-rails"
35 | gem "shoulda-matchers", git: "https://github.com/thoughtbot/shoulda-matchers.git", branch: "rails-5"
36 | gem "simplecov"
37 | end
38 |
39 | group :development do
40 | gem "annotate"
41 | gem "graphiql-rails"
42 | gem "listen", ">= 3.0.5", "< 3.2"
43 | gem "spring"
44 | gem "spring-watcher-listen", "~> 2.0.0"
45 | gem "web-console", ">= 3.3.0"
46 |
47 | gem "guard", require: false
48 | gem "guard-bundler", require: false
49 | gem "guard-rspec", require: false
50 |
51 | gem "rubocop", require: false
52 | end
53 |
54 | group :development, :tddium_ignore, :darwin do
55 | gem "terminal-notifier-guard", require: false # OSX-specific notifications for guard
56 | end
57 |
58 | gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby]
59 |
60 | gem "carrierwave"
61 | gem "devise"
62 | gem "fog"
63 | gem "graphql"
64 | gem "jwt"
65 | gem "kaminari"
66 | gem "mini_magick"
67 |
--------------------------------------------------------------------------------
/spec/models/image_spec.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: images
4 | #
5 | # id :integer not null, primary key
6 | # imageable_id :integer
7 | # imageable_type :string
8 | # file :string
9 | # user_id :integer
10 | # created_at :datetime not null
11 | # updated_at :datetime not null
12 | #
13 | # Indexes
14 | #
15 | # index_images_on_imageable_id (imageable_id)
16 | # index_images_on_imageable_type (imageable_type)
17 | # index_images_on_user_id (user_id)
18 | #
19 |
20 | require 'rails_helper'
21 |
22 | RSpec.describe Image, type: :model do
23 | context "validations" do
24 | let(:user) { create(:user) }
25 | let!(:photo) { create(:photo, user: user) }
26 | let(:image) { build(:image) }
27 |
28 | it "valid for User" do
29 | image.imageable = user
30 | image.save
31 | expect(image.imageable).to eq(user)
32 | end
33 |
34 | it "valid for Photo" do
35 | image.imageable = photo
36 | image.save
37 | expect(image.imageable).to eq(photo)
38 | end
39 |
40 | context "prsence file and user" do
41 | let(:image) { build(:image, imageable: nil, file: nil, user: nil) }
42 |
43 | it "for User" do
44 | image.imageable_type = "User"
45 | image.valid?
46 | expect(image.errors.messages.keys).to_not include %w[user file]
47 | end
48 |
49 | it "for Photo" do
50 | image.imageable_type = "Photo"
51 | image.valid?
52 | expect(image.errors.messages.keys).to include :user
53 | expect(image.errors.messages.keys).to include :file
54 | end
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/app/javascript/packs/components/forms/RegisterForm.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Button } from "material-ui"
3 | import { Link } from "react-router-dom"
4 | import { Field, reduxForm, propTypes } from "redux-form"
5 | import { TextField } from "@gfpacheco/redux-form-material-ui"
6 |
7 | import {
8 | required,
9 | email,
10 | username,
11 | min,
12 | } from "./validations"
13 |
14 | const RegisterForm = ({ handleSubmit }) => {
15 | return (
16 |
55 | )
56 | }
57 |
58 | RegisterForm.propTypes = {
59 | ...propTypes,
60 | }
61 |
62 | export default reduxForm({
63 | form: "register",
64 | })(RegisterForm)
65 |
--------------------------------------------------------------------------------
/app/javascript/packs/pages/LoginPage.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import Card, { CardContent } from "material-ui/Card"
3 | import { graphql } from "react-apollo"
4 | import { bindActionCreators } from "redux"
5 | import { connect } from "react-redux"
6 | import PropTypes from "prop-types"
7 |
8 | import LoginForm from "../components/forms/LoginForm"
9 |
10 | import * as userActions from "../actions/user"
11 | import * as alertActions from "../actions/alert"
12 |
13 | import { LOGIN } from "../mutations"
14 |
15 | const mergedActions = Object.assign({}, userActions, alertActions)
16 |
17 | const styles = {
18 | container: {
19 | width: "40%",
20 | margin: "0 auto",
21 | },
22 | }
23 |
24 | class LoginPage extends React.Component {
25 | handleLogin = (values) => {
26 | const user = { email: "", password: "", ...values }
27 | this.props.mutate({ variables: { user } }).then(({ data }) => {
28 | this.props.actions.setUserByToken(data.login.auth_token)
29 | this.props.history.push("/")
30 | }).catch(({ message }) => {
31 | this.props.actions.showAlert(message)
32 | })
33 | }
34 | render() {
35 | return (
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | )
44 | }
45 | }
46 |
47 | LoginPage.propTypes = {
48 | actions: PropTypes.object.isRequired,
49 | mutate: PropTypes.func.isRequired,
50 | history: PropTypes.object.isRequired,
51 | }
52 |
53 | const Connected = connect(
54 | (state) => state,
55 | (dispatch) => ({ actions: bindActionCreators(mergedActions, dispatch) }),
56 | )(LoginPage)
57 |
58 | export default graphql(LOGIN)(Connected)
59 |
--------------------------------------------------------------------------------
/spec/factories/users.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: users
4 | #
5 | # id :integer not null, primary key
6 | # email :string default(""), not null
7 | # encrypted_password :string default(""), not null
8 | # reset_password_token :string
9 | # reset_password_sent_at :datetime
10 | # remember_created_at :datetime
11 | # sign_in_count :integer default(0), not null
12 | # current_sign_in_at :datetime
13 | # last_sign_in_at :datetime
14 | # current_sign_in_ip :string
15 | # last_sign_in_ip :string
16 | # created_at :datetime not null
17 | # updated_at :datetime not null
18 | # username :string
19 | # photos_count :integer default(0)
20 | # name :string
21 | # birthdate :string
22 | # caption :string
23 | # website :string
24 | # followings_count :integer default(0)
25 | # followers_count :integer default(0)
26 | # confirmation_token :string
27 | # confirmed_at :datetime
28 | # confirmation_sent_at :datetime
29 | # unconfirmed_email :string
30 | #
31 | # Indexes
32 | #
33 | # index_users_on_confirmation_token (confirmation_token) UNIQUE
34 | # index_users_on_email (email) UNIQUE
35 | # index_users_on_reset_password_token (reset_password_token) UNIQUE
36 | # index_users_on_username (username) UNIQUE
37 | #
38 |
39 | FactoryGirl.define do
40 | factory :user do
41 | sequence(:username) { |n| "coolguy#{n}" }
42 | name { Faker::Name.name }
43 | email { Faker::Internet.email }
44 | password "letmein123!"
45 | birthdate { Date.today.to_formatted_s(:db) }
46 | website "https://instaqram.com"
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/app/javascript/packs/pages/RegisterPage.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import Card, { CardContent } from "material-ui/Card"
3 | import { Typography } from "material-ui"
4 | import { graphql, withApollo } from "react-apollo"
5 | import PropTypes from "prop-types"
6 | import { connect } from "react-redux"
7 | import { bindActionCreators } from "redux"
8 |
9 | import RegisterForm from "../components/forms/RegisterForm"
10 |
11 | import * as userActions from "../actions/user"
12 | import * as alertActions from "../actions/alert"
13 |
14 | import { REGISTER } from "../mutations"
15 |
16 | const mergedActions = Object.assign({}, userActions, alertActions)
17 |
18 | const styles = {
19 | container: {
20 | width: "40%",
21 | margin: "0 auto",
22 | },
23 | }
24 |
25 | class RegisterPage extends React.Component {
26 | handleRegister = (values) => {
27 | this.props.mutate({ variables: { user: values } }).then(({ data }) => {
28 | this.props.actions.setUserByToken(data.register.auth_token)
29 | this.props.history.push("/")
30 | }).catch((error) => {
31 | this.props.actions.showAlert(error.message)
32 | })
33 | }
34 | render() {
35 | return (
36 |
37 |
38 |
39 | Register
40 |
41 |
42 |
43 |
44 | )
45 | }
46 | }
47 |
48 | RegisterPage.propTypes = {
49 | mutate: PropTypes.func.isRequired,
50 | actions: PropTypes.object.isRequired,
51 | history: PropTypes.object.isRequired,
52 | }
53 |
54 | const Connected = connect(
55 | (state) => state,
56 | (dispatch) => ({ actions: bindActionCreators(mergedActions, dispatch) }),
57 | )(RegisterPage)
58 |
59 | export default withApollo(graphql(REGISTER)(Connected))
60 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure public file server for tests with Cache-Control for performance.
16 | config.public_file_server.enabled = true
17 | config.public_file_server.headers = {
18 | 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}"
19 | }
20 |
21 | # Show full error reports and disable caching.
22 | config.consider_all_requests_local = true
23 | config.action_controller.perform_caching = false
24 |
25 | # Raise exceptions instead of rendering exception templates.
26 | config.action_dispatch.show_exceptions = false
27 |
28 | # Disable request forgery protection in test environment.
29 | config.action_controller.allow_forgery_protection = false
30 | config.action_mailer.perform_caching = false
31 |
32 | # Tell Action Mailer not to deliver emails to the real world.
33 | # The :test delivery method accumulates sent emails in the
34 | # ActionMailer::Base.deliveries array.
35 | config.action_mailer.delivery_method = :test
36 |
37 | # Print deprecation notices to the stderr.
38 | config.active_support.deprecation = :stderr
39 |
40 | # Raises error for missing translations
41 | # config.action_view.raise_on_missing_translations = true
42 | end
43 |
--------------------------------------------------------------------------------
/config/webpack/shared.js:
--------------------------------------------------------------------------------
1 | // Note: You must restart bin/webpack-dev-server for changes to take effect
2 |
3 | /* eslint global-require: 0 */
4 | /* eslint import/no-dynamic-require: 0 */
5 |
6 | const webpack = require("webpack")
7 | const { basename, dirname, join, relative, resolve } = require("path")
8 | const { sync } = require("glob")
9 | const ExtractTextPlugin = require("extract-text-webpack-plugin")
10 | const ManifestPlugin = require("webpack-manifest-plugin")
11 | const extname = require("path-complete-extname")
12 | const { env, settings, output, loadersDir } = require("./configuration.js")
13 |
14 | const extensionGlob = `**/*{${settings.extensions.join(",")}}*`
15 | const entryPath = join(settings.source_path, settings.source_entry_path)
16 | const packPaths = sync(join(entryPath, extensionGlob))
17 |
18 | module.exports = {
19 | // entry: packPaths.reduce(
20 | // (map, entry) => {
21 | // const localMap = map
22 | // const namespace = relative(join(entryPath), dirname(entry))
23 | // localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry)
24 | // return localMap
25 | // }, {}
26 | // ),
27 |
28 | output: {
29 | filename: "[name].js",
30 | path: output.path,
31 | publicPath: output.publicPath,
32 | },
33 |
34 | module: {
35 | rules: sync(join(loadersDir, "*.js")).map((loader) => require(loader)),
36 | },
37 |
38 | plugins: [
39 | new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))),
40 | new ExtractTextPlugin(env.NODE_ENV === "production" ? "[name]-[hash].css" : "[name].css"),
41 | new ManifestPlugin({
42 | publicPath: output.publicPath,
43 | writeToFileEmit: true,
44 | }),
45 | ],
46 |
47 | resolve: {
48 | extensions: settings.extensions,
49 | modules: [
50 | resolve(settings.source_path),
51 | "node_modules",
52 | ],
53 | },
54 |
55 | resolveLoader: {
56 | modules: ["node_modules"],
57 | },
58 | }
59 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/javascript/packs/pages/UsersPage.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import List, { ListItem, ListItemText } from "material-ui/List"
3 | import { withStyles, createStyleSheet } from "material-ui/styles"
4 | import { Typography, Paper, Avatar } from "material-ui"
5 | import { graphql } from "react-apollo"
6 | import { Link } from "react-router-dom"
7 | import pl from "pluralize"
8 | import PropTypes from "prop-types"
9 |
10 | import FollowButton from "../components/FollowButton"
11 |
12 | import { GET_USERS } from "../queries"
13 |
14 | const styleSheet = createStyleSheet("UsersPage", () => ({
15 | container: {
16 | width: "40%",
17 | margin: "0 auto",
18 | },
19 | title: {
20 | padding: "20px",
21 | },
22 | }))
23 |
24 | class UsersPage extends React.Component {
25 | render() {
26 | const { classes, data, history } = this.props
27 |
28 | if (data.loading) {
29 | return null
30 | }
31 |
32 | const users = data.users.map((user) => {
33 | const link = {user.username}
34 | return (
35 |
36 |
37 |
41 |
42 |
43 | )
44 | })
45 |
46 | return (
47 |
48 |
49 | Browse Users
50 | {users}
51 |
52 |
53 | )
54 | }
55 | }
56 |
57 | UsersPage.propTypes = {
58 | classes: PropTypes.object.isRequired,
59 | data: PropTypes.object.isRequired,
60 | history: PropTypes.object.isRequired,
61 | }
62 |
63 | const WithStyle = withStyles(styleSheet)(UsersPage)
64 |
65 | export default graphql(GET_USERS)(WithStyle)
66 |
--------------------------------------------------------------------------------
/app/javascript/packs/components/FollowButton.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Button } from "material-ui"
3 | import PropTypes from "prop-types"
4 | import { graphql } from "react-apollo"
5 | import { connect } from "react-redux"
6 |
7 | import { FOLLOW_USER } from "../mutations"
8 |
9 | const FollowButton = ({ user: { followed, id }, follow, currentUser, history }) => {
10 | const followText = followed ? "Unfollow" : "Follow"
11 | const followColor = followed ? "accent" : "primary"
12 | return (
13 |
25 | )
26 | }
27 |
28 | FollowButton.defaultProps = {
29 | currentUser: null,
30 | }
31 |
32 | FollowButton.propTypes = {
33 | user: PropTypes.shape({
34 | id: PropTypes.string.isRequired,
35 | followed: PropTypes.bool.isRequired,
36 | username: PropTypes.string.isRequired,
37 | }).isRequired,
38 | follow: PropTypes.func.isRequired,
39 | currentUser: PropTypes.object,
40 | history: PropTypes.object.isRequired,
41 | }
42 |
43 | const Connected = connect(
44 | (state) => state,
45 | )(FollowButton)
46 |
47 | export default graphql(FOLLOW_USER, {
48 | props: ({ ownProps, mutate }) => ({
49 | follow(userId) {
50 | mutate({
51 | variables: { user_id: userId },
52 | refetchQueries: [
53 | "feed",
54 | ],
55 | updateQueries: {
56 | users: (prev, { mutationResult: { data } }) => {
57 | return Object.assign({}, prev, {
58 | users: prev.users.map((u) => {
59 | if (u.id === ownProps.user.id) {
60 | return {
61 | ...u,
62 | followed: !!data.follow,
63 | }
64 | }
65 | return u
66 | }),
67 | })
68 | },
69 | },
70 | })
71 | },
72 | }),
73 | })(Connected)
74 |
--------------------------------------------------------------------------------
/Guardfile:
--------------------------------------------------------------------------------
1 | ignore %r{^ignored/path/}, /public/
2 | filter /\.txt$/, /.*\.zip/
3 |
4 | notification :terminal_notifier if `uname` =~ /Darwin/
5 |
6 | group :backend do
7 | guard :bundler do
8 | require "guard/bundler"
9 | require "guard/bundler/verify"
10 | helper = Guard::Bundler::Verify.new
11 |
12 | files = ["Gemfile"]
13 | files += Dir["*.gemspec"] if files.any? { |f| helper.uses_gemspec?(f) }
14 |
15 | # Assume files are symlinked from somewhere
16 | files.each { |file| watch(helper.real_path(file)) }
17 | end
18 |
19 | guard :rspec, cmd: "bundle exec rspec" do
20 | require "guard/rspec/dsl"
21 | dsl = Guard::RSpec::Dsl.new(self)
22 |
23 | # Feel free to open issues for suggestions and improvements
24 |
25 | # RSpec files
26 | rspec = dsl.rspec
27 | watch(rspec.spec_helper) { rspec.spec_dir }
28 | watch(rspec.spec_support) { rspec.spec_dir }
29 | watch(rspec.spec_files)
30 |
31 | # Ruby files
32 | ruby = dsl.ruby
33 | dsl.watch_spec_files_for(ruby.lib_files)
34 |
35 | # Rails files
36 | rails = dsl.rails(view_extensions: %w(erb haml slim))
37 | dsl.watch_spec_files_for(rails.app_files)
38 | dsl.watch_spec_files_for(rails.views)
39 |
40 | watch(rails.controllers) do |m|
41 | [
42 | rspec.spec.call("routing/#{m[1]}_routing"),
43 | rspec.spec.call("controllers/#{m[1]}_controller"),
44 | rspec.spec.call("acceptance/#{m[1]}")
45 | ]
46 | end
47 |
48 | # Rails config changes
49 | watch(rails.spec_helper) { rspec.spec_dir }
50 | watch(rails.routes) { "#{rspec.spec_dir}/routing" }
51 | watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
52 |
53 | # Capybara features specs
54 | watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
55 | watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }
56 |
57 | # Turnip features and steps
58 | watch(%r{^spec/acceptance/(.+)\.feature$})
59 | watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
60 | Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/app/javascript/packs/components/forms/EditProfileForm.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Field, reduxForm, propTypes } from "redux-form"
3 | import { TextField } from "@gfpacheco/redux-form-material-ui"
4 | import { connect } from "react-redux"
5 | import { Button } from "material-ui"
6 |
7 | const EditProfileForm = ({ handleSubmit }) => {
8 | return (
9 |
64 | )
65 | }
66 |
67 | EditProfileForm.propTypes = {
68 | ...propTypes,
69 | }
70 |
71 | const ReduxForm = reduxForm({
72 | form: "editProfile",
73 | })(EditProfileForm)
74 |
75 | export default connect(
76 | (state) => {
77 | const initialValues = Object.assign({}, state.currentUser)
78 | delete initialValues.id
79 | delete initialValues.image
80 | return {
81 | initialValues,
82 | }
83 | },
84 | )(ReduxForm)
85 |
--------------------------------------------------------------------------------
/app/javascript/packs/Routes.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Route, Switch } from "react-router-dom"
3 | import { connect } from "react-redux"
4 | import { bindActionCreators } from "redux"
5 | import PropTypes from "prop-types"
6 |
7 | import Header from "./components/Header"
8 | import Alert from "./components/Alert"
9 | import PrivateRoute from "./components/PrivateRoute"
10 |
11 | import HomePage from "./pages/HomePage"
12 | import RegisterPage from "./pages/RegisterPage"
13 | import LoginPage from "./pages/LoginPage"
14 | import ProfilePage from "./pages/ProfilePage"
15 | import PhotoPage from "./pages/PhotoPage"
16 | import UsersPage from "./pages/UsersPage"
17 |
18 | import * as userActions from "./actions/user"
19 |
20 | const styles = {
21 | container: {
22 | paddingTop: "90px",
23 | },
24 | }
25 |
26 | class Routes extends React.Component {
27 | componentWillMount() {
28 | const token = window.localStorage.getItem("auth_token")
29 | if (token) {
30 | this.props.actions.setUserByToken(token)
31 | }
32 | }
33 | render() {
34 | const { history, ConnectedRouter } = this.props
35 |
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | )
56 | }
57 | }
58 |
59 | Routes.propTypes = {
60 | history: PropTypes.object.isRequired,
61 | ConnectedRouter: PropTypes.func.isRequired,
62 | actions: PropTypes.object.isRequired,
63 | }
64 |
65 | export default connect(
66 | (state) => state,
67 | (dispatch) => ({ actions: bindActionCreators(userActions, dispatch) }),
68 | )(Routes)
69 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports.
13 | config.consider_all_requests_local = true
14 |
15 | # Enable/disable caching. By default caching is disabled.
16 | if Rails.root.join('tmp/caching-dev.txt').exist?
17 | config.action_controller.perform_caching = true
18 |
19 | config.cache_store = :memory_store
20 | config.public_file_server.headers = {
21 | 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}"
22 | }
23 | else
24 | config.action_controller.perform_caching = false
25 |
26 | config.cache_store = :null_store
27 | end
28 |
29 | # Don't care if the mailer can't send.
30 | config.action_mailer.raise_delivery_errors = false
31 |
32 | config.action_mailer.perform_caching = false
33 |
34 | # Print deprecation notices to the Rails logger.
35 | config.active_support.deprecation = :log
36 |
37 | # Raise an error on page load if there are pending migrations.
38 | config.active_record.migration_error = :page_load
39 |
40 | # Debug mode disables concatenation and preprocessing of assets.
41 | # This option may cause significant delays in view rendering with a large
42 | # number of complex assets.
43 | config.assets.debug = true
44 |
45 | # Suppress logger output for asset requests.
46 | config.assets.quiet = true
47 |
48 | # Raises error for missing translations
49 | # config.action_view.raise_on_missing_translations = true
50 |
51 | # Use an evented file watcher to asynchronously detect changes in source code,
52 | # routes, locales, etc. This feature depends on the listen gem.
53 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker
54 |
55 | # Devise mailer
56 | config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
57 | end
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "learn-graphql",
3 | "private": true,
4 | "engines": {
5 | "node": "8"
6 | },
7 | "dependencies": {
8 | "@gfpacheco/redux-form-material-ui": "v1.0.0-alpha.2",
9 | "autoprefixer": "^7.1.2",
10 | "babel-core": "^6.25.0",
11 | "babel-loader": "7.x",
12 | "babel-plugin-syntax-dynamic-import": "^6.18.0",
13 | "babel-plugin-transform-class-properties": "^6.24.1",
14 | "babel-polyfill": "^6.23.0",
15 | "babel-preset-env": "^1.6.0",
16 | "babel-preset-react": "^6.24.1",
17 | "coffee-loader": "^0.7.3",
18 | "coffee-script": "^1.12.7",
19 | "compression-webpack-plugin": "^1.0.0",
20 | "css-loader": "^0.28.4",
21 | "extract-text-webpack-plugin": "^3.0.0",
22 | "file-loader": "^0.11.2",
23 | "glob": "^7.1.2",
24 | "history": "^4.6.3",
25 | "js-yaml": "^3.9.0",
26 | "json-web-token": "^2.1.3",
27 | "material-ui": "next",
28 | "material-ui-icons": "next",
29 | "node-sass": "^4.5.3",
30 | "path-complete-extname": "^0.1.0",
31 | "pluralize": "^6.0.0",
32 | "postcss-loader": "^2.0.6",
33 | "postcss-smart-import": "^0.7.5",
34 | "precss": "^2.0.0",
35 | "prop-types": "^15.5.10",
36 | "rails-erb-loader": "^5.0.2",
37 | "react": "^15.6.1",
38 | "react-apollo": "^1.4.8",
39 | "react-dom": "^15.6.1",
40 | "react-dropzone": "^3.13.3",
41 | "react-hot-loader": "next",
42 | "react-redux": "^5.0.5",
43 | "react-router-dom": "^4.1.2",
44 | "react-router-redux": "next",
45 | "react-tap-event-plugin": "^2.0.1",
46 | "react-timeago": "^3.4.3",
47 | "redux": "^3.7.2",
48 | "redux-form": "^7.0.1",
49 | "redux-form-material-ui": "^4.2.0",
50 | "redux-thunk": "^2.2.0",
51 | "resolve-url-loader": "^2.1.0",
52 | "sass-loader": "^6.0.6",
53 | "style-loader": "^0.18.2",
54 | "webpack": "^3.3.0",
55 | "webpack-manifest-plugin": "^1.2.1",
56 | "webpack-merge": "^4.1.0"
57 | },
58 | "devDependencies": {
59 | "babel-eslint": "^7.2.3",
60 | "babel-preset-stage-0": "^6.24.1",
61 | "eslint": "3",
62 | "eslint-config-airbnb": "^15.1.0",
63 | "eslint-plugin-import": "^2.7.0",
64 | "eslint-plugin-jsx-a11y": "5",
65 | "eslint-plugin-react": "^7.1.0",
66 | "webpack-dev-server": "^2.6.1"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/javascript/packs/components/UpdateProfile.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import Dialog, {
3 | DialogContent,
4 | DialogTitle,
5 | DialogActions,
6 | } from "material-ui/Dialog"
7 | import { withStyles, createStyleSheet } from "material-ui/styles"
8 | import PropTypes from "prop-types"
9 | import { Button } from "material-ui"
10 | import { graphql } from "react-apollo"
11 | import { connect } from "react-redux"
12 | import { bindActionCreators } from "redux"
13 |
14 | import EditProfileForm from "./forms/EditProfileForm"
15 |
16 | import * as userActions from "../actions/user"
17 | import * as alertActions from "../actions/alert"
18 |
19 | import { UPDATE_PROFILE } from "../mutations"
20 |
21 | const mergedActions = Object.assign({}, userActions, alertActions)
22 |
23 | const styleSheet = createStyleSheet("UpdateProfile", () => ({
24 | paper: {
25 | width: "60%",
26 | },
27 | }))
28 |
29 | class UpdateProfile extends React.Component {
30 | handleUpdateProfile = (user) => {
31 | this.props.mutate({ variables: { user } }).then(({ data }) => {
32 | this.props.actions.showAlert("User updated")
33 | this.props.actions.setUserByToken(data.updateProfile.auth_token)
34 | }).catch((err) => {
35 | this.props.actions.showAlert(err.message)
36 | })
37 | }
38 | render() {
39 | const { open, close, classes } = this.props
40 | return (
41 |
50 | )
51 | }
52 | }
53 |
54 | UpdateProfile.defaultProps = {
55 | open: false,
56 | }
57 |
58 | UpdateProfile.propTypes = {
59 | classes: PropTypes.object.isRequired,
60 | open: PropTypes.bool,
61 | close: PropTypes.func.isRequired,
62 | actions: PropTypes.object.isRequired,
63 | mutate: PropTypes.func.isRequired,
64 | }
65 |
66 | const WithStyle = withStyles(styleSheet)(UpdateProfile)
67 |
68 | const Connected = connect(
69 | (state) => state,
70 | (dispatch) => ({ actions: bindActionCreators(mergedActions, dispatch) }),
71 | )(WithStyle)
72 |
73 | export default graphql(UPDATE_PROFILE)(Connected)
74 |
--------------------------------------------------------------------------------
/app/javascript/packs/components/forms/PostCommentForm.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { TextField } from "material-ui"
3 | import { graphql } from "react-apollo"
4 | import PropTypes from "prop-types"
5 | import { connect } from "react-redux"
6 |
7 | import { COMMENT_PHOTO } from "../../mutations"
8 |
9 | class PostCommentForm extends React.Component {
10 | constructor() {
11 | super()
12 |
13 | this.state = {
14 | content: "",
15 | }
16 | }
17 | submitComment = (event) => {
18 | if (event.key === "Enter") {
19 | event.preventDefault()
20 |
21 | const { content } = this.state
22 | if (content) {
23 | this.setState({ content: "" })
24 |
25 | this.props.submit(this.props.photo.id, content)
26 | }
27 | }
28 | }
29 | render() {
30 | if (!this.props.currentUser) {
31 | return null
32 | }
33 | return (
34 |
43 | )
44 | }
45 | }
46 |
47 | PostCommentForm.defaultProps = {
48 | currentUser: null,
49 | }
50 |
51 | PostCommentForm.propTypes = {
52 | photo: PropTypes.object.isRequired,
53 | submit: PropTypes.func.isRequired,
54 | currentUser: PropTypes.object,
55 | }
56 |
57 | const Connected = connect(
58 | (state) => state,
59 | )(PostCommentForm)
60 |
61 | export default graphql(COMMENT_PHOTO, {
62 | props: ({ ownProps, mutate }) => ({
63 | submit(photoId, content) {
64 | mutate({
65 | variables: { photo_id: photoId, content },
66 | updateQueries: {
67 | feed: (prev, { mutationResult: { data: commentPhoto } }) => {
68 | return Object.assign({}, prev, {
69 | ...prev,
70 | feed: prev.feed.map((p) => {
71 | if (p.id === ownProps.photo.id) {
72 | return {
73 | ...p,
74 | comments: [
75 | ...p.comments,
76 | commentPhoto.commentPhoto,
77 | ],
78 | }
79 | }
80 |
81 | return p
82 | }),
83 | })
84 | },
85 | },
86 | })
87 | },
88 | }),
89 | })(Connected)
90 |
--------------------------------------------------------------------------------
/lib/tasks/auto_annotate_models.rake:
--------------------------------------------------------------------------------
1 | # NOTE: only doing this in development as some production environments (Heroku)
2 | # NOTE: are sensitive to local FS writes, and besides -- it's just not proper
3 | # NOTE: to have a dev-mode tool do its thing in production.
4 | if Rails.env.development?
5 | require 'annotate'
6 | task :set_annotation_options do
7 | # You can override any of these by setting an environment variable of the
8 | # same name.
9 | Annotate.set_defaults(
10 | 'routes' => 'false',
11 | 'position_in_routes' => 'before',
12 | 'position_in_class' => 'before',
13 | 'position_in_test' => 'before',
14 | 'position_in_fixture' => 'before',
15 | 'position_in_factory' => 'before',
16 | 'position_in_serializer' => 'before',
17 | 'show_foreign_keys' => 'true',
18 | 'show_complete_foreign_keys' => 'false',
19 | 'show_indexes' => 'true',
20 | 'simple_indexes' => 'false',
21 | 'model_dir' => 'app/models',
22 | 'root_dir' => '',
23 | 'include_version' => 'false',
24 | 'require' => '',
25 | 'exclude_tests' => 'false',
26 | 'exclude_fixtures' => 'false',
27 | 'exclude_factories' => 'false',
28 | 'exclude_serializers' => 'false',
29 | 'exclude_scaffolds' => 'true',
30 | 'exclude_controllers' => 'true',
31 | 'exclude_helpers' => 'true',
32 | 'exclude_sti_subclasses' => 'false',
33 | 'ignore_model_sub_dir' => 'false',
34 | 'ignore_columns' => nil,
35 | 'ignore_routes' => nil,
36 | 'ignore_unknown_models' => 'false',
37 | 'hide_limit_column_types' => 'integer,boolean',
38 | 'hide_default_column_types' => 'json,jsonb,hstore',
39 | 'skip_on_db_migrate' => 'false',
40 | 'format_bare' => 'true',
41 | 'format_rdoc' => 'false',
42 | 'format_markdown' => 'false',
43 | 'sort' => 'false',
44 | 'force' => 'false',
45 | 'trace' => 'false',
46 | 'wrapper_open' => nil,
47 | 'wrapper_close' => nil,
48 | 'with_comment' => true
49 | )
50 | end
51 |
52 | Annotate.load_tasks
53 | end
54 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | threads threads_count, threads_count
9 |
10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
11 | #
12 | port ENV.fetch("PORT") { 3000 }
13 |
14 | # Specifies the `environment` that Puma will run in.
15 | #
16 | environment ENV.fetch("RAILS_ENV") { "development" }
17 |
18 | # Specifies the number of `workers` to boot in clustered mode.
19 | # Workers are forked webserver processes. If using threads and workers together
20 | # the concurrency of the application would be max `threads` * `workers`.
21 | # Workers do not work on JRuby or Windows (both of which do not support
22 | # processes).
23 | #
24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
25 |
26 | # Use the `preload_app!` method when specifying a `workers` number.
27 | # This directive tells Puma to first boot the application and load code
28 | # before forking the application. This takes advantage of Copy On Write
29 | # process behavior so workers use less memory. If you use this option
30 | # you need to make sure to reconnect any threads in the `on_worker_boot`
31 | # block.
32 | #
33 | # preload_app!
34 |
35 | # If you are preloading your application and using Active Record, it's
36 | # recommended that you close any connections to the database before workers
37 | # are forked to prevent connection leakage.
38 | #
39 | # before_fork do
40 | # ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
41 | # end
42 |
43 | # The code in the `on_worker_boot` will be called if you are using
44 | # clustered mode by specifying a number of `workers`. After each worker
45 | # process is booted, this block will be run. If you are using the `preload_app!`
46 | # option, you will want to use this block to reconnect to any threads
47 | # or connections that may have been created at application boot, as Ruby
48 | # cannot share connections between processes.
49 | #
50 | # on_worker_boot do
51 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
52 | # end
53 | #
54 |
55 | # Allow puma to be restarted by `rails restart` command.
56 | plugin :tmp_restart
57 |
--------------------------------------------------------------------------------
/app/javascript/packs/components/Love.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Favorite, FavoriteBorder } from "material-ui-icons"
3 | import { IconButton, Typography } from "material-ui"
4 | import { withStyles, createStyleSheet } from "material-ui/styles"
5 | import { graphql } from "react-apollo"
6 | import pl from "pluralize"
7 | import { connect } from "react-redux"
8 | import PropTypes from "prop-types"
9 |
10 | import { LIKE_PHOTO } from "../mutations"
11 |
12 | const styleSheet = createStyleSheet("LoveButton", () => ({
13 | container: {
14 | display: "flex",
15 | justifyContent: "flex-start",
16 | alignItems: "center",
17 | },
18 | }))
19 |
20 | const Love = ({ classes, photo, currentUser, likePhoto, history }) => (
21 |
22 | {
24 | if (currentUser) {
25 | likePhoto(photo.id)
26 | } else {
27 | history.push("/login")
28 | }
29 | }}
30 | >
31 | { photo.liked ? : }
32 |
33 | { photo.likes_count } {pl("like", photo.likes_count)}
34 |
35 | )
36 |
37 | Love.defaultProps = {
38 | history: null,
39 | currentUser: null,
40 | }
41 |
42 | Love.propTypes = {
43 | photo: PropTypes.shape({
44 | id: PropTypes.string.isRequired,
45 | liked: PropTypes.bool.isRequired,
46 | likes_count: PropTypes.number.isRequired,
47 | }).isRequired,
48 | classes: PropTypes.object.isRequired,
49 | likePhoto: PropTypes.func.isRequired,
50 | history: PropTypes.object,
51 | currentUser: PropTypes.object,
52 | }
53 |
54 | const WithStyle = withStyles(styleSheet)(Love)
55 | const Connected = connect(
56 | (state) => state,
57 | )(WithStyle)
58 |
59 | export default graphql(LIKE_PHOTO, {
60 | props: ({ ownProps, mutate }) => ({
61 | likePhoto: (photoId) => {
62 | mutate({
63 | variables: { photo_id: photoId },
64 | updateQueries: {
65 | feed: (prev, { mutationResult: { data: { likePhoto } } }) => {
66 | return Object.assign({}, prev, {
67 | feed: prev.feed.map((p) => {
68 | if (p.id === ownProps.photo.id) {
69 | return {
70 | ...p,
71 | likes_count: likePhoto.likes_count,
72 | liked: likePhoto.liked,
73 | }
74 | }
75 | return p
76 | }),
77 | })
78 | },
79 | },
80 | })
81 | },
82 | }),
83 | })(Connected)
84 |
--------------------------------------------------------------------------------
/app/javascript/packs/components/PhotoOpts.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 | import { IconButton } from "material-ui"
4 | import MoreVertIcon from "material-ui-icons/MoreVert"
5 | import Menu, { MenuItem } from "material-ui/Menu"
6 | import { graphql } from "react-apollo"
7 | import { connect } from "react-redux"
8 | import { bindActionCreators } from "redux"
9 |
10 | import { DELETE_PHOTO } from "../mutations"
11 |
12 | import * as alertActions from "../actions/alert"
13 |
14 | import { isShow } from "../utils/helpers"
15 |
16 | class PhotoOpts extends React.Component {
17 | constructor(props) {
18 | super(props)
19 |
20 | this.state = {
21 | open: false,
22 | target: null,
23 | }
24 | }
25 | openMenu = (event) => {
26 | this.setState({ open: true, target: event.currentTarget })
27 | }
28 | close = () => {
29 | this.setState({ open: false })
30 | }
31 | deletePhoto = () => {
32 | this.props.deletePhoto(this.props.photo.id).then(() => {
33 | this.props.history.push(`/users/${this.props.currentUser.username}`)
34 | this.props.actions.showAlert("Photo deleted.")
35 | })
36 | }
37 | render() {
38 | const { currentUser, photo } = this.props
39 | return (
40 |
41 |
42 |
43 |
44 |
60 |
61 | )
62 | }
63 | }
64 |
65 | PhotoOpts.propTypes = {
66 | photo: PropTypes.object.isRequired,
67 | deletePhoto: PropTypes.func.isRequired,
68 | history: PropTypes.object.isRequired,
69 | actions: PropTypes.shape({
70 | showAlert: PropTypes.func.isRequired,
71 | }).isRequired,
72 | currentUser: PropTypes.object.isRequired,
73 | }
74 |
75 | const Connected = connect(
76 | (state) => state,
77 | (dispatch) => ({ actions: bindActionCreators(alertActions, dispatch) }),
78 | )(PhotoOpts)
79 |
80 | export default graphql(DELETE_PHOTO, {
81 | props: ({ mutate }) => ({
82 | deletePhoto: (id) => {
83 | return mutate({
84 | variables: { id },
85 | updateQueries: {
86 | feed: (prev) => {
87 | return Object.assign({}, {
88 | ...prev,
89 | feed: prev.feed.filter((feed) => {
90 | return feed.id !== id
91 | }),
92 | })
93 | },
94 | },
95 | })
96 | },
97 | }),
98 | })(Connected)
99 |
--------------------------------------------------------------------------------
/app/javascript/packs/pages/HomePage.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { withStyles, createStyleSheet } from "material-ui/styles"
3 | import { graphql } from "react-apollo"
4 | import { Grid, Button } from "material-ui"
5 | import SyncIcon from "material-ui-icons/Sync"
6 | import PropTypes from "prop-types"
7 |
8 | import { GET_FEED } from "../queries"
9 |
10 | import PhotoCard from "../components/PhotoCard"
11 |
12 | const styleSheet = createStyleSheet("HomePage", (theme) => ({
13 | root: theme.mixins.gutters({
14 | padding: "20px",
15 | }),
16 | container: {
17 | width: "40%",
18 | margin: "0 auto",
19 | },
20 | center: {
21 | textAlign: "center",
22 | },
23 | }))
24 |
25 | class HomePage extends React.Component {
26 | constructor() {
27 | super()
28 |
29 | this.state = {
30 | page: 2,
31 | fetched: false,
32 | }
33 | }
34 | loadMore = () => {
35 | this.props.loadMore(this.state.page).then(() => {
36 | this.setState({ page: this.state.page + 1 })
37 | })
38 | }
39 | loadMoreShow() {
40 | const { loading } = this.props.data
41 | if (!loading) {
42 | return (
43 |
44 |
45 |
48 |
49 |
50 | )
51 | }
52 |
53 | return null
54 | }
55 | render() {
56 | const { classes, data } = this.props
57 |
58 | if (data.loading && data.networkStatus !== 3) {
59 | return null
60 | }
61 |
62 | const list = data.feed.map((photo) => )
63 | return (
64 |
65 | {list}
66 | {this.loadMoreShow()}
67 |
68 | )
69 | }
70 | }
71 |
72 | HomePage.propTypes = {
73 | classes: PropTypes.object.isRequired,
74 | data: PropTypes.shape({
75 | feed: PropTypes.array,
76 | loading: PropTypes.bool,
77 | }).isRequired,
78 | loadMore: PropTypes.func.isRequired,
79 | }
80 |
81 | const WithStyle = withStyles(styleSheet)(HomePage)
82 |
83 | export default graphql(GET_FEED, {
84 | props(props) {
85 | return {
86 | ...props,
87 | loadMore(page) {
88 | return props.data.fetchMore({
89 | variables: { page },
90 | updateQuery: (prev, { fetchMoreResult }) => {
91 | if (!fetchMoreResult.feed.length) {
92 | return prev
93 | }
94 | return Object.assign({}, prev, {
95 | feed: [
96 | ...prev.feed,
97 | ...fetchMoreResult.feed,
98 | ],
99 | })
100 | },
101 | })
102 | },
103 | }
104 | },
105 | })(WithStyle)
106 |
--------------------------------------------------------------------------------
/spec/models/user_spec.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: users
4 | #
5 | # id :integer not null, primary key
6 | # email :string default(""), not null
7 | # encrypted_password :string default(""), not null
8 | # reset_password_token :string
9 | # reset_password_sent_at :datetime
10 | # remember_created_at :datetime
11 | # sign_in_count :integer default(0), not null
12 | # current_sign_in_at :datetime
13 | # last_sign_in_at :datetime
14 | # current_sign_in_ip :string
15 | # last_sign_in_ip :string
16 | # created_at :datetime not null
17 | # updated_at :datetime not null
18 | # username :string
19 | # photos_count :integer default(0)
20 | # name :string
21 | # birthdate :string
22 | # caption :string
23 | # website :string
24 | # followings_count :integer default(0)
25 | # followers_count :integer default(0)
26 | # confirmation_token :string
27 | # confirmed_at :datetime
28 | # confirmation_sent_at :datetime
29 | # unconfirmed_email :string
30 | #
31 | # Indexes
32 | #
33 | # index_users_on_confirmation_token (confirmation_token) UNIQUE
34 | # index_users_on_email (email) UNIQUE
35 | # index_users_on_reset_password_token (reset_password_token) UNIQUE
36 | # index_users_on_username (username) UNIQUE
37 | #
38 |
39 | require 'rails_helper'
40 |
41 | RSpec.describe User, type: :model do
42 | describe "relations" do
43 | it "have image after create" do
44 | user = build(:user, image: nil)
45 | user.save
46 |
47 | expect(user.image).to_not be_nil
48 | end
49 | end
50 |
51 | context "validations" do
52 | ["dimasjt", "ry3a"].each do |u|
53 | it "should valid username #{u}" do
54 | user = create(:user, username: u)
55 | expect(user.errors.messages.key?(:username)).to be false
56 | end
57 | end
58 |
59 | it "invalid username" do
60 | user = build(:user, username: "dim*123$$")
61 | user.valid?
62 | expect(user.errors.messages.key?(:username)).to be true
63 | end
64 | end
65 |
66 | describe "#attribute_token" do
67 | let(:user) { create(:user) }
68 |
69 | it "has exposed attributes" do
70 | image = user.image.file
71 | attrs = {
72 | id: user.id,
73 | name: user.name,
74 | email: user.email,
75 | caption: user.caption,
76 | website: user.website,
77 | birthdate: user.birthdate,
78 | username: user.username,
79 | image: {
80 | thumb: image.thumb.url,
81 | small: image.small.url,
82 | medium: image.medium.url,
83 | large: image.large.url,
84 | original: image.url
85 | }
86 | }
87 | expect(user.attribute_token.deep_symbolize_keys).to eq attrs
88 | end
89 | end
90 |
91 | describe "#feed" do
92 | let!(:following) { create(:user) }
93 | let!(:user) { create(:user) }
94 |
95 | before { user.followings << following }
96 |
97 | it "should return following user photos" do
98 | create_list(:photo, 2, user: following)
99 | expect(user.feed).to eq following.photos.order(created_at: :desc)
100 | end
101 |
102 | it "should include own photos" do
103 | create_list(:photo, 2, user: user)
104 | expect(user.feed).to eq user.photos.order(created_at: :desc)
105 | end
106 | end
107 | end
108 |
--------------------------------------------------------------------------------
/app/models/user.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: users
4 | #
5 | # id :integer not null, primary key
6 | # email :string default(""), not null
7 | # encrypted_password :string default(""), not null
8 | # reset_password_token :string
9 | # reset_password_sent_at :datetime
10 | # remember_created_at :datetime
11 | # sign_in_count :integer default(0), not null
12 | # current_sign_in_at :datetime
13 | # last_sign_in_at :datetime
14 | # current_sign_in_ip :string
15 | # last_sign_in_ip :string
16 | # created_at :datetime not null
17 | # updated_at :datetime not null
18 | # username :string
19 | # photos_count :integer default(0)
20 | # name :string
21 | # birthdate :string
22 | # caption :string
23 | # website :string
24 | # followings_count :integer default(0)
25 | # followers_count :integer default(0)
26 | # confirmation_token :string
27 | # confirmed_at :datetime
28 | # confirmation_sent_at :datetime
29 | # unconfirmed_email :string
30 | #
31 | # Indexes
32 | #
33 | # index_users_on_confirmation_token (confirmation_token) UNIQUE
34 | # index_users_on_email (email) UNIQUE
35 | # index_users_on_reset_password_token (reset_password_token) UNIQUE
36 | # index_users_on_username (username) UNIQUE
37 | #
38 |
39 | class User < ApplicationRecord
40 | USERNAME_REGEX = /\A[a-zA-Z0-9]+\Z/
41 |
42 | devise :database_authenticatable, :registerable,
43 | :recoverable, :rememberable, :trackable, :validatable
44 |
45 | has_many :photos, dependent: :destroy
46 | has_many :likes, dependent: :destroy
47 | has_many :temp_images, dependent: :destroy, class_name: "Image"
48 |
49 | with_options class_name: "Followship" do |f|
50 | f.has_many :followers_references, foreign_key: "following_id"
51 | f.has_many :followings_references, foreign_key: "follower_id"
52 | end
53 |
54 | with_options class_name: "User" do |f|
55 | f.has_many :followers, through: :followers_references
56 | f.has_many :followings, through: :followings_references
57 | end
58 |
59 | has_one :image, as: :imageable
60 |
61 | validates :username, uniqueness: true, presence: true,
62 | format: { with: USERNAME_REGEX, message: "only number and letter allowed", allow_blank: true }
63 | validates :name, presence: true
64 |
65 | after_create :create_image!
66 |
67 | def self.secret_token
68 | "secrets"
69 | end
70 |
71 | def self.authenticate(token)
72 | decoded = JWT.decode(token, User.secret_token).try(:first)
73 | User.find(decoded["id"])
74 | rescue JWT::DecodeError
75 | nil
76 | end
77 |
78 | def self.exposed_attributes
79 | %w[id name email caption website birthdate username image]
80 | end
81 |
82 | def auth_token
83 | JWT.encode attribute_token, User.secret_token
84 | end
85 |
86 | def attribute_token
87 | Hash[*User.exposed_attributes.map do |a|
88 | [a, a.eql?("image") ? decorated_image : send(a)]
89 | end.flatten(1)]
90 | end
91 |
92 | def decorated_image
93 | Hash[*%w[thumb small medium large original].map do |v|
94 | url = if v == "original"
95 | image.file.url
96 | else
97 | image.file.send(v).url
98 | end
99 | [v, url]
100 | end.flatten]
101 | end
102 |
103 | def avatar
104 | image.nil? ? build_image : image
105 | end
106 |
107 | def feed
108 | Photo.where(user_id: following_ids.push(id)).order(created_at: :desc)
109 | end
110 | end
111 |
--------------------------------------------------------------------------------
/app/javascript/packs/components/PhotoCard.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import Card, { CardContent, CardMedia, CardHeader, CardActions } from "material-ui/Card"
3 | import { Typography, Avatar } from "material-ui"
4 | import { withStyles, createStyleSheet } from "material-ui/styles"
5 | import PropTypes from "prop-types"
6 | import { Link } from "react-router-dom"
7 | import TimeAgo from "react-timeago"
8 |
9 | import Comment from "./Comment"
10 | import PostCommentForm from "./forms/PostCommentForm"
11 | import Love from "./Love"
12 |
13 | import { isShow } from "../utils/helpers"
14 |
15 | const styleSheet = createStyleSheet("PhotoCard", () => ({
16 | root: {
17 | marginBottom: "15px",
18 | },
19 | block: {
20 | display: "block",
21 | width: "100%",
22 | },
23 | content: {
24 | paddingTop: 0,
25 | paddingBottom: "16px",
26 | },
27 | comments: {
28 | marginTop: "10px",
29 | marginBottom: "10px",
30 | },
31 | commentPost: {
32 | paddingTop: "10px",
33 | borderTop: "1px solid #cccccc",
34 | },
35 | mediaWrapper: {
36 | borderTop: "1px solid #efefef",
37 | borderBottom: "1px solid #efefef",
38 | },
39 | }))
40 |
41 | class PhotoCard extends React.Component {
42 | constructor() {
43 | super()
44 |
45 | this.state = { liked: false }
46 | }
47 | render() {
48 | const { classes, raised, onlyMedia, photo } = this.props
49 | const { user } = photo
50 |
51 | const avatar =
52 | const username = {user.username}
53 | const createdAt =
54 | const comments = photo.comments.map((comment) => {
55 | return (
56 |
60 | )
61 | })
62 |
63 | return (
64 |
65 | {isShow(
66 | ,
71 | !onlyMedia,
72 | )}
73 |
74 |
79 |
80 |
81 | {isShow(
82 |
83 |
84 |
85 |
86 |
87 |
88 | {photo.caption}
89 |
90 |
91 | {comments}
92 |
93 |
96 |
97 |
,
98 | !onlyMedia,
99 | )}
100 |
101 | )
102 | }
103 | }
104 |
105 | PhotoCard.defaultProps = {
106 | onlyMedia: false,
107 | raised: false,
108 | photo: {
109 | image: {
110 | original: "http://danielkitchensandbathrooms.co.uk/wp-content/uploads/2014/04/placeholder-840x630.png",
111 | medium: "http://danielkitchensandbathrooms.co.uk/wp-content/uploads/2014/04/placeholder-840x630.png",
112 | thumb: "http://danielkitchensandbathrooms.co.uk/wp-content/uploads/2014/04/placeholder-840x630.png",
113 | },
114 | },
115 | }
116 |
117 | PhotoCard.propTypes = {
118 | classes: PropTypes.object.isRequired,
119 | onlyMedia: PropTypes.bool,
120 | raised: PropTypes.bool,
121 | photo: PropTypes.object.isRequired,
122 | }
123 |
124 | export default withStyles(styleSheet)(PhotoCard)
125 |
--------------------------------------------------------------------------------
/app/javascript/packs/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Link } from "react-router-dom"
3 | import { AppBar, Toolbar, Typography, Button, Avatar, IconButton } from "material-ui"
4 | import { withStyles, createStyleSheet } from "material-ui/styles"
5 | import Menu, { MenuItem } from "material-ui/Menu"
6 | import { connect } from "react-redux"
7 | import { withApollo } from "react-apollo"
8 | import { bindActionCreators } from "redux"
9 | import PropTypes from "prop-types"
10 | import PhotoCameraIcon from "material-ui-icons/PhotoCamera"
11 |
12 | import Upload from "./Upload"
13 |
14 | import { logoutUser } from "../actions/user"
15 |
16 | const styleSheet = createStyleSheet("ButtonAppBar", (theme) => ({
17 | root: {
18 | marginTop: 30,
19 | width: "100%",
20 | },
21 | flex: {
22 | flex: 1,
23 | },
24 | row: {
25 | display: "flex",
26 | flexDirection: "row",
27 | },
28 | appBar: {
29 | backgroundColor: theme.light,
30 | },
31 | toolbarRoot: {
32 | paddingLeft: "140px",
33 | paddingRight: "140px",
34 | },
35 | brand: {
36 | color: theme.lighter,
37 | display: "flex",
38 | },
39 | white: {
40 | color: "#fff",
41 | },
42 | }))
43 |
44 | class Header extends React.Component {
45 | constructor() {
46 | super()
47 |
48 | this.state = { open: false, target: undefined }
49 | }
50 | logout = () => {
51 | this.close()
52 | this.props.client.resetStore()
53 | this.props.actions.logoutUser()
54 | }
55 | openMenu = (event) => {
56 | this.setState({ open: true, target: event.currentTarget })
57 | }
58 | close = () => {
59 | this.setState({ open: false })
60 | }
61 | render() {
62 | const { classes, currentUser } = this.props
63 | return (
64 |
65 |
66 |
67 |
68 |
69 |
70 | Instaqram
71 |
72 |
73 |
74 |
75 | {
76 | currentUser ? (
77 |
78 |
79 |
80 |
81 |
82 |
94 |
95 | ) : (
96 |
97 |
98 |
99 |
100 | )
101 | }
102 |
103 |
104 | )
105 | }
106 | }
107 |
108 | Header.defaultProps = {
109 | currentUser: null,
110 | }
111 |
112 | Header.propTypes = {
113 | classes: PropTypes.object.isRequired,
114 | currentUser: PropTypes.object,
115 | client: PropTypes.object.isRequired,
116 | actions: PropTypes.object.isRequired,
117 | }
118 |
119 | const WithStyle = withStyles(styleSheet)(Header)
120 | const Connected = connect(
121 | (state) => state,
122 | (dispatch) => ({ actions: bindActionCreators({ logoutUser }, dispatch) }),
123 | )(WithStyle)
124 |
125 | export default withApollo(Connected)
126 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Attempt to read encrypted secrets from `config/secrets.yml.enc`.
18 | # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or
19 | # `config/secrets.yml.key`.
20 | config.read_encrypted_secrets = true
21 |
22 | # Disable serving static files from the `/public` folder by default since
23 | # Apache or NGINX already handles this.
24 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
25 |
26 | # Compress JavaScripts and CSS.
27 | config.assets.js_compressor = :uglifier
28 | # config.assets.css_compressor = :sass
29 |
30 | # Do not fallback to assets pipeline if a precompiled asset is missed.
31 | config.assets.compile = false
32 |
33 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
34 |
35 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
36 | # config.action_controller.asset_host = 'http://assets.example.com'
37 |
38 | # Specifies the header that your server uses for sending files.
39 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
40 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
41 |
42 | # Mount Action Cable outside main process or domain
43 | # config.action_cable.mount_path = nil
44 | # config.action_cable.url = 'wss://example.com/cable'
45 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
46 |
47 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
48 | # config.force_ssl = true
49 |
50 | # Use the lowest log level to ensure availability of diagnostic information
51 | # when problems arise.
52 | config.log_level = :debug
53 |
54 | # Prepend all log lines with the following tags.
55 | config.log_tags = [ :request_id ]
56 |
57 | # Use a different cache store in production.
58 | # config.cache_store = :mem_cache_store
59 |
60 | # Use a real queuing backend for Active Job (and separate queues per environment)
61 | # config.active_job.queue_adapter = :resque
62 | # config.active_job.queue_name_prefix = "learn-graphql_#{Rails.env}"
63 | config.action_mailer.perform_caching = false
64 |
65 | # Ignore bad email addresses and do not raise email delivery errors.
66 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
67 | # config.action_mailer.raise_delivery_errors = false
68 |
69 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
70 | # the I18n.default_locale when a translation cannot be found).
71 | config.i18n.fallbacks = true
72 |
73 | # Send deprecation notices to registered listeners.
74 | config.active_support.deprecation = :notify
75 |
76 | # Use default logging formatter so that PID and timestamp are not suppressed.
77 | config.log_formatter = ::Logger::Formatter.new
78 |
79 | # Use a different logger for distributed setups.
80 | # require 'syslog/logger'
81 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
82 |
83 | if ENV["RAILS_LOG_TO_STDOUT"].present?
84 | logger = ActiveSupport::Logger.new(STDOUT)
85 | logger.formatter = config.log_formatter
86 | config.logger = ActiveSupport::TaggedLogging.new(logger)
87 | end
88 |
89 | # Do not dump schema after migrations.
90 | config.active_record.dump_schema_after_migration = false
91 | end
92 |
--------------------------------------------------------------------------------
/config/locales/devise.en.yml:
--------------------------------------------------------------------------------
1 | # Additional translations at https://github.com/plataformatec/devise/wiki/I18n
2 |
3 | en:
4 | devise:
5 | confirmations:
6 | confirmed: "Your email address has been successfully confirmed."
7 | send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
8 | send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
9 | failure:
10 | already_authenticated: "You are already signed in."
11 | inactive: "Your account is not activated yet."
12 | invalid: "Invalid %{authentication_keys} or password."
13 | locked: "Your account is locked."
14 | last_attempt: "You have one more attempt before your account is locked."
15 | not_found_in_database: "Invalid %{authentication_keys} or password."
16 | timeout: "Your session expired. Please sign in again to continue."
17 | unauthenticated: "You need to sign in or sign up before continuing."
18 | unconfirmed: "You have to confirm your email address before continuing."
19 | mailer:
20 | confirmation_instructions:
21 | subject: "Confirmation instructions"
22 | reset_password_instructions:
23 | subject: "Reset password instructions"
24 | unlock_instructions:
25 | subject: "Unlock instructions"
26 | email_changed:
27 | subject: "Email Changed"
28 | password_change:
29 | subject: "Password Changed"
30 | omniauth_callbacks:
31 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"."
32 | success: "Successfully authenticated from %{kind} account."
33 | passwords:
34 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
35 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
36 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
37 | updated: "Your password has been changed successfully. You are now signed in."
38 | updated_not_active: "Your password has been changed successfully."
39 | registrations:
40 | destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
41 | signed_up: "Welcome! You have signed up successfully."
42 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated."
43 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked."
44 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account."
45 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address."
46 | updated: "Your account has been updated successfully."
47 | sessions:
48 | signed_in: "Signed in successfully."
49 | signed_out: "Signed out successfully."
50 | already_signed_out: "Signed out successfully."
51 | unlocks:
52 | send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes."
53 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes."
54 | unlocked: "Your account has been unlocked successfully. Please sign in to continue."
55 | errors:
56 | messages:
57 | already_confirmed: "was already confirmed, please try signing in"
58 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one"
59 | expired: "has expired, please request a new one"
60 | not_found: "not found"
61 | not_locked: "was not locked"
62 | not_saved:
63 | one: "1 error prohibited this %{resource} from being saved:"
64 | other: "%{count} errors prohibited this %{resource} from being saved:"
65 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # Note that this schema.rb definition is the authoritative source for your
6 | # database schema. If you need to create the application database on another
7 | # system, you should be using db:schema:load, not running all the migrations
8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9 | # you'll amass, the slower it'll run and the greater likelihood for issues).
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema.define(version: 20170807133058) do
14 |
15 | # These are extensions that must be enabled in order to support this database
16 | enable_extension "plpgsql"
17 |
18 | create_table "comments", force: :cascade do |t|
19 | t.bigint "user_id"
20 | t.bigint "photo_id"
21 | t.text "content"
22 | t.datetime "created_at", null: false
23 | t.datetime "updated_at", null: false
24 | t.index ["photo_id"], name: "index_comments_on_photo_id"
25 | t.index ["user_id"], name: "index_comments_on_user_id"
26 | end
27 |
28 | create_table "followships", force: :cascade do |t|
29 | t.integer "follower_id"
30 | t.integer "following_id"
31 | t.datetime "created_at", null: false
32 | t.datetime "updated_at", null: false
33 | t.index ["follower_id", "following_id"], name: "index_followships_on_follower_id_and_following_id", unique: true
34 | t.index ["follower_id"], name: "index_followships_on_follower_id"
35 | t.index ["following_id"], name: "index_followships_on_following_id"
36 | end
37 |
38 | create_table "images", force: :cascade do |t|
39 | t.integer "imageable_id"
40 | t.string "imageable_type"
41 | t.string "file"
42 | t.bigint "user_id"
43 | t.datetime "created_at", null: false
44 | t.datetime "updated_at", null: false
45 | t.index ["imageable_id"], name: "index_images_on_imageable_id"
46 | t.index ["imageable_type"], name: "index_images_on_imageable_type"
47 | t.index ["user_id"], name: "index_images_on_user_id"
48 | end
49 |
50 | create_table "likes", force: :cascade do |t|
51 | t.bigint "user_id"
52 | t.bigint "photo_id"
53 | t.datetime "created_at", null: false
54 | t.datetime "updated_at", null: false
55 | t.index ["photo_id"], name: "index_likes_on_photo_id"
56 | t.index ["user_id"], name: "index_likes_on_user_id"
57 | end
58 |
59 | create_table "photos", force: :cascade do |t|
60 | t.text "caption"
61 | t.bigint "user_id"
62 | t.integer "comments_count", default: 0
63 | t.integer "likes_count", default: 0
64 | t.datetime "created_at", null: false
65 | t.datetime "updated_at", null: false
66 | t.index ["user_id"], name: "index_photos_on_user_id"
67 | end
68 |
69 | create_table "users", force: :cascade do |t|
70 | t.string "email", default: "", null: false
71 | t.string "encrypted_password", default: "", null: false
72 | t.string "reset_password_token"
73 | t.datetime "reset_password_sent_at"
74 | t.datetime "remember_created_at"
75 | t.integer "sign_in_count", default: 0, null: false
76 | t.datetime "current_sign_in_at"
77 | t.datetime "last_sign_in_at"
78 | t.string "current_sign_in_ip"
79 | t.string "last_sign_in_ip"
80 | t.datetime "created_at", null: false
81 | t.datetime "updated_at", null: false
82 | t.string "username"
83 | t.integer "photos_count", default: 0
84 | t.string "name"
85 | t.string "birthdate"
86 | t.string "caption"
87 | t.string "website"
88 | t.integer "followings_count", default: 0
89 | t.integer "followers_count", default: 0
90 | t.string "confirmation_token"
91 | t.datetime "confirmed_at"
92 | t.datetime "confirmation_sent_at"
93 | t.string "unconfirmed_email"
94 | t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
95 | t.index ["email"], name: "index_users_on_email", unique: true
96 | t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
97 | t.index ["username"], name: "index_users_on_username", unique: true
98 | end
99 |
100 | add_foreign_key "comments", "photos"
101 | add_foreign_key "comments", "users"
102 | add_foreign_key "likes", "photos"
103 | add_foreign_key "likes", "users"
104 | end
105 |
--------------------------------------------------------------------------------
/app/javascript/packs/pages/PhotoPage.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Grid, Paper, Avatar, Typography } from "material-ui"
3 | import { withStyles, createStyleSheet } from "material-ui/styles"
4 | import { graphql } from "react-apollo"
5 | import { connect } from "react-redux"
6 | import PropTypes from "prop-types"
7 |
8 | import Comment from "../components/Comment"
9 | import Love from "../components/Love"
10 | import PostCommentForm from "../components/forms/PostCommentForm"
11 | import PhotoOpts from "../components/PhotoOpts"
12 |
13 | import { linkFor } from "../utils/helpers"
14 |
15 | import { GET_PHOTO } from "../queries"
16 |
17 | const styleSheet = createStyleSheet("PhotoPage", () => ({
18 | container: {
19 | width: "80%",
20 | margin: "0 auto",
21 | },
22 | paper: {
23 | height: "480px",
24 | },
25 | wrapper: {
26 | height: "100%",
27 | },
28 | photoWrapper: {
29 | lineHeight: "480px",
30 | textAlign: "center",
31 | },
32 | image: {
33 | maxWidth: "100%",
34 | maxHeight: "100%",
35 | verticalAlign: "middle",
36 | },
37 | profile: {
38 | display: "flex",
39 | justifyContent: "flex-start",
40 | padding: "10px 0 10px 10px",
41 | borderBottom: "1px solid #ccc",
42 | alignItems: "center",
43 | marginRight: "16px",
44 | },
45 | username: {
46 | marginLeft: "20px",
47 | },
48 | content: {
49 | padding: "8px 0 8px 16px",
50 | },
51 | details: {
52 | margin: "10px 0",
53 | padding: "10px 0",
54 | overflow: "auto",
55 | height: "60%",
56 | },
57 | caption: {
58 | paddingBottom: "16px",
59 | },
60 | postComment: {
61 | paddingRight: "16px",
62 | },
63 | headOther: {
64 | flex: 1,
65 | },
66 | headMid: {
67 | flex: 10,
68 | },
69 | }))
70 |
71 | class PhotoPage extends React.Component {
72 | render() {
73 | const { classes, data, history } = this.props
74 | const photo = data.photo || {}
75 | const user = photo.user || {}
76 |
77 | if (data.loading) {
78 | return null
79 | }
80 |
81 | const comments = photo.comments.map((comment) => {
82 | return
83 | })
84 |
85 | return (
86 |
87 |
88 |
89 |
90 |
95 |
96 |
97 |
98 | {linkFor(
99 |
,
103 | `/users/${user.username}`,
104 | { className: classes.headOther },
105 | )}
106 | {linkFor(
107 |
108 | {user.username}
109 | ,
110 | `/users/${user.username}`,
111 | { className: classes.headMid },
112 | )}
113 |
118 |
119 |
120 |
121 | {photo.caption}
122 |
123 |
124 | {comments}
125 |
126 |
127 |
128 |
129 |
130 |
133 |
134 |
135 |
136 |
137 | )
138 | }
139 | }
140 |
141 | PhotoPage.propTypes = {
142 | classes: PropTypes.object.isRequired,
143 | data: PropTypes.shape({
144 | photo: PropTypes.shape({
145 | user: PropTypes.object,
146 | }),
147 | }).isRequired,
148 | history: PropTypes.object.isRequired,
149 | }
150 |
151 | const WithStyle = withStyles(styleSheet)(PhotoPage)
152 | const Connected = connect(
153 | (state) => state,
154 | )(WithStyle)
155 |
156 | export default graphql(GET_PHOTO, {
157 | options: ({ match }) => ({ variables: { id: match.params.id } }),
158 | })(Connected)
159 |
--------------------------------------------------------------------------------
/app/javascript/packs/components/ProfilePicture.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Avatar, Button, Typography, Paper } from "material-ui"
3 | import { withStyles, createStyleSheet } from "material-ui/styles"
4 | import Dialog, {
5 | DialogTitle,
6 | DialogContent,
7 | DialogActions,
8 | } from "material-ui/Dialog"
9 | import Dropzone from "react-dropzone"
10 | import { connect } from "react-redux"
11 | import { bindActionCreators } from "redux"
12 | import PropTypes from "prop-types"
13 | import { graphql } from "react-apollo"
14 |
15 | import upload from "../utils/upload"
16 |
17 | import * as alertActions from "../actions/alert"
18 | import * as userActions from "../actions/user"
19 |
20 | import { UPDATE_PROFILE } from "../mutations"
21 |
22 | const mergedActions = Object.assign({}, alertActions, userActions)
23 |
24 | const styleSheet = createStyleSheet("ProfilePicture", (theme) => ({
25 | avatar: {
26 | width: 200,
27 | height: 200,
28 | margin: "0 auto",
29 | cursor: "pointer",
30 | },
31 | uploadWrapper: theme.upload.wrapper,
32 | placeholder: theme.upload.placeholder,
33 | placeholderText: theme.upload.placeholderText,
34 | }))
35 |
36 | class ProfilePicture extends React.Component {
37 | constructor() {
38 | super()
39 |
40 | this.state = {
41 | open: false,
42 | image: null,
43 | base64Image: null,
44 | }
45 | }
46 | openDialog = () => {
47 | const { currentUser } = this.props
48 | if (currentUser && currentUser.id.toString() === this.props.user.id) {
49 | this.setState({ open: true })
50 | }
51 | }
52 | hideDialog = () => {
53 | this.setState({ open: false })
54 | }
55 | uploadImage = async (files) => {
56 | this.setState({ image: files })
57 |
58 | const reader = new FileReader()
59 | reader.onload = (event) => {
60 | this.setState({ base64Image: event.target.result })
61 | }
62 | reader.readAsDataURL(files[0])
63 |
64 | try {
65 | const result = await upload({ file: files[0], type: "User" })
66 | const json = await result.json()
67 | this.setState({ imageId: json.id })
68 | } catch (err) {
69 | this.props.actions.showAlert(err.message)
70 | }
71 | }
72 | saveImage = () => {
73 | this.props.updateProfile(this.state.imageId).then(({ data }) => {
74 | this.props.actions.setUserByToken(data.updateProfile.auth_token)
75 | this.props.actions.showAlert("Your profile picture updated")
76 | this.hideDialog()
77 | }).catch(({ message }) => {
78 | this.props.actions.showAlert(message)
79 | })
80 | }
81 | renderPlaceholder() {
82 | let placeholder
83 | const { classes } = this.props
84 | if (this.state.base64Image) {
85 | placeholder = (
86 |
91 | )
92 | } else {
93 | placeholder = (
94 |
99 | Browse Image
100 |
101 | )
102 | }
103 |
104 | return {placeholder}
105 | }
106 | render() {
107 | const { classes, currentUser } = this.props
108 | const user = (currentUser && currentUser.id.toString() === this.props.user.id) ? currentUser : this.props.user
109 |
110 | return (
111 |
112 |
119 |
120 |
138 |
139 | )
140 | }
141 | }
142 |
143 | ProfilePicture.defaultProps = {
144 | currentUser: null,
145 | }
146 |
147 | ProfilePicture.propTypes = {
148 | user: PropTypes.shape({
149 | id: PropTypes.string.isRequired,
150 | image: PropTypes.object.isRequired,
151 | }).isRequired,
152 | classes: PropTypes.object.isRequired,
153 | currentUser: PropTypes.object,
154 | actions: PropTypes.object.isRequired,
155 | updateProfile: PropTypes.func.isRequired,
156 | }
157 |
158 | const WithStyle = withStyles(styleSheet)(ProfilePicture)
159 |
160 | const Connected = connect(
161 | (state) => state,
162 | (dispatch) => ({ actions: bindActionCreators(mergedActions, dispatch) }),
163 | )(WithStyle)
164 |
165 | export default graphql(UPDATE_PROFILE, {
166 | props: ({ mutate }) => ({
167 | updateProfile: (imageId) => {
168 | return mutate({
169 | variables: { user: { image_id: imageId } },
170 | })
171 | },
172 | }),
173 | })(Connected)
174 |
--------------------------------------------------------------------------------
/app/javascript/packs/components/Upload.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Button, TextField, Typography, IconButton } from "material-ui"
3 | import Dialog, {
4 | DialogContent,
5 | DialogTitle,
6 | DialogActions,
7 | } from "material-ui/Dialog"
8 | import Dropzone from "react-dropzone"
9 | import { graphql } from "react-apollo"
10 | import { connect } from "react-redux"
11 | import { bindActionCreators } from "redux"
12 | import { withStyles, createStyleSheet } from "material-ui/styles"
13 | import UploadIcon from "material-ui-icons/FileUpload"
14 | import PropTypes from "prop-types"
15 |
16 | import upload from "../utils/upload"
17 |
18 | import * as alertActions from "../actions/alert"
19 |
20 | import { POST_PHOTO } from "../mutations"
21 |
22 | const styleSheet = createStyleSheet("Upload", (theme) => ({
23 | uploadWrapper: theme.upload.wrapper,
24 | placeholder: theme.upload.placeholder,
25 | placeholderText: theme.upload.placeholderText,
26 | image: {
27 | maxHeight: "100%",
28 | maxWidth: "100%",
29 | },
30 | }))
31 |
32 | const initialState = {
33 | open: false,
34 | image: null,
35 | caption: null,
36 | base64Image: null,
37 | image_id: null,
38 | valid: false,
39 | loading: false,
40 | }
41 |
42 | class Upload extends React.Component {
43 | constructor() {
44 | super()
45 |
46 | this.state = Object.assign({}, initialState)
47 | }
48 | cleanState() {
49 | this.setState({
50 | ...initialState,
51 | open: this.state.open,
52 | })
53 | }
54 | hideDialog = () => {
55 | this.setState({ open: false })
56 | }
57 | postPhoto = () => {
58 | const variables = {
59 | photo: { caption: this.state.caption },
60 | image_id: this.state.image_id,
61 | }
62 | this.props.upload(variables).then(({ data }) => {
63 | this.props.actions.showAlert("Your photo uploaded.", {
64 | name: "View",
65 | to: `/photos/${data.postPhoto.id}`,
66 | })
67 | this.hideDialog()
68 | this.cleanState()
69 | }).catch((err) => {
70 | this.props.actions.showAlert(err.message)
71 | })
72 | }
73 | openImageFile = async (files) => {
74 | this.setState({ image: files, loading: true })
75 |
76 | const reader = new FileReader()
77 | reader.onload = (event) => {
78 | this.setState({ base64Image: event.target.result })
79 | }
80 | reader.readAsDataURL(files[0])
81 |
82 | try {
83 | const result = await upload({ file: files[0], type: "Photo" })
84 | const json = await result.json()
85 | this.setState({ image_id: json.id, loading: false, valid: true })
86 | } catch (err) {
87 | this.setState({ loading: false })
88 | this.props.actions.showAlert(err.message)
89 | }
90 | }
91 | renderPlaceholder() {
92 | let placeholder
93 | const { classes } = this.props
94 | if (this.state.base64Image) {
95 | placeholder = (
96 |
101 | )
102 | } else {
103 | placeholder = (
104 |
109 | Browse Image
110 |
111 | )
112 | }
113 |
114 | return {placeholder}
115 | }
116 | render() {
117 | const { classes } = this.props
118 | const { loading, valid } = this.state
119 |
120 | return (
121 |
122 | this.setState({ open: true })}>
123 |
124 |
125 |
149 |
150 | )
151 | }
152 | }
153 |
154 | Upload.propTypes = {
155 | upload: PropTypes.func.isRequired,
156 | actions: PropTypes.object.isRequired,
157 | classes: PropTypes.object.isRequired,
158 | }
159 |
160 | const WithStyle = withStyles(styleSheet)(Upload)
161 |
162 | const Connected = connect(
163 | (state) => state,
164 | (dispatch) => ({ actions: bindActionCreators(alertActions, dispatch) }),
165 | )(WithStyle)
166 |
167 | export default graphql(POST_PHOTO, {
168 | props: ({ mutate }) => ({
169 | upload: (variables) => {
170 | return mutate({
171 | variables,
172 | refetchQueries: [
173 | "feed",
174 | ],
175 | })
176 | },
177 | }),
178 | })(Connected)
179 |
--------------------------------------------------------------------------------
/app/javascript/packs/pages/ProfilePage.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { withStyles, createStyleSheet } from "material-ui/styles"
3 | import { Typography, Grid, Button, Paper } from "material-ui"
4 | import PropTypes from "prop-types"
5 | import SyncIcon from "material-ui-icons/Sync"
6 | import { Link } from "react-router-dom"
7 | import { graphql } from "react-apollo"
8 | import { connect } from "react-redux"
9 |
10 | import UpdateProfile from "../components/UpdateProfile"
11 | import FollowButton from "../components/FollowButton"
12 | import ProfilePicture from "../components/ProfilePicture"
13 |
14 | import { GET_USER } from "../queries"
15 |
16 | const styleSheet = createStyleSheet("ProfilePage", () => ({
17 | root: {
18 | width: "80%",
19 | margin: "0 auto",
20 | },
21 | list: {
22 | flexGrow: 1,
23 | },
24 | item: {
25 | width: "300px",
26 | height: "300px",
27 | },
28 | itemPaper: {
29 | height: "100%",
30 | width: "100%",
31 | padding: 3,
32 | textAlign: "center",
33 | lineHeight: "300px",
34 | },
35 | itemImage: {
36 | lineHeight: "300px",
37 | maxHeight: "100%",
38 | maxWidth: "100%",
39 | },
40 | profile: {
41 | marginBottom: "22px",
42 | },
43 | center: {
44 | textAlign: "center",
45 | },
46 | loadMore: {
47 | margin: "25px 0 25px 0",
48 | },
49 | }))
50 |
51 | class ProfilePage extends React.Component {
52 | constructor() {
53 | super()
54 |
55 | this.state = { edit: false, page: 2 }
56 | }
57 | ownProfile() {
58 | const { currentUser, match, history } = this.props
59 | if (currentUser && currentUser.username === match.params.username) {
60 | return
61 | }
62 |
63 | return
64 | }
65 | loadMore = () => {
66 | this.props.loadMore(this.state.page).then(() => {
67 | this.setState({ page: this.state.page + 1 })
68 | })
69 | }
70 | loadMoreShow() {
71 | const { loading, user } = this.props.data
72 | if (!loading && (user.photos.length < user.photos_count)) {
73 | return (
74 |
82 |
83 |
86 |
87 |
88 | )
89 | }
90 | return null
91 | }
92 | render() {
93 | const { classes, data } = this.props
94 | const user = data.user || {}
95 | const photos = user.photos || []
96 |
97 | const list = photos.map((photo) => {
98 | const style = {
99 | backgroundImage: `url(${photo.image.medium})`,
100 | backgroundSize: "cover",
101 | }
102 | return (
103 |
104 |
105 |
110 |
111 |
112 | )
113 | })
114 |
115 | if (data.loading && !user.id) {
116 | return null
117 | }
118 |
119 | this.user = user
120 |
121 | return (
122 |
123 |
131 |
132 |
133 |
134 |
135 |
142 |
143 |
144 | {user.username}
145 | {this.ownProfile()}
146 | this.setState({ edit: false })}
148 | open={this.state.edit}
149 | />
150 |
151 |
152 |
153 |
154 |
155 |
156 | Photos {user.photos_count}
157 |
158 |
159 |
160 |
161 | Folowers {user.followers_count}
162 |
163 |
164 |
165 |
166 | Followings {user.followings_count}
167 |
168 |
169 |
170 |
171 |
172 |
173 | {user.name}
174 |
175 |
176 | {user.caption}
177 |
178 |
179 |
180 |
181 |
182 |
183 |
191 | {list}
192 |
193 | {this.loadMoreShow()}
194 |
195 | )
196 | }
197 | }
198 |
199 | ProfilePage.defaultProps = {
200 | data: {
201 | user: {
202 | image: {},
203 | photos: [],
204 | },
205 | },
206 | }
207 |
208 | ProfilePage.defaultProps = {
209 | currentUser: null,
210 | }
211 |
212 | ProfilePage.propTypes = {
213 | classes: PropTypes.object.isRequired,
214 | data: PropTypes.shape({
215 | user: PropTypes.shape({
216 | image: PropTypes.object,
217 | photos: PropTypes.array,
218 | }),
219 | loading: PropTypes.bool,
220 | }).isRequired,
221 | currentUser: PropTypes.object,
222 | match: PropTypes.object.isRequired,
223 | history: PropTypes.object.isRequired,
224 | loadMore: PropTypes.func.isRequired,
225 | }
226 |
227 | const WithStyle = withStyles(styleSheet)(ProfilePage)
228 | const Connected = connect(
229 | (state) => state,
230 | )(WithStyle)
231 |
232 | export default graphql(GET_USER, {
233 | options: ({ match }) => ({ variables: { username: match.params.username } }),
234 | props(props) {
235 | return {
236 | ...props,
237 | loadMore(page) {
238 | return props.data.fetchMore({
239 | variables: { page },
240 | updateQuery: (prev, { fetchMoreResult }) => {
241 | return Object.assign({}, prev, {
242 | user: {
243 | ...fetchMoreResult.user,
244 | photos: [
245 | ...prev.user.photos,
246 | ...fetchMoreResult.user.photos,
247 | ],
248 | },
249 | })
250 | },
251 | })
252 | },
253 | }
254 | },
255 | })(Connected)
256 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: https://github.com/thoughtbot/shoulda-matchers.git
3 | revision: edaf9cb926ee9c59c59729e7a4f8c206b44da8a1
4 | branch: rails-5
5 | specs:
6 | shoulda-matchers (3.1.2)
7 | activesupport (>= 4.2.0)
8 |
9 | GEM
10 | remote: https://rubygems.org/
11 | specs:
12 | CFPropertyList (2.3.5)
13 | actioncable (5.1.2)
14 | actionpack (= 5.1.2)
15 | nio4r (~> 2.0)
16 | websocket-driver (~> 0.6.1)
17 | actionmailer (5.1.2)
18 | actionpack (= 5.1.2)
19 | actionview (= 5.1.2)
20 | activejob (= 5.1.2)
21 | mail (~> 2.5, >= 2.5.4)
22 | rails-dom-testing (~> 2.0)
23 | actionpack (5.1.2)
24 | actionview (= 5.1.2)
25 | activesupport (= 5.1.2)
26 | rack (~> 2.0)
27 | rack-test (~> 0.6.3)
28 | rails-dom-testing (~> 2.0)
29 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
30 | actionview (5.1.2)
31 | activesupport (= 5.1.2)
32 | builder (~> 3.1)
33 | erubi (~> 1.4)
34 | rails-dom-testing (~> 2.0)
35 | rails-html-sanitizer (~> 1.0, >= 1.0.3)
36 | activejob (5.1.2)
37 | activesupport (= 5.1.2)
38 | globalid (>= 0.3.6)
39 | activemodel (5.1.2)
40 | activesupport (= 5.1.2)
41 | activerecord (5.1.2)
42 | activemodel (= 5.1.2)
43 | activesupport (= 5.1.2)
44 | arel (~> 8.0)
45 | activesupport (5.1.2)
46 | concurrent-ruby (~> 1.0, >= 1.0.2)
47 | i18n (~> 0.7)
48 | minitest (~> 5.1)
49 | tzinfo (~> 1.1)
50 | addressable (2.5.1)
51 | public_suffix (~> 2.0, >= 2.0.2)
52 | annotate (2.7.2)
53 | activerecord (>= 3.2, < 6.0)
54 | rake (>= 10.4, < 13.0)
55 | arel (8.0.0)
56 | ast (2.3.0)
57 | bcrypt (3.1.11)
58 | bindex (0.5.0)
59 | builder (3.2.3)
60 | byebug (9.0.6)
61 | capybara (2.14.4)
62 | addressable
63 | mime-types (>= 1.16)
64 | nokogiri (>= 1.3.3)
65 | rack (>= 1.0.0)
66 | rack-test (>= 0.5.4)
67 | xpath (~> 2.0)
68 | carrierwave (1.1.0)
69 | activemodel (>= 4.0.0)
70 | activesupport (>= 4.0.0)
71 | mime-types (>= 1.16)
72 | childprocess (0.7.1)
73 | ffi (~> 1.0, >= 1.0.11)
74 | coderay (1.1.1)
75 | coffee-rails (4.2.2)
76 | coffee-script (>= 2.2.0)
77 | railties (>= 4.0.0)
78 | coffee-script (2.4.1)
79 | coffee-script-source
80 | execjs
81 | coffee-script-source (1.12.2)
82 | concurrent-ruby (1.0.5)
83 | database_cleaner (1.6.1)
84 | devise (4.3.0)
85 | bcrypt (~> 3.0)
86 | orm_adapter (~> 0.1)
87 | railties (>= 4.1.0, < 5.2)
88 | responders
89 | warden (~> 1.2.3)
90 | diff-lcs (1.3)
91 | docile (1.1.5)
92 | erubi (1.6.1)
93 | excon (0.58.0)
94 | execjs (2.7.0)
95 | factory_girl (4.8.0)
96 | activesupport (>= 3.0.0)
97 | factory_girl_rails (4.8.0)
98 | factory_girl (~> 4.8.0)
99 | railties (>= 3.0.0)
100 | faker (1.8.4)
101 | i18n (~> 0.5)
102 | ffi (1.9.18)
103 | figaro (1.1.1)
104 | thor (~> 0.14)
105 | fission (0.5.0)
106 | CFPropertyList (~> 2.2)
107 | fog (1.38.0)
108 | fog-aliyun (>= 0.1.0)
109 | fog-atmos
110 | fog-aws (>= 0.6.0)
111 | fog-brightbox (~> 0.4)
112 | fog-cloudatcost (~> 0.1.0)
113 | fog-core (~> 1.32)
114 | fog-dynect (~> 0.0.2)
115 | fog-ecloud (~> 0.1)
116 | fog-google (<= 0.1.0)
117 | fog-json
118 | fog-local
119 | fog-openstack
120 | fog-powerdns (>= 0.1.1)
121 | fog-profitbricks
122 | fog-rackspace
123 | fog-radosgw (>= 0.0.2)
124 | fog-riakcs
125 | fog-sakuracloud (>= 0.0.4)
126 | fog-serverlove
127 | fog-softlayer
128 | fog-storm_on_demand
129 | fog-terremark
130 | fog-vmfusion
131 | fog-voxel
132 | fog-vsphere (>= 0.4.0)
133 | fog-xenserver
134 | fog-xml (~> 0.1.1)
135 | ipaddress (~> 0.5)
136 | fog-aliyun (0.2.0)
137 | fog-core (~> 1.27)
138 | fog-json (~> 1.0)
139 | ipaddress (~> 0.8)
140 | xml-simple (~> 1.1)
141 | fog-atmos (0.1.0)
142 | fog-core
143 | fog-xml
144 | fog-aws (1.4.0)
145 | fog-core (~> 1.38)
146 | fog-json (~> 1.0)
147 | fog-xml (~> 0.1)
148 | ipaddress (~> 0.8)
149 | fog-brightbox (0.13.0)
150 | fog-core (~> 1.22)
151 | fog-json
152 | inflecto (~> 0.0.2)
153 | fog-cloudatcost (0.1.2)
154 | fog-core (~> 1.36)
155 | fog-json (~> 1.0)
156 | fog-xml (~> 0.1)
157 | ipaddress (~> 0.8)
158 | fog-core (1.45.0)
159 | builder
160 | excon (~> 0.58)
161 | formatador (~> 0.2)
162 | fog-dynect (0.0.3)
163 | fog-core
164 | fog-json
165 | fog-xml
166 | fog-ecloud (0.3.0)
167 | fog-core
168 | fog-xml
169 | fog-google (0.1.0)
170 | fog-core
171 | fog-json
172 | fog-xml
173 | fog-json (1.0.2)
174 | fog-core (~> 1.0)
175 | multi_json (~> 1.10)
176 | fog-local (0.3.1)
177 | fog-core (~> 1.27)
178 | fog-openstack (0.1.21)
179 | fog-core (>= 1.40)
180 | fog-json (>= 1.0)
181 | ipaddress (>= 0.8)
182 | fog-powerdns (0.1.1)
183 | fog-core (~> 1.27)
184 | fog-json (~> 1.0)
185 | fog-xml (~> 0.1)
186 | fog-profitbricks (3.0.0)
187 | fog-core (~> 1.42)
188 | fog-json (~> 1.0)
189 | fog-rackspace (0.1.5)
190 | fog-core (>= 1.35)
191 | fog-json (>= 1.0)
192 | fog-xml (>= 0.1)
193 | ipaddress (>= 0.8)
194 | fog-radosgw (0.0.5)
195 | fog-core (>= 1.21.0)
196 | fog-json
197 | fog-xml (>= 0.0.1)
198 | fog-riakcs (0.1.0)
199 | fog-core
200 | fog-json
201 | fog-xml
202 | fog-sakuracloud (1.7.5)
203 | fog-core
204 | fog-json
205 | fog-serverlove (0.1.2)
206 | fog-core
207 | fog-json
208 | fog-softlayer (1.1.4)
209 | fog-core
210 | fog-json
211 | fog-storm_on_demand (0.1.1)
212 | fog-core
213 | fog-json
214 | fog-terremark (0.1.0)
215 | fog-core
216 | fog-xml
217 | fog-vmfusion (0.1.0)
218 | fission
219 | fog-core
220 | fog-voxel (0.1.0)
221 | fog-core
222 | fog-xml
223 | fog-vsphere (1.11.3)
224 | fog-core
225 | rbvmomi (~> 1.9)
226 | fog-xenserver (0.3.0)
227 | fog-core
228 | fog-xml
229 | fog-xml (0.1.3)
230 | fog-core
231 | nokogiri (>= 1.5.11, < 2.0.0)
232 | formatador (0.2.5)
233 | globalid (0.4.0)
234 | activesupport (>= 4.2.0)
235 | graphiql-rails (1.4.2)
236 | rails
237 | graphql (1.6.6)
238 | guard (2.14.1)
239 | formatador (>= 0.2.4)
240 | listen (>= 2.7, < 4.0)
241 | lumberjack (~> 1.0)
242 | nenv (~> 0.1)
243 | notiffany (~> 0.0)
244 | pry (>= 0.9.12)
245 | shellany (~> 0.0)
246 | thor (>= 0.18.1)
247 | guard-bundler (2.1.0)
248 | bundler (~> 1.0)
249 | guard (~> 2.2)
250 | guard-compat (~> 1.1)
251 | guard-compat (1.2.1)
252 | guard-rspec (4.7.3)
253 | guard (~> 2.1)
254 | guard-compat (~> 1.1)
255 | rspec (>= 2.99.0, < 4.0)
256 | i18n (0.8.6)
257 | inflecto (0.0.2)
258 | ipaddress (0.8.3)
259 | json (2.1.0)
260 | jwt (1.5.6)
261 | kaminari (1.0.1)
262 | activesupport (>= 4.1.0)
263 | kaminari-actionview (= 1.0.1)
264 | kaminari-activerecord (= 1.0.1)
265 | kaminari-core (= 1.0.1)
266 | kaminari-actionview (1.0.1)
267 | actionview
268 | kaminari-core (= 1.0.1)
269 | kaminari-activerecord (1.0.1)
270 | activerecord
271 | kaminari-core (= 1.0.1)
272 | kaminari-core (1.0.1)
273 | listen (3.1.5)
274 | rb-fsevent (~> 0.9, >= 0.9.4)
275 | rb-inotify (~> 0.9, >= 0.9.7)
276 | ruby_dep (~> 1.2)
277 | loofah (2.0.3)
278 | nokogiri (>= 1.5.9)
279 | lumberjack (1.0.12)
280 | mail (2.6.6)
281 | mime-types (>= 1.16, < 4)
282 | method_source (0.8.2)
283 | mime-types (3.1)
284 | mime-types-data (~> 3.2015)
285 | mime-types-data (3.2016.0521)
286 | mini_magick (4.8.0)
287 | mini_portile2 (2.2.0)
288 | minitest (5.10.3)
289 | multi_json (1.12.1)
290 | nenv (0.3.0)
291 | nio4r (2.1.0)
292 | nokogiri (1.8.0)
293 | mini_portile2 (~> 2.2.0)
294 | notiffany (0.1.1)
295 | nenv (~> 0.1)
296 | shellany (~> 0.0)
297 | orm_adapter (0.5.0)
298 | parallel (1.12.0)
299 | parser (2.4.0.0)
300 | ast (~> 2.2)
301 | pg (0.21.0)
302 | powerpack (0.1.1)
303 | pry (0.10.4)
304 | coderay (~> 1.1.0)
305 | method_source (~> 0.8.1)
306 | slop (~> 3.4)
307 | pry-rails (0.3.6)
308 | pry (>= 0.10.4)
309 | public_suffix (2.0.5)
310 | puma (3.9.1)
311 | rack (2.0.3)
312 | rack-test (0.6.3)
313 | rack (>= 1.0)
314 | rails (5.1.2)
315 | actioncable (= 5.1.2)
316 | actionmailer (= 5.1.2)
317 | actionpack (= 5.1.2)
318 | actionview (= 5.1.2)
319 | activejob (= 5.1.2)
320 | activemodel (= 5.1.2)
321 | activerecord (= 5.1.2)
322 | activesupport (= 5.1.2)
323 | bundler (>= 1.3.0, < 2.0)
324 | railties (= 5.1.2)
325 | sprockets-rails (>= 2.0.0)
326 | rails-dom-testing (2.0.3)
327 | activesupport (>= 4.2.0)
328 | nokogiri (>= 1.6)
329 | rails-html-sanitizer (1.0.3)
330 | loofah (~> 2.0)
331 | railties (5.1.2)
332 | actionpack (= 5.1.2)
333 | activesupport (= 5.1.2)
334 | method_source
335 | rake (>= 0.8.7)
336 | thor (>= 0.18.1, < 2.0)
337 | rainbow (2.2.2)
338 | rake
339 | rake (12.0.0)
340 | rb-fsevent (0.10.2)
341 | rb-inotify (0.9.10)
342 | ffi (>= 0.5.0, < 2)
343 | rbvmomi (1.11.3)
344 | builder (~> 3.0)
345 | json (>= 1.8)
346 | nokogiri (~> 1.5)
347 | trollop (~> 2.1)
348 | responders (2.4.0)
349 | actionpack (>= 4.2.0, < 5.3)
350 | railties (>= 4.2.0, < 5.3)
351 | rspec (3.6.0)
352 | rspec-core (~> 3.6.0)
353 | rspec-expectations (~> 3.6.0)
354 | rspec-mocks (~> 3.6.0)
355 | rspec-core (3.6.0)
356 | rspec-support (~> 3.6.0)
357 | rspec-expectations (3.6.0)
358 | diff-lcs (>= 1.2.0, < 2.0)
359 | rspec-support (~> 3.6.0)
360 | rspec-mocks (3.6.0)
361 | diff-lcs (>= 1.2.0, < 2.0)
362 | rspec-support (~> 3.6.0)
363 | rspec-rails (3.6.0)
364 | actionpack (>= 3.0)
365 | activesupport (>= 3.0)
366 | railties (>= 3.0)
367 | rspec-core (~> 3.6.0)
368 | rspec-expectations (~> 3.6.0)
369 | rspec-mocks (~> 3.6.0)
370 | rspec-support (~> 3.6.0)
371 | rspec-support (3.6.0)
372 | rubocop (0.49.1)
373 | parallel (~> 1.10)
374 | parser (>= 2.3.3.1, < 3.0)
375 | powerpack (~> 0.1)
376 | rainbow (>= 1.99.1, < 3.0)
377 | ruby-progressbar (~> 1.7)
378 | unicode-display_width (~> 1.0, >= 1.0.1)
379 | ruby-progressbar (1.8.1)
380 | ruby_dep (1.5.0)
381 | rubyzip (1.2.1)
382 | sass (3.5.1)
383 | sass-listen (~> 4.0.0)
384 | sass-listen (4.0.0)
385 | rb-fsevent (~> 0.9, >= 0.9.4)
386 | rb-inotify (~> 0.9, >= 0.9.7)
387 | sass-rails (5.0.6)
388 | railties (>= 4.0.0, < 6)
389 | sass (~> 3.1)
390 | sprockets (>= 2.8, < 4.0)
391 | sprockets-rails (>= 2.0, < 4.0)
392 | tilt (>= 1.1, < 3)
393 | selenium-webdriver (3.4.4)
394 | childprocess (~> 0.5)
395 | rubyzip (~> 1.0)
396 | shellany (0.0.1)
397 | simplecov (0.14.1)
398 | docile (~> 1.1.0)
399 | json (>= 1.8, < 3)
400 | simplecov-html (~> 0.10.0)
401 | simplecov-html (0.10.1)
402 | slop (3.6.0)
403 | spring (2.0.2)
404 | activesupport (>= 4.2)
405 | spring-watcher-listen (2.0.1)
406 | listen (>= 2.7, < 4.0)
407 | spring (>= 1.2, < 3.0)
408 | sprockets (3.7.1)
409 | concurrent-ruby (~> 1.0)
410 | rack (> 1, < 3)
411 | sprockets-rails (3.2.0)
412 | actionpack (>= 4.0)
413 | activesupport (>= 4.0)
414 | sprockets (>= 3.0.0)
415 | terminal-notifier-guard (1.7.0)
416 | thor (0.19.4)
417 | thread_safe (0.3.6)
418 | tilt (2.0.8)
419 | trollop (2.1.2)
420 | tzinfo (1.2.3)
421 | thread_safe (~> 0.1)
422 | uglifier (3.2.0)
423 | execjs (>= 0.3.0, < 3)
424 | unicode-display_width (1.3.0)
425 | warden (1.2.7)
426 | rack (>= 1.0)
427 | web-console (3.5.1)
428 | actionview (>= 5.0)
429 | activemodel (>= 5.0)
430 | bindex (>= 0.4.0)
431 | railties (>= 5.0)
432 | webpacker (2.0)
433 | activesupport (>= 4.2)
434 | multi_json (~> 1.2)
435 | railties (>= 4.2)
436 | websocket-driver (0.6.5)
437 | websocket-extensions (>= 0.1.0)
438 | websocket-extensions (0.1.2)
439 | xml-simple (1.1.5)
440 | xpath (2.1.0)
441 | nokogiri (~> 1.3)
442 |
443 | PLATFORMS
444 | ruby
445 |
446 | DEPENDENCIES
447 | annotate
448 | byebug
449 | capybara (~> 2.13)
450 | carrierwave
451 | coffee-rails (~> 4.2)
452 | database_cleaner
453 | devise
454 | factory_girl_rails
455 | faker
456 | figaro
457 | fog
458 | graphiql-rails
459 | graphql
460 | guard
461 | guard-bundler
462 | guard-rspec
463 | jwt
464 | kaminari
465 | listen (>= 3.0.5, < 3.2)
466 | mini_magick
467 | pg
468 | pry-rails
469 | puma (~> 3.7)
470 | rails (~> 5.1.1)
471 | rspec-rails
472 | rubocop
473 | sass-rails (~> 5.0)
474 | selenium-webdriver
475 | shoulda-matchers!
476 | simplecov
477 | spring
478 | spring-watcher-listen (~> 2.0.0)
479 | terminal-notifier-guard
480 | tzinfo-data
481 | uglifier (>= 1.3.0)
482 | web-console (>= 3.3.0)
483 | webpacker
484 |
485 | RUBY VERSION
486 | ruby 2.3.3p222
487 |
488 | BUNDLED WITH
489 | 1.15.3
490 |
--------------------------------------------------------------------------------