├── log └── .keep ├── tmp └── .keep ├── vendor └── .keep ├── lib ├── assets │ └── .keep └── tasks │ ├── .keep │ └── auto_annotate_models.rake ├── .ruby-version ├── app ├── assets │ ├── images │ │ ├── .keep │ │ └── defaults │ │ │ └── profile.png │ ├── javascripts │ │ ├── channels │ │ │ └── .keep │ │ ├── cable.js │ │ └── application.js │ ├── config │ │ └── manifest.js │ └── stylesheets │ │ ├── styles.scss │ │ └── application.css ├── graphql │ ├── types │ │ ├── .keep │ │ ├── token_type.rb │ │ ├── auth_type.rb │ │ ├── user_follow_type.rb │ │ ├── comment_type.rb │ │ ├── image_type.rb │ │ ├── human_time_type.rb │ │ ├── mutation_type.rb │ │ ├── photo_type.rb │ │ ├── user_type.rb │ │ └── query_type.rb │ ├── mutations │ │ └── .keep │ ├── functions │ │ ├── delete_photo.rb │ │ ├── login.rb │ │ ├── followship_query.rb │ │ ├── comment_photo.rb │ │ ├── like_photo.rb │ │ ├── register.rb │ │ ├── followship.rb │ │ ├── post_photo.rb │ │ └── update_profile.rb │ └── instaqram_schema.rb ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ ├── comment.rb │ ├── like.rb │ ├── photo.rb │ ├── followship.rb │ ├── image.rb │ └── user.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── pages_controller.rb │ ├── application_controller.rb │ └── api │ │ ├── base_controller.rb │ │ ├── images_controller.rb │ │ └── graphql_controller.rb ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── mailer.html.erb │ │ └── application.html.erb │ └── pages │ │ └── home.html.erb ├── helpers │ └── application_helper.rb ├── jobs │ └── application_job.rb ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── mailers │ └── application_mailer.rb ├── javascript │ └── packs │ │ ├── mutations │ │ ├── follow_user.js │ │ ├── login.js │ │ ├── delete_photo.js │ │ ├── register.js │ │ ├── post_photo.js │ │ ├── like_photo.js │ │ ├── comment_photo.js │ │ ├── index.js │ │ └── update_profile.js │ │ ├── queries │ │ ├── get_users.js │ │ ├── index.js │ │ ├── get_user.js │ │ ├── get_feed.js │ │ └── get_photo.js │ │ ├── utils │ │ ├── helpers.js │ │ └── upload.js │ │ ├── components │ │ ├── EmptyState.js │ │ ├── forms │ │ │ ├── validations │ │ │ │ └── index.js │ │ │ ├── LoginForm.js │ │ │ ├── RegisterForm.js │ │ │ ├── EditProfileForm.js │ │ │ └── PostCommentForm.js │ │ ├── PrivateRoute.js │ │ ├── Comment.js │ │ ├── Alert.js │ │ ├── FollowButton.js │ │ ├── UpdateProfile.js │ │ ├── Love.js │ │ ├── PhotoOpts.js │ │ ├── PhotoCard.js │ │ ├── Header.js │ │ ├── ProfilePicture.js │ │ └── Upload.js │ │ ├── application.prod.js │ │ ├── reducers │ │ ├── user.js │ │ ├── index.js │ │ └── alert.js │ │ ├── actions │ │ ├── alert.js │ │ └── user.js │ │ ├── constants.js │ │ ├── application.dev.js │ │ ├── apollo.js │ │ ├── store │ │ └── index.js │ │ ├── Main.js │ │ ├── styles │ │ └── index.js │ │ ├── pages │ │ ├── LoginPage.js │ │ ├── RegisterPage.js │ │ ├── UsersPage.js │ │ ├── HomePage.js │ │ ├── PhotoPage.js │ │ └── ProfilePage.js │ │ └── Routes.js └── uploaders │ └── image_uploader.rb ├── public ├── apple-touch-icon.png ├── apple-touch-icon-precomposed.png ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── .rspec ├── .postcssrc.yml ├── spec ├── fixtures │ └── images │ │ └── example1.jpg ├── controllers │ └── api │ │ └── images_controller_spec.rb ├── factories │ ├── photos.rb │ ├── followships.rb │ ├── images.rb │ └── users.rb ├── models │ ├── photo_spec.rb │ ├── followship_spec.rb │ ├── image_spec.rb │ └── user_spec.rb ├── support │ └── controller_helpers.rb ├── spec_helper.rb └── rails_helper.rb ├── config ├── webpack │ ├── loaders │ │ ├── coffee.js │ │ ├── babel.js │ │ ├── erb.js │ │ ├── react.js │ │ ├── assets.js │ │ └── sass.js │ ├── test.js │ ├── production.js │ ├── development.js │ ├── configuration.js │ └── shared.js ├── spring.rb ├── boot.rb ├── environment.rb ├── initializers │ ├── mime_types.rb │ ├── application_controller_renderer.rb │ ├── filter_parameter_logging.rb │ ├── cookies_serializer.rb │ ├── carrierwave.rb │ ├── kaminari_config.rb │ ├── backtrace_silencers.rb │ ├── wrap_parameters.rb │ ├── assets.rb │ └── inflections.rb ├── cable.yml ├── application.yml.example ├── routes.rb ├── webpacker.yml ├── database.yml ├── locales │ ├── en.yml │ └── devise.en.yml ├── application.rb ├── secrets.yml ├── environments │ ├── test.rb │ ├── development.rb │ └── production.rb └── puma.rb ├── bin ├── bundle ├── rake ├── rails ├── yarn ├── spring ├── webpack ├── update ├── setup └── webpack-dev-server ├── config.ru ├── db ├── migrate │ ├── 20170724030057_add_username_to_users.rb │ ├── 20170801025720_remove_image_from_user_and_photos.rb │ ├── 20170724045437_create_likes.rb │ ├── 20170724043147_add_photos_count_to_users.rb │ ├── 20170724043847_create_comments.rb │ ├── 20170724083323_add_profile_to_users.rb │ ├── 20170724031825_create_photos.rb │ ├── 20170731032723_create_images.rb │ ├── 20170807133058_add_confirmable_to_users.rb │ ├── 20170802034248_create_followships.rb │ ├── 20170804085248_add_followings_count_and_followers_count_to_users.rb │ └── 20170724021850_devise_create_users.rb ├── seeds.rb └── schema.rb ├── Rakefile ├── .babelrc ├── .eslintrc ├── README.md ├── .gitignore ├── LICENSE ├── .rubocop.yml ├── Gemfile ├── Guardfile ├── package.json └── Gemfile.lock /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.3.3 2 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/graphql/types/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/graphql/mutations/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/views/pages/home.html.erb: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dimasjt/Instaqram/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.postcssrc.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | postcss-smart-import: {} 3 | precss: {} 4 | autoprefixer: {} 5 | -------------------------------------------------------------------------------- /app/controllers/pages_controller.rb: -------------------------------------------------------------------------------- 1 | class PagesController < ApplicationController 2 | def home 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /spec/fixtures/images/example1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dimasjt/Instaqram/HEAD/spec/fixtures/images/example1.jpg -------------------------------------------------------------------------------- /config/webpack/loaders/coffee.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test: /\.coffee(\.erb)?$/, 3 | loader: "coffee-loader", 4 | } 5 | -------------------------------------------------------------------------------- /app/assets/images/defaults/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dimasjt/Instaqram/HEAD/app/assets/images/defaults/profile.png -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | end 4 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /config/webpack/loaders/babel.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test: /\.js(\.erb)?$/, 3 | exclude: /node_modules/, 4 | loader: "babel-loader", 5 | } 6 | -------------------------------------------------------------------------------- /app/graphql/types/token_type.rb: -------------------------------------------------------------------------------- 1 | Types::TokenType = GraphQL::ObjectType.define do 2 | name "token" 3 | 4 | field :auth_token, types.String 5 | end 6 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/styles.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | padding-bottom: 60px; 5 | } 6 | 7 | a { 8 | text-decoration: none; 9 | } 10 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/controllers/api/images_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe Api::ImagesController, type: :controller do 4 | describe "POST create" do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/graphql/types/auth_type.rb: -------------------------------------------------------------------------------- 1 | Types::AuthType = GraphQL::InputObjectType.define do 2 | name "Auth" 3 | 4 | argument :email, !types.String 5 | argument :password, !types.String 6 | end 7 | -------------------------------------------------------------------------------- /app/javascript/packs/mutations/follow_user.js: -------------------------------------------------------------------------------- 1 | import { gql } from "react-apollo" 2 | 3 | export default gql` 4 | mutation follow($user_id: ID!) { 5 | follow(user_id: $user_id) 6 | } 7 | ` 8 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /app/javascript/packs/mutations/login.js: -------------------------------------------------------------------------------- 1 | import { gql } from "react-apollo" 2 | 3 | export default gql` 4 | mutation login($user: Auth!) { 5 | login(user: $user) { 6 | auth_token 7 | } 8 | } 9 | ` 10 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://localhost:6379/1 10 | channel_prefix: learn-graphql_production 11 | -------------------------------------------------------------------------------- /app/graphql/types/user_follow_type.rb: -------------------------------------------------------------------------------- 1 | Types::UserFollowType = GraphQL::ObjectType.define do 2 | name "UserFollow" 3 | 4 | field :id, types.ID 5 | field :username, types.String 6 | field :image, Types::ImageType 7 | end 8 | -------------------------------------------------------------------------------- /app/javascript/packs/mutations/delete_photo.js: -------------------------------------------------------------------------------- 1 | import { gql } from "react-apollo" 2 | 3 | export default gql` 4 | mutation deletePhoto($id: ID!) { 5 | deletePhoto(id: $id) { 6 | id 7 | } 8 | } 9 | ` 10 | -------------------------------------------------------------------------------- /app/javascript/packs/mutations/register.js: -------------------------------------------------------------------------------- 1 | import { gql } from "react-apollo" 2 | 3 | export default gql` 4 | mutation register($user: Register!){ 5 | register(user: $user) { 6 | auth_token 7 | } 8 | } 9 | ` 10 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) 7 | -------------------------------------------------------------------------------- /config/webpack/loaders/erb.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test: /\.erb$/, 3 | enforce: "pre", 4 | exclude: /node_modules/, 5 | loader: "rails-erb-loader", 6 | options: { 7 | runner: "bin/rails runner", 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /db/migrate/20170724030057_add_username_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddUsernameToUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :username, :string 4 | add_index :users, :username, unique: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20170801025720_remove_image_from_user_and_photos.rb: -------------------------------------------------------------------------------- 1 | class RemoveImageFromUserAndPhotos < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_column :users, :image 4 | remove_column :photos, :image 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /config/application.yml.example: -------------------------------------------------------------------------------- 1 | DB_NAME: 2 | DB_TEST_NAME: 3 | DB_PASSWORD: 4 | DB_USERNAME: 5 | DB_HOST: 6 | 7 | SECRET_KEY_BASE: 8 | DEVISE_SECRET_KEY: 9 | 10 | RAILS_SERVE_STATIC_FILES: true 11 | 12 | S3_KEY: 13 | S3_SECRET: 14 | S3_REGION: 15 | -------------------------------------------------------------------------------- /app/graphql/types/comment_type.rb: -------------------------------------------------------------------------------- 1 | Types::CommentType = GraphQL::ObjectType.define do 2 | name "Comment" 3 | 4 | field :id, types.ID 5 | field :content, types.String 6 | field :user, type: Types::UserType 7 | field :photo, type: Types::PhotoType 8 | end 9 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /app/javascript/packs/mutations/post_photo.js: -------------------------------------------------------------------------------- 1 | import { gql } from "react-apollo" 2 | 3 | export default gql` 4 | mutation postPhoto($photo: PostPhoto!, $image_id: ID!) { 5 | postPhoto(photo: $photo, image_id: $image_id) { 6 | id 7 | } 8 | } 9 | ` 10 | -------------------------------------------------------------------------------- /app/javascript/packs/mutations/like_photo.js: -------------------------------------------------------------------------------- 1 | import { gql } from "react-apollo" 2 | 3 | export default gql` 4 | mutation likePhoto($photo_id: ID!) { 5 | likePhoto(photo_id: $photo_id) { 6 | id 7 | likes_count 8 | liked 9 | } 10 | } 11 | ` 12 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /db/migrate/20170724045437_create_likes.rb: -------------------------------------------------------------------------------- 1 | class CreateLikes < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :likes do |t| 4 | t.belongs_to :user, foreign_key: true 5 | t.belongs_to :photo, foreign_key: true 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/javascript/packs/queries/get_users.js: -------------------------------------------------------------------------------- 1 | import { gql } from "react-apollo" 2 | 3 | export default gql` 4 | query users { 5 | users { 6 | id 7 | username 8 | name 9 | photos_count 10 | followed 11 | image { 12 | thumb 13 | } 14 | } 15 | } 16 | ` 17 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /db/migrate/20170724043147_add_photos_count_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddPhotosCountToUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :photos_count, :integer, default: 0 4 | 5 | User.pluck(:id).each do |id| 6 | User.reset_counters(id, :photos) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /config/webpack/loaders/react.js: -------------------------------------------------------------------------------- 1 | let loader 2 | if (process.env.NODE_ENV === "production") { 3 | loader = ["babel-loader"] 4 | } else { 5 | loader = ["react-hot-loader/webpack", "babel-loader"] 6 | } 7 | 8 | module.exports = { 9 | test: /\.(js|jsx)?(\.erb)?$/, 10 | exclude: /node_modules/, 11 | loader, 12 | } 13 | -------------------------------------------------------------------------------- /app/javascript/packs/utils/helpers.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "react-router-dom" 3 | 4 | export const linkFor = (component, to, rest) => { 5 | return {component} 6 | } 7 | 8 | export const isShow = (component, show) => { 9 | return show ? component : null 10 | } 11 | -------------------------------------------------------------------------------- /app/javascript/packs/components/EmptyState.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Typography } from "material-ui" 3 | import PropTypes from "prop-types" 4 | 5 | const EmptyState = () => { 6 | return ( 7 |
8 | Upload 9 |
10 | ) 11 | } 12 | 13 | export default EmptyState 14 | -------------------------------------------------------------------------------- /db/migrate/20170724043847_create_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateComments < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :comments do |t| 4 | t.belongs_to :user, foreign_key: true 5 | t.belongs_to :photo, foreign_key: true 6 | t.text :content 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/javascript/packs/queries/index.js: -------------------------------------------------------------------------------- 1 | import getUser from "./get_user" 2 | import getPhoto from "./get_photo" 3 | import getFeed from "./get_feed" 4 | import getUsers from "./get_users" 5 | 6 | export const GET_USER = getUser 7 | export const GET_PHOTO = getPhoto 8 | export const GET_FEED = getFeed 9 | export const GET_USERS = getUsers 10 | -------------------------------------------------------------------------------- /app/javascript/packs/application.prod.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import injectTapEventPlugin from "react-tap-event-plugin" 4 | import "babel-polyfill" 5 | 6 | import Main from "./Main" 7 | 8 | injectTapEventPlugin() 9 | 10 | ReactDOM.render( 11 |
, 12 | document.getElementById("root"), 13 | ) 14 | -------------------------------------------------------------------------------- /app/graphql/types/image_type.rb: -------------------------------------------------------------------------------- 1 | Types::ImageType = GraphQL::ObjectType.define do 2 | name "image" 3 | 4 | %i[thumb small medium large original].each do |version| 5 | field version, types.String do 6 | resolve ->(obj, args, ctx) { 7 | version == :original ? obj.url : obj.send(version).url 8 | } 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/javascript/packs/mutations/comment_photo.js: -------------------------------------------------------------------------------- 1 | import { gql } from "react-apollo" 2 | 3 | export default gql` 4 | mutation commentPhoto($photo_id: ID!, $content: String!) { 5 | commentPhoto(photo_id: $photo_id, content: $content) { 6 | id 7 | content 8 | user { 9 | id 10 | username 11 | } 12 | } 13 | } 14 | ` 15 | -------------------------------------------------------------------------------- /db/migrate/20170724083323_add_profile_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddProfileToUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :name, :string 4 | add_column :users, :birthdate, :string 5 | add_column :users, :caption, :string 6 | add_column :users, :website, :string 7 | add_column :users, :image, :string 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | VENDOR_PATH = File.expand_path('..', __dir__) 3 | Dir.chdir(VENDOR_PATH) do 4 | begin 5 | exec "yarnpkg #{ARGV.join(" ")}" 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /config/initializers/carrierwave.rb: -------------------------------------------------------------------------------- 1 | if Rails.env.production? 2 | CarrierWave.configure do |config| 3 | config.fog_credentials = { 4 | provider: "AWS", 5 | aws_access_key_id: ENV["S3_KEY"], 6 | aws_secret_access_key: ENV["S3_SECRET"], 7 | region: ENV["S3_REGION"] 8 | } 9 | 10 | config.fog_directory = ENV["S3_BUCKET"] 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /config/webpack/loaders/assets.js: -------------------------------------------------------------------------------- 1 | const { env, publicPath } = require("../configuration.js") 2 | 3 | module.exports = { 4 | test: /\.(jpg|jpeg|png|gif|svg|eot|ttf|woff|woff2)$/i, 5 | use: [{ 6 | loader: "file-loader", 7 | options: { 8 | publicPath, 9 | name: env.NODE_ENV === "production" ? "[name]-[hash].[ext]" : "[name].[ext]", 10 | }, 11 | }], 12 | } 13 | -------------------------------------------------------------------------------- /db/migrate/20170724031825_create_photos.rb: -------------------------------------------------------------------------------- 1 | class CreatePhotos < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :photos do |t| 4 | t.string :image 5 | t.text :caption 6 | t.belongs_to :user 7 | t.integer :comments_count, default: 0 8 | t.integer :likes_count, default: 0 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Instaqram 5 | <%= csrf_meta_tags %> 6 | 7 | <%= stylesheet_link_tag 'application', media: 'all' %> 8 | <%= javascript_include_tag 'application' %> 9 | 10 | 11 | 12 | <%= yield %> 13 | 14 | <%= javascript_pack_tag "application" %> 15 | 16 | 17 | -------------------------------------------------------------------------------- /config/webpack/test.js: -------------------------------------------------------------------------------- 1 | // Note: You must restart bin/webpack-dev-server for changes to take effect 2 | 3 | const merge = require("webpack-merge") 4 | const sharedConfig = require("./shared.js") 5 | 6 | module.exports = merge(sharedConfig, { 7 | entry: { 8 | application: [ 9 | "react-hot-loader/patch", 10 | "./app/javascript/packs/application.dev.js", 11 | ], 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /app/graphql/functions/delete_photo.rb: -------------------------------------------------------------------------------- 1 | class Functions::DeletePhoto < GraphQL::Function 2 | argument :id, !types.ID 3 | type Types::PhotoType 4 | 5 | def call(obj, args, ctx) 6 | if user = ctx[:current_user] 7 | photo = user.photos.find(args[:id]) 8 | photo.destroy 9 | photo 10 | else 11 | GraphQL::ExecutionError.new("Unauthorized") 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /config/initializers/kaminari_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | Kaminari.configure do |config| 3 | config.default_per_page = 12 4 | config.max_per_page = 24 5 | # config.window = 4 6 | # config.outer_window = 0 7 | # config.left = 0 8 | # config.right = 0 9 | # config.page_method_name = :page 10 | # config.param_name = :page 11 | # config.params_on_first_page = false 12 | end 13 | -------------------------------------------------------------------------------- /app/controllers/api/base_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::BaseController < ActionController::Base 2 | before_action :authenticate_by_auth_token 3 | 4 | private 5 | 6 | def authenticate_by_auth_token 7 | auth_token = request.headers["Authorization"].try(:sub, /Bearer /, "") 8 | 9 | if auth_token && user = User.authenticate(auth_token) 10 | sign_in user, store: false 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/graphql/instaqram_schema.rb: -------------------------------------------------------------------------------- 1 | InstaqramSchema = GraphQL::Schema.define do 2 | mutation(Types::MutationType) 3 | query(Types::QueryType) 4 | 5 | rescue_from ActiveRecord::RecordNotFound do 6 | "Not Found" 7 | end 8 | 9 | rescue_from ActiveRecord::RecordInvalid do |e| 10 | e.message 11 | end 12 | 13 | rescue_from ActiveRecord::RecordNotUnique do |e| 14 | e.message 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20170731032723_create_images.rb: -------------------------------------------------------------------------------- 1 | class CreateImages < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :images do |t| 4 | t.integer :imageable_id 5 | t.string :imageable_type 6 | t.string :file 7 | t.belongs_to :user 8 | 9 | t.timestamps 10 | end 11 | add_index :images, :imageable_id 12 | add_index :images, :imageable_type 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20170807133058_add_confirmable_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddConfirmableToUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :confirmation_token, :string 4 | add_column :users, :confirmed_at, :datetime 5 | add_column :users, :confirmation_sent_at, :datetime 6 | add_column :users, :unconfirmed_email, :string 7 | 8 | add_index :users, :confirmation_token, unique: true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/assets/javascripts/cable.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command. 3 | // 4 | //= require action_cable 5 | //= require_self 6 | //= require_tree ./channels 7 | 8 | (function() { 9 | this.App || (this.App = {}); 10 | 11 | App.cable = ActionCable.createConsumer(); 12 | 13 | }).call(this); 14 | -------------------------------------------------------------------------------- /app/javascript/packs/reducers/user.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_USER_BY_TOKEN, 3 | USER_LOGOUT, 4 | USER_UPDATED, 5 | } from "../constants" 6 | 7 | function user(state = null, action) { 8 | switch (action.type) { 9 | case SET_USER_BY_TOKEN: 10 | case USER_UPDATED: 11 | return action.user 12 | case USER_LOGOUT: 13 | return null 14 | default: 15 | return state 16 | } 17 | } 18 | 19 | export default user 20 | -------------------------------------------------------------------------------- /app/graphql/functions/login.rb: -------------------------------------------------------------------------------- 1 | class Functions::Login < GraphQL::Function 2 | argument :user, !Types::AuthType 3 | type Types::TokenType 4 | 5 | def call(obj, args, ctx) 6 | user = User.find_for_database_authentication(email: args[:user][:email]) 7 | 8 | # TODO enable touch the trackable user 9 | if user && user.valid_password?(args[:user][:password]) 10 | user 11 | # sign_in user 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/graphql/functions/followship_query.rb: -------------------------------------------------------------------------------- 1 | class Functions::FollowshipQuery < GraphQL::Function 2 | attr_reader :query 3 | 4 | def initialize(query) 5 | @query = query 6 | end 7 | 8 | type types[Types::UserFollowType] 9 | 10 | def call(obj, args, ctx) 11 | if current_user = ctx[:current_user] 12 | current_user.send(query) 13 | else 14 | GraphQL::ExecutionError.new("Unauthorized") 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | if Rails.env.development? 3 | mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/api/graphql" 4 | end 5 | 6 | namespace :api, default: { format: :json } do 7 | post "/graphql", to: "graphql#execute" 8 | post "/images", to: "images#create" 9 | end 10 | 11 | devise_for :users 12 | 13 | root to: "pages#home" 14 | 15 | get "*path", to: "pages#home" 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20170802034248_create_followships.rb: -------------------------------------------------------------------------------- 1 | class CreateFollowships < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :followships do |t| 4 | t.integer :follower_id 5 | t.integer :following_id 6 | 7 | t.timestamps 8 | end 9 | add_index :followships, :follower_id 10 | add_index :followships, :following_id 11 | add_index :followships, [:follower_id, :following_id], unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/javascript/packs/actions/alert.js: -------------------------------------------------------------------------------- 1 | import { 2 | HIDE_ALERT, 3 | SHOW_ALERT, 4 | } from "../constants" 5 | 6 | export function hideAlert() { 7 | return (dispatch) => { 8 | dispatch({ 9 | type: HIDE_ALERT, 10 | }) 11 | } 12 | } 13 | 14 | export function showAlert(message, action = null) { 15 | return (dispatch) => { 16 | dispatch({ 17 | type: SHOW_ALERT, 18 | message, 19 | action, 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "browsers": "> 1%", 9 | "uglify": true 10 | }, 11 | "useBuiltIns": true 12 | } 13 | ], 14 | "react", 15 | "stage-0" 16 | ], 17 | "plugins": [ 18 | "syntax-dynamic-import", 19 | [ 20 | "transform-class-properties", 21 | { 22 | "spec": true 23 | } 24 | ] 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /app/graphql/functions/comment_photo.rb: -------------------------------------------------------------------------------- 1 | class Functions::CommentPhoto < GraphQL::Function 2 | argument :content, !types.String 3 | argument :photo_id, !types.ID 4 | 5 | type Types::CommentType 6 | 7 | def call(obj, args, ctx) 8 | if user = ctx[:current_user] 9 | photo = Photo.find(args[:photo_id]) 10 | 11 | photo.comments.create!(content: args[:content], user: user) 12 | else 13 | GraphQL::ExecutionError.new("Unauthorized") 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20170804085248_add_followings_count_and_followers_count_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddFollowingsCountAndFollowersCountToUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :followings_count, :integer, default: 0 4 | add_column :users, :followers_count, :integer, default: 0 5 | 6 | User.find_each do |user| 7 | User.reset_counters(user.id, :followers_references) 8 | User.reset_counters(user.id, :followings_references) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/javascript/packs/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux" 2 | import { routerReducer as routing } from "react-router-redux" 3 | import { reducer as formReducer } from "redux-form" 4 | 5 | import { apolloReducer } from "../apollo" 6 | import alert from "./alert" 7 | import user from "./user" 8 | 9 | const combinedReducers = combineReducers({ 10 | routing, 11 | form: formReducer, 12 | apollo: apolloReducer, 13 | alert, 14 | currentUser: user, 15 | }) 16 | 17 | export default combinedReducers 18 | -------------------------------------------------------------------------------- /app/javascript/packs/utils/upload.js: -------------------------------------------------------------------------------- 1 | const token = () => localStorage.getItem("auth_token") 2 | const config = (obj) => { 3 | const formData = new FormData() 4 | formData.append("file", obj.file) 5 | formData.append("imageable_type", obj.type) 6 | 7 | return { 8 | method: "POST", 9 | headers: { 10 | authorization: `Bearer ${token()}`, 11 | }, 12 | body: formData, 13 | } 14 | } 15 | const upload = (obj) => { 16 | return fetch("/api/images", config(obj)) 17 | } 18 | 19 | export default upload 20 | -------------------------------------------------------------------------------- /app/graphql/types/human_time_type.rb: -------------------------------------------------------------------------------- 1 | Types::HumanTimeType = GraphQL::ObjectType.define do 2 | name "HumanTime" 3 | 4 | field :format, types.String do 5 | resolve ->(time, args, ctx) { 6 | time.to_date.to_formatted_s(:db) 7 | } 8 | end 9 | 10 | field :human, types.String do 11 | resolve ->(time, args, ctx) { 12 | time.to_date.to_formatted_s(:long) 13 | } 14 | end 15 | 16 | field :unix, types.Int do 17 | resolve ->(time, args, ctx) { 18 | time.utc.to_i * 1000 19 | } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config/webpack/loaders/sass.js: -------------------------------------------------------------------------------- 1 | const ExtractTextPlugin = require("extract-text-webpack-plugin") 2 | const { env } = require("../configuration.js") 3 | 4 | module.exports = { 5 | test: /\.(scss|sass|css)$/i, 6 | use: ExtractTextPlugin.extract({ 7 | fallback: "style-loader", 8 | use: [ 9 | { loader: "css-loader", options: { minimize: env.NODE_ENV === "production" } }, 10 | { loader: "postcss-loader", options: { sourceMap: true } }, 11 | "resolve-url-loader", 12 | { loader: "sass-loader", options: { sourceMap: true } }, 13 | ], 14 | }), 15 | } 16 | -------------------------------------------------------------------------------- /app/javascript/packs/queries/get_user.js: -------------------------------------------------------------------------------- 1 | import { gql } from "react-apollo" 2 | 3 | export default gql` 4 | query user($username: String!, $page: Int){ 5 | user(username: $username){ 6 | id 7 | username 8 | name 9 | caption 10 | photos_count 11 | followed 12 | followings_count 13 | followers_count 14 | image { 15 | small 16 | } 17 | photos(page: $page) { 18 | id 19 | caption 20 | image { 21 | original 22 | medium 23 | } 24 | } 25 | } 26 | } 27 | ` 28 | -------------------------------------------------------------------------------- /app/graphql/functions/like_photo.rb: -------------------------------------------------------------------------------- 1 | class Functions::LikePhoto < GraphQL::Function 2 | argument :photo_id, !types.ID 3 | type Types::PhotoType 4 | 5 | def call(obj, args, ctx) 6 | if user = ctx[:current_user] 7 | photo = Photo.find(args[:photo_id]) 8 | like = photo.likes.where(user: user).first 9 | 10 | if like.present? && like.persisted? 11 | like.destroy 12 | else 13 | photo.likes.create!(user: user) 14 | end 15 | 16 | photo.reload 17 | else 18 | GraphQL::ExecutionError.new("Unauthorized") 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/graphql/types/mutation_type.rb: -------------------------------------------------------------------------------- 1 | Types::MutationType = GraphQL::ObjectType.define do 2 | name "Mutation" 3 | 4 | field :register, function: Functions::Register.new 5 | field :login, function: Functions::Login.new 6 | field :updateProfile, function: Functions::UpdateProfile.new 7 | 8 | field :postPhoto, function: Functions::PostPhoto.new 9 | field :commentPhoto, function: Functions::CommentPhoto.new 10 | field :likePhoto, function: Functions::LikePhoto.new 11 | field :deletePhoto, function: Functions::DeletePhoto.new 12 | 13 | field :follow, function: Functions::Followship.new 14 | end 15 | -------------------------------------------------------------------------------- /app/javascript/packs/constants.js: -------------------------------------------------------------------------------- 1 | export const SHOW_ALERT = "SHOW_ALERT" 2 | export const HIDE_ALERT = "HIDE_ALERT" 3 | 4 | export const SET_USER_BY_TOKEN = "SET_USER_BY_TOKEN" 5 | export const USER_LOGOUT = "USER_LOGOUT" 6 | export const USER_UPDATED = "USER_UPDATED" 7 | 8 | export const APOLLO_QUERY_RESULT = "APOLLO_QUERY_RESULT" 9 | export const APOLLO_MUTATION_RESULT = "APOLLO_MUTATION_RESULT" 10 | export const APOLLO_QUERY_RESULT_CLIENT = "APOLLO_QUERY_RESULT_CLIENT" 11 | export const APOLLO_MUTATION_RESULT_CLIENT = "APOLLO_MUTATION_RESULT_CLIENT" 12 | 13 | export const LOCATION_CHANGE = "@@router/LOCATION_CHANGE" 14 | -------------------------------------------------------------------------------- /app/graphql/functions/register.rb: -------------------------------------------------------------------------------- 1 | class Functions::Register < GraphQL::Function 2 | RegisterInput = GraphQL::InputObjectType.define do 3 | name "Register" 4 | 5 | argument :email, !types.String 6 | argument :password, !types.String 7 | argument :name, !types.String 8 | argument :username, !types.String 9 | end 10 | 11 | argument :user, !RegisterInput 12 | type Types::TokenType 13 | 14 | def call(obj, args, ctx) 15 | params = args[:user].to_h 16 | 17 | if image = ctx[:files].try(:first) 18 | params[:image] = image 19 | end 20 | 21 | User.create!(params) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/factories/photos.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: photos 4 | # 5 | # id :integer not null, primary key 6 | # caption :text 7 | # user_id :integer 8 | # comments_count :integer default(0) 9 | # likes_count :integer default(0) 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | # Indexes 14 | # 15 | # index_photos_on_user_id (user_id) 16 | # 17 | 18 | FactoryGirl.define do 19 | factory :photo do 20 | association :user 21 | 22 | caption { Faker::Lorem.sentence } 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/models/photo_spec.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: photos 4 | # 5 | # id :integer not null, primary key 6 | # caption :text 7 | # user_id :integer 8 | # comments_count :integer default(0) 9 | # likes_count :integer default(0) 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | # Indexes 14 | # 15 | # index_photos_on_user_id (user_id) 16 | # 17 | 18 | require 'rails_helper' 19 | 20 | RSpec.describe Photo, type: :model do 21 | pending "add some examples to (or delete) #{__FILE__}" 22 | end 23 | -------------------------------------------------------------------------------- /app/javascript/packs/application.dev.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import injectTapEventPlugin from "react-tap-event-plugin" 4 | import { AppContainer } from "react-hot-loader" 5 | import "babel-polyfill" 6 | 7 | import Main from "./Main" 8 | 9 | injectTapEventPlugin() 10 | 11 | const render = (Component) => { 12 | ReactDOM.render( 13 | 14 | 15 | , 16 | document.getElementById("root"), 17 | ) 18 | } 19 | 20 | render(Main) 21 | 22 | if (module.hot) { 23 | module.hot.accept("./Main", () => { 24 | render(Main) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /app/javascript/packs/reducers/alert.js: -------------------------------------------------------------------------------- 1 | import { 2 | SHOW_ALERT, 3 | HIDE_ALERT, 4 | } from "../constants" 5 | 6 | const initializeState = { 7 | message: "", 8 | open: false, 9 | action: null, 10 | } 11 | 12 | function alert(state = initializeState, action) { 13 | switch (action.type) { 14 | case SHOW_ALERT: 15 | return { 16 | message: action.message, 17 | open: true, 18 | action: action.action, 19 | } 20 | case HIDE_ALERT: 21 | return { 22 | ...initializeState, 23 | } 24 | default: 25 | return state 26 | 27 | } 28 | } 29 | 30 | export default alert 31 | -------------------------------------------------------------------------------- /app/javascript/packs/queries/get_feed.js: -------------------------------------------------------------------------------- 1 | import { gql } from "react-apollo" 2 | 3 | export default gql` 4 | query feed($page: Int) { 5 | feed(page: $page) { 6 | id 7 | caption 8 | comments_count 9 | likes_count 10 | liked 11 | image { 12 | medium 13 | } 14 | created_at { 15 | human 16 | unix 17 | } 18 | comments { 19 | id 20 | content 21 | user { 22 | id 23 | username 24 | } 25 | } 26 | user { 27 | id 28 | username 29 | image { 30 | thumb 31 | } 32 | } 33 | } 34 | } 35 | ` 36 | -------------------------------------------------------------------------------- /app/javascript/packs/queries/get_photo.js: -------------------------------------------------------------------------------- 1 | import { gql } from "react-apollo" 2 | 3 | export default gql` 4 | query photo($id: ID!){ 5 | photo(id: $id) { 6 | id 7 | caption 8 | likes_count 9 | comments_count 10 | liked 11 | created_at { 12 | human 13 | } 14 | image { 15 | original 16 | large 17 | } 18 | user { 19 | id 20 | username 21 | image { 22 | thumb 23 | } 24 | } 25 | comments { 26 | id 27 | content 28 | user { 29 | id 30 | username 31 | } 32 | } 33 | } 34 | } 35 | ` 36 | -------------------------------------------------------------------------------- /app/javascript/packs/mutations/index.js: -------------------------------------------------------------------------------- 1 | import register from "./register" 2 | import login from "./login" 3 | import updateProfile from "./update_profile" 4 | import postPhoto from "./post_photo" 5 | import likePhoto from "./like_photo" 6 | import commentPhoto from "./comment_photo" 7 | import followUser from "./follow_user" 8 | import deletePhoto from "./delete_photo" 9 | 10 | export const REGISTER = register 11 | export const LOGIN = login 12 | export const UPDATE_PROFILE = updateProfile 13 | export const POST_PHOTO = postPhoto 14 | export const LIKE_PHOTO = likePhoto 15 | export const COMMENT_PHOTO = commentPhoto 16 | export const FOLLOW_USER = followUser 17 | export const DELETE_PHOTO = deletePhoto 18 | -------------------------------------------------------------------------------- /spec/factories/followships.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: followships 4 | # 5 | # id :integer not null, primary key 6 | # follower_id :integer 7 | # following_id :integer 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | # Indexes 12 | # 13 | # index_followships_on_follower_id (follower_id) 14 | # index_followships_on_follower_id_and_following_id (follower_id,following_id) UNIQUE 15 | # index_followships_on_following_id (following_id) 16 | # 17 | 18 | FactoryGirl.define do 19 | factory :followship do 20 | follower_id 1 21 | following_id 1 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/support/controller_helpers.rb: -------------------------------------------------------------------------------- 1 | module ControllerHelpers 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | include Devise::Test::ControllerHelpers 6 | end 7 | 8 | def authorize(user) 9 | request.headers["Authorization"] = "Bearer #{user.auth_token}" 10 | end 11 | 12 | def json_parsed(obj) 13 | JSON.parse(obj).deep_symbolize_keys 14 | end 15 | 16 | def fixture_image(index = 1) 17 | fixture_file_upload("images/example#{index}.jpg", "image/jpg") 18 | end 19 | 20 | %w[get post patch put delete].each do |m| 21 | define_method("auth_#{m}") do |user, action, **opts| 22 | authorize(user) 23 | send(m, action, opts) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/javascript/packs/mutations/update_profile.js: -------------------------------------------------------------------------------- 1 | import { gql } from "react-apollo" 2 | 3 | // export default gql` 4 | // mutation updateProfile($user: UpdateProfile!) { 5 | // updateProfile(user: $user) { 6 | // id 7 | // name 8 | // email 9 | // caption 10 | // website 11 | // birthdate 12 | // username 13 | // image { 14 | // thumb 15 | // small 16 | // medium 17 | // large 18 | // original 19 | // } 20 | // } 21 | // } 22 | // ` 23 | 24 | export default gql` 25 | mutation updateProfile($user: UpdateProfile!) { 26 | updateProfile(user: $user) { 27 | auth_token 28 | } 29 | } 30 | ` 31 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join('node_modules') 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /app/javascript/packs/apollo.js: -------------------------------------------------------------------------------- 1 | import { ApolloClient, createNetworkInterface } from "react-apollo" 2 | 3 | const networkInterface = createNetworkInterface({ 4 | uri: "/api/graphql", 5 | }) 6 | 7 | networkInterface.use([{ 8 | applyMiddleware(req, next) { 9 | if (!req.options.headers) { 10 | req.options.headers = {} 11 | } 12 | 13 | const token = window.localStorage.getItem("auth_token") 14 | req.options.headers.authorization = token ? `Bearer ${token}` : null 15 | next() 16 | }, 17 | }]) 18 | 19 | export const apolloClient = new ApolloClient({ networkInterface }) 20 | export const apolloReducer = apolloClient.reducer() 21 | export const apolloMiddleware = apolloClient.middleware() 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "plugins": [ 4 | "react", 5 | "jsx-a11y", 6 | "import" 7 | ], 8 | "parser": "babel-eslint", 9 | "parserOptions": { 10 | "ecmaFeatures": { 11 | "jsx": true 12 | } 13 | }, 14 | "env": { 15 | "browser": true, 16 | "node": true 17 | }, 18 | "rules": { 19 | "arrow-body-style": 0, 20 | "quotes": ["error", "double"], 21 | "jsx-quotes": ["error", "prefer-double"], 22 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 23 | "prefer-arrow-callback": ["error"], 24 | "react/forbid-prop-types": 0, 25 | "semi": ["error", "never"], 26 | "arrow-parens": ["error", "always"] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /config/webpacker.yml: -------------------------------------------------------------------------------- 1 | # Note: You must restart bin/webpack-dev-server for changes to take effect 2 | 3 | default: &default 4 | source_path: app/javascript 5 | source_entry_path: packs 6 | public_output_path: packs 7 | 8 | extensions: 9 | - .coffee 10 | - .erb 11 | - .js 12 | - .jsx 13 | - .ts 14 | - .vue 15 | - .sass 16 | - .scss 17 | - .css 18 | - .png 19 | - .svg 20 | - .gif 21 | - .jpeg 22 | - .jpg 23 | 24 | development: 25 | <<: *default 26 | 27 | dev_server: 28 | host: 0.0.0.0 29 | port: 8080 30 | https: false 31 | 32 | test: 33 | <<: *default 34 | 35 | public_output_path: packs-test 36 | 37 | production: 38 | <<: *default 39 | -------------------------------------------------------------------------------- /app/graphql/functions/followship.rb: -------------------------------------------------------------------------------- 1 | class Functions::Followship < GraphQL::Function 2 | argument :user_id, !types.ID 3 | type types.Boolean 4 | 5 | def call(obj, args, ctx) 6 | if @current_user = ctx[:current_user] 7 | @user = User.find(args[:user_id]) 8 | 9 | if followship 10 | @current_user.followings.delete(@user) 11 | else 12 | @current_user.followings << @user 13 | @current_user.save 14 | end 15 | 16 | followship && followship.persisted? 17 | else 18 | GraphQL::ExecutionError.new("Unauthorized") 19 | end 20 | end 21 | 22 | def followship 23 | ::Followship.where(follower: @current_user, following: @user).first 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/models/followship_spec.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: followships 4 | # 5 | # id :integer not null, primary key 6 | # follower_id :integer 7 | # following_id :integer 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | # Indexes 12 | # 13 | # index_followships_on_follower_id (follower_id) 14 | # index_followships_on_follower_id_and_following_id (follower_id,following_id) UNIQUE 15 | # index_followships_on_following_id (following_id) 16 | # 17 | 18 | require 'rails_helper' 19 | 20 | RSpec.describe Followship, type: :model do 21 | pending "add some examples to (or delete) #{__FILE__}" 22 | end 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | - Ruby version **2.3.3** 2 | - Rails version **5.1.2** 3 | - React version **15.6.1** 4 | 5 | ## Installation 6 | 7 | ### Install Dependencies 8 | - `bundle install` 9 | - `yarn install` 10 | 11 | ### Setup ENV vars 12 | - copy _config/application.yml.example_ to _config/application.yml_ and fill the value 13 | 14 | ### Database 15 | - `rake db:create` 16 | - `rake db:migrate` 17 | 18 | ## Run Application 19 | 20 | ### Development 21 | - `rails s` 22 | - `./bin/webpack-dev-server --hot` 23 | - open in browser `localhost:3000` 24 | 25 | ### Production 26 | - `rake assets:precompile RAILS_ENV=production` will automatically run yarn and webpack compile 27 | - `rails s -e production` 28 | - open in browser `localhost:3000` 29 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /app/javascript/packs/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from "redux" 2 | import thunk from "redux-thunk" 3 | import { createBrowserHistory } from "history" 4 | import { routerMiddleware } from "react-router-redux" 5 | 6 | import rootReducers from "../reducers" 7 | import { apolloMiddleware } from "../apollo" 8 | 9 | const enhancers = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() 10 | 11 | const history = createBrowserHistory() 12 | const routeMiddleware = routerMiddleware(history) 13 | const middleware = applyMiddleware(thunk, routeMiddleware, apolloMiddleware) 14 | 15 | const store = createStore(rootReducers, enhancers, middleware) 16 | 17 | export { 18 | store, 19 | history, 20 | } 21 | -------------------------------------------------------------------------------- /app/models/comment.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: comments 4 | # 5 | # id :integer not null, primary key 6 | # user_id :integer 7 | # photo_id :integer 8 | # content :text 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | # Indexes 13 | # 14 | # index_comments_on_photo_id (photo_id) 15 | # index_comments_on_user_id (user_id) 16 | # 17 | # Foreign Keys 18 | # 19 | # fk_rails_... (photo_id => photos.id) 20 | # fk_rails_... (user_id => users.id) 21 | # 22 | 23 | class Comment < ApplicationRecord 24 | belongs_to :user 25 | belongs_to :photo, counter_cache: true 26 | 27 | validates :content, :user, :photo, presence: true 28 | end 29 | -------------------------------------------------------------------------------- /app/javascript/packs/Main.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { MuiThemeProvider, createMuiTheme } from "material-ui/styles" 3 | import { ConnectedRouter } from "react-router-redux" 4 | import { ApolloProvider } from "react-apollo" 5 | 6 | import { store, history } from "./store" 7 | import { apolloClient } from "./apollo" 8 | 9 | import Routes from "./Routes" 10 | 11 | import styles from "./styles" 12 | 13 | const theme = createMuiTheme(styles) 14 | 15 | export default () => { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's 5 | // vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require rails-ujs 14 | //= require_tree . 15 | -------------------------------------------------------------------------------- /app/models/like.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: likes 4 | # 5 | # id :integer not null, primary key 6 | # user_id :integer 7 | # photo_id :integer 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | # Indexes 12 | # 13 | # index_likes_on_photo_id (photo_id) 14 | # index_likes_on_user_id (user_id) 15 | # 16 | # Foreign Keys 17 | # 18 | # fk_rails_... (photo_id => photos.id) 19 | # fk_rails_... (user_id => users.id) 20 | # 21 | 22 | class Like < ApplicationRecord 23 | belongs_to :user 24 | belongs_to :photo, counter_cache: true 25 | 26 | validates :user, uniqueness: { scope: :photo, message: "already liked" } 27 | validates :photo, :user, presence: true 28 | end 29 | -------------------------------------------------------------------------------- /app/models/photo.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: photos 4 | # 5 | # id :integer not null, primary key 6 | # caption :text 7 | # user_id :integer 8 | # comments_count :integer default(0) 9 | # likes_count :integer default(0) 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | # Indexes 14 | # 15 | # index_photos_on_user_id (user_id) 16 | # 17 | 18 | class Photo < ApplicationRecord 19 | paginates_per 12 20 | max_paginates_per 24 21 | 22 | belongs_to :user, counter_cache: true 23 | 24 | has_many :comments, dependent: :destroy 25 | has_many :likes, dependent: :destroy 26 | has_many :images, as: :imageable, dependent: :destroy 27 | end 28 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | /tmp/* 17 | !/log/.keep 18 | !/tmp/.keep 19 | 20 | /node_modules 21 | /yarn-error.log 22 | 23 | .byebug_history 24 | /public/packs 25 | /node_modules 26 | 27 | /public/uploads/ 28 | 29 | # Ignore application configuration 30 | /config/application.yml 31 | 32 | /coverage 33 | -------------------------------------------------------------------------------- /app/javascript/packs/components/forms/validations/index.js: -------------------------------------------------------------------------------- 1 | const EMAIL_REGEX = /^[A-Z0-9._-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i 2 | const USERNAME_REGEX = /^[a-zA-Z0-9]+$/ 3 | 4 | export const required = (value) => ( 5 | value == null ? "Required" : undefined 6 | ) 7 | 8 | export const email = (value) => ( 9 | value && EMAIL_REGEX.test(value) ? undefined : "Invalid email" 10 | ) 11 | 12 | export const username = (value) => ( 13 | value && USERNAME_REGEX.test(value) ? undefined : "Invalid username value" 14 | ) 15 | 16 | export const confirmation = (target) => ( 17 | (value) => ( 18 | value === target ? undefined : "Not match" 19 | ) 20 | ) 21 | 22 | export const min = (count) => ( 23 | (value) => ( 24 | value && value.length < count ? `Minimal ${count} characters` : undefined 25 | ) 26 | ) 27 | -------------------------------------------------------------------------------- /app/javascript/packs/styles/index.js: -------------------------------------------------------------------------------- 1 | import blue from "material-ui/colors/blue" 2 | import blueGrey from "material-ui/colors/blueGrey" 3 | import createPalette from "material-ui/styles/palette" 4 | 5 | export default { 6 | palette: createPalette({ 7 | primary: blue, 8 | accent: blueGrey, 9 | }), 10 | lighter: blue[50], 11 | light: blue[500], 12 | dark: blue[900], 13 | upload: { 14 | wrapper: { 15 | height: "300px", 16 | width: "360px", 17 | border: "none", 18 | marginBottom: "30px", 19 | }, 20 | placeholder: { 21 | width: "100%", 22 | height: "100%", 23 | cursor: "pointer", 24 | textAlign: "center", 25 | }, 26 | placeholderText: { 27 | lineHeight: "300px", 28 | textAlign: "center", 29 | }, 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /app/graphql/functions/post_photo.rb: -------------------------------------------------------------------------------- 1 | class Functions::PostPhoto < GraphQL::Function 2 | PhotoInput = GraphQL::InputObjectType.define do 3 | name "PostPhoto" 4 | 5 | argument :caption, types.String 6 | end 7 | 8 | argument :photo, !PhotoInput 9 | argument :image_id, !types.ID 10 | type Types::PhotoType 11 | 12 | def call(obj, args, ctx) 13 | if user = ctx[:current_user] 14 | params = args[:photo].to_h 15 | 16 | if image = user.temp_images.find(args[:image_id]) 17 | photo = user.photos.new(params) 18 | photo.images = [image] 19 | photo.save 20 | 21 | photo 22 | else 23 | raise GraphQL::ExecutionError.new("image required") 24 | end 25 | else 26 | raise GraphQL::ExecutionError.new("Unauthorized") 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "simplecov" 2 | require "database_cleaner" 3 | require "factory_girl" 4 | require "devise" 5 | 6 | SimpleCov.start "rails" 7 | 8 | RSpec.configure do |config| 9 | config.before(:suite) do 10 | DatabaseCleaner.strategy = :transaction 11 | DatabaseCleaner.clean_with(:truncation) 12 | end 13 | 14 | config.around(:each) do |example| 15 | DatabaseCleaner.cleaning do 16 | example.run 17 | end 18 | end 19 | 20 | config.include FactoryGirl::Syntax::Methods 21 | 22 | config.expect_with :rspec do |expectations| 23 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 24 | end 25 | 26 | config.mock_with :rspec do |mocks| 27 | mocks.verify_partial_doubles = true 28 | end 29 | 30 | config.shared_context_metadata_behavior = :apply_to_host_groups 31 | end 32 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: postgresql 9 | encoding: unicode 10 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 11 | username: <%= ENV['DB_USERNAME'] %> 12 | password: <%= ENV['DB_PASSWORD'] %> 13 | host: <%= ENV['DB_HOST'] %> 14 | 15 | development: 16 | <<: *default 17 | database: <%= ENV['DB_NAME'] %> 18 | 19 | # Warning: The database defined as "test" will be erased and 20 | # re-generated from your development database when you run "rake". 21 | # Do not set this db to the same as development or production. 22 | test: 23 | <<: *default 24 | database: <%= ENV['DB_TEST_NAME'] %> 25 | 26 | production: 27 | <<: *default 28 | database: <%= ENV['DB_NAME'] %> 29 | -------------------------------------------------------------------------------- /app/controllers/api/images_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::ImagesController < Api::BaseController 2 | before_action :auth_user! 3 | 4 | def create 5 | @image = current_user.temp_images.new(image_params) 6 | @image.imageable = current_user if @image.imageable_type == "User" 7 | 8 | if @image.save 9 | render json: @image.decorate_json, status: 201 10 | else 11 | render json: image_errors, status: 422 12 | end 13 | end 14 | 15 | private 16 | 17 | def auth_user! 18 | render json: { errors: [{ 19 | message: "Unauthenticated" 20 | }] }, status: 401 unless user_signed_in? 21 | end 22 | 23 | def image_params 24 | params.permit(:file, :imageable_type) 25 | end 26 | 27 | def image_errors 28 | { 29 | errors: @image.errors.full_messages.map { |i| { message: i } } 30 | } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /bin/webpack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $stdout.sync = true 3 | 4 | require "shellwords" 5 | require "yaml" 6 | 7 | ENV["RAILS_ENV"] ||= "development" 8 | RAILS_ENV = ENV["RAILS_ENV"] 9 | 10 | ENV["NODE_ENV"] ||= RAILS_ENV 11 | NODE_ENV = ENV["NODE_ENV"] 12 | 13 | APP_PATH = File.expand_path("../", __dir__) 14 | NODE_MODULES_PATH = File.join(APP_PATH, "node_modules") 15 | WEBPACK_CONFIG = File.join(APP_PATH, "config/webpack/#{NODE_ENV}.js") 16 | 17 | unless File.exist?(WEBPACK_CONFIG) 18 | puts "Webpack configuration not found." 19 | puts "Please run bundle exec rails webpacker:install to install webpacker" 20 | exit! 21 | end 22 | 23 | newenv = { "NODE_PATH" => NODE_MODULES_PATH.shellescape } 24 | cmdline = ["yarn", "run", "webpack", "--", "--config", WEBPACK_CONFIG] + ARGV 25 | 26 | Dir.chdir(APP_PATH) do 27 | exec newenv, *cmdline 28 | end 29 | -------------------------------------------------------------------------------- /app/javascript/packs/components/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Route, Redirect } from "react-router-dom" 3 | import { connect } from "react-redux" 4 | import PropTypes from "prop-types" 5 | 6 | const PrivateRoute = ({ component: Component, currentUser, ...rest }) => ( 7 | ( 10 | currentUser ? ( 11 | 12 | ) : ( 13 | 18 | ) 19 | )} 20 | /> 21 | ) 22 | 23 | PrivateRoute.defaultProps = { 24 | currentUser: null, 25 | } 26 | 27 | PrivateRoute.propTypes = { 28 | currentUser: PropTypes.object, 29 | component: PropTypes.any.isRequired, 30 | } 31 | 32 | export default connect( 33 | (state) => state, 34 | )(PrivateRoute) 35 | -------------------------------------------------------------------------------- /app/uploaders/image_uploader.rb: -------------------------------------------------------------------------------- 1 | class ImageUploader < CarrierWave::Uploader::Base 2 | include CarrierWave::MiniMagick 3 | 4 | storage Rails.env.production? ? :fog : :file 5 | 6 | def store_dir 7 | "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" 8 | end 9 | 10 | def default_url(*args) 11 | if model.imageable_type == "User" 12 | ActionController::Base.helpers.asset_path("defaults/profile.png") 13 | end 14 | end 15 | 16 | version :thumb do 17 | process resize_to_fit: [100, 100] 18 | end 19 | 20 | version :small do 21 | process resize_to_fit: [300, 300] 22 | end 23 | 24 | version :medium do 25 | process resize_to_fit: [600, 600] 26 | end 27 | 28 | version :large do 29 | process resize_to_fit: [800, 800] 30 | end 31 | 32 | def extension_whitelist 33 | %w[jpg jpeg png] 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/models/followship.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: followships 4 | # 5 | # id :integer not null, primary key 6 | # follower_id :integer 7 | # following_id :integer 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | # Indexes 12 | # 13 | # index_followships_on_follower_id (follower_id) 14 | # index_followships_on_follower_id_and_following_id (follower_id,following_id) UNIQUE 15 | # index_followships_on_following_id (following_id) 16 | # 17 | 18 | class Followship < ApplicationRecord 19 | with_options class_name: "User" do |u| 20 | u.belongs_to :follower, counter_cache: :followings_count 21 | u.belongs_to :following, counter_cache: :followers_count 22 | end 23 | 24 | validates :follower, uniqueness: { scope: :following, message: "already follow user" } 25 | end 26 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /spec/factories/images.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 | FactoryGirl.define do 21 | factory :image do 22 | transient do 23 | image_path { Rails.root.join("spec", "fixtures", "images", "example1.jpg") } 24 | uploaded_image { Rack::Test::UploadedFile.new(image_path, "image/jpeg") } 25 | end 26 | 27 | association :user 28 | 29 | file { uploaded_image } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at http://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /app/graphql/types/photo_type.rb: -------------------------------------------------------------------------------- 1 | Types::PhotoType = GraphQL::ObjectType.define do 2 | name "Photo" 3 | 4 | field :id, types.ID 5 | field :caption, types.String 6 | field :comments_count, types.Int 7 | field :likes_count, types.Int 8 | field :created_at, Types::HumanTimeType 9 | field :updated_at, Types::HumanTimeType 10 | 11 | field :liked, types.Boolean do 12 | resolve ->(obj, args, ctx) { 13 | if user = ctx[:current_user] 14 | !obj.likes.where(user_id: user.id).empty? 15 | else 16 | false 17 | end 18 | } 19 | end 20 | 21 | field :image, Types::ImageType do 22 | resolve ->(obj, args, ctx) { 23 | obj.images.first.file 24 | } 25 | end 26 | 27 | field :comments, types[Types::CommentType] do 28 | argument :size, types.Int, default_value: 6 29 | resolve ->(obj, args, ctx) { 30 | limit = args[:size] > 20 ? 20 : 6 31 | obj.comments.limit(limit) 32 | } 33 | end 34 | field :user, type: Types::UserType 35 | end 36 | -------------------------------------------------------------------------------- /app/graphql/types/user_type.rb: -------------------------------------------------------------------------------- 1 | Types::UserType = GraphQL::ObjectType.define do 2 | name "User" 3 | 4 | field :id, types.ID 5 | field :email, types.String 6 | field :username, types.String 7 | field :photos_count, types.Int 8 | field :followings_count, types.Int 9 | field :followers_count, types.Int 10 | field :image, Types::ImageType do 11 | resolve ->(obj, args, ctx) { 12 | obj.avatar.file 13 | } 14 | end 15 | field :website, types.String 16 | field :birthdate, types.String 17 | field :name, types.String 18 | field :caption, types.String 19 | 20 | field :followed, types.Boolean do 21 | resolve ->(obj, args, ctx) { 22 | user = ctx[:current_user] 23 | return false unless user 24 | 25 | user.followings.where(id: obj.id).present? 26 | } 27 | end 28 | 29 | field :photos do 30 | type types[Types::PhotoType] 31 | argument :page, types.Int, default_value: 1 32 | 33 | resolve ->(obj, args, ctx) { 34 | obj.photos.order(created_at: :desc).page(args[:page]) 35 | } 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /config/webpack/production.js: -------------------------------------------------------------------------------- 1 | // Note: You must restart bin/webpack-dev-server for changes to take effect 2 | 3 | /* eslint global-require: 0 */ 4 | 5 | const webpack = require("webpack") 6 | const merge = require("webpack-merge") 7 | const CompressionPlugin = require("compression-webpack-plugin") 8 | const sharedConfig = require("./shared.js") 9 | 10 | module.exports = merge(sharedConfig, { 11 | entry: { 12 | application: "./app/javascript/packs/application.prod.js", 13 | }, 14 | output: { filename: "application-[chunkhash].js" }, 15 | devtool: "source-map", 16 | stats: "normal", 17 | 18 | plugins: [ 19 | new webpack.optimize.UglifyJsPlugin({ 20 | minimize: true, 21 | sourceMap: true, 22 | 23 | compress: { 24 | warnings: false, 25 | }, 26 | 27 | output: { 28 | comments: false, 29 | }, 30 | }), 31 | 32 | new CompressionPlugin({ 33 | asset: "[path].gz[query]", 34 | algorithm: "gzip", 35 | test: /\.(js|css|html|json|ico|svg|eot|otf|ttf)$/, 36 | }), 37 | ], 38 | }) 39 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | require File.expand_path("../../config/environment", __FILE__) 3 | # Prevent database truncation if the environment is production 4 | abort("The Rails environment is running in production mode!") if Rails.env.production? 5 | require "spec_helper" 6 | require "rspec/rails" 7 | 8 | Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 9 | 10 | ActiveRecord::Migration.maintain_test_schema! 11 | 12 | RSpec.configure do |config| 13 | config.include ControllerHelpers, type: :controller 14 | 15 | config.before(:each, type: :controller) do 16 | @request.env["devise.mapping"] = Devise.mappings[:user] 17 | end 18 | 19 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 20 | 21 | config.use_transactional_fixtures = true 22 | 23 | config.infer_spec_type_from_file_location! 24 | 25 | config.filter_rails_from_backtrace! 26 | end 27 | 28 | Shoulda::Matchers.configure do |config| 29 | config.integrate do |with| 30 | with.test_framework :rspec 31 | with.library :rails 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/javascript/packs/components/forms/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Button } from "material-ui" 3 | import { Link } from "react-router-dom" 4 | import { TextField } from "@gfpacheco/redux-form-material-ui" 5 | import { Field, reduxForm, propTypes } from "redux-form" 6 | 7 | const LoginForm = ({ handleSubmit }) => { 8 | return ( 9 |
10 | 17 | 18 | 26 | 27 | 28 | 29 | 30 | ) 31 | } 32 | 33 | LoginForm.propTypes = { 34 | ...propTypes, 35 | } 36 | 37 | export default reduxForm({ 38 | form: "login", 39 | })(LoginForm) 40 | -------------------------------------------------------------------------------- /app/controllers/api/graphql_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::GraphqlController < Api::BaseController 2 | def execute 3 | variables = ensure_hash(params[:variables]) 4 | query = params[:query] 5 | operation_name = params[:operationName] 6 | result = InstaqramSchema.execute( 7 | query, 8 | variables: variables, 9 | context: context, 10 | operation_name: operation_name 11 | ) 12 | render json: result 13 | end 14 | 15 | private 16 | 17 | # Handle form data, JSON body, or a blank value 18 | def ensure_hash(ambiguous_param) 19 | case ambiguous_param 20 | when String 21 | if ambiguous_param.present? 22 | ensure_hash(JSON.parse(ambiguous_param)) 23 | else 24 | {} 25 | end 26 | when Hash, ActionController::Parameters 27 | ambiguous_param 28 | when nil 29 | {} 30 | else 31 | raise ArgumentError, "Unexpected parameter: #{ambiguous_param}" 32 | end 33 | end 34 | 35 | def context 36 | { 37 | current_user: current_user, 38 | files: params[:files] 39 | } 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Dimas J. Taniawan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/webpack/development.js: -------------------------------------------------------------------------------- 1 | // Note: You must restart bin/webpack-dev-server for changes to take effect 2 | 3 | const merge = require("webpack-merge") 4 | const sharedConfig = require("./shared.js") 5 | const { settings, output } = require("./configuration.js") 6 | 7 | module.exports = merge(sharedConfig, { 8 | entry: { 9 | application: [ 10 | "react-hot-loader/patch", 11 | "./app/javascript/packs/application.dev.js", 12 | ], 13 | }, 14 | devtool: "cheap-eval-source-map", 15 | 16 | stats: { 17 | errorDetails: true, 18 | }, 19 | 20 | output: { 21 | filename: "application.js", 22 | pathinfo: true, 23 | }, 24 | 25 | devServer: { 26 | clientLogLevel: "none", 27 | https: settings.dev_server.https, 28 | host: settings.dev_server.host, 29 | port: settings.dev_server.port, 30 | contentBase: output.path, 31 | publicPath: output.publicPath, 32 | compress: true, 33 | headers: { "Access-Control-Allow-Origin": "*" }, 34 | historyApiFallback: true, 35 | hot: true, 36 | inline: true, 37 | watchOptions: { 38 | ignored: /node_modules/, 39 | }, 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # Install JavaScript dependencies if using Yarn 22 | # system('bin/yarn') 23 | 24 | 25 | # puts "\n== Copying sample files ==" 26 | # unless File.exist?('config/database.yml') 27 | # cp 'config/database.yml.sample', 'config/database.yml' 28 | # end 29 | 30 | puts "\n== Preparing database ==" 31 | system! 'bin/rails db:setup' 32 | 33 | puts "\n== Removing old logs and tempfiles ==" 34 | system! 'bin/rails log:clear tmp:clear' 35 | 36 | puts "\n== Restarting application server ==" 37 | system! 'bin/rails restart' 38 | end 39 | -------------------------------------------------------------------------------- /config/webpack/configuration.js: -------------------------------------------------------------------------------- 1 | // Common configuration for webpacker loaded from config/webpacker.yml 2 | 3 | const { join, resolve } = require("path") 4 | const { env } = require("process") 5 | const { safeLoad } = require("js-yaml") 6 | const { readFileSync } = require("fs") 7 | 8 | const configPath = resolve("config", "webpacker.yml") 9 | const loadersDir = join(__dirname, "loaders") 10 | const settings = safeLoad(readFileSync(configPath), "utf8")[env.NODE_ENV] 11 | 12 | function removeOuterSlashes(string) { 13 | return string.replace(/^\/*/, "").replace(/\/*$/, "") 14 | } 15 | 16 | function formatPublicPath(host = "", path = "") { 17 | let formattedHost = removeOuterSlashes(host) 18 | if (formattedHost && !/^http/i.test(formattedHost)) { 19 | formattedHost = `//${formattedHost}` 20 | } 21 | const formattedPath = removeOuterSlashes(path) 22 | return `${formattedHost}/${formattedPath}/` 23 | } 24 | 25 | const output = { 26 | path: resolve("public", settings.public_output_path), 27 | publicPath: formatPublicPath(env.ASSET_HOST, settings.public_output_path), 28 | } 29 | 30 | module.exports = { 31 | settings, 32 | env, 33 | loadersDir, 34 | output, 35 | } 36 | -------------------------------------------------------------------------------- /app/javascript/packs/components/Comment.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { withStyles, createStyleSheet } from "material-ui/styles" 3 | import { Typography } from "material-ui" 4 | import PropTypes from "prop-types" 5 | 6 | import { linkFor } from "../utils/helpers" 7 | 8 | const styleSheet = createStyleSheet("Comment", () => ({ 9 | username: { 10 | display: "inline", 11 | paddingRight: "8px", 12 | }, 13 | content: { 14 | display: "inline", 15 | }, 16 | })) 17 | 18 | const Comment = ({ classes, comment }) => { 19 | return ( 20 |
21 | {linkFor( 22 | 27 | {comment.user.username} 28 | , 29 | `/users/${comment.user.username}`, 30 | )} 31 | 32 | {comment.content} 33 | 34 |
35 | ) 36 | } 37 | 38 | Comment.propTypes = { 39 | classes: PropTypes.object.isRequired, 40 | comment: PropTypes.object.isRequired, 41 | } 42 | 43 | export default withStyles(styleSheet)(Comment) 44 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "active_record/railtie" 4 | require "action_controller/railtie" 5 | require "action_view/railtie" 6 | require "action_mailer/railtie" 7 | require "active_job/railtie" 8 | require "action_cable/engine" 9 | # require "rails/test_unit/railtie" 10 | require "sprockets/railtie" 11 | 12 | # Require the gems listed in Gemfile, including any gems 13 | # you've limited to :test, :development, or :production. 14 | Bundler.require(*Rails.groups) 15 | 16 | module LearnGraphql 17 | class Application < Rails::Application 18 | # Initialize configuration defaults for originally generated Rails version. 19 | config.load_defaults 5.1 20 | 21 | # Settings in config/environments/* take precedence over those specified here. 22 | # Application configuration should go into files in config/initializers 23 | # -- all .rb files in that directory are automatically loaded. 24 | 25 | config.generators do |generate| 26 | generate.helper false 27 | generate.assets false 28 | generate.view_specs false 29 | generate.test_framework :rspec 30 | generate.factory_girl dir: "spec/factories" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/models/image.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 | class Image < ApplicationRecord 21 | mount_uploader :file, ImageUploader 22 | 23 | belongs_to :imageable, polymorphic: true, optional: true 24 | belongs_to :user, optional: true 25 | 26 | scope :find_by_user_id, ->(user_id) { where(imageable_type: "User", imageable_id: user_id) } 27 | 28 | validates :imageable_type, presence: true 29 | validates :user, :file, presence: { if: proc { |i| i.imageable_type == "Photo" || i.imageable_type.nil? } } 30 | 31 | def decorate_json 32 | { id: id } 33 | # case imageable_type 34 | # when "Photo" 35 | # { id: id } 36 | # when "User" 37 | # { auth_token: user.auth_token } 38 | # end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/graphql/functions/update_profile.rb: -------------------------------------------------------------------------------- 1 | class Functions::UpdateProfile < GraphQL::Function 2 | UpdateProfileInput = GraphQL::InputObjectType.define do 3 | name "UpdateProfile" 4 | 5 | argument :name, types.String 6 | argument :email, types.String 7 | argument :username, types.String 8 | argument :website, types.String 9 | argument :caption, types.String 10 | argument :birthdate, types.String 11 | argument :password, types.String 12 | argument :image_id, types.ID 13 | end 14 | 15 | argument :user, !UpdateProfileInput 16 | type Types::TokenType 17 | 18 | def call(obj, args, ctx) 19 | if user = ctx[:current_user] 20 | params = args[:user].to_h 21 | 22 | if image = Image.find_by_user_id(user.id).find_by(id: params.delete("image_id")) 23 | user.image.try(:destroy) 24 | user.reload.image = image 25 | user.save 26 | end 27 | 28 | user.update(params) 29 | user 30 | else 31 | GraphQL::ExecutionError.new("Unauthorized") 32 | end 33 | end 34 | 35 | def new_image(image) 36 | if Rails.env.production? 37 | File.open(open(image.file.url)) 38 | else 39 | File.open(File.join(Rails.root, "/public", image.file.url)) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/graphql/types/query_type.rb: -------------------------------------------------------------------------------- 1 | Types::QueryType = GraphQL::ObjectType.define do 2 | name "Query" 3 | 4 | field :user do 5 | type Types::UserType 6 | argument :username, !types.String 7 | 8 | resolve ->(obj, args, ctx) { 9 | User.find_by!(username: args[:username]) 10 | } 11 | end 12 | 13 | field :photo do 14 | type Types::PhotoType 15 | argument :id, !types.ID 16 | 17 | resolve ->(obj, args, ctx) { 18 | Photo.find(args[:id]) 19 | } 20 | end 21 | 22 | field :followers, function: Functions::FollowshipQuery.new(:followers) 23 | field :followings, function: Functions::FollowshipQuery.new(:followings) 24 | 25 | field :feed do 26 | type types[Types::PhotoType] 27 | argument :page, types.Int, default_value: 1 28 | resolve ->(obj, args, ctx) { 29 | if current_user = ctx[:current_user] 30 | current_user.feed.page(args[:page]).per(10) 31 | else 32 | Photo.none 33 | end 34 | } 35 | end 36 | 37 | field :users do 38 | type types[Types::UserType] 39 | resolve ->(obj, args, ctx) { 40 | if user = ctx[:current_user] 41 | User.where.not(id: user.id).order(created_at: :desc) 42 | else 43 | User.order(created_at: :desc) 44 | end 45 | } 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | # Shared secrets are available across all environments. 14 | 15 | # shared: 16 | # api_key: a1B2c3D4e5F6 17 | 18 | # Environmental secrets are only available for that specific environment. 19 | 20 | development: 21 | secret_key_base: 454f8a3ae0e39034991332b6e774e0c3c55d727e8456c451f4385d00dc7018a1b36a874afeb705899a038026fafe07aa65198279c8f0d45b84c9f55a6cd811e0 22 | 23 | test: 24 | secret_key_base: 2ec52a7c99bdc6c887b039d006b2f0f5cfb168ae3a882d5adbdaf81de0c6a667422e36f2a0c3047ec29d58ab7b8969ff4b06ea7a002e3ad51536e2210577480f 25 | 26 | # Do not keep production secrets in the unencrypted secrets file. 27 | # Instead, either read values from the environment. 28 | # Or, use `bin/rails secrets:setup` to configure encrypted secrets 29 | # and move the `production:` environment over there. 30 | 31 | production: 32 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 33 | -------------------------------------------------------------------------------- /bin/webpack-dev-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $stdout.sync = true 3 | 4 | require "shellwords" 5 | require "yaml" 6 | 7 | ENV["RAILS_ENV"] ||= "development" 8 | RAILS_ENV = ENV["RAILS_ENV"] 9 | 10 | ENV["NODE_ENV"] ||= RAILS_ENV 11 | NODE_ENV = ENV["NODE_ENV"] 12 | 13 | APP_PATH = File.expand_path("../", __dir__) 14 | CONFIG_FILE = File.join(APP_PATH, "config/webpacker.yml") 15 | NODE_MODULES_PATH = File.join(APP_PATH, "node_modules") 16 | WEBPACK_CONFIG = File.join(APP_PATH, "config/webpack/development.js") 17 | 18 | def args(key) 19 | index = ARGV.index(key) 20 | index ? ARGV[index + 1] : nil 21 | end 22 | 23 | begin 24 | dev_server = YAML.load_file(CONFIG_FILE)["development"]["dev_server"] 25 | 26 | DEV_SERVER_HOST = "http#{"s" if args('--https') || dev_server["https"]}://#{args('--host') || dev_server["host"]}:#{args('--port') || dev_server["port"]}" 27 | 28 | rescue Errno::ENOENT, NoMethodError 29 | puts "Webpack dev_server configuration not found in #{CONFIG_FILE}." 30 | puts "Please run bundle exec rails webpacker:install to install webpacker" 31 | exit! 32 | end 33 | 34 | newenv = { 35 | "NODE_PATH" => NODE_MODULES_PATH.shellescape, 36 | "ASSET_HOST" => DEV_SERVER_HOST.shellescape 37 | }.freeze 38 | 39 | cmdline = ["yarn", "run", "webpack-dev-server", "--", "--progress", "--color", "--config", WEBPACK_CONFIG] + ARGV 40 | 41 | Dir.chdir(APP_PATH) do 42 | exec newenv, *cmdline 43 | end 44 | -------------------------------------------------------------------------------- /app/javascript/packs/actions/user.js: -------------------------------------------------------------------------------- 1 | import { decode, encode } from "json-web-token" 2 | 3 | import { 4 | SET_USER_BY_TOKEN, 5 | SHOW_ALERT, 6 | USER_LOGOUT, 7 | USER_UPDATED, 8 | } from "../constants" 9 | 10 | 11 | export function setUserByToken(token) { 12 | return (dispatch) => { 13 | decode("secrets", token, (error, user) => { 14 | if (error) { 15 | dispatch({ 16 | type: SHOW_ALERT, 17 | message: "Token invalid", 18 | }) 19 | } 20 | 21 | if (user) { 22 | window.localStorage.setItem("auth_token", token) 23 | 24 | dispatch({ 25 | type: SET_USER_BY_TOKEN, 26 | user, 27 | }) 28 | } 29 | }) 30 | } 31 | } 32 | 33 | export function logoutUser() { 34 | return (dispatch) => { 35 | window.localStorage.removeItem("auth_token") 36 | dispatch({ 37 | type: USER_LOGOUT, 38 | }) 39 | } 40 | } 41 | 42 | export function setUser(param) { 43 | return (dispatch) => { 44 | const user = param 45 | delete user.__typename 46 | encode("secrets", user, (err, token) => { 47 | if (err) { 48 | dispatch({ 49 | type: SHOW_ALERT, 50 | message: "Can't encode token", 51 | }) 52 | } 53 | 54 | if (token) { 55 | window.localStorage.setItem("auth_token", token) 56 | dispatch({ 57 | type: USER_UPDATED, 58 | user, 59 | }) 60 | } 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - 'vendor/**/*' 4 | - 'spec/fixtures/**/*' 5 | - 'tmp/**/*' 6 | TargetRubyVersion: 2.3.1 7 | 8 | Style/FrozenStringLiteralComment: 9 | EnforcedStyle: always 10 | 11 | Style/Layout/EndOfLine: 12 | EnforcedStyle: lf 13 | 14 | Style/Layout/IndentHeredoc: 15 | EnforcedStyle: powerpack 16 | 17 | Lint/AmbiguousBlockAssociation: 18 | Exclude: 19 | - 'spec/**/*.rb' 20 | 21 | Lint/UselessAccessModifier: 22 | MethodCreatingMethods: 23 | - 'def_matcher' 24 | - 'def_node_matcher' 25 | 26 | Metrics/BlockLength: 27 | Exclude: 28 | - 'Rakefile' 29 | - '**/*.rake' 30 | - 'spec/**/*.rb' 31 | 32 | Metrics/ModuleLength: 33 | Exclude: 34 | - 'spec/**/*.rb' 35 | 36 | Performance/Caller: 37 | Exclude: 38 | - spec/rubocop/cop/performance/caller_spec.rb 39 | 40 | Style/StringLiterals: 41 | EnforcedStyle: double_quotes 42 | SupportedStyles: 43 | - double_quotes 44 | # If `true`, strings which span multiple lines using `\` for continuation must 45 | # use the same type of quotes on each line. 46 | ConsistentQuotesInMultiline: false 47 | 48 | Style/StringLiteralsInInterpolation: 49 | EnforcedStyle: double_quotes 50 | SupportedStyles: 51 | - double_quotes 52 | 53 | Style/ClassAndModuleChildren: 54 | EnforcedStyle: compact 55 | 56 | Style/DocumentationMethod: 57 | RequireForNonPublicMethods: false 58 | 59 | Style/FrozenStringLiteralComment: 60 | Enabled: false 61 | 62 | Documentation: 63 | Enabled: false 64 | -------------------------------------------------------------------------------- /app/javascript/packs/components/Alert.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Snackbar, IconButton, Button } from "material-ui" 3 | import CloseIcon from "material-ui-icons/Close" 4 | import { connect } from "react-redux" 5 | import { bindActionCreators } from "redux" 6 | import { Link } from "react-router-dom" 7 | import PropTypes from "prop-types" 8 | 9 | import * as alertActions from "../actions/alert" 10 | 11 | const Alert = ({ actions, alert }) => { 12 | const message = {alert.message} 13 | 14 | const action = (alert.action ? ( 15 | 23 | ) : null) 24 | 25 | return ( 26 | 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 |
17 | 26 | 34 | 42 | 51 | 52 | 53 | 54 | 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 |
10 | 17 | 24 | 31 | 38 | 45 | 53 | 61 | 62 | 63 | 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 | 42 | Edit Profile 43 | 44 | 45 | 46 | 47 | 48 | 49 | 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 |
35 | this.setState({ content: event.target.value })} 39 | onKeyPress={this.submitComment} 40 | value={this.state.content} 41 | /> 42 | 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 | 49 |
50 | {isShow( 51 |
52 | Edit 53 | Delete 54 |
, 55 | currentUser && currentUser.id.toString() === photo.user.id, 56 | )} 57 | Share 58 |
59 |
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 | {photo.caption} 79 | 80 | 81 | {isShow( 82 |
83 | 84 | 85 | 86 | 87 | 88 | {photo.caption} 89 | 90 |
91 | {comments} 92 |
93 |
94 | 95 |
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 | 87 | 88 | Profile 89 | 90 | 91 | Logout 92 | 93 | 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 | post 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 |
131 | 132 |
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 | Opened file 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 | 121 | Update Profile Picture 122 | 123 | 130 | {this.renderPlaceholder()} 131 | 132 | 133 | 134 | 135 | 136 | 137 | 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 | Opened file 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 | 126 | Upload photo 127 | 128 | 135 | {this.renderPlaceholder()} 136 | 137 | this.setState({ caption: event.target.value })} 142 | /> 143 | 144 | 145 | 146 | 147 | 148 | 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 | --------------------------------------------------------------------------------