├── 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 |
7 |
8 |
9 | Start a LetsMeet group
10 |
11 |
12 |
13 |
17 |
18 |
19 |
20 | {currentUser ? Log out : Log in }
21 |
22 |
23 |
24 |
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 |
29 |
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 |
31 |
32 |
33 | {group.name}
34 |
35 |
36 |
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 |
29 |
30 |
31 | {group.name}
32 |
33 |
34 |
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 |
9 |
10 | Create a LetsMeet group
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 | Log in
21 | Sign up
22 |
23 |
24 |
25 | );
26 |
27 | const loggedInLinks = (currentUser, logout) => (
28 |
29 |
30 |
31 | Create a LetsMeet group
32 |
33 |
34 |
35 |
38 |
39 |
40 |
41 | Log out
42 | Hello, {currentUser.username}
43 |
44 | Search
45 |
46 |
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 |
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 |
47 |
48 | {memberList}
49 |
50 |
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 | RSVP to Event
20 | );
21 | }
22 |
23 | toggleEventEditButton() {
24 | if (this.props.event.organizer_id === this.props.currentUser.id) {
25 | return(
26 | Edit Event
27 | );
28 | } else {
29 | return (
30 | {this.toggleRSVPButton}
31 | );
32 | }
33 | }
34 |
35 | render() {
36 | if(this.props.event) {
37 | return(
38 |
39 |
40 |
41 | {this.toggleEventEditButton()}
42 |
43 |
44 | Founded
45 |
46 |
47 | # of Members
48 |
49 |
50 | Upcoming Meetups
51 | 13
52 |
53 |
54 | Past Meetups
55 | 10
56 |
57 |
58 | Our calendar
59 | icon
60 |
61 |
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 |
33 |
34 |
35 |
36 |
37 | Location
38 | {this.state.group.location}
39 |
40 |
41 | Founded
42 | {this.state.group.formatted_date}
43 |
44 |
45 | Founder
46 | {this.state.group.creator.username}
47 |
48 |
53 |
54 | Upcoming events
55 | {upcomingEventsCount}
56 |
57 |
58 | Our calendar
59 |
60 |
61 |
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 | Leave group
32 | ); } else {
33 | return(
34 | Join us!
35 | );
36 | }
37 | }
38 |
39 | toggleEditButton() {
40 | if (this.props.group.creator_id === this.props.currentUser.id) {
41 | return(
42 | Edit Group
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 | Search
66 |
67 |
68 | Groups
69 | Calendar
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 |
54 |
55 | {event.name}
56 |
57 | {event.location_name}
58 | {event.location_address}
59 | {date}
60 | {time}
61 |
62 |
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 |
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 |
23 | { editGroupLink }
24 | { createEventLink }
25 |
26 | );
27 | case 'member':
28 | return (
29 |
30 | { leaveGroupButton }
31 | { createEventLink }
32 |
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 |
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 = Join ;
59 | let leaveGroupButton = Leave Group ;
60 |
61 | switch(this.props.memberType) {
62 | case 'owner':
63 | return (
64 |
65 | { editGroupLink }
66 | { createEventLink }
67 |
68 | );
69 | case 'member':
70 | return (
71 |
72 | { leaveGroupButton }
73 | { createEventLink }
74 |
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 |
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 =
109 | Your name (this is public):
110 |
111 |
112 |
116 |
117 |
118 | ;
119 | }
120 | else {
121 | getUsername="";
122 | }
123 |
124 | if (this.props.formType === "login") {
125 | activateDemoLogin =
126 | Log in as Guest
127 |
;
128 | } else {
129 | activateDemoLogin = "";
130 | }
131 | return (
132 |
133 |
134 | {this.formIntro()}
135 |
136 |
137 |
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 |
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 |
45 |
46 | Want to go?
47 | RSVP (click to change): YES
48 |
49 |
50 | );
51 | case "nonattendee":
52 | return (
53 |
54 |
55 | Want to go?
56 | RSVP (click to change): NO
57 |
58 |
59 | );
60 | default:
61 | return (
62 |
63 |
64 |
Want to go?
65 |
66 |
67 |
RSVP:
68 | Join us!
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 |
86 |
87 | {attendee.username}
88 |
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 |
143 |
144 |
145 |
146 |
147 |
148 | );
149 | }
150 | }
151 |
152 | export default withRouter(GroupForm);
153 |
--------------------------------------------------------------------------------