├── log └── .keep ├── tmp └── .keep ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── public ├── favicon.ico ├── apple-touch-icon.png ├── apple-touch-icon-precomposed.png ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── event_test.rb │ ├── group_test.rb │ ├── rsvp_test.rb │ ├── user_test.rb │ ├── category_test.rb │ ├── membership_test.rb │ └── category_group_test.rb ├── controllers │ ├── .keep │ └── api │ │ ├── events_controller_test.rb │ │ ├── groups_controller_test.rb │ │ ├── rsvps_controller_test.rb │ │ ├── users_controller_test.rb │ │ ├── sessions_controller_test.rb │ │ ├── categories_controller_test.rb │ │ └── memberships_controller_test.rb ├── fixtures │ ├── .keep │ ├── files │ │ └── .keep │ ├── categories.yml │ ├── rsvps.yml │ ├── memberships.yml │ ├── category_groups.yml │ ├── events.yml │ ├── groups.yml │ └── users.yml ├── integration │ └── .keep └── test_helper.rb ├── app ├── assets │ ├── images │ │ ├── .keep │ │ ├── favicon.ico │ │ ├── favicon2.ico │ │ ├── favicon3.ico │ │ ├── favicon4.ico │ │ ├── letsmeetcover.jpg │ │ ├── search_page_sample.png │ │ └── group_show_page_sample.png │ ├── javascripts │ │ ├── channels │ │ │ └── .keep │ │ ├── api │ │ │ ├── events.coffee │ │ │ ├── groups.coffee │ │ │ ├── rsvps.coffee │ │ │ ├── users.coffee │ │ │ ├── categories.coffee │ │ │ ├── memberships.coffee │ │ │ └── sessions.coffee │ │ ├── cable.js │ │ └── application.js │ ├── config │ │ └── manifest.js │ └── stylesheets │ │ ├── base │ │ ├── colors.scss │ │ ├── fonts.scss │ │ ├── grid.scss │ │ ├── reset.scss │ │ └── layouts.scss │ │ ├── api │ │ ├── rsvps.scss │ │ ├── users.scss │ │ ├── categories.scss │ │ ├── memberships.scss │ │ ├── groups.scss │ │ ├── sessions.scss │ │ └── events.scss │ │ ├── components │ │ ├── footer.scss │ │ ├── welcome.scss │ │ ├── search.scss │ │ └── group_show.scss │ │ └── application.scss ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ ├── category_group.rb │ ├── category.rb │ ├── rsvp.rb │ ├── membership.rb │ ├── event.rb │ ├── group.rb │ └── user.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── static_pages_controller.rb │ ├── api │ │ ├── categories_controller.rb │ │ ├── users_controller.rb │ │ ├── sessions_controller.rb │ │ ├── memberships_controller.rb │ │ ├── rsvps_controller.rb │ │ ├── events_controller.rb │ │ └── groups_controller.rb │ └── application_controller.rb ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── mailer.html.erb │ │ └── application.html.erb │ ├── api │ │ ├── users │ │ │ ├── show.json.jbuilder │ │ │ └── _user.json.jbuilder │ │ ├── events │ │ │ ├── show.json.jbuilder │ │ │ ├── index.json.jbuilder │ │ │ ├── search.json.jbuilder │ │ │ └── _event.json.jbuilder │ │ ├── groups │ │ │ ├── show.json.jbuilder │ │ │ ├── index.json.jbuilder │ │ │ ├── search.json.jbuilder │ │ │ └── _group.json.jbuilder │ │ └── memberships │ │ │ └── _membership.json.jbuilder │ └── static_pages │ │ └── root.html.erb ├── helpers │ ├── api │ │ ├── rsvps_helper.rb │ │ ├── users_helper.rb │ │ ├── events_helper.rb │ │ ├── groups_helper.rb │ │ ├── sessions_helper.rb │ │ ├── categories_helper.rb │ │ └── memberships_helper.rb │ └── application_helper.rb ├── jobs │ └── application_job.rb ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb └── mailers │ └── application_mailer.rb ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── frontend ├── components │ ├── groups │ │ ├── groups_index.jsx │ │ ├── groups_index_container.js │ │ ├── group_form_container.js │ │ ├── edit_group_container.js │ │ ├── group_show_container.js │ │ ├── group_members.jsx │ │ ├── group_side_bar.jsx │ │ ├── group_nav_bar.jsx │ │ ├── group_show.jsx │ │ ├── edit_group_form.jsx │ │ └── group_form.jsx │ ├── app.jsx │ ├── footer │ │ ├── footer_container.js │ │ └── footer.jsx │ ├── nav_bar │ │ ├── nav_bar_container.js │ │ └── nav_bar.jsx │ ├── search │ │ ├── category_search_results.jsx │ │ ├── search_bar_container.jsx │ │ ├── group_search_results.jsx │ │ ├── search_bar.js │ │ └── event_search_results.jsx │ ├── events │ │ ├── event_form_container.js │ │ ├── event_rsvp_container.js │ │ ├── event_rsvp.jsx │ │ ├── edit_event_container.js │ │ ├── event_show_container.js │ │ ├── event_sidebar.jsx │ │ ├── edit_event_form.jsx │ │ └── event_show.jsx │ ├── session_form │ │ ├── session_form_container.js │ │ └── session_form.jsx │ ├── welcome_page │ │ ├── welcome_page.jsx │ │ └── home_page.jsx │ └── root.jsx ├── actions │ ├── error_actions.js │ ├── session_actions.js │ ├── event_actions.js │ └── group_actions.js ├── store │ └── store.js ├── util │ ├── session_api_util.js │ ├── event_api_util.js │ └── group_api_util.js ├── reducers │ ├── root_reducer.js │ ├── errors_reducer.js │ ├── selectors.js │ ├── session_reducer.js │ ├── events_reducer.js │ └── groups_reducer.js └── lets_meet.jsx ├── docs ├── wireframes │ ├── login.png │ ├── footer.png │ ├── header.png │ ├── signup.png │ ├── welcome.png │ ├── homepage.png │ ├── search_bar.png │ ├── create_event.png │ ├── create_group.png │ ├── user_profile.png │ ├── event_show_page.png │ ├── group_show_page.png │ ├── footer_when_logged_in.png │ └── header_when_logged_in.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 │ └── production.rb ├── puma.rb └── database.yml ├── config.ru ├── db ├── migrate │ ├── 20170427170214_add_column_to_groups.rb │ ├── 20170428021154_add_pic_column_to_users_table.rb │ ├── 20170423010254_rename_category_id_to_category.rb │ ├── 20170423011050_change_category_format_in_groups.rb │ ├── 20170424161911_add_organizer_column_to_events_table.rb │ ├── 20170424162301_add_constraint_to_events_table.rb │ ├── 20170421143423_create_categories.rb │ ├── 20170427124505_add_attachment_image_to_users.rb │ ├── 20170511030806_create_category_groups.rb │ ├── 20170425152102_change_event_table_columns.rb │ ├── 20170425003201_create_rsvps.rb │ ├── 20170423031250_create_memberships.rb │ ├── 20170424160906_create_events.rb │ ├── 20170421131305_create_groups.rb │ └── 20170418160540_create_users.rb └── schema.rb ├── Rakefile ├── webpack.config.js ├── .gitignore ├── package.json ├── 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 | -------------------------------------------------------------------------------- /frontend/components/groups/groups_index.jsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /frontend/components/groups/groups_index_container.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/helpers/api/rsvps_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::RsvpsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/users_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/events_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::EventsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/groups_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::GroupsHelper 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/api/categories_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::CategoriesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/memberships_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::MembershipsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/views/api/users/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "api/users/user", user: @user 2 | -------------------------------------------------------------------------------- /app/views/api/events/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "api/events/event", event: @event 2 | -------------------------------------------------------------------------------- /app/views/api/groups/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "api/groups/group", group: @group 2 | -------------------------------------------------------------------------------- /docs/wireframes/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/docs/wireframes/login.png -------------------------------------------------------------------------------- /app/views/api/memberships/_membership.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! membership :id, :member_id, :group_id 2 | -------------------------------------------------------------------------------- /app/views/api/users/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! user, :id, :email, :username, :profile_pic_url 2 | -------------------------------------------------------------------------------- /docs/wireframes/footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/docs/wireframes/footer.png -------------------------------------------------------------------------------- /docs/wireframes/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/docs/wireframes/header.png -------------------------------------------------------------------------------- /docs/wireframes/signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/docs/wireframes/signup.png -------------------------------------------------------------------------------- /docs/wireframes/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/docs/wireframes/welcome.png -------------------------------------------------------------------------------- /app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /app/assets/images/favicon2.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/app/assets/images/favicon2.ico -------------------------------------------------------------------------------- /app/assets/images/favicon3.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/app/assets/images/favicon3.ico -------------------------------------------------------------------------------- /app/assets/images/favicon4.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/app/assets/images/favicon4.ico -------------------------------------------------------------------------------- /docs/wireframes/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/docs/wireframes/homepage.png -------------------------------------------------------------------------------- /docs/wireframes/search_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/docs/wireframes/search_bar.png -------------------------------------------------------------------------------- /docs/wireframes/create_event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/docs/wireframes/create_event.png -------------------------------------------------------------------------------- /docs/wireframes/create_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/docs/wireframes/create_group.png -------------------------------------------------------------------------------- /docs/wireframes/user_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/docs/wireframes/user_profile.png -------------------------------------------------------------------------------- /app/assets/images/letsmeetcover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/app/assets/images/letsmeetcover.jpg -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /docs/wireframes/event_show_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/docs/wireframes/event_show_page.png -------------------------------------------------------------------------------- /docs/wireframes/group_show_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/docs/wireframes/group_show_page.png -------------------------------------------------------------------------------- /app/assets/images/search_page_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/app/assets/images/search_page_sample.png -------------------------------------------------------------------------------- /app/controllers/static_pages_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticPagesController < ApplicationController 2 | def root 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /docs/wireframes/footer_when_logged_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/docs/wireframes/footer_when_logged_in.png -------------------------------------------------------------------------------- /docs/wireframes/header_when_logged_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/docs/wireframes/header_when_logged_in.png -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /app/assets/images/group_show_page_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RonKew28/LetsMeet/HEAD/app/assets/images/group_show_page_sample.png -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /app/controllers/api/categories_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::CategoriesController < ApplicationController 2 | 3 | # def create 4 | # 5 | # end 6 | end 7 | -------------------------------------------------------------------------------- /app/views/api/events/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @events.each do |event| 2 | json.set! event.id do 3 | json.partial! 'event', event: event 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/api/groups/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @groups.each do |group| 2 | json.set! group.id do 3 | json.partial! 'group', group: group 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /app/views/api/events/search.json.jbuilder: -------------------------------------------------------------------------------- 1 | @events.each do |event| 2 | json.set! event.id do 3 | json.partial! 'event', event: event 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/api/groups/search.json.jbuilder: -------------------------------------------------------------------------------- 1 | @groups.each do |group| 2 | json.set! group.id do 3 | json.partial! 'group', group: group 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /test/models/event_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class EventTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/group_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class GroupTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/rsvp_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RsvpTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/models/category_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CategoryTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/membership_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MembershipTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /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: '_LetsMeet_session' 4 | -------------------------------------------------------------------------------- /app/models/category_group.rb: -------------------------------------------------------------------------------- 1 | class CategoryGroup < ApplicationRecord 2 | validates :group, :category, presence: true 3 | 4 | belongs_to :group 5 | 6 | belongs_to :category 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20170427170214_add_column_to_groups.rb: -------------------------------------------------------------------------------- 1 | class AddColumnToGroups < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :groups, :image_url, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/categories.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | title: MyString 5 | 6 | two: 7 | title: MyString 8 | -------------------------------------------------------------------------------- /test/models/category_group_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CategoryGroupTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /app/assets/stylesheets/base/colors.scss: -------------------------------------------------------------------------------- 1 | $main-red: #ed1c40; 2 | $red-text: #e51937; 3 | $main-red-hover: #d01031; 4 | $lighter-red: #dd1b16; 5 | $off-white: #F0EFEF; 6 | $blue: #1F24CC; 7 | $off-grey: #B6B6B6; 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/base/fonts.scss: -------------------------------------------------------------------------------- 1 | $serif: georgia,"times new roman",times,serif; 2 | $sans-serif: 'Roboto', sans-serif; 3 | $labels-sans-serif: 'Arial', sans-serif; 4 | $logo-font: 'Pacifico', cursive; 5 | -------------------------------------------------------------------------------- /db/migrate/20170428021154_add_pic_column_to_users_table.rb: -------------------------------------------------------------------------------- 1 | class AddPicColumnToUsersTable < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :profile_pic_url, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170423010254_rename_category_id_to_category.rb: -------------------------------------------------------------------------------- 1 | class RenameCategoryIdToCategory < ActiveRecord::Migration[5.0] 2 | def change 3 | rename_column :groups, :category_id, :category 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170423011050_change_category_format_in_groups.rb: -------------------------------------------------------------------------------- 1 | class ChangeCategoryFormatInGroups < ActiveRecord::Migration[5.0] 2 | def change 3 | change_column :groups, :category, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/controllers/api/events_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::EventsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/groups_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::GroupsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/rsvps_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::RsvpsControllerTest < 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /db/migrate/20170424161911_add_organizer_column_to_events_table.rb: -------------------------------------------------------------------------------- 1 | class AddOrganizerColumnToEventsTable < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :events, :organizer_id, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/controllers/api/categories_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::CategoriesControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/memberships_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::MembershipsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/rsvps.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/Rsvps 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/20170424162301_add_constraint_to_events_table.rb: -------------------------------------------------------------------------------- 1 | class AddConstraintToEventsTable < ActiveRecord::Migration[5.0] 2 | def change 3 | change_column :events, :organizer_id, :integer, :null => false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/rsvps.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | attendee_id: 1 5 | event_id: 1 6 | 7 | two: 8 | attendee_id: 1 9 | event_id: 1 10 | -------------------------------------------------------------------------------- /app/models/category.rb: -------------------------------------------------------------------------------- 1 | class Category < ApplicationRecord 2 | validates :title, presence: true 3 | 4 | has_many :category_groups 5 | 6 | has_many :groups, 7 | through: :category_groups, 8 | source: :group 9 | end 10 | -------------------------------------------------------------------------------- /test/fixtures/memberships.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | member_id: 1 5 | group_id: 1 6 | 7 | two: 8 | member_id: 1 9 | group_id: 1 10 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/categories.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/Categories 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/memberships.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/Memberships 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 | -------------------------------------------------------------------------------- /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/fixtures/category_groups.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | category_id: 1 5 | group_id: 1 6 | 7 | two: 8 | category_id: 1 9 | group_id: 1 10 | -------------------------------------------------------------------------------- /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/events.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/groups.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/rsvps.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/api/categories.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/memberships.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 | -------------------------------------------------------------------------------- /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/views/static_pages/root.html.erb: -------------------------------------------------------------------------------- 1 | 7 |
8 | -------------------------------------------------------------------------------- /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/20170421143423_create_categories.rb: -------------------------------------------------------------------------------- 1 | class CreateCategories < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :categories do |t| 4 | t.string :title, null: false 5 | 6 | t.timestamps 7 | end 8 | 9 | add_index :categories, :title 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/20170427124505_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 | -------------------------------------------------------------------------------- /frontend/actions/error_actions.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_ERRORS = "RECEIVE_ERRORS"; 2 | export const CLEAR_ERRORS = "CLEAR_ERRORS"; 3 | 4 | export const receiveErrors = errors => ({ 5 | type: RECEIVE_ERRORS, 6 | errors 7 | }); 8 | 9 | export const clearErrors = () => ({ 10 | type: CLEAR_ERRORS 11 | }); 12 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /db/migrate/20170511030806_create_category_groups.rb: -------------------------------------------------------------------------------- 1 | class CreateCategoryGroups < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :category_groups do |t| 4 | t.integer :category_id, null: false 5 | t.integer :group_id, null: false 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/models/rsvp.rb: -------------------------------------------------------------------------------- 1 | class Rsvp < ApplicationRecord 2 | validates :attendee, :event, presence: true 3 | validates :attendee_id, uniqueness: { scope: :event_id } 4 | 5 | belongs_to :attendee, 6 | class_name: :User, 7 | primary_key: :id, 8 | foreign_key: :attendee_id 9 | 10 | belongs_to :event 11 | end 12 | -------------------------------------------------------------------------------- /app/models/membership.rb: -------------------------------------------------------------------------------- 1 | class Membership < ApplicationRecord 2 | 3 | validates :member_id, uniqueness: { scope: :group_id } 4 | validates :member_id, :group_id, presence: true 5 | 6 | belongs_to :member, 7 | class_name: :User, 8 | primary_key: :id, 9 | foreign_key: :member_id 10 | 11 | belongs_to :group 12 | 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20170425152102_change_event_table_columns.rb: -------------------------------------------------------------------------------- 1 | class ChangeEventTableColumns < ActiveRecord::Migration[5.0] 2 | def change 3 | change_column :events, :date, :date 4 | rename_column :events, :location, :location_name 5 | add_column :events, :time, :time 6 | add_column :events, :location_address, :string 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/views/api/groups/_group.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! group, :id, :name, :description, :location, :category, :founded_date, :creator_id, :creator, :memberships, :members, :image_url, :events 2 | 3 | 4 | json.member_count group.members.length 5 | json.formatted_date group.founded_date.strftime("%B %d, %Y") 6 | json.event_count group.events.length 7 | -------------------------------------------------------------------------------- /db/migrate/20170425003201_create_rsvps.rb: -------------------------------------------------------------------------------- 1 | class CreateRsvps < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :rsvps do |t| 4 | t.integer :attendee_id, null: false 5 | t.integer :event_id, null: false 6 | 7 | t.timestamps 8 | end 9 | add_index :rsvps, [:attendee_id, :event_id], unique: true 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 | -------------------------------------------------------------------------------- /db/migrate/20170423031250_create_memberships.rb: -------------------------------------------------------------------------------- 1 | class CreateMemberships < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :memberships do |t| 4 | t.integer :member_id, null: false 5 | t.integer :group_id, null: false 6 | 7 | t.timestamps 8 | end 9 | 10 | add_index :memberships, [:group_id, :member_id], unique: true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/fixtures/events.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | name: MyString 5 | description: MyText 6 | group_id: 1 7 | location: MyString 8 | date: 2017-04-24 12:09:06 9 | 10 | two: 11 | name: MyString 12 | description: MyText 13 | group_id: 1 14 | location: MyString 15 | date: 2017-04-24 12:09:06 16 | -------------------------------------------------------------------------------- /frontend/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import RootReducer from '../reducers/root_reducer'; 3 | import thunk from 'redux-thunk'; 4 | import logger from 'redux-logger' 5 | 6 | const configureStore = (preloadedState = {}) => ( 7 | createStore( 8 | RootReducer, 9 | preloadedState, 10 | applyMiddleware(thunk, logger) 11 | ) 12 | ) 13 | 14 | export default configureStore; 15 | -------------------------------------------------------------------------------- /frontend/components/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import NavBarContainer from './nav_bar/nav_bar_container'; 4 | import FooterContainer from './footer/footer_container'; 5 | 6 | const App = ({ children }) => ( 7 |
8 |
9 | 10 | { children } 11 | 12 |
13 |
14 | ); 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/util/session_api_util.js: -------------------------------------------------------------------------------- 1 | 2 | export const login = (user) => { 3 | return $.ajax({ 4 | method: 'POST', 5 | url: '/api/session', 6 | data: { user } 7 | }); 8 | }; 9 | export const signup = (user) => { 10 | return $.ajax({ 11 | method: 'POST', 12 | url: '/api/users', 13 | data: { user } 14 | }); 15 | }; 16 | 17 | export const logout = () => { 18 | return $.ajax({ 19 | method: 'DELETE', 20 | url: '/api/session' 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/reducers/root_reducer.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | import SessionReducer from './session_reducer'; 3 | import ErrorsReducer from './errors_reducer'; 4 | import GroupsReducer from './groups_reducer'; 5 | import EventsReducer from './events_reducer'; 6 | 7 | const RootReducer = combineReducers({ 8 | session: SessionReducer, 9 | groups: GroupsReducer, 10 | events: EventsReducer, 11 | errors: ErrorsReducer 12 | }); 13 | 14 | export default RootReducer; 15 | -------------------------------------------------------------------------------- /frontend/reducers/errors_reducer.js: -------------------------------------------------------------------------------- 1 | import { CLEAR_ERRORS, RECEIVE_ERRORS } from '../actions/error_actions'; 2 | 3 | const _errors = {}; 4 | 5 | const ErrorsReducer = (state = _errors, action) => { 6 | Object.freeze(state); 7 | switch(action.type) { 8 | case RECEIVE_ERRORS: 9 | return Object.assign({}, action.errors.responseJSON); 10 | case CLEAR_ERRORS: 11 | return {}; 12 | default: 13 | return state; 14 | } 15 | }; 16 | 17 | export default ErrorsReducer; 18 | -------------------------------------------------------------------------------- /db/migrate/20170424160906_create_events.rb: -------------------------------------------------------------------------------- 1 | class CreateEvents < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :events do |t| 4 | t.string :name, null: false 5 | t.text :description, null: false 6 | t.integer :group_id, null: false 7 | t.string :location, null: false 8 | t.datetime :date, null: false 9 | 10 | t.timestamps 11 | end 12 | 13 | add_index :events, :group_id 14 | add_index :events, :name 15 | 16 | 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/fixtures/groups.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | name: MyString 5 | founded_date: 2017-04-21 6 | category_id: 1 7 | creator_id: 1 8 | description: MyText 9 | location: MyString 10 | lat: 1.5 11 | lng: 1.5 12 | 13 | two: 14 | name: MyString 15 | founded_date: 2017-04-21 16 | category_id: 1 17 | creator_id: 1 18 | description: MyText 19 | location: MyString 20 | lat: 1.5 21 | lng: 1.5 22 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | password_digest: MyString 5 | session_token: MyString 6 | username: MyString 7 | email: MyString 8 | location: MyString 9 | image_url: MyString 10 | bio: MyText 11 | 12 | two: 13 | password_digest: MyString 14 | session_token: MyString 15 | username: MyString 16 | email: MyString 17 | location: MyString 18 | image_url: MyString 19 | bio: MyText 20 | -------------------------------------------------------------------------------- /frontend/components/footer/footer_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { logout } from '../../actions/session_actions'; 3 | import Footer from './footer'; 4 | 5 | const mapStateToProps = ({ session }) => ({ 6 | currentUser: session.currentUser 7 | }); 8 | 9 | const mapDispatchToProps = dispatch => ({ 10 | logout: () => dispatch(logout()), 11 | login: user => dispatch(login(user)) 12 | }); 13 | 14 | export default connect( 15 | mapStateToProps, 16 | mapDispatchToProps 17 | )(Footer); 18 | -------------------------------------------------------------------------------- /frontend/components/nav_bar/nav_bar_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { logout } from '../../actions/session_actions'; 3 | import NavBar from './nav_bar'; 4 | 5 | const mapStateToProps = ({ session }) => ({ 6 | currentUser: session.currentUser 7 | }); 8 | 9 | const mapDispatchToProps = dispatch => ({ 10 | logout: () => dispatch(logout()), 11 | login: user => dispatch(login(user)) 12 | }); 13 | 14 | export default connect( 15 | mapStateToProps, 16 | mapDispatchToProps 17 | )(NavBar); 18 | -------------------------------------------------------------------------------- /frontend/components/search/category_search_results.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { fetchGroups } from '../../actions/group_actions'; 4 | import { Link, withRouter, hashHistory} from 'react-router'; 5 | import GroupSearchResults from './group_search_results'; 6 | 7 | class CategorySearchResults extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { category: this.props.params.category, title: "" } 11 | } 12 | 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UsersController < ApplicationController 2 | 3 | def create 4 | @user = User.new(user_params) 5 | if @user.save 6 | login(@user) 7 | render "api/users/show" 8 | else 9 | render json: @user.errors.messages, status: 422 10 | end 11 | end 12 | 13 | private 14 | 15 | def user_params 16 | params.require(:user).permit( 17 | :username, 18 | :password, 19 | :email, 20 | :location, 21 | :image_url, 22 | :bio) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /db/migrate/20170421131305_create_groups.rb: -------------------------------------------------------------------------------- 1 | class CreateGroups < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :groups do |t| 4 | t.string :name, null: false 5 | t.date :founded_date, null: false 6 | t.integer :category_id, null: false 7 | t.integer :creator_id, null: false 8 | t.text :description, null: false 9 | t.string :location, null: false 10 | 11 | t.timestamps 12 | 13 | end 14 | add_index :groups, :name, unique:true 15 | add_index :groups, :creator_id 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/views/api/events/_event.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! event, :id, :group_id, :name, :description, :location_name, :location_address, :time, :date, :organizer_id 2 | 3 | json.attendee_count event.attendees.length 4 | 5 | json.formatted_event_date event.date.strftime("%B %d, %Y") 6 | json.event_time event.time.strftime("%H %M %S") 7 | 8 | json.organizer do 9 | json.extract! event.organizer, :id, :username 10 | end 11 | 12 | 13 | json.attendees do 14 | json.array! event.attendees do |attendee| 15 | json.extract! attendee, :id, :username, :profile_pic_url 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/assets/stylesheets/base/grid.scss: -------------------------------------------------------------------------------- 1 | .col { 2 | float: left; 3 | box-sizing: border-box; 4 | } 5 | 6 | /* Attribute selector targeting all elements 7 | with a class attribute containing 'col-' */ 8 | [class*='col-'] { 9 | padding-right: 20px; 10 | } 11 | 12 | [class*='col-']:last-of-type { 13 | padding-right: 0; 14 | } 15 | 16 | .col-2-3 { 17 | width: 66.6666%; 18 | } 19 | 20 | .col-1-2 { 21 | width: 50%; 22 | } 23 | 24 | .col-1-3 { 25 | width: 33.3333%; 26 | } 27 | 28 | @media all and (max-width: 1000px) { 29 | [class*='col-'] { 30 | width: 100%; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/components/groups/group_form_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { createGroup } from '../../actions/group_actions'; 3 | import { clearErrors } from '../../actions/error_actions'; 4 | import GroupForm from './group_form'; 5 | 6 | 7 | const mapStateToProps = (state) => ({ 8 | 9 | }); 10 | 11 | const mapDispatchToProps = (dispatch) => ({ 12 | createGroup: group => dispatch(createGroup(group)), 13 | clearErrors: () => dispatch(clearErrors()) 14 | }); 15 | 16 | export default connect( 17 | mapStateToProps, 18 | mapDispatchToProps 19 | )(GroupForm); 20 | -------------------------------------------------------------------------------- /frontend/reducers/selectors.js: -------------------------------------------------------------------------------- 1 | 2 | export const selectGroup = ({ groups }, id) => { 3 | const group = groups[id]; 4 | return group; 5 | }; 6 | 7 | export const groupsArray = (groups) => { 8 | let arr = []; 9 | if (groups) { 10 | let keys = Object.keys(groups); 11 | keys.forEach( (key) => arr.push(groups[key])) 12 | } 13 | return arr; 14 | }; 15 | 16 | 17 | export const selectEvent = ({ events }, id) => { 18 | const event = events[id]; 19 | return event; 20 | }; 21 | 22 | export const eventsArray = (events) => ( 23 | Object.keys(events).map(key => events[key]) 24 | ); 25 | -------------------------------------------------------------------------------- /db/migrate/20170418160540_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :users do |t| 4 | t.string :password_digest, null: false 5 | t.string :session_token, null: false 6 | t.string :username, null: false 7 | t.string :email, null: false 8 | t.string :location 9 | t.string :image_url 10 | t.text :bio 11 | 12 | t.timestamps 13 | end 14 | 15 | add_index :users, :username, unique: true 16 | add_index :users, :email, unique: true 17 | add_index :users, :session_token, unique: true 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /frontend/lets_meet.jsx: -------------------------------------------------------------------------------- 1 | //React 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | //Components 5 | import Root from './components/root'; 6 | import configureStore from './store/store'; 7 | 8 | 9 | document.addEventListener('DOMContentLoaded', () => { 10 | let store; 11 | if (window.currentUser) { 12 | const preloadedState = { session: { currentUser: window.currentUser } }; 13 | store = configureStore(preloadedState); 14 | } else { 15 | store = configureStore(); 16 | } 17 | const root = document.getElementById('root'); 18 | ReactDOM.render(, root); 19 | }); 20 | -------------------------------------------------------------------------------- /frontend/components/events/event_form_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { createEvent } from '../../actions/event_actions'; 3 | import { clearErrors } from '../../actions/error_actions'; 4 | import EventForm from './event_form'; 5 | 6 | 7 | const mapStateToProps = (state, ownProps) => ({ 8 | group_id: ownProps.params.groupId 9 | }); 10 | 11 | const mapDispatchToProps = (dispatch) => ({ 12 | createEvent: event => dispatch(createEvent(event)), 13 | clearErrors: () => dispatch(clearErrors()) 14 | }); 15 | 16 | export default connect( 17 | mapStateToProps, 18 | mapDispatchToProps 19 | )(EventForm); 20 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: './frontend/lets_meet.jsx', 6 | output: { 7 | path: path.resolve(__dirname, 'app', 'assets', 'javascripts'), 8 | filename: "bundle.js" 9 | }, 10 | module: { 11 | loaders: [ 12 | { 13 | test: [/\.jsx?$/, /\.js?$/], 14 | exclude: /node_modules/, 15 | loader: 'babel-loader', 16 | query: { 17 | presets: ['es2015', 'react'] 18 | } 19 | } 20 | ] 21 | }, 22 | devtool: 'source-maps', 23 | resolve: { 24 | extensions: [".js", ".jsx", "*"] 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /.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 | node_modules/ 20 | bundle.js 21 | bundle.js.map 22 | .byebug_history 23 | .DS_Store 24 | npm-debug.log 25 | 26 | # Ignore application configuration 27 | /config/application.yml 28 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /app/controllers/api/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SessionsController < ApplicationController 2 | def create 3 | @user = User.find_by_credentials( 4 | user_params[:email], 5 | user_params[:password] 6 | ) 7 | 8 | if @user 9 | login(@user) 10 | render "api/users/show" 11 | else 12 | render( 13 | json: { login: ["Invalid email or password"] }, 14 | status: 401 15 | ) 16 | end 17 | end 18 | 19 | def destroy 20 | @user = current_user 21 | if @user 22 | logout 23 | render "api/users/show" 24 | else 25 | render( 26 | json: {logout: ["No one is signed in"] }, 27 | status: 404 28 | ) 29 | end 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /app/models/event.rb: -------------------------------------------------------------------------------- 1 | class Event < ApplicationRecord 2 | include PgSearch 3 | pg_search_scope :search_by_details, :against => [:name, :description] 4 | validates :name, :description, :group, :location_name, :location_address, :time, :date, :organizer, presence: true 5 | 6 | belongs_to :group 7 | 8 | belongs_to :organizer, 9 | class_name: :User, 10 | primary_key: :id, 11 | foreign_key: :organizer_id 12 | 13 | has_many :rsvps 14 | 15 | has_many :attendees, 16 | through: :rsvps, 17 | source: :attendee 18 | 19 | def attend 20 | Rsvp.create(event_id: self.id, attendee_id: organizer.id) 21 | end 22 | 23 | def self.upcoming_events(events) 24 | events.select { |event| event.date > DateTime.now } 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | LetsMeet 5 | <%= csrf_meta_tags %> 6 | 7 | <%= stylesheet_link_tag 'application', media: 'all' %> 8 | <%= javascript_include_tag 'application' %> 9 | <%= favicon_link_tag "favicon.ico" %> 10 | 11 | 12 | 13 | 14 | 15 | 16 | <%= yield %> 17 | 18 | 19 | -------------------------------------------------------------------------------- /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 | //= require bundle 17 | -------------------------------------------------------------------------------- /frontend/components/events/event_rsvp_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { clearErrors } from '../../actions/error_actions'; 4 | import {createRsvp, deleteRsvp} from '../../actions/event_actions'; 5 | import { createMembership } from '../../actions/group_actions'; 6 | 7 | const mapStateToProps = (state) => { 8 | return { 9 | currentUser: state.session.currentUser 10 | }; 11 | }; 12 | 13 | const mapDispatchToProps = (dispatch) => { 14 | return { 15 | createRsvp: (eventId, memberId) => dispatch(createRsvp(eventId, memberId)), 16 | deleteRsvp: (id) => dispatch(deleteRsvp(id)), 17 | createMembership: (groupId, userId) => dispatch(createMembership(groupId, userId)) 18 | }; 19 | }; 20 | 21 | export default connect( 22 | mapStateToProps, 23 | mapDispatchToProps 24 | )(EventRsvp); 25 | -------------------------------------------------------------------------------- /app/controllers/api/memberships_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::MembershipsController < ApplicationController 2 | 3 | def create 4 | @membership = Membership.new(membership_params) 5 | @group = @membership.group 6 | if @membership.save 7 | render 'api/groups/show' 8 | else 9 | render json: @membership.errors.messages, status: 422 10 | end 11 | end 12 | 13 | def destroy 14 | @membership = Membership.find_by(member_id: current_user.id, group_id: params[:id]) 15 | @group = @membership.group 16 | if @membership.destroy 17 | render 'api/groups/show' 18 | else 19 | render json: @membership.errors.messages, status: 422 20 | end 21 | end 22 | 23 | private 24 | def membership_params 25 | params.require(:membership).permit(:member_id, :group_id) 26 | 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /app/controllers/api/rsvps_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::RsvpsController < ApplicationController 2 | def create 3 | @rsvp = current_user.rsvps.new(event_id: params[:event_id]) 4 | if @rsvp.save 5 | @event = Event.find(params[:event_id]) 6 | render 'api/events/show' 7 | else 8 | render json: @rsvps.errors.messages, status: 422 9 | end 10 | end 11 | 12 | def destroy 13 | @rsvp = Rsvp.find_by(attendee_id: current_user.id, event_id: params[:event_id]) 14 | @event = Event.find(params[:event_id]) 15 | if @rsvp.destroy 16 | render 'api/events/show' 17 | else 18 | render json: @rsvp.errors.messages, status: 422 19 | end 20 | end 21 | 22 | private 23 | def rsvp_params 24 | params.require(:rsvp).permit(:event_id) 25 | 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | 3 | namespace :api, defaults: { format: :json } do 4 | get '/groups/search', to: 'groups#search' 5 | get '/events/search', to: 'events#search' 6 | 7 | resources :users, only: [:create, :show] 8 | resource :session, only: [:create, :destroy] 9 | resources :groups, only: [:create, :update, :destroy, :show, :index] do 10 | resources :memberships, only: [:create] 11 | end 12 | delete 'groups/:id/memberships', to: 'memberships#destroy', as: 'group_membership' 13 | resources :events, only: [:create, :update, :destroy, :index, :show] do 14 | resources :rsvps, only: [:create] 15 | end 16 | delete 'events/:event_id/rsvps', to: 'rsvps#destroy', as: 'event_membership' 17 | end 18 | # root "static_pages#root" 19 | root to: "static_pages#root" 20 | end 21 | -------------------------------------------------------------------------------- /frontend/components/events/event_rsvp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRsvp, deleteRsvp } from '../../actions/event_actions'; 3 | import { createMembership } from '.../.../actions/group_actions'; 4 | 5 | class EventRsvp extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { attendeeIds: this.props.attendeeIds, memberIds: this.props.memberIds }; 10 | this.handleConfirm = this.handleConfirm.bind(this); 11 | this.handleUnconfirm = this.handleUnconfirm.bind(this); 12 | this.handleGroupJoin = this.handleGroupJoin.bind(this); 13 | } 14 | 15 | componentWillReceiveProps(nextProps) { 16 | this.setState({attendeeIds: nextProps.attendeeIds, memberIds: this.props.memberIds}); 17 | } 18 | 19 | handleUnconfirm { 20 | this.props.deleteRsvp(this.props.eventId). 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /frontend/components/session_form/session_form_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { login, logout, signup } from '../../actions/session_actions'; 3 | import { clearErrors } from '../../actions/error_actions'; 4 | import SessionForm from './session_form'; 5 | 6 | const mapStateToProps = ({ session, errors }) => ({ 7 | loggedIn: Boolean(session.currentUser), 8 | errors 9 | }); 10 | 11 | const mapDispatchToProps = (dispatch, { location }) => { 12 | const formType = location.pathname.slice(1); 13 | const processForm = (formType === 'login') ? login : signup; 14 | 15 | return { 16 | clearErrors: () => dispatch(clearErrors()), 17 | processForm: user => dispatch(processForm(user)), 18 | formType 19 | }; 20 | }; 21 | 22 | export default connect( 23 | mapStateToProps, 24 | mapDispatchToProps 25 | )(SessionForm); 26 | -------------------------------------------------------------------------------- /frontend/reducers/session_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_CURRENT_USER, 3 | RECEIVE_ERRORS, 4 | CLEAR_ERRORS } from '../actions/session_actions'; 5 | import merge from 'lodash/merge'; 6 | 7 | const _nullUser = Object.freeze({ 8 | currentUser: null 9 | }); 10 | 11 | const SessionReducer = (state = _nullUser, action) => { 12 | Object.freeze(state); 13 | switch(action.type) { 14 | case RECEIVE_CURRENT_USER: 15 | const currentUser = action.currentUser; 16 | return merge({}, _nullUser, { currentUser } 17 | ); 18 | case RECEIVE_ERRORS: 19 | const errors = action.errors; 20 | return merge({}, state, { errors: action.errors }); 21 | // case CLEAR_ERRORS: 22 | // return Object.assign({}, state, { errors: [] }); 23 | default: 24 | return state; 25 | } 26 | }; 27 | 28 | export default SessionReducer; 29 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/footer.scss: -------------------------------------------------------------------------------- 1 | 2 | .footer-main { 3 | display:flex; 4 | justify-content: space-between; 5 | padding-right: 50px; 6 | padding-left: 15px; 7 | padding-top: 10px; 8 | padding-bottom: 10px; 9 | border: 1px solid black; 10 | background-color: black; 11 | bottom: 0; 12 | flex-direction: row; 13 | align-items: center; 14 | } 15 | 16 | 17 | 18 | .footer-left > ul, 19 | .footer-right > ul, 20 | .footer-center > ul { 21 | display: flex; 22 | cursor: pointer; 23 | color: white; 24 | font-size: 16px; 25 | } 26 | 27 | .footer-center > ul > li { 28 | padding: 0 30px; 29 | font-size: 36px; 30 | } 31 | 32 | .footer-left > ul > li, 33 | .footer-right > ul > li { 34 | font-size: 16px; 35 | font-weight: 500; 36 | cursor: pointer; 37 | text-align: center; 38 | } 39 | 40 | .footer-right { 41 | margin-left: 30px; 42 | } 43 | -------------------------------------------------------------------------------- /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 LetsMeet 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 | config.paperclip_defaults = { 15 | :storage => :s3, 16 | :s3_credentials => { 17 | :bucket => ENV["s3_bucket"], 18 | :access_key_id => ENV["s3_access_key_id"], 19 | :secret_access_key => ENV["s3_secret_access_key"], 20 | :s3_region => ENV["s3_region"] 21 | } 22 | } 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /frontend/reducers/events_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_EVENTS, 3 | RECEIVE_EVENT, 4 | REMOVE_EVENT, RECEIVE_ERRORS } from '../actions/event_actions'; 5 | import { merge } from 'lodash'; 6 | 7 | 8 | const _nullEvent = {}; 9 | 10 | const EventsReducer = (state = _nullEvent, action) => { 11 | Object.freeze(state); 12 | switch(action.type) { 13 | case RECEIVE_EVENTS: 14 | return action.events; 15 | case RECEIVE_EVENT: 16 | return Object.assign({}, state, { [action.event.id]: action.event }); 17 | case REMOVE_EVENT: 18 | let newState = merge({}, state); 19 | delete newState[action.event.id]; 20 | return newState; 21 | case RECEIVE_ERRORS: 22 | const errors = action.errors; 23 | return merge({}, state, { errors: action.errors }); 24 | default: 25 | return state; 26 | } 27 | }; 28 | 29 | export default EventsReducer; 30 | -------------------------------------------------------------------------------- /frontend/reducers/groups_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_GROUPS, 3 | RECEIVE_GROUP, 4 | REMOVE_GROUP, RECEIVE_ERRORS } from '../actions/group_actions'; 5 | import { merge } from 'lodash'; 6 | 7 | 8 | const _nullGroup = {}; 9 | 10 | const GroupsReducer = (state = _nullGroup, action) => { 11 | Object.freeze(state); 12 | switch(action.type) { 13 | case RECEIVE_GROUPS: 14 | return action.groups; 15 | case RECEIVE_GROUP: 16 | return Object.assign({}, state, { [action.group.id]: action.group }); 17 | case REMOVE_GROUP: 18 | let newState = merge({}, state); 19 | delete newState[action.group.id]; 20 | return newState; 21 | case RECEIVE_ERRORS: 22 | const errors = action.errors; 23 | return merge({}, state, { errors: action.errors }); 24 | default: 25 | return state; 26 | } 27 | }; 28 | 29 | export default GroupsReducer; 30 | -------------------------------------------------------------------------------- /frontend/components/search/search_bar_container.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { fetchGroups, searchGroups } from '../../actions/group_actions'; 4 | import { fetchEvents, searchEvents } from '../../actions/event_actions'; 5 | import { Link } from 'react-router'; 6 | import SearchBar from './search_bar'; 7 | 8 | 9 | const mapStateToProps = (state) => { 10 | return { 11 | groups: state.groups, 12 | events: state.events 13 | }; 14 | }; 15 | 16 | const mapDispatchToProps = (dispatch) => { 17 | return { 18 | fetchGroups: () => dispatch(fetchGroups()), 19 | searchGroups: (search) => dispatch(searchGroups(search)), 20 | fetchEvents: () => dispatch(fetchEvents()), 21 | searchEvents: (search) => dispatch(searchEvents(search)) 22 | }; 23 | }; 24 | 25 | export default connect(mapStateToProps, mapDispatchToProps)(SearchBar); 26 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /app/models/group.rb: -------------------------------------------------------------------------------- 1 | class Group < ApplicationRecord 2 | include PgSearch 3 | pg_search_scope :search_by_details, :against => [:name, :description] 4 | validates :name, :founded_date, :category, :creator, :description, :location, presence: true 5 | validates :name, uniqueness: true 6 | 7 | belongs_to :creator, 8 | class_name: :User, 9 | primary_key: :id, 10 | foreign_key: :creator_id 11 | 12 | has_many :events 13 | 14 | has_many :memberships, 15 | dependent: :destroy 16 | 17 | has_many :members, 18 | through: :memberships, 19 | source: :member 20 | 21 | has_many :category_groups, 22 | dependent: :destroy 23 | 24 | has_many :categories, 25 | through: :category_groups, 26 | source: :category 27 | 28 | def save_and_join 29 | transaction do 30 | self.founded_date = Date.parse(Time.now.to_s) 31 | save 32 | Membership.create(group_id: self.id, member_id: creator.id) 33 | end 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | helper_method :current_user, :logged_in? 4 | 5 | private 6 | 7 | def current_user 8 | return nil unless session[:session_token] 9 | @current_user ||= User.find_by(session_token: session[:session_token]) 10 | end 11 | 12 | def login(user) 13 | user.reset_session_token! 14 | session[:session_token] = user.session_token 15 | @current_user = user 16 | end 17 | 18 | def logged_in? 19 | !!current_user 20 | end 21 | 22 | def logout 23 | current_user.reset_session_token! 24 | session[:session_token] = nil 25 | @current_user = nil 26 | end 27 | 28 | def require_logged_in 29 | render json: {base: ['invalid credentials']}, status: 401 if !current_user 30 | end 31 | 32 | def user_params 33 | params.require(:user).permit(:email, :password, :username) 34 | end 35 | 36 | 37 | end 38 | -------------------------------------------------------------------------------- /frontend/components/events/edit_event_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { updateEvent, deleteEvent } from '../../actions/event_actions'; 3 | import { hashHistory } from 'react-router'; 4 | import { selectEvent } from '../../reducers/selectors'; 5 | import { clearErrors } from '../../actions/error_actions'; 6 | import { fetchEvent } from '../../actions/event_actions'; 7 | import EditEventForm from './edit_event_form'; 8 | 9 | const mapStateToProps = (state, { params }) => { 10 | const eventId = parseInt(params.eventId); 11 | const event = selectEvent(state, eventId); 12 | return { 13 | eventId, 14 | event 15 | }; 16 | }; 17 | const mapDispatchToProps = (dispatch) => { 18 | return { 19 | updateEvent: (event) => dispatch(updateEvent(event)), 20 | deleteEvent: (id) => dispatch(deleteEvent(id)), 21 | fetchEvent: id => dispatch(fetchEvent(id)) 22 | }; 23 | }; 24 | 25 | export default connect(mapStateToProps, mapDispatchToProps)(EditEventForm); 26 | -------------------------------------------------------------------------------- /frontend/components/groups/edit_group_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { updateGroup, deleteGroup } from '../../actions/group_actions'; 3 | import { hashHistory } from 'react-router'; 4 | import { selectGroup } from '../../reducers/selectors'; 5 | import { clearErrors } from '../../actions/error_actions'; 6 | import { fetchGroup } from '../../actions/group_actions'; 7 | import EditGroupForm from './edit_group_form'; 8 | 9 | const mapStateToProps = (state, { params }) => { 10 | const groupId = parseInt(params.groupId); 11 | const group = selectGroup(state, groupId); 12 | return { 13 | groupId, 14 | group 15 | }; 16 | }; 17 | const mapDispatchToProps = (dispatch) => { 18 | return { 19 | updateGroup: (group) => dispatch(updateGroup(group)), 20 | deleteGroup: (id) => dispatch(deleteGroup(id)), 21 | fetchGroup: id => dispatch(fetchGroup(id)) 22 | }; 23 | }; 24 | 25 | export default connect(mapStateToProps, mapDispatchToProps)(EditGroupForm); 26 | -------------------------------------------------------------------------------- /frontend/actions/session_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/session_api_util'; 2 | import { clearErrors, receiveErrors } from './error_actions'; 3 | import { hashHistory } from 'react-router'; 4 | 5 | export const RECEIVE_CURRENT_USER = "RECEIVE_CURRENT_USER"; 6 | 7 | 8 | export const signup = user => dispatch => ( 9 | APIUtil.signup(user) 10 | .then(user => dispatch(receiveCurrentUser(user)), 11 | err => dispatch(receiveErrors(err))) 12 | ); 13 | 14 | export const login = user => dispatch => { 15 | return APIUtil.login(user) 16 | .then(user => dispatch(receiveCurrentUser(user)), 17 | err => { 18 | dispatch(receiveErrors(err)); 19 | }); 20 | }; 21 | 22 | export const logout = () => dispatch => { 23 | APIUtil.logout().then(user => dispatch(receiveCurrentUser(null))); 24 | hashHistory.push('/'); 25 | }; 26 | 27 | export const receiveCurrentUser = currentUser => { 28 | return { 29 | type: RECEIVE_CURRENT_USER, 30 | currentUser 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /frontend/components/footer/footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | 5 | const Footer = ({currentUser, login, logout}) => ( 6 | 25 | ); 26 | 27 | 28 | export default Footer; 29 | -------------------------------------------------------------------------------- /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: decbd158dbbe9ec261e4165acbf41b29311df66656ce26fd57b19eb04f5f579f0b35a4ac3745b70d4c36c1f127aa9347ec04ef57fc33b8607ee623de4096e18d 15 | 16 | test: 17 | secret_key_base: eb7343d1c4935f94f3ea11c9057e13e4c24229497da01d3cf630becb54da93662ef3b5952c73eb3e415399c00113bae8c521ad29476a164bac7c167cc1d512e9 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/welcome_page/welcome_page.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | 5 | class WelcomePage extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | return( 12 |
13 | 14 |
15 |

Want to do something you are passionate about?

16 |

Then LetsMeet!

17 | Sign up 18 |
19 | 20 |
21 |
30 | 31 |
32 | ); 33 | } 34 | } 35 | 36 | export default WelcomePage; 37 | -------------------------------------------------------------------------------- /app/assets/stylesheets/base/reset.scss: -------------------------------------------------------------------------------- 1 | html, body, header, nav, h1, a, 2 | ul, li, strong, main, button, i, 3 | section, img, div, h2, p, form, 4 | fieldset, label, input, textarea, 5 | span, article, footer, time, small { 6 | margin: 0; 7 | padding: 0; 8 | border: 0; 9 | outline: 0; 10 | font: inherit; 11 | color: inherit; 12 | text-align: inherit; 13 | text-decoration: inherit; 14 | vertical-align: inherit; 15 | box-sizing: inherit; 16 | background: transparent; 17 | } 18 | 19 | ul { 20 | list-style: none; 21 | } 22 | 23 | img { 24 | display: block; 25 | width: 100%; 26 | height: auto; 27 | } 28 | 29 | input[type="password"], 30 | input[type="email"], 31 | input[type="text"], 32 | input[type="submit"], 33 | textarea, 34 | button { 35 | /* 36 | Get rid of native styling. Read more here: 37 | http://css-tricks.com/almanac/properties/a/appearance/ 38 | */ 39 | -webkit-appearance: none; 40 | -moz-appearance: none; 41 | appearance: none; 42 | } 43 | 44 | button, 45 | input[type="submit"] { 46 | cursor: pointer; 47 | } 48 | 49 | /* Clearfix */ 50 | 51 | .group:after { 52 | content: ""; 53 | display: block; 54 | clear: both; 55 | } 56 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/util/event_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchEvents = () => { 2 | return $.ajax({ 3 | method: 'GET', 4 | url: '/api/events', 5 | }); 6 | }; 7 | 8 | export const fetchEvent = (id) => { 9 | return $.ajax({ 10 | method: 'GET', 11 | url: `/api/events/${id}`, 12 | }); 13 | }; 14 | 15 | export const createEvent = (event) => { 16 | return $.ajax({ 17 | method: 'POST', 18 | url: '/api/events', 19 | data: { event } 20 | }); 21 | }; 22 | 23 | export const updateEvent = (event) => { 24 | return $.ajax({ 25 | method: 'PATCH', 26 | url: `/api/events/${event.id}`, 27 | data: { event } 28 | }); 29 | }; 30 | 31 | export const deleteEvent = (id) => { 32 | return $.ajax({ 33 | method: 'DELETE', 34 | url: `/api/events/${id}`, 35 | }); 36 | }; 37 | 38 | export const searchEvents = (search) => { 39 | return $.ajax({ 40 | method: 'GET', 41 | url: `/api/events/search?search=${search}` 42 | }); 43 | }; 44 | 45 | export const createRsvp = (eventId, memberId) => { 46 | return $.ajax({ 47 | method: 'POST', 48 | url: `/api/events/${eventId}/rsvps` 49 | }); 50 | }; 51 | 52 | export const deleteRsvp = (id) => { 53 | return $.ajax({ 54 | method: 'DELETE', 55 | url: `/api/events/${id}/rsvps` 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /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 | *= require_self 14 | */ 15 | 16 | /*CSS Reset*/ 17 | @import "base/reset.scss"; 18 | 19 | /*Core*/ 20 | @import "base/colors.scss"; 21 | @import "base/fonts.scss"; 22 | @import "base/layouts.scss"; 23 | 24 | // Grid 25 | @import "base/grid.scss"; 26 | 27 | @import "components/welcome.scss"; 28 | @import "api/sessions.scss"; 29 | @import "components/search.scss"; 30 | @import "api/groups.scss"; 31 | @import "api/events.scss"; 32 | @import "components/group_show.scss"; 33 | @import "components/footer.scss"; 34 | -------------------------------------------------------------------------------- /docs/api-endpoints.md: -------------------------------------------------------------------------------- 1 | # API Endpoints 2 | 3 | ## HTML API 4 | 5 | ### Root 6 | 7 | - `GET /` - loads React web app 8 | 9 | 10 | ## JSON API 11 | 12 | ### Users 13 | 14 | * `POST /api/users` - create new user 15 | * `PATCH /api/users` - update user information 16 | * `GET /users/:id` - get user information for user's profile 17 | 18 | ### Session 19 | 20 | * `POST /api/session` - sign in user 21 | * `DELETE /api/session` - sing out user 22 | 23 | ### Groups 24 | 25 | * `GET /api/groups` - accepts location, name, and category query params to fetch group search results 26 | * `POST /api/groups` - create new group 27 | * `GET /api/groups/:id` - get specific group information 28 | * `PATCH /api/groups/:id` - update group information if user created group (?) 29 | * `DELETE /api/groups/:id` - delete group if user created it (?) 30 | 31 | ### Events 32 | * `GET /api/events` - gets all events (??) 33 | * `POST /api/events` - create new event 34 | * `GET /api/events/:id` - get specific event information 35 | * `PATCH /api/events/:id` - update event information if user created group (?) 36 | * `DELETE /api/events/:id` - delete event if user created it (?) 37 | 38 | ### RSVPs 39 | * `GET /api/events/:id/rsvps` 40 | * `POST /api/rsvps` 41 | * `PATCH /api/rsvps/:id` 42 | 43 | ### Memberships 44 | * `GET /api/groups/:id/members` 45 | * `POST /api/members` 46 | * `DELETE /api/members/:id` 47 | -------------------------------------------------------------------------------- /app/assets/stylesheets/base/layouts.scss: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | height: 100%; 4 | } 5 | 6 | body { 7 | width: 100%; 8 | margin: 0 auto; 9 | font-family: $sans-serif; 10 | font-size: 12px; 11 | color: rgba(0,0,0,.85); 12 | } 13 | 14 | .hidden { 15 | display: none; 16 | } 17 | 18 | /* 19 | Font Elements 20 | */ 21 | 22 | p { 23 | line-height: 16px; 24 | } 25 | 26 | img { 27 | width: 100%; 28 | height: 100%; 29 | } 30 | 31 | 32 | /* 33 | Header Rules 34 | */ 35 | // 36 | // hr { 37 | // border: 0; 38 | // height: 1px; 39 | // background-color: light-gray; 40 | // } 41 | 42 | .hr-top, .hr-bottom { 43 | width: 100%; 44 | margin: 0; 45 | } 46 | 47 | .hr-top { 48 | height: 2px; 49 | margin-bottom: 1px; 50 | } 51 | 52 | /* 53 | Images 54 | */ 55 | 56 | .thumbnail { 57 | height: 100px; 58 | } 59 | 60 | .thumbnail img { 61 | object-fit: cover; 62 | } 63 | 64 | /* 65 | Inputs 66 | */ 67 | 68 | .input-wrapper { 69 | display: flex; 70 | margin: 5px 0; 71 | } 72 | 73 | .input-wrapper input { 74 | flex: 1; 75 | font-family: $sans-serif; 76 | padding: 0 10px; 77 | border: 1px solid gray; 78 | border-right: none; 79 | border-top-right-radius: 0; 80 | border-bottom-right-radius: 0; 81 | } 82 | 83 | .input-wrapper button { 84 | border-top-left-radius: 0; 85 | border-bottom-left-radius: 0; 86 | } 87 | 88 | input[type="search"] { 89 | padding: 5px; 90 | } 91 | -------------------------------------------------------------------------------- /app/controllers/api/events_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::EventsController < ApplicationController 2 | 3 | def index 4 | @events = Event.all 5 | render :index 6 | end 7 | 8 | def create 9 | @event = current_user.organized_events.new(event_params) 10 | if @event.save 11 | @event.attend 12 | render :show 13 | else 14 | render json: @event.errors.messages, status: 422 15 | end 16 | end 17 | 18 | def update 19 | @event = Event.includes(:group, :organizer, :attendees).find(params[:id]) 20 | if @event.update(event_params) 21 | render :show 22 | else 23 | render json: @event.errors.messages, status: 422 24 | end 25 | end 26 | 27 | def destroy 28 | @event = current_user.organized_events.find(params[:id]) 29 | @group = Group.find(@event.group.id) 30 | if @event.destroy 31 | render 'api/groups/show' 32 | else 33 | render json: @event.errors.messages, status: 422 34 | end 35 | end 36 | 37 | 38 | def show 39 | @event = Event.find(params[:id]) 40 | end 41 | 42 | def search 43 | @events= Event.includes(:group, :organizer, :attendees).search_by_details(params[:search]).order(:date) 44 | render :search 45 | end 46 | 47 | private 48 | def event_params 49 | params.require(:event).permit(:id, :name, :description, :date, :time, :group_id, :group, :organizer_id, :location_name, :location_address, :rsvps, :attendees) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /docs/component-hierarchy.md: -------------------------------------------------------------------------------- 1 | # Component Hierarchy 2 | 3 | **AppContainer** 4 | * HeaderSignedOut/HeaderSignedIn 5 | * FooterSignedOut/FooterSignedIn 6 | 7 | **AuthFormContainer** 8 | * AuthForm 9 | * UserSignUp 10 | * UserSignIn 11 | 12 | **WelcomeContainer** 13 | * IntroCover 14 | * CategoriesIndex 15 | * CategoryItem 16 | 17 | **HomeContainer** 18 | * SearchBarContainer 19 | * SearchBar 20 | * SearchResultsContainer 21 | * GroupSearchResultsIndex 22 | * GroupSearchResultItem 23 | * EventSearchResultsContainer 24 | * EventSearchResultItem 25 | 26 | **GroupFormContainer** 27 | * GroupForm 28 | 29 | **EventFormContainer** 30 | * EventForm 31 | 32 | **GroupContainer** 33 | * GroupEventsIndex 34 | * GroupEventIndexItem 35 | * GroupInfoSidebar 36 | * WhatsNewSidebar 37 | * GroupToolbar 38 | 39 | **EventContainer** 40 | * EventDetails 41 | * AttendanceInformationSidebar 42 | 43 | **UserProfileContainer** 44 | * UserProfile 45 | 46 | **CalendarContainer** 47 | * CalendarIndex 48 | * CalendarIndexItems 49 | 50 | # Routes 51 | 52 | Path | Component 53 | :---:|:---: 54 | "/" | "App" 55 | "signup" | "AuthFormContainer" 56 | "/signin" | "AuthFormContainer" 57 | "/home" | "HomeContainer" 58 | "/home/groups" | "GroupsContainer" 59 | "/new-group" | "GroupFormContainer" 60 | "/home/groups/:groupId" | "GroupContainer" 61 | "/group/:groupid/new-event" | "EventFormContainer" 62 | "/home/event/:eventId" | "EventContainer" 63 | "/search" | "SearchContainer" 64 | "/user/:userId" | "UserProfileContainer" 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LetsMeet", 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 | "webpack": "webpack --w" 14 | }, 15 | "engines": { 16 | "node": "6.7.0", 17 | "npm": "3.10.7" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/RonKew28/LetsMeet.git" 22 | }, 23 | "keywords": [], 24 | "author": "", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/RonKew28/LetsMeet/issues" 28 | }, 29 | "homepage": "https://github.com/RonKew28/LetsMeet#readme", 30 | "dependencies": { 31 | "animate.css": "^3.5.2", 32 | "babel-core": "^6.24.1", 33 | "babel-loader": "^6.4.1", 34 | "babel-preset-es2015": "^6.24.1", 35 | "babel-preset-react": "^6.24.1", 36 | "lodash": "^4.17.4", 37 | "logger": "0.0.1", 38 | "react": "^15.5.4", 39 | "react-dom": "^15.5.4", 40 | "react-dropdown": "^1.2.1", 41 | "react-modal": "^1.7.7", 42 | "react-native-modal-dropdown": "^0.4.2", 43 | "react-redux": "^5.0.4", 44 | "react-router": "^3.0.5", 45 | "redux": "^3.6.0", 46 | "redux-logger": "^3.0.1", 47 | "redux-thunk": "^2.2.0", 48 | "webpack": "^2.4.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/sample-state.md: -------------------------------------------------------------------------------- 1 | ```js 2 | 3 | { 4 | session: { 5 | current_user: { 6 | id: 2, 7 | username: "rohaan28", 8 | image_url: "rohaan.png" 9 | }, 10 | errors: { 11 | errors: [] 12 | } 13 | }, 14 | groups: { 15 | 1: { 16 | id: 1, 17 | name: "NJ Tennis", 18 | category_id: 3, 19 | description: "Play tennis with people from New Jersey", 20 | location: "Summit, New Jersey", 21 | image_url: "tennis.png", 22 | founded_date: "2017-01-01", 23 | members: { 24 | 9: { 25 | id: 9, 26 | username: "jeff12", 27 | image_url: "jeff.png" 28 | }, 29 | 30 | 13: { 31 | id: 13, 32 | username: "TashaK", 33 | image_url: "tasha.png" 34 | }, 35 | 36 | 14: { 37 | id: 14, 38 | username: "Bobby345", 39 | image_url: "bobby.png" 40 | } 41 | } 42 | } 43 | }, 44 | events: { 45 | 1: { 46 | id: 1, 47 | date: "2017-02-01 07:00:00", 48 | name: "Doubles tennis at Memorial Field", 49 | description: "Let's play some tennis!", 50 | location: "100 Larned Road, Summit, NJ 07901", 51 | group_id: 1 52 | }, 53 | 2: { 54 | id: 2, 55 | date: "2017-09-01 10:00:00", 56 | name: "Tennis and BBQ", 57 | description: "Let's play some tennis and also barbeque!", 58 | location: "22 Mountain Avenue, Summit, NJ 07901", 59 | group_id: 1 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /frontend/util/group_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchGroups = () => { 2 | return $.ajax({ 3 | method: 'GET', 4 | url: '/api/groups/', 5 | }); 6 | }; 7 | 8 | export const fetchGroup = (id) => { 9 | return $.ajax({ 10 | method: 'GET', 11 | url: `/api/groups/${id}`, 12 | }); 13 | }; 14 | 15 | export const createGroup = (group) => { 16 | return $.ajax({ 17 | method: 'POST', 18 | url: '/api/groups', 19 | data: { group } 20 | }); 21 | }; 22 | 23 | export const updateGroup = (group) => { 24 | return $.ajax({ 25 | method: 'PATCH', 26 | url: `/api/groups/${group.id}`, 27 | data: { group } 28 | }); 29 | }; 30 | 31 | export const deleteGroup = (id) => { 32 | return $.ajax({ 33 | method: 'DELETE', 34 | url: `/api/groups/${id}`, 35 | }); 36 | }; 37 | 38 | export const searchGroups = (search) => { 39 | return $.ajax({ 40 | method: 'GET', 41 | url: `/api/groups/search?search=${search}` 42 | }); 43 | }; 44 | 45 | export const fetchGroupsByCategory = (category) => { 46 | return $.ajax({ 47 | method: 'GET', 48 | url: '/api/category', 49 | data: { category } 50 | }); 51 | }; 52 | 53 | export const createMembership = (groupId, userId) => { 54 | return $.ajax({ 55 | method: 'POST', 56 | url: `api/groups/${groupId}/memberships`, 57 | data: { membership: { group_id: groupId, member_id: userId } } 58 | }); 59 | }; 60 | 61 | export const deleteMembership = (groupId) => { 62 | return $.ajax({ 63 | method: 'DELETE', 64 | url: `api/groups/${groupId}/memberships` 65 | }); 66 | }; 67 | -------------------------------------------------------------------------------- /frontend/components/events/event_show_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { clearErrors } from '../../actions/error_actions'; 4 | 5 | import { fetchEvent, createRsvp, deleteRsvp } from '../../actions/event_actions'; 6 | import { fetchGroup } from '../../actions/group_actions'; 7 | import { selectEvent } from '../../reducers/selectors'; 8 | import EventShow from './event_show'; 9 | 10 | const mapStateToProps = (state, ownProps) => { 11 | const eventId = parseInt(ownProps.params.eventId); 12 | const event = selectEvent(state, eventId) || {}; 13 | const currentUser = state.session.currentUser; 14 | 15 | let attendees = []; 16 | let attendeeIds = []; 17 | if (event.attendees) { 18 | attendees = event.attendees; 19 | attendees.forEach((attendee) => { 20 | attendeeIds.push(attendee.id); 21 | }); 22 | } 23 | 24 | let attendeeType; 25 | 26 | if (currentUser && currentUser.id === event.organizer_id) { 27 | attendeeType = "organizer"; 28 | } else if (currentUser && attendeeIds.includes(currentUser.id)) { 29 | attendeeType = "attendee"; 30 | } else { 31 | attendeeType = "nonattendee"; 32 | } 33 | return { 34 | attendeeType, 35 | event, 36 | currentUser 37 | }; 38 | }; 39 | 40 | const mapDispatchToProps = dispatch => ({ 41 | fetchEvent: id => dispatch(fetchEvent(id)), 42 | createRsvp: id => dispatch(createRsvp(id)), 43 | deleteRsvp: id => dispatch(deleteRsvp(id)) 44 | }); 45 | 46 | 47 | export default connect( 48 | mapStateToProps, 49 | mapDispatchToProps 50 | )(EventShow); 51 | -------------------------------------------------------------------------------- /frontend/components/groups/group_show_container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { connect } from 'react-redux'; 4 | 5 | import { clearErrors } from '../../actions/error_actions'; 6 | 7 | import { fetchGroup, deleteMembership, createMembership } from '../../actions/group_actions'; 8 | import { selectGroup } from '../../reducers/selectors'; 9 | 10 | import GroupShow from './group_show'; 11 | 12 | const mapStateToProps = (state, ownProps) => { 13 | const groupId = parseInt(ownProps.params.groupId); 14 | const group = selectGroup(state, groupId); 15 | const currentUser = state.session.currentUser; 16 | let members = []; 17 | let memberIds = []; 18 | 19 | let memberType; 20 | if (group) { 21 | members = group.members; 22 | members.forEach((member) => { 23 | memberIds.push(member.id); 24 | }); 25 | if (currentUser && currentUser.id === group.creator_id) { 26 | memberType = "owner"; 27 | } else if (currentUser && memberIds.includes(currentUser.id)) { 28 | memberType = "member"; 29 | } else { 30 | memberType = "nonmember"; 31 | } 32 | } 33 | 34 | 35 | 36 | 37 | return { 38 | memberType, 39 | group, 40 | groupId, 41 | currentUser 42 | }; 43 | }; 44 | 45 | const mapDispatchToProps = dispatch => ({ 46 | fetchGroup: id => dispatch(fetchGroup(id)), 47 | createMembership: (groupId, memberId) => dispatch(createMembership(groupId, memberId)), 48 | deleteMembership: id => dispatch(deleteMembership(id)) 49 | }); 50 | 51 | export default connect( 52 | mapStateToProps, 53 | mapDispatchToProps 54 | )(GroupShow); 55 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | attr_reader :password 3 | 4 | validates :password_digest, :session_token, :username, :email, presence: true 5 | validates :session_token, :username, :email, uniqueness: true 6 | validates :password, length: { minimum: 6, allow_nil: true } 7 | 8 | after_initialize :ensure_session_token 9 | 10 | has_attached_file :image 11 | validates_attachment_content_type :image, content_type: /\Aimage\/.*\Z/ 12 | # default_url: 13 | 14 | has_many :created_groups, 15 | class_name: :Group, 16 | primary_key: :id, 17 | foreign_key: :creator_id 18 | 19 | has_many :organized_events, 20 | class_name: :Event, 21 | primary_key: :id, 22 | foreign_key: :organizer_id 23 | 24 | has_many :rsvps, 25 | class_name: :Rsvp, 26 | primary_key: :id, 27 | foreign_key: :attendee_id 28 | 29 | has_many :confirmed_events, 30 | through: :rsvps, 31 | source: :event 32 | 33 | def self.find_by_credentials(email, password) 34 | user = User.find_by(email: email) 35 | return nil unless user 36 | user.is_password?(password) ? user : nil 37 | end 38 | 39 | def self.generate_session_token 40 | SecureRandom::urlsafe_base64(32) 41 | end 42 | 43 | def password=(password) 44 | @password = password 45 | self.password_digest = BCrypt::Password.create(password) 46 | end 47 | 48 | def is_password?(password) 49 | BCrypt::Password.new(self.password_digest).is_password?(password) 50 | end 51 | 52 | def reset_session_token! 53 | self.session_token = User.generate_session_token 54 | self.save! 55 | self.session_token 56 | end 57 | 58 | private 59 | def ensure_session_token 60 | self.session_token ||= User.generate_session_token 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /frontend/components/search/group_search_results.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { fetchGroups } from '../../actions/group_actions'; 4 | import { Link, withRouter, hashHistory} from 'react-router'; 5 | import { groupsArray } from '../../reducers/selectors'; 6 | 7 | class GroupSearchResults extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = {groups: [] }; 11 | } 12 | 13 | componentDidMount() { 14 | this.props.fetchGroups(); 15 | } 16 | 17 | componentWillReceiveProps(nextProps) { 18 | this.setState({ groups: nextProps.groups }); 19 | } 20 | 21 | render() { 22 | if(!this.props.groups) { 23 | return

Loading

; 24 | } 25 | 26 | let groupList = []; 27 | this.state.groups.forEach((group) => { 28 | groupList.push( 29 |
  • 30 | 37 |
  • ); 38 | }); 39 | 40 | return ( 41 | 44 | ); 45 | } 46 | } 47 | 48 | const mapStateToProps = (state) => { 49 | return { 50 | groups: groupsArray(state.groups), 51 | }; 52 | }; 53 | 54 | const mapDispatchToProps = (dispatch) => { 55 | return { 56 | fetchGroups: () => dispatch(fetchGroups()) 57 | }; 58 | }; 59 | 60 | export default connect(mapStateToProps, mapDispatchToProps)(GroupSearchResults); 61 | -------------------------------------------------------------------------------- /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/welcome_page/home_page.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router'; 3 | import ReactDOM from 'react-dom'; 4 | import { connect } from 'react-redux'; 5 | import { Link } from 'react-router'; 6 | import { fetchGroups } from '../../actions/group_actions'; 7 | import { groupsArray } from '../../reducers/selectors'; 8 | import SearchBarContainer from '../search/search_bar_container'; 9 | 10 | 11 | class HomePage extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | } 15 | componentDidMount() { 16 | this.props.fetchGroups(); 17 | } 18 | 19 | render() { 20 | if(!this.props.groups) { 21 | return

    Loading

    ; 22 | } 23 | 24 | let groupList = []; 25 | this.props.groups.forEach((group) => { 26 | groupList.push( 27 |
  • 28 | 35 |
  • ); 36 | }); 37 | 38 | return ( 39 | 42 | ); 43 | } 44 | } 45 | 46 | 47 | 48 | 49 | const mapStateToProps = (state) => { 50 | return ({ 51 | groups: groupsArray(state.groups), 52 | currentUser: state.session.currentUser 53 | }); 54 | }; 55 | 56 | const mapDispatchToProps = (dispatch) => { 57 | return {fetchGroups: () => dispatch(fetchGroups())}; 58 | }; 59 | 60 | export default connect( 61 | mapStateToProps, mapDispatchToProps 62 | )(HomePage); 63 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The change you wanted was rejected.

    62 |

    Maybe you tried to change something you didn't have access to.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The page you were looking for doesn't exist.

    62 |

    You may have mistyped the address or the page may have moved.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/welcome.scss: -------------------------------------------------------------------------------- 1 | .welcome-cover { 2 | display: inline-block; 3 | height: 300px; 4 | width: 100%; 5 | margin-bottom: 400px; 6 | } 7 | 8 | #video { 9 | z-index: -3; 10 | overflow: hidden; 11 | display: block; 12 | height: 460px; 13 | } 14 | #profile-pic { 15 | width: 50px; 16 | height: 50px; 17 | border-radius: 30px; 18 | margin-right: 20px; 19 | margin-left: 5px; 20 | cursor: pointer; 21 | } 22 | 23 | .welcome-slogan { 24 | padding-top: 170px; 25 | width: 100%; 26 | background: transparent; 27 | z-index: 1; 28 | position: absolute; 29 | text-align: center; 30 | color: white; 31 | } 32 | 33 | .welcome-slogan h1 { 34 | font-size: 48px; 35 | font-weight: 600; 36 | } 37 | 38 | .welcome-slogan h3 { 39 | font-size: 32px; 40 | font-weight: 600; 41 | } 42 | 43 | .video-signup { 44 | 45 | background-color: $main-red; 46 | align-items: center; 47 | color: white; 48 | padding: 15px 100px; 49 | border-radius: 8px; 50 | font-size: 20px; 51 | text-align: center; 52 | font-weight: 600; 53 | } 54 | 55 | .video-signup:hover { 56 | background-color: $main-red-hover; 57 | cursor: pointer; 58 | } 59 | 60 | #welcome-group-img { 61 | width: 250px; 62 | height: 250px; 63 | border: 1px solid black; 64 | margin: 20px; 65 | } 66 | 67 | #welcome-img-txt{ 68 | position: absolute; 69 | width: 231px; 70 | left: 21px; 71 | padding: 5px 10px; 72 | bottom: 0px; 73 | right: 0px; 74 | font-size: 18px; 75 | background: $main-red; 76 | color: white; 77 | text-overflow: ellipsis; 78 | font-weight: normal; 79 | text-align: center; 80 | } 81 | 82 | .welcome-group-container ul { 83 | position: relative; 84 | } 85 | 86 | 87 | .flexthis { 88 | width: 100%; 89 | display: flex; 90 | justify-content: center; 91 | flex-direction: row; 92 | align-items: center; 93 | flex-wrap: wrap; 94 | } 95 | -------------------------------------------------------------------------------- /frontend/components/nav_bar/nav_bar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import Modal from 'react-modal'; 4 | 5 | 6 | const loggedOutLinks = login => ( 7 |
    8 | 13 | 18 | 24 |
    25 | ); 26 | 27 | const loggedInLinks = (currentUser, logout) => ( 28 |
    29 | 34 | 39 | 47 |
    48 | ); 49 | 50 | 51 | const NavBar = ({ currentUser, logout, login }) => ( 52 | currentUser ? loggedInLinks(currentUser, logout) : loggedOutLinks(login) 53 | ); 54 | 55 | 56 | export default NavBar; 57 | -------------------------------------------------------------------------------- /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/controllers/api/groups_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::GroupsController < ApplicationController 2 | 3 | def index 4 | @groups = Group.includes(:members).all 5 | render :index 6 | end 7 | 8 | def create 9 | @group = current_user.created_groups.new(group_params) 10 | if @group.save_and_join 11 | if params['group']['categorizations'] 12 | params['group']['categorizations'].each do |categorization| 13 | category = Category.find_by(title: categorization) 14 | CategoryGroup.create(group_id: @group.id, category_id: category.id) 15 | end 16 | end 17 | render :show 18 | else 19 | render json: @group.errors.messages, status: 422 20 | end 21 | end 22 | 23 | def update 24 | @group = Group.find(params[:id]) 25 | if @group.update(group_params) 26 | render :show 27 | else 28 | render json :groups.errors.messages, status: 422 29 | end 30 | end 31 | 32 | def destroy 33 | @group = current_user.created_groups.find(params[:id]) 34 | if @group.destroy 35 | render :show 36 | else 37 | render json :groups.errors.messages, status: 422 38 | end 39 | end 40 | 41 | def show 42 | @group = Group.find(params[:id]) 43 | end 44 | 45 | def search 46 | @groups = Group.includes(:members).search_by_details(params[:search]) 47 | render :search 48 | end 49 | 50 | def category 51 | category_id = Category.find_by(title: params[:category]).id 52 | @groups = Group.includes(:members).joins(:categories).where("categories.id = ?", category_id) 53 | end 54 | 55 | 56 | private 57 | def group_params 58 | params.require(:group).permit( 59 | :id, 60 | :name, 61 | :creator_id, 62 | :creator, 63 | :description, 64 | :category, 65 | :location, 66 | :founded_date, 67 | :memberships, 68 | :members, 69 | :categorizations) 70 | 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /frontend/components/groups/group_members.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { connect } from 'react-redux'; 4 | import { Link } from 'react-router'; 5 | import { selectGroup } from '../../reducers/selectors'; 6 | import { fetchGroup } from '../../actions/group_actions'; 7 | 8 | class GroupMembers extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | } 12 | componentDidMount() { 13 | this.props.fetchGroup(this.props.groupId); 14 | } 15 | render() { 16 | if(!this.props.group) { 17 | return

    Loading

    ; 18 | } 19 | 20 | let memberList = []; 21 | memberList.push( 22 |
  • 23 |
    24 |
      25 |
    • Members:

    • 26 |
    27 |
    28 |
  • 29 | ); 30 | this.props.group.members.forEach((member) => { 31 | memberList.push( 32 |
  • 33 |
    34 |
      35 |
    • 36 |
    • {member.username}
    • 37 |
    38 |
    39 |
  • 40 | ); 41 | 42 | }); 43 | 44 | return( 45 | 46 | 51 | 52 | ); 53 | } 54 | } 55 | 56 | const mapStateToProps = (state, ownProps) => { 57 | const groupId = parseInt(ownProps.params.groupId); 58 | const group = selectGroup(state, groupId); 59 | 60 | return { 61 | groupId, 62 | group 63 | }; 64 | }; 65 | 66 | const mapDispatchToProps = (dispatch) => { 67 | return {fetchGroup: id => dispatch(fetchGroup(id))}; 68 | }; 69 | 70 | export default connect( 71 | mapStateToProps, mapDispatchToProps 72 | )(GroupMembers); 73 | -------------------------------------------------------------------------------- /frontend/components/events/event_sidebar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | import EventShow from './event_show'; 4 | 5 | class EventSideBar extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.navigateToEventEdit = this.navigateToEventEdit.bind(this); 9 | this.toggleRSVPButton = this.toggleRSVPButton.bind(this); 10 | this.toggleEventEditButton = this.toggleEventEditButton.bind(this); 11 | } 12 | 13 | navigateToEventEdit() { 14 | this.props.router.push(`/events/${this.props.event.id}/edit`); 15 | } 16 | 17 | toggleRSVPButton() { 18 | return( 19 | 20 | ); 21 | } 22 | 23 | toggleEventEditButton() { 24 | if (this.props.event.organizer_id === this.props.currentUser.id) { 25 | return( 26 | 27 | ); 28 | } else { 29 | return ( 30 |
    {this.toggleRSVPButton}
    31 | ); 32 | } 33 | } 34 | 35 | render() { 36 | if(this.props.event) { 37 | return( 38 |
    39 | 62 |
    63 | ); 64 | } else { 65 | return

    Loading

    ; 66 | } 67 | } 68 | } 69 | 70 | export default withRouter(EventSideBar); 71 | -------------------------------------------------------------------------------- /docs/schema.md: -------------------------------------------------------------------------------- 1 | # Schema Information 2 | 3 | ## users 4 | column name | data type | details 5 | :---:|:---:|:---: 6 | id | integer | not null, primary key 7 | password_digest | string | not null 8 | session_token | string | not null, indexed, unique 9 | username | string | not null, indexed, unique 10 | email | string | not null, indexed, unique 11 | location | string | not null 12 | image_url | string | 13 | bio | string | 14 | 15 | ## groups 16 | column name | data type | details 17 | :---:|:---:|:---: 18 | id | integer | not null, primary key 19 | name | string | not null, indexed, unique 20 | category_id | integer | not null, foreign key (references categories), indexed 21 | description | text | not null 22 | location | string | not null 23 | image_url | string 24 | founded_date | date | not null 25 | 26 | ## events 27 | column name | data type | details 28 | :---:|:---:|:---: 29 | id | integer | not null, primary key 30 | name | string | not null, indexed 31 | description | text | not null 32 | location | string | not null 33 | date | datetime | not null 34 | 35 | ## categories 36 | column name | data type | details 37 | :---:|:---:|:---: 38 | id | integer | not null, primary key 39 | title | string | not null, unique, indexed 40 | image_url | string 41 | 42 | ## category_groups 43 | column name | data type | details 44 | :---:|:---:|:---: 45 | id | integer | not null, primary key 46 | group_id | integer | not null, foreign key (references groups), indexed 47 | category_id | integer | not null, foreign key (references categories), indexed 48 | 49 | ## rsvps 50 | column name | data type | details 51 | :---:|:---:|:---: 52 | id | integer | not null, primary key 53 | user_id | integer | not null, foreign key (references users), indexed 54 | event_id | integer | not null, foreign key (references events), indexed 55 | 56 | ## memberships 57 | column name | data type | details 58 | :---:|:---:|:---: 59 | id | integer | not null, primary key 60 | user_id | integer | not null, foreign key (references users), indexed 61 | group_id | integer | not null, foreign key (references groups), indexed 62 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => 'public, max-age=172800' 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Don't care if the mailer can't send. 30 | config.action_mailer.raise_delivery_errors = false 31 | 32 | config.action_mailer.perform_caching = false 33 | 34 | # Print deprecation notices to the Rails logger. 35 | config.active_support.deprecation = :log 36 | 37 | # Raise an error on page load if there are pending migrations. 38 | config.active_record.migration_error = :page_load 39 | 40 | # Debug mode disables concatenation and preprocessing of assets. 41 | # This option may cause significant delays in view rendering with a large 42 | # number of complex assets. 43 | config.assets.debug = true 44 | 45 | # Suppress logger output for asset requests. 46 | config.assets.quiet = true 47 | 48 | # Raises error for missing translations 49 | # config.action_view.raise_on_missing_translations = true 50 | 51 | # Use an evented file watcher to asynchronously detect changes in source code, 52 | # routes, locales, etc. This feature depends on the listen gem. 53 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 54 | end 55 | -------------------------------------------------------------------------------- /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 | gem 'pry-rails' 12 | # Use postgresql as the database for Active Record 13 | gem 'pg', '~> 0.18' 14 | # Use Puma as the app server 15 | gem 'puma', '~> 3.0' 16 | # Use SCSS for stylesheets 17 | gem 'sass-rails', '~> 5.0' 18 | # Use Uglifier as compressor for JavaScript assets 19 | gem 'uglifier', '>= 1.3.0' 20 | # Use CoffeeScript for .coffee assets and views 21 | gem 'coffee-rails', '~> 4.2' 22 | # See https://github.com/rails/execjs#readme for more supported runtimes 23 | # gem 'therubyracer', platforms: :ruby 24 | gem 'figaro' 25 | 26 | gem 'faker' 27 | # Use jquery as the JavaScript library 28 | gem 'jquery-rails' 29 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 30 | gem 'jbuilder', '~> 2.5' 31 | gem 'paperclip', '~>5.0.0.beta1' 32 | gem 'aws-sdk', '>=2.0' 33 | 34 | gem 'annotate' 35 | gem 'bcrypt' 36 | gem 'pg_search' 37 | # Use Redis adapter to run Action Cable in production 38 | # gem 'redis', '~> 3.0' 39 | # Use ActiveModel has_secure_password 40 | # gem 'bcrypt', '~> 3.1.7' 41 | 42 | # Use Capistrano for deployment 43 | # gem 'capistrano-rails', group: :development 44 | 45 | group :development, :test do 46 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 47 | gem 'byebug', platform: :mri 48 | end 49 | 50 | group :development do 51 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. 52 | gem 'web-console', '>= 3.3.0' 53 | gem 'listen', '~> 3.0.5' 54 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 55 | gem 'spring' 56 | gem 'spring-watcher-listen', '~> 2.0.0' 57 | end 58 | 59 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 60 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 61 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/components/groups/group_side_bar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | import { connect } from 'react-redux'; 4 | 5 | class GroupSideBar extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { group: this.props.group, members: this.props.members}; 10 | } 11 | 12 | componentWillReceiveProps(nextProps) { 13 | this.setState({group: nextProps.group, members: nextProps.members}); 14 | } 15 | 16 | render() { 17 | let upcomingEventsCount = 0; 18 | if(this.props.group) { 19 | this.props.group.events.forEach((event) => { 20 | let currentDate = Date.now(); 21 | let date = new Date(event.date).toLocaleDateString(); 22 | let time = new Date(event.time).toLocaleTimeString(); 23 | let eventDateValue = new Date(date + " " + time).valueOf(); 24 | if (eventDateValue > currentDate) { 25 | upcomingEventsCount += 1; 26 | } 27 | }); 28 | 29 | 30 | return( 31 |
    32 | 62 |
    63 | ); 64 | } else { 65 | return null; 66 | } 67 | } 68 | } 69 | 70 | export default GroupSideBar; 71 | -------------------------------------------------------------------------------- /frontend/actions/event_actions.js: -------------------------------------------------------------------------------- 1 | import * as EventAPIUtil from "../util/event_api_util"; 2 | import { receiveErrors, clearErrors} from './error_actions'; 3 | import { hashHistory } from 'react-router'; 4 | 5 | export const RECEIVE_EVENTS = 'RECEIVE_EVENTS'; 6 | export const RECEIVE_EVENT = 'RECEIVE_EVENT'; 7 | export const REMOVE_EVENT = 'REMOVE_EVENT'; 8 | 9 | export const fetchEvents = () => dispatch => { 10 | return( 11 | EventAPIUtil.fetchEvents(). 12 | then(events => dispatch(receiveEvents(events)), 13 | err => dispatch(receiveErrors(err))) 14 | ); 15 | }; 16 | 17 | export const fetchEvent = id => dispatch => { 18 | return( 19 | EventAPIUtil.fetchEvent(id) 20 | .then(event => dispatch(receiveEvent(event)), 21 | err => dispatch(receiveErrors(err))) 22 | ); 23 | }; 24 | 25 | export const createEvent = event => dispatch => { 26 | return( 27 | EventAPIUtil.createEvent(event) 28 | .then(event => { 29 | return dispatch(receiveEvent(event)); 30 | }, 31 | err => dispatch(receiveErrors(err))) 32 | ); 33 | }; 34 | 35 | export const updateEvent = event => dispatch => { 36 | return( 37 | EventAPIUtil.updateEvent(event) 38 | .then(event => dispatch(receiveEvent(event)), 39 | err => dispatch(receiveErrors(err))) 40 | ); 41 | }; 42 | 43 | export const deleteEvent = id => dispatch => { 44 | return( 45 | EventAPIUtil.deleteEvent(id) 46 | .then(event => dispatch(removeEvent(event)), 47 | err => dispatch(receiveErrors(err))) 48 | ); 49 | }; 50 | 51 | export const searchEvents = (search) => dispatch => { 52 | return( 53 | EventAPIUtil.searchEvents(search) 54 | .then(events => dispatch(receiveEvents(events))) 55 | ); 56 | }; 57 | 58 | export const createRsvp = (eventId) => { 59 | return (dispatch) => { 60 | return EventAPIUtil.createRsvp(eventId) 61 | .then(event => dispatch(receiveEvent(event)), 62 | err => dispatch(receiveErrors(err))); 63 | }; 64 | }; 65 | 66 | export const deleteRsvp = (id) => { 67 | return (dispatch) => { 68 | return EventAPIUtil.deleteRsvp(id) 69 | .then(event => dispatch(receiveEvent(event)), 70 | err => dispatch(receiveErrors(err))); 71 | }; 72 | }; 73 | 74 | const receiveEvents = events => ({ 75 | type: RECEIVE_EVENTS, 76 | events 77 | }); 78 | 79 | const receiveEvent = event => ({ 80 | type: RECEIVE_EVENT, 81 | event 82 | }); 83 | 84 | const removeEvent = event => ({ 85 | type: REMOVE_EVENT, 86 | event 87 | }); 88 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/search.scss: -------------------------------------------------------------------------------- 1 | 2 | .search-container { 3 | width: 950px; 4 | display: flex; 5 | justify-content: center; 6 | flex-direction: row; 7 | align-items: center; 8 | margin: 0 auto; 9 | } 10 | 11 | .search-main { 12 | background-color: $off-white; 13 | } 14 | 15 | 16 | .search-bar { 17 | background-color: black; 18 | color: white; 19 | border-radius: 4px; 20 | margin: 20px 0; 21 | padding: 8px; 22 | height: 65px; 23 | width: 950px; 24 | display: flex; 25 | flex-direction: row; 26 | justify-content: space-between; 27 | border: 1px solid black; 28 | } 29 | .search-bar-left { 30 | display: flex; 31 | flex-direction: row; 32 | justify-content: center; 33 | align-items: center; 34 | margin: 10px; 35 | font-size: 20px; 36 | } 37 | 38 | .search-bar-right { 39 | display: flex; 40 | flex-direction: row; 41 | justify-content: center; 42 | margin-right: 20px; 43 | } 44 | 45 | .search-button { 46 | background: black; 47 | border: 1px solid white; 48 | margin: 10px 0; 49 | padding: 10px 50px; 50 | font-size: 20px; 51 | border-radius: 4px; 52 | 53 | } 54 | 55 | .search-input { 56 | width: 250px; 57 | color: black; 58 | background-color: white; 59 | margin-right: 10px; 60 | padding: 10px; 61 | border-radius: 4px; 62 | } 63 | 64 | 65 | 66 | .search-hidden { 67 | display: none; 68 | } 69 | 70 | .groups-search-results-items { 71 | display: flex; 72 | flex-direction: row; 73 | align-items: center; 74 | flex-wrap: wrap; 75 | justify-content: space-between; 76 | flex-wrap: wrap; 77 | } 78 | 79 | .events-search-results-items { 80 | width: 950px; 81 | align-items: center; 82 | margin-bottom: 20px; 83 | 84 | 85 | } 86 | 87 | 88 | .search-day-container > h1 { 89 | font-family: $labels-sans-serif; 90 | font-size: 14px; 91 | } 92 | 93 | #search-day { 94 | font-size: 16px; 95 | font-weight: bold; 96 | margin: 20px 0; 97 | } 98 | 99 | .day-events { 100 | display: flex; 101 | flex-direction: column; 102 | background-color: white; 103 | padding: 15px; 104 | border: 1px solid $off-grey; 105 | border-radius: 4px; 106 | } 107 | 108 | .day-events > ul:last-child { 109 | border-bottom: none; 110 | } 111 | 112 | 113 | .day-events > li { 114 | font-size: 20px; 115 | font-weight: 400; 116 | 117 | } 118 | 119 | 120 | .day-events > li > span { 121 | font-size: 16px; 122 | color: black; 123 | text-shadow: none; 124 | } 125 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 2 | # LetsMeet 3 | 4 | [LetsMeet link (heroku)][heroku] 5 | 6 | [Meetup link][meetup] 7 | 8 | 9 | [heroku]: https://www.heroku.com 10 | [meetup]: https://www.meetup.com 11 | 12 | 13 | ## Minimum Viable Product 14 | 15 | LetsMeet is a single page web application inspired by Meetup, built using Ruby on Rails and React/Redux. By the end of week 9, this web app will, at a minimum, satisfy the following criteria with smooth, bug-free navigation, adequate seed data, and sufficient CSS styling: 16 | 17 | - [ ] New account creation, login, and guest/demo login 18 | - [ ] A production README, replacing this README 19 | - [ ] Hosting on Heroku 20 | - [ ] Groups and joining groups 21 | - [ ] Events and RSVPs 22 | - [ ] Calendar (on group page) 23 | - [ ] Search by location and group info (name, description) 24 | - [ ] **BONUS**: Categories 25 | - [ ] **BONUS**: Calendar (for all groups in search results) 26 | 27 | ## Design Docs 28 | * [View Wireframes][wireframes] 29 | * [React Components][components] 30 | * [API endpoints][api-endpoints] 31 | * [Sample State][sample-state] 32 | * [DB Schema][schema] 33 | 34 | [wireframes]: ./wireframes/ 35 | [components]: ./component-hierarchy.md 36 | [api-endpoints]: ./api-endpoints.md 37 | [sample-state]: ./sample-state.md 38 | [schema]: ./schema.md 39 | 40 | ## Implementation Timeline 41 | 42 | ### Phase 1: Backend setup and Front End User Authentication (2 days) 43 | **Objective:** Functioning rails project with front-end Authentication. This includes a welcome page when there is no current user, sign up and sign in pages, and a homepage for signed in users. 44 | 45 | ### Phase 2: Groups model, API, and components (2 days) 46 | **Objective:** Groups can be created, read, edited (by creator of group), and destroyed (by creator of group) through the API. Users can also join groups. Creator of group can assign the group to one or more categories. 47 | 48 | ### Phase 3: Events model, API, and components (2 days) 49 | **Objective:** Events can be created, read, edited (by creator of event), and destroyed (by creator of event) through the API. Members of the group can RSVP to events. 50 | 51 | ### Phase 4: Calendars (1 day) 52 | **Objective:** Group show page has a calendar that lists all upcoming events. User can toggle between viewing both upcoming and previous events. 53 | 54 | ### Phase 5: Search by location and group information (name and description) (2 days) 55 | **Objective:** User can search by category, group name, group description, and location. 56 | 57 | ### Bonus features (TBD) 58 | 59 | - [ ] Calendar (for all groups in search results) 60 | - [ ] Group members can leave comments on events 61 | -------------------------------------------------------------------------------- /frontend/actions/group_actions.js: -------------------------------------------------------------------------------- 1 | import * as GroupAPIUtil from '../util/group_api_util'; 2 | import { receiveErrors, clearErrors} from './error_actions'; 3 | import { hashHistory } from 'react-router'; 4 | 5 | export const RECEIVE_GROUPS = "RECEIVE_GROUPS"; 6 | export const RECEIVE_GROUP = "RECEIVE_GROUP"; 7 | export const REMOVE_GROUP = "REMOVE_GROUP"; 8 | // export const RECEIVE_MEMBERSHIP = "RECEIVE_MEMBERSHIP"; 9 | // export const REMOVE_MEMBERSHIP = "REMOVE_MEMBERSHIP"; 10 | 11 | export const fetchGroups = () => dispatch => { 12 | return( 13 | GroupAPIUtil.fetchGroups(). 14 | then(groups => dispatch(receiveGroups(groups)), 15 | err => dispatch(receiveErrors(err))) 16 | ); 17 | }; 18 | 19 | export const fetchGroup = id => dispatch => { 20 | return( 21 | GroupAPIUtil.fetchGroup(id) 22 | .then(group => dispatch(receiveGroup(group)), 23 | err => dispatch(receiveErrors(err))) 24 | ); 25 | }; 26 | 27 | export const createGroup = group => dispatch => { 28 | return( 29 | GroupAPIUtil.createGroup(group) 30 | .then(group => dispatch(receiveGroup(group)), 31 | err => dispatch(receiveErrors(err))) 32 | ); 33 | }; 34 | 35 | export const updateGroup = group => dispatch => { 36 | return( 37 | GroupAPIUtil.updateGroup(group) 38 | .then(group => dispatch(receiveGroup(group)), 39 | err => dispatch(receiveErrors(err))) 40 | ); 41 | }; 42 | 43 | export const deleteGroup = id => dispatch => { 44 | return( 45 | GroupAPIUtil.deleteGroup(id) 46 | .then(group => dispatch(removeGroup(group)), 47 | err => dispatch(receiveErrors(err))) 48 | ); 49 | }; 50 | 51 | export const searchGroups = (search) => dispatch => { 52 | return( 53 | GroupAPIUtil.searchGroups(search) 54 | .then(groups => dispatch(receiveGroups(groups))) 55 | ); 56 | }; 57 | 58 | export const fetchGroupsByCategory = (category) => dispatch => { 59 | return( 60 | GroupAPIUtil.fetchGroupsByCategory(category) 61 | .then(groups => dispatch(receiveGroups(groups))) 62 | ); 63 | }; 64 | 65 | export const createMembership = (groupId, userId) => dispatch => { 66 | return( 67 | GroupAPIUtil.createMembership(groupId, userId) 68 | .then(group => dispatch(receiveGroup(group))) 69 | ); 70 | }; 71 | 72 | export const deleteMembership = id => dispatch => { 73 | return( 74 | GroupAPIUtil.deleteMembership(id) 75 | .then(group => dispatch(receiveGroup(group))) 76 | ); 77 | }; 78 | 79 | 80 | const receiveGroups = groups => ({ 81 | type: RECEIVE_GROUPS, 82 | groups 83 | }); 84 | 85 | const receiveGroup = group => ({ 86 | type: RECEIVE_GROUP, 87 | group 88 | }); 89 | 90 | const removeGroup = group => ({ 91 | type: REMOVE_GROUP, 92 | group 93 | }); 94 | -------------------------------------------------------------------------------- /frontend/components/groups/group_nav_bar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | import GroupShow from './group_show'; 4 | import EventShow from '../events/event_show'; 5 | import { createMembership, deleteMembership } from '../../actions/group_actions'; 6 | 7 | class GroupNavBar extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.navigateToEdit = this.navigateToEdit.bind(this); 12 | this.toggleJoinButton = this.toggleJoinButton.bind(this); 13 | this.toggleEditButton = this.toggleEditButton.bind(this); 14 | } 15 | 16 | navigateToEdit() { 17 | this.props.router.push(`/groups/${this.props.group.id}/edit`); 18 | } 19 | 20 | toggleJoinButton() { 21 | let members = this.props.group.members; 22 | let memberIDs = []; 23 | if(this.props.group.members) { 24 | members = this.props.group.members; 25 | members.forEach( (member) => { 26 | memberIDs.push(member.id); 27 | }); 28 | } 29 | if(this.props.currentUser && memberIDs.includes(this.props.currentUser.id)) { 30 | return( 31 | 32 | ); } else { 33 | return( 34 | 35 | ); 36 | } 37 | } 38 | 39 | toggleEditButton() { 40 | if (this.props.group.creator_id === this.props.currentUser.id) { 41 | return( 42 | 43 | ); 44 | } else { 45 | return ( 46 |
    {this.toggleJoinButton()}
    47 | ); 48 | } 49 | } 50 | 51 | addMember () { 52 | this.props.createMembership({group_id: this.props.group.id, member_id: this.props.currentUser.id}); 53 | } 54 | 55 | 56 | removeMember () { 57 | this.props.deleteMembership(({group_id: this.props.group.id})); 58 | } 59 | 60 | 61 | render() { 62 | if(this.props.group) { 63 | return( 64 |
    65 |
    66 |

    {this.props.group.name}

    67 |
    68 |
    69 |
    70 | Home 71 | Members 72 |
    73 |
    74 | {this.toggleEditButton()} 75 |
    76 |
    77 |
    78 | ); 79 | } else { 80 | return

    What

    ; 81 | } 82 | } 83 | } 84 | 85 | export default withRouter(GroupNavBar); 86 | -------------------------------------------------------------------------------- /frontend/components/root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | 4 | // react router 5 | import { Router, Route, IndexRoute, hashHistory } from 'react-router'; 6 | 7 | //react components 8 | import App from './app'; 9 | import SessionFormContainer from './session_form/session_form_container'; 10 | import NavBarContainer from './nav_bar/nav_bar_container'; 11 | import FooterContainer from './footer/footer_container'; 12 | import WelcomePage from './welcome_page/welcome_page'; 13 | import GroupFormContainer from './groups/group_form_container'; 14 | import GroupShowContainer from './groups/group_show_container'; 15 | import EditGroupContainer from './groups/edit_group_container'; 16 | import EventShowContainer from './events/event_show_container'; 17 | import EventFormContainer from './events/event_form_container'; 18 | import EditEventContainer from './events/edit_event_container'; 19 | import GroupBody from './groups/group_body'; 20 | import GroupMembers from './groups/group_members'; 21 | import HomePage from './welcome_page/home_page'; 22 | import SearchBarContainer from './search/search_bar_container'; 23 | 24 | const Root = ({ store }) => { 25 | const _redirectIfLoggedIn = (nextState, replace) => { 26 | const currentUser = store.getState().session.currentUser; 27 | if (currentUser) { 28 | replace('/'); 29 | } 30 | }; 31 | 32 | const _ensureLoggedIn = (nextState, replace) => { 33 | const currentUser = store.getState().session.currentUser; 34 | if (!currentUser) { 35 | replace('/login'); 36 | } 37 | }; 38 | return( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ); 60 | }; 61 | 62 | export default Root; 63 | -------------------------------------------------------------------------------- /frontend/components/search/search_bar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter, hashHistory} from 'react-router'; 3 | import GroupSearchResults from './group_search_results'; 4 | import EventSearchResults from './event_search_results'; 5 | 6 | class SearchBar extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { search: "", eventsResultsDisplay: false }; 10 | this.handleUpdateSearch = this.handleUpdateSearch.bind(this); 11 | this.handleSubmit = this.handleSubmit.bind(this); 12 | this.handleKeyPress = this.handleKeyPress.bind(this); 13 | this.toggleEventsResults = this.toggleEventsResults.bind(this); 14 | this.toggleGroupsResults = this.toggleGroupsResults.bind(this); 15 | this.showEventsResults = this.showEventsResults.bind(this); 16 | this.showGroupsResults = this.showGroupsResults.bind(this); 17 | } 18 | 19 | handleUpdateSearch(e) { 20 | this.setState({ search: e.target.value }); 21 | } 22 | 23 | handleSubmit() { 24 | if (this.state.search === "") { 25 | this.props.fetchGroups(); 26 | this.props.fetchEvents(); 27 | } else { 28 | this.props.searchGroups(this.state.search); 29 | this.props.searchEvents(this.state.search); 30 | } 31 | } 32 | 33 | handleKeyPress(e) { 34 | if (e.key === "Enter") { 35 | this.handleSubmit(); 36 | } 37 | } 38 | 39 | showEventsResults() { 40 | this.setState({ eventsResultsDisplay: true }); 41 | } 42 | 43 | showGroupsResults() { 44 | this.setState({ eventsResultsDisplay: false }); 45 | } 46 | 47 | 48 | toggleEventsResults() { 49 | return this.state.eventsResultsDisplay === true ? "events-search-results-items" : "search-hidden"; 50 | } 51 | 52 | toggleGroupsResults() { 53 | return this.state.eventsResultsDisplay === true ? "search-hidden" : "groups-search-results-items"; 54 | } 55 | 56 | 57 | render() { 58 | 59 | return( 60 |
    61 |
    62 |
    63 |
    64 | 65 | 66 |
    67 |
    68 | 69 | 70 |
    71 |
    72 |
    73 | 74 | 75 |
    76 |
    77 |
    78 | ); 79 | } 80 | } 81 | 82 | export default SearchBar; 83 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/groups.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .new-group-container { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | margin: 0 auto; 8 | } 9 | 10 | .step:first-of-type { 11 | border: none; 12 | } 13 | 14 | .step { 15 | border-top: 1px solid $off-grey; 16 | margin-bottom: 20px; 17 | letter-spacing: 2px; 18 | display: flex; 19 | flex-direction: column; 20 | 21 | } 22 | 23 | #member-header { 24 | font-size: 24px; 25 | height: 24px; 26 | font-weight: bold; 27 | 28 | } 29 | 30 | .member-pic { 31 | width: 40px; 32 | height: 40px; 33 | border-radius: 15px; 34 | } 35 | 36 | .group-member-container { 37 | border: 1px solid $off-grey; 38 | border-bottom: none; 39 | margin-left: 10px; 40 | width: 768px; 41 | margin-bottom: 10px; 42 | border-radius: 4px; 43 | } 44 | 45 | .group-member-list { 46 | display: flex; 47 | flex-direction: column; 48 | background: white; 49 | border-bottom: 1px solid $off-grey; 50 | 51 | } 52 | 53 | 54 | 55 | .group-member-list > ul { 56 | display: flex; 57 | flex-direction: row; 58 | padding: 12px; 59 | justify-content: flex-start; 60 | align-items: center; 61 | margin-bottom: 0; 62 | background: white; 63 | } 64 | 65 | .member-name { 66 | font-size: 14px; 67 | margin-left: 20px; 68 | font-family: $labels-sans-serif; 69 | font-weight: bold; 70 | } 71 | 72 | 73 | 74 | .step-button { 75 | background-color: $main-red; 76 | font-size: 16px; 77 | border-radius: 3px; 78 | margin: 10px 0; 79 | padding: 10px 0; 80 | color: white; 81 | font-weight: 400; 82 | width: 15%; 83 | text-align: center; 84 | } 85 | 86 | .step-button:hover { 87 | background-color: $main-red-hover; 88 | } 89 | 90 | .cancel-button { 91 | background-color: $off-grey; 92 | color: white; 93 | font-size: 12px; 94 | border-radius: 3px; 95 | padding: 10px; 96 | } 97 | 98 | .cancel-button:hover { 99 | background-color: grey; 100 | color: white; 101 | font-size: 12px; 102 | border-radius: 3px; 103 | } 104 | 105 | .group-form-header { 106 | color: white; 107 | background-color: $main-red; 108 | display: flex; 109 | flex-direction: column; 110 | width: 100%; 111 | align-items: center; 112 | justify-content: center; 113 | } 114 | 115 | .group-form-header > h3 { 116 | font-size: 32px; 117 | } 118 | 119 | .group-form-header > h4 { 120 | font-size: 22px; 121 | } 122 | 123 | .new-group-form { 124 | display: flex; 125 | flex-direction: column; 126 | justify-content: center; 127 | align-items: center; 128 | align-text: left; 129 | padding: 20px 50px; 130 | } 131 | 132 | .group-form-question { 133 | align-items: center; 134 | display: flex; 135 | font-weight: 300; 136 | font-size: 28px; 137 | padding-top: 20px; 138 | padding-bottom: 10px; 139 | } 140 | .step input { 141 | display: flex; 142 | background-color: $off-white; 143 | font-size: 16px; 144 | line-height: 28px; 145 | padding: 5px; 146 | font-weight: 400px; 147 | box-sizing: border-box; 148 | border: 1px solid #DDD; 149 | letter-spacing: 2px; 150 | width: 80%; 151 | 152 | 153 | } 154 | -------------------------------------------------------------------------------- /frontend/components/search/event_search_results.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { fetchEvents } from '../../actions/event_actions'; 4 | import { Link, withRouter, hashHistory} from 'react-router'; 5 | import { eventsArray } from '../../reducers/selectors'; 6 | 7 | class EventsSearchResults extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = {events: [] }; 11 | } 12 | 13 | componentDidMount() { 14 | this.props.fetchEvents(); 15 | } 16 | 17 | componentWillReceiveProps(nextProps) { 18 | this.setState({ events: nextProps.events }); 19 | } 20 | 21 | compareDateValues(a, b) { 22 | return b - a; 23 | } 24 | 25 | render() { 26 | if(!this.props.events) { 27 | return

    Loading

    ; 28 | } 29 | let nextSevenDays = {}; 30 | for(let i = 0; i < 7; i++) { 31 | let day = new Date().getDate() + i; 32 | let dateItem = new Date(new Date().setDate(day)).toDateString(); 33 | nextSevenDays[dateItem] = []; 34 | } 35 | 36 | 37 | 38 | let eventList = []; 39 | let currentDate = Date.now(); 40 | 41 | 42 | this.state.events.forEach((event) => { 43 | let date = new Date(event.date).toLocaleDateString(); 44 | let time = new Date(event.time).toLocaleTimeString(); 45 | let eventDateValue = new Date(date + " " + time).valueOf(); 46 | let lastDate = new Date(new Date().setDate(new Date().getDate() + 6)); 47 | 48 | let dateStr = new Date(event.date).toDateString(); 49 | 50 | if (eventDateValue > currentDate && new Date(dateStr)<= lastDate) { 51 | let eventItem = ( 52 |
  • 53 | 63 |
  • ); 64 | 65 | nextSevenDays[dateStr].push(eventItem); 66 | } 67 | }); 68 | 69 | let fullCalendar = []; 70 | 71 | Object.keys(nextSevenDays).forEach((day) => { 72 | let dayItem = ( 73 |
    74 |

    {day}

    75 | {nextSevenDays[day]} 76 |
    77 | ); 78 | fullCalendar.push(dayItem); 79 | }); 80 | 81 | 82 | 83 | 84 | return ( 85 | 88 | ); 89 | } 90 | } 91 | 92 | const mapStateToProps = (state) => { 93 | return { 94 | events: eventsArray(state.events), 95 | }; 96 | }; 97 | 98 | const mapDispatchToProps = (dispatch) => { 99 | return { 100 | fetchEvents: () => dispatch(fetchEvents()) 101 | }; 102 | }; 103 | 104 | export default connect(mapStateToProps, mapDispatchToProps)(EventsSearchResults); 105 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 9.1 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On OS X with Homebrew: 6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config 7 | # On OS X with MacPorts: 8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config 9 | # On Windows: 10 | # gem install pg 11 | # Choose the win32 build. 12 | # Install PostgreSQL and put its /bin directory on your path. 13 | # 14 | # Configure Using Gemfile 15 | # gem 'pg' 16 | # 17 | default: &default 18 | adapter: postgresql 19 | encoding: unicode 20 | # For details on connection pooling, see rails configuration guide 21 | # http://guides.rubyonrails.org/configuring.html#database-pooling 22 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 23 | 24 | development: 25 | <<: *default 26 | database: LetsMeet_development 27 | 28 | # The specified database role being used to connect to postgres. 29 | # To create additional roles in postgres see `$ createuser --help`. 30 | # When left blank, postgres will use the default role. This is 31 | # the same name as the operating system user that initialized the database. 32 | #username: LetsMeet 33 | 34 | # The password associated with the postgres role (username). 35 | #password: 36 | 37 | # Connect on a TCP socket. Omitted by default since the client uses a 38 | # domain socket that doesn't need configuration. Windows does not have 39 | # domain sockets, so uncomment these lines. 40 | #host: localhost 41 | 42 | # The TCP port the server listens on. Defaults to 5432. 43 | # If your server runs on a different port number, change accordingly. 44 | #port: 5432 45 | 46 | # Schema search path. The server defaults to $user,public 47 | #schema_search_path: myapp,sharedapp,public 48 | 49 | # Minimum log levels, in increasing order: 50 | # debug5, debug4, debug3, debug2, debug1, 51 | # log, notice, warning, error, fatal, and panic 52 | # Defaults to warning. 53 | #min_messages: notice 54 | 55 | # Warning: The database defined as "test" will be erased and 56 | # re-generated from your development database when you run "rake". 57 | # Do not set this db to the same as development or production. 58 | test: 59 | <<: *default 60 | database: LetsMeet_test 61 | 62 | # As with config/secrets.yml, you never want to store sensitive information, 63 | # like your database password, in your source code. If your source code is 64 | # ever seen by anyone, they now have access to your database. 65 | # 66 | # Instead, provide the password as a unix environment variable when you boot 67 | # the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database 68 | # for a full rundown on how to provide these environment variables in a 69 | # production deployment. 70 | # 71 | # On Heroku and other platform providers, you may have a full connection URL 72 | # available as an environment variable. For example: 73 | # 74 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" 75 | # 76 | # You can use this database configuration with: 77 | # 78 | # production: 79 | # url: <%= ENV['DATABASE_URL'] %> 80 | # 81 | production: 82 | <<: *default 83 | database: LetsMeet_production 84 | username: LetsMeet 85 | password: <%= ENV['LETSMEET_DATABASE_PASSWORD'] %> 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LetsMeet 2 | 3 | Check out a live version of LetsMeet here: [LetsMeet Live][letsmeet] 4 | 5 | [letsmeet]: https://lets-meet-app.herokuapp.com/ 6 | 7 | LetsMeet is a full-stack web application inspired by Meetup. The application allows users can join groups that they are interested in and participate in events. It utilizes Ruby on Rails with a PostgreSQL database on the backend, and React.js with a Redux architectural framework on the frontend. 8 | Go on the website, find people who share similar passions, and [LetsMeet!][letsmeet] 9 | 10 | ## Features and Implementation 11 | 12 | ### Groups and Events show pages 13 | 14 | Group Show Page 15 | 16 | The show pages for individual groups and events contain a great deal of functionality. Both types of pages contain a Group Side Bar with basic information about the group, as well as a Group Navigation Bar. By toggling different buttons on the Group Navigation Bar, the user can view different components, such as a list of the Group's upcoming events and a list of the Group's members. These are rendered in the main show area on the page. Depending on the type of user that is viewing the page, a different set of buttons will render on the right side of the Group Navigation Bar (for example, a user who is not a member of a Group would see a button to join the group, whereas a creator of a group would see a button to edit the group). 17 | 18 | ```JavaScript 19 | switch(this.props.memberType) { 20 | case 'owner': 21 | return ( 22 | 26 | ); 27 | case 'member': 28 | return ( 29 | 33 | ); 34 | case 'nonmember': 35 | return ( 36 | 39 | ); 40 | } 41 | ``` 42 | 43 | The events route is nested under its corresponding group's route, allowing the main event show component to render as a separate component, without having to rerender the Group Nav and Side Bars. The Redux cycle guarantees that these various components can rendered separately on the screen while simultaneously allowing for a rich and seamless user experience. 44 | 45 | ### Search Bar 46 | 47 | Group Show Page 48 | 49 | Upon logging in, users are directed to the search page. Here, users can search for groups and upcoming events. Search is implemented using the pg_search gem, which searches the names and descriptions of the events and groups tables in the database. The search returns groups and events that match the input that the user entered in the search bar. Users can toggle between the Groups button and Calendar button to view either groups or a list of upcoming events that match the specified criteria. Clicking on an item on the search results page directs the user to the respective Group or Event show page. 50 | 51 | 52 | ### Future Directions for the Project 53 | In addition to the features I have already implemented, there are a couple more features that I would like to add to this project in the future. The next steps are outlined below. 54 | 55 | * ***Location-based Search:*** Users will be able to search for groups and events based on their location. This feature will be implemented using the Google Geocoder API. 56 | * ***Live Chat:*** Users will be able to communicate with each other in real time via a messaging feature. 57 | * ***Custom Group and Event Recommendations:*** Users will be able to view Group and Event recommendations based on their search activity. 58 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Disable serving static files from the `/public` folder by default since 18 | # Apache or NGINX already handles this. 19 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 20 | 21 | # Compress JavaScripts and CSS. 22 | config.assets.js_compressor = :uglifier 23 | # config.assets.css_compressor = :sass 24 | 25 | # Do not fallback to assets pipeline if a precompiled asset is missed. 26 | config.assets.compile = false 27 | 28 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 29 | 30 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 31 | # config.action_controller.asset_host = 'http://assets.example.com' 32 | 33 | # Specifies the header that your server uses for sending files. 34 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 35 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 36 | 37 | # Mount Action Cable outside main process or domain 38 | # config.action_cable.mount_path = nil 39 | # config.action_cable.url = 'wss://example.com/cable' 40 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 41 | 42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 43 | # config.force_ssl = true 44 | 45 | # Use the lowest log level to ensure availability of diagnostic information 46 | # when problems arise. 47 | config.log_level = :debug 48 | 49 | # Prepend all log lines with the following tags. 50 | config.log_tags = [ :request_id ] 51 | 52 | # Use a different cache store in production. 53 | # config.cache_store = :mem_cache_store 54 | 55 | # Use a real queuing backend for Active Job (and separate queues per environment) 56 | # config.active_job.queue_adapter = :resque 57 | # config.active_job.queue_name_prefix = "LetsMeet_#{Rails.env}" 58 | config.action_mailer.perform_caching = false 59 | 60 | # Ignore bad email addresses and do not raise email delivery errors. 61 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 62 | # config.action_mailer.raise_delivery_errors = false 63 | 64 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 65 | # the I18n.default_locale when a translation cannot be found). 66 | config.i18n.fallbacks = true 67 | 68 | # Send deprecation notices to registered listeners. 69 | config.active_support.deprecation = :notify 70 | 71 | # Use default logging formatter so that PID and timestamp are not suppressed. 72 | config.log_formatter = ::Logger::Formatter.new 73 | 74 | # Use a different logger for distributed setups. 75 | # require 'syslog/logger' 76 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 77 | 78 | if ENV["RAILS_LOG_TO_STDOUT"].present? 79 | logger = ActiveSupport::Logger.new(STDOUT) 80 | logger.formatter = config.log_formatter 81 | config.logger = ActiveSupport::TaggedLogging.new(logger) 82 | end 83 | 84 | # Do not dump schema after migrations. 85 | config.active_record.dump_schema_after_migration = false 86 | end 87 | -------------------------------------------------------------------------------- /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 | 5 | 6 | .red-button { 7 | background-color: $main-red; 8 | color: white; 9 | padding: 6px 8px; 10 | border-radius: 3px; 11 | margin-bottom: 16px; 12 | font-family: $sans-serif; 13 | font-size: 16px; 14 | text-align: center; 15 | cursor: pointer; 16 | font-weight: 500; 17 | } 18 | 19 | .red-button:hover { 20 | background-color: $main-red-hover; 21 | } 22 | 23 | .main-nav { 24 | display: flex; 25 | width: 100%; 26 | min-width: 900px; 27 | align-items: center; 28 | padding: 10px 5px; 29 | border-bottom: 1px solid $off-grey; 30 | 31 | 32 | } 33 | 34 | .right-nav{ 35 | width: 40%; 36 | } 37 | 38 | .left-nav { 39 | width: 40%; 40 | } 41 | 42 | .center-nav { 43 | width: 20%; 44 | } 45 | 46 | .left-nav > ul, 47 | .right-nav > ul, 48 | .center-nav > ul { 49 | display: flex; 50 | align-items: center; 51 | } 52 | 53 | .left-nav > ul { 54 | justify-content: flex-start; 55 | } 56 | 57 | .right-nav > ul { 58 | justify-content: flex-end; 59 | } 60 | 61 | #user-greeting { 62 | cursor: auto; 63 | } 64 | 65 | .center-nav > ul { 66 | justify-content: center; 67 | } 68 | 69 | .left-nav > ul > li, 70 | .right-nav > ul > li { 71 | font-family: $labels-sans-serif; 72 | font-size: 16px; 73 | font-weight: bold; 74 | margin: 0 15px; 75 | cursor: pointer; 76 | text-align: center; 77 | 78 | } 79 | // 80 | .right-nav > ul > li:last-of-type { 81 | background-color: $main-red; 82 | color: white; 83 | padding: 8px 15px; 84 | border-radius: 3px; 85 | 86 | font-size: 16px; 87 | text-align: center; 88 | font-weight: 500; 89 | } 90 | 91 | .right-nav > ul > li:last-of-type:hover { 92 | background-color: $main-red-hover; 93 | } 94 | 95 | 96 | 97 | 98 | 99 | 100 | #logo { 101 | color: $red-text; 102 | 103 | border-radius: 20px; 104 | padding: 5px 12px; 105 | font-size: 28px; 106 | font-weight: bold; 107 | align-text: center; 108 | font-family: $logo-font; 109 | border: 2px solid $red-text; 110 | } 111 | 112 | 113 | 114 | 115 | .session-form { 116 | text-align: left; 117 | display: absolute; 118 | background-color: 'red'; 119 | border-top: 1px solid $off-grey; 120 | border-right: 1px solid $off-grey; 121 | border-left: 1px solid $off-grey; 122 | width: 450px; 123 | padding: 20px 20px; 124 | margin: 0 auto; 125 | background-color: white; 126 | } 127 | 128 | .session-form:last-of-type { 129 | border-bottom: 1px solid $off-grey; 130 | margin-bottom: 160px; 131 | } 132 | 133 | .session-form:first-of-type { 134 | margin-top: 60px; 135 | } 136 | .entire-session-form { 137 | background-color: $off-white; 138 | display: flex; 139 | flex-direction: column; 140 | 141 | } 142 | .session-form label { 143 | background-color: white; 144 | font-size: 13px; 145 | font-weight: bold; 146 | font-family: $labels-sans-serif; 147 | 148 | // padding: 10px 10px; 149 | 150 | } 151 | 152 | .session-form input[type=text], input[type=password]{ 153 | background-color: #f6ffcc; 154 | font-size: 16px; 155 | box-sizing: border-box; 156 | width: 400px; 157 | border: 1px solid $off-grey; 158 | margin-bottom: 30px; 159 | margin-top: 10px; 160 | line-height: 32px; 161 | padding-left: 5px; 162 | 163 | } 164 | 165 | 166 | .intro-text { 167 | font-size: 16px; 168 | font-color: black; 169 | font-weight: 400; 170 | text-align: left; 171 | } 172 | 173 | .intro-text h2 { 174 | color: $red-text; 175 | font-weight: 400; 176 | font-size: 45px; 177 | margin-bottom: 10px; 178 | text-align: left; 179 | } 180 | 181 | .intro-text span { 182 | font-family: $labels-sans-serif; 183 | font-weight: bold; 184 | font-size: 14px; 185 | } 186 | 187 | 188 | 189 | 190 | 191 | .link { 192 | color: $blue; 193 | } 194 | 195 | input[type=text]:focus { 196 | border-color: $blue; 197 | background-color: white; 198 | } 199 | input[type=password]:focus { 200 | border-color: $blue; 201 | background-color: white; 202 | } 203 | .errors { 204 | font-size: 12px; 205 | color: $red-text; 206 | } 207 | -------------------------------------------------------------------------------- /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: 20170511030806) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "plpgsql" 17 | 18 | create_table "categories", force: :cascade do |t| 19 | t.string "title", null: false 20 | t.datetime "created_at", null: false 21 | t.datetime "updated_at", null: false 22 | t.index ["title"], name: "index_categories_on_title", using: :btree 23 | end 24 | 25 | create_table "category_groups", force: :cascade do |t| 26 | t.integer "category_id", null: false 27 | t.integer "group_id", null: false 28 | t.datetime "created_at", null: false 29 | t.datetime "updated_at", null: false 30 | end 31 | 32 | create_table "events", force: :cascade do |t| 33 | t.string "name", null: false 34 | t.text "description", null: false 35 | t.integer "group_id", null: false 36 | t.string "location_name", null: false 37 | t.date "date", null: false 38 | t.datetime "created_at", null: false 39 | t.datetime "updated_at", null: false 40 | t.integer "organizer_id", null: false 41 | t.time "time" 42 | t.string "location_address" 43 | t.index ["group_id"], name: "index_events_on_group_id", using: :btree 44 | t.index ["name"], name: "index_events_on_name", using: :btree 45 | end 46 | 47 | create_table "groups", force: :cascade do |t| 48 | t.string "name", null: false 49 | t.date "founded_date", null: false 50 | t.string "category", null: false 51 | t.integer "creator_id", null: false 52 | t.text "description", null: false 53 | t.string "location", null: false 54 | t.datetime "created_at", null: false 55 | t.datetime "updated_at", null: false 56 | t.string "image_url" 57 | t.index ["creator_id"], name: "index_groups_on_creator_id", using: :btree 58 | t.index ["name"], name: "index_groups_on_name", unique: true, using: :btree 59 | end 60 | 61 | create_table "memberships", force: :cascade do |t| 62 | t.integer "member_id", null: false 63 | t.integer "group_id", null: false 64 | t.datetime "created_at", null: false 65 | t.datetime "updated_at", null: false 66 | t.index ["group_id", "member_id"], name: "index_memberships_on_group_id_and_member_id", unique: true, using: :btree 67 | end 68 | 69 | create_table "rsvps", force: :cascade do |t| 70 | t.integer "attendee_id", null: false 71 | t.integer "event_id", null: false 72 | t.datetime "created_at", null: false 73 | t.datetime "updated_at", null: false 74 | t.index ["attendee_id", "event_id"], name: "index_rsvps_on_attendee_id_and_event_id", unique: true, using: :btree 75 | end 76 | 77 | create_table "users", force: :cascade do |t| 78 | t.string "password_digest", null: false 79 | t.string "session_token", null: false 80 | t.string "username", null: false 81 | t.string "email", null: false 82 | t.string "location" 83 | t.string "image_url" 84 | t.text "bio" 85 | t.datetime "created_at", null: false 86 | t.datetime "updated_at", null: false 87 | t.string "image_file_name" 88 | t.string "image_content_type" 89 | t.integer "image_file_size" 90 | t.datetime "image_updated_at" 91 | t.string "profile_pic_url" 92 | t.index ["email"], name: "index_users_on_email", unique: true, using: :btree 93 | t.index ["session_token"], name: "index_users_on_session_token", unique: true, using: :btree 94 | t.index ["username"], name: "index_users_on_username", unique: true, using: :btree 95 | end 96 | 97 | end 98 | -------------------------------------------------------------------------------- /frontend/components/groups/group_show.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter, hashHistory} from 'react-router'; 3 | import GroupNavBar from './group_nav_bar'; 4 | import GroupSideBar from './group_side_bar'; 5 | import GroupBody from './group_body'; 6 | import GroupMembers from './group_members'; 7 | 8 | 9 | 10 | class GroupShow extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | this.handleLeave = this.handleLeave.bind(this); 14 | this.handleJoin = this.handleJoin.bind(this); 15 | this.homeSelector = this.homeSelector.bind(this); 16 | this.memberSelector = this.memberSelector.bind(this); 17 | this.groupButtons = this.groupButtons.bind(this); 18 | 19 | } 20 | 21 | homeSelector() { 22 | return this.props.location.pathname.slice(-7) !== "members" ? "left-group-nav-selected" : ""; 23 | } 24 | 25 | memberSelector() { 26 | return this.props.location.pathname.slice(-7) === "members" ? "left-group-nav-selected" : ""; 27 | } 28 | 29 | 30 | componentDidMount() { 31 | this.props.fetchGroup(this.props.groupId); 32 | } 33 | 34 | componentWillReceiveProps(nextProps) { 35 | if (this.props.groupId && nextProps.params.groupId !== this.props.groupId.toString()) { 36 | this.props.fetchGroup(nextProps.params.groupId); 37 | } 38 | } 39 | 40 | handleLeave() { 41 | this.props.deleteMembership(this.props.groupId).then(() => { 42 | this.props.router.push(`groups/${this.props.groupId}`); 43 | }); 44 | } 45 | handleJoin() { 46 | if(!this.props.currentUser) { 47 | this.props.router.push("/login"); 48 | } else { 49 | this.props.createMembership(this.props.groupId, this.props.currentUser.id).then(() => { 50 | this.props.router.push(`groups/${this.props.groupId}`); 51 | }); 52 | } 53 | } 54 | 55 | groupButtons() { 56 | let editGroupLink =
  • Edit group
  • ; 57 | let createEventLink =
  • Create Event
  • ; 58 | let joinButton =
  • ; 59 | let leaveGroupButton =
  • ; 60 | 61 | switch(this.props.memberType) { 62 | case 'owner': 63 | return ( 64 | 68 | ); 69 | case 'member': 70 | return ( 71 | 75 | ); 76 | case 'nonmember': 77 | return ( 78 | 81 | ); 82 | } 83 | } 84 | 85 | render() { 86 | if(!this.props.group) { 87 | return

    Loading

    ; 88 | } 89 | let membersLink = Members; 90 | let homeLink = Home; 91 | 92 | if(this.props.group) { 93 | return( 94 |
    95 |
    96 |
    97 |

    {this.props.group.name}

    98 |
    99 |
    100 |
    101 |
      102 |
    • {homeLink}
    • 103 |
    • {membersLink}
    • 104 |
    105 |
    106 |
    107 |
      108 | { this.groupButtons() } 109 |
    110 |
    111 |
    112 |
    113 | 114 |
    115 | 116 | {this.props.children} 117 |
    118 |
    119 | ); 120 | } else { 121 | return

    Loading

    ; 122 | } 123 | } 124 | } 125 | 126 | 127 | export default GroupShow; 128 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/events.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/Events controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | 5 | .group-event-item { 6 | display: flex; 7 | flex-direction: column; 8 | background-color: white; 9 | padding: 15px; 10 | border-bottom: 1px solid $off-grey; 11 | } 12 | 13 | 14 | .group-event-item > ul:last-child { 15 | border-bottom: none; 16 | } 17 | 18 | 19 | .group-event-item > li { 20 | font-size: 20px; 21 | font-weight: 400; 22 | 23 | } 24 | 25 | 26 | .group-event-item > li > span { 27 | font-size: 16px; 28 | color: black; 29 | text-shadow: none; 30 | } 31 | 32 | .event-list-container { 33 | 34 | } 35 | 36 | #hello { 37 | font-weight: 600; 38 | margin-bottom: 5px; 39 | } 40 | #hello:hover { 41 | color: blue; 42 | } 43 | 44 | #event-description { 45 | margin-left: 10px; 46 | border-radius: 4px; 47 | background: white; 48 | border: 1px solid $off-grey; 49 | padding: 10px; 50 | line-height: 22px; 51 | } 52 | 53 | .event-list-buttons { 54 | display: flex; 55 | flex-direction: row; 56 | padding: 15px; 57 | background: white; 58 | border-bottom: 1px solid $off-grey; 59 | 60 | } 61 | 62 | .event-list-buttons > li { 63 | color: #000077; 64 | margin-right: 20px; 65 | 66 | } 67 | 68 | .event-list-buttons > li:hover { 69 | cursor: pointer; 70 | 71 | } 72 | 73 | 74 | .selected { 75 | font-weight: bold; 76 | border-bottom: 3px solid; 77 | color: #000077; 78 | } 79 | 80 | 81 | .rsvp-button { 82 | background: $main-red; 83 | color: white; 84 | padding: 6px 8px; 85 | font-size: 16px; 86 | margin-left: 60px; 87 | font-weight: bold; 88 | border-radius: 3px; 89 | 90 | } 91 | 92 | .event-show-right-bar { 93 | width: 200px; 94 | border: 1px solid $off-grey; 95 | padding: 10px; 96 | margin-left: 10px; 97 | border-radius: 4px; 98 | margin-bottom: 20px; 99 | background: white; 100 | } 101 | 102 | .rsvp-container { 103 | font-size: 14px; 104 | display: flex; 105 | flex-direction: row; 106 | 107 | } 108 | 109 | .rsvp-container h2 { 110 | font-size: 24px; 111 | font-weight: 900; 112 | } 113 | 114 | .rsvp-container h3 { 115 | font-size: 15px; 116 | } 117 | 118 | .rsvp-mini { 119 | display: flex; 120 | flex-direction: row; 121 | 122 | 123 | } 124 | 125 | .event-show-content-main { 126 | width: 780px; 127 | text-align: left; 128 | border: 1px solid $off-grey; 129 | border-radius: 4px; 130 | padding: 20px; 131 | background: white; 132 | margin-left: 10px; 133 | margin-bottom: 20px; 134 | font-size: 16px; 135 | line-height: 20px; 136 | } 137 | 138 | 139 | .event-show-content-main > p { 140 | font-size: 16px; 141 | line-height: 19px; 142 | } 143 | 144 | .event-member-list { 145 | font-family: $labels-sans-serif; 146 | font-weight: bold; 147 | list-style: none; 148 | font-size: 14px; 149 | display: flex; 150 | flex-direction: row; 151 | align-items: center; 152 | 153 | img { 154 | width: 40px; 155 | height: 40px; 156 | } 157 | 158 | padding-right: 8px; 159 | 160 | } 161 | 162 | .event-member-list li { 163 | padding-bottom: 15px; 164 | } 165 | 166 | .event-member-list li:first-of-type { 167 | padding-right: 10px; 168 | } 169 | 170 | .event-show-date-loc { 171 | display: flex; 172 | flex-direction: row; 173 | font-size: 20px; 174 | font-weight: 500; 175 | font-weight: bold; 176 | 177 | } 178 | 179 | 180 | .event-show-date-loc > span{ 181 | margin-right: 10px; 182 | 183 | } 184 | 185 | .event-show-date-loc-info { 186 | display: flex; 187 | flex-direction: column; 188 | margin-bottom: 15px; 189 | font-size: 20px; 190 | } 191 | 192 | .event-show-date-icon { 193 | font-size: 20px; 194 | margin-right: 10px; 195 | } 196 | 197 | .event-show-loc-icon { 198 | margin-right: 15px; 199 | } 200 | .event-show-date-loc-info > li { 201 | margin-left: 10px; 202 | font-size: 20px; 203 | } 204 | 205 | .event-show-date-loc-info > li:last-child{ 206 | font-size: 16px; 207 | } 208 | 209 | .event-show-time-address{ 210 | font-size: 16px; 211 | } 212 | 213 | .event-show-time-address > span{ 214 | margin-left: 25px; 215 | } 216 | 217 | .members-container { 218 | h2 { 219 | font-weight: bold; 220 | font-size: 16px; 221 | margin: 20px 0; 222 | } 223 | } 224 | 225 | 226 | .event-show-content-container { 227 | width: 745px; 228 | display: flex; 229 | flex-direction: row; 230 | 231 | } 232 | -------------------------------------------------------------------------------- /frontend/components/groups/edit_group_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | import { hashHistory } from 'react-router'; 4 | 5 | class EditGroupForm extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | id: this.props.group.id, 11 | name: this.props.group.name, 12 | category: this.props.group.category, 13 | location: this.props.group.location, 14 | description: this.props.group.description 15 | }; 16 | this.handleSubmit = this.handleSubmit.bind(this); 17 | this.handleDelete = this.handleDelete.bind(this); 18 | } 19 | 20 | update(field) { 21 | return e => this.setState({ 22 | [field]: e.target.value 23 | }); 24 | } 25 | 26 | componentDidMount() { 27 | this.props.fetchGroup(this.props.groupId); 28 | } 29 | 30 | componentWillReceiveProps(newProps) { 31 | if (this.props.group !== newProps) { 32 | this.setState({ 33 | id: newProps.group.id, 34 | name: newProps.group.name, 35 | description: newProps.group.description, 36 | location: newProps.group.location, 37 | category: newProps.group.category 38 | }); 39 | } 40 | } 41 | 42 | handleDelete(e) { 43 | e.preventDefault(); 44 | this.props.deleteGroup(this.state.id); 45 | hashHistory.push('/'); 46 | } 47 | 48 | handleSubmit(e) { 49 | e.preventDefault(); 50 | const newGroup = this.state; 51 | this.props.updateGroup(newGroup) 52 | .then((result) => { 53 | this.props.router.push(`groups/${result.group.id}`); 54 | }); 55 | } 56 | 57 | render() { 58 | if(this.props.group.name) { 59 | return ( 60 | 61 |
    62 |
    63 |

    Update your LetsMeet group details

    64 |
    65 |
    66 |
    67 | 68 |
    69 | 72 | 75 |
    76 | 77 |
    78 | 81 | 84 |
    85 |
    86 | 87 |
    88 | 91 | 93 |
    94 |
    95 | 96 |
    97 | 100 | 103 |
    104 | 105 |
    106 | 110 |
    111 | 116 |
    117 | 122 |
    123 | 124 |
    125 |
    126 |
    127 |
    128 | ); 129 | } 130 | else { 131 | return

    What

    ; 132 | } 133 | } 134 | } 135 | 136 | 137 | 138 | export default withRouter(EditGroupForm); 139 | -------------------------------------------------------------------------------- /frontend/components/session_form/session_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | 4 | class SessionForm extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { username: "", password: "", email: "" }; 8 | this.handleSubmit = this.handleSubmit.bind(this); 9 | this.demoLogin = this.demoLogin.bind(this); 10 | } 11 | 12 | componentDidUpdate() { 13 | this.redirectIfLoggedIn(); 14 | } 15 | 16 | componentDidMount() { 17 | this.props.clearErrors(); 18 | } 19 | 20 | componentWillReceiveProps(newProps) { 21 | if (this.props.formType !== newProps.formType) { 22 | this.props.clearErrors(); 23 | } 24 | } 25 | 26 | componentWillUnmount() { 27 | this.props.clearErrors(); 28 | } 29 | 30 | renderLoginErrors({login}) { 31 | if(login) { 32 | return ( 33 |
    {login}
    34 | ); 35 | } 36 | } 37 | 38 | renderUsernameError({username}) { 39 | if(username) { 40 | return ( 41 |
    Username {username}
    42 | ); 43 | } 44 | } 45 | 46 | renderPasswordError({password}) { 47 | if(password) { 48 | return ( 49 |
    Password {password}
    50 | ); 51 | } 52 | } 53 | 54 | renderEmailError({email}) { 55 | if(email) { 56 | return ( 57 |
    Email address {email}
    58 | ); 59 | } 60 | } 61 | 62 | demoLogin() { 63 | const guest = { email: "guest_user@guest.com", password: "password" }; 64 | this.props.processForm(guest); 65 | } 66 | 67 | redirectIfLoggedIn() { 68 | if(this.props.loggedIn) { 69 | this.props.router.push("/search"); 70 | } 71 | } 72 | 73 | update(field) { 74 | return e => this.setState({ 75 | [field]: e.currentTarget.value 76 | }); 77 | } 78 | 79 | handleSubmit(e) { 80 | e.preventDefault(); 81 | const user = this.state; 82 | this.props.processForm(user); 83 | } 84 | 85 | formIntro() { 86 | let navText; 87 | let formTitle; 88 | if (this.props.formType === "login") { 89 | navText = Not registered with us yet? Sign up; 90 | formTitle =

    Log in

    ; 91 | } else { 92 | navText = Already a member? Log in.; 93 | formTitle =

    Sign up

    ; 94 | } 95 | 96 | return ( 97 |
    98 | {formTitle} 99 | {navText} 100 |
    101 | ); 102 | } 103 | 104 | render() { 105 | let getUsername; 106 | let activateDemoLogin; 107 | if (this.props.formType === "signup") { 108 | getUsername =
    111 |
    112 | 116 |
    117 |
    118 | ; 119 | } 120 | else { 121 | getUsername=""; 122 | } 123 | 124 | if (this.props.formType === "login") { 125 | activateDemoLogin =
    126 | 127 |
    ; 128 | } else { 129 | activateDemoLogin = ""; 130 | } 131 | return ( 132 |
    133 |
    134 | {this.formIntro()} 135 |
    136 |
    137 |
    138 | {getUsername} 139 | {this.renderUsernameError(this.props.errors)} 140 | {this.renderLoginErrors(this.props.errors)} 141 | 142 |
    143 | 147 | {this.renderEmailError(this.props.errors)} 148 | 149 |
    150 | 154 | {this.renderPasswordError(this.props.errors)} 155 |
    156 | 157 |
    158 |
    159 | {activateDemoLogin} 160 |
    161 | ); 162 | } 163 | } 164 | 165 | export default withRouter(SessionForm); 166 | -------------------------------------------------------------------------------- /frontend/components/events/edit_event_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | import { hashHistory } from 'react-router'; 4 | 5 | class EditEventForm extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | id: this.props.event.id, 11 | name: this.props.event.name, 12 | location_name: this.props.event.location_name, 13 | location_address: this.props.event.location_address, 14 | date: this.props.event.date, 15 | time: this.props.event.time, 16 | description: this.props.event.description 17 | }; 18 | this.handleSubmit = this.handleSubmit.bind(this); 19 | this.handleEventDelete = this.handleEventDelete.bind(this); 20 | } 21 | 22 | update(field) { 23 | return e => this.setState({ 24 | [field]: e.target.value 25 | }); 26 | } 27 | 28 | componentDidMount() { 29 | this.props.fetchEvent(this.props.eventId); 30 | } 31 | 32 | componentWillReceiveProps(newProps) { 33 | if (this.props.event !== newProps) { 34 | this.setState({ 35 | id: newProps.event.id, 36 | name: newProps.event.name, 37 | location_name: newProps.event.location_name, 38 | location_address: newProps.event.location_address, 39 | date: newProps.event.date, 40 | time: newProps.event.time, 41 | description: newProps.event.description 42 | }); 43 | } 44 | } 45 | 46 | handleEventDelete(e) { 47 | e.preventDefault(); 48 | this.props.deleteEvent(this.state.id); 49 | hashHistory.push('/'); 50 | } 51 | 52 | handleSubmit(e) { 53 | e.preventDefault(); 54 | const newEvent = this.state; 55 | this.props.updateEvent(newEvent) 56 | .then((result) => { 57 | this.props.router.push(`events/${result.event.id}`); 58 | }); 59 | } 60 | 61 | render() { 62 | return ( 63 | 64 |
    65 |
    66 |

    Want to edit your event?!

    67 |

    No problem. Edit your details below.

    68 |
    69 |
    70 |
    71 | 72 |
    73 | 76 | 79 | 82 |
    83 | 84 |
    85 | 88 | 91 | 94 |
    95 |
    96 | 97 |
    98 | 101 | 103 |
    104 |
    105 | 106 |
    107 | 110 | 113 |
    114 | 115 |
    116 | 120 |
    121 | 126 |
    127 | 132 |
    133 | 134 |
    135 |
    136 |
    137 |
    138 | ); 139 | } 140 | } 141 | 142 | export default withRouter(EditEventForm); 143 | -------------------------------------------------------------------------------- /frontend/components/events/event_show.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | import GroupNavBar from '../groups/group_nav_bar'; 4 | import GroupSideBar from '../groups/group_side_bar'; 5 | import EventSideBar from './event_sidebar'; 6 | 7 | 8 | class EventShow extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.handleCreateRsvp = this.handleCreateRsvp.bind(this); 12 | this.handleDeleteRsvp = this.handleDeleteRsvp.bind(this); 13 | } 14 | 15 | componentDidMount() { 16 | this.props.fetchEvent(this.props.params.eventId); 17 | } 18 | 19 | componentWillReceiveProps(nextProps) { 20 | if (this.props.event && nextProps.params.eventId !== this.props.params.eventId) { 21 | this.props.fetchEvent(nextProps.params.eventId); 22 | } 23 | } 24 | 25 | 26 | handleCreateRsvp() { 27 | this.props.createRsvp(this.props.event.id); 28 | } 29 | 30 | handleDeleteRsvp() { 31 | this.props.deleteRsvp(this.props.event.id); 32 | } 33 | 34 | eventButtons() { 35 | switch(this.props.attendeeType) { 36 | case 'organizer': 37 | return ( 38 | 41 | ); 42 | case 'attendee': 43 | return ( 44 | 50 | ); 51 | case "nonattendee": 52 | return ( 53 | 59 | ); 60 | default: 61 | return ( 62 |
    63 |
    64 |

    Want to go?

    65 |
    66 |
    67 |

    RSVP:

    68 | 69 |
    70 | 71 |
    72 | ); 73 | } 74 | } 75 | 76 | render() { 77 | 78 | if (!this.props.event.id) { 79 | return

    Loading

    ; 80 | } 81 | let attendeeList = []; 82 | 83 | this.props.event.attendees.forEach((attendee) => { 84 | attendeeList.push( 85 | 89 | ); 90 | 91 | }); 92 | 93 | let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; 94 | let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; 95 | let eventName = this.props.event.name; 96 | let date = new Date(this.props.event.date); 97 | let dayOfWeek = days[date.getDay()]; 98 | let month = months[date.getMonth()]; 99 | let day = date.getDate(); 100 | let year = date.getFullYear(); 101 | let dateString = dayOfWeek + ", " + month + " " + day + ", " + year; 102 | 103 | let time = new Date(this.props.event.time).toLocaleTimeString(); 104 | let timeOfDay = time.slice(-3); 105 | let timeIdx = time.length -6; 106 | let timeString = time.substring(0, timeIdx) + timeOfDay; 107 | 108 | let locationName = this.props.event.location_name; 109 | let locationAddress = this.props.event.location_address; 110 | 111 | return( 112 |
    113 |
    114 |

    {eventName}

    115 |
    116 |
    117 |
    118 | 119 |
    120 |
    121 |
      122 |
    • {dateString}
    • 123 |
    • {timeString}
    • 124 |
    125 |
    126 |
    127 |
    128 | 129 |
    130 |
    131 |
      132 |
    • {locationName}
    • 133 |
    • {locationAddress}
    • 134 |
    135 |
    136 |

    {this.props.event.description}

    137 |
    138 |
    139 |
    140 | { this.eventButtons() } 141 |
    142 |
    143 |

    {this.props.event.attendees.length} going

    144 | {attendeeList} 145 |
    146 |
    147 |
    148 | ); 149 | } 150 | } 151 | 152 | export default withRouter(EventShow); 153 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/group_show.scss: -------------------------------------------------------------------------------- 1 | .group-nav-bar { 2 | width: 960px; 3 | margin: 0 auto; 4 | margin-bottom: 20px; 5 | border-radius: 30px; 6 | 7 | } 8 | 9 | .group-nav-name { 10 | color: white; 11 | padding: 20px 0; 12 | margin-top: 20px; 13 | box-sizing: border-box; 14 | font-size: 40px; 15 | font-weight: 500; 16 | text-align: center; 17 | position: relative; 18 | border: 1px solid $off-grey; 19 | border-bottom: none; 20 | background-color: $main-red; 21 | border-radius: 4px 4px 0 0; 22 | } 23 | 24 | #group-img { 25 | z-index: 0; 26 | height: 100px; 27 | } 28 | .group-lower-nav { 29 | display: flex; 30 | flex-direction: row; 31 | justify-content: space-between; 32 | align-items: center; 33 | border: 1px solid $off-grey; 34 | font-size: 20px; 35 | padding: 5px; 36 | background-color: white; 37 | border-radius: 0 0 4px 4px; 38 | } 39 | 40 | .left-group-nav { 41 | display: flex; 42 | flex-direction: row; 43 | justify-content: space-between; 44 | 45 | } 46 | 47 | .left-group-nav > ul { 48 | display: flex; 49 | flex-direction: row; 50 | justify-content: space-between; 51 | } 52 | 53 | .left-group-nav > ul > li { 54 | margin: 0 10px; 55 | padding: 6px 8px; 56 | border-radius: 3px; 57 | font-size: 16px; 58 | cursor: pointer; 59 | } 60 | 61 | 62 | .left-group-nav > ul > li:hover { 63 | background: $main-red; 64 | color: white; 65 | } 66 | 67 | .left-group-nav-selected { 68 | background: $main-red; 69 | color: white; 70 | } 71 | 72 | 73 | .right-group-nav { 74 | } 75 | 76 | .right-group-nav > ul > li { 77 | background-color: $main-red; 78 | margin: 0 10px; 79 | padding: 6px 8px; 80 | font-size: 16px; 81 | cursor: pointer; 82 | color: white; 83 | border-radius: 3px; 84 | } 85 | 86 | .right-group-nav > ul { 87 | display: flex; 88 | flex-direction: row; 89 | justify-content: space-between; 90 | } 91 | 92 | .left-group-nav > ul { 93 | display: flex; 94 | flex-direction: row; 95 | justify-content: space-between; 96 | } 97 | 98 | .right-group-nav { 99 | border-radius: 5px; 100 | } 101 | 102 | 103 | .join-us-button { 104 | background-color: $main-red; 105 | color: white; 106 | padding: 6px 8px; 107 | border-radius: 3px; 108 | font-family: $sans-serif; 109 | font-size: 20px; 110 | text-align: center; 111 | cursor: pointer; 112 | font-weight: 500; 113 | } 114 | 115 | .join-us-button:hover { 116 | transform: scale(1.1); 117 | transition: all .1s ease-in-out; 118 | } 119 | 120 | .group-sidebar-container { 121 | display: flex; 122 | flex-direction: column; 123 | justify-content: space-between; 124 | width: 200px; 125 | font-size: 16px; 126 | padding: 10px; 127 | margin-bottom: 20px; 128 | border: 1px solid $off-grey; 129 | border-radius: 4px; 130 | font-family: 'Yantramanav', sans-serif; 131 | background: white; 132 | } 133 | 134 | #member-count-sidebar:hover { 135 | color: blue; 136 | } 137 | #member-count-sidebar:focus { 138 | color: blue; 139 | } 140 | 141 | .group-sidebar-container > ul { 142 | display: flex; 143 | flex-direction: column; 144 | justify-content: space-between; 145 | } 146 | 147 | .group-sidebar-container > ul > li { 148 | display: flex; 149 | flex-direction: row; 150 | justify-content: space-between; 151 | margin-bottom: 15px; 152 | } 153 | 154 | 155 | 156 | .group-sidebar-container > ul > li > span:last-of-type { 157 | text-align: right; 158 | } 159 | .group-sidebar-container > ul > li:last-of-type { 160 | margin-bottom: 0; 161 | } 162 | .group-show-container { 163 | display: flex; 164 | flex-direction: column; 165 | width: 100%; 166 | margin: 0 auto; 167 | background: $off-white; 168 | } 169 | 170 | 171 | .group-show-content { 172 | display: flex; 173 | flex-direction: row; 174 | margin: 0 auto; 175 | 176 | width: 960px; 177 | background: $off-white; 178 | } 179 | 180 | .group-show-content-right { 181 | display: flex; 182 | flex-direction: column; 183 | width: 780px; 184 | 185 | } 186 | 187 | .group-show-content-right > p, 188 | .group-show-content-right > ul { 189 | font-size: 16px; 190 | margin-left: 10px; 191 | border: 1px solid $off-grey; 192 | border-bottom: none; 193 | 194 | margin-bottom: 20px; 195 | 196 | } 197 | 198 | 199 | 200 | .event-show-content-main > p { 201 | font-size: 16px; 202 | text-align: justify; 203 | } 204 | .event-show-content-main > h1 { 205 | font-size: 28px; 206 | font-weight: bold; 207 | margin-bottom: 10px; 208 | } 209 | 210 | .event-show-content-main > h2 { 211 | font-size: 20px; 212 | } 213 | 214 | .group-show-date-loc { 215 | display: flex; 216 | flex-direction: row; 217 | font-size: 20px; 218 | font-weight: 500; 219 | font-weight: bold; 220 | 221 | } 222 | 223 | 224 | .group-show-date-loc > span{ 225 | margin-right: 10px; 226 | 227 | } 228 | 229 | .group-show-date-loc-info { 230 | display: flex; 231 | flex-direction: column; 232 | margin-bottom: 15px; 233 | font-size: 16px; 234 | } 235 | 236 | .group-show-date-icon { 237 | font-size: 16px; 238 | margin-right: 10px; 239 | } 240 | 241 | .group-show-loc-icon { 242 | margin-right: 15px; 243 | } 244 | .group-show-date-loc-info > li { 245 | margin-left: 10px; 246 | font-size: 16px; 247 | } 248 | 249 | .group-show-date-loc-info > li:last-child{ 250 | font-size: 12px; 251 | } 252 | 253 | .group-show-time-address{ 254 | font-size: 12px; 255 | } 256 | 257 | .group-show-time-address > span{ 258 | margin-left: 25px; 259 | } 260 | -------------------------------------------------------------------------------- /frontend/components/groups/group_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | 4 | class GroupForm extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.handleSubmit = this.handleSubmit.bind(this); 8 | this.navigateToSearch = this.navigateToSearch.bind(this); 9 | this.navigateToGroupShow = this.navigateToGroupShow.bind(this); 10 | this.state = { 11 | name: "", 12 | location: "", 13 | description: "", 14 | category: "", 15 | }; 16 | 17 | } 18 | 19 | navigateToSearch() { 20 | this.props.router.push("/"); 21 | } 22 | componentDidMount() { 23 | this.props.clearErrors(); 24 | } 25 | 26 | componentWillUnmount() { 27 | this.props.clearErrors(); 28 | } 29 | 30 | componentWillReceiveProps(newProps) { 31 | if (this.props.formType !== newProps.formType) { 32 | this.props.clearErrors(); 33 | } 34 | } 35 | 36 | update(field) { 37 | return e => this.setState({ 38 | [field]: e.currentTarget.value 39 | }); 40 | } 41 | navigateToGroupShow() { 42 | this.props.router.push(`groups/${this.state.group.id}`); 43 | } 44 | 45 | handleSubmit(e) { 46 | e.preventDefault(); 47 | const newGroup = this.state; 48 | this.props.createGroup(newGroup) 49 | .then((result) => { 50 | this.props.router.push(`groups/${result.group.id}`); 51 | }); 52 | } 53 | 54 | goToStepTwo(e) { 55 | e.preventDefault(); 56 | document.getElementById('button-one').style = "display: none"; 57 | document.getElementById('step-two').style = "display: flex"; 58 | } 59 | 60 | goToStepThree(e) { 61 | e.preventDefault(); 62 | document.getElementById('button-two').style = "display: none"; 63 | document.getElementById('step-three').style = "display: flex"; 64 | } 65 | 66 | goToStepFour(e) { 67 | e.preventDefault(); 68 | document.getElementById('button-three').style = "display: none"; 69 | document.getElementById('new-group-submit').style="display: block"; 70 | document.getElementById('step-four').style = "display: flex"; 71 | } 72 | 73 | render() { 74 | return ( 75 | 76 |
    77 |
    78 |

    Start a new LetsMeet group

    79 |

    We'll help you find people who are interested.

    80 |
    81 |
    82 |
    83 | 84 |
    85 | 88 | 91 | 93 |
    94 | 95 |
    97 | 100 | 103 |
    104 | 106 |
    107 | 108 |
    110 | 113 | 115 |
    116 | 118 |
    119 | 120 |
    122 | 125 | 128 |
    129 | 130 |
    131 | 136 |
    137 | 142 |
    143 | 144 |
    145 |
    146 |
    147 |
    148 | ); 149 | } 150 | } 151 | 152 | export default withRouter(GroupForm); 153 | --------------------------------------------------------------------------------