├── log └── .keep ├── tmp └── .keep ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── public ├── favicon.ico ├── apple-touch-icon.png ├── apple-touch-icon-precomposed.png ├── images │ └── favicon.ico ├── system │ └── users │ │ └── images │ │ └── 000 │ │ └── 000 │ │ └── 004 │ │ └── original │ │ └── girl-avatar.jpg ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── like_test.rb │ ├── follow_test.rb │ ├── comment_test.rb │ ├── post_test.rb │ └── user_test.rb ├── controllers │ ├── .keep │ ├── users_controller_test.rb │ ├── sessions_controller_test.rb │ ├── api │ │ ├── likes_controller_test.rb │ │ ├── posts_controller_test.rb │ │ ├── users_controller_test.rb │ │ ├── comments_controller_test.rb │ │ ├── essions_controller_test.rb │ │ ├── follows_controller_test.rb │ │ ├── sessions_controller_test.rb │ │ └── search_results_controller_test.rb │ ├── static_pages_controller_test.rb │ └── static_pages_controller_controller_test.rb ├── fixtures │ ├── .keep │ ├── files │ │ └── .keep │ ├── likes.yml │ ├── comments.yml │ ├── follows.yml │ ├── posts.yml │ └── users.yml ├── integration │ └── .keep └── test_helper.rb ├── app ├── assets │ ├── images │ │ ├── .keep │ │ ├── ny.jpg │ │ ├── beach.jpg │ │ ├── began.JPG │ │ ├── budva.jpg │ │ ├── dream.JPG │ │ ├── ghost.jpg │ │ ├── roma.JPG │ │ ├── tutin.jpg │ │ ├── bbridge.jpg │ │ ├── bearded.jpg │ │ ├── cactus.JPG │ │ ├── coffee.JPG │ │ ├── cordoba.JPG │ │ ├── darkest.JPG │ │ ├── gg-park.jpg │ │ ├── giralda.JPG │ │ ├── granada.JPG │ │ ├── grand-c.JPG │ │ ├── imagine.JPG │ │ ├── jamaica.JPG │ │ ├── m-woods.JPG │ │ ├── search.png │ │ ├── sedona.JPG │ │ ├── seville.JPG │ │ ├── spy-eye.JPG │ │ ├── sunset.JPG │ │ ├── IMG_2278.JPG │ │ ├── al-hambra.jpg │ │ ├── antelope.jpg │ │ ├── avatar-1.png │ │ ├── avatar-2.png │ │ ├── avatar-3.png │ │ ├── avatar-4.png │ │ ├── avatar-5.png │ │ ├── avatar-g2.png │ │ ├── avatar-g3.png │ │ ├── avatar-g5.png │ │ ├── avatar-g6.png │ │ ├── colorado.jpg │ │ ├── desert-veg.JPG │ │ ├── grand-c2.JPG │ │ ├── homepage.png │ │ ├── malaga-cat.JPG │ │ ├── san-fran.jpg │ │ ├── girl-avatar.jpg │ │ ├── long-island.jpg │ │ ├── malaga_ruins.JPG │ │ ├── plaza-de-esp.JPG │ │ ├── spring-bloom.JPG │ │ └── readme │ │ │ ├── follows.png │ │ │ ├── search.png │ │ │ ├── posts-feed.png │ │ │ └── user-profile.png │ ├── javascripts │ │ ├── channels │ │ │ └── .keep │ │ ├── users.coffee │ │ ├── api │ │ │ ├── likes.coffee │ │ │ ├── posts.coffee │ │ │ ├── users.coffee │ │ │ ├── comments.coffee │ │ │ ├── essions.coffee │ │ │ ├── follows.coffee │ │ │ ├── sessions.coffee │ │ │ └── search_results.coffee │ │ ├── sessions.coffee │ │ ├── static_pages.coffee │ │ ├── static_pages_controller.coffee │ │ ├── cable.js │ │ └── application.js │ ├── stylesheets │ │ ├── base │ │ │ ├── fonts.scss │ │ │ ├── colors.scss │ │ │ ├── gallery.scss │ │ │ ├── reset.scss │ │ │ └── layout.scss │ │ ├── custom-fonts │ │ │ ├── billabong.eot │ │ │ └── billabong.ttf │ │ ├── font-awesome │ │ │ ├── fonts │ │ │ │ ├── FontAwesome.otf │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ ├── fontawesome-webfont.woff │ │ │ │ └── fontawesome-webfont.woff2 │ │ │ ├── less │ │ │ │ ├── screen-reader.less │ │ │ │ ├── fixed-width.less │ │ │ │ ├── larger.less │ │ │ │ ├── list.less │ │ │ │ ├── core.less │ │ │ │ ├── stacked.less │ │ │ │ ├── font-awesome.less │ │ │ │ ├── bordered-pulled.less │ │ │ │ ├── rotated-flipped.less │ │ │ │ ├── path.less │ │ │ │ ├── animated.less │ │ │ │ └── mixins.less │ │ │ ├── scss │ │ │ │ ├── _fixed-width.scss │ │ │ │ ├── _screen-reader.scss │ │ │ │ ├── _larger.scss │ │ │ │ ├── _list.scss │ │ │ │ ├── _core.scss │ │ │ │ ├── font-awesome.scss │ │ │ │ ├── _stacked.scss │ │ │ │ ├── _bordered-pulled.scss │ │ │ │ ├── _rotated-flipped.scss │ │ │ │ ├── _path.scss │ │ │ │ ├── _animated.scss │ │ │ │ └── _mixins.scss │ │ │ └── HELP-US-OUT.txt │ │ ├── users.scss │ │ ├── sessions.scss │ │ ├── api │ │ │ ├── follows.scss │ │ │ ├── likes.scss │ │ │ ├── posts.scss │ │ │ ├── users.scss │ │ │ ├── comments.scss │ │ │ ├── sessions.scss │ │ │ └── search_results.scss │ │ ├── static_pages.scss │ │ ├── components │ │ │ ├── _footer.scss │ │ │ ├── _upload_post.scss │ │ │ ├── _modal_post_item.scss │ │ │ ├── _search.scss │ │ │ ├── _main_content.scss │ │ │ ├── _landing_page.scss │ │ │ └── _edit_profile_page.scss │ │ └── application.scss │ └── config │ │ └── manifest.js ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ ├── like.rb │ ├── comment.rb │ ├── follow.rb │ ├── post.rb │ └── user.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── static_pages_controller.rb │ ├── api │ │ ├── search_results_controller.rb │ │ ├── likes_controller.rb │ │ ├── comments_controller.rb │ │ ├── sessions_controller.rb │ │ ├── follows_controller.rb │ │ ├── users_controller.rb │ │ └── posts_controller.rb │ └── application_controller.rb ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── mailer.html.erb │ │ └── application.html.erb │ ├── api │ │ ├── likes │ │ │ ├── show.json.jbuilder │ │ │ └── _like.json.jbuilder │ │ ├── posts │ │ │ ├── edit.json.jbuilder │ │ │ ├── show.json.jbuilder │ │ │ ├── index.json.jbuilder │ │ │ └── _post.json.jbuilder │ │ ├── comments │ │ │ ├── show.json.jbuilder │ │ │ └── _comment.json.jbuilder │ │ ├── follows │ │ │ ├── show.json.jbuilder │ │ │ └── _follow.json.jbuilder │ │ ├── users │ │ │ ├── show.json.jbuilder │ │ │ ├── index.json.jbuilder │ │ │ └── _user.json.jbuilder │ │ └── search_results │ │ │ └── index.json.jbuilder │ └── static_pages │ │ └── root.html.erb ├── helpers │ ├── users_helper.rb │ ├── api │ │ ├── likes_helper.rb │ │ ├── posts_helper.rb │ │ ├── users_helper.rb │ │ ├── comments_helper.rb │ │ ├── essions_helper.rb │ │ ├── follows_helper.rb │ │ ├── sessions_helper.rb │ │ └── search_results_helper.rb │ ├── sessions_helper.rb │ ├── application_helper.rb │ ├── static_pages_helper.rb │ └── static_pages_controller_helper.rb ├── jobs │ └── application_job.rb ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb └── mailers │ └── application_mailer.rb ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── docs ├── wireframes │ ├── Login.png │ ├── Singin.png │ ├── PhotoFeed.png │ ├── PhotoView.png │ └── UserProfile.png ├── api-endpoints.md ├── component-hierarchy.md ├── sample-state.md ├── schema.md └── README.md ├── bin ├── bundle ├── rake ├── rails ├── spring ├── update └── setup ├── config ├── spring.rb ├── boot.rb ├── environment.rb ├── cable.yml ├── initializers │ ├── session_store.rb │ ├── mime_types.rb │ ├── application_controller_renderer.rb │ ├── filter_parameter_logging.rb │ ├── cookies_serializer.rb │ ├── backtrace_silencers.rb │ ├── assets.rb │ ├── wrap_parameters.rb │ ├── inflections.rb │ └── new_framework_defaults.rb ├── locales │ └── en.yml ├── routes.rb ├── application.rb ├── secrets.yml ├── environments │ ├── test.rb │ └── development.rb └── puma.rb ├── config.ru ├── frontend ├── util │ ├── search_api_util.js │ ├── comments_api_util.js │ ├── likes_api_util.js │ ├── util.js │ ├── follows_api_util.js │ ├── session_api_util.js │ ├── users_api_util.js │ ├── posts_api_util.js │ └── modal_style.js ├── components │ ├── shared │ │ ├── spinner.jsx │ │ ├── error_list.jsx │ │ └── footer.jsx │ ├── app.jsx │ ├── posts │ │ ├── posts.jsx │ │ ├── upload_post_container.js │ │ ├── modal │ │ │ ├── post_item_modal_container.js │ │ │ └── post_item_modal.jsx │ │ ├── comment_item.jsx │ │ ├── posts_feed_container.js │ │ ├── posts_feed.jsx │ │ ├── add_comment_form.jsx │ │ └── upload_post.jsx │ ├── user_profile │ │ ├── user_profile_post_item.jsx │ │ ├── edit_profile_container.js │ │ ├── modal │ │ │ └── follows_modal_container.js │ │ ├── user_profile_posts.jsx │ │ ├── user_profile_container.js │ │ └── user_profile.jsx │ ├── greeting │ │ ├── greeting_container.js │ │ └── greeting.jsx │ ├── navigation │ │ ├── nav_links_container.js │ │ ├── nav_bar.jsx │ │ └── nav_links.jsx │ ├── follow │ │ ├── follow_container.js │ │ └── follow.jsx │ ├── search │ │ ├── search_container.js │ │ ├── search_result_item.jsx │ │ └── search.jsx │ ├── session │ │ └── auth_form_container.js │ └── root.jsx ├── reducers │ ├── selectors.js │ ├── fetching_reducer.js │ ├── search_reducer.js │ ├── root_reducer.js │ ├── session_reducer.js │ ├── users_reducer.js │ └── posts_reducer.js ├── store │ └── store.js ├── travelgram.jsx └── actions │ ├── likes_actions.js │ ├── search_actions.js │ ├── comments_actions.js │ ├── follows_actions.js │ ├── users_actions.js │ ├── session_actions.js │ └── posts_actions.js ├── db ├── migrate │ ├── 20170424000824_remove_post_url_constraint.rb │ ├── 20170428020005_add_bio_and_website_to_user.rb │ ├── 20170424151949_create_likes.rb │ ├── 20170426151824_create_follows.rb │ ├── 20170425173221_create_comments.rb │ ├── 20170423172049_add_attachment_image_to_users.rb │ ├── 20170423212138_add_attachment_image_to_posts.rb │ ├── 20170420154504_create_posts.rb │ └── 20170418161629_create_users.rb └── schema.rb ├── Rakefile ├── .gitignore ├── webpack.config.js ├── package.json ├── Guardfile ├── Gemfile └── README.md /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.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/helpers/users_helper.rb: -------------------------------------------------------------------------------- 1 | module UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/likes_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::LikesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/posts_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::PostsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/users_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/sessions_helper.rb: -------------------------------------------------------------------------------- 1 | module SessionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/comments_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::CommentsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/essions_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::EssionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/follows_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::FollowsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/sessions_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::SessionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/static_pages_helper.rb: -------------------------------------------------------------------------------- 1 | module StaticPagesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/views/api/likes/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'like', like: @like 2 | -------------------------------------------------------------------------------- /app/views/api/posts/edit.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'post', post: @post 2 | -------------------------------------------------------------------------------- /app/views/api/posts/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'post', post: @post 2 | -------------------------------------------------------------------------------- /app/helpers/api/search_results_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::SearchResultsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/views/api/comments/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'comment', comment: @comment 2 | -------------------------------------------------------------------------------- /app/views/api/follows/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'follow', follow: @follow 2 | -------------------------------------------------------------------------------- /app/views/api/users/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "api/users/user", user: @user 2 | -------------------------------------------------------------------------------- /app/helpers/static_pages_controller_helper.rb: -------------------------------------------------------------------------------- 1 | module StaticPagesControllerHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/assets/images/ny.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/ny.jpg -------------------------------------------------------------------------------- /app/assets/images/beach.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/beach.jpg -------------------------------------------------------------------------------- /app/assets/images/began.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/began.JPG -------------------------------------------------------------------------------- /app/assets/images/budva.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/budva.jpg -------------------------------------------------------------------------------- /app/assets/images/dream.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/dream.JPG -------------------------------------------------------------------------------- /app/assets/images/ghost.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/ghost.jpg -------------------------------------------------------------------------------- /app/assets/images/roma.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/roma.JPG -------------------------------------------------------------------------------- /app/assets/images/tutin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/tutin.jpg -------------------------------------------------------------------------------- /docs/wireframes/Login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/docs/wireframes/Login.png -------------------------------------------------------------------------------- /docs/wireframes/Singin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/docs/wireframes/Singin.png -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/public/images/favicon.ico -------------------------------------------------------------------------------- /app/assets/images/bbridge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/bbridge.jpg -------------------------------------------------------------------------------- /app/assets/images/bearded.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/bearded.jpg -------------------------------------------------------------------------------- /app/assets/images/cactus.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/cactus.JPG -------------------------------------------------------------------------------- /app/assets/images/coffee.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/coffee.JPG -------------------------------------------------------------------------------- /app/assets/images/cordoba.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/cordoba.JPG -------------------------------------------------------------------------------- /app/assets/images/darkest.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/darkest.JPG -------------------------------------------------------------------------------- /app/assets/images/gg-park.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/gg-park.jpg -------------------------------------------------------------------------------- /app/assets/images/giralda.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/giralda.JPG -------------------------------------------------------------------------------- /app/assets/images/granada.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/granada.JPG -------------------------------------------------------------------------------- /app/assets/images/grand-c.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/grand-c.JPG -------------------------------------------------------------------------------- /app/assets/images/imagine.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/imagine.JPG -------------------------------------------------------------------------------- /app/assets/images/jamaica.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/jamaica.JPG -------------------------------------------------------------------------------- /app/assets/images/m-woods.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/m-woods.JPG -------------------------------------------------------------------------------- /app/assets/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/search.png -------------------------------------------------------------------------------- /app/assets/images/sedona.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/sedona.JPG -------------------------------------------------------------------------------- /app/assets/images/seville.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/seville.JPG -------------------------------------------------------------------------------- /app/assets/images/spy-eye.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/spy-eye.JPG -------------------------------------------------------------------------------- /app/assets/images/sunset.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/sunset.JPG -------------------------------------------------------------------------------- /docs/wireframes/PhotoFeed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/docs/wireframes/PhotoFeed.png -------------------------------------------------------------------------------- /docs/wireframes/PhotoView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/docs/wireframes/PhotoView.png -------------------------------------------------------------------------------- /app/assets/images/IMG_2278.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/IMG_2278.JPG -------------------------------------------------------------------------------- /app/assets/images/al-hambra.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/al-hambra.jpg -------------------------------------------------------------------------------- /app/assets/images/antelope.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/antelope.jpg -------------------------------------------------------------------------------- /app/assets/images/avatar-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/avatar-1.png -------------------------------------------------------------------------------- /app/assets/images/avatar-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/avatar-2.png -------------------------------------------------------------------------------- /app/assets/images/avatar-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/avatar-3.png -------------------------------------------------------------------------------- /app/assets/images/avatar-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/avatar-4.png -------------------------------------------------------------------------------- /app/assets/images/avatar-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/avatar-5.png -------------------------------------------------------------------------------- /app/assets/images/avatar-g2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/avatar-g2.png -------------------------------------------------------------------------------- /app/assets/images/avatar-g3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/avatar-g3.png -------------------------------------------------------------------------------- /app/assets/images/avatar-g5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/avatar-g5.png -------------------------------------------------------------------------------- /app/assets/images/avatar-g6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/avatar-g6.png -------------------------------------------------------------------------------- /app/assets/images/colorado.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/colorado.jpg -------------------------------------------------------------------------------- /app/assets/images/desert-veg.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/desert-veg.JPG -------------------------------------------------------------------------------- /app/assets/images/grand-c2.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/grand-c2.JPG -------------------------------------------------------------------------------- /app/assets/images/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/homepage.png -------------------------------------------------------------------------------- /app/assets/images/malaga-cat.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/malaga-cat.JPG -------------------------------------------------------------------------------- /app/assets/images/san-fran.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/san-fran.jpg -------------------------------------------------------------------------------- /docs/wireframes/UserProfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/docs/wireframes/UserProfile.png -------------------------------------------------------------------------------- /app/assets/images/girl-avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/girl-avatar.jpg -------------------------------------------------------------------------------- /app/assets/images/long-island.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/long-island.jpg -------------------------------------------------------------------------------- /app/assets/images/malaga_ruins.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/malaga_ruins.JPG -------------------------------------------------------------------------------- /app/assets/images/plaza-de-esp.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/plaza-de-esp.JPG -------------------------------------------------------------------------------- /app/assets/images/spring-bloom.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/spring-bloom.JPG -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/assets/images/readme/follows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/readme/follows.png -------------------------------------------------------------------------------- /app/assets/images/readme/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/readme/search.png -------------------------------------------------------------------------------- /app/views/api/likes/_like.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! like, :id 2 | json.user_id like.user.id 3 | json.post_id like.post.id 4 | -------------------------------------------------------------------------------- /app/assets/images/readme/posts-feed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/readme/posts-feed.png -------------------------------------------------------------------------------- /app/assets/images/readme/user-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/images/readme/user-profile.png -------------------------------------------------------------------------------- /app/assets/stylesheets/base/fonts.scss: -------------------------------------------------------------------------------- 1 | $serif: georgia,"times new roman",times,serif; 2 | $sans-serif: arial, helvetica, sans-serif; 3 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/static_pages_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticPagesController < ApplicationController 2 | 3 | def root 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /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/assets/stylesheets/custom-fonts/billabong.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/stylesheets/custom-fonts/billabong.eot -------------------------------------------------------------------------------- /app/assets/stylesheets/custom-fonts/billabong.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/stylesheets/custom-fonts/billabong.ttf -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /app/views/api/posts/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @posts.each do |post| 2 | json.set! post.id do 3 | json.partial! 'post', post: post 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/api/users/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @users.each do |user| 2 | json.set! user.id do 3 | json.partial! 'user', user: user 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /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/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/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/stylesheets/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/system/users/images/000/000/004/original/girl-avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/public/system/users/images/000/000/004/original/girl-avatar.jpg -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-sanel/travelgram/HEAD/app/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /app/views/api/comments/_comment.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! comment, :id, :body 2 | json.user_id comment.user.id 3 | json.username comment.user.username 4 | json.post_id comment.post.id 5 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_Travelgram_session' 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/less/screen-reader.less: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { .sr-only(); } 5 | .sr-only-focusable { .sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/less/fixed-width.less: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .@{fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/scss/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/util/search_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchSearchResults = (query) => { 2 | return $.ajax({ 3 | method: "GET", 4 | url: "api/search_results", 5 | data: { query } 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /test/controllers/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UsersControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/sessions_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SessionsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/scss/_screen-reader.scss: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { @include sr-only(); } 5 | .sr-only-focusable { @include sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /test/controllers/api/likes_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::LikesControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/posts_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::PostsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::UsersControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/users.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Users controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /test/controllers/api/comments_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::CommentsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/essions_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::EssionsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/follows_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::FollowsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/sessions_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::SessionsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/static_pages_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class StaticPagesControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/sessions.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Sessions controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/views/api/search_results/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @results do |user| 2 | json.id user.id 3 | json.name user.name 4 | json.username user.username 5 | json.profile_photo asset_path(user.image.url) 6 | end 7 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/follows.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/follows controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/likes.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/likes controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/posts.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/posts controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/users.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/users controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /db/migrate/20170424000824_remove_post_url_constraint.rb: -------------------------------------------------------------------------------- 1 | class RemovePostUrlConstraint < ActiveRecord::Migration[5.0] 2 | def change 3 | remove_column :posts, :url 4 | add_column :posts, :url, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/controllers/api/search_results_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::SearchResultsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/comments.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/comments controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/sessions.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/sessions controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/static_pages.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the StaticPages controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /db/migrate/20170428020005_add_bio_and_website_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddBioAndWebsiteToUser < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :bio, :string 4 | add_column :users, :website, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /test/controllers/static_pages_controller_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class StaticPagesControllerControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/search_results.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Api::SearchResults controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /frontend/components/shared/spinner.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Spinner() { 4 | return ( 5 |
6 |
7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /app/assets/javascripts/users.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/likes.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/posts.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/users.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/sessions.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/comments.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/essions.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/follows.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/sessions.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/static_pages.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /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/assets/javascripts/api/search_results.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /db/migrate/20170424151949_create_likes.rb: -------------------------------------------------------------------------------- 1 | class CreateLikes < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :likes do |t| 4 | t.integer :user_id 5 | t.integer :post_id 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /frontend/components/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Footer from './shared/footer' 3 | 4 | const App = ({ children }) => ( 5 |
6 | { children } 7 |
9 | ); 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /app/assets/javascripts/static_pages_controller.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/controllers/api/search_results_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SearchResultsController < ApplicationController 2 | 3 | def index 4 | query = params[:query].downcase 5 | @results = User.where("LOWER(username) LIKE ?", "%#{query}%").limit(10) 6 | end 7 | 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20170426151824_create_follows.rb: -------------------------------------------------------------------------------- 1 | class CreateFollows < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :follows do |t| 4 | t.integer :follower_id 5 | t.integer :following_id 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /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/20170425173221_create_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateComments < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :comments do |t| 4 | t.string :body 5 | t.integer :user_id 6 | t.integer :post_id 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /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/20170423172049_add_attachment_image_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAttachmentImageToUsers < ActiveRecord::Migration 2 | def self.up 3 | change_table :users do |t| 4 | t.attachment :image 5 | end 6 | end 7 | 8 | def self.down 9 | remove_attachment :users, :image 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20170423212138_add_attachment_image_to_posts.rb: -------------------------------------------------------------------------------- 1 | class AddAttachmentImageToPosts < ActiveRecord::Migration 2 | def self.up 3 | change_table :posts do |t| 4 | t.attachment :image 5 | end 6 | end 7 | 8 | def self.down 9 | remove_attachment :posts, :image 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/views/static_pages/root.html.erb: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | -------------------------------------------------------------------------------- /db/migrate/20170420154504_create_posts.rb: -------------------------------------------------------------------------------- 1 | class CreatePosts < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :posts do |t| 4 | t.string :url, null: false 5 | t.string :description 6 | t.integer :user_id, null: false 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 7 | fixtures :all 8 | 9 | # Add more helper methods to be used by all tests here... 10 | end 11 | -------------------------------------------------------------------------------- /frontend/components/shared/error_list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function ErrorList({ errors }) { 4 | if (!errors) return null; 5 | 6 | const errorItems = errors.map(error => 7 |
  • { error }
  • 8 | ); 9 | 10 | return ( 11 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /frontend/util/comments_api_util.js: -------------------------------------------------------------------------------- 1 | export const createComment = (post_id, body) => { 2 | return $.ajax({ 3 | method: "POST", 4 | url: "api/comments", 5 | data: {comment : {post_id, body}} 6 | }); 7 | }; 8 | 9 | export const deleteComment = (id) => { 10 | return $.ajax({ 11 | method: "DELETE", 12 | url: `/api/comments/${id}` 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/HELP-US-OUT.txt: -------------------------------------------------------------------------------- 1 | I hope you love Font Awesome. If you've found it useful, please do me a favor and check out my latest project, 2 | Fort Awesome (https://fortawesome.com). It makes it easy to put the perfect icons on your website. Choose from our awesome, 3 | comprehensive icon sets or copy and paste your own. 4 | 5 | Please. Check it out. 6 | 7 | -Dave Gandy 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/base/colors.scss: -------------------------------------------------------------------------------- 1 | $white: #ffffff; 2 | $red: #ff0000; 3 | 4 | $lightest-gray: #fafafa; 5 | $light-gray: #e6e6e6; 6 | $gray: #939393; 7 | $dark-gray: #666666; 8 | $darkest-gray: #333333; 9 | $separator-gray: #c7c7c7; 10 | 11 | $input-border: #dbdbdb; 12 | $insta-blue : #3897f0; 13 | $insta-blue-disabled: #b4daff; 14 | $footer-text: #003569; 15 | 16 | $gray-link: #262626; 17 | -------------------------------------------------------------------------------- /frontend/util/likes_api_util.js: -------------------------------------------------------------------------------- 1 | export const createLike = (post_id) => { 2 | return $.ajax({ 3 | method: "POST", 4 | url: "/api/likes/", 5 | data: {like: { post_id: post_id }} 6 | }); 7 | }; 8 | 9 | export const deleteLike = (post_id) => { 10 | return $.ajax({ 11 | method: "DELETE", 12 | url: `/api/likes/${post_id}`, 13 | data: {like: { post_id }} 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/util/util.js: -------------------------------------------------------------------------------- 1 | export const getIndex = (arr, obj) => { 2 | let ind; 3 | arr.forEach((el, index) => { 4 | if (el.id === obj.id) { 5 | ind = index; 6 | } 7 | }); 8 | return ind; 9 | }; 10 | 11 | 12 | export const getIndexById = (arr, id) => { 13 | let ind; 14 | arr.forEach((el, index) => { 15 | if (el.id === id) { 16 | ind = index; 17 | } 18 | }); 19 | return ind; 20 | }; 21 | -------------------------------------------------------------------------------- /db/migrate/20170418161629_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table :users do |t| 4 | t.string :username, null: false 5 | t.string :email, null: false 6 | t.string :password_digest, null:false 7 | t.string :session_token, null:false 8 | t.string :name 9 | t.string :profile_photo 10 | 11 | t.timestamps 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /frontend/reducers/selectors.js: -------------------------------------------------------------------------------- 1 | import { values } from 'lodash'; 2 | 3 | export const selectUserPosts = (state, userId) => { 4 | let arrayPosts = Object.values(state.posts); 5 | const foundPosts = arrayPosts.filter(post => { 6 | // return post.user.id == userId; 7 | return post.user_id == userId; 8 | } ); 9 | return foundPosts || {}; 10 | }; 11 | 12 | export const selectAllUsers = (state) => values(state.users); 13 | -------------------------------------------------------------------------------- /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/models/like.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: likes 4 | # 5 | # id :integer not null, primary key 6 | # user_id :integer 7 | # post_id :integer 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | class Like < ApplicationRecord 13 | 14 | validates :user_id, :post_id, presence: true 15 | 16 | belongs_to :user 17 | belongs_to :post 18 | end 19 | -------------------------------------------------------------------------------- /test/models/like_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: likes 4 | # 5 | # id :integer not null, primary key 6 | # user_id :integer 7 | # post_id :integer 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | require 'test_helper' 13 | 14 | class LikeTest < ActiveSupport::TestCase 15 | # test "the truth" do 16 | # assert true 17 | # end 18 | end 19 | -------------------------------------------------------------------------------- /frontend/reducers/fetching_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_ALL_POSTS, RECEIVE_POST, FETCH_POSTS, FETCH_POST } from '../actions/posts_actions'; 2 | 3 | export default function fetchingReducer(state = false, action) { 4 | switch (action.type) { 5 | case FETCH_POSTS: 6 | case FETCH_POST: 7 | return true; 8 | case RECEIVE_ALL_POSTS: 9 | case RECEIVE_POST: 10 | return false; 11 | default: 12 | return state; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/components/posts/posts.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavBar from '../navigation/nav_bar'; 3 | import PostsFeedContainer from './posts_feed_container'; 4 | import PostsFeed from './posts_feed'; 5 | 6 | const Posts = ({ children }) => ( 7 |
    8 | 9 |
    10 | 11 |
    12 | 13 |
    14 | ); 15 | 16 | export default Posts; 17 | -------------------------------------------------------------------------------- /frontend/util/follows_api_util.js: -------------------------------------------------------------------------------- 1 | export const createFollow = (follower_id, following_id) => { 2 | return $.ajax({ 3 | method: "POST", 4 | url: "api/follows", 5 | data: {follow : {follower_id, following_id}} 6 | }); 7 | }; 8 | 9 | export const deleteFollow = (follower_id, following_id) => { 10 | return $.ajax({ 11 | method: "GET", 12 | url: "api/follows/delete", 13 | data: {follow : {follower_id, following_id}} 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /test/models/follow_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: follows 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 | 12 | require 'test_helper' 13 | 14 | class FollowTest < ActiveSupport::TestCase 15 | # test "the truth" do 16 | # assert true 17 | # end 18 | end 19 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/less/larger.less: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .@{fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .@{fa-css-prefix}-2x { font-size: 2em; } 11 | .@{fa-css-prefix}-3x { font-size: 3em; } 12 | .@{fa-css-prefix}-4x { font-size: 4em; } 13 | .@{fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /frontend/util/session_api_util.js: -------------------------------------------------------------------------------- 1 | export const signup = (user) => { 2 | return $.ajax({ 3 | method: 'POST', 4 | url: '/api/users', 5 | data: user 6 | }); 7 | }; 8 | 9 | export const login = (user) => { 10 | return $.ajax({ 11 | method: 'POST', 12 | url: '/api/session', 13 | data: user 14 | }); 15 | }; 16 | 17 | export const logout = () => { 18 | return $.ajax({ 19 | method: 'DELETE', 20 | url: '/api/session', 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/scss/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /test/models/comment_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: comments 4 | # 5 | # id :integer not null, primary key 6 | # body :string 7 | # user_id :integer 8 | # post_id :integer 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | require 'test_helper' 14 | 15 | class CommentTest < ActiveSupport::TestCase 16 | # test "the truth" do 17 | # assert true 18 | # end 19 | end 20 | -------------------------------------------------------------------------------- /app/models/comment.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: comments 4 | # 5 | # id :integer not null, primary key 6 | # body :string 7 | # user_id :integer 8 | # post_id :integer 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | class Comment < ApplicationRecord 14 | validates :body, :user_id, :post_id, presence: true 15 | 16 | belongs_to :user 17 | belongs_to :post 18 | 19 | end 20 | -------------------------------------------------------------------------------- /test/fixtures/likes.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: likes 4 | # 5 | # id :integer not null, primary key 6 | # user_id :integer 7 | # post_id :integer 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 13 | 14 | one: 15 | user_id: 1 16 | post_id: 1 17 | 18 | two: 19 | user_id: 1 20 | post_id: 1 21 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/less/list.less: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: @fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .@{fa-css-prefix}-li { 11 | position: absolute; 12 | left: -@fa-li-width; 13 | width: @fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.@{fa-css-prefix}-lg { 17 | left: (-@fa-li-width + (4em / 14)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/scss/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/components/user_profile/user_profile_post_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter} from 'react-router'; 3 | import PostItemModalContainer from '../posts/modal/post_item_modal_container'; 4 | 5 | class UserProfilePostItem extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render(){ 11 | return( 12 | 13 | ); 14 | } 15 | } 16 | 17 | export default withRouter(UserProfilePostItem); 18 | -------------------------------------------------------------------------------- /frontend/components/greeting/greeting_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { logout } from "../../actions/session_actions"; 3 | import Greeting from './greeting'; 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return { 7 | currentUser: state.session.currentUser 8 | }; 9 | }; 10 | 11 | const mapDispatchToProps = (dispatch) => ({ 12 | logout: () => dispatch(logout()) 13 | }); 14 | 15 | export default connect ( 16 | mapStateToProps, 17 | mapDispatchToProps 18 | )(Greeting); 19 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/less/core.less: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Travelgram 6 | <%= csrf_meta_tags %> 7 | 8 | 9 | 10 | <%= stylesheet_link_tag 'application', media: 'all' %> 11 | <%= javascript_include_tag 'application' %> 12 | 13 | 14 | 15 | <%= yield %> 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/scss/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /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 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /frontend/reducers/search_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_SEARCH_RESULTS, REMOVE_SEARCH_RESULTS } from '../actions/search_actions'; 2 | import merge from 'lodash/merge'; 3 | 4 | const _nullSearchResults = []; 5 | 6 | const SearchResultsReducer = (oldState = [], action) => { 7 | switch(action.type){ 8 | case RECEIVE_SEARCH_RESULTS: 9 | return action.searchResults; 10 | case REMOVE_SEARCH_RESULTS: 11 | return _nullSearchResults; 12 | default: 13 | return oldState; 14 | } 15 | }; 16 | 17 | export default SearchResultsReducer; 18 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/scss/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | @import "screen-reader"; 19 | -------------------------------------------------------------------------------- /frontend/util/users_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchUsers = () => { 2 | return $.ajax({ 3 | method: "GET", 4 | url: "/api/users" 5 | }); 6 | }; 7 | 8 | export const fetchUser = (id) => { 9 | return $.ajax({ 10 | method: "GET", 11 | url: `/api/users/${id}` 12 | }); 13 | }; 14 | 15 | export const updateUser = (user) => { 16 | return $.ajax({ 17 | method: "PATCH", 18 | url: `/api/users/${user.get('user[id]')}`, 19 | dataType: 'json', 20 | contentType: false, 21 | processData: false, 22 | data: user 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /test/fixtures/comments.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: comments 4 | # 5 | # id :integer not null, primary key 6 | # body :string 7 | # user_id :integer 8 | # post_id :integer 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 14 | 15 | one: 16 | body: MyString 17 | user_id: 1 18 | post_id: 1 19 | 20 | two: 21 | body: MyString 22 | user_id: 1 23 | post_id: 1 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/components/posts/upload_post_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { fetchPosts, createPost } from '../../actions/posts_actions'; 3 | import UploadPost from './upload_post'; 4 | 5 | const mapStateToProps = (state) => { 6 | return { 7 | currentUser: state.session.currentUser, 8 | user: state.user 9 | }; 10 | }; 11 | 12 | const mapDispatchToProps = (dispatch) => ({ 13 | createPost: (post) => dispatch(createPost(post)) 14 | }); 15 | 16 | export default connect( 17 | mapStateToProps, 18 | mapDispatchToProps 19 | )(UploadPost); 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/store/store.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from '../reducers/root_reducer'; 4 | const middlewares = [thunk]; 5 | 6 | if (process.env.NODE_ENV !== 'production') { 7 | const { createLogger } = require('redux-logger'); 8 | middlewares.push(createLogger()); 9 | } 10 | 11 | const configureStore = (preloadedState = {}) => ( 12 | createStore( 13 | rootReducer, 14 | preloadedState, 15 | applyMiddleware(...middlewares) 16 | ) 17 | ); 18 | 19 | export default configureStore; 20 | -------------------------------------------------------------------------------- /frontend/components/navigation/nav_links_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { logout } from "../../actions/session_actions"; 3 | import NavLinks from './nav_links'; 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return { 7 | currentUser: state.session.currentUser, 8 | user: state.user 9 | }; 10 | }; 11 | 12 | const mapDispatchToProps = (dispatch) => { 13 | return ({ 14 | logout: () => dispatch(logout()) 15 | }); 16 | }; 17 | 18 | export default connect ( 19 | mapStateToProps, 20 | mapDispatchToProps 21 | )(NavLinks); 22 | -------------------------------------------------------------------------------- /frontend/reducers/root_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import SessionReducer from './session_reducer'; 3 | import PostsReducer from './posts_reducer'; 4 | import UsersReducer from './users_reducer'; 5 | import FetchingReducer from './fetching_reducer'; 6 | import SearchResultsReducer from './search_reducer'; 7 | 8 | const rootReducer = combineReducers({ 9 | session: SessionReducer, 10 | posts: PostsReducer, 11 | user: UsersReducer, 12 | searchResults: SearchResultsReducer, 13 | fetching: FetchingReducer 14 | }); 15 | 16 | export default rootReducer; 17 | -------------------------------------------------------------------------------- /app/controllers/api/likes_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::LikesController < ApplicationController 2 | 3 | def create 4 | @like = current_user.likes.new(like_params) 5 | if @like.save 6 | render "api/likes/show" 7 | else 8 | render json: @like.errors, status: 422 9 | end 10 | end 11 | 12 | def destroy 13 | @like = current_user.likes.find_by(post_id: params[:like][:post_id]) 14 | @like.destroy 15 | render json: @like 16 | end 17 | 18 | private 19 | def like_params 20 | params.require(:like).permit(:user_id, :post_id) 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/less/stacked.less: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .@{fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .@{fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .@{fa-css-prefix}-inverse { color: @fa-inverse; } 21 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/scss/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /app/controllers/api/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::CommentsController < ApplicationController 2 | 3 | 4 | def create 5 | @comment = current_user.comments.new(comment_params) 6 | if @comment.save 7 | render "api/comments/show" 8 | else 9 | render json: @comment.errors, status: 422 10 | end 11 | end 12 | 13 | def destroy 14 | @comment = current_user.comments.find(params[:id]) 15 | @comment.destroy 16 | render json: @comment 17 | end 18 | 19 | private 20 | def comment_params 21 | params.require(:comment).permit(:user_id, :post_id, :body) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/less/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | @import "path.less"; 9 | @import "core.less"; 10 | @import "larger.less"; 11 | @import "fixed-width.less"; 12 | @import "list.less"; 13 | @import "bordered-pulled.less"; 14 | @import "animated.less"; 15 | @import "rotated-flipped.less"; 16 | @import "stacked.less"; 17 | @import "icons.less"; 18 | @import "screen-reader.less"; 19 | -------------------------------------------------------------------------------- /app/views/api/users/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! user, :id, :username, :name, :email, :bio, :website 2 | json.profile_photo asset_path(user.image.url) 3 | 4 | # json.posts_c user.posts 5 | 6 | json.followers user.follows_as_followee.each do |follow| 7 | json.extract! follow.follower, :id, :username 8 | json.profile_photo asset_path(follow.follower.image.url) 9 | json.follow_id follow.id 10 | end 11 | 12 | json.followees user.follows_as_follower.each do |follow| 13 | json.extract! follow.followee, :id, :username 14 | json.profile_photo asset_path(follow.followee.image.url) 15 | json.follow_id follow.id 16 | end 17 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/_footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | 6 | } 7 | 8 | .footer-content { 9 | flex: 1; 10 | max-width: 1010px; 11 | margin: 0 auto; 12 | color: $gray-link; 13 | display: flex; 14 | justify-content: space-between; 15 | font-size: 12px; 16 | } 17 | 18 | .links { 19 | color: $footer-text; 20 | text-transform: uppercase; 21 | font-weight: 600px; 22 | } 23 | 24 | .mark { 25 | color: $gray; 26 | } 27 | 28 | .footer-nav { 29 | display: flex; 30 | } 31 | 32 | .footer-nav li { 33 | margin-right: 10px; 34 | } 35 | -------------------------------------------------------------------------------- /app/models/follow.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: follows 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 | 12 | class Follow < ApplicationRecord 13 | validates :follower, :followee, presence: true 14 | 15 | belongs_to :follower, 16 | class_name: :User, 17 | primary_key: :id, 18 | foreign_key: :follower_id 19 | 20 | belongs_to :followee, 21 | class_name: :User, 22 | primary_key: :id, 23 | foreign_key: :following_id 24 | end 25 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | 4 | helper_method :current_user, :logged_in? 5 | 6 | def current_user 7 | @currentUser ||= User.find_by_session_token(session[:session_token]) 8 | end 9 | 10 | def logged_in? 11 | !!current_user 12 | end 13 | 14 | def login!(user) 15 | @current_user = user 16 | session[:session_token] = user.reset_session_token! 17 | end 18 | 19 | def logout! 20 | current_user.reset_session_token! 21 | @current_user = nil 22 | session[:session_token] = nil 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /app/views/api/follows/_follow.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! follow, :id, :follower_id, :following_id 2 | 3 | json.followee follow.followee 4 | json.follower follow.follower 5 | 6 | # json.follower_id follow.follower.id 7 | # json.follower_username follow.follower.username 8 | # json.follower_photo asset_path(follow.follower.image.url) 9 | # 10 | # json.followee_id follow.followee.id 11 | # json.followee_username follow.followee.username 12 | # json.followee_photo asset_path(follow.followee.image.url) 13 | 14 | # json.extract! follow.follower, :id, :username 15 | # json.profile_photo asset_path(follow.follower.image.url) 16 | # json.follow_id follow.id 17 | -------------------------------------------------------------------------------- /frontend/components/user_profile/edit_profile_container.js: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux'; 2 | import { fetchUser, updateUser } from '../../actions/users_actions'; 3 | import EditProfile from './edit_profile'; 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return { 7 | user: state.user, 8 | currentUser: state.session.currentUser 9 | }; 10 | }; 11 | 12 | const mapDispatchToProps = (dispatch, ownProps) => ({ 13 | fetchUser: (id) => dispatch(fetchUser(id)), 14 | updateUser: (user) => dispatch(updateUser(user)) 15 | }); 16 | 17 | export default connect( 18 | mapStateToProps, 19 | mapDispatchToProps 20 | )(EditProfile); 21 | -------------------------------------------------------------------------------- /frontend/components/user_profile/modal/follows_modal_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { fetchUser } from '../../../actions/users_actions'; 3 | import FollowsModal from './follows_modal'; 4 | 5 | const mapStateToProps = (state) => ({ 6 | currentUser: state.session.currentUser, 7 | user: state.user, 8 | followers: state.user.followers, 9 | followees: state.user.followees 10 | }); 11 | 12 | const mapDispatchToProps = (dispatch) => { 13 | return ({ 14 | fetchUser: (id) => dispatch(fetchUser(id)) 15 | }); 16 | }; 17 | 18 | export default connect ( 19 | mapStateToProps, 20 | mapDispatchToProps 21 | )(FollowsModal); 22 | -------------------------------------------------------------------------------- /app/controllers/api/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SessionsController < ApplicationController 2 | 3 | 4 | def create 5 | @user = User.find_by_credentials( 6 | params[:user][:username], 7 | params[:user][:password] 8 | ) 9 | 10 | if @user 11 | login!(@user) 12 | render "api/users/show" 13 | else 14 | render json: { login: ["Invalid username/password"] }, status: 401 15 | end 16 | end 17 | 18 | def destroy 19 | @user = current_user 20 | if @user 21 | logout! 22 | render json: {} 23 | else 24 | render json: ["Nobody signed in"], status: 404 25 | end 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /frontend/travelgram.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import configureStore from './store/store'; 4 | import Root from './components/root'; 5 | import Modal from 'react-modal'; 6 | 7 | document.addEventListener('DOMContentLoaded', () => { 8 | let store; 9 | if (window.currentUser) { 10 | const preloadedState = { session: { currentUser: window.currentUser } }; 11 | store = configureStore(preloadedState); 12 | } else { 13 | store = configureStore(); 14 | } 15 | Modal.setAppElement(document.body); 16 | const root = document.getElementById('root'); 17 | ReactDOM.render(, root); 18 | }); 19 | -------------------------------------------------------------------------------- /app/views/api/posts/_post.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! post, :id, :description 2 | json.url asset_path(post.image.url) 3 | json.url_large asset_path(post.image.url(:large)) 4 | json.url_medium asset_path(post.image.url(:medium)) 5 | json.created_ago post.created_ago 6 | 7 | json.user_id post.user.id 8 | json.username post.user.username 9 | json.user_profile_photo asset_path(post.user.image.url) 10 | 11 | json.likes post.likes 12 | 13 | json.comments do 14 | json.array! post.comments do |comment| 15 | json.extract! comment, :id, :body 16 | json.user_id comment.user.id 17 | json.username comment.user.username 18 | json.post_id comment.post.id 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /frontend/components/user_profile/user_profile_posts.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | import UserProfilePostItem from './user_profile_post_item'; 4 | 5 | class UserProfilePosts extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render(){ 11 | return( 12 |
    13 | { 14 | this.props.posts.reverse().map(post => ( 15 | 18 | )) 19 | } 20 |
    21 | ); 22 | } 23 | } 24 | 25 | export default UserProfilePosts; 26 | -------------------------------------------------------------------------------- /test/models/post_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: posts 4 | # 5 | # id :integer not null, primary key 6 | # description :string 7 | # user_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # image_file_name :string 11 | # image_content_type :string 12 | # image_file_size :integer 13 | # image_updated_at :datetime 14 | # url :string 15 | # 16 | 17 | require 'test_helper' 18 | 19 | class PostTest < ActiveSupport::TestCase 20 | # test "the truth" do 21 | # assert true 22 | # end 23 | end 24 | -------------------------------------------------------------------------------- /frontend/reducers/session_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_CURRENT_USER, RECEIVE_ERRORS } from '../actions/session_actions'; 2 | import { merge } from 'lodash/merge'; 3 | 4 | let defaultState = { currentUser: null, errors: [] }; 5 | 6 | 7 | const SessionReducer = (state = defaultState, action) => { 8 | switch (action.type) { 9 | case RECEIVE_CURRENT_USER: 10 | let currentUser = action.currentUser; 11 | return Object.assign({}, defaultState, {currentUser}); 12 | case RECEIVE_ERRORS: 13 | let errors = action.errors; 14 | return Object.assign({}, defaultState, { errors } ); 15 | default: 16 | return state; 17 | } 18 | }; 19 | 20 | export default SessionReducer; 21 | -------------------------------------------------------------------------------- /test/fixtures/follows.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: follows 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 | 12 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 13 | 14 | # This model initially had no columns defined. If you add columns to the 15 | # model remove the '{}' from the fixture names and add the columns immediately 16 | # below each fixture, per the syntax in the comments below 17 | # 18 | one: {} 19 | # column: value 20 | # 21 | two: {} 22 | # column: value 23 | -------------------------------------------------------------------------------- /frontend/components/follow/follow_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { createFollow, deleteFollow } from "../../actions/follows_actions"; 3 | import Follow from './follow'; 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return { 7 | currentUser: state.session.currentUser, 8 | user: state.user 9 | }; 10 | }; 11 | 12 | const mapDispatchToProps = (dispatch) => ({ 13 | createFollow: (follower_id, following_id) => dispatch(createFollow(follower_id, following_id)), 14 | deleteFollow: (follower_id, following_id) => dispatch(deleteFollow(follower_id, following_id)) 15 | }); 16 | 17 | export default connect ( 18 | mapStateToProps, 19 | mapDispatchToProps 20 | )(Follow); 21 | -------------------------------------------------------------------------------- /frontend/actions/likes_actions.js: -------------------------------------------------------------------------------- 1 | import * as LikeApiUtil from "../util/likes_api_util"; 2 | export const RECEIVE_LIKE = "RECEIVE_LIKE"; 3 | export const REMOVE_LIKE = "REMOVE_LIKE"; 4 | 5 | export const receiveLike = like => ({ 6 | type: RECEIVE_LIKE, 7 | like 8 | }); 9 | 10 | export const removeLike = like => ({ 11 | type: REMOVE_LIKE, 12 | like 13 | }); 14 | 15 | export const createLike = postId => dispatch => { 16 | return ( 17 | LikeApiUtil.createLike(postId) 18 | .then( like => dispatch(receiveLike(like))) 19 | ); 20 | }; 21 | 22 | export const deleteLike = postId => dispatch => { 23 | return ( 24 | LikeApiUtil.deleteLike(postId) 25 | .then( like => dispatch(removeLike(like))) 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /.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 all logfiles and tempfiles. 11 | /log/* 12 | /tmp/* 13 | !/log/.keep 14 | !/tmp/.keep 15 | 16 | # Ignore Byebug command history file. 17 | .byebug_history 18 | 19 | 20 | node_modules/ 21 | bundle.js 22 | bundle.js.map 23 | .byebug_history 24 | .DS_Store 25 | npm-debug.log 26 | 27 | # Ignore application configuration 28 | /config/application.yml 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 | -------------------------------------------------------------------------------- /frontend/components/shared/footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Footer = ({ children }) => ( 4 |
    5 |
    6 | 7 |
    8 | 14 | 15 |
    16 |
    @ 2017 TRAVELGRAM
    17 |
    18 |
    19 | ); 20 | 21 | export default Footer; 22 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/less/bordered-pulled.less: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em @fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .@{fa-css-prefix}-pull-left { float: left; } 11 | .@{fa-css-prefix}-pull-right { float: right; } 12 | 13 | .@{fa-css-prefix} { 14 | &.@{fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.@{fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .@{fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /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 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html 3 | 4 | root "static_pages#root" 5 | 6 | namespace :api, defaults: { format: :json } do 7 | resources :users, only: [:index, :create, :edit, :show, :update, :destroy] 8 | resource :session, only: [:create, :destroy, :show] 9 | 10 | resources :posts 11 | resources :likes, only: [:create, :destroy] 12 | resources :comments, only: [:create, :destroy] 13 | # comments nested under posts 14 | 15 | resources :follows, only: [:create, :destroy] 16 | get "follows/delete", to: "follows#delete" 17 | 18 | resources :search_results, only: [:index] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /frontend/actions/search_actions.js: -------------------------------------------------------------------------------- 1 | import * as SearchAPIUtil from '../util/search_api_util'; 2 | 3 | export const RECEIVE_SEARCH_RESULTS = "RECEIVE_SEARCH_RESULTS"; 4 | export const REMOVE_SEARCH_RESULTS = "CLEAR_SEARCH_RESULTS"; 5 | 6 | export const receiveSearchResults = (searchResults) => { 7 | return { 8 | type: RECEIVE_SEARCH_RESULTS, 9 | searchResults 10 | }; 11 | }; 12 | 13 | export const removeSearchResults = () => { 14 | return { 15 | type: REMOVE_SEARCH_RESULTS, 16 | }; 17 | }; 18 | 19 | export const fetchSearchResults = (query) => { 20 | return (dispatch) => { 21 | return SearchAPIUtil.fetchSearchResults(query) 22 | .then((search_results) => dispatch(receiveSearchResults(search_results))); 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/scss/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/less/rotated-flipped.less: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } 5 | .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } 6 | .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } 7 | 8 | .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } 9 | .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .@{fa-css-prefix}-rotate-90, 15 | :root .@{fa-css-prefix}-rotate-180, 16 | :root .@{fa-css-prefix}-rotate-270, 17 | :root .@{fa-css-prefix}-flip-horizontal, 18 | :root .@{fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /app/controllers/api/follows_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::FollowsController < ApplicationController 2 | 3 | def create 4 | @follow = Follow.new(follow_params) 5 | if @follow.save 6 | render "api/follows/show" 7 | else 8 | render json: @follow.errors, status: 422 9 | end 10 | end 11 | 12 | def delete 13 | @follow = Follow.find_by(follower_id: follow_params[:follower_id], following_id: follow_params[:following_id]) 14 | @follow.destroy 15 | render json: @follow 16 | end 17 | 18 | def destroy 19 | @follow = Follow.find(params[:id]) 20 | @follow.destroy 21 | render json: @follow 22 | end 23 | 24 | private 25 | def follow_params 26 | params.require(:follow).permit(:follower_id, :following_id) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /docs/api-endpoints.md: -------------------------------------------------------------------------------- 1 | # API Endpoints 2 | 3 | ## HTML API 4 | 5 | ### Root 6 | 7 | - `GET /` - loads React web app 8 | 9 | ## JSON API 10 | 11 | ### Users 12 | 13 | - `POST /api/users` 14 | - `PATCH /api/users/:id` 15 | - `GET /api/users` 16 | - `GET /api/users/:id` 17 | 18 | ### Session 19 | 20 | - `POST /api/session` 21 | - `DELETE /api/session` 22 | 23 | ### Posts 24 | 25 | - `GET /api/posts` 26 | - `POST /api/posts` 27 | - `GET /api/posts/:id` 28 | - `PATCH /api/posts/:id` 29 | - `DELETE /api/posts/:id` 30 | 31 | ### Likes 32 | 33 | - `POST /api/likes` 34 | - `DELETE /api/likes/:id` 35 | 36 | ### Comments 37 | 38 | - `POST /api/comments` 39 | - `DELETE /api/comments/:id` 40 | 41 | ### Follows 42 | 43 | - `POST /api/users/:userId/follows` 44 | - `DELETE /api/users/:userId/follows/:id` 45 | -------------------------------------------------------------------------------- /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, vendor/assets/javascripts, 5 | // or any plugin's 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 jquery 14 | //= require jquery_ujs 15 | //= require_tree . 16 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :integer not null, primary key 6 | # username :string not null 7 | # email :string not null 8 | # password_digest :string not null 9 | # session_token :string not null 10 | # name :string 11 | # profile_photo :string 12 | # created_at :datetime 13 | # updated_at :datetime 14 | # image_file_name :string 15 | # image_content_type :string 16 | # image_file_size :integer 17 | # image_updated_at :datetime 18 | # 19 | 20 | require 'test_helper' 21 | 22 | class UserTest < ActiveSupport::TestCase 23 | # test "the truth" do 24 | # assert true 25 | # end 26 | end 27 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/scss/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /frontend/actions/comments_actions.js: -------------------------------------------------------------------------------- 1 | import * as CommentApiUtil from "../util/comments_api_util"; 2 | export const RECEIVE_COMMENT = "RECEIVE_COMMENT"; 3 | export const REMOVE_COMMENT = "REMOVE_COMMENT"; 4 | 5 | export const receiveComment = comment => ({ 6 | type: RECEIVE_COMMENT, 7 | comment 8 | }); 9 | 10 | export const removeComment = comment => ({ 11 | type: REMOVE_COMMENT, 12 | comment 13 | }); 14 | 15 | export const createComment = (postId, body) => dispatch => { 16 | return ( 17 | CommentApiUtil.createComment(postId, body) 18 | .then( comment => dispatch(receiveComment(comment))) 19 | ); 20 | }; 21 | 22 | export const deleteComment = id => dispatch => { 23 | return ( 24 | CommentApiUtil.deleteComment(id) 25 | .then( comment => dispatch(removeComment(comment))) 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /frontend/components/search/search_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import Search from './search'; 3 | 4 | import { fetchSearchResults, removeSearchResults } from '../../actions/search_actions'; 5 | import { fetchUser } from '../../actions/users_actions'; 6 | 7 | const mapStateToProps = (state) => { 8 | return { 9 | user: state.user, 10 | searchResults: state.searchResults 11 | }; 12 | }; 13 | 14 | const mapDispatchToProps = (dispatch) => { 15 | return{ 16 | fetchSearchResults: (query) => dispatch(fetchSearchResults(query)), 17 | removeSearchResults: () => dispatch(removeSearchResults()), 18 | fetchUser: (id) => dispatch(fetchUser(id)) 19 | }; 20 | }; 21 | 22 | export default connect( 23 | mapStateToProps, 24 | mapDispatchToProps 25 | )(Search); 26 | -------------------------------------------------------------------------------- /frontend/util/posts_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchPosts = () => { 2 | return $.ajax({ 3 | method: "GET", 4 | url: "/api/posts" 5 | }); 6 | }; 7 | 8 | export const fetchPost = (id) => { 9 | return $.ajax({ 10 | method: "GET", 11 | url: `/api/posts/${id}` 12 | }); 13 | }; 14 | 15 | export const createPost = (post) => { 16 | return $.ajax({ 17 | method: "POST", 18 | url: "/api/posts", 19 | dataType: 'json', 20 | contentType: false, 21 | processData: false, 22 | data: post 23 | }); 24 | }; 25 | 26 | export const updatePost = (post) => { 27 | return $.ajax({ 28 | method: "PATCH", 29 | url: `/api/posts/${post.id}`, 30 | data: {post} 31 | }); 32 | }; 33 | 34 | export const deletePost = (id) => { 35 | return $.ajax({ 36 | method: "DELETE", 37 | url: `/api/posts/${id}` 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /frontend/actions/follows_actions.js: -------------------------------------------------------------------------------- 1 | import * as FollowApiUtil from "../util/follows_api_util"; 2 | export const RECEIVE_FOLLOW = "RECEIVE_FOLLOW"; 3 | export const REMOVE_FOLLOW = "REMOVE_FOLLOW"; 4 | 5 | export const receiveFollow = follow => ({ 6 | type: RECEIVE_FOLLOW, 7 | follow 8 | }); 9 | 10 | export const removeFollow = follow => ({ 11 | type: REMOVE_FOLLOW, 12 | follow 13 | }); 14 | 15 | export const createFollow = (follower_id, following_id) => dispatch => { 16 | return ( 17 | FollowApiUtil.createFollow(follower_id, following_id) 18 | .then( follow => dispatch(receiveFollow(follow))) 19 | ); 20 | }; 21 | 22 | export const deleteFollow = (follower_id, following_id) => dispatch => { 23 | return ( 24 | FollowApiUtil.deleteFollow(follower_id, following_id) 25 | .then( follow => dispatch(removeFollow(follow))) 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/less/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); 7 | src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), 8 | url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), 9 | url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), 10 | url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), 11 | url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/scss/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), 9 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 10 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 11 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/actions/users_actions.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_USERS = "RECEIVE_USERS"; 2 | export const RECEIVE_USER = "RECEIVE_USER"; 3 | 4 | import * as APIUtil from "../util/users_api_util"; 5 | 6 | export const receiveAllUsers = (users) => { 7 | return { 8 | type: RECEIVE_USERS, 9 | users 10 | }; 11 | }; 12 | 13 | export const receiveUser = (user) => { 14 | return { 15 | type: RECEIVE_USER, 16 | user 17 | }; 18 | }; 19 | 20 | export const fetchUsers = () => dispatch => ( 21 | APIUtil.fetchUsers() 22 | .then( (users) => dispatch(receiveAllUsers(users))) 23 | ); 24 | 25 | export const fetchUser = user => dispatch => ( 26 | APIUtil.fetchUser(user) 27 | .then( (user) => dispatch(receiveUser(user))) 28 | ); 29 | 30 | export const updateUser = user => dispatch => ( 31 | APIUtil.updateUser(user) 32 | .then( (user) => dispatch(receiveUser(user))) 33 | ); 34 | -------------------------------------------------------------------------------- /frontend/components/session/auth_form_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { login, logout, signup, receiveErrors } from '../../actions/session_actions'; 3 | import AuthForm from './auth_form'; 4 | 5 | const mapStateToProps = ({ session }) => ({ 6 | loggedIn: Boolean(session.currentUser), 7 | errors: session.errors 8 | }); 9 | 10 | const mapDispatchToProps = (dispatch, { location }) => { 11 | const formType = location.pathname.slice(1); 12 | const processForm = (formType == 'login') ? login : signup; 13 | return { 14 | processForm: user => dispatch(processForm(user)), 15 | clearErrors: () => dispatch(receiveErrors({})), 16 | demoLogin: () => dispatch(login({user: {username: "guest", password: "123456"}})), 17 | formType 18 | }; 19 | }; 20 | 21 | export default connect( 22 | mapStateToProps, 23 | mapDispatchToProps 24 | )(AuthForm); 25 | -------------------------------------------------------------------------------- /frontend/components/user_profile/user_profile_container.js: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux'; 2 | import { fetchPosts } from '../../actions/posts_actions'; 3 | import { fetchUser, updateUser } from '../../actions/users_actions'; 4 | import UserProfile from './user_profile'; 5 | import { selectUserPosts } from '../../reducers/selectors'; 6 | 7 | const mapStateToProps = (state, ownProps) => { 8 | return { 9 | posts: selectUserPosts(state, ownProps.params.user_id), 10 | user: state.user, 11 | currentUser: state.session.currentUser 12 | }; 13 | }; 14 | 15 | const mapDispatchToProps = (dispatch, ownProps) => ({ 16 | fetchPosts: () => dispatch(fetchPosts()), 17 | fetchUser: (id) => dispatch(fetchUser(id)), 18 | updateUser: (user) => dispatch(updateUser(user)) 19 | }); 20 | 21 | export default connect( 22 | mapStateToProps, 23 | mapDispatchToProps 24 | )(UserProfile); 25 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Travelgram 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | config.paperclip_defaults = { 16 | :storage => :s3, 17 | :s3_credentials => { 18 | :bucket => ENV["s3_bucket"], 19 | :access_key_id => ENV["s3_access_key_id"], 20 | :secret_access_key => ENV["s3_secret_access_key"], 21 | :s3_region => ENV["s3_region"] 22 | } 23 | } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/less/animated.less: -------------------------------------------------------------------------------- 1 | // Animated Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .@{fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/scss/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/fixtures/posts.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: posts 4 | # 5 | # id :integer not null, primary key 6 | # description :string 7 | # user_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # image_file_name :string 11 | # image_content_type :string 12 | # image_file_size :integer 13 | # image_updated_at :datetime 14 | # url :string 15 | # 16 | 17 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 18 | 19 | # This model initially had no columns defined. If you add columns to the 20 | # model remove the '{}' from the fixture names and add the columns immediately 21 | # below each fixture, per the syntax in the comments below 22 | # 23 | one: {} 24 | # column: value 25 | # 26 | two: {} 27 | # column: value 28 | -------------------------------------------------------------------------------- /frontend/components/posts/modal/post_item_modal_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { fetchPost } from '../../../actions/posts_actions'; 3 | import { createLike, deleteLike } from '../../../actions/likes_actions'; 4 | import { createComment, deleteComment } from '../../../actions/comments_actions'; 5 | import PostItemModal from './post_item_modal'; 6 | 7 | const mapStateToProps = (state) => ({ 8 | currentUser: state.session.currentUser, 9 | user: state.user 10 | }); 11 | 12 | const mapDispatchToProps = (dispatch) => { 13 | return ({ 14 | createLike: (postId) => dispatch(createLike(postId)), 15 | deleteLike: (postId) => dispatch(deleteLike(postId)), 16 | createComment: (postId, body) => dispatch(createComment(postId, body)), 17 | deleteComment: id => dispatch(deleteComment(id)) 18 | }); 19 | 20 | }; 21 | 22 | export default connect ( 23 | mapStateToProps, 24 | mapDispatchToProps 25 | )(PostItemModal); 26 | -------------------------------------------------------------------------------- /docs/component-hierarchy.md: -------------------------------------------------------------------------------- 1 | ## Component Hierarchy 2 | 3 | **App** 4 | + LoginForm 5 | + SignupForm 6 | + PhotosIndexContainer 7 | 8 | 9 | **PhotosIndexContainer** 10 | - PhotoListItem 11 | + Likes 12 | + CommentsContainer 13 | + CommentItem 14 | + CommentForm 15 | 16 | 17 | **UserProfileContainer** 18 | + UserInfo 19 | - EditUser 20 | + UserPostIndex 21 | - UserPostItem 22 | 23 | **PostShowContainer** 24 | - PhotoListItem 25 | + Likes 26 | + CommentsContainer 27 | - CommentItem 28 | + CommentForm 29 | 30 | **PostPhotoContainer** 31 | + NewPost 32 | 33 | 34 | 35 | ## Routes 36 | 37 | |Path | Component | 38 | |-------|-------------| 39 | | "/" | "App" | 40 | | "/" | "PhotosIndexContainer" | 41 | | "/login" | "AuthFormContainer" | 42 | | "/signup" | "AuthFormContainer" | 43 | | "/posts/:postId" | "PostShowContainer" | 44 | | "/users/:userId" | "UserProfileContainer" | 45 | | "/create-post" | "PostPhotoContainer" | 46 | -------------------------------------------------------------------------------- /app/assets/stylesheets/base/gallery.scss: -------------------------------------------------------------------------------- 1 | .border-box { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | 5 | box-sizing: border-box; 6 | } 7 | 8 | .paddingBlock { 9 | padding: 20px 0; 10 | } 11 | 12 | .eqWrap { 13 | display: flex; 14 | } 15 | 16 | .eq { 17 | padding: 10px; 18 | } 19 | 20 | .eq:nth-of-type(odd) { 21 | background: white; 22 | } 23 | 24 | .eq:nth-of-type(even) { 25 | background: white; 26 | } 27 | 28 | .equalHW { 29 | flex: 1; 30 | } 31 | 32 | .equalHMWrap { 33 | justify-content: space-between; 34 | } 35 | 36 | .equalHM { 37 | width: 32%; 38 | } 39 | 40 | .equalHMRWrap { 41 | justify-content: space-between; 42 | flex-wrap: wrap; 43 | } 44 | 45 | .equalHMR { 46 | width: 32%; 47 | margin-bottom: 2%; 48 | } 49 | 50 | .equalHMVWrap { 51 | flex-wrap: wrap; 52 | } 53 | 54 | .equalHMV { 55 | width: 32%; 56 | margin: %; 57 | } 58 | 59 | .equalHMV:nth-of-type(3n) { 60 | margin-right: 0; 61 | } 62 | 63 | .equalHMV:nth-of-type(3n+1) { 64 | margin-left: 0; 65 | } 66 | -------------------------------------------------------------------------------- /frontend/actions/session_actions.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_CURRENT_USER = "RECEIVE_CURRENT_USER"; 2 | export const RECEIVE_ERRORS = "RECEIVE_ERRORS"; 3 | 4 | import * as APIUtil from "../util/session_api_util"; 5 | 6 | export const receiveCurrentUser = (currentUser) => { 7 | return { 8 | type: RECEIVE_CURRENT_USER, 9 | currentUser 10 | }; 11 | }; 12 | 13 | export const receiveErrors = (errors) => ( 14 | { 15 | type: RECEIVE_ERRORS, 16 | errors 17 | } 18 | ); 19 | 20 | export const login = user => dispatch => ( 21 | APIUtil.login(user) 22 | .then( user => dispatch(receiveCurrentUser(user)), 23 | err => dispatch(receiveErrors(err.responseJSON))) 24 | ); 25 | 26 | export const signup = user => dispatch => ( 27 | APIUtil.signup(user) 28 | .then( user => dispatch(receiveCurrentUser(user)), 29 | err => dispatch(receiveErrors(err.responseJSON))) 30 | ); 31 | 32 | export const logout = () => dispatch => ( 33 | APIUtil.logout().then(user => dispatch(receiveCurrentUser(null))) 34 | ); 35 | -------------------------------------------------------------------------------- /frontend/reducers/users_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_USERS, RECEIVE_USER } from '../actions/users_actions'; 2 | import { RECEIVE_FOLLOW, REMOVE_FOLLOW } from '../actions/follows_actions'; 3 | import { getIndexById } from '../util/util'; 4 | import merge from 'lodash/merge'; 5 | 6 | const UsersReducer = (oldState = {}, action) => { 7 | Object.freeze(oldState); 8 | switch (action.type) { 9 | case RECEIVE_USERS: 10 | return action.users; 11 | case RECEIVE_USER: 12 | return merge({}, action.user); 13 | case RECEIVE_FOLLOW: 14 | let copyAddFollow = merge({}, oldState); 15 | copyAddFollow.followers.push(action.follow.follower); 16 | return copyAddFollow; 17 | case REMOVE_FOLLOW: 18 | let copyRemFollow = merge({}, oldState); 19 | let followeeIndex = getIndexById(copyRemFollow.followers, action.follow.follower_id); 20 | copyRemFollow.followers.splice(followeeIndex, 1); 21 | return copyRemFollow; 22 | default: 23 | return oldState; 24 | } 25 | }; 26 | 27 | export default UsersReducer; 28 | -------------------------------------------------------------------------------- /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 | development: 14 | secret_key_base: 9772545d0d1129b096fb7e35b342f706ec982cb707ece7df3ad30a132618e410ed2e081db294d9a1fe669a1ebd02e9d55c7a3041706a6631afa863cecc04a5c8 15 | 16 | test: 17 | secret_key_base: 526ca6a8424e51e8a25f9c7ef115e1d8ef28de27058999249935d128eeef7e6716032938d558bf309839643021c06ad15e7fcdbcb41273640c53a65904ccc903 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /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 | # puts "\n== Copying sample files ==" 22 | # unless File.exist?('config/database.yml') 23 | # cp 'config/database.yml.sample', 'config/database.yml' 24 | # end 25 | 26 | puts "\n== Preparing database ==" 27 | system! 'bin/rails db:setup' 28 | 29 | puts "\n== Removing old logs and tempfiles ==" 30 | system! 'bin/rails log:clear tmp:clear' 31 | 32 | puts "\n== Restarting application server ==" 33 | system! 'bin/rails restart' 34 | end 35 | -------------------------------------------------------------------------------- /frontend/components/posts/comment_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | class CommentItem extends React.Component { 5 | constructor(props){ 6 | super(props); 7 | this.deleteComment = this.deleteComment.bind(this); 8 | } 9 | 10 | 11 | deleteComment(){ 12 | this.props.deleteComment(this.props.comment.id); 13 | } 14 | 15 | renderDeleteButton(){ 16 | if(this.props.currentUser){ 17 | if(this.props.comment.user_id == this.props.currentUser.id){ 18 | return ; 19 | } 20 | } 21 | } 22 | 23 | render(){ 24 | return( 25 |
  • 26 |
    27 | {this.props.comment.username} {this.props.comment.body} 28 |
    29 | {this.renderDeleteButton()} 30 |
  • 31 | ); 32 | } 33 | } 34 | 35 | export default CommentItem; 36 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :integer not null, primary key 6 | # username :string not null 7 | # email :string not null 8 | # password_digest :string not null 9 | # session_token :string not null 10 | # name :string 11 | # profile_photo :string 12 | # created_at :datetime 13 | # updated_at :datetime 14 | # image_file_name :string 15 | # image_content_type :string 16 | # image_file_size :integer 17 | # image_updated_at :datetime 18 | # 19 | 20 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 21 | 22 | one: 23 | username: MyString 24 | email: MyString 25 | password_digest: MyString 26 | session_token: MyString 27 | name: MyString 28 | profile_photo: MyString 29 | 30 | two: 31 | username: MyString 32 | email: MyString 33 | password_digest: MyString 34 | session_token: MyString 35 | name: MyString 36 | profile_photo: MyString 37 | -------------------------------------------------------------------------------- /docs/sample-state.md: -------------------------------------------------------------------------------- 1 | ```js 2 | { 3 | currentUser: { 4 | id: 1, 5 | username: "travel-academy", 6 | name: "Travelgram Academy" 7 | }, 8 | forms: { 9 | signUp: {errors: []}, 10 | logIn: {errors: []}, 11 | uploadPhoto: {errors: ["url can't be blank"]} 12 | }, 13 | user: { 14 | id: 1, 15 | username: "travel-academy", 16 | name: "Travelgram Academy" 17 | }, 18 | posts: { 19 | 1: { 20 | description: "is useful to plan", 21 | url: "http//:www.url.com/image.jpg", 22 | user: { 23 | id: 1, 24 | username: "travel-academy", 25 | name: "Travelgram Academy" 26 | } 27 | comments: { 28 | 1: { 29 | body: "The best beach on the world!" 30 | user_id: 1, 31 | photo_id: 1 32 | }, 33 | 2: { 34 | body: "Indeed!" 35 | user_id: 2, 36 | photo_id: 1 37 | } 38 | } 39 | likes: { 40 | 1: { 41 | user_id: 1, 42 | photo_id: 1 43 | } 44 | } 45 | } 46 | 47 | } 48 | 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /frontend/components/posts/posts_feed_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { fetchPosts } from '../../actions/posts_actions'; 3 | import { createLike, deleteLike } from '../../actions/likes_actions'; 4 | import { createComment, deleteComment } from '../../actions/comments_actions'; 5 | import PostsFeed from './posts_feed'; 6 | 7 | const mapStateToProps = (state) => { 8 | return { 9 | currentUser: state.session.currentUser, 10 | user: state.user, 11 | posts: Object.keys(state.posts).map(id => state.posts[id]).reverse(), 12 | fetching: state.fetching 13 | }; 14 | }; 15 | 16 | const mapDispatchToProps = (dispatch) => { 17 | return ({ 18 | fetchPosts: () => dispatch(fetchPosts()), 19 | createLike: (postId) => dispatch(createLike(postId)), 20 | deleteLike: (postId) => dispatch(deleteLike(postId)), 21 | createComment: (postId, body) => dispatch(createComment(postId, body)), 22 | deleteComment: id => dispatch(deleteComment(id)) 23 | }); 24 | 25 | }; 26 | 27 | export default connect( 28 | mapStateToProps, 29 | mapDispatchToProps 30 | )(PostsFeed); 31 | -------------------------------------------------------------------------------- /frontend/util/modal_style.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | overlay : { 3 | position : 'fixed', 4 | top : 0, 5 | left : 0, 6 | right : 0, 7 | bottom : 0, 8 | backgroundColor : 'rgba(0, 0, 0, 0.65)', 9 | zIndex : 10 10 | }, 11 | content : { 12 | position : 'absolute', 13 | width: '935px', 14 | height: '540px', 15 | top : '50%', 16 | left : '50%', 17 | right : 'auto', 18 | bottom : 'auto', 19 | marginRight : '-50%', 20 | transform : 'translate(-50%, -50%)', 21 | border : '0px solid #ccc', 22 | background : '#fff', 23 | overflow : 'none', 24 | WebkitOverflowScrolling : 'touch', 25 | borderRadius : '2px', 26 | outline : 'none', 27 | padding : '0px', 28 | opacity : '0', 29 | transition : 'opacity 1s', 30 | zIndex : 11 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UsersController < ApplicationController 2 | 3 | def index 4 | @users = User.includes(:posts, :followers, :followees).all 5 | end 6 | 7 | def show 8 | @user = User.includes(:posts, :followers, :followees).find(params[:id]) 9 | end 10 | 11 | def edit 12 | @user = User.find(params[:id]) 13 | end 14 | 15 | def create 16 | @user = User.new(user_params) 17 | if @user.save 18 | login!(@user) 19 | render "api/users/show" 20 | else 21 | render json: @user.errors, status: 422 22 | end 23 | end 24 | 25 | def update 26 | update_params = user_params 27 | update_params = user_params_no_image if params[:user][:image] == "null" 28 | @user = User.find(params[:id]) 29 | if @user.update(update_params) 30 | render @user 31 | else 32 | render json: @user.errors, status: 422 33 | end 34 | end 35 | 36 | private 37 | def user_params 38 | params.require(:user).permit(:username, :password, :email, :name, :bio, :website, :image) 39 | end 40 | def user_params_no_image 41 | params.require(:user).permit(:username, :password, :email, :name, :bio, :website) 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /frontend/components/search/search_result_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | 4 | class SearchResultItem extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | 9 | this.profilePage = this.profilePage.bind(this); 10 | } 11 | 12 | profilePage(){ 13 | let id = this.props.user.id; 14 | this.props.router.push(`users/${id}`); 15 | this.props.closeResultsList(); 16 | } 17 | 18 | render() { 19 | return ( 20 |
  • 21 | 22 |
    23 | img 24 |
    25 |
    26 | {this.props.user.username} 27 | {this.props.user.name} 28 |
    29 | 30 |
  • 31 | ); 32 | } 33 | 34 | } 35 | 36 | export default withRouter(SearchResultItem); 37 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains migration options to ease your Rails 5.0 upgrade. 4 | # 5 | # Read the Guide for Upgrading Ruby on Rails for more info on each option. 6 | 7 | # Enable per-form CSRF tokens. Previous versions had false. 8 | Rails.application.config.action_controller.per_form_csrf_tokens = true 9 | 10 | # Enable origin-checking CSRF mitigation. Previous versions had false. 11 | Rails.application.config.action_controller.forgery_protection_origin_check = true 12 | 13 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. 14 | # Previous versions had false. 15 | ActiveSupport.to_time_preserves_timezone = true 16 | 17 | # Require `belongs_to` associations by default. Previous versions had false. 18 | Rails.application.config.active_record.belongs_to_required_by_default = true 19 | 20 | # Do not halt callback chains when a callback returns false. Previous versions had true. 21 | ActiveSupport.halt_callback_chains_on_return_false = false 22 | 23 | # Configure SSL options to enable HSTS with subdomains. Previous versions had false. 24 | Rails.application.config.ssl_options = { hsts: { subdomains: true } } 25 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var webpack = require("webpack"); 3 | 4 | var plugins = []; // if using any plugins for both dev and production 5 | var devPlugins = []; // if using any plugins for development 6 | 7 | var prodPlugins = [ 8 | new webpack.DefinePlugin({ 9 | 'process.env': { 10 | 'NODE_ENV': JSON.stringify('production') 11 | } 12 | }), 13 | new webpack.optimize.UglifyJsPlugin({ 14 | compress: { 15 | warnings: true 16 | } 17 | }) 18 | ]; 19 | 20 | plugins = plugins.concat( 21 | process.env.NODE_ENV === 'production' ? prodPlugins : devPlugins 22 | ); 23 | 24 | module.exports = { 25 | context: __dirname, 26 | entry: "./frontend/travelgram.jsx", 27 | output: { 28 | path: path.resolve(__dirname, 'app', 'assets', 'javascripts'), 29 | filename: "bundle.js" 30 | }, 31 | plugins: plugins, 32 | module: { 33 | loaders: [ 34 | { 35 | test: [/\.jsx?$/, /\.js?$/], 36 | exclude: /node_modules/, 37 | loader: 'babel-loader', 38 | query: { 39 | presets: ['es2015', 'react'] 40 | } 41 | } 42 | ] 43 | }, 44 | devtool: 'source-maps', 45 | resolve: { 46 | extensions: [".js", ".jsx", "*"] 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "travelgram", 3 | "version": "1.0.0", 4 | "description": "This README would normally document whatever steps are necessary to get the application up and running.", 5 | "main": "index.js", 6 | "directories": { 7 | "doc": "docs", 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "postinstall": "webpack" 13 | }, 14 | "engines": { 15 | "node": "6.7.0", 16 | "npm": "3.10.7" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/s-sanel/travelgram.git" 21 | }, 22 | "keywords": [], 23 | "author": "", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/s-sanel/travelgram/issues" 27 | }, 28 | "homepage": "https://github.com/s-sanel/travelgram#readme", 29 | "dependencies": { 30 | "babel-core": "^6.24.1", 31 | "babel-loader": "^6.4.1", 32 | "babel-preset-es2015": "^6.24.1", 33 | "babel-preset-react": "^6.24.1", 34 | "react": "^15.5.4", 35 | "react-dom": "^15.5.4", 36 | "react-modal": "^1.7.7", 37 | "react-redux": "^5.0.4", 38 | "react-router": "^3.0.5", 39 | "redux": "^3.6.0", 40 | "redux-thunk": "^2.2.0", 41 | "webpack": "^2.4.1" 42 | }, 43 | "devDependencies": { 44 | "redux-logger": "^3.0.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /frontend/components/posts/posts_feed.jsx: -------------------------------------------------------------------------------- 1 | import { withRouter } from 'react-router'; 2 | import React from 'react'; 3 | import PostItem from './post_item'; 4 | import Spinner from '../shared/spinner'; 5 | 6 | 7 | class PostsFeed extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | componentDidMount(){ 13 | this.props.fetchPosts(); 14 | } 15 | 16 | componentWillReceiveProps(newProps){ 17 | if(!newProps.currentUser){ 18 | this.props.router.push("/signup"); 19 | } 20 | } 21 | 22 | render() { 23 | if (this.props.fetching) return ; 24 | return ( 25 |
    26 |

     

    27 |
      28 | { 29 | this.props.posts.slice(0,15).map(post => ( 30 | 40 | )) 41 | } 42 |
    43 |
    44 | ); 45 | } 46 | } 47 | 48 | export default withRouter(PostsFeed); 49 | -------------------------------------------------------------------------------- /frontend/actions/posts_actions.js: -------------------------------------------------------------------------------- 1 | import * as PostApiUtil from "../util/posts_api_util"; 2 | export const RECEIVE_ALL_POSTS = "RECEIVE_ALL_POSTS"; 3 | export const RECEIVE_POST = "RECEIVE_POST"; 4 | export const REMOVE_POST = "REMOVE_POST"; 5 | export const FETCH_POSTS = "FETCH_POSTS"; 6 | export const FETCH_POST = "FETCH_POST"; 7 | export const CREATE_POST = "CREATE_POST"; 8 | 9 | export const receiveAllPosts = posts => ({ 10 | type: RECEIVE_ALL_POSTS, 11 | posts 12 | }); 13 | 14 | export const receivePost = post => ({ 15 | type: RECEIVE_POST, 16 | post 17 | }); 18 | 19 | export const removePost = post => ({ 20 | type: REMOVE_POST, 21 | post 22 | }); 23 | 24 | 25 | export const fetchPosts = () => { 26 | return (dispatch) => { 27 | dispatch({ type: FETCH_POSTS }); 28 | return PostApiUtil.fetchPosts().then(posts => dispatch(receiveAllPosts(posts))); 29 | }; 30 | }; 31 | 32 | 33 | export const fetchPost = id => dispatch => ( 34 | PostApiUtil.fetchPost(id).then(post => dispatch(receivePost(post))) 35 | ); 36 | 37 | export const createPost = post => dispatch => ( 38 | PostApiUtil.createPost(post).then(post => dispatch(receivePost(post))) 39 | ); 40 | 41 | export const updatePost = post => dispatch => ( 42 | PostApiUtil.updatePost(post).then(post => dispatch(receivePost(post))) 43 | ); 44 | 45 | export const deletePost = post => dispatch => ( 46 | PostApiUtil.deletePost(post).then(post => dispatch(removePost(post))) 47 | ); 48 | -------------------------------------------------------------------------------- /frontend/components/navigation/nav_bar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | import NavLinksContainer from './nav_links_container'; 4 | import SearchContainer from '../search/search_container'; 5 | 6 | class NavBar extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.homePageNav = this.homePageNav.bind(this); 11 | } 12 | 13 | homePageNav(){ 14 | this.props.router.push("/"); 15 | } 16 | 17 | profilePageNav(){ 18 | this.props.router.push(`/${this.props.currentUser.id}`); 19 | } 20 | 21 | handleLogout(e) { 22 | e.preventDefault(); 23 | this.props.logout().then(() => this.props.router.push('/signup')); 24 | } 25 | 26 | render() { 27 | return ( 28 |
    29 |
    30 | 40 |
    41 |
    42 | ); 43 | } 44 | 45 | } 46 | 47 | export default withRouter(NavBar); 48 | -------------------------------------------------------------------------------- /app/controllers/api/posts_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::PostsController < ApplicationController 2 | 3 | def index 4 | # @posts = Post.where(user: current_user.followees).includes(:comments => [:user]).includes(:user, :likes).order(created_at: :desc).limit(2) 5 | @posts = Post.includes(:comments => [:user]).includes(:user, :likes).order(created_at: :desc) 6 | render :index 7 | 8 | # followees = current_user.followees 9 | # followees.map { |followee| followee.id } 10 | # @posts = Post.where(user_id: followees).includes(:user, comments: :user, likes: :user).order('created_at DESC') 11 | 12 | end 13 | 14 | def show 15 | # @post = Post.includes(:user, :comments, :likes).find(params[:id]) 16 | @posts = Post.includes(:comments => [:user]).includes(:user, :likes).all 17 | end 18 | 19 | def edit 20 | @post = Post.includes(:user, :comments, :likes).find(params[:id]) 21 | end 22 | 23 | 24 | def create 25 | @post = current_user.posts.new(post_params) 26 | if @post.save 27 | render "api/posts/show" 28 | else 29 | render json: @post.errors, status: 422 30 | end 31 | end 32 | 33 | def update 34 | @post = current_user.posts.find(params[:id]) 35 | if @post.update(post_params) 36 | render json: @post 37 | else 38 | render json: @post.errors, status: 422 39 | end 40 | end 41 | 42 | def destroy 43 | @post = current_user.posts.find(params[:id]) 44 | @post.destroy 45 | render json: @post 46 | end 47 | 48 | private 49 | 50 | def post_params 51 | params.require(:post).permit(:image, :description) 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.scss: -------------------------------------------------------------------------------- 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, vendor/assets/stylesheets, 6 | * or any plugin's 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 | 14 | *= require_self 15 | */ 16 | 17 | // CSS Reset 18 | @import "base/reset.scss"; 19 | 20 | // @import "font-awesome/css/font-awesome.min.css"; 21 | @font-face { 22 | font-family: 'Billabong'; 23 | src: asset-url('custom-fonts/billabong.eot'), asset-url('custom-fonts/billabong.ttf'); 24 | } 25 | 26 | // Core 27 | @import "base/colors.scss"; 28 | @import "base/fonts.scss"; 29 | @import "base/layout.scss"; 30 | 31 | // Components 32 | @import "components/*"; 33 | // @import "components/_landing_page.scss"; 34 | // @import "components/_main_content.scss"; 35 | // @import "components/_posts_feed.scss"; 36 | // @import "components/_user_profile_page.scss"; 37 | 38 | 39 | @media only screen and (max-width : 768px) { 40 | .landing-page-img { display: none; } 41 | } 42 | 43 | @media only screen and (max-width : 500px) { 44 | .search { display: none; } 45 | } 46 | -------------------------------------------------------------------------------- /app/assets/stylesheets/base/reset.scss: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | margin: 0; 15 | border: 0; 16 | padding: 0; 17 | outline: 0; 18 | font: inherit; 19 | color: inherit; 20 | text-align: inherit; 21 | text-decoration: inherit; 22 | vertical-align: inherit; 23 | box-sizing: inherit; 24 | background: transparent; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1.16; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | input[type="text"], 51 | input[type="password"], 52 | textarea, 53 | button { 54 | -webkit-appearance: none; 55 | -moz-appearance: none; 56 | appearance: none; 57 | } 58 | 59 | a, button, input[type="submit"] { 60 | cursor: pointer; 61 | } 62 | 63 | .group:after { 64 | content: ""; 65 | display: block; 66 | clear: both; 67 | } 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/components/greeting/greeting.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | 4 | class Greeting extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.handleLogout = this.handleLogout.bind(this); 9 | this.profilePage = this.profilePage.bind(this); 10 | this.uploadPost = this.uploadPost.bind(this); 11 | } 12 | 13 | profilePage(){ 14 | let id = this.props.currentUser.id; 15 | this.props.router.push(`users/${id}`); 16 | } 17 | 18 | uploadPost(){ 19 | this.props.router.push("/upload-post"); 20 | } 21 | 22 | handleLogout(e) { 23 | e.preventDefault(); 24 | this.props.logout().then(() => this.props.router.push('/signup')); 25 | } 26 | 27 | render() { 28 | return ( 29 |
    30 |
    31 | 32 | 33 | 34 |
    35 |
    36 | 37 | 38 | 39 |
    40 |
    41 | 42 | 43 | 44 |
    45 |
    46 | 47 |
    48 |
    ); 49 | } 50 | 51 | } 52 | export default withRouter(Greeting); 53 | -------------------------------------------------------------------------------- /frontend/components/posts/add_comment_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | class AddCommentForm extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.state = {body: ""}; 9 | this.handleSubmit = this.handleSubmit.bind(this); 10 | } 11 | 12 | handleSubmit(e){ 13 | e.preventDefault(); 14 | let postId = this.props.post.id; 15 | let body = this.state.body; 16 | this.props.createComment(postId, body).then(() => this.resetState()); 17 | if (this.props.incrementCommentCount) this.props.incrementCommentCount(); 18 | } 19 | 20 | update(field) { 21 | return e => this.setState({ 22 | [field]: e.currentTarget.value 23 | }); 24 | } 25 | 26 | resetState(){ 27 | this.setState({body: ""}); 28 | } 29 | 30 | isDisabledSubmit(){ 31 | if(this.state.body.length === 0) { 32 | return "disabled"; 33 | }else { 34 | return ""; 35 | } 36 | } 37 | 38 | render() { 39 | let post_id = this.props.post.id; 40 | let comm = "input-comment-" + post_id; 41 | let disabled = this.isDisabledSubmit(); 42 | let submitClass ="add-comment-submit " + disabled; 43 | 44 | return ( 45 |
    46 |
    47 | 48 | 49 |
    50 |
    51 | ); 52 | 53 | } 54 | 55 | } 56 | 57 | export default AddCommentForm; 58 | -------------------------------------------------------------------------------- /frontend/components/root.jsx: -------------------------------------------------------------------------------- 1 | import { Router, Route, IndexRoute, hashHistory } from 'react-router'; 2 | import { Provider } from 'react-redux'; 3 | import React from 'react'; 4 | import App from './app' 5 | import AuthFormContainer from './session/auth_form_container'; 6 | import UserProfileContainer from './user_profile/user_profile_container'; 7 | import EditProfileContainer from './user_profile/edit_profile_container'; 8 | import UploadPostContainer from './posts/upload_post_container'; 9 | import Posts from './posts/posts'; 10 | 11 | const Root = ({ store }) => { 12 | const _ensureLoggedIn = (nextState, replace) => { 13 | const currentUser = store.getState().session.currentUser; 14 | if (!currentUser) { 15 | replace('/signup'); 16 | } 17 | }; 18 | 19 | const _redirectIfLoggedIn = (nextState, replace) => { 20 | const currentUser = store.getState().session.currentUser; 21 | if (currentUser) { 22 | replace('/'); 23 | } 24 | } 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | }; 41 | 42 | export default Root; 43 | -------------------------------------------------------------------------------- /frontend/components/navigation/nav_links.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | 4 | class NavLinks extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.handleLogout = this.handleLogout.bind(this); 9 | this.profilePage = this.profilePage.bind(this); 10 | this.uploadPost = this.uploadPost.bind(this); 11 | } 12 | 13 | profilePage(){ 14 | let id = this.props.currentUser.id; 15 | this.props.router.push(`users/${id}`); 16 | } 17 | 18 | uploadPost(){ 19 | this.props.router.push("/upload-post"); 20 | } 21 | 22 | handleLogout(e) { 23 | e.preventDefault(); 24 | this.props.logout().then(() => this.props.router.push('/signup')); 25 | } 26 | 27 | render() { 28 | return ( 29 |
    30 |
    31 | 32 | 33 |
    34 |
    35 | 36 | 37 | 38 |
    39 |
    40 | 41 | 42 | 43 |
    44 |
    45 | 46 |
    47 |
    ); 48 | } 49 | 50 | } 51 | export default withRouter(NavLinks); 52 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | ## Uncomment and set this to only include directories you want to watch 5 | # directories %w(app lib config test spec features) \ 6 | # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")} 7 | 8 | ## Note: if you are using the `directories` clause above and you are not 9 | ## watching the project directory ('.'), then you will want to move 10 | ## the Guardfile to a watched dir and symlink it back, e.g. 11 | # 12 | # $ mkdir config 13 | # $ mv Guardfile config/ 14 | # $ ln -s config/Guardfile . 15 | # 16 | # and, you'll have to watch "config/Guardfile" instead of "Guardfile" 17 | 18 | guard 'livereload' do 19 | extensions = { 20 | css: :css, 21 | scss: :css, 22 | sass: :css, 23 | js: :js, 24 | coffee: :js, 25 | html: :html, 26 | png: :png, 27 | gif: :gif, 28 | jpg: :jpg, 29 | jpeg: :jpeg, 30 | # less: :less, # uncomment if you want LESS stylesheets done in browser 31 | } 32 | 33 | rails_view_exts = %w(erb haml slim) 34 | 35 | # file types LiveReload may optimize refresh for 36 | compiled_exts = extensions.values.uniq 37 | watch(%r{public/.+\.(#{compiled_exts * '|'})}) 38 | 39 | extensions.each do |ext, type| 40 | watch(%r{ 41 | (?:app|vendor) 42 | (?:/assets/\w+/(?[^.]+) # path+base without extension 43 | (?\.#{ext})) # matching extension (must be first encountered) 44 | (?:\.\w+|$) # other extensions 45 | }x) do |m| 46 | path = m[1] 47 | "/assets/#{path}.#{type}" 48 | end 49 | end 50 | 51 | # file needing a full reload of the page anyway 52 | watch(%r{app/views/.+\.(#{rails_view_exts * '|'})$}) 53 | watch(%r{app/helpers/.+\.rb}) 54 | watch(%r{config/locales/.+\.yml}) 55 | end 56 | -------------------------------------------------------------------------------- /docs/schema.md: -------------------------------------------------------------------------------- 1 | # Schema Information 2 | 3 | ## users 4 | column name | data type | details 5 | ----------------|-----------|----------------------- 6 | id | integer | not null, primary key 7 | username | string | not null, indexed, unique 8 | email | string | not null, indexed, unique 9 | password_digest | string | not null 10 | session_token | string | not null, indexed, unique 11 | name | string | 12 | profile_photo | string | 13 | 14 | ## posts 15 | column name | data type | details 16 | ------------|-----------|----------------------- 17 | id | integer | not null, primary key 18 | url | string | not null 19 | description | text | 20 | user_id | integer | not null, foreign key (references users), indexed 21 | 22 | ## comments 23 | column name | data type | details 24 | ------------|-----------|----------------------- 25 | id | integer | not null, primary key 26 | body | string | not null 27 | user_id | integer | not null, foreign key (references users), indexed 28 | post_id | integer | not null, foreign key (references photos), indexed 29 | 30 | ## likes 31 | column name | data type | details 32 | ------------|-----------|----------------------- 33 | id | integer | not null, primary key 34 | user_id | integer | not null, foreign key (references users), indexed, unique[post_id] 35 | post_id | integer | not null, foreign key (references photos), indexed 36 | 37 | ## follows 38 | column name | data type | details 39 | ------------|-----------|----------------------- 40 | id | integer | not null, primary key 41 | follower_id | integer | not null, foreign key (references users), indexed, unique[following_id] 42 | following_id| integer | not null, foreign key (references users), indexed 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/reducers/posts_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_ALL_POSTS, RECEIVE_POST, REMOVE_POST } from '../actions/posts_actions'; 2 | import { RECEIVE_LIKE, REMOVE_LIKE } from '../actions/likes_actions'; 3 | import { RECEIVE_COMMENT, REMOVE_COMMENT } from '../actions/comments_actions'; 4 | import merge from 'lodash/merge'; 5 | import { getIndex } from '../util/util'; 6 | 7 | 8 | const PostsReducer = (oldState = {}, action) => { 9 | Object.freeze(oldState); 10 | switch (action.type) { 11 | case RECEIVE_ALL_POSTS: 12 | let posts = action.posts; 13 | return merge({}, oldState, posts); 14 | case RECEIVE_POST: 15 | return merge({}, oldState, { [action.post.id]: action.post }); 16 | case REMOVE_POST: 17 | let newState = merge({}, oldState); 18 | delete newState[action.post.id]; 19 | return newState; 20 | case RECEIVE_LIKE: 21 | let copyState = merge({}, oldState); 22 | copyState[action.like.post_id].likes.push(action.like); 23 | return copyState; 24 | case REMOVE_LIKE: 25 | let nextState = merge({}, oldState); 26 | let likeIndex = getIndex(nextState[action.like.post_id].likes, action.like); 27 | nextState[action.like.post_id].likes.splice(likeIndex, 1); 28 | return nextState; 29 | case RECEIVE_COMMENT: 30 | let copyAddComState = merge({}, oldState); 31 | copyAddComState[action.comment.post_id].comments.push(action.comment); 32 | return copyAddComState; 33 | case REMOVE_COMMENT: 34 | let copyRemComState = merge({}, oldState); 35 | let commentIndex = getIndex(copyRemComState[action.comment.post_id].comments, action.comment); 36 | copyRemComState[action.comment.post_id].comments.splice(commentIndex, 1); 37 | return copyRemComState; 38 | default: 39 | return oldState; 40 | } 41 | }; 42 | 43 | 44 | export default PostsReducer; 45 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/less/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | .fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | .fa-icon-rotate(@degrees, @rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})"; 16 | -webkit-transform: rotate(@degrees); 17 | -ms-transform: rotate(@degrees); 18 | transform: rotate(@degrees); 19 | } 20 | 21 | .fa-icon-flip(@horiz, @vert, @rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)"; 23 | -webkit-transform: scale(@horiz, @vert); 24 | -ms-transform: scale(@horiz, @vert); 25 | transform: scale(@horiz, @vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | .sr-only() { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | .sr-only-focusable() { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # FresherNote 2 | 3 | [Travelgram][heroku] 4 | 5 | [Trello link][trello] 6 | 7 | [heroku]: http://www.travelgram.world/ 8 | [trello]: https://trello.com/b/fa9hOu1T/travelgram 9 | 10 | ## Minimum Viable Product 11 | 12 | Travelgram is a web application inspired by Instagram built using Ruby on Rails 13 | and React/Redux. By the end of Week 9, this app will, at a minimum, satisfy the 14 | following criteria with smooth, bug-free navigation, adequate seed data and 15 | sufficient CSS styling: 16 | 17 | - [ ] Hosting on Heroku 18 | - [ ] [Production README](../README.md) 19 | - [ ] New account creation, login, and guest/demo login 20 | - [ ] Images 21 | - [ ] Likes 22 | - [ ] Commenting on images 23 | - [ ] Following & Photo feed 24 | 25 | ## Design Docs 26 | * [View Wireframes](wireframes) 27 | * [React Components](components) 28 | * [API endpoints](api-endpoints) 29 | * [DB schema](schema) 30 | * [Sample State](sample-state) 31 | 32 | ## Implementation Timeline 33 | 34 | ### Phase 1: Backend setup and Front End User Authentication (2 days) 35 | 36 | **Objective:** Functioning rails project with front-end Authentication 37 | 38 | ### Phase 2: Image Model, API, and components (2 days) 39 | 40 | **Objective:** Posts with uploaded photos can be created, displayed, edited and deleted. 41 | 42 | ### Phase 3: Likes (1 days) 43 | 44 | **Objective:** Users can like and unlike photos. 45 | 46 | ### Phase 4: Commenting on images (1 day) 47 | 48 | **Objective:** Users can create and delete comments on photos. 49 | 50 | ### Phase 5: Following & Photo feed (2 day) 51 | 52 | **Objective:** Users can follow/unfollow another users. Photo feed displays photos of followed users. 53 | 54 | ### Phase 6: - Pagination / infinite scroll for Photo feed (1 day) 55 | 56 | **Objective:** Add infinite scroll to Photo feed 57 | 58 | ### Bonus Features (TBD) 59 | - [ ] Direct messaging 60 | - [ ] Hash tags 61 | -------------------------------------------------------------------------------- /app/models/post.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: posts 4 | # 5 | # id :integer not null, primary key 6 | # description :string 7 | # user_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # image_file_name :string 11 | # image_content_type :string 12 | # image_file_size :integer 13 | # image_updated_at :datetime 14 | # url :string 15 | # 16 | 17 | class Post < ApplicationRecord 18 | validates :user_id, presence: true 19 | 20 | has_attached_file :image, s3_protocol: :https, default_url: "beach.jpg", 21 | :styles => { 22 | :thumb => "50x50", 23 | :medium => "300x300", 24 | :large => "600x" 25 | } 26 | validates_attachment_content_type :image, content_type: /\Aimage\/.*\Z/ 27 | 28 | belongs_to :user 29 | has_many :likes 30 | has_many :comments 31 | 32 | def created_ago 33 | diff_in_secs =(Time.now - self.created_at).round 34 | mins = diff_in_secs / 1.minutes 35 | hours = diff_in_secs / 1.hours 36 | days = diff_in_secs / 1.days 37 | 38 | if (days > 730) 39 | "#{days / 365} year" 40 | elsif (days >= 365) 41 | "#{days / 365} years" 42 | elsif (days >= 14) 43 | "#{days / 7} weeks" 44 | elsif (days >= 7) 45 | "#{days / 7} week" 46 | elsif (days > 1) 47 | "#{days} days" 48 | elsif (days > 0) 49 | "#{days} day" 50 | else 51 | if (hours > 1) 52 | "#{hours} hours" 53 | elsif (hours > 0) 54 | "#{hours} hour" 55 | else 56 | if(mins > 1) 57 | "#{mins} minutes" 58 | else 59 | "1 minute" 60 | end 61 | end 62 | end 63 | end 64 | 65 | end 66 | -------------------------------------------------------------------------------- /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=3600' 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 | -------------------------------------------------------------------------------- /app/assets/stylesheets/font-awesome/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; 16 | -webkit-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | transform: rotate($degrees); 19 | } 20 | 21 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; 23 | -webkit-transform: scale($horiz, $vert); 24 | -ms-transform: scale($horiz, $vert); 25 | transform: scale($horiz, $vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | @mixin sr-only { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | @mixin sr-only-focusable { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/_upload_post.scss: -------------------------------------------------------------------------------- 1 | .upload-post-main { 2 | padding-top: 60px; 3 | text-align: center; 4 | } 5 | 6 | .upload-post-article { 7 | flex-grow: 1; 8 | margin: 20px auto 30px; 9 | max-width: 535px; 10 | display: flex; 11 | flex-direction: column; 12 | background-color: white; 13 | border: 1px solid #dbdbdb; 14 | border-radius: 3px; 15 | min-height: 500px; 16 | } 17 | 18 | .upload-post-preview { 19 | flex: 1; 20 | padding: 10px; 21 | } 22 | 23 | .upload-post-caption { 24 | overflow: hidden; 25 | padding: 10px; 26 | border-top: 1px solid $input-border; 27 | } 28 | 29 | .description-textarea { 30 | width: 100%; 31 | max-width: 500px; 32 | max-height: 100px; 33 | -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */ 34 | -moz-box-sizing: border-box; /* Firefox, other Gecko */ 35 | box-sizing: border-box; /* Opera/IE 8+ */ 36 | 37 | border: 1px solid $input-border; 38 | color: $gray-link; 39 | font-size: 14px; 40 | // overflow:scroll; 41 | // overflow-y:scroll; 42 | // overflow-x:hidden; 43 | } 44 | 45 | .submit-post { 46 | background: $insta-blue; 47 | border-color: $insta-blue; 48 | color: $white; 49 | font-size: 14px; 50 | padding: 4px; 51 | width: 140px; 52 | border-radius: 5px; 53 | cursor: pointer; 54 | text-align: center; 55 | margin: 10px auto 20px;; 56 | } 57 | 58 | 59 | .choose_file{ 60 | position:relative; 61 | display:inline-block; 62 | border-radius:8px; 63 | border:$light-gray solid 1px; 64 | width:90%; 65 | height: 100%; 66 | padding: 4px 6px 4px 8px; 67 | font: normal 14px Myriad Pro, Verdana, Geneva, sans-serif; 68 | color: #7f7f7f; 69 | margin-top: 2px; 70 | background:white; 71 | cursor: pointer; 72 | padding: 5px; 73 | } 74 | .choose_file input[type="file"]{ 75 | -webkit-appearance:none; 76 | position:absolute; 77 | top:0; left:0; 78 | opacity:0; 79 | cursor: pointer; 80 | width: 100%; 81 | padding: 5px 82 | } 83 | -------------------------------------------------------------------------------- /frontend/components/user_profile/user_profile.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavBar from '../navigation/nav_bar'; 3 | import UserProfileDetails from './user_profile_details'; 4 | import UserProfilePosts from './user_profile_posts'; 5 | import Spinner from '../shared/spinner'; 6 | import PostItemModalContainer from '../posts/modal/post_item_modal_container'; 7 | 8 | class UserProfile extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | componentDidMount(){ 14 | if(!this.props.posts){ 15 | this.props.fetchPosts(); 16 | } 17 | this.props.fetchUser(parseInt(this.props.params.user_id)); 18 | } 19 | 20 | componentWillReceiveProps(newProps){ 21 | if(this.props.params.user_id != newProps.params.user_id){ 22 | this.props.fetchUser(parseInt(newProps.params.user_id)); 23 | } 24 | } 25 | 26 | 27 | 28 | render() { 29 | if (!this.props.user.name) return ( 30 |
    31 | 32 |
    33 | ; 34 |
    35 |
    36 | ); 37 | 38 | return ( 39 |
    40 | 41 |
    42 |
    43 | 49 | 50 |
    51 | { 52 | this.props.posts.reverse().map(post => ( 53 | 56 | )) 57 | } 58 |
    59 |
    60 |
    61 |
    62 | ); 63 | } 64 | 65 | } 66 | 67 | 68 | export default UserProfile; 69 | -------------------------------------------------------------------------------- /frontend/components/follow/follow.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | import Spinner from '../shared/spinner'; 4 | 5 | class Follow extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = {followed: false}; 10 | this.unfollow = this.unfollow.bind(this); 11 | this.follow = this.follow.bind(this); 12 | } 13 | 14 | componentDidMount(){ 15 | if(this.isUserFollowedByCurrentUser()){ 16 | this.setState({followed: true}); 17 | }else { 18 | this.setState({followed: false}); 19 | } 20 | } 21 | 22 | componentWillReceiveProps(newProps){} 23 | 24 | 25 | handleLogout(e) { 26 | e.preventDefault(); 27 | this.props.logout().then(() => this.props.router.push('/signup')); 28 | } 29 | 30 | isUserFollowedByCurrentUser(){ 31 | let currentId = this.props.currentUser.id; 32 | let res = this.props.user.followers.find( followee => { 33 | return followee.id == currentId; 34 | }); 35 | let val = (res) ? true : false; 36 | return val; 37 | } 38 | 39 | unfollow(){ 40 | let follower_id = this.props.currentUser.id; 41 | let following_id = this.props.user.id; 42 | this.props.deleteFollow(follower_id, following_id); 43 | this.setState({followed: false}); 44 | } 45 | 46 | follow(){ 47 | let follower_id = this.props.currentUser.id; 48 | let following_id = this.props.user.id; 49 | this.props.createFollow(follower_id, following_id); 50 | this.setState({followed: true}); 51 | } 52 | 53 | renderButton(){ 54 | if (this.isUserFollowedByCurrentUser()){ 55 | return Following; 56 | }else { 57 | return Follow; 58 | } 59 | } 60 | 61 | render() { 62 | if (!this.props.user.name) return ; 63 | return ( 64 |
    65 | {this.renderButton()} 66 |
    67 | ); 68 | } 69 | 70 | } 71 | export default Follow; 72 | -------------------------------------------------------------------------------- /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 }.to_i 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 | # The code in the `on_worker_boot` will be called if you are using 36 | # clustered mode by specifying a number of `workers`. After each worker 37 | # process is booted this block will be run, if you are using `preload_app!` 38 | # option you will want to use this block to reconnect to any threads 39 | # or connections that may have been created at application boot, Ruby 40 | # cannot share connections between processes. 41 | # 42 | # on_worker_boot do 43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 44 | # end 45 | 46 | # Allow puma to be restarted by `rails restart` command. 47 | plugin :tmp_restart 48 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/_modal_post_item.scss: -------------------------------------------------------------------------------- 1 | .modal-post-holder{ 2 | display: flex; 3 | box-sizing: border-box; 4 | height: 100%; 5 | } 6 | 7 | .modal-post-image { 8 | flex-grow: 3; 9 | width: 65%; 10 | height: 100%; 11 | box-sizing: border-box; 12 | cursor: pointer; 13 | } 14 | 15 | .a{ 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | 20 | .modal-post-data { 21 | flex-grow: 1; 22 | width: 320px; 23 | position: relative; 24 | display: flex; 25 | flex-direction: column; 26 | box-sizing: border-box; 27 | } 28 | 29 | .rem-margin { 30 | margin-bottom: -20px; 31 | } 32 | 33 | .modal-close-btn { 34 | position: fixed; 35 | top: 0; 36 | right: 0; 37 | width: 20px; 38 | height: 20px; 39 | z-index: 12; 40 | font-size: 20px; 41 | color: $gray-link; 42 | } 43 | 44 | .new-header { 45 | border-bottom: 1px solid #efefef; 46 | height: 78px; 47 | margin-right: 0; 48 | padding: 20px 0; 49 | position: absolute; 50 | right: 24px; 51 | top: 0; 52 | width: 287px; 53 | } 54 | 55 | .add-com{ 56 | position: absolute; 57 | bottom: 0; 58 | } 59 | 60 | .comments-scrollable { 61 | overflow-y: scroll; 62 | height: 100%; 63 | max-height: calc(100% - 30px); 64 | } 65 | 66 | .modal-data { 67 | border-bottom: 1px solid $gray; 68 | margin-right: 0; 69 | padding: 20px 0; 70 | position: absolute; 71 | right: 24px; 72 | top: 0; 73 | width: 287px; 74 | } 75 | 76 | .modal-item-header { 77 | display: flex; 78 | justify-content: flex-start; 79 | align-items: center; 80 | } 81 | 82 | .profile-image { 83 | 84 | } 85 | 86 | .modal-ddd { 87 | bottom: 0; 88 | box-sizing: border-box; 89 | padding-left: 24px; 90 | padding-right: 24px; 91 | position: absolute; 92 | right: 0; 93 | top: 78px; 94 | width: 335px; 95 | } 96 | 97 | 98 | .single-post-footer-items { 99 | max-height: 280px; 100 | height: 280px; 101 | overflow-y: scroll; 102 | padding: 15px; 103 | border-bottom: 1px solid $light-gray; 104 | } 105 | 106 | .btm{ 107 | position: fixed; 108 | bottom: 5px; 109 | width: 290px; 110 | } 111 | 112 | .padded { 113 | padding: 15px; 114 | } 115 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | 3 | # Automatically inject JavaScript needed for LiveReload 4 | config.middleware.insert_after(ActionDispatch::Static, Rack::LiveReload) 5 | 6 | # Settings specified here will take precedence over those in config/application.rb. 7 | 8 | # In the development environment your application's code is reloaded on 9 | # every request. This slows down response time but is perfect for development 10 | # since you don't have to restart the web server when you make code changes. 11 | config.cache_classes = false 12 | 13 | # Do not eager load code on boot. 14 | config.eager_load = false 15 | 16 | # Show full error reports. 17 | config.consider_all_requests_local = true 18 | 19 | # Enable/disable caching. By default caching is disabled. 20 | if Rails.root.join('tmp/caching-dev.txt').exist? 21 | config.action_controller.perform_caching = true 22 | 23 | config.cache_store = :memory_store 24 | config.public_file_server.headers = { 25 | 'Cache-Control' => 'public, max-age=172800' 26 | } 27 | else 28 | config.action_controller.perform_caching = false 29 | 30 | config.cache_store = :null_store 31 | end 32 | 33 | # Don't care if the mailer can't send. 34 | config.action_mailer.raise_delivery_errors = false 35 | 36 | config.action_mailer.perform_caching = false 37 | 38 | # Print deprecation notices to the Rails logger. 39 | config.active_support.deprecation = :log 40 | 41 | # Raise an error on page load if there are pending migrations. 42 | config.active_record.migration_error = :page_load 43 | 44 | # Debug mode disables concatenation and preprocessing of assets. 45 | # This option may cause significant delays in view rendering with a large 46 | # number of complex assets. 47 | config.assets.debug = true 48 | 49 | # Suppress logger output for asset requests. 50 | config.assets.quiet = true 51 | 52 | # Raises error for missing translations 53 | # config.action_view.raise_on_missing_translations = true 54 | 55 | # Use an evented file watcher to asynchronously detect changes in source code, 56 | # routes, locales, etc. This feature depends on the listen gem. 57 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 58 | end 59 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/_search.scss: -------------------------------------------------------------------------------- 1 | .search-results { 2 | background: white; 3 | width: 250px; 4 | padding: 0px 0; 5 | position: absolute; 6 | top: 70px; 7 | margin: 0 auto; 8 | z-index: 111; 9 | } 10 | 11 | .search-results-list { 12 | overflow-y: scroll; 13 | max-height: 600px; 14 | border-top: 1px solid $light-gray; 15 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 16 | } 17 | 18 | 19 | .search-results-list li { 20 | // height: 50px; 21 | border-bottom: 1px solid $light-gray; 22 | padding: 5px 10px; 23 | overflow: hidden; 24 | } 25 | 26 | .search-results-list li:hover { 27 | background-color: $lightest-gray; 28 | } 29 | 30 | .search-results-list li a { 31 | // margin: 0px; 32 | // display: block; 33 | width: 100%; 34 | height: 100%; 35 | } 36 | 37 | 38 | .search-results-list-item { 39 | display: flex; 40 | justify-content: center; 41 | } 42 | 43 | .search-results-item{ 44 | display: flex; 45 | align-items: center; 46 | justify-content: center; 47 | } 48 | 49 | .search-results-item-img { 50 | flex: 1; 51 | } 52 | 53 | .search-results-item-data { 54 | display: flex; 55 | flex-direction: column; 56 | flex: 4; 57 | margin-left: 10px; 58 | } 59 | 60 | .search-results-text-username { 61 | color: $gray-link; 62 | font-weight: bold; 63 | } 64 | 65 | .search-results-text-name { 66 | font-weight: 200; 67 | color: $gray; 68 | } 69 | 70 | 71 | .search-no-results { 72 | color: $gray; 73 | display: flex; 74 | justify-content: center; 75 | align-items: center; 76 | font-weight: 450; 77 | cursor: default; 78 | height: 50px; 79 | border-bottom: 1px solid $light-gray; 80 | padding: 5px 10px; 81 | } 82 | 83 | 84 | .triangle { 85 | position: relative; 86 | width:0; 87 | border-bottom:solid 15px $light-gray; 88 | border-right:solid 15px transparent; 89 | border-left:solid 15px transparent; 90 | margin: 0 auto; 91 | } 92 | .triangle .empty { 93 | position: absolute; 94 | top:2px; 95 | left:-15px; 96 | width:0; 97 | border-bottom:solid 15px white; 98 | border-right:solid 15px transparent; 99 | border-left:solid 15px transparent; 100 | } 101 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) do |repo_name| 4 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 5 | "https://github.com/#{repo_name}.git" 6 | end 7 | 8 | 9 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 10 | gem 'rails', '~> 5.0.2' 11 | # Use postgresql as the database for Active Record 12 | gem 'pg', '~> 0.18' 13 | # Use Puma as the app server 14 | gem 'puma', '~> 3.0' 15 | # Use SCSS for stylesheets 16 | gem 'sass-rails', '~> 5.0' 17 | # Use Uglifier as compressor for JavaScript assets 18 | gem 'uglifier', '>= 1.3.0' 19 | # Use CoffeeScript for .coffee assets and views 20 | gem 'coffee-rails', '~> 4.2' 21 | # See https://github.com/rails/execjs#readme for more supported runtimes 22 | # gem 'therubyracer', platforms: :ruby 23 | 24 | # Use jquery as the JavaScript library 25 | gem 'jquery-rails' 26 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 27 | gem 'jbuilder', '~> 2.5' 28 | # Use Redis adapter to run Action Cable in production 29 | # gem 'redis', '~> 3.0' 30 | # Use ActiveModel has_secure_password 31 | gem 'bcrypt', '~> 3.1.7' 32 | 33 | 34 | gem "paperclip", '~> 5.0.0' 35 | gem 'figaro' 36 | gem 'aws-sdk', '>= 2.0' 37 | gem 'faker' 38 | 39 | # Use Capistrano for deployment 40 | # gem 'capistrano-rails', group: :development 41 | 42 | group :development, :test do 43 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 44 | gem 'byebug', platform: :mri 45 | gem 'better_errors' 46 | gem 'binding_of_caller' 47 | gem 'pry-rails' 48 | gem 'annotate' 49 | end 50 | 51 | group :development do 52 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. 53 | gem 'web-console', '>= 3.3.0' 54 | gem 'listen', '~> 3.0.5' 55 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 56 | gem 'spring' 57 | gem 'spring-watcher-listen', '~> 2.0.0' 58 | 59 | #auto sass reloading 60 | gem "guard", ">= 2.2.2", :require => false 61 | gem "guard-livereload", :require => false 62 | gem "rack-livereload" 63 | gem "rb-fsevent", :require => false 64 | end 65 | 66 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 67 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 68 | -------------------------------------------------------------------------------- /frontend/components/posts/upload_post.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router'; 3 | import NavBar from '../navigation/nav_bar'; 4 | 5 | class UploadPost extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | description: "", 10 | imageFile: null, 11 | imageUrl: null 12 | }; 13 | this.updateFile = this.updateFile.bind(this); 14 | this.updateDescription = this.updateDescription.bind(this); 15 | this.handleSubmit = this.handleSubmit.bind(this); 16 | } 17 | 18 | updateFile(e) { 19 | let file = e.currentTarget.files[0]; 20 | let fileReader = new FileReader(); 21 | fileReader.onloadend = () => this.setState({ imageFile: file, imageUrl: fileReader.result }); 22 | 23 | if (file) { 24 | fileReader.readAsDataURL(file); 25 | } 26 | } 27 | 28 | updateDescription(e) { 29 | let description = e.target.value; 30 | this.setState({ 31 | description 32 | }); 33 | } 34 | 35 | handleSubmit(e) { 36 | e.preventDefault(); 37 | let formData = new FormData(); 38 | formData.append("post[description]", this.state.description); 39 | formData.append("post[image]", this.state.imageFile); 40 | formData.append("post[url]", "test"); 41 | let id = this.props.currentUser.id; 42 | this.props.createPost(formData).then(() => this.props.router.push(`users/${id}`)); 43 | } 44 | 45 | render() { 46 | return ( 47 |
    48 | 49 |
    50 |
    51 | 52 |
    53 |
    54 |
    55 | Choose File 56 | 57 |
    58 | 59 |
    60 |
    61 | 63 |
    64 | 65 | 66 |
    67 |
    68 |
    69 | ); 70 | } 71 | 72 | } 73 | 74 | 75 | export default withRouter(UploadPost); 76 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :integer not null, primary key 6 | # username :string not null 7 | # email :string not null 8 | # password_digest :string not null 9 | # session_token :string not null 10 | # name :string 11 | # profile_photo :string 12 | # created_at :datetime 13 | # updated_at :datetime 14 | # image_file_name :string 15 | # image_content_type :string 16 | # image_file_size :integer 17 | # image_updated_at :datetime 18 | # 19 | 20 | class User < ApplicationRecord 21 | 22 | validates :username, :password_digest, :session_token, :email, presence: true 23 | validates :username, uniqueness: true 24 | validates :email, uniqueness: true 25 | validates :password, length: {minimum: 6, allow_nil: true} 26 | 27 | has_attached_file :image, s3_protocol: :https, default_url: "ghost.jpg" 28 | validates_attachment_content_type :image, content_type: /\Aimage\/.*\Z/ 29 | 30 | has_many :posts, dependent: :destroy 31 | has_many :likes 32 | has_many :comments 33 | 34 | has_many :follows_as_followee, 35 | class_name: :Follow, 36 | primary_key: :id, 37 | foreign_key: :following_id 38 | 39 | 40 | has_many :follows_as_follower, 41 | class_name: :Follow, 42 | primary_key: :id, 43 | foreign_key: :follower_id 44 | 45 | has_many :followers, 46 | through: :follows_as_followee, 47 | source: :follower 48 | 49 | has_many :followees, 50 | through: :follows_as_follower, 51 | source: :followee 52 | 53 | after_initialize :ensure_session_token 54 | 55 | attr_reader :password 56 | 57 | def self.find_by_credentials (username, password) 58 | user = User.find_by(username: username) 59 | return nil unless user 60 | user.password_is?(password) ? user : nil 61 | end 62 | 63 | def password= (password) 64 | @password = password 65 | self.password_digest = BCrypt::Password.create(password) 66 | end 67 | 68 | def password_is? (password) 69 | BCrypt::Password.new(self.password_digest).is_password?(password) 70 | end 71 | 72 | def reset_session_token! 73 | self.session_token = User.generate_session_token 74 | self.save! 75 | self.session_token 76 | end 77 | 78 | def self.generate_session_token 79 | SecureRandom.urlsafe_base64(16) 80 | end 81 | 82 | private 83 | def ensure_session_token 84 | self.session_token ||= User.generate_session_token 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /frontend/components/posts/modal/post_item_modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link, withRouter} from 'react-router'; 3 | import CommentItem from '../comment_item'; 4 | import AddCommentForm from '../add_comment_form'; 5 | import PostItemModalData from './post_item_modal_data'; 6 | 7 | import Modal from 'react-modal'; 8 | import ModalStyle from '../../../util/modal_style'; 9 | 10 | 11 | class PostItemModal extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { modalOpen: false }; 15 | this.openModal = this.openModal.bind(this); 16 | this.closeModal = this.closeModal.bind(this); 17 | this.onModalOpen = this.onModalOpen.bind(this); 18 | } 19 | 20 | openModal(){ 21 | this.setState({modalOpen: true}); 22 | } 23 | 24 | closeModal(){ 25 | this.setState({modalOpen: false}); 26 | ModalStyle.content.opacity = 0; 27 | } 28 | 29 | onModalOpen(){ 30 | ModalStyle.content.opacity = 100; 31 | } 32 | 33 | componentDidMount(){} 34 | componentWillReceiveProps(){} 35 | 36 | render(){ 37 | return( 38 |
    39 | 40 | {this.props.post.url} 41 |
    42 | {this.props.post.comments.length}   43 | {this.props.post.likes.length} 44 |
    45 | 46 | 47 | 53 | 54 | 64 | 65 |
    66 | ); 67 | } 68 | } 69 | 70 | export default withRouter(PostItemModal); 71 | -------------------------------------------------------------------------------- /app/assets/stylesheets/base/layout.scss: -------------------------------------------------------------------------------- 1 | body, button, input, textarea { 2 | font-family: -apple-system, BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; 3 | font-size:14px; 4 | } 5 | 6 | #root, html, body { 7 | height: 100%; 8 | box-sizing: border-box; 9 | } 10 | 11 | #root { 12 | height: 100%; 13 | background-color: $lightest-gray; 14 | display: flex; 15 | flex-direction: column; 16 | padding: 0px; 17 | } 18 | 19 | #root section { 20 | height: 100%; 21 | flex: 1; 22 | } 23 | 24 | footer { 25 | border: 1px solid $light-gray; 26 | background-color: $white; 27 | // margin-top: 20px; 28 | padding: 25px; 29 | } 30 | 31 | h1 { 32 | font-family: 'Billabong'; 33 | font-size: 46px; 34 | margin: 22px auto 14px; 35 | text-align: center; 36 | color: $darkest-gray; 37 | } 38 | 39 | h2 { 40 | font-size: 17px; 41 | line-height: 20px; 42 | margin: 0 40px 10px; 43 | text-align: center; 44 | color: $dark-gray; 45 | font-weight: 600; 46 | } 47 | 48 | a, button { 49 | cursor: pointer; 50 | } 51 | button { 52 | border: 2px solid $dark-gray; 53 | border-radius: 5px; 54 | } 55 | 56 | button:focus { 57 | outline:0px; 58 | } 59 | 60 | a { 61 | color: $insta-blue; 62 | text-decoration:none; 63 | } 64 | 65 | a:visited{ 66 | color: $insta-blue; 67 | text-decoration:none 68 | } 69 | a:active { 70 | opacity:.5 71 | } 72 | 73 | 74 | .img-circle { 75 | border-radius: 50%; 76 | } 77 | 78 | .bold { 79 | font-weight: 600; 80 | } 81 | 82 | .border { 83 | border: 1px solid red; 84 | } 85 | 86 | .dotted { 87 | border: 1px dotted blue; 88 | } 89 | 90 | 91 | .loader { 92 | border: 16px solid #f3f3f3; 93 | border-radius: 50%; 94 | border-top: 16px solid #3498db; 95 | width: 120px; 96 | height: 120px; 97 | -webkit-animation: spin 2s linear infinite; 98 | animation: spin 2s linear infinite; 99 | } 100 | 101 | @-webkit-keyframes spin { 102 | 0% { -webkit-transform: rotate(0deg); } 103 | 100% { -webkit-transform: rotate(360deg); } 104 | } 105 | 106 | @keyframes spin { 107 | 0% { transform: rotate(0deg); } 108 | 100% { transform: rotate(360deg); } 109 | } 110 | 111 | .spinner-pos { 112 | margin-top: 100px; 113 | display: flex; 114 | justify-content: center; 115 | } 116 | 117 | .exit-modal-button { 118 | position: absolute; 119 | top: 0; 120 | right: 0; 121 | color: #fff; 122 | font-weight: bold; 123 | margin: 15px 15px 0 0; 124 | width: 17px; 125 | z-index: 12; 126 | } 127 | 128 | .pointer { 129 | cursor: pointer; 130 | } 131 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/_main_content.scss: -------------------------------------------------------------------------------- 1 | // ------------- data-root posts page ------------- // 2 | .data-root { 3 | display: flex; 4 | flex-direction: column; 5 | flex: 1 0 auto; 6 | } 7 | 8 | .nav-bar{ 9 | background-color: $white; 10 | border-bottom: 1px solid $light-gray; 11 | display: flex; 12 | flex-direction: row; 13 | justify-content: center; 14 | } 15 | 16 | .main-nav-bar-max-width { 17 | max-width: 1010px; 18 | flex: 1; 19 | } 20 | 21 | .main-nav-bar{ 22 | background-color: $white; 23 | display: flex; 24 | align-items: center; 25 | justify-content: space-between; 26 | padding: 23px; 27 | } 28 | 29 | .main-nav-bar a { 30 | color: $darkest-gray; 31 | } 32 | 33 | .main-nav-logo { 34 | font-family: "Billabong"; 35 | font-size: 33px; 36 | min-width: 160px; 37 | display: flex; 38 | color: $darkest-gray; 39 | } 40 | .search { 41 | // min-width: 80px; 42 | flex: 1; 43 | align-self: center; 44 | display: flex; 45 | justify-content: center; 46 | margin-bottom: -2px; 47 | } 48 | .main-nav-links { 49 | min-width: 140px; 50 | display: flex; 51 | justify-content: flex-end; 52 | align-items: center; 53 | padding-left: 0px; 54 | font-size: 22px; 55 | font-weight: 100; 56 | } 57 | 58 | .main-nav-links a { 59 | color: $dark-gray; 60 | text-decoration: none; 61 | } 62 | 63 | .main-nav-explore{ 64 | margin-left: 20px; 65 | } 66 | .main-nav-user{ 67 | margin-left: 20px; 68 | } 69 | .upload-photo{ 70 | margin-left: 20px; 71 | } 72 | 73 | .main-nav-logout { 74 | margin-left: 20px; 75 | } 76 | 77 | .main-content{ 78 | // border: 1px solid red; 79 | background-color: $lightest-gray; 80 | flex: 1; 81 | display: flex; 82 | flex-direction: column; 83 | } 84 | 85 | .main-nav-logo-text { 86 | border-left: 1px solid $gray; 87 | padding-left: 10px; 88 | margin-left: 10px; 89 | } 90 | 91 | .max{ 92 | max-width: 1010px; 93 | flex: 1; 94 | } 95 | 96 | .search-form input[type=text]:focus { 97 | // width: 100%; 98 | background-color: $white; 99 | } 100 | 101 | .search-form { 102 | height: 30px; 103 | } 104 | 105 | .search-form > input { 106 | background-image: image-url('search.png'); 107 | background-size: 17px 17px; 108 | background-position:5px 3px; 109 | background-repeat: no-repeat; 110 | 111 | background-color: $lightest-gray; 112 | border: solid 1px #dbdbdb; 113 | border-radius: 3px; 114 | color: $gray-link; 115 | font-size: 14px; 116 | outline: none; 117 | padding: 3px 10px 3px 26px; 118 | z-index: 2; 119 | height: 19px; 120 | } 121 | -------------------------------------------------------------------------------- /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: 20170428020005) 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.string "body" 20 | t.integer "user_id" 21 | t.integer "post_id" 22 | t.datetime "created_at", null: false 23 | t.datetime "updated_at", null: false 24 | end 25 | 26 | create_table "follows", force: :cascade do |t| 27 | t.integer "follower_id" 28 | t.integer "following_id" 29 | t.datetime "created_at", null: false 30 | t.datetime "updated_at", null: false 31 | end 32 | 33 | create_table "likes", force: :cascade do |t| 34 | t.integer "user_id" 35 | t.integer "post_id" 36 | t.datetime "created_at", null: false 37 | t.datetime "updated_at", null: false 38 | end 39 | 40 | create_table "posts", force: :cascade do |t| 41 | t.string "description" 42 | t.integer "user_id", null: false 43 | t.datetime "created_at", null: false 44 | t.datetime "updated_at", null: false 45 | t.string "image_file_name" 46 | t.string "image_content_type" 47 | t.integer "image_file_size" 48 | t.datetime "image_updated_at" 49 | t.string "url" 50 | end 51 | 52 | create_table "users", force: :cascade do |t| 53 | t.string "username", null: false 54 | t.string "email", null: false 55 | t.string "password_digest", null: false 56 | t.string "session_token", null: false 57 | t.string "name" 58 | t.string "profile_photo" 59 | t.datetime "created_at" 60 | t.datetime "updated_at" 61 | t.string "image_file_name" 62 | t.string "image_content_type" 63 | t.integer "image_file_size" 64 | t.datetime "image_updated_at" 65 | t.string "bio" 66 | t.string "website" 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/_landing_page.scss: -------------------------------------------------------------------------------- 1 | .landing-page-main{ 2 | box-sizing: border-box; 3 | margin: 30px auto 0; 4 | display: flex; 5 | justify-content: center; 6 | flex-direction: row; 7 | align-items: center; 8 | width: 100%; 9 | // flex:1; 10 | flex: 1 0 auto; 11 | } 12 | 13 | .landing-page-img { 14 | align-self: center; 15 | width: 454px; 16 | height: 618px; 17 | background-color: $lightest-gray; 18 | background-image: image-url('homepage.png'); 19 | background-repeat: no-repeat; 20 | margin: 0 10px; 21 | } 22 | 23 | .landing-page-intro{ 24 | margin-top: 20px; 25 | min-width: 300px; 26 | max-width: 350px; 27 | flex-grow: 1; 28 | justify-content: center; 29 | } 30 | 31 | .landing-intro-nav{ 32 | background-color: $white; 33 | border: 1px solid $light-gray; 34 | border-radius: 1px; 35 | color: $darkest-gray; 36 | font-size: 14px; 37 | margin: 10px 0 10px; 38 | padding: 25px; 39 | text-align: center; 40 | } 41 | 42 | .login-box { 43 | background-color: $white; 44 | border: 1px solid $light-gray; 45 | border-radius: 1px; 46 | margin: 0 0 10px; 47 | padding: 25px; 48 | flex: 1; 49 | } 50 | 51 | .login-box-logo { 52 | border: 0px solid black; 53 | } 54 | 55 | .login-box-form { 56 | display: flex; 57 | flex-direction: column; 58 | /*justify-content: center;*/ 59 | } 60 | 61 | .login-box-form input{ 62 | background-color: white; 63 | border: 1px solid $input-border; 64 | border-radius: 3px; 65 | box-sizing: border-box; 66 | color: $darkest-gray; 67 | font-size: 14px; 68 | padding: 9px 8px 7px; 69 | -webkit-appearance: none; 70 | width: 100%; 71 | } 72 | 73 | li { 74 | list-style: none; 75 | } 76 | 77 | .demo-login { 78 | background: $insta-blue; 79 | border-color: $insta-blue; 80 | color: $white; 81 | font-size: 14px; 82 | padding: 8px; 83 | } 84 | 85 | .errors{ 86 | padding: 0px; 87 | margin-top: 20px; 88 | color: $red; 89 | } 90 | 91 | .or-separator { 92 | text-align: center; 93 | color: $gray; 94 | font-weight: 500; 95 | display: flex; 96 | } 97 | 98 | .or-separator-line { 99 | -webkit-box-flex: 1; 100 | -ms-flex-positive: 1; 101 | flex-grow: 1; 102 | -ms-flex-negative: 1; 103 | flex-shrink: 1; 104 | background-color: $separator-gray; 105 | height: 1px; 106 | position: relative; 107 | top: .45em; 108 | } 109 | 110 | .or-separator-text { 111 | -webkit-box-flex: 0; 112 | -ms-flex-positive: 0; 113 | flex-grow: 0; 114 | -ms-flex-negative: 0; 115 | flex-shrink: 0; 116 | text-transform: uppercase; 117 | font-size: 13px; 118 | line-height: 15px; 119 | margin: 0 18px; 120 | } 121 | -------------------------------------------------------------------------------- /frontend/components/search/search.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | import SearchResultItem from './search_result_item' 4 | 5 | class Search extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | query: "", 11 | active: false 12 | }; 13 | 14 | this.handleInput = this.handleInput.bind(this); 15 | this.closeResultsList = this.closeResultsList.bind(this); 16 | this.handleClick = this.handleClick.bind(this); 17 | } 18 | 19 | handleClick(e) { 20 | if (this.state.query) { 21 | this.setState({ active: false, query: ""}); 22 | } 23 | } 24 | 25 | componentDidMount() { 26 | window.addEventListener("click", this.handleClick); 27 | } 28 | 29 | componentWillUnmount() { 30 | window.removeEventListener("click", this.handleClick); 31 | } 32 | 33 | componentWillReceiveProps(nextProps) { 34 | if (this.props.user !== nextProps.user) { 35 | this.setState({ query: '' }); 36 | } 37 | } 38 | 39 | handleInput(e) { 40 | e.preventDefault(); 41 | let query = e.target.value; 42 | if(query === "") { 43 | this.props.removeSearchResults(); 44 | this.setState({query: "", active: false}); 45 | }else { 46 | this.setState({ 47 | query: query, 48 | active: true }, 49 | () => this.props.fetchSearchResults(this.state.query)); 50 | } 51 | } 52 | 53 | closeResultsList(){ 54 | this.setState({query: "", active: false}); 55 | } 56 | 57 | renderResults(){ 58 | if(this.props.searchResults.length > 0) { 59 | return( 60 | this.props.searchResults.map((user) => 61 | ) 62 | ) 63 | }else { 64 | return
    No results found.
    65 | } 66 | } 67 | 68 | render() { 69 | let resultsList = "" 70 | if( this.state.active) { 71 | resultsList = 72 |
    73 |
    74 |
    75 |
    76 | 77 |
      78 | {this.renderResults()} 79 |
    80 | 81 |
    ; 82 | } 83 | 84 | return ( 85 |
    86 |
    87 | 89 |
    90 | 91 | {resultsList} 92 |
    93 | ); 94 | } 95 | 96 | } 97 | 98 | export default withRouter(Search); 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Travelgram 2 | 3 | [Travelgram][heroku] 4 | 5 | [heroku]: https://insta-travelgram.herokuapp.com/#/ 6 | 7 | Travelgram is a full-stack web application inspired by Instagram. It utilizes Ruby on Rails on the backend, a PostgreSQL database, and React.js with a Redux architectural framework on the frontend. 8 | 9 | ## Features & Implementation 10 | 11 | ### Secure User Authentication 12 | 13 | Users can sign up and log in with validations in Rails' models to ensure that only valid data is saved to the database. Only registered users can access application features. Passwords are hashed using BCrypt. 14 | 15 | ### Posts Feed 16 | Posts are stored in the database table, with columns for `id`, `user_id` that references the person who uploaded the post, `image` which is an attached file uploaded to AWS (Amazon Web Services). There is an optional column of `description` of a post. Once logged in, user is redirected to posts feed page and an AJAX call is made to the database and posts of the users that current user is following are fetched. 17 | 18 | Each post on posts feed page has owner's profile picture, username, image, likes and comments associated to the post 19 | 20 | Posts are created through update post form, where user can choose image and add a description to a post. 21 | As a post requires a `user_id` column to be successfully saved, posts are always created through current user. 22 | 23 | ![posts-feed](./app/assets/images/readme/posts-feed.png) 24 | ### Users 25 | Users are stored in the database in a `users` table. User's show page displays their details and posts. Users can edit their own data and change profile photo. 26 | 27 | ![posts-feed](./app/assets/images/readme/user-profile.png) 28 | 29 | ### Search 30 | 31 | As the user types in the search bar, a list of users matching the input are fetched from the database through an AJAX call. 32 | 33 | ![search](./app/assets/images/readme/search.png) 34 | 35 | ### Likes and Comments 36 | Users can like and unlike any post by clicking on the heart icon which color indicates action. Same action can be done with double click on the posts image. 37 | User can post a comment by clicking on a cloud icon which gives focus to an input field and when user start typing comment can be submitted. 38 | On the each post on the posts feed page, only last three comments are displayed by default. User can choose to see more comments by clicking on text `load more comments` which every time displays five more latest comments. 39 | 40 | ### Follows 41 | 42 | User can follow and unfollow any other user. 43 | 44 | ![follows](./app/assets/images/readme/follows.png) 45 | 46 | ## Future Directions for the Project 47 | 48 | In addition to the features already implemented, I plan to continue work on this project. The next steps for Travelgram are outlined below. 49 | * [ ] Direct Messaging 50 | * [ ] Hashtags 51 | * [ ] Full responsiveness (optimization for mobile devices) 52 | * [ ] Infinite scroll 53 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/_edit_profile_page.scss: -------------------------------------------------------------------------------- 1 | .edit-profile-main { 2 | padding-top: 60px; 3 | } 4 | 5 | .edit-profile-article { 6 | flex-grow: 1; 7 | margin: 0 auto 30px; 8 | max-width: 935px; 9 | display: flex; 10 | background-color: white; 11 | border: 1px solid #dbdbdb; 12 | border-radius: 3px; 13 | min-height: 400px; 14 | } 15 | 16 | .edit-profile-navigation { 17 | flex-grow: 1; 18 | border-right: 1px solid #dbdbdb; 19 | 20 | } 21 | 22 | .edit-profile-navigation-item{ 23 | font-size: 16px; 24 | font-weight: bold; 25 | padding: 20px 10px; 26 | border-left: 3px solid $darkest-gray; 27 | } 28 | 29 | .edit-profile-form-header { 30 | margin: 32px 0 16px 0; 31 | display: flex; 32 | align-items: center; 33 | } 34 | 35 | .edit-profile-header-img { 36 | flex: 1; 37 | text-align: right; 38 | } 39 | 40 | .edit-profile-header-img img { 41 | border-radius: 50%; 42 | } 43 | 44 | .edit-profile-header-name { 45 | flex: 3; 46 | text-align: left; 47 | margin-left: 30px; 48 | font-size: 24px; 49 | font-weight: 600; 50 | } 51 | 52 | .edit-profile-data { 53 | flex-grow: 3; 54 | padding: 10px; 55 | display: flex; 56 | flex-direction: column; 57 | font-size: 16px; 58 | font-weight: bold; 59 | } 60 | 61 | .edit-profile-form { 62 | display: flex; 63 | flex-direction: column; 64 | margin: 32px 0 16px 0; 65 | } 66 | 67 | .edit-profile-data-row{ 68 | display: flex; 69 | align-items: center; 70 | margin-bottom: 16px; 71 | 72 | } 73 | 74 | .edit-profile-field-title { 75 | flex: 1; 76 | text-align: right; 77 | margin-right: 30px; 78 | min-width: 100px; 79 | } 80 | 81 | .edit-profile-field-input-holder { 82 | flex: 3; 83 | } 84 | 85 | .edit-profile-field-input-holder > * { 86 | width: 70%; 87 | min-width: 150px; 88 | font-size: 18px; 89 | font-weight: 500; 90 | color: $gray-link; 91 | border-radius: 2px; 92 | border: 1px solid #e6e6e6; 93 | background-color: #ffffff; 94 | padding: 10px; 95 | } 96 | 97 | .edit-bio-textarea { 98 | max-width: 500px; 99 | border: 1px solid $input-border; 100 | } 101 | 102 | .edit-user-submit { 103 | background: $insta-blue; 104 | border-color: $insta-blue; 105 | color: $white; 106 | font-size: 16px; 107 | padding: 8px; 108 | width: 30%; 109 | border-radius: 5px; 110 | margin-top: 20px; 111 | cursor: pointer; 112 | } 113 | 114 | .profile-user-follow-btn { 115 | background: $insta-blue; 116 | color: $white; 117 | padding: 4px 24px; 118 | margin-left: 20px; 119 | border-radius: 3px; 120 | border: 1px solid $insta-blue; 121 | font-size: 14px; 122 | font-weight: 600; 123 | } 124 | 125 | .input-file { 126 | position: absolute; 127 | bottom: 0; 128 | right: 0; 129 | width: 90px; 130 | height: 90px; 131 | cursor: pointer; 132 | opacity: 0; 133 | // background-color: green; 134 | } 135 | 136 | .edit-profile { 137 | overflow: hidden; 138 | // background-color: red; 139 | position: relative; 140 | } 141 | --------------------------------------------------------------------------------